From e54acc982755b32d43e5462c1ff614ee6b92d243 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 11 Mar 2026 19:19:17 +0100 Subject: [PATCH] docs(01): research phase design token foundation --- .../01-design-token-foundation/01-RESEARCH.md | 650 ++++++++++++++++++ 1 file changed, 650 insertions(+) create mode 100644 .planning/phases/01-design-token-foundation/01-RESEARCH.md diff --git a/.planning/phases/01-design-token-foundation/01-RESEARCH.md b/.planning/phases/01-design-token-foundation/01-RESEARCH.md new file mode 100644 index 0000000..fafcf1e --- /dev/null +++ b/.planning/phases/01-design-token-foundation/01-RESEARCH.md @@ -0,0 +1,650 @@ +# 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)