Files
SimpleFinanceDash/.planning/phases/04-full-app-design-consistency/04-RESEARCH.md

32 KiB
Raw Blame History

Phase 4: Full-App Design Consistency - Research

Researched: 2026-03-17 Domain: React/TypeScript UI polish — pattern application, i18n completeness, skeleton loading states, auth page redesign Confidence: HIGH (all findings from direct codebase inspection — no external library uncertainty)


<user_constraints>

User Constraints (from CONTEXT.md)

Locked Decisions

Auth pages (Login & Register):

  • Solid muted background color behind the centered card (not plain white, not gradient)
  • Card accent styling: Claude's discretion on whether top border, shadow, or ring treatment
  • App icon/logo above the title text for brand presence (icon asset or emoji/Lucide placeholder)
  • OAuth buttons (Google, GitHub) get provider SVG icons next to text labels
  • Pages remain standalone centered layout (outside AppLayout sidebar)

BudgetDetail category sections:

  • Migrate to semantic color tokens (--color-on-budget, --color-over-budget) replacing hardcoded text-green-600/text-red-600
  • Adopt direction-aware diff logic from Phase 3: spending types over when actual > budgeted, income under-earned when actual < budgeted
  • Visual style upgrade: left-border accent + badge chips to match dashboard CategorySection appearance
  • Collapsible behavior vs always-expanded: Claude's discretion based on editing context
  • Tier badges (Fixed/Variable/One-off): Claude's discretion on keep vs remove
  • Overall totals box: Claude's discretion on whether to use StatCards or keep as styled box

Category group headers (Categories, Template, QuickAdd pages):

  • Group header styling upgrade: Claude's discretion on matching full dashboard CategorySection style (left-border card) vs enhanced dot style (larger dot, bolder label)
  • Template group totals placement (header badge vs table footer): Claude's discretion
  • BudgetList enrichment (card per budget vs table): Claude's discretion
  • Settings card structure (single vs multiple cards): Claude's discretion

Page descriptions & polish:

  • Page descriptions via PageShell description prop: Claude's discretion per-page on whether subtitle adds value
  • Empty states: Claude's discretion on whether to add icon/illustration treatment or keep text-only
  • Loading states: Add skeleton placeholders for all pages (replacing current return null loading states)
  • i18n: Locale-aware month formatting using Intl.DateTimeFormat with user's locale (e.g., "Marz 2026" in German)
  • All hardcoded English strings (month names, "Month"/"Year" labels) must get i18n keys in both en.json and de.json

Claude's Discretion

  • Auth card accent treatment (top border vs shadow vs ring)
  • BudgetDetail: collapsible sections vs visual-style-only (always expanded)
  • BudgetDetail: keep or remove tier badges
  • BudgetDetail: overall totals as StatCards vs styled box
  • CRUD page group headers: dashboard-style cards vs enhanced dots
  • Template: group totals in header vs table footer
  • BudgetList: card layout vs table layout
  • Settings: single card vs multiple cards
  • Per-page description text decisions
  • Empty state visual treatment level
  • Skeleton component designs for each page type

Deferred Ideas (OUT OF SCOPE)

None — discussion stayed within phase scope. </user_constraints>


<phase_requirements>

Phase Requirements

ID Description Research Support
UI-DESIGN-01 All 9 pages use PageShell with consistent typography, card style, and color token usage PageShell already exists in shared/PageShell.tsx — 7 of 9 pages need it wired in; DashboardPage already uses it
UI-AUTH-01 Login and Register pages have refreshed visual design matching dashboard card/color patterns Both pages use plain bg-background — need bg-muted background + card accent treatment + app logo/icon
UI-CATEGORIES-01 Categories page group headers upgraded to match design system CategoriesPage uses plain dot+label headers — upgrade to left-border card or enhanced dot style
UI-TEMPLATE-01 Template page group headers upgraded and totals displayed TemplatePage uses same plain dot+label headers as Categories
UI-BUDGETS-01 BudgetDetail displays category groups with color-accented cards and semantic diff tokens BudgetDetailPage uses hardcoded text-green-600/text-red-600 + plain dot headers + no semantic tokens
UI-QUICKADD-01 Quick Add page uses PageShell with consistent styling QuickAddPage has no group headers (flat list) — primarily needs PageShell + possible restructure
UI-SETTINGS-01 Settings page uses PageShell with consistent styling SettingsPage has no PageShell, has a Card already but redundant h1+CardTitle
UI-RESPONSIVE-01 Navigating between any two pages produces no jarring visual discontinuity All pages need consistent gap/spacing, same PageShell header heights, same font sizing
</phase_requirements>

