Files
SimpleFinanceDash/.planning/phases/05-design-system-token-rework/05-PATTERNS.md

23 KiB

Phase 5: Design System Token Rework - Pattern Map

Mapped: 2026-04-20 Files analyzed: 14 files (1 CSS + 2 chart components + 1 shared component + 9 pages + 1 component) Analogs found: 14 / 14 (all files are modifications of existing code — no new files created)


File Classification

Modified File Role Data Flow Closest Analog / Self Match Quality
src/index.css config (CSS tokens) transform self — no analog needed self
src/components/dashboard/charts/SpendBarChart.tsx component (chart) transform src/components/dashboard/charts/IncomeBarChart.tsx exact
src/components/dashboard/charts/IncomeBarChart.tsx component (chart) transform src/components/dashboard/charts/SpendBarChart.tsx exact
src/components/dashboard/charts/ExpenseDonutChart.tsx component (chart) transform SpendBarChart.tsx / IncomeBarChart.tsx role-match
src/components/shared/PageShell.tsx component (layout) request-response self self
src/components/dashboard/DashboardSkeleton.tsx component (skeleton) request-response CategoriesPage.tsx skeleton block role-match
src/components/dashboard/CategorySection.tsx component (list item) request-response BudgetDetailPage.tsx group heading role-match
src/components/dashboard/charts/ChartEmptyState.tsx component (empty state) request-response self self
src/components/QuickAddPicker.tsx component (picker) request-response BudgetDetailPage.tsx select label dot role-match
src/pages/DashboardPage.tsx page request-response self self
src/pages/BudgetListPage.tsx page request-response DashboardPage.tsx / BudgetDetailPage.tsx role-match
src/pages/BudgetDetailPage.tsx page CRUD CategoriesPage.tsx / TemplatePage.tsx exact
src/pages/TemplatePage.tsx page CRUD BudgetDetailPage.tsx / CategoriesPage.tsx exact
src/pages/CategoriesPage.tsx page CRUD BudgetDetailPage.tsx / TemplatePage.tsx exact
src/pages/QuickAddPage.tsx page CRUD CategoriesPage.tsx role-match
src/pages/SettingsPage.tsx page request-response CategoriesPage.tsx role-match
src/pages/LoginPage.tsx page request-response SettingsPage.tsx role-match
src/pages/RegisterPage.tsx page request-response SettingsPage.tsx role-match

Pattern Assignments

src/index.css (config, CSS tokens)

Analog: self — authoritative token source, no external analog needed.

Current @theme inline block (lines 1-99, full file):

@import "tailwindcss";
@custom-variant dark (&:is(.dark *));

@theme inline {
  --color-background: oklch(0.98 0.005 260);   /* → 0.01 chroma (warm) */
  --color-foreground: oklch(0.25 0.02 260);

  /* ... (base tokens unchanged) ... */

  /* Category fill tokens — RAISE chroma to 0.22+ */
  --color-income-fill: oklch(0.68 0.19 155);   /* → 0.22 */
  --color-bill-fill: oklch(0.65 0.19 25);       /* → 0.22 */
  --color-variable-expense-fill: oklch(0.70 0.18 50); /* → 0.22 */
  --color-debt-fill: oklch(0.60 0.20 355);      /* → 0.23 */
  --color-saving-fill: oklch(0.68 0.18 220);    /* → 0.22 */
  --color-investment-fill: oklch(0.65 0.18 285); /* → 0.22 */

  /* Chart vars — DELETE these 5 lines entirely */
  --color-chart-1: oklch(0.72 0.14 155);
  --color-chart-2: oklch(0.7 0.14 25);
  --color-chart-3: oklch(0.72 0.14 50);
  --color-chart-4: oklch(0.65 0.16 355);
  --color-chart-5: oklch(0.72 0.14 220);

  --radius: 0.625rem;  /* → 0 */
}

@layer base {
  * { @apply border-border; }
  body {
    @apply bg-background text-foreground;
    font-feature-settings: "rlig" 1, "calt" 1;
  }
}

