Files
SimpleFinanceDash/.planning/phases/01-design-token-foundation/01-RESEARCH.md

651 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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>
## 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
</user_constraints>
<phase_requirements>
## 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 |
</phase_requirements>
---
## 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 `<TableCell>` |
| Recharts | 2.15.4 | Chart rendering | Already used; palette.ts colors will feed into `<Cell fill>` 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 `<Cell fill>` — 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.0050.02 for near-neutral tokens, C ≈ 0.100.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 `<Cell fill>` 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<CategoryType, CategoryShades> = {
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
<CardHeader className="bg-gradient-to-r from-blue-50 to-indigo-50">
```
**Correct (palette-driven):**
```tsx
import { headerGradient } from '@/lib/palette'
<CardHeader style={headerGradient('bill')}>
```
**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 `<TableCell>` 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'
<TableCell className={cn('text-right', amountColorClass({ type: 'bill', actual, budgeted }))}>
{formatCurrency(actual, currency)}
</TableCell>
```
### Pattern 5: InlineEditCell Extraction (FIX-02)
**What:** Extract the near-identical `InlineEditRow` private function from `BillsTracker.tsx` (lines 59110), `VariableExpenses.tsx` (lines 86142), and `DebtTracker.tsx` (lines 61112) 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<void>
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 (
<TableCell className={cn('text-right', className)}>
{editing ? (
<Input
type="number"
step="0.01"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onBlur={handleBlur}
onKeyDown={(e) => e.key === 'Enter' && handleBlur()}
className="ml-auto w-28 text-right"
autoFocus
/>
) : (
<span
className="cursor-pointer rounded px-2 py-1 hover:bg-muted"
onClick={() => { setInputValue(String(value)); setEditing(true) }}
>
{formatCurrency(value, currency)}
</span>
)}
</TableCell>
)
}
```
**After extraction:** Each caller replaces its private `InlineEditRow` with `<InlineEditCell>` inside the existing `<TableRow>`. 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
<CardHeader style={headerGradient('bill')} className="px-4 py-3">
<CardTitle className="text-base font-semibold">{t('...')}</CardTitle>
```
**Hero cards (FinancialOverview, AvailableBalance):**
```tsx
<CardHeader style={overviewHeaderGradient()} className="px-6 py-5">
<CardTitle className="text-2xl font-semibold">{t('...')}</CardTitle>
```
**AvailableBalance center amount:**
```tsx
<span className={cn('text-3xl font-bold tabular-nums', available >= 0 ? 'text-success' : 'text-destructive')}>
{formatCurrency(available, budget.currency)}
</span>
<span className="text-xs text-muted-foreground">{t('dashboard.available')}</span>
```
### 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 `<Card>` vs `<CardHeader>` className/style split
**What goes wrong:** Adding `style={{ background: ... }}` to `<Card>` instead of `<CardHeader>` 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 `<CardHeader>`, never to `<Card>`.
**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 `<TableRow>` and renders the label cell, budget cell, and (optionally) remaining cell around the shared `<InlineEditCell>`.
**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'
<TableCell className={cn(
'text-right tabular-nums',
// income: green when positive, neutral when zero
isIncome && actual > 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)}
</TableCell>
```
### Chart cell fill from palette.ts
```tsx
// Source: existing AvailableBalance.tsx pattern + STACK.md research
import { palette } from '@/lib/palette'
{data.map((entry, index) => (
<Cell
key={index}
fill={palette[entry.categoryType]?.base ?? palette.carryover.base}
/>
))}
```
### Inline gradient on CardHeader
```tsx
// Source: CONTEXT.md + shadcn customization pattern
import { headerGradient } from '@/lib/palette'
<CardHeader style={headerGradient('bill')} className="px-4 py-3">
<CardTitle className="text-base font-semibold">
{t('dashboard.billsTracker')}
</CardTitle>
</CardHeader>
```
---
## 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 `<TableCell>`.
2. **Exact oklch values for the pastel palette**
- What we know: Lightness ≈ 0.880.97 for light/medium shades; chroma ≈ 0.030.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)