style: padroniza widgets e listas do dashboard

This commit is contained in:
Felipe Coutinho
2026-03-15 23:23:12 +00:00
parent 64eb29d807
commit 2712d4919a
18 changed files with 169 additions and 182 deletions

View File

@@ -56,8 +56,6 @@ const VARIANT_CONFIG = {
increase: "text-success",
decrease: "text-destructive",
},
listItemClassName:
"flex flex-col gap-1.5 py-2 border-b border-dashed last:border-0",
includeBudgetAmount: true,
},
expense: {
@@ -70,8 +68,6 @@ const VARIANT_CONFIG = {
increase: "text-destructive",
decrease: "text-success",
},
listItemClassName:
"flex flex-col py-2 border-b border-dashed last:border-0",
includeBudgetAmount: false,
},
} as const;
@@ -194,7 +190,7 @@ export function CategoryBreakdownWidgetView({
</div>
<TabsContent value="list" className="mt-0">
<div className="flex flex-col px-0">
<div>
{data.categories.map((category, index) => {
const hasIncrease =
category.percentageChange !== null &&
@@ -218,11 +214,8 @@ export function CategoryBreakdownWidgetView({
: "text-muted-foreground";
return (
<div
key={category.categoryId}
className={config.listItemClassName}
>
<div className="flex items-center justify-between gap-3">
<div key={category.categoryId}>
<div className="flex items-center justify-between gap-3 transition-all duration-300 py-2">
<div className="flex min-w-0 flex-1 items-center gap-2">
<CategoryIconBadge
icon={category.categoryIcon}
@@ -245,7 +238,7 @@ export function CategoryBreakdownWidgetView({
/>
</Link>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
<span>
{formatPercentage(
category.percentageOfTotal,
@@ -253,6 +246,36 @@ export function CategoryBreakdownWidgetView({
)}{" "}
da {config.shareLabel}
</span>
{hasBudget && category.budgetUsedPercentage !== null ? (
<>
<span aria-hidden>·</span>
<span
className={`flex items-center gap-1 ${budgetExceeded ? "text-destructive" : "text-info"}`}
>
<RiWallet3Line className="size-3 shrink-0" />
{budgetExceeded ? (
<>
excedeu{" "}
<span className="font-medium">
{formatCurrency(exceededAmount)}
</span>
</>
) : (
<>
{formatPercentage(
category.budgetUsedPercentage,
config.percentageDigits,
)}{" "}
do limite
{config.includeBudgetAmount &&
category.budgetAmount !== null
? ` ${formatCurrency(category.budgetAmount)}`
: ""}
</>
)}
</span>
</>
) : null}
</div>
</div>
</div>
@@ -280,48 +303,6 @@ export function CategoryBreakdownWidgetView({
) : null}
</div>
</div>
{hasBudget && category.budgetUsedPercentage !== null ? (
<div className="ml-11 flex items-center gap-1.5 text-xs">
<RiWallet3Line
className={`size-3 ${
budgetExceeded ? "text-destructive" : "text-info"
}`}
/>
<span
className={
budgetExceeded ? "text-destructive" : "text-info"
}
>
{budgetExceeded ? (
<>
{formatPercentage(
category.budgetUsedPercentage,
config.percentageDigits,
)}{" "}
do limite
{config.includeBudgetAmount &&
category.budgetAmount !== null
? ` ${formatCurrency(category.budgetAmount)}`
: ""}{" "}
- excedeu em {formatCurrency(exceededAmount)}
</>
) : (
<>
{formatPercentage(
category.budgetUsedPercentage,
config.percentageDigits,
)}{" "}
do limite
{config.includeBudgetAmount &&
category.budgetAmount !== null
? ` ${formatCurrency(category.budgetAmount)}`
: ""}
</>
)}
</span>
</div>
) : null}
</div>
);
})}

View File

@@ -3,15 +3,14 @@ import {
RiArrowDownSFill,
RiArrowUpLine,
RiArrowUpSFill,
RiCashLine,
RiIncreaseDecreaseLine,
RiCalendarCheckLine,
RiScalesLine,
RiSubtractLine,
} from "@remixicon/react";
import type { DashboardCardMetrics } from "@/features/dashboard/dashboard-metrics-queries";
import MoneyValues from "@/shared/components/money-values";
import {
Card,
CardAction,
CardFooter,
CardHeader,
CardTitle,
@@ -32,20 +31,33 @@ const CARDS = [
key: "receitas",
icon: RiArrowUpLine,
invertTrend: false,
cardClass: "",
iconClass: "text-success",
},
{
label: "Despesas",
key: "despesas",
icon: RiArrowDownLine,
invertTrend: true,
cardClass: "",
iconClass: "text-destructive",
},
{
label: "Balanço",
key: "balanco",
icon: RiIncreaseDecreaseLine,
icon: RiScalesLine,
invertTrend: false,
cardClass: "",
iconClass: "text-amber-500",
},
{
label: "Previsto",
key: "previsto",
icon: RiCalendarCheckLine,
invertTrend: false,
cardClass: "border border-dashed",
iconClass: "",
},
{ label: "Previsto", key: "previsto", icon: RiCashLine, invertTrend: false },
] as const;
const TREND_ICONS = {
@@ -62,7 +74,7 @@ const getTrend = (current: number, previous: number): Trend => {
};
const getPercentChange = (current: number, previous: number): string => {
const EPSILON = 0.01; // Considera valores menores que 1 centavo como zero
const EPSILON = 0.01;
if (Math.abs(previous) < EPSILON) {
if (Math.abs(current) < EPSILON) return "0%";
@@ -80,48 +92,49 @@ const getPercentChange = (current: number, previous: number): string => {
};
const getTrendColor = (trend: Trend, invertTrend: boolean): string => {
if (trend === "flat") return "";
if (trend === "flat") return "text-muted-foreground";
const isPositive = invertTrend ? trend === "down" : trend === "up";
return isPositive
? "text-success border-success"
: "text-destructive border-destructive";
return isPositive ? "text-success" : "text-destructive";
};
export function DashboardMetricsCards({ metrics }: DashboardMetricsCardsProps) {
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, invertTrend }) => {
const metric = metrics[key];
const trend = getTrend(metric.current, metric.previous);
const TrendIcon = TREND_ICONS[trend];
const trendColor = getTrendColor(trend, invertTrend);
<div className="grid grid-cols-1 gap-3 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
{CARDS.map(
({ label, key, icon: Icon, invertTrend, cardClass, iconClass }) => {
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">
<CardHeader>
<CardTitle className="flex items-center gap-1 tracking-tighter lowercase">
<Icon className="size-4" />
{label}
</CardTitle>
<MoneyValues className="text-2xl" amount={metric.current} />
<CardAction>
<div className={`flex items-center text-xs ${trendColor}`}>
<TrendIcon size={16} />
{getPercentChange(metric.current, metric.previous)}
return (
<Card
key={label}
className={`@container/card flex flex-col justify-between min-h-34 ${cardClass}`}
>
<CardHeader>
<CardTitle className="flex items-center gap-1 tracking-tight lowercase">
<Icon className={`size-4 ${iconClass}`} />
{label}
</CardTitle>
<div className="flex items-baseline gap-2 mt-auto pt-2">
<MoneyValues className="text-2xl" amount={metric.current} />
<div className={`flex items-center text-xs ${trendColor}`}>
<TrendIcon size={14} />
{getPercentChange(metric.current, metric.previous)}
</div>
</div>
</CardAction>
</CardHeader>
<CardFooter className="flex-col items-start gap-2 text-sm">
<div className="line-clamp-1 flex gap-2 text-xs">
mês anterior
</div>
<div className="text-foreground">
<MoneyValues amount={metric.previous} />
</div>
</CardFooter>
</Card>
);
})}
</CardHeader>
<CardFooter className="text-sm">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<span>vs. mês anterior</span>
<MoneyValues amount={metric.previous} />
</div>
</CardFooter>
</Card>
);
},
)}
</div>
);
}

View File

@@ -9,9 +9,9 @@ export function DashboardWelcome({ name }: { name?: string | null }) {
<section className="p-2">
<div className="tracking-tight">
<h1 className="text-xl">
{greeting}, {displayName}
{greeting}, <span className="text-primary">{displayName}</span>
</h1>
<p className="text-sm mt-1">{formattedDate}</p>
<p className="text-sm mt-1 text-muted-foreground">{formattedDate}</p>
</div>
</section>
);

View File

@@ -26,7 +26,7 @@ export function GoalProgressItem({
const percentageDelta = item.usedPercentage - 100;
return (
<li className="border-b border-dashed py-2 last:border-b-0 last:pb-0">
<div className="transition-all duration-300 py-2">
<div className="flex items-start justify-between gap-3">
<div className="flex min-w-0 flex-1 items-start gap-2">
<CategoryIconBadge
@@ -52,9 +52,9 @@ export function GoalProgressItem({
</span>
<Button
type="button"
variant="ghost"
variant="outline"
size="icon-sm"
className="size-7 text-muted-foreground hover:text-foreground"
className="text-muted-foreground hover:text-foreground"
onClick={() => onEdit(item)}
aria-label={`Editar orçamento de ${item.categoryName}`}
>
@@ -65,6 +65,6 @@ export function GoalProgressItem({
<div className="ml-11 mt-1.5">
<Progress value={progressValue} />
</div>
</li>
</div>
);
}

View File

@@ -26,7 +26,7 @@ export function InstallmentExpenseListItem({
} = buildInstallmentExpenseDisplay(expense);
return (
<li className="flex items-center gap-3 border-b border-dashed pb-3 last:border-b-0 last:pb-0">
<div className="flex items-center gap-3 transition-all duration-300 py-2">
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
@@ -71,6 +71,6 @@ export function InstallmentExpenseListItem({
<Progress value={progress} className="mt-1 h-2" />
</div>
</li>
</div>
);
}

View File

@@ -21,7 +21,7 @@ export function InstallmentExpensesList({
}
return (
<ul className="flex flex-col gap-2">
<ul className="flex flex-col">
{expenses.map((expense) => (
<InstallmentExpenseListItem key={expense.id} expense={expense} />
))}

View File

@@ -9,7 +9,7 @@ export function InstallmentExpensesWidgetView({
data,
}: InstallmentExpensesWidgetViewProps) {
return (
<div className="flex flex-col gap-4 px-0">
<div className="flex flex-col">
<InstallmentExpensesList expenses={data.expenses} />
</div>
);

View File

@@ -27,12 +27,12 @@ export function MyAccountsWidget({
return (
<>
<div className="flex justify-between py-2">
<div className="flex justify-between py-1">
Saldo Total
<MoneyValues className="text-2xl" amount={totalBalance} />
</div>
<div className="py-2 px-0">
<div>
{displayedAccounts.length === 0 ? (
<div className="-mt-10">
<WidgetEmptyState
@@ -49,12 +49,12 @@ export function MyAccountsWidget({
const logoSrc = resolveLogoSrc(account.logo);
return (
<li
<div
key={account.id}
className="flex items-center justify-between gap-2 border-b border-dashed py-2 last:border-0"
className="flex items-center justify-between transition-all duration-300 py-1.5 "
>
<div className="flex min-w-0 flex-1 items-center gap-3">
<div className="relative size-10 overflow-hidden">
<div className="flex min-w-0 flex-1 items-center gap-2 py-1">
<div className="relative size-9.5 overflow-hidden">
{logoSrc ? (
<Image
src={logoSrc}
@@ -88,7 +88,7 @@ export function MyAccountsWidget({
<div className="flex flex-col items-end gap-0.5 text-right">
<MoneyValues amount={account.balance} />
</div>
</li>
</div>
);
})}
</ul>

View File

@@ -23,7 +23,7 @@ export function NoteListItem({
const createdAtLabel = formatNoteCreatedAt(note.createdAt);
return (
<li className="flex items-center justify-between gap-2 border-b border-dashed py-2 last:border-b-0 last:pb-0">
<div className="flex items-center justify-between gap-2 transition-all duration-300 py-2">
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-foreground">
{displayTitle}
@@ -40,9 +40,9 @@ export function NoteListItem({
</div>
</div>
<div className="flex shrink-0 items-center">
<div className="flex shrink-0 items-center gap-1">
<Button
variant="ghost"
variant="outline"
size="icon-sm"
className="text-muted-foreground hover:text-foreground"
onClick={() => onOpenEdit(note)}
@@ -51,7 +51,7 @@ export function NoteListItem({
<RiPencilLine className="size-4" />
</Button>
<Button
variant="ghost"
variant="outline"
size="icon-sm"
className="text-muted-foreground hover:text-foreground"
onClick={() => onOpenDetails(note)}
@@ -60,6 +60,6 @@ export function NoteListItem({
<RiFileList2Line className="size-4" />
</Button>
</div>
</li>
</div>
);
}

View File

@@ -48,7 +48,7 @@ export function PayersWidget({ payers }: PayersWidgetProps) {
description="Quando houver despesas associadas a pagadores, eles aparecerão aqui."
/>
) : (
<ul className="flex flex-col">
<div className="flex flex-col">
{payers.map((payer) => {
const initials = buildInitials(payer.name);
const hasValidPercentageChange =
@@ -59,12 +59,12 @@ export function PayersWidget({ payers }: PayersWidgetProps) {
: null;
return (
<li
<div
key={payer.id}
className="flex items-center justify-between border-b border-dashed last:border-b-0 last:pb-0"
className="flex items-center justify-between transition-all duration-300 py-1.5"
>
<div className="flex min-w-0 flex-1 items-center gap-2 py-2">
<Avatar className="size-10 shrink-0">
<div className="flex min-w-0 flex-1 items-center gap-2 py-1">
<Avatar className="size-9.5 shrink-0">
<AvatarImage
src={getAvatarSrc(payer.avatarUrl)}
alt={`Avatar de ${payer.name}`}
@@ -118,10 +118,10 @@ export function PayersWidget({ payers }: PayersWidgetProps) {
</span>
)}
</div>
</li>
</div>
);
})}
</ul>
</div>
)}
</CardContent>
);

View File

@@ -26,7 +26,7 @@ export function PaymentBreakdownListItem({
item,
}: PaymentBreakdownListItemProps) {
return (
<li className="flex items-center gap-3 border-b border-dashed pb-3 last:border-b-0 last:pb-0">
<div className="flex items-center gap-3 transition-all duration-300 py-1.5">
<div className={ICON_WRAPPER_CLASS}>{item.icon}</div>
<div className="min-w-0 flex-1">
@@ -46,6 +46,6 @@ export function PaymentBreakdownListItem({
<Progress value={item.percentage} />
</div>
</div>
</li>
</div>
);
}

View File

@@ -34,7 +34,7 @@ export function PaymentStatusWidgetView({
pending={data.income.pending}
/>
<div className="border-t border-dashed" />
<div className="border-t" />
<PaymentStatusCategorySection
title="A Pagar"

View File

@@ -170,12 +170,12 @@ export function PurchasesByCategoryWidget({
}
/>
) : (
<ul className="flex flex-col">
<div className="flex flex-col">
{currentTransactions.map((transaction) => {
return (
<li
<div
key={transaction.id}
className="flex items-center justify-between gap-3 border-b border-dashed py-2 last:border-b-0 last:pb-0"
className="flex items-center justify-between gap-3 transition-all duration-300 py-2"
>
<div className="flex min-w-0 flex-1 items-center gap-3">
<EstabelecimentoLogo name={transaction.name} size={37} />
@@ -193,10 +193,10 @@ export function PurchasesByCategoryWidget({
<div className="shrink-0 text-foreground">
<MoneyValues amount={transaction.amount} />
</div>
</li>
</div>
);
})}
</ul>
</div>
)}
</div>
);

View File

@@ -30,36 +30,34 @@ export function RecurringExpensesWidget({
}
return (
<div className="flex flex-col gap-4 px-0">
<ul className="flex flex-col gap-2">
{data.expenses.map((expense) => {
return (
<li
key={expense.id}
className="flex items-center gap-3 border-b border-dashed pb-2 last:border-b-0 last:pb-0"
>
<EstabelecimentoLogo name={expense.name} size={37} />
<div className="flex flex-col">
{data.expenses.map((expense) => {
return (
<div
key={expense.id}
className="flex items-center gap-2 transition-all duration-300 py-1.5"
>
<EstabelecimentoLogo name={expense.name} size={37} />
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between">
<p className="truncate text-foreground text-sm font-medium">
{expense.name}
</p>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between">
<p className="truncate text-foreground text-sm font-medium">
{expense.name}
</p>
<MoneyValues amount={expense.amount} />
</div>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1">
{expense.paymentMethod}
</span>
<span>{formatOccurrences(expense.recurrenceCount)}</span>
</div>
<MoneyValues amount={expense.amount} />
</div>
</li>
);
})}
</ul>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1">
{expense.paymentMethod}
</span>
<span>{formatOccurrences(expense.recurrenceCount)}</span>
</div>
</div>
</div>
);
})}
</div>
);
}

View File

@@ -27,12 +27,12 @@ export function TopEstablishmentsWidget({
description="Quando houver despesas registradas, elas aparecerão aqui."
/>
) : (
<ul className="flex flex-col">
<div className="flex flex-col">
{data.establishments.map((establishment) => {
return (
<li
<div
key={establishment.id}
className="flex items-center justify-between gap-3 border-b border-dashed py-2 last:border-b-0 last:pb-0"
className="flex items-center justify-between gap-3 transition-all duration-300 py-2"
>
<div className="flex min-w-0 flex-1 items-center gap-3">
<EstabelecimentoLogo name={establishment.name} size={37} />
@@ -50,10 +50,10 @@ export function TopEstablishmentsWidget({
<div className="shrink-0 text-foreground">
<MoneyValues amount={establishment.amount} />
</div>
</li>
</div>
);
})}
</ul>
</div>
)}
</div>
);

View File

@@ -85,9 +85,7 @@ export function TopExpensesWidget({
htmlFor="card-only-toggle"
className="text-sm text-muted-foreground"
>
{cardOnly
? "Somente cartões de crédito ou débito."
: "Todas as despesas"}
Apenas cartões
</label>
<Switch
id="card-only-toggle"
@@ -107,12 +105,12 @@ export function TopExpensesWidget({
/>
</div>
) : (
<ul className="flex flex-col">
<div className="flex flex-col">
{data.expenses.map((expense) => {
return (
<li
<div
key={expense.id}
className="flex items-center justify-between gap-3 border-b border-dashed py-2 last:border-b-0 last:pb-0"
className="flex items-center justify-between gap-3 transition-all duration-300 py-2"
>
<div className="flex min-w-0 flex-1 items-center gap-3">
<EstabelecimentoLogo name={expense.name} size={37} />
@@ -130,10 +128,10 @@ export function TopExpensesWidget({
<div className="shrink-0 text-foreground">
<MoneyValues amount={expense.amount} />
</div>
</li>
</div>
);
})}
</ul>
</div>
)}
</div>
);