docs(01): research phase domain — design foundation and primitives

This commit is contained in:
2026-03-16 12:01:28 +01:00
parent 4387795947
commit 952d250b38

View File

@@ -0,0 +1,548 @@
# Phase 1: Design Foundation and Primitives - Research
**Researched:** 2026-03-16
**Domain:** Design system tokens (OKLCH/CSS variables), shadcn/ui primitives, shared React components
**Confidence:** HIGH
## Summary
Phase 1 establishes the design system building blocks that every subsequent phase consumes. The work breaks into four domains: (1) installing shadcn/ui primitives (`chart` and `collapsible`) with the known Recharts v3 compatibility patch, (2) extending the existing OKLCH color token system in `index.css` with richer category chroma and semantic status tokens, (3) building two shared components (`PageShell` for consistent page headers and `StatCard`/`SummaryStrip` for KPI cards), and (4) creating skeleton loading components that mirror the final dashboard layout.
The existing codebase already has a well-structured `@theme inline` block in `index.css` with six category colors and five chart colors, a `palette.ts` mapping those CSS variables to a TypeScript record, and a `formatCurrency` utility. The current `DashboardPage.tsx` contains a simple `SummaryCard` component and an unmemoized `DashboardContent` function that this phase will partially replace. The shadcn/ui `skeleton.tsx` primitive already exists in `components/ui/`.
The highest-risk item is the `chart.tsx` Recharts v3 patch. The generated `chart.tsx` from `npx shadcn@latest add chart` requires adding `initialDimension={{ width: 320, height: 200 }}` to the `ResponsiveContainer` inside `ChartContainer`. Without this, all charts will produce `width(-1) and height(-1)` console warnings and may render at zero dimensions. The patch is documented in shadcn-ui/ui issue #9892 and is a one-line fix.
**Primary recommendation:** Install primitives first, patch chart.tsx immediately, then extend tokens, then build shared components, then skeletons. This order ensures each layer is available before the next layer depends on it.
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|-----------------|
| UI-DASH-01 | Redesign dashboard with hybrid layout -- summary cards, charts, and collapsible category sections | This phase delivers the summary cards layer (StatCard/SummaryStrip) and installs the chart and collapsible primitives that Phase 2 and 3 will consume. The existing `SummaryCard` in DashboardPage.tsx is replaced with a richer `StatCard` component with semantic color coding and variance badges. |
| UI-DESIGN-01 | Redesign all pages with rich, colorful visual style -- consistent design language | This phase delivers the design foundation: extended OKLCH color tokens with richer chroma (0.18+ vs current 0.14), semantic status tokens (`--color-over-budget`, `--color-on-budget`), and `PageShell` -- the shared component that enforces consistent page headers across all 9 pages. Without this phase, design drift (Pitfall 6) is guaranteed. |
| UI-RESPONSIVE-01 | Desktop-first responsive layout across all pages | This phase sets the responsive grid patterns for summary cards (`grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`) and establishes `PageShell` with responsive padding and header layout. All subsequent phases inherit these breakpoints. |
</phase_requirements>
## Standard Stack
### Core (Already Installed -- No New Packages)
| Library | Version | Purpose | Status |
|---------|---------|---------|--------|
| React | 19.2.4 | UI framework | Locked |
| Tailwind CSS | 4.2.1 | Styling via `@theme inline` tokens | Locked |
| Recharts | 3.8.0 | Charts (consumed by Phase 2, but `chart.tsx` wrapper installed here) | Locked |
| radix-ui | 1.4.3 | Primitives (Collapsible, Accordion) | Locked |
| Lucide React | 0.577.0 | Icons (TrendingUp, TrendingDown, ChevronDown) | Locked |
| shadcn/ui | new-york style | UI component library (Card, Badge, Skeleton, etc.) | Locked |
### shadcn/ui Primitives to Add (Phase 1 Deliverables)
| Component | Install Command | Purpose | Post-Install Action |
|-----------|----------------|---------|---------------------|
| `chart` | `npx shadcn@latest add chart` | `ChartContainer`, `ChartTooltip`, `ChartTooltipContent` wrappers | **CRITICAL:** Patch `chart.tsx` -- add `initialDimension={{ width: 320, height: 200 }}` to `ResponsiveContainer` |
| `collapsible` | `npx shadcn@latest add collapsible` | Radix `Collapsible` primitive for Phase 3 category sections | None -- install and verify import works |
### What NOT to Add
| Avoid | Why |
|-------|-----|
| `accordion` | Research initially suggested it, but `Collapsible` gives independent per-section state without fighting Accordion's root-state coordination. Use individual `Collapsible` per `CategorySection`. |
| Framer Motion | CSS transitions via `transition-all duration-200` cover all needed animations. No bundle weight added. |
| Any new npm package | Stack is locked. All additions are shadcn CLI-generated component files, not npm dependencies. |
## Architecture Patterns
### Recommended Project Structure (Phase 1 Additions)
```
src/
components/
ui/
chart.tsx # ADD via shadcn CLI + apply initialDimension patch
collapsible.tsx # ADD via shadcn CLI
skeleton.tsx # EXISTS -- already installed
card.tsx # EXISTS -- used by StatCard
badge.tsx # EXISTS -- used for variance badges
dashboard/ # ADD -- dashboard-specific view components
StatCard.tsx # KPI card with semantic color, value, label, variance badge
SummaryStrip.tsx # Row of 3 StatCards (income, expenses, balance)
DashboardSkeleton.tsx # Skeleton loading for cards + chart placeholders
shared/ # ADD -- cross-page reusable components
PageShell.tsx # Consistent page header with title, description, CTA slot
index.css # MODIFY -- extend @theme inline with richer tokens
i18n/
en.json # MODIFY -- add new dashboard keys
de.json # MODIFY -- add new dashboard keys (same commit)
```
### Pattern 1: PageShell -- Consistent Page Header
**What:** A wrapper component that enforces consistent heading size, spacing, optional description, and CTA slot across all pages.
**When to use:** Every page in the app wraps its top section in `PageShell`.
```typescript
// src/components/shared/PageShell.tsx
interface PageShellProps {
title: string
description?: string
action?: React.ReactNode
children: React.ReactNode
}
export function PageShell({ title, description, action, children }: PageShellProps) {
return (
<div className="flex flex-col gap-6">
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
{description && (
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
)}
</div>
{action && <div className="shrink-0">{action}</div>}
</div>
{children}
</div>
)
}
```
**Key decisions:**
- `text-2xl font-semibold tracking-tight` matches the existing `DashboardPage` heading style
- `action` is a `ReactNode` slot, not a button-specific prop -- allows any CTA element
- No `padding` baked in -- the `<main>` in `AppLayout.tsx` already applies `p-6`
- The existing `DashboardPage` header (`<div className="mb-6 flex items-center justify-between">`) 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 (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
</CardHeader>
<CardContent>
<p className={cn("text-2xl font-bold tabular-nums tracking-tight", valueClassName)}>
{value}
</p>
{variance && (
<div className="mt-1 flex items-center gap-1">
{variance.direction === "up" && <TrendingUp className="size-3" />}
{variance.direction === "down" && <TrendingDown className="size-3" />}
{variance.direction === "neutral" && <Minus className="size-3" />}
<span className="text-xs text-muted-foreground">
{variance.amount} {variance.label}
</span>
</div>
)}
</CardContent>
</Card>
)
}
```
**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 (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<StatCard
title={t("dashboard.totalIncome")}
value={income.value}
valueClassName="text-income"
variance={{
amount: income.budgeted,
direction: "neutral",
label: t("budgets.budgeted"),
}}
/>
<StatCard
title={t("dashboard.totalExpenses")}
value={expenses.value}
valueClassName="text-destructive"
variance={{
amount: expenses.budgeted,
direction: "neutral",
label: t("budgets.budgeted"),
}}
/>
<StatCard
title={t("dashboard.availableBalance")}
value={balance.value}
valueClassName={balance.isPositive ? "text-on-budget" : "text-over-budget"}
/>
</div>
)
}
```
**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 (
<div className="flex flex-col gap-6">
{/* Summary cards skeleton */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 3 }).map((_, i) => (
<Card key={i}>
<CardHeader className="pb-2">
<Skeleton className="h-4 w-24" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-32" />
<Skeleton className="mt-2 h-3 w-20" />
</CardContent>
</Card>
))}
</div>
{/* Chart area skeleton */}
<div className="grid gap-6 lg:grid-cols-2">
<Card>
<CardHeader>
<Skeleton className="h-5 w-40" />
</CardHeader>
<CardContent>
<Skeleton className="h-[240px] w-full rounded-md" />
</CardContent>
</Card>
<Card>
<CardHeader>
<Skeleton className="h-5 w-40" />
</CardHeader>
<CardContent>
<Skeleton className="h-[240px] w-full rounded-md" />
</CardContent>
</Card>
</div>
</div>
)
}
```
**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):
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
// AFTER (patched for Recharts v3):
<RechartsPrimitive.ResponsiveContainer
initialDimension={{ width: 320, height: 200 }}
>
{children}
</RechartsPrimitive.ResponsiveContainer>
```
**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 `<h1>` | 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)