Changes to make in this file:

  1. Line 7: --color-background: oklch(0.98 0.005 260)oklch(0.98 0.01 260)
  2. Lines 65-70: raise all six --color-*-fill chroma values to 0.22+ per CONTEXT.md
  3. Lines 52-57: delete --color-chart-1 through --color-chart-5 entirely
  4. Line 72: --radius: 0.625rem--radius: 0
  5. After @layer base block: add CSS override selectors for Recharts and Sonner (see Shared Patterns section below)

src/components/dashboard/charts/SpendBarChart.tsx (component, transform)

Analog: src/components/dashboard/charts/IncomeBarChart.tsx

Current Bar radius props (lines 64-80):

<Bar
  dataKey="budgeted"
  fill="var(--color-budgeted)"
  radius={4}           /* → radius={0} */
/>
<Bar dataKey="actual" radius={4}>   /* → radius={0} */
  {data.map((entry, index) => (
    <Cell
      key={index}
      fill={
        entry.actual > entry.budgeted
          ? "var(--color-over-budget)"
          : `var(--color-${entry.type}-fill)`  /* already correct — no change */
      }
    />
  ))}
</Bar>

Changes to make in this file:

  • Line 67: radius={4}radius={0}
  • Line 69: radius={4}radius={0}
  • chartConfig (lines 31-34): no change needed — actual already references --color-muted-foreground, budgeted references --color-budget-bar-bg; neither references deleted --color-chart-* vars.

src/components/dashboard/charts/IncomeBarChart.tsx (component, transform)

Analog: src/components/dashboard/charts/SpendBarChart.tsx

Current Bar radius props (lines 54-69):

<Bar
  dataKey="budgeted"
  fill="var(--color-budgeted)"
  radius={[4, 4, 0, 0]}    /* → radius={0} */
/>
<Bar dataKey="actual" radius={[4, 4, 0, 0]}>  /* → radius={0} */
  {data.map((entry, index) => (
    <Cell
      key={index}
      fill={
        entry.actual > entry.budgeted
          ? "var(--color-over-budget)"
          : "var(--color-income-fill)"   /* already correct — no change */
      }
    />
  ))}
</Bar>

Current chartConfig (lines 26-29):

const chartConfig = {
  budgeted: { label: "Budgeted", color: "var(--color-budget-bar-bg)" },
  actual: { label: "Actual", color: "var(--color-income-fill)" },  /* already references fill token */
} satisfies ChartConfig

Changes to make in this file:

  • Line 57: radius={[4, 4, 0, 0]}radius={0}
  • Line 59: radius={[4, 4, 0, 0]}radius={0}
  • chartConfig: no change needed — already references --color-income-fill, not a deleted --color-chart-* var.

src/components/dashboard/charts/ExpenseDonutChart.tsx (component, transform)

Analog: self — already uses var(--color-${entry.type}-fill) pattern throughout.

The one hardcoded rounded-full (line 141):

/* Before */
<span
  className="inline-block size-3 shrink-0 rounded-full"
  style={{ backgroundColor: `var(--color-${entry.type}-fill)` }}
/>

/* After — remove rounded-full entirely */
<span
  className="inline-block size-3 shrink-0"
  style={{ backgroundColor: `var(--color-${entry.type}-fill)` }}
/>

Changes to make in this file:

  • Line 141: remove rounded-full from className string.
  • No ChartConfig changes needed — ExpenseDonutChart builds its config dynamically from data using var(--color-${entry.type}-fill) (lines 47-51), which is already the correct post-rework pattern.

src/components/shared/PageShell.tsx (component, layout)

Current spacing (line 15):

<div className="flex flex-col gap-6">   /* → gap-8 */

Changes to make in this file:

  • Line 15: gap-6gap-8

This single change propagates the header-to-content gap increase to all 9 pages that use PageShell.


src/components/dashboard/DashboardSkeleton.tsx (component, skeleton)

Analog: Skeleton patterns from CategoriesPage.tsx lines 98-114 and BudgetDetailPage.tsx lines 284-306.

Hardcoded rounded-* locations in this file:

