Files

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

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-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.

// 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).

// 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.

// 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.

/* 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:

  • 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)

Secondary (MEDIUM confidence)

Tertiary (LOW confidence)

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)