feat(03-01): add CSS animation tokens, i18n keys, and carryover display

- Add collapsible-open/close keyframes and CSS animation tokens to index.css
- Add dashboard.sections and dashboard.carryoverIncludes keys to en.json and de.json
- Add optional subtitle/subtitleClassName props to StatCard
- Extend SummaryStrip balance prop with carryoverSubtitle/carryoverIsNegative
- Compute and pass carryover subtitle from DashboardContent to SummaryStrip
This commit is contained in:
2026-03-17 15:07:08 +01:00
parent 1a4035bea1
commit 21ce6d8230
6 changed files with 51 additions and 3 deletions

View File

@@ -7,6 +7,8 @@ interface StatCardProps {
title: string title: string
value: string value: string
valueClassName?: string valueClassName?: string
subtitle?: string
subtitleClassName?: string
variance?: { variance?: {
amount: string amount: string
direction: "up" | "down" | "neutral" direction: "up" | "down" | "neutral"
@@ -24,6 +26,8 @@ export function StatCard({
title, title,
value, value,
valueClassName, valueClassName,
subtitle,
subtitleClassName,
variance, variance,
}: StatCardProps) { }: StatCardProps) {
return ( return (
@@ -42,6 +46,11 @@ export function StatCard({
> >
{value} {value}
</p> </p>
{subtitle && (
<p className={cn("mt-0.5 text-xs text-muted-foreground", subtitleClassName)}>
{subtitle}
</p>
)}
{variance && ( {variance && (
<div className="mt-1 flex items-center gap-1 text-xs text-muted-foreground"> <div className="mt-1 flex items-center gap-1 text-xs text-muted-foreground">
{(() => { {(() => {

View File

@@ -3,7 +3,12 @@ import { StatCard } from "./StatCard"
interface SummaryStripProps { interface SummaryStripProps {
income: { value: string; budgeted: string } income: { value: string; budgeted: string }
expenses: { value: string; budgeted: string } expenses: { value: string; budgeted: string }
balance: { value: string; isPositive: boolean } balance: {
value: string
isPositive: boolean
carryoverSubtitle?: string
carryoverIsNegative?: boolean
}
t: (key: string) => string t: (key: string) => string
} }
@@ -39,6 +44,8 @@ export function SummaryStrip({
title={t("dashboard.availableBalance")} title={t("dashboard.availableBalance")}
value={balance.value} value={balance.value}
valueClassName={balance.isPositive ? "text-on-budget" : "text-over-budget"} valueClassName={balance.isPositive ? "text-on-budget" : "text-over-budget"}
subtitle={balance.carryoverSubtitle}
subtitleClassName={balance.carryoverIsNegative ? "text-over-budget" : undefined}
/> />
</div> </div>
) )

View File

@@ -84,7 +84,12 @@
"actual": "Tatsaechlich", "actual": "Tatsaechlich",
"noBudgetForMonth": "Kein Budget fuer diesen Monat", "noBudgetForMonth": "Kein Budget fuer diesen Monat",
"createBudget": "Budget erstellen", "createBudget": "Budget erstellen",
"generateFromTemplate": "Aus Vorlage generieren" "generateFromTemplate": "Aus Vorlage generieren",
"sections": {
"itemName": "Posten",
"groupTotal": "{{label}} Gesamt"
},
"carryoverIncludes": "Inkl. {{amount}} Übertrag"
}, },
"quickAdd": { "quickAdd": {
"title": "Schnelleingabe-Bibliothek", "title": "Schnelleingabe-Bibliothek",

View File

@@ -84,7 +84,12 @@
"actual": "Actual", "actual": "Actual",
"noBudgetForMonth": "No budget for this month", "noBudgetForMonth": "No budget for this month",
"createBudget": "Create Budget", "createBudget": "Create Budget",
"generateFromTemplate": "Generate from Template" "generateFromTemplate": "Generate from Template",
"sections": {
"itemName": "Item",
"groupTotal": "{{label}} Total"
},
"carryoverIncludes": "Includes {{amount}} carryover"
}, },
"quickAdd": { "quickAdd": {
"title": "Quick Add Library", "title": "Quick Add Library",

View File

@@ -71,9 +71,23 @@
--radius: 0.625rem; --radius: 0.625rem;
/* Collapsible animation */
--animate-collapsible-open: collapsible-open 200ms ease-out;
--animate-collapsible-close: collapsible-close 200ms ease-out;
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
} }
@keyframes collapsible-open {
from { height: 0; overflow: hidden; }
to { height: var(--radix-collapsible-content-height); overflow: hidden; }
}
@keyframes collapsible-close {
from { height: var(--radix-collapsible-content-height); overflow: hidden; }
to { height: 0; overflow: hidden; }
}
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;

View File

@@ -124,6 +124,12 @@ function DashboardContent({ budgetId }: { budgetId: string }) {
const currency = budget.currency const currency = budget.currency
const availableBalance = totalIncome - totalExpenses + budget.carryover_amount const availableBalance = totalIncome - totalExpenses + budget.carryover_amount
const carryover = budget.carryover_amount
const carryoverSubtitle = carryover !== 0
? t("dashboard.carryoverIncludes", { amount: formatCurrency(Math.abs(carryover), currency) })
: undefined
const carryoverIsNegative = carryover < 0
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Summary cards */} {/* Summary cards */}
@@ -139,6 +145,8 @@ function DashboardContent({ budgetId }: { budgetId: string }) {
balance={{ balance={{
value: formatCurrency(availableBalance, currency), value: formatCurrency(availableBalance, currency),
isPositive: availableBalance >= 0, isPositive: availableBalance >= 0,
carryoverSubtitle,
carryoverIsNegative,
}} }}
t={t} t={t}
/> />