diff --git a/.vscode/settings.json b/.vscode/settings.json index 5f80392..3759003 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,5 +27,6 @@ }, "eslint.enable": false, "prettier.enable": false, - "typescript.preferences.organizeImportsCollation": "ordinal" + "typescript.preferences.organizeImportsCollation": "ordinal", + "editor.fontSize": 15 } diff --git a/CHANGELOG.md b/CHANGELOG.md index d29c360..bb64bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,58 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR ## [Unreleased] +## [2.0.0] - 2026-03-09 + +### Alterado + +- Hooks e memoização: removidos `useCallback`/`useMemo` preventivos em páginas de listagem, navegação, formulários de ajustes, calendário e controllers da dashboard; também foi corrigido o uso indevido de `setState` dentro de `useMemo` no relatório por categoria. +- Pagadores: a tela de detalhe agora mantém o card principal do pagador visível durante a navegação entre abas, sem repetir o bloco completo dentro de cada seção. +- Pagadores: detalhes sensíveis como envio automático, último envio e observações agora ficam ocultos quando o acesso ao pagador é somente leitura. +- Pagadores: o e-mail do pagador agora aparece apenas no cabeçalho fixo, evitando repetição dentro do card de detalhes. +- Relatório de tendências: a tabela e os cards mobile agora exibem a média mensal do período filtrado ao lado do total, com destaque visual em azul; a coluna de categoria também ficou mais compacta com truncamento para nomes longos. +- Dashboard: o welcome banner deixou de ser um bloco colorido para virar apenas texto destacado. +- Tema global: removidos do `globals.css` os tokens legados de `welcome-banner`, que não eram mais usados após a simplificação do dashboard. +- UI base: o `Card` compartilhado agora mantém a borda neutra no estado padrão e aplica um gradiente entre `border` e `primary` no hover. +- Assets: imagens que estavam soltas na raiz de `public/` foram movidas para `public/imagens/`, com atualização dos caminhos usados por landing page, logos, exports e manifesto do app. +- Dashboard: `section-cards` foi renomeado para `dashboard-metrics-cards`, deixando mais claro que o componente representa os cards de métricas no topo da dashboard. +- Dashboard: nomes em `lib/dashboard` foram padronizados por responsabilidade, com domínio `bills` em inglês no código, helpers mais explícitos e remoção do `common.ts` genérico. +- Widgets: `widget-card` foi separado entre um card base e uma versão expansível, isolando a lógica de overflow sem alterar o visual atual dos widgets. +- Dashboard: `invoices-widget` foi dividido em componentes locais (`invoices/`) e teve helpers/controle movidos para `lib/dashboard`, deixando o arquivo principal focado na composição visual. +- Dashboard: `boletos-widget` foi renomeado para `bill-widget`, ganhou componentes locais em `bills/` e teve o estado/formatadores extraídos para `lib/dashboard`. +- Dashboard: `payment-status-widget` teve os componentes visuais internos separados em uma pasta local, deixando o arquivo principal só como ponto de composição. +- Dashboard: `notes-widget` teve lista, item e composição dos dialogs separados em `notes/`, com helpers/controller extraídos para `lib/dashboard` para manter responsabilidades mais claras. +- Dashboard: `goals-progress-widget` agora separa lista, item e diálogo em `goals-progress/`, com helpers/controller extraídos para `lib/dashboard` para manter o componente principal focado em composição. +- Dashboard: `payment-overview-widget` passou a separar o shell de abas da regra de estado, isolando a composição dos widgets de condições e formas de pagamento. +- Dashboard: `payment-conditions-widget` e `payment-methods-widget` passaram a usar uma base visual compartilhada para listas de distribuição, reduzindo duplicação e deixando cada widget responsável só por mapear seus dados. +- Dashboard: os componentes internos de comportamento de pagamento foram reunidos em `payment-overview/`, deixando apenas o widget pai na raiz de `components/dashboard`. +- Dashboard: `installment-expenses-widget` teve item/lista/view separados em `installment-expenses/`, com cálculos auxiliares movidos para `lib/dashboard` para deixar o componente principal focado na composição. +- Datas: helpers de `YYYY-MM-DD`, labels de vencimento/pagamento e o relógio de negócio foram centralizados em `lib/utils/date.ts`, com adoção inicial em dashboard, pagadores, calendário, exports e actions de pagamento para reduzir drift de timezone. +- Dashboard: `bill-widget` e `invoices-widget` agora compartilham um hook base de confirmação de pagamento em `lib/dashboard`, mantendo os wrappers específicos só com as regras de cada domínio. +- Dashboard: os widgets de receita e despesa por categoria passaram a compartilhar uma view de breakdown no client e um builder de agregação no server, preservando queries separadas para não misturar regras financeiras específicas. +- Dashboard: filtros repetidos de `lancamentos` passaram a usar helpers compartilhados em `lib/dashboard/lancamento-filters.ts`, reduzindo duplicação nas queries centrais de métricas, categorias, pagamentos, compras e estabelecimentos. +- Lançamentos: a tabela deixou de quebrar ao formatar datas inválidas ou serializadas como ISO completo, normalizando `purchaseDate` para `YYYY-MM-DD` e adicionando fallback seguro no `formatDate`. +- Períodos: `lib/utils/period` passou a concentrar conversões `Date <-> YYYY-MM` e labels reutilizáveis, com adoção inicial em pickers, calendário e filtros de relatórios. +- Períodos: a adoção dos helpers centrais avançou para histórico de categorias, relatórios de cartões, exportações, insights, actions e cálculos de parcelas, reduzindo parse manual de `YYYY-MM` em regras de domínio. +- Dashboard e faturas: labels financeiros de vencimento/pagamento agora compartilham uma base em `lib/utils/financial-dates.ts`, reduzindo duplicação entre `bills` e `invoices`. +- Formatadores: porcentagens passaram a usar um util compartilhado em `lib/utils/percentage.ts`, com adoção inicial em breakdowns, metas, relatórios e cabeçalhos de categoria. +- Formatadores: moeda passou a ter base compartilhada em `lib/utils/currency.ts`, com adoção inicial em componentes compartilhados, notificações e cards de relatórios. +- Formatadores: a adoção do util de moeda avançou em resumo de fatura, extrato de conta, diálogo de orçamento, cabeçalho de pagador e histórico de categorias. +- Datas e labels: `formatDateTime` foi adicionado em `lib/utils/date.ts`, com adoção em pagadores, notificações relacionadas e no modal de calendário para reduzir repetição de `toLocaleString`/`toLocaleDateString`. +- Logos e cartões: resolução de logos e brand assets foi consolidada em `lib/logo/index.ts` e `lib/cartoes/brand-assets.ts`, com adoção principal em cartões, contas, notificações, inbox, relatórios e seletores. +- Notas: helpers transversais saíram de `lib/dashboard` e foram separados entre `lib/notes/formatters.ts` e `lib/dashboard/notes-mappers.ts`, deixando o dashboard responsável apenas pela adaptação dos dados do widget. +- Documentação: o relatório de duplicações de datas e utils agora inclui um checklist executivo com itens feitos, pendentes e o que não vale abstrair agora. + +### Corrigido + +- Hooks e sincronização: o provider de privacidade voltou a reagir corretamente às mudanças do modo privado, e o resumo de fatura agora reseta a data de pagamento quando a prop inicial deixa de existir. +- Compatibilidade da refatoração de hooks e relatórios: `useMobile`/`useIsMobile` voltaram a ter exports compatíveis, o shim de `components/ui/use-mobile.ts` foi restaurado para o sidebar e `lib/relatorios/types.ts` voltou a reexportar os tipos usados pelos fetchers legados. +- Widgets expansíveis: o shell compartilhado voltou a aplicar `relative` e `overflow-hidden`, mantendo o gradiente e o botão "Ver tudo" presos ao card. +- Dashboard: o widget "Lançamentos por categoria" deixou de ler a categoria salva no `sessionStorage` durante a renderização inicial, evitando mismatch de hidratação entre servidor e cliente. + +### Removido + +- Dashboard/Ajustes: toda a implementação legada de `magnet-lines` foi removida, incluindo componente órfão, preferência de usuário e a coluna `disable_magnetlines` do schema com migration dedicada. + ## [1.7.7] - 2026-03-05 ### Alterado diff --git a/components/ajustes/api-tokens-form.tsx b/components/ajustes/api-tokens-form.tsx index 9edf151..60d551d 100644 --- a/components/ajustes/api-tokens-form.tsx +++ b/components/ajustes/api-tokens-form.tsx @@ -38,6 +38,7 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { formatDateTime } from "@/lib/utils/date"; interface ApiToken { id: string; @@ -290,7 +291,11 @@ export function ApiTokensForm({ tokens }: ApiTokensFormProps) { )} {" · "} Criado em{" "} - {new Date(token.createdAt).toLocaleDateString("pt-BR")} + {formatDateTime(token.createdAt, { + day: "2-digit", + month: "2-digit", + year: "numeric", + }) ?? "—"}

