34 KiB
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.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:
/* 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:
// 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):
<CardHeader className="bg-gradient-to-r from-blue-50 to-indigo-50">
Correct (palette-driven):
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.
/* 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);
/* @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:
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 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.InlineEditRowhas an extraremainingcomputed cell — this means the extracted component needs an optionalshowRemaining?: booleanprop, or theremainingcell 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.
// 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):
<CardHeader style={headerGradient('bill')} className="px-4 py-3">
<CardTitle className="text-base font-semibold">{t('...')}</CardTitle>
Hero cards (FinancialOverview, AvailableBalance):
<CardHeader style={overviewHeaderGradient()} className="px-6 py-5">
<CardTitle className="text-2xl font-semibold">{t('...')}</CardTitle>
AvailableBalance center amount:
<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. Usestyle={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
styleprops or CSS files. - Never edit
src/components/ui/*.tsx: This is a firm project constraint. All color/typography changes happen inindex.cssor at component call sites. - Never add
dark:manual overrides: Use semantic tokens. The.darkblock inindex.csswill be updated as a separate concern. - Never color the budget column: Only the
actualcolumn 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)
/* 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()
// 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
// 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
// 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-50etc. on CardHeaders: hardcoded Tailwind color utilities for category identityconst PASTEL_COLORS = [...]hex arrays inAvailableBalance.tsxandExpenseBreakdown.tsx- Three copies of the
InlineEditRowprivate function
Open Questions
-
VariableExpenseshas 4 columns;BillsTrackerandDebtTrackerhave 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 inVariableExpenses.tsxas a plain<TableCell>.
- What we know: The extra "remaining" column in VariableExpenses is
-
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
-
--chart-*variable count vs category count- What we know: shadcn provides
--chart-1through--chart-5; the project has 7 category types (including carryover) - What's unclear: Which 5 chart colors to prioritize
- Recommendation: Map
--chart-1through--chart-5to the 5 non-carryover expense-type categories (bills, variable_expense, debt, saving, investment). Income and carryover usebasecolor from palette.ts directly where needed in charts.
- What we know: shadcn provides
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— addvitest,@testing-library/react,@testing-library/jest-dom,@testing-library/user-event,jsdomas devDependencies:cd frontend && bun add -d vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdomfrontend/vite.config.ts— addtest: { environment: 'jsdom', globals: true, setupFiles: ['./src/test-setup.ts'] }todefineConfigfrontend/src/test-setup.ts—import '@testing-library/jest-dom'frontend/src/lib/palette.test.ts— covers DSGN-02, DSGN-03, DSGN-05frontend/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 inlinestructure - Codebase direct read:
frontend/src/components/BillsTracker.tsx,VariableExpenses.tsx,DebtTracker.tsx— confirmed three duplicateInlineEditRowimplementations, confirmed hardcoded gradient classes - Codebase direct read:
frontend/src/components/AvailableBalance.tsx,ExpenseBreakdown.tsx— confirmed hardcodedPASTEL_COLORShex arrays - Codebase direct read:
frontend/src/components/FinancialOverview.tsx— confirmed hardcoded row color strings and gradient - Codebase direct read:
frontend/components.json— confirmedstyle: "radix-nova",tailwindVersionis 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 v4bg-(--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)