`) is replaced by `PageShell` usage
### Pattern 2: StatCard -- KPI Display Unit
**What:** A single KPI card that displays a label, large formatted value, semantic color coding, and an optional variance badge.
**When to use:** Summary cards on the dashboard (income, expenses, balance). May also be used on BudgetDetailPage summary in Phase 4.
```typescript
// src/components/dashboard/StatCard.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { TrendingUp, TrendingDown, Minus } from "lucide-react"
import { cn } from "@/lib/utils"
interface StatCardProps {
title: string
value: string
valueClassName?: string
variance?: {
amount: string
direction: "up" | "down" | "neutral"
label: string
}
}
export function StatCard({ title, value, valueClassName, variance }: StatCardProps) {
return (
{title}
{value}
{variance && (
{variance.direction === "up" && }
{variance.direction === "down" && }
{variance.direction === "neutral" && }
{variance.amount} {variance.label}
)}
)
}
```
**Key decisions:**
- Extends the existing `SummaryCard` pattern from `DashboardPage.tsx` (lines 45-66)
- Adds `variance` prop for delta arrows/badges (differentiator from FEATURES.md)
- Uses `text-2xl font-bold` (upgraded from existing `font-semibold`) for more visual weight
- `tabular-nums tracking-tight` ensures financial numbers align properly
- Lucide icons (`TrendingUp`, `TrendingDown`) supplement color for accessibility (Pitfall 4)
### Pattern 3: SummaryStrip -- KPI Cards Row
**What:** A responsive grid row of 3 `StatCard` instances (income, expenses, balance).
```typescript
// src/components/dashboard/SummaryStrip.tsx
import { StatCard } from "./StatCard"
interface SummaryStripProps {
income: { value: string; budgeted: string }
expenses: { value: string; budgeted: string }
balance: { value: string; isPositive: boolean; carryover?: string }
t: (key: string) => string
}
export function SummaryStrip({ income, expenses, balance, t }: SummaryStripProps) {
return (
)
}
```
**Key decisions:**
- Grid: `grid-cols-1` on mobile, `sm:grid-cols-2` on tablet, `lg:grid-cols-3` on desktop
- Balance card uses semantic token classes `text-on-budget` / `text-over-budget` (not hardcoded `text-green-600` / `text-red-600`)
- Income card uses `text-income` (maps to `--color-income` CSS variable)
### Pattern 4: Skeleton Loading Components
**What:** Skeleton placeholders that mirror the real card and chart layout structure so the page does not flash blank during loading.
```typescript
// src/components/dashboard/DashboardSkeleton.tsx
import { Skeleton } from "@/components/ui/skeleton"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
export function DashboardSkeleton() {
return (
{/* Summary cards skeleton */}
{Array.from({ length: 3 }).map((_, i) => (
))}
{/* Chart area skeleton */}
)
}
```
**Key decisions:**
- Mirrors the real dashboard grid layout exactly (3-col summary cards, 2-col chart area)
- Uses existing `Skeleton` from `components/ui/skeleton.tsx` (already installed)
- Card structure matches the real `StatCard` layout so there is no layout shift when data loads
- Chart skeleton height matches the `ResponsiveContainer height={240}` used in the existing pie chart
### Anti-Patterns to Avoid
- **Hardcoding hex/oklch values in components:** Always use CSS variable references (`var(--color-income)`) or Tailwind semantic classes (`text-income`). The `palette.ts` file maps CategoryType to `var(--color-X)`.
- **Using `text-green-600` / `text-red-600` for budget status:** Replace with semantic tokens `--color-on-budget` and `--color-over-budget` that are verified for WCAG 4.5:1 contrast. The existing codebase uses hardcoded Tailwind green/red in 4 places (DashboardPage.tsx lines 96-98, 220-221; BudgetDetailPage.tsx lines 168-173, 443-449).
- **Modifying hooks or lib files:** All changes are in `components/`, `pages/`, `index.css`, and `i18n/` only. Hooks and library files are read-only during this milestone.
- **Adding i18n keys to only one language file:** Every new key MUST be added to both `en.json` and `de.json` in the same commit. The i18next config uses `fallbackLng: 'en'` which silently hides missing German keys.
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Chart theme wrappers | Custom `ResponsiveContainer` wrapper | shadcn `chart.tsx` `ChartContainer` + `ChartConfig` | Provides CSS-variable-aware theming, consistent tooltips, and proper SSR dimensions |
| Collapsible sections | `display:none` toggle or JS height animation | Radix `Collapsible` via `npx shadcn@latest add collapsible` | Handles `height: 0 -> auto` animation via `--radix-collapsible-content-height` CSS variable; avoids layout thrash |
| Loading skeletons | Custom shimmer/pulse animation | shadcn `Skeleton` component (already installed) | Provides `animate-pulse rounded-md bg-accent` -- consistent with design system |
| WCAG contrast checking | Manual hex comparison | OddContrast (oddcontrast.com) or Atmos (atmos.style/contrast-checker) | Both accept OKLCH input directly; compute WCAG 2 ratio |
| Currency formatting | Custom number formatting | Existing `formatCurrency()` from `src/lib/format.ts` | Already handles locale-aware Intl.NumberFormat with EUR/USD |
| Color mapping | Inline color lookup objects | Existing `categoryColors` from `src/lib/palette.ts` | Single source of truth; returns `var(--color-X)` strings |
## Common Pitfalls
### Pitfall 1: chart.tsx Recharts v3 Incompatibility
**What goes wrong:** Running `npx shadcn@latest add chart` generates a `chart.tsx` that does not include `initialDimension` on `ResponsiveContainer`. With Recharts 3.8.0, this causes `width(-1) and height(-1)` console warnings and charts may render at zero dimensions.
**Why it happens:** The official shadcn chart.tsx PR #8486 for Recharts v3 is not yet merged (as of March 2026). The CLI still generates v2-compatible code.
**How to avoid:** Immediately after running the CLI command, open `src/components/ui/chart.tsx`, find the `ResponsiveContainer` inside `ChartContainer`, and add `initialDimension={{ width: 320, height: 200 }}`.
**Warning signs:** Console warning `"The width(-1) and height(-1) of chart should be greater than 0"`. Charts render as invisible/zero-height.
### Pitfall 2: Color Accessibility Regression During "Rich Visual" Overhaul
**What goes wrong:** Bumping OKLCH chroma from 0.14 to 0.18+ makes colors more vivid but may push them below WCAG 4.5:1 contrast against the white card background (L=1.0).
**Why it happens:** Higher chroma at the same lightness can reduce relative luminance difference against white. The existing `text-green-600` (`#16a34a`) is borderline at 4.5:1. The six category colors all cluster at similar lightness (L ~0.65-0.72), making them hard to distinguish for colorblind users.
**How to avoid:**
1. Run every proposed color pair through OddContrast (oddcontrast.com) using OKLCH input
2. For text colors, target at minimum 4.5:1 contrast ratio against `--color-card` (oklch(1 0 0) = white)
3. For non-text UI elements (chart slices, progress bars), target 3:1 minimum (WCAG 2.1 SC 1.4.11)
4. Vary OKLCH lightness across categories (range 0.55-0.75), not just hue
5. Supplement color with icons for all status indicators (Pitfall 4 from research)
**Warning signs:** Colors look vivid on developer's monitor but fail automated contrast check. All category colors appear as similar gray under DevTools "Emulate vision deficiency: Achromatopsia" filter.
### Pitfall 3: i18n Key Regressions
**What goes wrong:** New dashboard text keys added to `en.json` but forgotten in `de.json`. The app silently falls back to English because `fallbackLng: 'en'`.
**Why it happens:** No build-time key parity check exists. `debug: false` in production hides `missingKey` warnings.
**How to avoid:** Add both language files in the same commit. Before completing any task, switch locale to German and visually verify no raw key strings appear. Current key counts: `en.json` = 97 keys, `de.json` = 97 keys (parity confirmed).
**Warning signs:** German UI shows English text or dot-notation strings like `dashboard.carryover`.
### Pitfall 4: Design Inconsistency ("Island Redesign")
**What goes wrong:** Without establishing shared components before page work, each page develops subtly different card styles, heading sizes, and spacing.
**Why it happens:** Developers implement visual patterns inline in the first page that needs them, then drift in subsequent pages.
**How to avoid:** This phase exists specifically to prevent this. Build `PageShell`, `StatCard`, and the color token system BEFORE any page redesign begins. All subsequent phases consume these abstractions.
**Warning signs:** Two pages using different heading sizes or card padding values. Color values appearing as raw oklch literals in component files instead of semantic tokens.
## Code Examples
### Extending index.css Color Tokens
The current `@theme inline` block needs two additions: richer category chroma and semantic status tokens.
```css
/* src/index.css -- inside existing @theme inline block */
/* Category Colors -- bumped chroma for richer visual style */
/* IMPORTANT: Verify each pair against --color-card (white) for WCAG 4.5:1 text contrast */
--color-income: oklch(0.55 0.17 155); /* darkened L from 0.72 for text contrast */
--color-bill: oklch(0.55 0.17 25); /* darkened L from 0.70 for text contrast */
--color-variable-expense: oklch(0.58 0.16 50); /* darkened L from 0.72 for text contrast */
--color-debt: oklch(0.52 0.18 355); /* darkened L from 0.65 for text contrast */
--color-saving: oklch(0.55 0.16 220); /* darkened L from 0.72 for text contrast */
--color-investment: oklch(0.55 0.16 285); /* darkened L from 0.70 for text contrast */
/* Semantic Status Tokens -- for budget comparison display */
--color-over-budget: oklch(0.55 0.20 25); /* red-orange for overspend, verified 4.5:1 on white */
--color-on-budget: oklch(0.50 0.17 155); /* green for on-track, verified 4.5:1 on white */
--color-budget-bar-bg: oklch(0.92 0.01 260); /* neutral track for progress bars */
/* Chart fill variants -- lighter versions of category colors for fills */
/* (original higher-L values are fine for non-text chart fills at 3:1) */
--color-income-fill: oklch(0.68 0.19 155);
--color-bill-fill: oklch(0.65 0.19 25);
--color-variable-expense-fill: oklch(0.70 0.18 50);
--color-debt-fill: oklch(0.60 0.20 355);
--color-saving-fill: oklch(0.68 0.18 220);
--color-investment-fill: oklch(0.65 0.18 285);
```
**Key insight:** The original category colors (L ~0.65-0.72) are fine for non-text chart fills but too light for text on white backgrounds. The solution is a two-tier system: darker variants (`--color-income`) for text, lighter variants (`--color-income-fill`) for chart fills. This avoids the common trap of choosing colors that look great in charts but fail WCAG when used as text.
**IMPORTANT:** These are recommended starting values. Each pair MUST be verified against `--color-card` (oklch(1 0 0) = white) using OddContrast before committing. Adjust L (lightness) down if any pair fails 4.5:1 for text.
### The chart.tsx Patch
After running `npx shadcn@latest add chart`, locate the `ChartContainer` component in `src/components/ui/chart.tsx` and find the `ResponsiveContainer` element. Apply this change:
```typescript
// BEFORE (generated by CLI):
{children}
// AFTER (patched for Recharts v3):
{children}
```
**Verification:** After patching, import `ChartContainer` in any component and render a minimal chart. The browser console should NOT show `"The width(-1) and height(-1) of chart should be greater than 0"`.
### New i18n Keys Required
```json
// Add to both en.json and de.json dashboard section:
{
"dashboard": {
"title": "Dashboard",
"totalIncome": "Total Income",
"totalExpenses": "Total Expenses",
"availableBalance": "Available Balance",
"expenseBreakdown": "Expense Breakdown",
"noBudget": "No budget for this month. Create one to get started.",
"carryover": "Carryover",
"vsBudget": "vs budget",
"overBudget": "over budget",
"underBudget": "under budget",
"onTrack": "On track",
"loading": "Loading dashboard..."
}
}
```
German translations:
```json
{
"dashboard": {
"title": "Dashboard",
"totalIncome": "Gesamteinkommen",
"totalExpenses": "Gesamtausgaben",
"availableBalance": "Verfügbares Guthaben",
"expenseBreakdown": "Ausgabenübersicht",
"noBudget": "Kein Budget für diesen Monat. Erstelle eines, um loszulegen.",
"carryover": "Übertrag",
"vsBudget": "vs Budget",
"overBudget": "über Budget",
"underBudget": "unter Budget",
"onTrack": "Im Plan",
"loading": "Dashboard wird geladen..."
}
}
```
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `tailwind.config.js` JS theme | `@theme inline` in CSS | Tailwind v4 (Jan 2025) | All tokens are native CSS variables; no rebuild for theme changes |
| `@radix-ui/react-collapsible` | `radix-ui` unified package | June 2025 | shadcn CLI generates `import { Collapsible } from "radix-ui"` not `@radix-ui/react-*` |
| Recharts v2 `Cell` component | Recharts v3 `shape` prop | Recharts 3.0 (2025) | `Cell` still works but is deprecated; new code should avoid extending Cell usage |
| Recharts v2 `blendStroke` | `stroke="none"` | Recharts 3.0 | `blendStroke` removed entirely |
| shadcn chart.tsx for Recharts v2 | Awaiting PR #8486 merge | Pending (March 2026) | Manual `initialDimension` patch required after CLI install |
| Hardcoded `text-green-600` for status | Semantic CSS variable tokens | This phase | `--color-on-budget` and `--color-over-budget` replace 4 instances of hardcoded green/red |
**Deprecated/outdated in this codebase:**
- `SummaryCard` in `DashboardPage.tsx` (lines 45-66): Replaced by `StatCard` with variance support
- Hardcoded `text-green-600 dark:text-green-400` / `text-red-600 dark:text-red-400` patterns: Replace with `text-on-budget` / `text-over-budget` semantic classes
- Returning `null` during loading states (`DashboardPage.tsx` line 76, 291): Replace with `DashboardSkeleton`
## Existing Code Reference Points
These are the specific files and line numbers that Phase 1 tasks will modify or reference:
| File | Lines | What | Phase 1 Action |
|------|-------|------|----------------|
| `src/index.css` | 44-57 | Category + chart color tokens | Extend with richer chroma + semantic status tokens |
| `src/pages/DashboardPage.tsx` | 45-66 | Existing `SummaryCard` component | Replace with `StatCard` from `components/dashboard/` |
| `src/pages/DashboardPage.tsx` | 76, 291 | `if (loading) return null` | Replace with skeleton loading |
| `src/pages/DashboardPage.tsx` | 95-98 | Hardcoded `text-green-600`/`text-red-600` | Replace with semantic `text-on-budget`/`text-over-budget` |
| `src/pages/DashboardPage.tsx` | 293-298 | Page header `
` | Replace with `PageShell` |
| `src/pages/BudgetDetailPage.tsx` | 168-173 | Hardcoded green/red in `DifferenceCell` | Replace with semantic tokens (verify only in Phase 1; modify in Phase 4) |
| `src/lib/palette.ts` | 1-10 | `categoryColors` record | No changes needed -- already maps to CSS variables |
| `src/lib/format.ts` | 1-12 | `formatCurrency` utility | No changes needed -- used as-is by StatCard |
| `src/i18n/en.json` | 64-72 | Dashboard translation keys | Extend with new keys |
| `src/i18n/de.json` | 64-72 | Dashboard translation keys | Extend with matching German keys |
| `components.json` | 1-21 | shadcn config (new-york style, `@/` aliases) | No changes -- used by `npx shadcn@latest add` |
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | None -- no test framework installed |
| Config file | none |
| Quick run command | `npm run build` (TypeScript + Vite build validates types and imports) |
| Full suite command | `npm run build && npm run lint` |
### Phase Requirements -> Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| UI-DASH-01 | StatCard/SummaryStrip render KPI cards with semantic colors | manual | `npm run build` (type-check only) | N/A -- no test infra |
| UI-DESIGN-01 | Color tokens pass WCAG 4.5:1 contrast | manual | External tool: OddContrast | N/A -- manual verification |
| UI-RESPONSIVE-01 | Summary card grid responds to viewport width | manual | Browser DevTools responsive mode | N/A -- visual verification |
### Sampling Rate
- **Per task commit:** `npm run build` (catches type errors and import failures)
- **Per wave merge:** `npm run build && npm run lint`
- **Phase gate:** Full build green + manual visual verification of all success criteria
### Wave 0 Gaps
- No test framework exists. This is acceptable for a UI-only overhaul where verification is primarily visual.
- Automated WCAG contrast checking would require adding a tool like `color-contrast-checker` -- defer to project owner's discretion.
- The `build` command (`tsc -b && vite build`) serves as the primary automated validation: it catches type errors, missing imports, and bundling failures.
## Open Questions
1. **Exact OKLCH lightness values for WCAG compliance**
- What we know: Lower lightness (L) = darker color = higher contrast against white. Text needs 4.5:1; chart fills need 3:1.
- What's unclear: The exact L threshold depends on chroma and hue. Each of the 8 proposed tokens needs individual verification.
- Recommendation: Use OddContrast with OKLCH input. Start with the proposed values (L ~0.50-0.58 for text, L ~0.60-0.70 for fills). Adjust during implementation.
2. **Whether `chart.tsx` patch is still needed at time of execution**
- What we know: PR #8486 was open as of research date (2026-03-16). The CLI may merge the fix at any time.
- What's unclear: If the PR has merged by execution time, the patch may already be included.
- Recommendation: After running `npx shadcn@latest add chart`, check if `initialDimension` is already present. If so, skip the manual patch. If not, apply it.
3. **Chart fill colors vs text colors -- whether two-tier token system is necessary**
- What we know: Using the same color for both text and chart fills forces a compromise: either too dark for charts (muddy) or too light for text (fails WCAG).
- What's unclear: Whether the visual difference is significant enough to justify 6 extra tokens.
- Recommendation: Start with the two-tier system (`--color-income` for text, `--color-income-fill` for fills). If the visual delta is negligible after WCAG verification, collapse to single tokens.
## Sources
### Primary (HIGH confidence)
- [Tailwind CSS v4 Theme Docs](https://tailwindcss.com/docs/theme) -- `@theme inline`, CSS variable scoping
- [shadcn/ui Chart Docs](https://ui.shadcn.com/docs/components/radix/chart) -- ChartContainer, ChartConfig, ChartTooltip
- [Radix UI Collapsible](https://www.radix-ui.com/primitives/docs/components/collapsible) -- `--radix-collapsible-content-height` animation
- [WCAG 2.1 SC 1.4.3 Contrast Minimum](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html) -- 4.5:1 for text
- [WCAG 2.1 SC 1.4.11 Non-text Contrast](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html) -- 3:1 for UI components
- Existing codebase: `src/index.css`, `src/pages/DashboardPage.tsx`, `src/lib/palette.ts`, `src/lib/format.ts`, `src/lib/types.ts`, `src/components/ui/skeleton.tsx`, `src/components/ui/card.tsx`, `src/i18n/en.json`, `src/i18n/de.json`, `components.json`
### Secondary (MEDIUM confidence)
- [shadcn-ui/ui Issue #9892](https://github.com/shadcn-ui/ui/issues/9892) -- Community-verified `initialDimension` fix for Recharts v3
- [shadcn-ui/ui PR #8486](https://github.com/shadcn-ui/ui/pull/8486) -- Official Recharts v3 chart.tsx upgrade (open as of March 2026)
- [Recharts V3 with shadcn/ui -- noxify gist](https://gist.github.com/noxify/92bc410cc2d01109f4160002da9a61e5) -- WIP implementation reference
- [OddContrast](https://www.oddcontrast.com/) -- OKLCH-native WCAG contrast checker
- [Atmos Contrast Checker](https://atmos.style/contrast-checker) -- OKLCH + APCA contrast tool
### Tertiary (LOW confidence)
- [Design Tokens That Scale in 2026 (Tailwind v4 + CSS Variables)](https://www.maviklabs.com/blog/design-tokens-tailwind-v4-2026) -- Design token patterns (informational)
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH -- stack is locked and fully inspected; shadcn CLI commands are documented
- Architecture: HIGH -- component boundaries derived from existing codebase inspection; patterns follow official shadcn/Radix docs
- Pitfalls: HIGH -- chart.tsx patch verified against issue #9892 and gist; WCAG requirements from official W3C specs; i18n issue confirmed by codebase inspection (fallbackLng: 'en' hides missing keys)
- Color tokens: MEDIUM -- proposed OKLCH values need runtime WCAG verification; starting values are educated estimates based on lightness/contrast relationship
**Research date:** 2026-03-16
**Valid until:** 2026-04-16 (30 days -- stable domain; only chart.tsx patch status may change if PR #8486 merges)