diff --git a/components/ajustes/update-email-form.tsx b/components/ajustes/update-email-form.tsx index e899de4..de96a9e 100644 --- a/components/ajustes/update-email-form.tsx +++ b/components/ajustes/update-email-form.tsx @@ -6,7 +6,7 @@ import { RiEyeLine, RiEyeOffLine, } from "@remixicon/react"; -import { useMemo, useState, useTransition } from "react"; +import { useState, useTransition } from "react"; import { toast } from "sonner"; import { updateEmailAction } from "@/app/(dashboard)/ajustes/actions"; import { Button } from "@/components/ui/button"; @@ -32,16 +32,14 @@ export function UpdateEmailForm({ const isGoogleAuth = authProvider === "google"; // Validação em tempo real: e-mails coincidem - const emailsMatch = useMemo(() => { - if (!confirmEmail) return null; // Não mostrar erro se campo vazio - return newEmail.toLowerCase() === confirmEmail.toLowerCase(); - }, [newEmail, confirmEmail]); + const emailsMatch = !confirmEmail + ? null + : newEmail.toLowerCase() === confirmEmail.toLowerCase(); // Validação: novo e-mail é diferente do atual - const isEmailDifferent = useMemo(() => { - if (!newEmail) return true; - return newEmail.toLowerCase() !== currentEmail.toLowerCase(); - }, [newEmail, currentEmail]); + const isEmailDifferent = !newEmail + ? true + : newEmail.toLowerCase() !== currentEmail.toLowerCase(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); diff --git a/components/ajustes/update-password-form.tsx b/components/ajustes/update-password-form.tsx index 840ae41..514a6b0 100644 --- a/components/ajustes/update-password-form.tsx +++ b/components/ajustes/update-password-form.tsx @@ -7,7 +7,7 @@ import { RiEyeLine, RiEyeOffLine, } from "@remixicon/react"; -import { useMemo, useState, useTransition } from "react"; +import { useState, useTransition } from "react"; import { toast } from "sonner"; import { updatePasswordAction } from "@/app/(dashboard)/ajustes/actions"; import { Button } from "@/components/ui/button"; @@ -85,16 +85,12 @@ export function UpdatePasswordForm({ authProvider }: UpdatePasswordFormProps) { const isGoogleAuth = authProvider === "google"; // Validação em tempo real: senhas coincidem - const passwordsMatch = useMemo(() => { - if (!confirmPassword) return null; // Não mostrar erro se campo vazio - return newPassword === confirmPassword; - }, [newPassword, confirmPassword]); + const passwordsMatch = !confirmPassword + ? null + : newPassword === confirmPassword; // Validação de requisitos da senha - const passwordValidation = useMemo( - () => validatePassword(newPassword), - [newPassword], - ); + const passwordValidation = validatePassword(newPassword); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); diff --git a/components/animated-theme-toggler.tsx b/components/animated-theme-toggler.tsx deleted file mode 100644 index f97cac8..0000000 --- a/components/animated-theme-toggler.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client"; -import { RiMoonClearLine, RiSunLine } from "@remixicon/react"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { flushSync } from "react-dom"; -import { buttonVariants } from "@/components/ui/button"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils/ui"; - -interface AnimatedThemeTogglerProps - extends React.ComponentPropsWithoutRef<"button"> { - duration?: number; -} - -export const AnimatedThemeToggler = ({ - className, - duration = 400, - ...props -}: AnimatedThemeTogglerProps) => { - const [isDark, setIsDark] = useState(false); - const buttonRef = useRef(null); - - useEffect(() => { - const updateTheme = () => { - setIsDark(document.documentElement.classList.contains("dark")); - }; - - updateTheme(); - - const observer = new MutationObserver(updateTheme); - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ["class"], - }); - - return () => observer.disconnect(); - }, []); - - const toggleTheme = useCallback(async () => { - if (!buttonRef.current) return; - - await document.startViewTransition(() => { - flushSync(() => { - const newTheme = !isDark; - setIsDark(newTheme); - document.documentElement.classList.toggle("dark"); - localStorage.setItem("theme", newTheme ? "dark" : "light"); - }); - }).ready; - - const { top, left, width, height } = - buttonRef.current.getBoundingClientRect(); - const x = left + width / 2; - const y = top + height / 2; - const maxRadius = Math.hypot( - Math.max(left, window.innerWidth - left), - Math.max(top, window.innerHeight - top), - ); - - document.documentElement.animate( - { - clipPath: [ - `circle(0px at ${x}px ${y}px)`, - `circle(${maxRadius}px at ${x}px ${y}px)`, - ], - }, - { - duration, - easing: "ease-in-out", - pseudoElement: "::view-transition-new(root)", - }, - ); - }, [isDark, duration]); - - return ( - - - - - - {isDark ? "Tema claro" : "Tema escuro"} - - - ); -}; diff --git a/components/calculadora/use-calculator-keyboard.ts b/components/calculadora/use-calculator-keyboard.ts deleted file mode 100644 index 8e1a711..0000000 --- a/components/calculadora/use-calculator-keyboard.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { useEffect } from "react"; -import type { Operator } from "@/lib/utils/calculator"; - -type UseCalculatorKeyboardParams = { - isOpen: boolean; - canCopy: boolean; - onCopy: () => void | Promise; - onPaste: () => void | Promise; - inputDigit: (digit: string) => void; - inputDecimal: () => void; - setNextOperator: (op: Operator) => void; - evaluate: () => void; - deleteLastDigit: () => void; - reset: () => void; - applyPercent: () => void; -}; - -function shouldIgnoreForEditableTarget(target: EventTarget | null): boolean { - if (!target || !(target instanceof HTMLElement)) { - return false; - } - - const tagName = target.tagName; - return ( - tagName === "INPUT" || tagName === "TEXTAREA" || target.isContentEditable - ); -} - -const KEY_TO_OPERATOR: Record = { - "+": "add", - "-": "subtract", - "*": "multiply", - "/": "divide", -}; - -export function useCalculatorKeyboard({ - isOpen, - canCopy, - onCopy, - onPaste, - inputDigit, - inputDecimal, - setNextOperator, - evaluate, - deleteLastDigit, - reset, - applyPercent, -}: UseCalculatorKeyboardParams) { - useEffect(() => { - if (!isOpen) return; - - const handleKeyDown = (event: KeyboardEvent) => { - const { key, ctrlKey, metaKey } = event; - - // Ctrl/Cmd shortcuts - if (ctrlKey || metaKey) { - if (shouldIgnoreForEditableTarget(event.target)) return; - - const lowerKey = key.toLowerCase(); - if (lowerKey === "c" && canCopy) { - const selection = window.getSelection(); - if (selection && selection.toString().trim().length > 0) return; - event.preventDefault(); - void onCopy(); - } else if (lowerKey === "v") { - const selection = window.getSelection(); - if (selection && selection.toString().trim().length > 0) return; - if (!navigator.clipboard?.readText) return; - event.preventDefault(); - void onPaste(); - } - return; - } - - // Digits - if (key >= "0" && key <= "9") { - event.preventDefault(); - inputDigit(key); - return; - } - - // Decimal - if (key === "." || key === ",") { - event.preventDefault(); - inputDecimal(); - return; - } - - // Operators - const op = KEY_TO_OPERATOR[key]; - if (op) { - event.preventDefault(); - setNextOperator(op); - return; - } - - // Evaluate - if (key === "Enter" || key === "=") { - event.preventDefault(); - evaluate(); - return; - } - - // Backspace - if (key === "Backspace") { - event.preventDefault(); - deleteLastDigit(); - return; - } - - // Escape resets calculator (dialog close is handled by onEscapeKeyDown) - if (key === "Escape") { - event.preventDefault(); - reset(); - return; - } - - // Percent - if (key === "%") { - event.preventDefault(); - applyPercent(); - return; - } - }; - - document.addEventListener("keydown", handleKeyDown); - return () => { - document.removeEventListener("keydown", handleKeyDown); - }; - }, [ - isOpen, - canCopy, - onCopy, - onPaste, - inputDigit, - inputDecimal, - setNextOperator, - evaluate, - deleteLastDigit, - reset, - applyPercent, - ]); -} diff --git a/components/calculadora/use-calculator-state.ts b/components/calculadora/use-calculator-state.ts deleted file mode 100644 index c98a9d2..0000000 --- a/components/calculadora/use-calculator-state.ts +++ /dev/null @@ -1,379 +0,0 @@ -import type { VariantProps } from "class-variance-authority"; -import { useEffect, useRef, useState } from "react"; -import type { buttonVariants } from "@/components/ui/button"; -import { - formatLocaleValue, - formatNumber, - normalizeClipboardNumber, - OPERATOR_SYMBOLS, - type Operator, - performOperation, -} from "@/lib/utils/calculator"; - -export type CalculatorButtonConfig = { - label: string; - onClick: () => void; - variant?: VariantProps["variant"]; - colSpan?: number; - className?: string; -}; - -export function useCalculatorState() { - const [display, setDisplay] = useState("0"); - const [accumulator, setAccumulator] = useState(null); - const [operator, setOperator] = useState(null); - const [overwrite, setOverwrite] = useState(false); - const [history, setHistory] = useState(null); - const [copied, setCopied] = useState(false); - const resetCopiedTimeoutRef = useRef(undefined); - - const currentValue = Number(display); - - const resultText = (() => { - if (display === "Erro") { - return null; - } - - const normalized = formatNumber(currentValue); - if (normalized === "Erro") { - return null; - } - - return formatLocaleValue(normalized); - })(); - - const reset = () => { - setDisplay("0"); - setAccumulator(null); - setOperator(null); - setOverwrite(false); - setHistory(null); - }; - - const inputDigit = (digit: string) => { - // Check conditions before state updates - const shouldReset = overwrite || display === "Erro"; - - setDisplay((prev) => { - if (shouldReset) { - return digit; - } - - if (prev === "0") { - return digit; - } - - // Limitar a 10 dígitos (excluindo sinal negativo e ponto decimal) - const digitCount = prev.replace(/[-.]/g, "").length; - if (digitCount >= 10) { - return prev; - } - - return `${prev}${digit}`; - }); - - // Update related states after display update - if (shouldReset) { - setOverwrite(false); - setHistory(null); - } - }; - - const inputDecimal = () => { - // Check conditions before state updates - const shouldReset = overwrite || display === "Erro"; - - setDisplay((prev) => { - if (shouldReset) { - return "0."; - } - - if (prev.includes(".")) { - return prev; - } - - // Limitar a 10 dígitos antes de adicionar o ponto decimal - const digitCount = prev.replace(/[-]/g, "").length; - if (digitCount >= 10) { - return prev; - } - - return `${prev}.`; - }); - - // Update related states after display update - if (shouldReset) { - setOverwrite(false); - setHistory(null); - } - }; - - const setNextOperator = (nextOperator: Operator) => { - if (display === "Erro") { - reset(); - return; - } - - const value = currentValue; - - if (accumulator === null || operator === null || overwrite) { - setAccumulator(value); - } else { - const result = performOperation(accumulator, value, operator); - const formatted = formatNumber(result); - setAccumulator(Number.isFinite(result) ? result : null); - setDisplay(formatted); - if (!Number.isFinite(result)) { - setOperator(null); - setOverwrite(true); - setHistory(null); - return; - } - } - - setOperator(nextOperator); - setOverwrite(true); - setHistory(null); - }; - - const evaluate = () => { - if (operator === null || accumulator === null || display === "Erro") { - return; - } - - const value = currentValue; - const left = formatNumber(accumulator); - const right = formatNumber(value); - const symbol = OPERATOR_SYMBOLS[operator]; - const operation = `${formatLocaleValue(left)} ${symbol} ${formatLocaleValue( - right, - )}`; - const result = performOperation(accumulator, value, operator); - const formatted = formatNumber(result); - - setDisplay(formatted); - setAccumulator(Number.isFinite(result) ? result : null); - setOperator(null); - setOverwrite(true); - setHistory(operation); - }; - - const toggleSign = () => { - setDisplay((prev) => { - if (prev === "Erro") { - return prev; - } - if (prev.startsWith("-")) { - return prev.slice(1); - } - return prev === "0" ? prev : `-${prev}`; - }); - if (overwrite) { - setOverwrite(false); - setHistory(null); - } - }; - - const deleteLastDigit = () => { - setHistory(null); - - // Check conditions before state updates - const isError = display === "Erro"; - - setDisplay((prev) => { - if (prev === "Erro") { - return "0"; - } - - if (overwrite) { - return "0"; - } - - if (prev.length <= 1 || (prev.length === 2 && prev.startsWith("-"))) { - return "0"; - } - - return prev.slice(0, -1); - }); - - // Update related states after display update - if (isError) { - setAccumulator(null); - setOperator(null); - setOverwrite(false); - } else if (overwrite) { - setOverwrite(false); - } - }; - - const applyPercent = () => { - setDisplay((prev) => { - if (prev === "Erro") { - return prev; - } - const value = Number(prev); - return formatNumber(value / 100); - }); - setOverwrite(true); - setHistory(null); - }; - - const expression = (() => { - if (display === "Erro") { - return "Erro"; - } - - if (operator && accumulator !== null) { - const symbol = OPERATOR_SYMBOLS[operator]; - const left = formatLocaleValue(formatNumber(accumulator)); - - if (overwrite) { - return `${left} ${symbol}`; - } - - return `${left} ${symbol} ${formatLocaleValue(display)}`; - } - - return formatLocaleValue(display); - })(); - - const makeOperatorHandler = (nextOperator: Operator) => () => - setNextOperator(nextOperator); - - const buttons: CalculatorButtonConfig[][] = [ - [ - { label: "C", onClick: reset, variant: "destructive" }, - { label: "⌫", onClick: deleteLastDigit, variant: "secondary" }, - { label: "%", onClick: applyPercent, variant: "secondary" }, - { - label: "÷", - onClick: makeOperatorHandler("divide"), - variant: "outline", - }, - ], - [ - { label: "7", onClick: () => inputDigit("7") }, - { label: "8", onClick: () => inputDigit("8") }, - { label: "9", onClick: () => inputDigit("9") }, - { - label: "×", - onClick: makeOperatorHandler("multiply"), - variant: "outline", - }, - ], - [ - { label: "4", onClick: () => inputDigit("4") }, - { label: "5", onClick: () => inputDigit("5") }, - { label: "6", onClick: () => inputDigit("6") }, - { - label: "-", - onClick: makeOperatorHandler("subtract"), - variant: "outline", - }, - ], - [ - { label: "1", onClick: () => inputDigit("1") }, - { label: "2", onClick: () => inputDigit("2") }, - { label: "3", onClick: () => inputDigit("3") }, - { label: "+", onClick: makeOperatorHandler("add"), variant: "outline" }, - ], - [ - { label: "±", onClick: toggleSign, variant: "secondary" }, - { label: "0", onClick: () => inputDigit("0") }, - { label: ",", onClick: inputDecimal }, - { label: "=", onClick: evaluate, variant: "default" }, - ], - ]; - - const copyToClipboard = async () => { - if (!resultText) return; - - try { - await navigator.clipboard.writeText(resultText); - - setCopied(true); - if (resetCopiedTimeoutRef.current !== undefined) { - window.clearTimeout(resetCopiedTimeoutRef.current); - } - resetCopiedTimeoutRef.current = window.setTimeout(() => { - setCopied(false); - }, 2000); - } catch (error) { - console.error( - "Não foi possível copiar o resultado da calculadora.", - error, - ); - } - }; - - const pasteFromClipboard = async () => { - if (!navigator.clipboard?.readText) return; - - try { - const rawValue = await navigator.clipboard.readText(); - const normalized = normalizeClipboardNumber(rawValue); - - if (resetCopiedTimeoutRef.current !== undefined) { - window.clearTimeout(resetCopiedTimeoutRef.current); - resetCopiedTimeoutRef.current = undefined; - } - - setCopied(false); - - if (!normalized) { - setDisplay("Erro"); - setAccumulator(null); - setOperator(null); - setOverwrite(true); - setHistory(null); - return; - } - - // Limitar a 10 dígitos - const digitCount = normalized.replace(/[-.]/g, "").length; - if (digitCount > 10) { - setDisplay("Erro"); - setAccumulator(null); - setOperator(null); - setOverwrite(true); - setHistory(null); - return; - } - - setDisplay(normalized); - setAccumulator(null); - setOperator(null); - setOverwrite(false); - setHistory(null); - } catch (error) { - console.error("Não foi possível colar o valor na calculadora.", error); - } - }; - - useEffect(() => { - return () => { - if (resetCopiedTimeoutRef.current !== undefined) { - window.clearTimeout(resetCopiedTimeoutRef.current); - } - }; - }, []); - - return { - display, - operator, - expression, - history, - resultText, - copied, - buttons, - inputDigit, - inputDecimal, - setNextOperator, - evaluate, - deleteLastDigit, - reset, - applyPercent, - copyToClipboard, - pasteFromClipboard, - }; -} diff --git a/components/calculadora/use-draggable-dialog.ts b/components/calculadora/use-draggable-dialog.ts deleted file mode 100644 index b7b5f67..0000000 --- a/components/calculadora/use-draggable-dialog.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { useCallback, useRef } from "react"; - -type Position = { x: number; y: number }; - -const MIN_VISIBLE_PX = 20; - -function clampPosition( - x: number, - y: number, - elementWidth: number, - elementHeight: number, -): Position { - // Dialog starts centered (left/top 50% + translate(-50%, -50%)). - // Clamp offsets so at least MIN_VISIBLE_PX remains visible on each axis. - const halfViewportWidth = window.innerWidth / 2; - const halfViewportHeight = window.innerHeight / 2; - const halfElementWidth = elementWidth / 2; - const halfElementHeight = elementHeight / 2; - - const minX = MIN_VISIBLE_PX - (halfViewportWidth + halfElementWidth); - const maxX = halfViewportWidth + halfElementWidth - MIN_VISIBLE_PX; - const minY = MIN_VISIBLE_PX - (halfViewportHeight + halfElementHeight); - const maxY = halfViewportHeight + halfElementHeight - MIN_VISIBLE_PX; - - return { - x: Math.min(Math.max(x, minX), maxX), - y: Math.min(Math.max(y, minY), maxY), - }; -} - -function applyPosition(el: HTMLElement, x: number, y: number) { - if (x === 0 && y === 0) { - el.style.translate = ""; - el.style.transform = ""; - } else { - // Keep the dialog's centered baseline (-50%, -50%) and only add drag offset. - el.style.translate = `calc(-50% + ${x}px) calc(-50% + ${y}px)`; - el.style.transform = ""; - } -} - -export function useDraggableDialog() { - const offset = useRef({ x: 0, y: 0 }); - const dragStart = useRef(null); - const initialOffset = useRef({ x: 0, y: 0 }); - const contentRef = useRef(null); - - const onPointerDown = useCallback((e: React.PointerEvent) => { - if (e.button !== 0) return; - - dragStart.current = { x: e.clientX, y: e.clientY }; - initialOffset.current = { x: offset.current.x, y: offset.current.y }; - (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); - }, []); - - const onPointerMove = useCallback((e: React.PointerEvent) => { - if (!dragStart.current || !contentRef.current) return; - - const dx = e.clientX - dragStart.current.x; - const dy = e.clientY - dragStart.current.y; - - const rawX = initialOffset.current.x + dx; - const rawY = initialOffset.current.y + dy; - - const el = contentRef.current; - const clamped = clampPosition(rawX, rawY, el.offsetWidth, el.offsetHeight); - - offset.current = clamped; - applyPosition(el, clamped.x, clamped.y); - }, []); - - const onPointerUp = useCallback((e: React.PointerEvent) => { - dragStart.current = null; - if ((e.currentTarget as HTMLElement).hasPointerCapture(e.pointerId)) { - (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId); - } - }, []); - - const onPointerCancel = useCallback(() => { - dragStart.current = null; - }, []); - - const onLostPointerCapture = useCallback(() => { - dragStart.current = null; - }, []); - - const resetPosition = useCallback(() => { - offset.current = { x: 0, y: 0 }; - if (contentRef.current) { - applyPosition(contentRef.current, 0, 0); - } - }, []); - - const dragHandleProps = { - onPointerDown, - onPointerMove, - onPointerUp, - onPointerCancel, - onLostPointerCapture, - style: { touchAction: "none" as const, cursor: "grab" }, - }; - - const contentRefCallback = useCallback((node: HTMLElement | null) => { - contentRef.current = node; - }, []); - - return { - dragHandleProps, - contentRefCallback, - resetPosition, - }; -} diff --git a/components/confirm-action-dialog.tsx b/components/confirm-action-dialog.tsx deleted file mode 100644 index 7558f7d..0000000 --- a/components/confirm-action-dialog.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import type { VariantProps } from "class-variance-authority"; -import { useState, useTransition } from "react"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { buttonVariants } from "@/components/ui/button"; - -interface ConfirmActionDialogProps { - trigger?: React.ReactNode; - title: string; - description?: string; - confirmLabel?: string; - cancelLabel?: string; - pendingLabel?: string; - confirmVariant?: VariantProps["variant"]; - open?: boolean; - onOpenChange?: (open: boolean) => void; - onConfirm?: () => Promise | void; - disabled?: boolean; - className?: string; -} - -export function ConfirmActionDialog({ - trigger, - title, - description, - confirmLabel = "Confirmar", - cancelLabel = "Cancelar", - pendingLabel, - confirmVariant = "default", - open, - onOpenChange, - onConfirm, - disabled = false, - className, -}: ConfirmActionDialogProps) { - const [internalOpen, setInternalOpen] = useState(false); - const [isPending, startTransition] = useTransition(); - const dialogOpen = open ?? internalOpen; - - const setDialogOpen = (value: boolean) => { - if (open === undefined) { - setInternalOpen(value); - } - onOpenChange?.(value); - }; - - const resolvedPendingLabel = pendingLabel ?? confirmLabel; - - const handleConfirm = () => { - if (!onConfirm) { - setDialogOpen(false); - return; - } - - startTransition(async () => { - try { - await onConfirm(); - setDialogOpen(false); - } catch { - // Mantém o diálogo aberto para que o chamador trate o erro. - } - }); - }; - - return ( - - {trigger ? ( - {trigger} - ) : null} - - - {title} - {description ? ( - {description} - ) : null} - - - - {cancelLabel} - - - {isPending ? resolvedPendingLabel : confirmLabel} - - - - - ); -} diff --git a/components/dot-icon.tsx b/components/dot-icon.tsx deleted file mode 100644 index 193ea4b..0000000 --- a/components/dot-icon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -type DotIconProps = { - color: string; -}; - -export default function DotIcon({ color }: DotIconProps) { - return ( - - - - ); -} diff --git a/components/font-provider.tsx b/components/font-provider.tsx deleted file mode 100644 index ac57ae7..0000000 --- a/components/font-provider.tsx +++ /dev/null @@ -1,80 +0,0 @@ -"use client"; - -import { - createContext, - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from "react"; -import { getFontVariable } from "@/public/fonts/font_index"; - -type FontContextValue = { - systemFont: string; - moneyFont: string; - setSystemFont: (key: string) => void; - setMoneyFont: (key: string) => void; -}; - -const FontContext = createContext(null); - -export function FontProvider({ - systemFont: initialSystemFont, - moneyFont: initialMoneyFont, - children, -}: { - systemFont: string; - moneyFont: string; - children: React.ReactNode; -}) { - const [systemFont, setSystemFontState] = useState(initialSystemFont); - const [moneyFont, setMoneyFontState] = useState(initialMoneyFont); - - const applyFontVars = useCallback((sys: string, money: string) => { - document.documentElement.style.setProperty( - "--font-app", - getFontVariable(sys), - ); - document.documentElement.style.setProperty( - "--font-money", - getFontVariable(money), - ); - }, []); - - useEffect(() => { - applyFontVars(systemFont, moneyFont); - }, [systemFont, moneyFont, applyFontVars]); - - const setSystemFont = useCallback((key: string) => { - setSystemFontState(key); - }, []); - - const setMoneyFont = useCallback((key: string) => { - setMoneyFontState(key); - }, []); - - const value = useMemo( - () => ({ systemFont, moneyFont, setSystemFont, setMoneyFont }), - [systemFont, moneyFont, setSystemFont, setMoneyFont], - ); - - return ( - <> -