Summary

Phase 4 is a pure polish and pattern-application phase — no new features, no backend changes. The design system (OKLCH color tokens, semantic status tokens, CategorySection component, PageShell) is fully established in Phases 13. The work is applying it uniformly to all 9 pages.

The current state has a clear divide: DashboardPage is the polished reference, and all other authenticated pages are functional-but-unstyled first-drafts. Seven pages have inline <h1> headings instead of PageShell. Six pages return null while loading instead of showing skeletons. BudgetDetailPage has hardcoded Tailwind color classes (text-green-600, text-red-600) that bypass the established semantic token system. Auth pages have a plain bg-background root div where the design spec calls for bg-muted.

The favicon.svg in /public/ is a real stylized lightning-bolt SVG with the app's purple brand color (#863bff) — this is the logo asset to use above the auth card title. No additional icon asset is needed.

There is no test infrastructure in this project (no test files, no test framework configured). nyquist_validation is enabled in config.json, so this section must be addressed, but with a note that all validation is manual/visual for a UI-only phase.

Primary recommendation: Treat this phase as 9 small, sequential page upgrades. Apply PageShell + skeleton + i18n cleanup as a checklist across each page. Use direct codebase inspection — not external research — as the source of truth.


Standard Stack

Core (already installed — no new dependencies needed)

Library Version Purpose Why Standard
React 19.2.4 Component rendering Project foundation
react-i18next 16.5.8 i18n translation hook useTranslation Already in use project-wide
Tailwind CSS 4.2.1 Utility classes Project styling system
shadcn/ui primitives (radix-ui 1.4.3) Card, Badge, Skeleton, Button, etc. Already installed and used
lucide-react 0.577.0 Icons (including logo placeholder) Already in use project-wide

Supporting

Library Version Purpose When to Use
Intl.DateTimeFormat Native browser API Locale-aware month/year formatting Replace hardcoded English month arrays in BudgetListPage and BudgetDetailPage

Alternatives Considered

Instead of Could Use Tradeoff
Intl.DateTimeFormat date-fns or dayjs No new dependency needed — native API does exactly what's required (month+year locale formatting)
Lucide Zap icon for auth logo Custom SVG import from /public/favicon.svg The favicon.svg is a real brand asset — using an <img src="/favicon.svg"> is simpler and more authentic than a Lucide icon

Installation: No new packages needed.


Architecture Patterns

No structural changes. All new/modified files fit within the existing layout:

src/
├── components/
│   ├── shared/
│   │   └── PageShell.tsx          # Already exists — use as-is
│   └── dashboard/
│       ├── CategorySection.tsx    # Reuse in BudgetDetailPage
│       └── DashboardSkeleton.tsx  # Reference for new skeletons
├── pages/
│   ├── LoginPage.tsx              # Redesign auth card
│   ├── RegisterPage.tsx           # Redesign auth card
│   ├── BudgetDetailPage.tsx       # Upgrade group headers + diff tokens
│   ├── BudgetListPage.tsx         # Add PageShell + i18n month names
│   ├── CategoriesPage.tsx         # Add PageShell + header upgrade
│   ├── TemplatePage.tsx           # Add PageShell + header upgrade
│   ├── QuickAddPage.tsx           # Add PageShell
│   └── SettingsPage.tsx           # Wrap with PageShell, fix duplication
└── i18n/
    ├── en.json                    # Add month/year i18n keys, page descriptions
    └── de.json                    # German equivalents

Pattern 1: PageShell Adoption (7 pages)

What: Replace each page's inline <div> + <h1> + action button header with <PageShell title={t("...")} action={<Button>}>

When to use: Every authenticated page (all pages inside AppLayout)

Current pattern to replace:

// Before — every CRUD page looks like this:
<div>
  <div className="mb-6 flex items-center justify-between">
    <h1 className="text-2xl font-semibold">{t("categories.title")}</h1>
    <Button onClick={openCreate} size="sm">...</Button>
  </div>
  {/* content */}
</div>

Target pattern:

// After — consistent with DashboardPage
import { PageShell } from "@/components/shared/PageShell"

