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 ? (
-
- ) : (
-
- )}
-
- {isDark ? "Ativar tema claro" : "Ativar tema escuro"}
-
-
-
-
- {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 (
- <>
-
- {children}
- >
- );
-}
-
-export function useFont() {
- const ctx = useContext(FontContext);
- if (!ctx) {
- throw new Error("useFont must be used within FontProvider");
- }
- return ctx;
-}
diff --git a/components/logo-picker.tsx b/components/logo-picker.tsx
deleted file mode 100644
index 786fbe0..0000000
--- a/components/logo-picker.tsx
+++ /dev/null
@@ -1,194 +0,0 @@
-"use client";
-
-import Image from "next/image";
-import { useState } from "react";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { Input } from "@/components/ui/input";
-import { deriveNameFromLogo } from "@/lib/logo";
-import { cn } from "@/lib/utils/ui";
-
-const DEFAULT_BASE_PATH = "/logos";
-
-const resolveLogoSrc = (logo: string, basePath: string) => {
- if (/^https?:\/\//.test(logo)) {
- return logo;
- }
- return `${basePath.replace(/\/$/, "")}/${logo.replace(/^\//, "")}`;
-};
-
-interface LogoPickerTriggerProps {
- selectedLogo?: string | null;
- disabled?: boolean;
- helperText?: string;
- placeholder?: string;
- basePath?: string;
- onOpen: () => void;
- className?: string;
-}
-
-export function LogoPickerTrigger({
- selectedLogo,
- disabled,
- helperText = "Clique para trocar o logo",
- placeholder = "Selecionar logo",
- basePath = DEFAULT_BASE_PATH,
- onOpen,
- className,
-}: LogoPickerTriggerProps) {
- const hasLogo = Boolean(selectedLogo);
- const selectedLogoLabel = deriveNameFromLogo(selectedLogo);
- const selectedLogoPath =
- hasLogo && selectedLogo ? resolveLogoSrc(selectedLogo, basePath) : null;
-
- return (
-
-
- {selectedLogoPath ? (
-
- ) : (
- Logo
- )}
-
-
-
-
- {selectedLogoLabel || placeholder}
-
-
- {disabled ? "Nenhum logo disponível" : helperText}
-
-
-
- );
-}
-
-interface LogoPickerDialogProps {
- open: boolean;
- logos: string[];
- value: string;
- onOpenChange: (open: boolean) => void;
- onSelect: (logo: string) => void;
- basePath?: string;
- title?: string;
- description?: string;
- emptyState?: React.ReactNode;
-}
-
-export function LogoPickerDialog({
- open,
- logos,
- value,
- onOpenChange,
- onSelect,
- basePath = DEFAULT_BASE_PATH,
- title = "Escolher logo",
- description = "Selecione o logo que será usado para identificar este item.",
- emptyState,
-}: LogoPickerDialogProps) {
- const [search, setSearch] = useState("");
-
- const filteredLogos = logos.filter((logo) => {
- if (!search.trim()) return true;
- const logoLabel = deriveNameFromLogo(logo).toLowerCase();
- return logoLabel.includes(search.toLowerCase().trim());
- });
-
- const handleOpenChange = (isOpen: boolean) => {
- if (!isOpen) setSearch("");
- onOpenChange(isOpen);
- };
-
- return (
-
-
-
- {title}
- {description ? (
- {description}
- ) : null}
-
-
- {logos.length > 0 && (
- setSearch(e.target.value)}
- className="h-8 text-sm"
- />
- )}
-
- {logos.length === 0 ? (
- (emptyState ?? (
-
- Nenhum logo encontrado. Adicione arquivos na pasta de logos.
-
- ))
- ) : filteredLogos.length === 0 ? (
-
- Nenhum logo encontrado para “{search}”
-
- ) : (
-
- {filteredLogos.map((logo) => {
- const isActive = value === logo;
- const logoLabel = deriveNameFromLogo(logo);
-
- return (
- {
- e.stopPropagation();
- e.preventDefault();
- onSelect(logo);
- }}
- onPointerDown={(e) => e.stopPropagation()}
- onTouchStart={(e) => e.stopPropagation()}
- className={cn(
- "flex flex-col items-center gap-1 rounded-md bg-card p-2 text-center text-xs transition-all hover:border-primary hover:bg-primary/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
- isActive &&
- "border-primary bg-primary/5 ring-2 ring-primary/40",
- )}
- >
-
-
-
-
- {logoLabel}
-
-
- );
- })}
-
- )}
-
-
- );
-}
diff --git a/components/logo.tsx b/components/logo.tsx
deleted file mode 100644
index 2469250..0000000
--- a/components/logo.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import Image from "next/image";
-import { cn } from "@/lib/utils/ui";
-import { version } from "@/package.json";
-
-interface LogoProps {
- variant?: "full" | "small" | "compact";
- className?: string;
- showVersion?: boolean;
-}
-
-export function Logo({
- variant = "full",
- className,
- showVersion = false,
-}: LogoProps) {
- if (variant === "compact") {
- return (
-
-
-
-
- );
- }
-
- if (variant === "small") {
- return (
-
- );
- }
-
- return (
-
-
-
- {showVersion && (
-
- {version}
-
- )}
-
- );
-}
diff --git a/components/money-values.tsx b/components/money-values.tsx
deleted file mode 100644
index 2a2b7e1..0000000
--- a/components/money-values.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-"use client";
-
-import { cn } from "@/lib/utils/ui";
-import { usePrivacyMode } from "./privacy-provider";
-
-type Props = {
- amount: number;
- className?: string;
- showPositiveSign?: boolean;
-};
-
-function MoneyValues({ amount, className, showPositiveSign = false }: Props) {
- const { privacyMode } = usePrivacyMode();
-
- const formattedValue = amount.toLocaleString("pt-BR", {
- style: "currency",
- currency: "BRL",
- maximumFractionDigits: 2,
- });
-
- const displayValue =
- showPositiveSign && amount > 0 ? `+${formattedValue}` : formattedValue;
-
- return (
-
- {displayValue}
-
- );
-}
-
-export default MoneyValues;
diff --git a/components/period-picker.tsx b/components/period-picker.tsx
deleted file mode 100644
index 522b8fb..0000000
--- a/components/period-picker.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-"use client";
-
-import { RiCalendarLine } from "@remixicon/react";
-import { format } from "date-fns";
-import { ptBR } from "date-fns/locale";
-import { useState } from "react";
-import { Button } from "@/components/ui/button";
-import { MonthPicker } from "@/components/ui/month-picker";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { cn } from "@/lib/utils/ui";
-
-interface PeriodPickerProps {
- value: string; // "YYYY-MM" format
- onChange: (value: string) => void;
- disabled?: boolean;
- className?: string;
- placeholder?: string;
- variant?: "default" | "outline" | "ghost";
- size?: "default" | "sm" | "lg";
-}
-
-export function PeriodPicker({
- value,
- onChange,
- disabled = false,
- className,
- placeholder = "Selecione o período",
- variant = "outline",
- size = "default",
-}: PeriodPickerProps) {
- const [open, setOpen] = useState(false);
-
- // Convert period string (YYYY-MM) to Date object
- const periodToDate = (period: string): Date => {
- const [year, month] = period.split("-").map(Number);
- return new Date(year, month - 1, 1);
- };
-
- // Convert Date object to period string (YYYY-MM)
- const dateToPeriod = (date: Date): string => {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, "0");
- return `${year}-${month}`;
- };
-
- // Format date for display
- const formatDisplay = (period: string): string => {
- try {
- const date = periodToDate(period);
- return format(date, "MMMM yyyy", { locale: ptBR });
- } catch {
- return placeholder;
- }
- };
-
- const handleSelect = (date: Date) => {
- const period = dateToPeriod(date);
- onChange(period);
- setOpen(false);
- };
-
- return (
-
-
-
-
- {value ? formatDisplay(value) : placeholder}
-
-
-
-
-
-
- );
-}
diff --git a/components/privacy-provider.tsx b/components/privacy-provider.tsx
deleted file mode 100644
index 4e671a1..0000000
--- a/components/privacy-provider.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-"use client";
-
-import type React from "react";
-import {
- createContext,
- useContext,
- useEffect,
- useRef,
- useState,
- useSyncExternalStore,
-} from "react";
-
-interface PrivacyContextType {
- privacyMode: boolean;
- toggle: () => void;
- set: (value: boolean) => void;
-}
-
-const PrivacyContext = createContext(undefined);
-
-const STORAGE_KEY = "app:privacyMode";
-
-// Read from localStorage safely (returns false on server)
-function getStoredValue(): boolean {
- if (typeof window === "undefined") return false;
- return localStorage.getItem(STORAGE_KEY) === "true";
-}
-
-// Subscribe to storage changes
-function subscribeToStorage(callback: () => void) {
- window.addEventListener("storage", callback);
- return () => window.removeEventListener("storage", callback);
-}
-
-export function PrivacyProvider({ children }: { children: React.ReactNode }) {
- // useSyncExternalStore handles hydration safely
- const storedValue = useSyncExternalStore(
- subscribeToStorage,
- getStoredValue,
- () => false, // Server snapshot
- );
-
- const [privacyMode, setPrivacyMode] = useState(storedValue);
- const isFirstRender = useRef(true);
-
- // Sync with stored value on mount
- useEffect(() => {
- if (isFirstRender.current) {
- setPrivacyMode(storedValue);
- isFirstRender.current = false;
- }
- }, [storedValue]);
-
- // Persist to localStorage when privacyMode changes
- useEffect(() => {
- localStorage.setItem(STORAGE_KEY, String(privacyMode));
- }, [privacyMode]);
-
- const toggle = () => {
- setPrivacyMode((prev) => !prev);
- };
-
- const set = (value: boolean) => {
- setPrivacyMode(value);
- };
-
- return (
-
- {children}
-
- );
-}
-
-export function usePrivacyMode() {
- const context = useContext(PrivacyContext);
- if (context === undefined) {
- throw new Error("usePrivacyMode must be used within a PrivacyProvider");
- }
- return context;
-}
diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx
deleted file mode 100644
index 7c8090f..0000000
--- a/components/theme-provider.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-"use client";
-
-import { ThemeProvider as NextThemesProvider } from "next-themes";
-import type * as React from "react";
-
-export function ThemeProvider({
- children,
- ...props
-}: React.ComponentProps) {
- return {children} ;
-}
diff --git a/components/type-badge.tsx b/components/type-badge.tsx
deleted file mode 100644
index a2ec198..0000000
--- a/components/type-badge.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { Badge } from "@/components/ui/badge";
-import { cn } from "@/lib/utils/ui";
-import DotIcon from "./dot-icon";
-
-type TypeBadgeType =
- | "receita"
- | "despesa"
- | "Receita"
- | "Despesa"
- | "Transferência"
- | "transferência"
- | "Saldo inicial"
- | "Saldo Inicial";
-
-interface TypeBadgeProps {
- type: TypeBadgeType | string;
- className?: string;
-}
-
-const TYPE_LABELS: Record = {
- receita: "Receita",
- despesa: "Despesa",
- Receita: "Receita",
- Despesa: "Despesa",
- Transferência: "Transferência",
- transferência: "Transferência",
- "Saldo inicial": "Saldo Inicial",
- "Saldo Inicial": "Saldo Inicial",
-};
-
-export function TypeBadge({ type, className }: TypeBadgeProps) {
- const normalizedType = type.toLowerCase();
- const isReceita = normalizedType === "receita";
- const isTransferencia = normalizedType === "transferência";
- const isSaldoInicial = normalizedType === "saldo inicial";
- const label = TYPE_LABELS[type] || type;
-
- const colorClass = isTransferencia
- ? "text-info"
- : isReceita || isSaldoInicial
- ? "text-success"
- : "text-destructive";
-
- const dotColor = isTransferencia
- ? "bg-info"
- : isReceita || isSaldoInicial
- ? "bg-success"
- : "bg-destructive";
-
- return (
-
-
- {label}
-
- );
-}
diff --git a/components/widget-card.tsx b/components/widget-card.tsx
deleted file mode 100644
index 1537362..0000000
--- a/components/widget-card.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-"use client";
-import { RiExpandDiagonalLine } from "@remixicon/react";
-import { useCallback, useEffect, useRef, useState } from "react";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { Button } from "./ui/button";
-
-const OVERFLOW_THRESHOLD_PX = 16;
-const OVERFLOW_CHECK_DEBOUNCE_MS = 100;
-
-type WidgetProps = {
- title: string;
- subtitle: string;
- children: React.ReactNode;
- icon: React.ReactElement;
- action?: React.ReactNode;
-};
-
-export default function WidgetCard({
- title,
- subtitle,
- icon,
- children,
- action,
-}: WidgetProps) {
- const contentRef = useRef(null);
- const [hasOverflow, setHasOverflow] = useState(false);
- const [isOpen, setIsOpen] = useState(false);
- const debounceTimerRef = useRef(null);
-
- const checkOverflow = useCallback(() => {
- const el = contentRef.current;
- if (!el) return;
-
- if (debounceTimerRef.current) {
- clearTimeout(debounceTimerRef.current);
- }
-
- debounceTimerRef.current = setTimeout(() => {
- const hasOverflowNow =
- el.scrollHeight - el.clientHeight > OVERFLOW_THRESHOLD_PX;
- setHasOverflow(hasOverflowNow);
- }, OVERFLOW_CHECK_DEBOUNCE_MS);
- }, []);
-
- useEffect(() => {
- const el = contentRef.current;
- if (!el) return;
-
- // Checagem inicial
- checkOverflow();
-
- // Observa apenas resize do container (suficiente para detectar overflow)
- const ro = new ResizeObserver(checkOverflow);
- ro.observe(el);
-
- return () => {
- ro.disconnect();
- if (debounceTimerRef.current) {
- clearTimeout(debounceTimerRef.current);
- }
- };
- }, [checkOverflow]);
-
- return (
-
-
-
-
-
- {icon}
- {title}
-
-
- {subtitle}
-
-
- {action &&
{action}
}
-
-
-
-
- {children}
-
-
- {hasOverflow && (
-
- setIsOpen(true)}
- aria-label="Expandir para ver todo o conteúdo"
- >
- Ver tudo
-
-
- )}
-
-
-
-
-
- {icon}
- {title}
-
- {subtitle ? (
- {subtitle}
- ) : null}
-
-
- {children}
-
-
-
-
- );
-}
diff --git a/components/widget-empty-state.tsx b/components/widget-empty-state.tsx
deleted file mode 100644
index c4554dc..0000000
--- a/components/widget-empty-state.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import type { ReactNode } from "react";
-import {
- Empty,
- EmptyDescription,
- EmptyHeader,
- EmptyMedia,
- EmptyTitle,
-} from "@/components/ui/empty";
-
-type WidgetEmptyStateProps = {
- icon?: ReactNode;
- title: string;
- description?: string;
-};
-
-export function WidgetEmptyState({
- icon,
- title,
- description,
-}: WidgetEmptyStateProps) {
- return (
-
-
- {icon}
- {title}
- {description}
-
-
- );
-}
diff --git a/hooks/use-controlled-state.ts b/hooks/use-controlled-state.ts
deleted file mode 100644
index 04d526e..0000000
--- a/hooks/use-controlled-state.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { useCallback, useEffect, useState } from "react";
-
-/**
- * Hook for managing controlled/uncontrolled state pattern
- * Allows a component to work both in controlled and uncontrolled mode
- *
- * @param controlledValue - The controlled value (undefined for uncontrolled mode)
- * @param defaultValue - Default value for uncontrolled mode
- * @param onChange - Callback when value changes
- * @returns Tuple of [currentValue, setValue]
- *
- * @example
- * ```tsx
- * function MyComponent({ value, onChange }) {
- * const [internalValue, setValue] = useControlledState(value, false, onChange);
- * // Works both as controlled and uncontrolled
- * }
- * ```
- */
-export function useControlledState(
- controlledValue: T | undefined,
- defaultValue: T,
- onChange?: (value: T) => void,
-): [T, (value: T) => void] {
- const [internalValue, setInternalValue] = useState(defaultValue);
-
- // Sync internal value when controlled value changes
- useEffect(() => {
- if (controlledValue !== undefined) {
- setInternalValue(controlledValue);
- }
- }, [controlledValue]);
-
- // Use controlled value if provided, otherwise use internal value
- const value = controlledValue !== undefined ? controlledValue : internalValue;
-
- const setValue = useCallback(
- (newValue: T) => {
- // Update internal state if uncontrolled
- if (controlledValue === undefined) {
- setInternalValue(newValue);
- }
-
- // Always call onChange if provided
- onChange?.(newValue);
- },
- [controlledValue, onChange],
- );
-
- return [value, setValue];
-}
diff --git a/hooks/use-form-state.ts b/hooks/use-form-state.ts
deleted file mode 100644
index def4fe1..0000000
--- a/hooks/use-form-state.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { useCallback, useRef, useState } from "react";
-
-/**
- * Hook for managing form state with type-safe field updates
- *
- * @param initialValues - Initial form values
- * @returns Object with formState, updateField, updateFields, replaceForm, resetForm
- *
- * @example
- * ```tsx
- * const { formState, updateField, resetForm } = useFormState({
- * name: '',
- * email: ''
- * });
- *
- * updateField('name', 'John');
- * ```
- */
-export function useFormState(initialValues: T) {
- const latestInitialValuesRef = useRef(initialValues);
- latestInitialValuesRef.current = initialValues;
-
- const [formState, setFormState] = useState(initialValues);
-
- /**
- * Updates a single field in the form state
- */
- const updateField = useCallback(
- (field: K, value: T[K]) => {
- setFormState((prev) => ({ ...prev, [field]: value }));
- },
- [],
- );
-
- /**
- * Resets form to initial values
- */
- const resetForm = useCallback((nextValues?: T) => {
- setFormState(nextValues ?? latestInitialValuesRef.current);
- }, []);
-
- /**
- * Updates multiple fields at once
- */
- const updateFields = useCallback((updates: Partial) => {
- setFormState((prev) => ({ ...prev, ...updates }));
- }, []);
-
- const replaceForm = useCallback((nextValues: T) => {
- setFormState(nextValues);
- }, []);
-
- return {
- formState,
- updateField,
- updateFields,
- replaceForm,
- resetForm,
- };
-}
diff --git a/lib/actions/types.ts b/lib/actions/types.ts
deleted file mode 100644
index b947c67..0000000
--- a/lib/actions/types.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * Standard action result type
- */
-export type ActionResult =
- | { success: true; message: string; data?: TData }
- | { success: false; error: string };
-
-/**
- * Error result helper
- */
-export function errorResult(error: string): ActionResult {
- return { success: false, error };
-}
diff --git a/lib/faturas.ts b/lib/faturas.ts
deleted file mode 100644
index 5a0f0a7..0000000
--- a/lib/faturas.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-export const INVOICE_PAYMENT_STATUS = {
- PENDING: "pendente",
- PAID: "pago",
-} as const;
-
-export const INVOICE_STATUS_VALUES = Object.values(INVOICE_PAYMENT_STATUS);
-
-export type InvoicePaymentStatus =
- (typeof INVOICE_PAYMENT_STATUS)[keyof typeof INVOICE_PAYMENT_STATUS];
-
-export const INVOICE_STATUS_LABEL: Record = {
- [INVOICE_PAYMENT_STATUS.PENDING]: "Em aberto",
- [INVOICE_PAYMENT_STATUS.PAID]: "Pago",
-};
-
-export const INVOICE_STATUS_BADGE_VARIANT: Record<
- InvoicePaymentStatus,
- "default" | "secondary" | "success" | "info"
-> = {
- [INVOICE_PAYMENT_STATUS.PENDING]: "info",
- [INVOICE_PAYMENT_STATUS.PAID]: "success",
-};
-
-export const INVOICE_STATUS_DESCRIPTION: Record =
- {
- [INVOICE_PAYMENT_STATUS.PENDING]:
- "Esta fatura ainda não foi quitada. Você pode realizar o pagamento assim que revisar os lançamentos.",
- [INVOICE_PAYMENT_STATUS.PAID]:
- "Esta fatura está quitada. Caso tenha sido um engano, é possível desfazer o pagamento.",
- };
-
-export const PERIOD_FORMAT_REGEX = /^\d{4}-\d{2}$/;
diff --git a/package.json b/package.json
index a61c0e3..d0370f5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "openmonetis",
- "version": "1.7.7",
+ "version": "2.0.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",