docs(01): research phase domain — design foundation and primitives
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user