return (
  <PageShell
    title={t("categories.title")}
    description={t("categories.description")}  // optional
    action={<Button onClick={openCreate} size="sm">...</Button>}
  >
    {/* content */}
  </PageShell>
)

Note: SettingsPage already has a Card inside — the redundant <h1> heading above the card should be removed when wrapping with PageShell. The CardTitle inside can become the section header.

Pattern 2: Skeleton Loading States (6 pages)

What: Replace if (loading) return null with a page-appropriate skeleton

Current state: 6 pages use return null as loading state:

  • CategoriesPage — if (loading) return null
  • TemplatePage — if (loading) return null
  • BudgetListPage — if (loading) return null
  • BudgetDetailPage — if (loading) return null
  • QuickAddPage — if (loading) return null
  • SettingsPage — if (loading) return null

DashboardSkeleton as pattern reference:

// Source: src/components/dashboard/DashboardSkeleton.tsx
// Pattern: Skeleton primitive wrapped in Card layout to mirror real content shape
import { Skeleton } from "@/components/ui/skeleton"
import { Card, CardContent, CardHeader } from "@/components/ui/card"

// Table page skeleton (Categories, Template, BudgetDetail, QuickAdd):
function TablePageSkeleton() {
  return (
    <div className="space-y-4">
      {/* Mimics group header shape */}
      <div className="flex items-center gap-3 rounded-md border-l-4 border-muted bg-card px-4 py-3">
        <Skeleton className="h-4 w-32" />
      </div>
      {/* Mimics table rows */}
      {[1, 2, 3].map((i) => (
        <div key={i} className="flex items-center gap-4 px-4 py-2">
          <Skeleton className="h-4 w-40" />
          <Skeleton className="ml-auto h-4 w-20" />
        </div>
      ))}
    </div>
  )
}

Rule of Hooks compliance: Skeletons must be returned AFTER all hooks have been called. The existing pages already follow this — return null always appears after all useState/useEffect/derived-state code.

Pattern 3: Auth Page Redesign

What: Upgrade Login and Register from plain bg-background to brand-presence auth layout

Current state:

// LoginPage.tsx (line 35) — plain white background
<div className="flex min-h-screen items-center justify-center bg-background p-4">
  <Card className="w-full max-w-sm">
    <CardHeader className="text-center">
      <CardTitle className="text-2xl">{t("app.title")}</CardTitle>
    </CardHeader>

Target pattern:

// Muted background + logo above title + card accent
<div className="flex min-h-screen items-center justify-center bg-muted/60 p-4">
  <Card className="w-full max-w-sm border-t-4 border-t-primary">
    <CardHeader className="text-center">
      <img src="/favicon.svg" alt="SimpleFinanceDash" className="mx-auto mb-3 size-10" />
      <CardTitle className="text-2xl">{t("app.title")}</CardTitle>
    </CardHeader>

OAuth provider icons: Add SVG inline icons for Google and GitHub next to button text labels. Standard approach is a small inline SVG (16x16) or use a well-known path. Both Google G and GitHub Octocat have canonical simple SVG marks.

Pattern 4: BudgetDetail — Semantic Token Migration

What: Replace DifferenceCell's hardcoded color classes with semantic tokens

Current problem code (BudgetDetailPage.tsx lines 169173):

const color =
  diff > 0
    ? "text-green-600 dark:text-green-400"
    : diff < 0
      ? "text-red-600 dark:text-red-400"
      : "text-muted-foreground"

Correct pattern (matching CategorySection):

// Use the same tokens established in Phase 1
import { cn } from "@/lib/utils"

// isOver uses same direction-aware logic as CategorySection
const isOver = isSpendingType(type) ? actual > budgeted : actual < budgeted
const colorClass = isOver ? "text-over-budget" : diff !== 0 ? "text-on-budget" : "text-muted-foreground"

Note on text-on-budget vs text-muted-foreground: The CategorySection uses text-on-budget for non-over items in the header but text-muted-foreground for non-over item rows in the table body. For consistency, replicate that exact distinction.

Pattern 5: Group Header Upgrade (CategoriesPage, TemplatePage, BudgetDetailPage)

Current state: All three CRUD pages use the same small-dot pattern:

