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

647 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 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
### Recommended Project Structure
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:**
```tsx
// 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:**
```tsx
// 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:**
```tsx
// 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:**
```tsx
// 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:**
```tsx
// 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):**
```tsx
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):**
```tsx
// 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:
```tsx
<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:
```tsx
<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):**
```tsx
// 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):**
```tsx
return date.toLocaleDateString("en", { month: "long", year: "numeric" })
// Hardcoded "en" locale — always English regardless of user's language setting
```
**Correct pattern:**
```tsx
// 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:**
```json
// 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)
```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)
```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
```tsx
// 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)
```tsx
// 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)
```tsx
// 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
```tsx
// 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.