/* Line 35 — chart placeholder */
<Skeleton className="h-[250px] w-full rounded-md" />  /* remove rounded-md */

/* Line 43 — chart placeholder */
<Skeleton className="h-[250px] w-full rounded-md" />  /* remove rounded-md */

/* Line 51 — chart placeholder */
<Skeleton className="h-[250px] w-full rounded-md" />  /* remove rounded-md */

/* Line 59 — collapsible section row */
<div className="flex items-center gap-3 rounded-md border-l-4 ...">  /* remove rounded-md */

/* Lines 63-64 — inline summary badges */
<Skeleton className="h-5 w-24 rounded-full" />  /* remove rounded-full — CRITICAL: rounded-full is 9999px, not from --radius */
<Skeleton className="h-5 w-24 rounded-full" />  /* remove rounded-full */

Also: spacing upgrades:

/* Line 20 — top-level container */
<div className="flex flex-col gap-6">   /* → gap-8 */

/* Line 22 — summary cards grid */
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">  /* → gap-6 */

/* Line 29 — chart grid */
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">  /* → gap-8 */

src/components/dashboard/CategorySection.tsx (component, list item)

Analog: Group heading divs in BudgetDetailPage.tsx lines 352-356, CategoriesPage.tsx lines 134-137, TemplatePage.tsx lines 292-295.

The hardcoded rounded-md (line 73):

/* Before */
<button
  className="group flex w-full items-center gap-3 rounded-md border-l-4 bg-card px-4 py-3 text-left hover:bg-muted/40 transition-colors"
  style={{ borderLeftColor: categoryColors[type] }}
>

/* After — remove rounded-md */
<button
  className="group flex w-full items-center gap-3 border-l-4 bg-card px-4 py-3 text-left hover:bg-muted/40 transition-colors"
  style={{ borderLeftColor: categoryColors[type] }}
>

Changes to make in this file:

  • Line 73: remove rounded-md from the button's className.

src/components/dashboard/charts/ChartEmptyState.tsx (component, empty state)

Current rounded-lg (line 12):

/* Before */
className={cn(
  "flex min-h-[250px] w-full items-center justify-center rounded-lg border border-dashed ...",
  className
)}

/* After — remove rounded-lg */
className={cn(
  "flex min-h-[250px] w-full items-center justify-center border border-dashed ...",
  className
)}

Changes to make in this file:

  • Line 12: remove rounded-lg from the className string.

src/components/QuickAddPicker.tsx (component, picker)

Analog: Same pattern as BudgetDetailPage.tsx line 496 and TemplatePage.tsx line 385 — size-2 rounded-full color swatch dots used as SelectLabel decorators.

Hardcoded rounded-* locations:

/* Line 156 — picker item button */
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 ..."
/* → remove rounded-sm */

/* Line 201 — SelectLabel category dot */
<div
  className="size-2 rounded-full"
  style={{ backgroundColor: categoryColors[type] }}
/>
/* → remove rounded-full */

Changes to make in this file:

  • Line 156: remove rounded-sm from className.
  • Line 201: remove rounded-full from className.

src/pages/DashboardPage.tsx (page, request-response)

Current spacing (lines 186, 207):

/* Line 186 — DashboardContent top-level wrapper */
<div className="space-y-6">     /* → space-y-8 */

/* Line 207 — chart grid */
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">   /* → gap-8 */

Changes to make in this file:

  • Line 186: space-y-6space-y-8
  • Line 207: gap-6gap-8

No hardcoded rounded-* classes in this file.


src/pages/BudgetListPage.tsx (page, CRUD)

Analog: BudgetDetailPage.tsx lines 242-256 — same rounded-md border p-3 container pattern.

Hardcoded rounded-md (line 243):

/* Before */
<div className="flex items-center gap-3 rounded-md border p-3">

/* After */
<div className="flex items-center gap-3 border p-3">

Spacing: Check for gap-4, gap-6, space-y-* patterns and apply standard upgrades:

  • gap-4gap-6
  • gap-6gap-8
  • space-y-6space-y-8