<div className="mb-2 flex items-center gap-2">
  <div className="size-3 rounded-full" style={{ backgroundColor: categoryColors[type] }} />
  <h2 className="text-sm font-medium text-muted-foreground">
    {t(`categories.types.${type}`)}
  </h2>
</div>

Recommended upgrade (enhanced dot — not full CategorySection card): For CRUD pages, a full left-border card with collapse is excessive (editing context favors always-expanded). Use a larger dot with bolder label for visual consistency without the overhead:

<div className="mb-2 flex items-center gap-3 border-l-4 bg-muted/30 px-3 py-2 rounded-sm"
     style={{ borderLeftColor: categoryColors[type] }}>
  <span className="text-sm font-semibold">{t(`categories.types.${type}`)}</span>
</div>

This matches the left-border accent visual language without the collapsible trigger complexity. CRUD pages are editing interfaces — always-expanded is correct UX.

Pattern 6: i18n — Locale-Aware Month Formatting

What: Replace hardcoded English month arrays and label strings with Intl.DateTimeFormat

Current problem in BudgetListPage.tsx (lines 3649):

// Hardcoded English month labels
const MONTHS = [
  { value: 1, label: "January" },
  // ... 11 more hardcoded English strings
]

And the dialog labels (lines 189, 210): <Label>Month</Label> and <Label>Year</Label> — hardcoded English.

Current problem in BudgetDetailPage.tsx (line 279):

return date.toLocaleDateString("en", { month: "long", year: "numeric" })
// Hardcoded "en" locale — always English regardless of user's language setting

Correct pattern:

// Use the i18n hook to get the active locale
const { i18n } = useTranslation()
const locale = i18n.language  // "en" or "de"

// Locale-aware month name generation
const monthOptions = Array.from({ length: 12 }, (_, i) => ({
  value: i + 1,
  label: new Intl.DateTimeFormat(locale, { month: "long" }).format(new Date(2000, i, 1)),
}))

// Locale-aware budget heading
function budgetHeading(startDate: string, locale: string): string {
  const [year, month] = startDate.split("-").map(Number)
  return new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" })
    .format(new Date(year ?? 0, (month ?? 1) - 1, 1))
}

New i18n keys needed for month dialog labels:

// en.json additions
"budgets": {
  "month": "Month",
  "year": "Year"
}

// de.json equivalents
"budgets": {
  "month": "Monat",
  "year": "Jahr"
}

Anti-Patterns to Avoid

  • Returning null during loading: Every page currently does if (loading) return null — replace all with skeleton components. This is the most visible UX gap.
  • Hardcoded locale string "en" in toLocaleDateString: BudgetDetailPage line 279 and BudgetListPage's budgetLabel helper both force English formatting. Must use i18n.language instead.
  • Inline <h1> + action div: 7 pages duplicate the exact pattern that PageShell was built to replace. Don't leave any of these after this phase.
  • Hardcoded text-green-600 / text-red-600: BudgetDetailPage DifferenceCell component bypasses the semantic token system established in Phase 1. This breaks dark mode and design consistency.
  • Double heading in SettingsPage: SettingsPage has both <h1 className="mb-6 text-2xl font-semibold"> and <CardTitle> both showing "Settings" — wrap with PageShell and remove the redundant h1.

Don't Hand-Roll

Problem Don't Build Use Instead Why
Locale-aware month names Custom MONTHS array with translations Intl.DateTimeFormat Already returns localized month names in any locale; zero maintenance
Loading placeholder UI Custom spinners or CSS animations Skeleton from ui/skeleton.tsx Already installed, same design language as DashboardSkeleton
Auth page logo New SVG asset or Lucide icon /public/favicon.svg via <img> Brand asset already exists, consistent with browser tab favicon
Direction-aware diff logic New computation function Extract from CategorySection.tsx (or import computeDiff) Logic is already correct and battle-tested in Phase 3
Group header card styling New component Inline left-border pattern from CategorySection's trigger element Consistent look without creating a new abstraction

Key insight: This phase adds no new libraries and creates minimal new abstractions. Almost everything needed is already in the codebase.


Common Pitfalls

Pitfall 1: Rules of Hooks — return null to Skeleton Migration

