Files
SimpleFinanceDash/.planning/research/ARCHITECTURE.md

16 KiB

Architecture Patterns: Design System

Domain: shadcn/ui + Tailwind CSS 4 design system for personal finance dashboard Researched: 2026-03-11 Confidence: HIGH (based on direct codebase inspection + framework documentation patterns)


The goal is a pastel design system layered on top of shadcn/ui without replacing it. The architecture has three tiers:

  1. Token layer — CSS custom properties in index.css (already exists, needs pastel values)
  2. Variant layer — CVA-based component variants in components/ui/ (shadcn files, lightly patched)
  3. Composition layer — Feature components in components/ that assemble ui primitives with domain semantics
frontend/src/
  index.css                     ← Token layer: ALL CSS variables live here
  lib/
    utils.ts                    ← cn() helper (already exists)
  components/
    ui/                         ← Variant layer: shadcn primitives (owned, patchable)
      button.tsx
      card.tsx
      badge.tsx
      ...
    category-badge.tsx          ← Composition layer: domain-specific wrapper
    stat-card.tsx               ← Composition layer: reusable financial card pattern
    progress-bar.tsx            ← Composition layer: budget vs actual bar
    page-header.tsx             ← Composition layer: consistent page headers
  pages/                        ← Page layer: route views that compose components

Component Boundaries

Component Responsibility Communicates With
index.css All CSS variables and @theme inline mappings Tailwind engine only
components/ui/* Headless + styled primitives (shadcn-owned) Tailwind classes, CVA variants
components/*.tsx Domain-aware compositions (app-owned) ui primitives, hooks, i18n
pages/*.tsx Route-level views components, hooks, router
hooks/*.ts Data fetching + state API client only

The key boundary: components/ui/ components must not import domain types from lib/api.ts. Only components/*.tsx and pages/*.tsx know about API shapes.


Data Flow

CSS Variables (index.css)
  ↓ consumed by
Tailwind @theme inline → utility classes (bg-primary, text-muted-foreground, etc.)
  ↓ applied in
shadcn ui primitives (button, card, badge, input)
  ↓ assembled into
Domain components (stat-card, category-badge, progress-bar)
  ↓ composed into
Pages (DashboardPage, LoginPage, CategoriesPage, SettingsPage)
  ↑ data from
Hooks (useAuth, useBudgets) → API client (lib/api.ts) → Go REST API

Styling flows downward (tokens → primitives → components → pages). Data flows upward (API → hooks → pages → components via props).


Patterns to Follow

Pattern 1: Token-First Color System

What: Define all palette values as CSS custom properties in :root, then map them to Tailwind via @theme inline. Never write raw color values (oklch(0.88 0.06 310)) in component files.

When: Any time a new color is needed for the pastel palette.

How it works in this codebase:

The project uses Tailwind 4 which reads tokens exclusively from @theme inline {} in index.css. The two-step pattern already present in the codebase is correct — add raw values to :root {}, then expose them as Tailwind utilities in @theme inline {}.

/* Step 1: Define semantic tokens in :root */
:root {
  /* Pastel palette — raw values */
  --pastel-pink:    oklch(0.92 0.04 340);
  --pastel-blue:    oklch(0.92 0.04 220);
  --pastel-green:   oklch(0.92 0.04 145);
  --pastel-amber:   oklch(0.92 0.06 80);
  --pastel-violet:  oklch(0.92 0.04 290);
  --pastel-sky:     oklch(0.94 0.04 215);

  /* Semantic role tokens — reference palette */
  --color-income:     var(--pastel-green);
  --color-bill:       var(--pastel-blue);
  --color-expense:    var(--pastel-amber);
  --color-debt:       var(--pastel-pink);
  --color-saving:     var(--pastel-violet);
  --color-investment: var(--pastel-sky);

  /* Override shadcn semantic tokens with pastel values */
  --primary:    oklch(0.55 0.14 255);   /* soft indigo */
  --background: oklch(0.985 0.005 240); /* barely-blue white */
  --card:       oklch(1 0 0);
  --muted:      oklch(0.96 0.01 240);
}

/* Step 2: Expose as Tailwind utilities inside @theme inline */
@theme inline {
  /* Existing shadcn bridge (keep as-is) */
  --color-primary: var(--primary);
  --color-background: var(--background);
  /* ... */

  /* New pastel category utilities */
  --color-income:     var(--color-income);
  --color-bill:       var(--color-bill);
  --color-expense:    var(--color-expense);
  --color-debt:       var(--color-debt);
  --color-saving:     var(--color-saving);
  --color-investment: var(--color-investment);
}

Components then use bg-income, text-bill, bg-saving/30 etc. as Tailwind classes.

Pattern 2: Category Color Mapping via CVA

What: A single categoryVariants CVA definition maps category types to their pastel color classes. Any component that needs category coloring imports this one function.

When: CategoryBadge, table rows in FinancialOverview, chart color arrays, category icons.

// components/category-badge.tsx
import { cva } from "class-variance-authority"