src/pages/BudgetDetailPage.tsx (page, CRUD)

Analog: CategoriesPage.tsx and TemplatePage.tsx — structurally identical page pattern (group headings, Tables, PageShell).

Hardcoded rounded-* locations (confirmed from RESEARCH.md inventory):

Line Current Action
290 rounded-sm on skeleton header div Remove
303 rounded-md on <Skeleton> className Remove
353 rounded-sm on budget item group heading div Remove
439 rounded-md on summary totals box Remove
497 rounded-full on SelectLabel category dot Remove

Line 353 group heading (current pattern to modify):

/* Before */
<div
  className="mb-2 flex items-center gap-3 rounded-sm border-l-4 bg-muted/30 px-3 py-2"
  style={{ borderLeftColor: categoryColors[type] }}
>

/* After */
<div
  className="mb-2 flex items-center gap-3 border-l-4 bg-muted/30 px-3 py-2"
  style={{ borderLeftColor: categoryColors[type] }}
>

Line 439 summary box (current pattern to modify):

/* Before */
<div className="rounded-md border p-4">

/* After */
<div className="border p-4">

Spacing upgrades:

  • space-y-8 already present at line 338 (inner grouped content) — verify no space-y-6 / gap-6 remaining
  • Check for p-4 on card content wrappers → p-6

src/pages/TemplatePage.tsx (page, CRUD)

Analog: BudgetDetailPage.tsx — same group heading + Table structure, same skeleton pattern.

Hardcoded rounded-* locations (confirmed from RESEARCH.md inventory):

Line Current Action
250 rounded-sm on skeleton group heading div Remove
256 rounded-full on <Skeleton> className Remove
258 rounded-md on <Skeleton> className Remove
292 rounded-sm on template item group heading div Remove
385 rounded-full on SelectLabel category dot Remove

Line 292 group heading (current):

/* Before */
<div
  className="mb-2 flex items-center gap-3 rounded-sm border-l-4 bg-muted/30 px-3 py-2"
  style={{ borderLeftColor: categoryColors[type] }}
>

Same removal pattern as BudgetDetailPage.tsx line 353.

Line 385 SelectLabel dot (current):

/* Before — same pattern as BudgetDetailPage line 497 */
<div
  className="size-2 rounded-full"
  style={{ backgroundColor: categoryColors[type] }}
/>
/* After: remove rounded-full */

Spacing upgrades (current at lines 247, 268, 287):

  • Line 247: space-y-6space-y-8 (skeleton wrapper)
  • Line 268: gap-6gap-8 (TemplatePage own flex header, not using PageShell)
  • Line 287: space-y-6space-y-8 (main content groups)

src/pages/CategoriesPage.tsx (page, CRUD)

Analog: BudgetDetailPage.tsx / TemplatePage.tsx — structurally identical.

Hardcoded rounded-* locations (confirmed from RESEARCH.md inventory):

Line Current Action
101 rounded-sm on skeleton group heading div Remove
107 rounded-full on <Skeleton> className Remove
108 rounded-md on <Skeleton> className Remove
134 rounded-sm on category group heading div Remove

Line 134 group heading (current):

/* Before */
<div
  className="mb-2 flex items-center gap-3 rounded-sm border-l-4 bg-muted/30 px-3 py-2"
  style={{ borderLeftColor: categoryColors[type] }}
>
/* After: remove rounded-sm */

Spacing upgrades (current at lines 98, 130):

  • Line 98: space-y-6space-y-8 (skeleton wrapper)
  • Line 130: space-y-6space-y-8 (main content groups)

src/pages/QuickAddPage.tsx (page, CRUD)

Analog: CategoriesPage.tsx — same row-list layout, same Skeleton usage.

Hardcoded rounded-* locations (confirmed from RESEARCH.md inventory):

Line Current Action
98 rounded-full on <Skeleton> className Remove
100 rounded-md on <Skeleton> className Remove

Current skeleton block (lines 93-105):