What goes wrong: Moving if (loading) return null to return a Skeleton component without checking that all hooks come before the condition. Why it happens: React requires all hooks to be called unconditionally on every render. If return null is currently AFTER all hooks, swapping to return <Skeleton> is safe. But if any hook was accidentally placed after the loading check, switching breaks the rules. How to avoid: Verify each page's hook ordering before replacing. In this codebase, all 6 pages that use return null have their hooks before the check (confirmed by code inspection). Safe to swap directly. Warning signs: TypeScript/eslint react-hooks/rules-of-hooks lint error.

Pitfall 2: i18n.language vs navigator.language

What goes wrong: Using navigator.language for locale instead of i18n.language, causing month names to display in the system locale rather than the user's chosen app locale. Why it happens: Both are "the user's language" but they represent different things — system preference vs app preference. How to avoid: Always use i18n.language from useTranslation() for Intl.DateTimeFormat locale argument. The user's locale preference is stored in their Supabase profile and applied via i18n.changeLanguage() in SettingsPage.

Pitfall 3: SettingsPage Double-Header

What goes wrong: Wrapping SettingsPage in <PageShell title={t("settings.title")}> without removing the existing <h1 className="mb-6 text-2xl font-semibold">, producing two "Settings" headings. Why it happens: SettingsPage is the only page that already has a Card structure — it's tempting to just prepend PageShell and leave existing content. How to avoid: Remove the <h1> on line 67 of SettingsPage when adding PageShell.

Pitfall 4: BudgetDetail DifferenceCell isIncome Logic vs Direction-Aware Logic

What goes wrong: The existing DifferenceCell uses a simplified isIncome boolean prop. Upgrading to the full direction-aware logic from Phase 3 must be consistent — saving and investment types should behave like income (under-earned = over-budget), not like expenses. Why it happens: The existing code only checks isIncome (type === "income"), missing saving/investment types. How to avoid: Use the same SPENDING_TYPES: CategoryType[] = ["bill", "variable_expense", "debt"] pattern from CategorySection.tsx. Any type NOT in this array uses the income/saving logic. Warning signs: Savings showing red when you've saved MORE than budgeted.

Pitfall 5: Auth Card Background Mismatch

What goes wrong: Using bg-muted (the Tailwind utility class) which maps to --color-muted (oklch 0.95) on top of bg-background (oklch 0.98) — the contrast is very subtle. If the wrong token is used, the intended visual separation disappears. Why it happens: The muted background needs enough contrast to make the white card "float." How to avoid: Use bg-muted/60 or bg-secondary instead. --color-secondary is oklch 0.93 vs card white oklch 1.0 — clearer separation. Or use bg-muted with a subtle shadow on the card.

Pitfall 6: Missing i18n Keys Causing Raw Key Strings

What goes wrong: Adding new translation calls (t("budgets.month")) before adding the key to both en.json and de.json — causes the raw key string to render on screen. Why it happens: It's easy to forget de.json when en.json is the primary authoring language. How to avoid: Always update both files atomically in the same task. The phase success criterion explicitly requires no raw i18n key strings in German locale.


Code Examples

Verified patterns from existing codebase:

PageShell API (src/components/shared/PageShell.tsx)

// PageShell signature — already final, no changes needed to the component itself
interface PageShellProps {
  title: string
  description?: string       // optional subtitle below title
  action?: React.ReactNode   // CTA slot (buttons, etc.)
  children: React.ReactNode
}

// Usage (from DashboardPage.tsx — the reference implementation):
<PageShell
  title={t("dashboard.title")}
  action={<MonthNavigator availableMonths={availableMonths} t={t} />}
>
  {/* page content */}
</PageShell>

Skeleton Primitive (src/components/ui/skeleton.tsx)

// Available for import in all page skeletons
import { Skeleton } from "@/components/ui/skeleton"

// Example: table row skeleton (for CategoriesPage, TemplatePage, etc.)
function TableRowSkeleton() {
  return (
    <div className="flex items-center gap-4 px-4 py-2.5 border-b border-border">
      <Skeleton className="h-4 w-36" />
      <Skeleton className="h-5 w-16 rounded-full" />
      <Skeleton className="ml-auto h-7 w-7 rounded-md" />
    </div>
  )
}

Group Header Upgrade Pattern

// Upgrade from plain dot to left-border accent header
// Before (all CRUD pages):
<div className="mb-2 flex items-center gap-2">
  <div className="size-3 rounded-full" style={{ backgroundColor: categoryColors[type] }} />
  <h2 className="text-sm font-medium text-muted-foreground">
    {t(`categories.types.${type}`)}
  </h2>
