# Phase 1: Design Token Foundation - Research **Researched:** 2026-03-11 **Domain:** shadcn/ui CSS variable theming, oklch pastel color system, React component extraction **Confidence:** HIGH ## User Constraints (from CONTEXT.md) ### Locked Decisions **Pastel palette direction:** - Rainbow pastels: distinct pastel hue per category type, unified lightness and saturation across all hues - Base shadcn tokens (primary, secondary, accent, muted, ring) carry a soft lavender tint — not plain grey - App background gets a very subtle lavender tint (e.g., `oklch(0.98 0.005 290)`); cards stay pure white so they float on the tinted surface - Chart colors (chart-1 through chart-5) match the category colors from palette.ts — same color in charts as in tables **Category color mapping:** - income: soft green (money in) - bills: soft blue (recurring, stable) - variable_expense: soft amber (variable, caution) - debt: soft rose (obligation) - saving: soft violet (aspirational) - investment: soft pink (growth) - carryover: soft sky (neutral carry-forward) - 3 shades per category in palette.ts: light (row backgrounds), medium (header gradients, badges), base (chart fills, text accents) - Card header gradients go from category light to category medium (within the same hue) - FinancialOverview table rows each tinted with their category's light shade (per-row category color) **Hero element prominence:** - FinancialOverview and AvailableBalance get larger titles (text-2xl font-semibold vs text-lg font-medium on regular cards) and more generous padding (p-6 vs p-4) - AvailableBalance center amount uses text-3xl font-bold, color-coded: green when positive, destructive red when negative - Small muted-foreground label ("Available") sits below the center amount in the donut - FinancialOverview header uses a multi-pastel gradient (e.g., sky-light via lavender-light to green-light) to signal it represents all categories **Amount coloring rules:** - Amber triggers when actual_amount > budgeted_amount for expense categories (bills, variable_expense, debt). Exactly on budget = normal (no amber) - Only the actual column gets colored; budget column stays neutral - Income: any positive actual shows green (money received is always positive). Zero actual stays neutral - Amount coloring applies everywhere consistently: FinancialOverview summary rows AND individual item rows in BillsTracker, VariableExpenses, DebtTracker - Remaining/Available row: green when positive, destructive red when negative ### Claude's Discretion - Exact oklch values for all palette colors (lightness, chroma, hue angles) — as long as they feel pastel and harmonious - Loading skeleton tint colors - Exact spacing values and font weight choices beyond the hero vs regular distinction - InlineEditCell extraction approach (props interface design, where to place the shared component) ### Deferred Ideas (OUT OF SCOPE) None — discussion stayed within phase scope ## Phase Requirements | ID | Description | Research Support | |----|-------------|-----------------| | DSGN-01 | All shadcn CSS variables (primary, accent, muted, sidebar, chart-1 through chart-5) use pastel oklch values instead of zero-chroma neutrals | Covered by CSS variable replacement in `:root` block of `index.css`; zero-chroma tokens identified in codebase | | DSGN-02 | Semantic category color tokens defined in a single source of truth (`lib/palette.ts`) replacing scattered hardcoded hex and Tailwind classes | Covered by `palette.ts` creation; three locations with hardcoded hex arrays and Tailwind color classes identified | | DSGN-03 | Dashboard card header gradients unified to a single pastel palette family | Covered by replacing per-component gradient classNames with palette.ts references; five components with hardcoded gradients identified | | DSGN-04 | Typography hierarchy established — FinancialOverview and AvailableBalance sections are visually dominant hero elements | Covered by Tailwind class changes to CardTitle and CardContent; no new dependencies needed | | DSGN-05 | Consistent positive/negative amount coloring across all tables and summaries (green for positive, destructive for negative, amber for over-budget) | Covered by `cn()` logic on amount cells; requires adding `--success` CSS variable for green | | FIX-02 | `InlineEditRow` extracted into a shared component (currently duplicated in BillsTracker, VariableExpenses, DebtTracker) | Covered by extracting `InlineEditCell.tsx` to `@/components/InlineEditCell.tsx`; three near-identical implementations found | --- ## Summary Phase 1 is a pure frontend styling and refactoring phase — no backend changes, no new npm packages, and no new shadcn components. The stack is already fully assembled. The work falls into three buckets: (1) replace zero-chroma oklch values in `index.css` with pastel-tinted equivalents, (2) create `lib/palette.ts` as the single source of truth for category colors and wire it into all components that currently use hardcoded hex arrays or Tailwind color class strings, and (3) extract the triplicated `InlineEditRow` function into a shared `InlineEditCell.tsx` component. The existing codebase already uses the correct patterns — oklch in `:root`, `@theme inline` Tailwind v4 bridging, `cn()` for conditional classes, and `CardHeader/CardContent` composition. This phase replaces hardcoded values with semantic references; it does not restructure component architecture. The `shadcn` skill constrains the approach: customize via CSS variables only, never edit `src/components/ui/` source files, and use semantic tokens rather than raw Tailwind color utilities. The primary technical decisions are: (a) the oklch values to use for each category's three shades (light/medium/base), (b) how to expose those values to both CSS (for Tailwind utilities) and TypeScript (for inline `style` props on chart cells), and (c) the exact props interface for the extracted `InlineEditCell`. The CONTEXT.md has locked all product decisions; what remains is the mechanical implementation. **Primary recommendation:** Implement in three sequential tasks — CSS token replacement first (establishes the foundation all other changes reference), then `palette.ts` creation and component wiring, then `InlineEditCell` extraction. Commit after each task so partial work is always in a clean state. --- ## Standard Stack ### Core (all already installed — no new packages) | Library | Version | Purpose | Why Standard | |---------|---------|---------|--------------| | Tailwind CSS | 4.2.1 | Utility classes and `@theme inline` token bridging | Already the project's styling layer | | shadcn/ui | 4.0.0 | Component primitives; CSS variable contract | Already installed; all 18 components wired | | oklch (CSS native) | CSS Color Level 4 | Perceptually uniform color space for pastels | Already in use throughout `index.css` | | lucide-react | 0.577.0 | Icons | Project standard per `components.json` | | tw-animate-css | 1.4.0 | CSS animation utilities | Already imported in `index.css` | ### Supporting | Library | Version | Purpose | When to Use | |---------|---------|---------|-------------| | clsx + tailwind-merge (via `cn()`) | already installed | Conditional class merging | Use for amount-coloring logic on `` | | Recharts | 2.15.4 | Chart rendering | Already used; palette.ts colors will feed into `` props | | @fontsource-variable/geist-mono | optional | Tabular numbers on currency amounts | Install only if tabular numeral rendering is needed; not strictly required for this phase | ### Alternatives Considered | Instead of | Could Use | Tradeoff | |------------|-----------|----------| | CSS variable replacement in-place | shadcn preset swap (`npx shadcn@latest init --preset`) | Preset overwrites existing component customizations; in-place is safer | | Single `palette.ts` export | CSS-only custom properties | TypeScript type safety is needed for chart `` — a `.ts` file serves both CSS and JS consumers | | Inline `style` props for gradient headers | Tailwind arbitrary gradient classes | `style` props allow full dynamic color references from `palette.ts`; arbitrary classes would re-hardcode values | **Installation:** No new packages required for this phase. All tools are present. --- ## Architecture Patterns ### Recommended Project Structure (additions only) ``` frontend/src/ ├── components/ │ ├── InlineEditCell.tsx # NEW — extracted from BillsTracker, VariableExpenses, DebtTracker │ └── ui/ # UNCHANGED — never edit shadcn source files ├── lib/ │ ├── palette.ts # NEW — single source of truth for category colors │ ├── api.ts # unchanged │ ├── format.ts # unchanged │ └── utils.ts # unchanged └── index.css # MODIFIED — replace zero-chroma tokens with pastel oklch values ``` ### Pattern 1: CSS Variable Replacement (DSGN-01) **What:** Replace every `oklch(L 0 0)` zero-chroma value in the `:root` block with a pastel-tinted oklch that carries a small chroma value (C ≈ 0.005–0.02 for near-neutral tokens, C ≈ 0.10–0.15 for primary/accent tokens). **When to use:** All shadcn semantic tokens (`--background`, `--primary`, `--secondary`, `--muted`, `--accent`, `--ring`, `--border`, `--input`, `--sidebar-*`, `--chart-1` through `--chart-5`) must be updated. **Example — the before/after for `:root`:** ```css /* BEFORE (zero-chroma neutral) */ --background: oklch(1 0 0); --primary: oklch(0.205 0 0); --secondary: oklch(0.97 0 0); --muted: oklch(0.97 0 0); --accent: oklch(0.97 0 0); --ring: oklch(0.708 0 0); --sidebar: oklch(0.985 0 0); /* AFTER (pastel lavender tint; exact values are Claude's discretion) */ --background: oklch(0.98 0.005 290); /* very subtle lavender tint */ --card: oklch(1 0 0); /* pure white — floats on tinted bg */ --primary: oklch(0.50 0.12 260); /* soft lavender-blue */ --primary-foreground: oklch(0.99 0 0); --secondary: oklch(0.95 0.015 280); /* near-white with lavender cast */ --muted: oklch(0.95 0.010 280); --accent: oklch(0.94 0.020 280); --ring: oklch(0.65 0.08 260); /* tinted focus ring */ --sidebar: oklch(0.97 0.010 280); /* slightly distinct from background */ ``` **Key constraint from shadcn skill:** Edit only `src/index.css`. Never touch `src/components/ui/*.tsx` to change colors. ### Pattern 2: Palette TypeScript Module (DSGN-02) **What:** A `lib/palette.ts` file that exports typed color objects for each category. Three shades per category (light, medium, base). Used by both component inline styles (gradients, row backgrounds) and chart `` attributes. **When to use:** Any component that previously had hardcoded hex arrays (`PASTEL_COLORS = [...]`) or Tailwind color classes like `bg-blue-50`, `from-amber-50 to-yellow-50`. **Example:** ```typescript // Source: CONTEXT.md locked decision + codebase analysis export type CategoryType = | 'income' | 'bill' | 'variable_expense' | 'debt' | 'saving' | 'investment' | 'carryover' export interface CategoryShades { light: string // oklch — row backgrounds, tinted surfaces medium: string // oklch — header gradient to-color, badges base: string // oklch — chart fills, text accents } export const palette: Record = { income: { light: 'oklch(0.96 0.04 145)', medium: 'oklch(0.88 0.08 145)', base: 'oklch(0.76 0.14 145)', }, bill: { light: 'oklch(0.96 0.03 250)', medium: 'oklch(0.88 0.07 250)', base: 'oklch(0.76 0.12 250)', }, variable_expense: { light: 'oklch(0.97 0.04 85)', medium: 'oklch(0.90 0.08 85)', base: 'oklch(0.80 0.14 85)', }, debt: { light: 'oklch(0.96 0.04 15)', medium: 'oklch(0.88 0.08 15)', base: 'oklch(0.76 0.13 15)', }, saving: { light: 'oklch(0.95 0.04 280)', medium: 'oklch(0.87 0.08 280)', base: 'oklch(0.75 0.13 280)', }, investment: { light: 'oklch(0.96 0.04 320)', medium: 'oklch(0.88 0.07 320)', base: 'oklch(0.76 0.12 320)', }, carryover: { light: 'oklch(0.96 0.03 210)', medium: 'oklch(0.88 0.06 210)', base: 'oklch(0.76 0.11 210)', }, } // Helper: gradient style object for CardHeader export function headerGradient(type: CategoryType): React.CSSProperties { const { light, medium } = palette[type] return { background: `linear-gradient(to right, ${light}, ${medium})` } } // Helper: determine amount color class based on category and comparison export function amountColorClass(opts: { type: CategoryType actual: number budgeted: number isIncome?: boolean isAvailable?: boolean }): string { const { type, actual, budgeted, isIncome, isAvailable } = opts if (isAvailable || isIncome) { if (actual > 0) return 'text-success' if (actual < 0) return 'text-destructive' return '' } // Expense categories (bill, variable_expense, debt) if (actual > budgeted) return 'text-warning' return '' } ``` **Note on `--color-bill` CSS custom properties:** The `@theme inline` block in `index.css` should also expose `--chart-1` through `--chart-5` mapped to the palette base colors, so the shadcn `ChartContainer` / `ChartConfig` pattern works correctly. The palette.ts values and the `--chart-*` CSS variables must be kept in sync manually. ### Pattern 3: Card Header Gradient Application (DSGN-03) **What:** Replace hardcoded gradient classNames on `CardHeader` with inline `style` props driven by `palette.ts`. **Current (hardcoded — remove this pattern):** ```tsx ``` **Correct (palette-driven):** ```tsx import { headerGradient } from '@/lib/palette' ``` **Components to update:** `BillsTracker.tsx`, `VariableExpenses.tsx`, `DebtTracker.tsx`, `AvailableBalance.tsx`, `ExpenseBreakdown.tsx`, `FinancialOverview.tsx`. **FinancialOverview special case:** Uses a multi-category gradient (sky-light via lavender-light to green-light). This cannot use the single-category `headerGradient()` helper. Define a separate `overviewHeaderGradient` export in `palette.ts`. ### Pattern 4: Amount Color Logic (DSGN-05) **What:** Wrap the `actual` amount `` with `cn()` plus the `amountColorClass()` helper from `palette.ts`. **Important:** Requires adding `--success` and `--warning` CSS custom properties to `index.css` since shadcn's default tokens only include `--destructive`. The `@theme inline` block must also expose them as `--color-success` and `--color-warning`. ```css /* index.css :root additions */ --success: oklch(0.55 0.15 145); /* green — positive amounts */ --success-foreground: oklch(0.99 0 0); --warning: oklch(0.60 0.14 75); /* amber — over-budget */ --warning-foreground: oklch(0.99 0 0); ``` ```css /* @theme inline additions */ --color-success: var(--success); --color-success-foreground: var(--success-foreground); --color-warning: var(--warning); --color-warning-foreground: var(--warning-foreground); ``` **Usage in component:** ```tsx import { cn } from '@/lib/utils' import { amountColorClass } from '@/lib/palette' {formatCurrency(actual, currency)} ``` ### Pattern 5: InlineEditCell Extraction (FIX-02) **What:** Extract the near-identical `InlineEditRow` private function from `BillsTracker.tsx` (lines 59–110), `VariableExpenses.tsx` (lines 86–142), and `DebtTracker.tsx` (lines 61–112) into a single shared `components/InlineEditCell.tsx`. **Differences between the three implementations:** - `VariableExpenses.InlineEditRow` has an extra `remaining` computed cell — this means the extracted component needs an optional `showRemaining?: boolean` prop, or the `remaining` cell can be composed externally by the caller. - All three share identical edit-mode logic (state, handleBlur, onKeyDown, onBlur). **Recommended approach (Claude's discretion):** Keep the component focused on the actual-amount cell only. The caller renders the remaining cell separately. This avoids a prop-driven layout branch inside the extracted component. ```tsx // src/components/InlineEditCell.tsx import { useState } from 'react' import { TableCell } from '@/components/ui/table' import { Input } from '@/components/ui/input' import { cn } from '@/lib/utils' import { formatCurrency } from '@/lib/format' interface InlineEditCellProps { value: number currency: string onSave: (value: number) => Promise className?: string } export function InlineEditCell({ value, currency, onSave, className }: InlineEditCellProps) { const [editing, setEditing] = useState(false) const [inputValue, setInputValue] = useState(String(value)) const handleBlur = async () => { const num = parseFloat(inputValue) if (!isNaN(num) && num !== value) { await onSave(num) } setEditing(false) } return ( {editing ? ( setInputValue(e.target.value)} onBlur={handleBlur} onKeyDown={(e) => e.key === 'Enter' && handleBlur()} className="ml-auto w-28 text-right" autoFocus /> ) : ( { setInputValue(String(value)); setEditing(true) }} > {formatCurrency(value, currency)} )} ) } ``` **After extraction:** Each caller replaces its private `InlineEditRow` with `` inside the existing ``. The amount coloring `className` prop can pass the `amountColorClass()` result, giving DSGN-05 coloring "for free" at the call site. ### Pattern 6: Hero Typography (DSGN-04) **What:** Increase visual weight of `FinancialOverview` and `AvailableBalance` card headers and content. Apply larger text and more padding than regular section cards. **Regular cards (BillsTracker, VariableExpenses, DebtTracker, ExpenseBreakdown):** ```tsx {t('...')} ``` **Hero cards (FinancialOverview, AvailableBalance):** ```tsx {t('...')} ``` **AvailableBalance center amount:** ```tsx = 0 ? 'text-success' : 'text-destructive')}> {formatCurrency(available, budget.currency)} {t('dashboard.available')} ``` ### Anti-Patterns to Avoid - **Never use hardcoded Tailwind color names for category colors** (`bg-blue-50`, `from-amber-50`): these won't respond to theme changes. Use `style={headerGradient('bill')}` instead. - **Never import hex strings from palette.ts into className strings**: className strings must use CSS variables or Tailwind utilities. Raw hex/oklch values belong in `style` props or CSS files. - **Never edit `src/components/ui/*.tsx`**: This is a firm project constraint. All color/typography changes happen in `index.css` or at component call sites. - **Never add `dark:` manual overrides**: Use semantic tokens. The `.dark` block in `index.css` will be updated as a separate concern. - **Never color the budget column**: Only the `actual` column receives amount coloring per CONTEXT.md locked decisions. --- ## Don't Hand-Roll | Problem | Don't Build | Use Instead | Why | |---------|-------------|-------------|-----| | Conditional class merging | Manual template literal ternaries | `cn()` from `@/lib/utils` | Already available; handles Tailwind class conflicts correctly | | Currency formatting | Custom number formatter | `formatCurrency()` from `@/lib/format` | Already in use across all components | | Status color tokens | Raw green/amber hex values | `--success`/`--warning` CSS variables + `text-success`/`text-warning` utilities | Theming-safe; consistent with shadcn semantic token pattern | | Gradient definitions | Repeated inline gradient strings | `headerGradient()` from `palette.ts` | Single source of truth; change once, update everywhere | | Inline edit state logic | New hook or HOC | Inline state in `InlineEditCell.tsx` | Complexity doesn't justify abstraction beyond a single component | **Key insight:** This phase is entirely about connecting existing infrastructure (shadcn CSS variables, Tailwind v4 `@theme inline`, `cn()`) to a typed TypeScript palette module. The hard infrastructure work was done when the project was initialized. --- ## Common Pitfalls ### Pitfall 1: Forgetting to sync `--chart-*` variables with `palette.ts` base colors **What goes wrong:** `palette.ts` has `bill.base: 'oklch(0.76 0.12 250)'` but `--chart-2` in `index.css` still has the original blue value. Charts using `ChartConfig` (which reads CSS vars) show different colors from table rows (which read `palette.ts` directly). **Why it happens:** Two parallel color systems — CSS custom properties for shadcn's `ChartContainer`, and TypeScript objects for direct `fill=` props. **How to avoid:** In `palette.ts`, add a secondary export that maps the five expense-category base colors to `--chart-1` through `--chart-5`. Document that `index.css` `--chart-*` values must be updated whenever `palette.ts` base values change. **Warning signs:** Donut chart segments use different shades than the corresponding table row backgrounds. ### Pitfall 2: Using Tailwind v3 CSS variable syntax **What goes wrong:** Writing `bg-[--color-bill]` (brackets) instead of `bg-(--color-bill)` (parentheses) causes the utility class to not generate in Tailwind v4. **Why it happens:** Tailwind v4 changed arbitrary property syntax from square brackets to parentheses for CSS variable references. **How to avoid:** Use `bg-(--color-bill)/20` for CSS variable references with opacity. Use `style={{ background: palette.bill.light }}` for dynamic values where a utility class isn't appropriate. **Warning signs:** Background color doesn't appear; no Tailwind class is generated for the element. ### Pitfall 3: Breaking the `` vs `` className/style split **What goes wrong:** Adding `style={{ background: ... }}` to `` instead of `` changes the entire card background, removing the white card body that "floats" on the tinted gradient header. **Why it happens:** The gradient should only apply to the header area, not the full card. **How to avoid:** Always apply gradient styles to ``, never to ``. **Warning signs:** Full card shows the gradient color including the table/content area. ### Pitfall 4: `InlineEditCell` prop mismatch after extraction **What goes wrong:** `VariableExpenses.InlineEditRow` renders a 4-column row (label, budget, actual, remaining) while `BillsTracker.InlineEditRow` renders 3 columns. Naively extracting to a shared "row" component requires conditional rendering based on a prop, making the component more complex than the original duplicates. **Why it happens:** The three implementations have slightly different column structures. **How to avoid:** Extract only the editable *cell* (the actual amount cell), not the full row. The caller continues to own the `` and renders the label cell, budget cell, and (optionally) remaining cell around the shared ``. **Warning signs:** VariableExpenses rows show an extra empty column or miss the remaining calculation. ### Pitfall 5: Missing `--success` semantic token causes raw color fallback **What goes wrong:** Amount coloring code writes `text-green-600` as a fallback when `--success` isn't defined. This hardcodes a raw Tailwind color rather than a semantic token, violating the shadcn skill's "No raw color values for status/state indicators" rule. **Why it happens:** `--success` is not in shadcn's default token set; it must be added manually. **How to avoid:** Add `--success` and `--warning` to `index.css` `:root` AND the `@theme inline` block before any component uses `text-success` or `text-warning` Tailwind utilities. **Warning signs:** `text-success` has no effect (browser applies no color); amounts appear in default foreground color. --- ## Code Examples Verified patterns from existing codebase and shadcn skill documentation: ### Adding custom semantic tokens (Tailwind v4 pattern) ```css /* index.css — in :root block */ --success: oklch(0.55 0.15 145); --success-foreground: oklch(0.99 0 0); --warning: oklch(0.60 0.14 75); --warning-foreground: oklch(0.99 0 0); /* index.css — in @theme inline block */ @theme inline { --color-success: var(--success); --color-success-foreground: var(--success-foreground); --color-warning: var(--warning); --color-warning-foreground: var(--warning-foreground); } ``` Source: `customization.md` — "Adding Custom Colors" section + existing `index.css` `@theme inline` pattern. ### Conditional amount coloring with cn() ```tsx // Source: shadcn skill styling.md + CONTEXT.md locked rules import { cn } from '@/lib/utils' 0 && 'text-success', // expense: amber when over budget isExpense && actual > budgeted && 'text-warning', // available/remaining: green positive, red negative isAvailable && actual > 0 && 'text-success', isAvailable && actual < 0 && 'text-destructive', )}> {formatCurrency(actual, currency)} ``` ### Chart cell fill from palette.ts ```tsx // Source: existing AvailableBalance.tsx pattern + STACK.md research import { palette } from '@/lib/palette' {data.map((entry, index) => ( ))} ``` ### Inline gradient on CardHeader ```tsx // Source: CONTEXT.md + shadcn customization pattern import { headerGradient } from '@/lib/palette' {t('dashboard.billsTracker')} ``` --- ## State of the Art | Old Approach (in codebase) | Current Correct Approach | Impact | |----------------------------|--------------------------|--------| | `bg-gradient-to-r from-blue-50 to-indigo-50` on CardHeader | `style={headerGradient('bill')}` from palette.ts | Colors controlled from one file, not scattered across 6 components | | `const PASTEL_COLORS = ['#93c5fd', '#f9a8d4', ...]` arrays | `palette[type].base` references | Charts and tables use identical colors; no more separate hex arrays | | `text-destructive` only for negative values | `text-success`, `text-warning`, `text-destructive` semantic trio | Consistent amount coloring vocabulary | | `oklch(L 0 0)` zero-chroma shadcn defaults | Pastel-tinted oklch with low-but-nonzero chroma | Components show intentional color tones, not grey neutrals | | Private `InlineEditRow` function in each file | Shared `InlineEditCell.tsx` component | Single edit/fix point for inline editing behavior | **Deprecated/outdated in this codebase (to remove):** - `bg-blue-50`, `from-amber-50`, `from-blue-50 to-indigo-50` etc. on CardHeaders: hardcoded Tailwind color utilities for category identity - `const PASTEL_COLORS = [...]` hex arrays in `AvailableBalance.tsx` and `ExpenseBreakdown.tsx` - Three copies of the `InlineEditRow` private function --- ## Open Questions 1. **`VariableExpenses` has 4 columns; `BillsTracker` and `DebtTracker` have 3** - What we know: The extra "remaining" column in VariableExpenses is `budgeted - actual` - What's unclear: Whether any future tracker will also need a remaining column - Recommendation: Extract only the `InlineEditCell` (actual column cell). Callers own their row structure. The remaining cell stays in `VariableExpenses.tsx` as a plain ``. 2. **Exact oklch values for the pastel palette** - What we know: Lightness ≈ 0.88–0.97 for light/medium shades; chroma ≈ 0.03–0.15; hue angles from CONTEXT.md - What's unclear: Precise values that render harmoniously across the hues — this requires visual browser testing - Recommendation: Claude's discretion to choose initial values; plan should include a review/adjustment step after first implementation 3. **`--chart-*` variable count vs category count** - What we know: shadcn provides `--chart-1` through `--chart-5`; the project has 7 category types (including carryover) - What's unclear: Which 5 chart colors to prioritize - Recommendation: Map `--chart-1` through `--chart-5` to the 5 non-carryover expense-type categories (bills, variable_expense, debt, saving, investment). Income and carryover use `base` color from palette.ts directly where needed in charts. --- ## Validation Architecture ### Test Framework | Property | Value | |----------|-------| | Framework | vitest (not yet installed — see Wave 0) | | Config file | `vite.config.ts` (extend with `test` block) or `vitest.config.ts` | | Quick run command | `cd frontend && bun vitest run --reporter=verbose` | | Full suite command | `cd frontend && bun vitest run` | **Note:** The `package.json` has no `vitest` dependency and no test script. The CLAUDE.md documents `bun vitest` as the test runner command, indicating it is the intended framework but not yet installed. Wave 0 must install it. ### Phase Requirements → Test Map | Req ID | Behavior | Test Type | Automated Command | File Exists? | |--------|----------|-----------|-------------------|-------------| | DSGN-01 | All `:root` oklch values have non-zero chroma | unit (CSS parsing) | manual visual check — CSS values are not unit-testable without a browser | manual-only | | DSGN-02 | `palette.ts` exports all 7 category types with 3 shades each | unit | `bun vitest run src/lib/palette.test.ts -t "exports all categories"` | ❌ Wave 0 | | DSGN-02 | `amountColorClass` returns correct class per scenario | unit | `bun vitest run src/lib/palette.test.ts -t "amountColorClass"` | ❌ Wave 0 | | DSGN-03 | `headerGradient` returns a style object with a `background` string | unit | `bun vitest run src/lib/palette.test.ts -t "headerGradient"` | ❌ Wave 0 | | DSGN-04 | Visual hierarchy — no automated test; verified by screenshot | manual-only | n/a | manual-only | | DSGN-05 | Amount coloring: positive income → text-success, over-budget → text-warning, negative available → text-destructive | unit | `bun vitest run src/lib/palette.test.ts -t "amountColorClass"` | ❌ Wave 0 | | FIX-02 | `InlineEditCell` renders display mode showing formatted currency | unit | `bun vitest run src/components/InlineEditCell.test.tsx -t "renders formatted value"` | ❌ Wave 0 | | FIX-02 | `InlineEditCell` enters edit mode on click | unit | `bun vitest run src/components/InlineEditCell.test.tsx -t "enters edit mode"` | ❌ Wave 0 | | FIX-02 | `InlineEditCell` calls onSave with parsed number on blur | unit | `bun vitest run src/components/InlineEditCell.test.tsx -t "calls onSave"` | ❌ Wave 0 | ### Sampling Rate - **Per task commit:** `cd frontend && bun vitest run src/lib/palette.test.ts src/components/InlineEditCell.test.tsx` - **Per wave merge:** `cd frontend && bun vitest run` - **Phase gate:** Full suite green before `/gsd:verify-work` ### Wave 0 Gaps - [ ] `frontend/package.json` — add `vitest`, `@testing-library/react`, `@testing-library/jest-dom`, `@testing-library/user-event`, `jsdom` as devDependencies: `cd frontend && bun add -d vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom` - [ ] `frontend/vite.config.ts` — add `test: { environment: 'jsdom', globals: true, setupFiles: ['./src/test-setup.ts'] }` to `defineConfig` - [ ] `frontend/src/test-setup.ts` — `import '@testing-library/jest-dom'` - [ ] `frontend/src/lib/palette.test.ts` — covers DSGN-02, DSGN-03, DSGN-05 - [ ] `frontend/src/components/InlineEditCell.test.tsx` — covers FIX-02 --- ## Sources ### Primary (HIGH confidence) - Codebase direct read: `frontend/src/index.css` — confirmed zero-chroma token baseline and `@theme inline` structure - Codebase direct read: `frontend/src/components/BillsTracker.tsx`, `VariableExpenses.tsx`, `DebtTracker.tsx` — confirmed three duplicate `InlineEditRow` implementations, confirmed hardcoded gradient classes - Codebase direct read: `frontend/src/components/AvailableBalance.tsx`, `ExpenseBreakdown.tsx` — confirmed hardcoded `PASTEL_COLORS` hex arrays - Codebase direct read: `frontend/src/components/FinancialOverview.tsx` — confirmed hardcoded row color strings and gradient - Codebase direct read: `frontend/components.json` — confirmed `style: "radix-nova"`, `tailwindVersion` is v4, `iconLibrary: "lucide"`, `rsc: false`, alias `@/` - Skill file: `.agents/skills/shadcn/customization.md` — confirmed "Adding Custom Colors" pattern (define in `:root`, register in `@theme inline`, use as utility class) - Skill file: `.agents/skills/shadcn/rules/styling.md` — confirmed "No raw color values for status/state indicators" rule - Codebase direct read: `frontend/package.json` — confirmed no vitest installed; confirmed bun as package manager - Prior planning research: `.planning/research/STACK.md` — confirmed Tailwind v4 `bg-(--var)` syntax, oklch chroma guidance, no new packages needed ### Secondary (MEDIUM confidence) - `.planning/phases/01-design-token-foundation/01-CONTEXT.md` — locked product decisions about color assignments, hero sizing, amount coloring rules ### Tertiary (LOW confidence) - None — all key claims are backed by direct codebase inspection or skill documentation --- ## Metadata **Confidence breakdown:** - Standard stack: HIGH — confirmed from package.json and existing source files; no external research needed - Architecture patterns: HIGH — confirmed from direct reading of all 6 dashboard components plus shadcn skill documentation - Pitfalls: HIGH — pitfalls derived from direct diff of three InlineEditRow implementations plus known Tailwind v4 syntax changes observed in codebase - Test infrastructure: HIGH for gap identification (no vitest present); MEDIUM for exact vitest/jsdom config syntax (based on training data) **Research date:** 2026-03-11 **Valid until:** 2026-04-11 (stable library versions; no fast-moving dependencies in scope)