<div className="space-y-1">
  {[1, 2, 3, 4, 5].map((i) => (
    <div key={i} className="flex items-center gap-4 px-4 py-2.5 border-b border-border">
      <Skeleton className="h-5 w-10 rounded-full" />   /* line 98 — remove rounded-full */
      <Skeleton className="h-4 w-36" />
      <Skeleton className="ml-auto h-7 w-7 rounded-md" />  /* line 100 — remove rounded-md */
    </div>
  ))}
</div>

No major spacing changes in this file (list layout, not grid/section-based).


src/pages/SettingsPage.tsx (page, request-response)

Analog: CategoriesPage.tsx — PageShell wrapper, Card with CardContent.

No hardcoded rounded-* classes in this file (Card uses token cascade).

Spacing upgrade (line 87):

/* Before — CardContent internal spacing */
<CardContent className="space-y-4 pt-6">   /* → space-y-6 */

Changes to make in this file:

  • Line 87 (and corresponding skeleton line 69): space-y-4space-y-6 inside CardContent

src/pages/LoginPage.tsx and src/pages/RegisterPage.tsx (pages, request-response)

Analog: SettingsPage.tsx — Card-based page, no group headings.

Per RESEARCH.md: "Card already uses --radius token cascade." No hardcoded rounded-* classes confirmed. No --color-chart-* references.

Minimal changes: Verify p-4p-6 in any card content wrappers if present. No spacing grids to upgrade. Treat as low-touch pages in implementation.


Shared Patterns

Pattern A: @theme inline Token Edit (CSS variable cascade)

Source: src/index.css lines 4-79 Apply to: src/index.css only — single source of truth Rule: All shadcn rounded-* utility classes (rounded-md, rounded-sm, rounded-lg, rounded-xl) derive from --radius. Setting --radius: 0 makes them all 0px automatically. Only rounded-full (9999px) is immune.

/* The pattern: edit token value, cascade propagates everywhere */
@theme inline {
  --radius: 0;  /* single edit cascades to all shadcn components */
}

Pattern B: Hardcoded rounded-full Removal (critical pitfall)

Source: Every page and component file listed in RESEARCH.md inventory Apply to: All 17 rounded-full class occurrences across 8 files Rule: rounded-full = border-radius: 9999px — hardcoded, NOT derived from --radius. Must be removed manually from every occurrence.

/* Before — still circular after --radius: 0 */
<Skeleton className="h-5 w-16 rounded-full" />
<span className="inline-block size-3 shrink-0 rounded-full" ... />
<div className="size-2 rounded-full" ... />

/* After — becomes square from --radius: 0 */
<Skeleton className="h-5 w-16" />
<span className="inline-block size-3 shrink-0" ... />
<div className="size-2" ... />

Pattern C: CSS Override for Third-Party Radius

Source: To be added to src/index.css after @layer base block Apply to: Recharts bars (SpendBarChart, IncomeBarChart) and Sonner toasts Rule: Recharts renders bars as <rect rx="4" ry="4"> SVG — not CSS. Must change radius prop directly AND add CSS override. Sonner wires --border-radius: var(--radius) (confirmed at sonner.tsx line 28) — should auto-propagate, but add CSS override as safety net.

/* Add to src/index.css after the @layer base block */

/* Recharts: SVG rect elements for bar charts */
.recharts-rectangle {
  rx: 0;
  ry: 0;
}

/* Sonner: toast container — safety net if var(--radius) cascade insufficient */
[data-sonner-toast] {
  border-radius: 0 !important;
}

Note: Sonner's sonner.tsx already passes "--border-radius": "var(--radius)" as an inline style (line 28). When --radius: 0, this should cascade automatically. The CSS override is a safety net only — verify in browser after token change before deciding if it is needed.

Pattern D: Group Heading Div (shared across 5 files)

Source: src/pages/CategoriesPage.tsx line 134, BudgetDetailPage.tsx line 353, TemplatePage.tsx line 292 — all structurally identical Apply to: CategoriesPage, BudgetDetailPage, TemplatePage, CategorySection, DashboardSkeleton row