export const categoryVariants = cva(
  "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
  {
    variants: {
      type: {
        income:          "bg-income/20 text-income-foreground",
        bill:            "bg-bill/20 text-bill-foreground",
        variable_expense:"bg-expense/20 text-expense-foreground",
        debt:            "bg-debt/20 text-debt-foreground",
        saving:          "bg-saving/20 text-saving-foreground",
        investment:      "bg-investment/20 text-investment-foreground",
      },
    },
  }
)

This is the single source of truth for category-to-color mapping. Chart colors derive from the same CSS variables (not hardcoded hex).

Pattern 3: Wrapper Components Over shadcn Modification

What: When domain semantics need to be expressed (e.g., a "stat card" showing budget vs. actual), create a wrapper component in components/ that uses shadcn Card internally. Do not modify components/ui/card.tsx for domain logic.

When: Any component that appears more than twice with the same structure across pages.

// components/stat-card.tsx  — app-owned, domain-aware
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { cn } from "@/lib/utils"

interface StatCardProps {
  title: string
  value: string
  subtext?: string
  accent?: "income" | "bill" | "expense" | "debt" | "saving" | "investment"
}

export function StatCard({ title, value, subtext, accent }: StatCardProps) {
  return (
    <Card className={cn(accent && `border-l-4 border-l-${accent}`)}>
      <CardHeader><CardTitle>{title}</CardTitle></CardHeader>
      <CardContent>
        <p className="text-2xl font-semibold">{value}</p>
        {subtext && <p className="text-sm text-muted-foreground">{subtext}</p>}
      </CardContent>
    </Card>
  )
}

Pattern 4: Extend shadcn Primitives via className Override Only

What: When a shadcn component needs a slight visual adjustment, pass a className prop using cn() to override. Do not fork the component unless adding a new CVA variant that belongs at the primitive level.

When: One-off adjustments in page/feature components.

When to actually patch components/ui/: Adding a CVA variant used in 3+ places (e.g., adding a pastel button variant). Keep patches minimal and document them.

// Good — className override in consumer
<Button className="bg-primary/90 hover:bg-primary">Save Budget</Button>

// Good — new CVA variant if used everywhere
// In button.tsx, add to variants.variant:
pastel: "bg-primary/15 text-primary hover:bg-primary/25 border-primary/20",

Pattern 5: Recharts Color Consistency

What: Recharts color arrays must reference the same CSS variables used in Tailwind classes. Use getComputedStyle to read the variable at render time, not hardcoded hex values.

When: Any chart component (donut, bar, pie in ExpenseBreakdown, FinancialOverview).

// In chart component or a lib/colors.ts utility
function getCategoryColor(type: CategoryType): string {
  return getComputedStyle(document.documentElement)
    .getPropertyValue(`--color-${type}`)
    .trim()
}

Anti-Patterns to Avoid

Anti-Pattern 1: Inline Color Values in Components

What: Writing bg-sky-50, bg-emerald-50, bg-amber-50 directly in component files (already present in FinancialOverview.tsx).

Why bad: The color assignment is scattered across every component. Changing a category color requires hunting every file. The existing code in FinancialOverview.tsx rows array hardcodes color: 'bg-sky-50' — this must be replaced by the category variant system.

Instead: All category-to-color mappings live in categoryVariants (CVA, single file). Components only pass the category type.

Anti-Pattern 2: Separate CSS Files per Component

What: Creating StatCard.css, Dashboard.css etc.

Why bad: The project uses Tailwind 4's @theme inline — all custom styles belong in index.css or as Tailwind utilities. Split CSS files fragment the token system.

Instead: All tokens in index.css. All component-specific styles as Tailwind classes or CVA variants.

Anti-Pattern 3: Dark Mode Parallel Pastel Palette

What: Trying to create a full pastel dark mode in the same milestone.

Why bad: High complexity for low value (budgeting is primarily a desktop daytime activity). The PROJECT.md explicitly defers custom themes.