</div>

// After (enhanced dot — keeps always-expanded for editing context):
<div
  className="mb-2 flex items-center gap-3 rounded-sm border-l-4 bg-muted/30 px-3 py-2"
  style={{ borderLeftColor: categoryColors[type] }}
>
  <span className="text-sm font-semibold">{t(`categories.types.${type}`)}</span>
</div>

Semantic Token Migration (BudgetDetailPage)

// Before (hardcoded Tailwind colors — bypasses design tokens):
const color =
  diff > 0 ? "text-green-600 dark:text-green-400"
  : diff < 0 ? "text-red-600 dark:text-red-400"
  : "text-muted-foreground"

// After (semantic tokens — consistent with CategorySection):
// SPENDING_TYPES same as in CategorySection.tsx
const SPENDING_TYPES: CategoryType[] = ["bill", "variable_expense", "debt"]
function isOver(type: CategoryType, budgeted: number, actual: number): boolean {
  return SPENDING_TYPES.includes(type) ? actual > budgeted : actual < budgeted
}
// In render:
const over = isOver(type, item.budgeted_amount, item.actual_amount)
const colorClass = cn(
  "text-right tabular-nums",
  over ? "text-over-budget" : diff !== 0 ? "text-on-budget" : "text-muted-foreground"
)

Locale-Aware Month Name (replaces hardcoded MONTHS array)

// In BudgetListPage — replaces the 12-item MONTHS constant:
const { i18n } = useTranslation()
const locale = i18n.language  // "en" | "de"

const monthItems = useMemo(
  () =>
    Array.from({ length: 12 }, (_, i) => ({
      value: i + 1,
      label: new Intl.DateTimeFormat(locale, { month: "long" }).format(
        new Date(2000, i, 1)
      ),
    })),
  [locale]
)

// In BudgetDetailPage and BudgetListPage — replaces hardcoded "en" locale:
function budgetHeading(startDate: string, locale: string): string {
  const [year, month] = startDate.split("-").map(Number)
  return new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }).format(
    new Date(year ?? 0, (month ?? 1) - 1, 1)
  )
}

Auth Page Redesign Structure

// LoginPage / RegisterPage root structure
<div className="flex min-h-screen items-center justify-center bg-muted/60 p-4">
  <Card className="w-full max-w-sm border-t-4 border-t-primary shadow-lg">
    <CardHeader className="text-center pb-4">
      {/* App logo from public/favicon.svg */}
      <img
        src="/favicon.svg"
        alt="SimpleFinanceDash"
        className="mx-auto mb-3 size-10"
        aria-hidden
      />
      <CardTitle className="text-2xl">{t("app.title")}</CardTitle>
      <p className="text-sm text-muted-foreground">{t("auth.loginSubtitle")}</p>
    </CardHeader>
    {/* ... existing form content ... */}
  </Card>
</div>

State of the Art

Old Approach Current Approach When Changed Impact
No loading state (return null) Skeleton components Phase 4 Users see content-shaped placeholders instead of blank pages
Hardcoded color classes Semantic CSS tokens Phase 1 (dashboard), Phase 4 extends to BudgetDetail Dark mode support, single-source-of-truth for status colors
Hardcoded "en" locale i18n.language locale Phase 4 Month names now display in German when locale is "de"
Inline h1 + action div PageShell component Phase 4 extends Phase 1's PageShell Consistent header height and spacing across all pages
Plain auth background Muted background + brand logo Phase 4 Auth pages feel part of the same app, not a generic template

Still current (no change needed):

  • formatCurrency — already locale-aware via Intl.NumberFormat (no changes needed)
  • categoryColors / categoryLabels in palette.ts — complete and correct
  • AppLayout sidebar — no changes needed, routes unchanged
  • collapsible-open / collapsible-close animations — complete and correct