/* Pattern: border-l-4 accent bar heading — remove rounded-sm entirely */
<div
  className="mb-2 flex items-center gap-3 border-l-4 bg-muted/30 px-3 py-2"
  style={{ borderLeftColor: categoryColors[type] }}
>
  <span className="text-sm font-semibold">{label}</span>
</div>

Pattern E: Spacing Upgrade Map

Source: src/pages/DashboardPage.tsx lines 186, 207; src/components/shared/PageShell.tsx line 15 Apply to: All 9 pages and PageShell

PageShell.tsx line 15:     gap-6      → gap-8      (header-to-content gap)
DashboardPage.tsx line 186: space-y-6 → space-y-8  (section rhythm)
DashboardPage.tsx line 207: gap-6     → gap-8      (chart grid)
DashboardSkeleton.tsx line 22: gap-4  → gap-6      (summary cards grid)
DashboardSkeleton.tsx line 29: gap-6  → gap-8      (chart grid skeleton)
CategoriesPage.tsx line 98:  space-y-6 → space-y-8  (skeleton wrapper)
CategoriesPage.tsx line 130: space-y-6 → space-y-8  (main content)
TemplatePage.tsx line 247:   space-y-6 → space-y-8  (skeleton wrapper)
TemplatePage.tsx line 268:   gap-6     → gap-8      (self-managed header flex)
TemplatePage.tsx line 287:   space-y-6 → space-y-8  (main content)
BudgetDetailPage.tsx:        verify no space-y-6/gap-6 remain after existing space-y-8 at line 338
SettingsPage.tsx line 87:    space-y-4 → space-y-6  (CardContent internal)

Pattern F: ChartConfig Token Reference (no --color-chart-*)

Source: src/components/dashboard/charts/IncomeBarChart.tsx lines 26-29 Apply to: Any future chart components — SpendBarChart and IncomeBarChart require no ChartConfig change Rule: After deleting --color-chart-* from index.css, all chart color references must use --color-*-fill tokens.

/* Correct pattern post-rework — reference fill tokens directly */
const chartConfig = {
  budgeted: { label: "Budgeted", color: "var(--color-budget-bar-bg)" },
  actual: { label: "Actual", color: "var(--color-income-fill)" },
} satisfies ChartConfig

No Analog Found

None — all files are modifications of existing code. No net-new files are created in this phase.


Implementation Wave Order

Per RESEARCH.md recommendation (planner reference):

Wave 1 — Token edit only (src/index.css)

  • Change --radius: 0
  • Raise --color-*-fill chroma values to 0.22+
  • Warm --color-background chroma 0.005 → 0.01
  • Delete --color-chart-1 through --color-chart-5
  • Add .recharts-rectangle and [data-sonner-toast] CSS overrides

Wave 2 — Chart component updates (2 files)

  • SpendBarChart.tsx: radius={4}radius={0} (x2 Bar props)
  • IncomeBarChart.tsx: radius={[4, 4, 0, 0]}radius={0} (x2 Bar props)
  • ExpenseDonutChart.tsx: remove rounded-full from legend span (line 141)

Wave 3 — Page and shared component sweep (12 files)

  • PageShell.tsx: gap-6gap-8
  • DashboardSkeleton.tsx: remove 5 rounded-* classes, upgrade 3 spacing values
  • CategorySection.tsx: remove rounded-md from trigger button
  • ChartEmptyState.tsx: remove rounded-lg from empty state div
  • QuickAddPicker.tsx: remove 2 rounded-* classes
  • All 9 pages: spacing upgrades + hardcoded rounded-* removals per inventory

Metadata

Analog search scope: src/pages/, src/components/dashboard/, src/components/shared/, src/components/ui/, src/index.css Files read for pattern extraction: 16 (index.css, PageShell, DashboardPage, DashboardSkeleton, SpendBarChart, IncomeBarChart, ExpenseDonutChart, ChartEmptyState, CategorySection, QuickAddPicker, SettingsPage, BudgetDetailPage excerpt x3, CategoriesPage excerpt, TemplatePage excerpt, QuickAddPage excerpt, BudgetListPage excerpt, sonner.tsx) Pattern extraction date: 2026-04-20