feat(v1.4.0): design system semântico, correções de revalidação e melhorias de UX

- Adicionar tokens semânticos de estado (success, warning, info) no globals.css
- Migrar ~60+ componentes de cores hardcoded do Tailwind para tokens semânticos
- Unificar 3 arrays duplicados de cores de categorias em importação única
- Corrigir widgets de boleto/fatura que não atualizavam após pagamento
  (actions de fatura e antecipação não invalidavam cache do dashboard)
- Corrigir scroll em listas Popover+Command (modal prop)
- Adicionar link "detalhes" no card de orçamento para página da categoria
- Adicionar indicadores de tendência coloridos nos cards de métricas
- Estender cores de chart de 6 para 10
- Normalizar dark mode e remover tokens não utilizados

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-02-07 15:14:59 +00:00
parent 390754c0e8
commit f50261208a
60 changed files with 324 additions and 305 deletions

View File

@@ -182,8 +182,7 @@ export function BoletosWidget({ boletos }: BoletosWidgetProps) {
<span
className={cn(
"rounded-full py-0.5",
boleto.isSettled &&
"text-green-600 dark:text-green-400",
boleto.isSettled && "text-success",
)}
>
{statusLabel}
@@ -203,7 +202,7 @@ export function BoletosWidget({ boletos }: BoletosWidgetProps) {
onClick={() => handleOpenModal(boleto.id)}
>
{boleto.isSettled ? (
<span className="flex items-center gap-1 text-green-600 dark:text-green-400">
<span className="flex items-center gap-1 text-success">
<RiCheckboxCircleFill className="size-3" /> Pago
</span>
) : (
@@ -248,7 +247,7 @@ export function BoletosWidget({ boletos }: BoletosWidgetProps) {
>
{modalState === "success" ? (
<div className="flex flex-col items-center gap-4 py-6 text-center">
<div className="flex size-16 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-500">
<div className="flex size-16 items-center justify-center rounded-full bg-success/10 text-success">
<RiCheckboxCircleLine className="size-8" />
</div>
<div className="space-y-2">

View File

@@ -28,6 +28,7 @@ import {
} from "@/components/ui/popover";
import { WidgetEmptyState } from "@/components/widget-empty-state";
import type { CategoryHistoryData } from "@/lib/dashboard/categories/category-history";
import { CATEGORY_COLORS } from "@/lib/utils/category-colors";
import { getIconComponent } from "@/lib/utils/icons";
type CategoryHistoryWidgetProps = {
@@ -36,14 +37,7 @@ type CategoryHistoryWidgetProps = {
const STORAGE_KEY_SELECTED = "dashboard-category-history-selected";
// Vibrant colors for categories
const CHART_COLORS = [
"#ef4444", // red-500
"#3b82f6", // blue-500
"#10b981", // emerald-500
"#f59e0b", // amber-500
"#8b5cf6", // violet-500
];
const CHART_COLORS = CATEGORY_COLORS;
export function CategoryHistoryWidget({ data }: CategoryHistoryWidgetProps) {
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
@@ -260,7 +254,7 @@ export function CategoryHistoryWidget({ data }: CategoryHistoryWidgetProps) {
)}
{selectedCategories.length < 5 && availableCategories.length > 0 && (
<Popover open={open} onOpenChange={setOpen}>
<Popover open={open} onOpenChange={setOpen} modal>
<PopoverTrigger asChild>
<Button
variant="outline"
@@ -295,9 +289,9 @@ export function CategoryHistoryWidget({ data }: CategoryHistoryWidgetProps) {
className="gap-2"
>
{IconComponent ? (
<IconComponent className="size-4 text-red-600" />
<IconComponent className="size-4 text-destructive" />
) : (
<div className="size-3 rounded-sm bg-red-600" />
<div className="size-3 rounded-sm bg-destructive" />
)}
<span>{category.name}</span>
</CommandItem>
@@ -320,9 +314,9 @@ export function CategoryHistoryWidget({ data }: CategoryHistoryWidgetProps) {
className="gap-2"
>
{IconComponent ? (
<IconComponent className="size-4 text-green-600" />
<IconComponent className="size-4 text-success" />
) : (
<div className="size-3 rounded-sm bg-green-600" />
<div className="size-3 rounded-sm bg-success" />
)}
<span>{category.name}</span>
</CommandItem>

View File

@@ -1,8 +1,8 @@
"use client";
import {
RiArrowDownLine,
RiArrowUpLine,
RiArrowDownSFill,
RiArrowUpSFill,
RiExternalLinkLine,
RiListUnordered,
RiPieChart2Line,
@@ -220,14 +220,14 @@ export function ExpensesByCategoryWidgetWithChart({
<span
className={`flex items-center gap-0.5 text-xs ${
hasIncrease
? "text-red-600 dark:text-red-500"
? "text-destructive"
: hasDecrease
? "text-green-600 dark:text-green-500"
? "text-success"
: "text-muted-foreground"
}`}
>
{hasIncrease && <RiArrowUpLine className="size-3" />}
{hasDecrease && <RiArrowDownLine className="size-3" />}
{hasIncrease && <RiArrowUpSFill className="size-3" />}
{hasDecrease && <RiArrowDownSFill className="size-3" />}
{formatPercentage(category.percentageChange)}
</span>
)}
@@ -238,16 +238,12 @@ export function ExpensesByCategoryWidgetWithChart({
<div className="ml-11 flex items-center gap-1.5 text-xs">
<RiWallet3Line
className={`size-3 ${
budgetExceeded
? "text-red-600"
: "text-blue-600 dark:text-blue-400"
budgetExceeded ? "text-destructive" : "text-info"
}`}
/>
<span
className={
budgetExceeded
? "text-red-600"
: "text-blue-600 dark:text-blue-400"
budgetExceeded ? "text-destructive" : "text-info"
}
>
{budgetExceeded ? (

View File

@@ -1,6 +1,6 @@
import {
RiArrowDownLine,
RiArrowUpLine,
RiArrowDownSFill,
RiArrowUpSFill,
RiExternalLinkLine,
RiPieChartLine,
RiWallet3Line,
@@ -127,14 +127,14 @@ export function ExpensesByCategoryWidget({
<span
className={`flex items-center gap-0.5 text-xs ${
hasIncrease
? "text-red-600 dark:text-red-500"
? "text-destructive"
: hasDecrease
? "text-green-600 dark:text-green-500"
? "text-success"
: "text-muted-foreground"
}`}
>
{hasIncrease && <RiArrowUpLine className="size-3" />}
{hasDecrease && <RiArrowDownLine className="size-3" />}
{hasIncrease && <RiArrowUpSFill className="size-3" />}
{hasDecrease && <RiArrowDownSFill className="size-3" />}
{formatPercentage(category.percentageChange)}
</span>
)}
@@ -145,17 +145,11 @@ export function ExpensesByCategoryWidget({
<div className="ml-11 flex items-center gap-1.5 text-xs">
<RiWallet3Line
className={`size-3 ${
budgetExceeded
? "text-red-600"
: "text-blue-600 dark:text-blue-400"
budgetExceeded ? "text-destructive" : "text-info"
}`}
/>
<span
className={
budgetExceeded
? "text-red-600"
: "text-blue-600 dark:text-blue-400"
}
className={budgetExceeded ? "text-destructive" : "text-info"}
>
{budgetExceeded ? (
<>

View File

@@ -1,8 +1,8 @@
"use client";
import {
RiArrowDownLine,
RiArrowUpLine,
RiArrowDownSFill,
RiArrowUpSFill,
RiExternalLinkLine,
RiListUnordered,
RiPieChart2Line,
@@ -220,14 +220,14 @@ export function IncomeByCategoryWidgetWithChart({
<span
className={`flex items-center gap-0.5 text-xs ${
hasIncrease
? "text-green-600 dark:text-green-500"
? "text-success"
: hasDecrease
? "text-red-600 dark:text-red-500"
? "text-destructive"
: "text-muted-foreground"
}`}
>
{hasIncrease && <RiArrowUpLine className="size-3" />}
{hasDecrease && <RiArrowDownLine className="size-3" />}
{hasIncrease && <RiArrowUpSFill className="size-3" />}
{hasDecrease && <RiArrowDownSFill className="size-3" />}
{formatPercentage(category.percentageChange)}
</span>
)}
@@ -240,16 +240,12 @@ export function IncomeByCategoryWidgetWithChart({
<div className="ml-11 flex items-center gap-1.5 text-xs">
<RiWallet3Line
className={`size-3 ${
budgetExceeded
? "text-red-600"
: "text-blue-600 dark:text-blue-400"
budgetExceeded ? "text-destructive" : "text-info"
}`}
/>
<span
className={
budgetExceeded
? "text-red-600"
: "text-blue-600 dark:text-blue-400"
budgetExceeded ? "text-destructive" : "text-info"
}
>
{budgetExceeded ? (

View File

@@ -1,6 +1,6 @@
import {
RiArrowDownLine,
RiArrowUpLine,
RiArrowDownSFill,
RiArrowUpSFill,
RiExternalLinkLine,
RiPieChartLine,
RiWallet3Line,
@@ -127,14 +127,14 @@ export function IncomeByCategoryWidget({
<span
className={`flex items-center gap-0.5 text-xs ${
hasIncrease
? "text-green-600 dark:text-green-500"
? "text-success"
: hasDecrease
? "text-red-600 dark:text-red-500"
? "text-destructive"
: "text-muted-foreground"
}`}
>
{hasIncrease && <RiArrowUpLine className="size-3" />}
{hasDecrease && <RiArrowDownLine className="size-3" />}
{hasIncrease && <RiArrowUpSFill className="size-3" />}
{hasDecrease && <RiArrowDownSFill className="size-3" />}
{formatPercentage(category.percentageChange)}
</span>
)}
@@ -147,16 +147,12 @@ export function IncomeByCategoryWidget({
<div className="ml-11 flex items-center gap-1.5 text-xs">
<RiWallet3Line
className={`size-3 ${
budgetExceeded
? "text-red-600"
: "text-blue-600 dark:text-blue-400"
budgetExceeded ? "text-destructive" : "text-info"
}`}
/>
<span
className={
budgetExceeded
? "text-red-600"
: "text-blue-600 dark:text-blue-400"
budgetExceeded ? "text-destructive" : "text-info"
}
>
{budgetExceeded ? (

View File

@@ -176,7 +176,7 @@ export function InstallmentGroupCard({
"flex items-center gap-3 rounded-md border p-2 transition-colors",
isSelected && !isPaid && "border-primary/50 bg-primary/5",
isPaid &&
"border-green-400 bg-green-50 dark:border-green-900 dark:bg-green-950/30",
"border-success/40 bg-success/5 dark:border-success/20 dark:bg-success/5",
)}
>
<Checkbox
@@ -194,7 +194,7 @@ export function InstallmentGroupCard({
className={cn(
"text-xs font-medium",
isPaid &&
"text-green-700 dark:text-green-400 line-through decoration-green-600/50",
"text-success line-through decoration-success/50",
)}
>
Parcela {installment.currentInstallment}/
@@ -202,7 +202,7 @@ export function InstallmentGroupCard({
{isPaid && (
<Badge
variant="outline"
className="ml-1 text-xs border-none border-green-700 text-green-700 dark:text-green-400"
className="ml-1 text-xs border-none text-success"
>
<RiCheckboxCircleFill /> Pago
</Badge>
@@ -211,9 +211,7 @@ export function InstallmentGroupCard({
<p
className={cn(
"text-xs mt-1",
isPaid
? "text-green-700 dark:text-green-500"
: "text-muted-foreground",
isPaid ? "text-success" : "text-muted-foreground",
)}
>
Vencimento: {dueDate}
@@ -224,7 +222,7 @@ export function InstallmentGroupCard({
amount={installment.amount}
className={cn(
"shrink-0 text-sm",
isPaid && "text-green-700 dark:text-green-400",
isPaid && "text-success",
)}
/>
</div>

View File

@@ -358,7 +358,7 @@ export function InvoicesWidget({ invoices }: InvoicesWidgetProps) {
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
{!isPaid ? <span>{dueInfo.label}</span> : null}
{isPaid && paymentInfo ? (
<span className="text-green-600 dark:text-green-400">
<span className="text-success">
{paymentInfo.label}
</span>
) : null}
@@ -378,7 +378,7 @@ export function InvoicesWidget({ invoices }: InvoicesWidgetProps) {
className="p-0 h-auto disabled:opacity-100"
>
{isPaid ? (
<span className="text-green-600 dark:text-green-400 flex items-center gap-1">
<span className="text-success flex items-center gap-1">
<RiCheckboxCircleFill className="size-3" /> Pago
</span>
) : (
@@ -421,7 +421,7 @@ export function InvoicesWidget({ invoices }: InvoicesWidgetProps) {
>
{modalState === "success" ? (
<div className="flex flex-col items-center gap-4 py-6 text-center">
<div className="flex size-16 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-500">
<div className="flex size-16 items-center justify-center rounded-full bg-success/10 text-success">
<RiCheckboxCircleLine className="size-8" />
</div>
<div className="space-y-2">
@@ -489,7 +489,7 @@ export function InvoicesWidget({ invoices }: InvoicesWidgetProps) {
) : null}
{selectedInvoice.paymentStatus ===
INVOICE_PAYMENT_STATUS.PAID && selectedPaymentInfo ? (
<p className="text-xs text-emerald-600">
<p className="text-xs text-success">
{selectedPaymentInfo.label}
</p>
) : null}

View File

@@ -47,13 +47,13 @@ function CategorySection({
{/* Status de confirmados e pendentes */}
<div className="flex items-center justify-between gap-4 text-sm">
<div className="flex items-center gap-1.5 ">
<RiCheckboxCircleLine className="size-3 text-emerald-600" />
<RiCheckboxCircleLine className="size-3 text-success" />
<MoneyValues amount={confirmed} />
<span className="text-xs text-muted-foreground">confirmados</span>
</div>
<div className="flex items-center gap-1.5 ">
<RiHourglass2Line className="size-3 text-orange-500" />
<RiHourglass2Line className="size-3 text-warning" />
<MoneyValues amount={pending} />
<span className="text-xs text-muted-foreground">pendentes</span>
</div>

View File

@@ -1,6 +1,6 @@
"use client";
import { RiArrowDownLine, RiStore3Line } from "@remixicon/react";
import { RiArrowDownSFill, RiStore3Line } from "@remixicon/react";
import { useEffect, useMemo, useState } from "react";
import { EstabelecimentoLogo } from "@/components/lancamentos/shared/estabelecimento-logo";
import MoneyValues from "@/components/money-values";
@@ -146,7 +146,7 @@ export function PurchasesByCategoryWidget({
{currentTransactions.length === 0 ? (
<WidgetEmptyState
icon={<RiArrowDownLine className="size-6 text-muted-foreground" />}
icon={<RiArrowDownSFill className="size-6 text-muted-foreground" />}
title="Nenhuma compra encontrada"
description={
selectedCategory

View File

@@ -1,11 +1,12 @@
import {
RiArrowDownLine,
RiArrowDownSFill,
RiArrowUpLine,
RiCurrencyLine,
RiArrowUpSFill,
RiCashLine,
RiIncreaseDecreaseLine,
RiSubtractLine,
} from "@remixicon/react";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardAction,
@@ -25,15 +26,30 @@ type Trend = "up" | "down" | "flat";
const TREND_THRESHOLD = 0.005;
const CARDS = [
{ label: "Receitas", key: "receitas", icon: RiArrowUpLine },
{ label: "Despesas", key: "despesas", icon: RiArrowDownLine },
{ label: "Balanço", key: "balanco", icon: RiIncreaseDecreaseLine },
{ label: "Previsto", key: "previsto", icon: RiCurrencyLine },
{
label: "Receitas",
key: "receitas",
icon: RiArrowUpLine,
invertTrend: false,
},
{
label: "Despesas",
key: "despesas",
icon: RiArrowDownLine,
invertTrend: true,
},
{
label: "Balanço",
key: "balanco",
icon: RiIncreaseDecreaseLine,
invertTrend: false,
},
{ label: "Previsto", key: "previsto", icon: RiCashLine, invertTrend: false },
] as const;
const TREND_ICONS = {
up: RiArrowUpLine,
down: RiArrowDownLine,
up: RiArrowUpSFill,
down: RiArrowDownSFill,
flat: RiSubtractLine,
} as const;
@@ -58,13 +74,22 @@ const getPercentChange = (current: number, previous: number): string => {
: "—";
};
const getTrendColor = (trend: Trend, invertTrend: boolean): string => {
if (trend === "flat") return "";
const isPositive = invertTrend ? trend === "down" : trend === "up";
return isPositive
? "text-success border-success"
: "text-destructive border-destructive";
};
export function SectionCards({ metrics }: SectionCardsProps) {
return (
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-3 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
{CARDS.map(({ label, key, icon: Icon }) => {
{CARDS.map(({ label, key, icon: Icon, invertTrend }) => {
const metric = metrics[key];
const trend = getTrend(metric.current, metric.previous);
const TrendIcon = TREND_ICONS[trend];
const trendColor = getTrendColor(trend, invertTrend);
return (
<Card key={label} className="@container/card gap-2">
@@ -75,10 +100,10 @@ export function SectionCards({ metrics }: SectionCardsProps) {
</CardTitle>
<MoneyValues className="text-2xl" amount={metric.current} />
<CardAction>
<Badge variant="outline">
<TrendIcon />
<div className={`flex items-center text-xs ${trendColor}`}>
<TrendIcon size={16} />
{getPercentChange(metric.current, metric.previous)}
</Badge>
</div>
</CardAction>
</CardHeader>
<CardFooter className="flex-col items-start gap-2 text-sm">