Open Questions

  1. BudgetDetail: Keep or remove TierBadge?

    • What we know: TierBadge shows Fixed/Variable/One-off on each line item. This metadata is useful for planning but adds visual noise when tracking actuals.
    • What's unclear: Whether the "editing actuals" context of BudgetDetailPage makes the tier less useful than in TemplatePage.
    • Recommendation: Remove tier column from BudgetDetailPage to reduce visual noise and align with the CategorySection display style (which shows no tier). Keep tier in TemplatePage since it's a planning interface.
  2. BudgetDetail: Collapsible or always-expanded?

    • What we know: BudgetDetailPage is an editing interface where users click inline cells to edit actual amounts. Collapsing sections would require an extra click before editing.
    • What's unclear: Whether the always-expanded view with full left-border card headers is sufficient, or whether the visual match to the dashboard collapsible style is more important.
    • Recommendation: Always-expanded with left-border headers. The visual upgrade (left-border cards, semantic tokens, badge chips) delivers the design consistency without the UX cost of collapsing an editing interface.
  3. BudgetDetail: StatCards or styled box for overall totals?

    • What we know: The current "overall totals" is a rounded-md border p-4 div with a 3-column grid.
    • What's unclear: Whether StatCard's Card+CardHeader+CardContent structure adds meaningful value over the existing styled box.
    • Recommendation: Keep styled box but upgrade to semantic tokens for the difference color. StatCards are designed for KPI highlight panels (like SummaryStrip) — a dense summary row inside a detail page fits better as a styled section.

Validation Architecture

nyquist_validation is enabled in .planning/config.json. However, this phase is 100% visual UI polish — no new logic, no new data flows, no new API calls. There are no automated tests in this project and none of the changes are unit-testable in the traditional sense.

Test Framework

Property Value
Framework None — no test framework configured
Config file None
Quick run command bun run build (TypeScript compile + Vite build — catches type errors)
Full suite command bun run build && bun run lint

Phase Requirements → Test Map

Req ID Behavior Test Type Automated Command File Exists?
UI-DESIGN-01 All 9 pages render with PageShell header Visual/manual bun run build (no TS errors) Wave 0
UI-AUTH-01 Auth pages show muted bg + logo + accent card Visual/manual bun run build Wave 0
UI-CATEGORIES-01 Categories group headers have left-border accent Visual/manual bun run build Wave 0
UI-TEMPLATE-01 Template group headers upgraded Visual/manual bun run build Wave 0
UI-BUDGETS-01 BudgetDetail uses semantic tokens, no text-green-600 grep check grep -r "text-green-600" src/pages/BudgetDetailPage.tsx || echo "CLEAN" Wave 0
UI-QUICKADD-01 QuickAdd page renders PageShell Visual/manual bun run build Wave 0
UI-SETTINGS-01 Settings page uses PageShell, no double heading Visual/manual bun run build Wave 0
UI-RESPONSIVE-01 No visual discontinuity between pages Visual/manual Manual browser navigation Wave 0

Sampling Rate

  • Per task commit: bun run build — TypeScript compile validates no type regressions
  • Per wave merge: bun run build && bun run lint
  • Phase gate: Manual browser review of all 9 pages in English and German locale before /gsd:verify-work

Wave 0 Gaps

  • No test files to create — this phase has no unit-testable logic
  • Recommend a manual checklist in VERIFY.md covering: all 9 pages load without null flash, German locale shows no raw keys, BudgetDetail shows no text-green-600/text-red-600 classes in DevTools

Sources

Primary (HIGH confidence)

  • Direct codebase inspection of all 9 page files — source of all findings
  • src/index.css — confirmed all OKLCH tokens, semantic status tokens, animation tokens
  • src/i18n/en.json and de.json — confirmed missing keys (month, year, page descriptions)
  • src/components/shared/PageShell.tsx — confirmed interface and implementation
  • src/components/dashboard/CategorySection.tsx — reference pattern for group headers
  • src/components/dashboard/DashboardSkeleton.tsx — reference pattern for skeletons
  • src/lib/palette.ts — confirmed categoryColors CSS variable map
  • package.json — confirmed no test framework is installed

Secondary (MEDIUM confidence)

  • MDN Web Docs pattern: Intl.DateTimeFormat for locale-aware month names — standard browser API, zero risk

Tertiary (LOW confidence)

  • None

Metadata

Confidence breakdown:

  • Standard stack: HIGH — no new libraries, all from direct package.json inspection
  • Architecture: HIGH — all patterns derived from existing codebase, not external research
  • Pitfalls: HIGH — all identified from actual code in the repo (specific file + line references)
  • i18n patterns: HIGH — Intl.DateTimeFormat is a stable native API

Research date: 2026-03-17 Valid until: Stable — no external dependencies to go stale. Re-verify only if major packages are upgraded.