# 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)