31 KiB
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.
// 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-tightmatches the existingDashboardPageheading styleactionis aReactNodeslot, not a button-specific prop -- allows any CTA element- No
paddingbaked in -- the<main>inAppLayout.tsxalready appliesp-6 - The existing
DashboardPageheader (<div className="mb-6 flex items-center justify-between">) is replaced byPageShellusage
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.
// 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
SummaryCardpattern fromDashboardPage.tsx(lines 45-66) - Adds
varianceprop for delta arrows/badges (differentiator from FEATURES.md) - Uses
text-2xl font-bold(upgraded from existingfont-semibold) for more visual weight tabular-nums tracking-tightensures 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).
// 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-1on mobile,sm:grid-cols-2on tablet,lg:grid-cols-3on desktop - Balance card uses semantic token classes
text-on-budget/text-over-budget(not hardcodedtext-green-600/text-red-600) - Income card uses
text-income(maps to--color-incomeCSS 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.
// 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
Skeletonfromcomponents/ui/skeleton.tsx(already installed) - Card structure matches the real
StatCardlayout 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). Thepalette.tsfile maps CategoryType tovar(--color-X). - Using
text-green-600/text-red-600for budget status: Replace with semantic tokens--color-on-budgetand--color-over-budgetthat 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, andi18n/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.jsonandde.jsonin the same commit. The i18next config usesfallbackLng: '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:
- Run every proposed color pair through OddContrast (oddcontrast.com) using OKLCH input
- For text colors, target at minimum 4.5:1 contrast ratio against
--color-card(oklch(1 0 0) = white) - For non-text UI elements (chart slices, progress bars), target 3:1 minimum (WCAG 2.1 SC 1.4.11)
- Vary OKLCH lightness across categories (range 0.55-0.75), not just hue
- 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.
/* 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:
// 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
// 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:
{
"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:
SummaryCardinDashboardPage.tsx(lines 45-66): Replaced byStatCardwith variance support- Hardcoded
text-green-600 dark:text-green-400/text-red-600 dark:text-red-400patterns: Replace withtext-on-budget/text-over-budgetsemantic classes - Returning
nullduring loading states (DashboardPage.tsxline 76, 291): Replace withDashboardSkeleton
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
buildcommand (tsc -b && vite build) serves as the primary automated validation: it catches type errors, missing imports, and bundling failures.
Open Questions
-
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.
-
Whether
chart.tsxpatch 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 ifinitialDimensionis already present. If so, skip the manual patch. If not, apply it.
-
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-incomefor text,--color-income-fillfor fills). If the visual delta is negligible after WCAG verification, collapse to single tokens.
Sources
Primary (HIGH confidence)
- Tailwind CSS v4 Theme Docs --
@theme inline, CSS variable scoping - shadcn/ui Chart Docs -- ChartContainer, ChartConfig, ChartTooltip
- Radix UI Collapsible --
--radix-collapsible-content-heightanimation - WCAG 2.1 SC 1.4.3 Contrast Minimum -- 4.5:1 for text
- WCAG 2.1 SC 1.4.11 Non-text Contrast -- 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 -- Community-verified
initialDimensionfix for Recharts v3 - shadcn-ui/ui PR #8486 -- Official Recharts v3 chart.tsx upgrade (open as of March 2026)
- Recharts V3 with shadcn/ui -- noxify gist -- WIP implementation reference
- OddContrast -- OKLCH-native WCAG contrast checker
- Atmos Contrast Checker -- OKLCH + APCA contrast tool
Tertiary (LOW confidence)
- Design Tokens That Scale in 2026 (Tailwind v4 + CSS Variables) -- 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)