Instead: Keep .dark {} block in index.css as-is (it's already defined). Focus dark mode on the existing neutral dark values. Pastel = light mode only for this milestone.

Anti-Pattern 4: Tailwind tailwind.config.js for Custom Tokens

What: Creating a tailwind.config.js or tailwind.config.ts to add custom colors.

Why bad: This project uses Tailwind CSS 4 (@import "tailwindcss" in index.css, @tailwindcss/vite plugin). Tailwind 4 configures everything through CSS — tailwind.config.js is a Tailwind 3 pattern. Using both causes conflicts and confusion.

Instead: All custom tokens go in @theme inline {} inside index.css.


CSS Variable Organization

The existing index.css should be organized into clearly labeled sections:

/* 1. FRAMEWORK IMPORTS */
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@fontsource-variable/geist";

/* 2. CUSTOM DARK VARIANT */
@custom-variant dark (&:is(.dark *));

/* 3. LIGHT MODE TOKENS */
:root {
  /* === PASTEL PALETTE (raw values) === */
  --pastel-pink:    ...;
  --pastel-blue:    ...;
  /* ... */

  /* === CATEGORY SEMANTIC TOKENS === */
  --color-income:     var(--pastel-green);
  /* ... */

  /* === SHADCN SEMANTIC TOKENS (override defaults) === */
  --background: ...;
  --primary: ...;
  /* ... all existing shadcn vars ... */

  /* === CHART TOKENS === */
  --chart-1: ...;
  /* ... */
}

/* 4. DARK MODE OVERRIDES */
.dark {
  /* shadcn dark vars only — no dark pastel */
}

/* 5. TAILWIND BRIDGE (@theme inline) */
@theme inline {
  /* font */
  --font-sans: 'Geist Variable', sans-serif;

  /* shadcn token bridge (existing) */
  --color-background: var(--background);
  --color-primary: var(--primary);
  /* ... */

  /* category token bridge (new) */
  --color-income: var(--color-income);
  --color-bill: var(--color-bill);
  /* ... */

  /* radius scale (existing) */
  --radius-sm: calc(var(--radius) * 0.6);
  /* ... */
}

/* 6. BASE STYLES */
@layer base {
  * { @apply border-border outline-ring/50; }
  body { @apply bg-background text-foreground; }
  html { @apply font-sans; }
}

File Structure: Design System

frontend/src/
  index.css                         ← SINGLE SOURCE: all tokens, @theme inline, base
  lib/
    utils.ts                        ← cn() (keep as-is)
    colors.ts                       ← NEW: getCategoryColor() for Recharts, CATEGORY_TYPES map
  components/
    ui/                             ← shadcn primitives (patchable with CVA variants)
      button.tsx                    ← Add: pastel variant
      badge.tsx                     ← Add: category type variants via CVA
      card.tsx                      ← Keep as-is (className override is sufficient)
      input.tsx                     ← May need focus ring color patch
      ...
    category-badge.tsx              ← NEW: domain badge using badge.tsx + categoryVariants
    stat-card.tsx                   ← NEW: financial metric card
    progress-bar.tsx                ← NEW: budget vs actual with color coding
    page-header.tsx                 ← NEW: consistent page header with title + actions slot
    empty-state.tsx                 ← NEW: consistent empty state for lists

Scalability Considerations

Concern Current scope Future (theming milestone)
Color tokens Pastel light mode only Add data-theme="ocean" etc. on <html>, swap :root vars
Dark mode Neutral dark (existing) Pastel dark values in .dark {}
Component variants Per-category CVA Per-theme variant maps
Chart colors CSS variable lookup Same — already theme-aware

The CSS variable architecture supports future theming without structural change. When custom themes are added, only :root values change; components need zero modification.


Build Order: Foundation to Pages

This is the correct sequence because each layer depends on the previous:

Phase A: Token foundation

  1. Define pastel palette in index.css :root (raw oklch values)
  2. Define category semantic tokens in index.css
  3. Add category tokens to @theme inline bridge
  4. Define pastel overrides for shadcn semantic tokens (background, primary, etc.)

Phase B: Primitive polish 5. Patch button.tsx — add pastel variant, adjust default hover states 6. Patch badge.tsx — add category type variants 7. Patch input.tsx — ensure focus ring uses --ring pastel value 8. Create lib/colors.ts — getCategoryColor() and category type constants

Phase C: Domain components 9. Create category-badge.tsx — wraps badge with category semantics 10. Create stat-card.tsx — financial metric display card 11. Create progress-bar.tsx — budget vs actual visual 12. Create page-header.tsx — consistent header slot 13. Create empty-state.tsx — consistent empty list state

Phase D: Page-by-page polish 14. Login + Register pages — full branded treatment (no generic shadcn defaults) 15. Dashboard — replace hardcoded color strings with category variants, add stat cards 16. Categories page — category badge integration, polished CRUD UI 17. Settings page — form layout, preference toggles

Phase E: Charts 18. ExpenseBreakdown — donut chart with CSS variable colors 19. FinancialOverview chart — bar chart with category palette 20. AvailableBalance — progress indicator

Build order rationale: Pages cannot be polished until domain components exist. Domain components cannot use consistent tokens until the token layer is established. Charts come last because they depend on both the token system and the data components being stable.


Sources

  • Direct inspection of frontend/src/index.css — confirms Tailwind 4 (@import "tailwindcss", @theme inline) and oklch color space already in use
  • Direct inspection of frontend/components.json — confirms shadcn/ui radix-nova style, cssVariables: true, Tailwind 4 CSS-only config (no tailwind.config.ts)
  • Direct inspection of frontend/src/components/ui/button.tsx — confirms CVA pattern with class-variance-authority already in use
  • Direct inspection of frontend/src/components/FinancialOverview.tsx — identifies hardcoded color anti-pattern (bg-sky-50, bg-emerald-50) that needs migration
  • .planning/codebase/STACK.md — confirms Tailwind CSS 4.2.1, shadcn/ui 4.0.0, CVA 0.7.1
  • .planning/PROJECT.md — confirms pastel spreadsheet aesthetic goal, CSS variable customization approach, desktop-first target