diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 8778ba1..e15a5ff 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -81,11 +81,12 @@ Plans:
3. Budget Detail page displays category groups with the same color-accented card style and line-item presentation as the dashboard collapsible sections
4. Navigating between any two pages in the app produces no jarring visual discontinuity in layout, color, or typography
5. Switching the app to German locale shows fully translated text on every page — no raw i18n key strings visible anywhere
-**Plans**: TBD
+**Plans**: 3 plans
Plans:
-- [ ] 04-01: TBD
-- [ ] 04-02: TBD
+- [ ] 04-01-PLAN.md — Redesign auth pages (Login, Register) with brand presence, muted background, card accent, OAuth SVG icons
+- [ ] 04-02-PLAN.md — Upgrade CRUD pages (Categories, Template, QuickAdd, Settings) with PageShell, skeletons, group header accents
+- [ ] 04-03-PLAN.md — Upgrade budget pages (BudgetList, BudgetDetail) with semantic tokens, direction-aware diff, locale-aware months, skeletons
## Requirements Traceability
@@ -137,4 +138,4 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4
| 1. Design Foundation and Primitives | 2/2 | Complete | 2026-03-16 |
| 2. Dashboard Charts and Layout | 3/3 | Complete | 2026-03-16 |
| 3. Collapsible Dashboard Sections | 1/2 | In Progress| |
-| 4. Full-App Design Consistency | 0/TBD | Not started | - |
+| 4. Full-App Design Consistency | 0/3 | Not started | - |
diff --git a/.planning/phases/04-full-app-design-consistency/04-01-PLAN.md b/.planning/phases/04-full-app-design-consistency/04-01-PLAN.md
new file mode 100644
index 0000000..f1da840
--- /dev/null
+++ b/.planning/phases/04-full-app-design-consistency/04-01-PLAN.md
@@ -0,0 +1,211 @@
+---
+phase: 04-full-app-design-consistency
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - src/pages/LoginPage.tsx
+ - src/pages/RegisterPage.tsx
+ - src/i18n/en.json
+ - src/i18n/de.json
+autonomous: true
+requirements: [UI-AUTH-01, UI-DESIGN-01]
+
+must_haves:
+ truths:
+ - "Login page shows muted background with the card floating on top, app logo above title"
+ - "Register page matches Login page design — same background, logo, card accent treatment"
+ - "OAuth buttons (Google, GitHub) display provider SVG icons next to text labels"
+ - "Auth subtitle text appears below the app title inside the card"
+ - "Switching to German locale shows fully translated auth page text"
+ artifacts:
+ - path: "src/pages/LoginPage.tsx"
+ provides: "Redesigned login page with muted bg, logo, card accent, OAuth icons"
+ contains: "bg-muted"
+ - path: "src/pages/RegisterPage.tsx"
+ provides: "Redesigned register page matching login design"
+ contains: "bg-muted"
+ - path: "src/i18n/en.json"
+ provides: "Auth subtitle i18n keys"
+ contains: "auth.loginSubtitle"
+ - path: "src/i18n/de.json"
+ provides: "German auth subtitle translations"
+ contains: "auth.loginSubtitle"
+ key_links:
+ - from: "src/pages/LoginPage.tsx"
+ to: "/favicon.svg"
+ via: "img src for app logo"
+ pattern: 'src="/favicon.svg"'
+ - from: "src/pages/RegisterPage.tsx"
+ to: "/favicon.svg"
+ via: "img src for app logo"
+ pattern: 'src="/favicon.svg"'
+---
+
+
+Redesign the Login and Register pages with brand presence and visual polish matching the established design system.
+
+Purpose: Auth pages are the first impression of the app. Currently they use a plain `bg-background` with a bare card. This plan upgrades them to use a muted background, app logo, card accent styling, and provider SVG icons on OAuth buttons -- establishing visual consistency from the very first screen.
+
+Output: Redesigned LoginPage.tsx, RegisterPage.tsx, and new i18n keys for auth subtitles.
+
+
+
+@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
+@/home/jlmak/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/04-full-app-design-consistency/04-CONTEXT.md
+@.planning/phases/04-full-app-design-consistency/04-RESEARCH.md
+
+
+
+
+From src/pages/LoginPage.tsx (current structure to modify):
+- Root: `
`
+- Card: ``
+- Has OAuth buttons for Google and GitHub (text-only, no icons)
+- Has Separator with "Or continue with" text
+
+From src/pages/RegisterPage.tsx (current structure to modify):
+- Same root div pattern as LoginPage
+- No OAuth buttons (only email/password form)
+- No Separator
+
+From src/i18n/en.json (existing auth keys):
+```json
+"auth": {
+ "login": "Login",
+ "register": "Register",
+ "email": "Email",
+ "password": "Password",
+ "displayName": "Display Name",
+ "noAccount": "Don't have an account?",
+ "hasAccount": "Already have an account?",
+ "orContinueWith": "Or continue with"
+}
+```
+
+Logo asset: `/public/favicon.svg` -- stylized lightning-bolt SVG in purple (#863bff). Use via ``.
+
+
+
+
+
+
+ Task 1: Redesign LoginPage with brand presence and OAuth icons
+ src/pages/LoginPage.tsx, src/i18n/en.json, src/i18n/de.json
+
+Modify LoginPage.tsx:
+
+1. **Background:** Change root div className from `bg-background` to `bg-muted/60`.
+
+2. **Card accent:** Add `border-t-4 border-t-primary shadow-lg` to the Card className.
+
+3. **App logo:** Inside CardHeader, above the CardTitle, add:
+ ```tsx
+
+ ```
+
+4. **Subtitle:** Below the CardTitle, add:
+ ```tsx
+
{t("auth.loginSubtitle")}
+ ```
+
+5. **OAuth provider SVG icons:** Replace the plain text-only Google and GitHub buttons with inline SVG icons. Add a small (size-4) SVG before the text label in each button:
+
+ For Google button, add before "Google" text:
+ ```tsx
+
+ ```
+
+ For GitHub button, add before "GitHub" text:
+ ```tsx
+
+ ```
+
+ Add `gap-2` to each Button's className to space the icon and text. The buttons already have `className="flex-1"` -- add `gap-2` via the className string.
+
+6. **i18n keys:** Add to en.json inside the "auth" object:
+ - `"loginSubtitle": "Sign in to your account"`
+ - `"registerSubtitle": "Create a new account"`
+
+ Add to de.json inside the "auth" object:
+ - `"loginSubtitle": "Melde dich bei deinem Konto an"`
+ - `"registerSubtitle": "Erstelle ein neues Konto"`
+
+IMPORTANT: Update both en.json and de.json atomically in this task. Do not leave any raw i18n key strings.
+
+
+ cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build
+
+ LoginPage shows muted/60 background, primary-colored top border on card, favicon.svg logo above title, "Sign in to your account" subtitle, Google SVG icon + GitHub SVG icon on OAuth buttons. Both en.json and de.json have the new auth subtitle keys.
+
+
+
+ Task 2: Redesign RegisterPage to match LoginPage treatment
+ src/pages/RegisterPage.tsx
+
+Modify RegisterPage.tsx to match the LoginPage design established in Task 1:
+
+1. **Background:** Change root div className from `bg-background` to `bg-muted/60`.
+
+2. **Card accent:** Add `border-t-4 border-t-primary shadow-lg` to the Card className.
+
+3. **App logo:** Inside CardHeader, above the CardTitle, add:
+ ```tsx
+
+ ```
+
+4. **Subtitle:** Below the CardTitle, add:
+ ```tsx
+
{t("auth.registerSubtitle")}
+ ```
+ The i18n key `auth.registerSubtitle` was already added in Task 1.
+
+5. **CardHeader padding:** Add `pb-4` to CardHeader className to match LoginPage spacing: `className="text-center pb-4"`.
+
+Also apply `pb-4` to LoginPage's CardHeader if not already done in Task 1 (add `className="text-center pb-4"`).
+
+Do NOT add OAuth buttons to RegisterPage -- it only has email/password registration. The existing "Already have an account?" link stays as-is.
+
+
+ cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build
+
+ RegisterPage shows same muted/60 background, same card accent (border-t-4 primary, shadow-lg), same favicon logo, and register-specific subtitle. Visual parity with LoginPage minus OAuth buttons.
+
+
+
+
+
+- `bun run build` compiles without TypeScript errors
+- `grep -c "bg-background" src/pages/LoginPage.tsx src/pages/RegisterPage.tsx` returns 0 for both files (old pattern fully replaced)
+- `grep -c "bg-muted" src/pages/LoginPage.tsx src/pages/RegisterPage.tsx` returns 1 for each (new pattern applied)
+- `grep "loginSubtitle" src/i18n/en.json src/i18n/de.json` returns matches in both files
+- `grep "registerSubtitle" src/i18n/en.json src/i18n/de.json` returns matches in both files
+
+
+
+- Both auth pages use `bg-muted/60` background instead of `bg-background`
+- Both auth pages show the app logo (`favicon.svg`) above the title
+- Both auth pages have a card with `border-t-4 border-t-primary shadow-lg`
+- LoginPage OAuth buttons show Google and GitHub SVG icons
+- Both en.json and de.json have `auth.loginSubtitle` and `auth.registerSubtitle` keys
+- `bun run build` passes
+
+
+
diff --git a/.planning/phases/04-full-app-design-consistency/04-02-PLAN.md b/.planning/phases/04-full-app-design-consistency/04-02-PLAN.md
new file mode 100644
index 0000000..c3e1c0f
--- /dev/null
+++ b/.planning/phases/04-full-app-design-consistency/04-02-PLAN.md
@@ -0,0 +1,405 @@
+---
+phase: 04-full-app-design-consistency
+plan: 02
+type: execute
+wave: 2
+depends_on: [04-01]
+files_modified:
+ - src/pages/CategoriesPage.tsx
+ - src/pages/TemplatePage.tsx
+ - src/pages/QuickAddPage.tsx
+ - src/pages/SettingsPage.tsx
+ - src/i18n/en.json
+ - src/i18n/de.json
+autonomous: true
+requirements: [UI-CATEGORIES-01, UI-TEMPLATE-01, UI-QUICKADD-01, UI-SETTINGS-01, UI-DESIGN-01]
+
+must_haves:
+ truths:
+ - "Categories page uses PageShell for header with title and Add Category button"
+ - "Categories page shows category group headers with left-border accent styling"
+ - "Categories page shows skeleton loading state instead of blank screen"
+ - "Template page uses PageShell with inline-editable name and Add Item button"
+ - "Template page shows category group headers with left-border accent styling"
+ - "QuickAdd page uses PageShell for header"
+ - "QuickAdd page shows skeleton loading state instead of blank screen"
+ - "Settings page uses PageShell with no duplicate heading"
+ - "Settings page shows skeleton loading state instead of blank screen"
+ - "German locale shows all text translated on all four pages"
+ artifacts:
+ - path: "src/pages/CategoriesPage.tsx"
+ provides: "PageShell adoption, skeleton, group header upgrade"
+ contains: "PageShell"
+ - path: "src/pages/TemplatePage.tsx"
+ provides: "PageShell adoption, skeleton, group header upgrade"
+ contains: "PageShell"
+ - path: "src/pages/QuickAddPage.tsx"
+ provides: "PageShell adoption, skeleton"
+ contains: "PageShell"
+ - path: "src/pages/SettingsPage.tsx"
+ provides: "PageShell adoption, skeleton, no double heading"
+ contains: "PageShell"
+ key_links:
+ - from: "src/pages/CategoriesPage.tsx"
+ to: "src/components/shared/PageShell.tsx"
+ via: "import and render"
+ pattern: 'import.*PageShell.*from.*shared/PageShell'
+ - from: "src/pages/SettingsPage.tsx"
+ to: "src/components/shared/PageShell.tsx"
+ via: "import and render — replacing redundant h1"
+ pattern: 'import.*PageShell.*from.*shared/PageShell'
+---
+
+
+Apply PageShell, skeleton loading states, and group header upgrades to the four CRUD/settings pages (Categories, Template, QuickAdd, Settings).
+
+Purpose: These four authenticated pages currently use inline `
` + action div headers, return `null` while loading, and use small-dot group headers. This plan upgrades them to match the dashboard's design language -- consistent headers via PageShell, skeleton loading placeholders, and left-border accent group headers.
+
+Output: Four updated page components with consistent design system application, plus new i18n keys for page descriptions.
+
+
+
+@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
+@/home/jlmak/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/04-full-app-design-consistency/04-CONTEXT.md
+@.planning/phases/04-full-app-design-consistency/04-RESEARCH.md
+
+
+From src/components/shared/PageShell.tsx:
+```tsx
+interface PageShellProps {
+ title: string
+ description?: string
+ action?: React.ReactNode
+ children: React.ReactNode
+}
+export function PageShell({ title, description, action, children }: PageShellProps)
+```
+
+From src/components/ui/skeleton.tsx:
+```tsx
+// Skeleton primitive -- use for building page-specific loading states
+import { Skeleton } from "@/components/ui/skeleton"
+// Usage:
+```
+
+From src/lib/palette.ts:
+```tsx
+export const categoryColors: Record
+// Maps category type to CSS variable string like "var(--color-income)"
+```
+
+Group header upgrade pattern (from RESEARCH.md):
+```tsx
+// Replace plain dot headers with left-border accent
+
+ {t(`categories.types.${type}`)}
+
+```
+
+Current pattern in all CRUD pages to replace:
+```tsx
+
+
+
{t(`categories.types.${type}`)}
+
+```
+
+
+
+
+
+
+ Task 1: Upgrade CategoriesPage and TemplatePage with PageShell, skeletons, and group headers
+ src/pages/CategoriesPage.tsx, src/pages/TemplatePage.tsx, src/i18n/en.json, src/i18n/de.json
+
+**CategoriesPage.tsx changes:**
+
+1. **Import PageShell:** Add `import { PageShell } from "@/components/shared/PageShell"` and `import { Skeleton } from "@/components/ui/skeleton"`.
+
+2. **Replace header:** Remove the `
+
+ )
+ ```
+
+4. **Group header upgrade:** Replace the plain dot group header pattern in the `grouped.map` with the left-border accent pattern:
+ ```tsx
+
+ {t(`categories.types.${type}`)}
+
+ ```
+ Remove the old `
` block with the `size-3 rounded-full` dot and `
`.
+
+**TemplatePage.tsx changes:**
+
+1. **Import PageShell and Skeleton:** Same imports as CategoriesPage.
+
+2. **Replace header:** The TemplatePage header has an inline-editable `TemplateName` component. Wrap with PageShell, putting TemplateName as the title area. Since PageShell accepts a `title` string but TemplateName is a component, use PageShell differently here:
+
+ Instead of wrapping with PageShell using `title` prop, replace the header div with PageShell but pass the template name as a plain string title when NOT editing. Actually, the TemplateName component handles its own editing state inline. The cleanest approach: keep the TemplateName component but wrap the page content differently.
+
+ Replace the entire page structure:
+ ```tsx
+
+ {/* Header */}
+
+
+
+
+ ...
+
+ ```
+
+ With:
+ ```tsx
+
+
+
+ }
+ >
+ ...
+
+ ```
+
+ **Note:** The TemplateName inline-edit functionality is a nice feature that will be lost if we just use a plain title string. To preserve it while using PageShell: remove the `title` prop from PageShell and instead render TemplateName inside the PageShell children, ABOVE the content. Actually, the simplest correct approach is to NOT use PageShell's title prop for TemplatePage -- instead, pass a custom `action` that includes the Add button, and render TemplateName as the first child inside PageShell with the title styling matching PageShell's own h1 style. But this defeats the purpose.
+
+ Best approach: Use PageShell for the layout but pass the TemplateName component as a React node for the title slot. Since PageShell only accepts `title: string`, we need to slightly modify the approach. Just use PageShell's wrapper layout manually:
+
+ Replace the header with:
+ ```tsx
+
+
+
+
+
+
+
+ {/* rest of content */}
+
+ ```
+
+ This mirrors PageShell's exact DOM structure (flex flex-col gap-6 > flex items-start justify-between gap-4) without importing PageShell, since TemplateName is a custom component that cannot be a plain string. This keeps visual consistency.
+
+ Additionally, update TemplateName's `
` to use `className="text-2xl font-semibold tracking-tight"` (add `tracking-tight` to match PageShell's h1 styling).
+
+3. **Skeleton loading:** Replace `if (loading) return null` with a skeleton that mirrors the template page layout:
+ ```tsx
+ if (loading) return (
+
+
+
+
+
+
+ {[1, 2].map((i) => (
+
+
+
+
+ {[1, 2, 3].map((j) => (
+
+
+
+
+
+
+ ))}
+
+ ))}
+
+
+ )
+ ```
+
+4. **Group header upgrade:** Same left-border accent pattern as CategoriesPage. Replace the dot+h2 pattern in grouped.map with:
+ ```tsx
+
+ {t(`categories.types.${type}`)}
+
+ ```
+
+**i18n: No new keys needed for this task.** Categories and Template pages already have all required i18n keys. The page descriptions are optional (Claude's discretion) -- skip them for these two pages since the page purpose is self-evident from the content.
+
+
+ cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build
+
+ CategoriesPage and TemplatePage both show: consistent header layout matching PageShell spacing (flex-col gap-6), left-border accent group headers replacing dot headers, skeleton loading states replacing `return null`. No inline h1 header pattern remains. Build passes.
+
+
+
+ Task 2: Upgrade QuickAddPage and SettingsPage with PageShell and skeletons
+ src/pages/QuickAddPage.tsx, src/pages/SettingsPage.tsx
+
+**QuickAddPage.tsx changes:**
+
+1. **Import PageShell and Skeleton:** Add `import { PageShell } from "@/components/shared/PageShell"` and `import { Skeleton } from "@/components/ui/skeleton"`.
+
+2. **Replace header:** Remove the `
` root since PageShell provides the outer container.
+
+3. **Skeleton loading:** Replace `if (loading) return null` with:
+ ```tsx
+ if (loading) return (
+
+
+ {[1, 2, 3, 4, 5].map((i) => (
+
+
+
+
+
+ ))}
+
+
+ )
+ ```
+
+**SettingsPage.tsx changes:**
+
+1. **Import PageShell and Skeleton:** Add `import { PageShell } from "@/components/shared/PageShell"` and `import { Skeleton } from "@/components/ui/skeleton"`.
+
+2. **Remove duplicate heading:** Delete the `
{t("settings.title")}
` on line 67. This creates a double heading since the Card below also has a CardTitle with "Settings".
+
+3. **Wrap with PageShell:** Replace the `
` root with:
+ ```tsx
+
+
+
+ {/* Remove CardHeader with CardTitle since PageShell provides the title.
+ Keep CardContent as-is. */}
+
+ {/* existing form fields unchanged */}
+
+
+
+
+ ```
+ Remove the CardHeader and CardTitle entirely -- PageShell provides the page-level title, and the Card should just contain the form. Add `pt-6` to CardContent's className since without CardHeader the content needs top padding.
+
+4. **Skeleton loading:** Replace `if (loading) return null` with:
+ ```tsx
+ if (loading) return (
+
+
+
+
+ {[1, 2, 3].map((i) => (
+
+
+
+
+ ))}
+
+
+
+
+
+ )
+ ```
+
+5. **Clean up unused imports:** After removing CardHeader and CardTitle usage, update the import to: `import { Card, CardContent } from "@/components/ui/card"`. Remove `CardHeader` and `CardTitle` from the import.
+
+**No i18n changes needed for this task.** QuickAdd and Settings pages already have all required translation keys.
+
+
+ cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build
+
+ QuickAddPage uses PageShell with title and action button, shows skeleton on load. SettingsPage uses PageShell with no double "Settings" heading, Card contains only the form, shows skeleton on load. No `return null` loading patterns remain in either file. Build passes.
+
+
+
+
+
+- `bun run build` compiles without TypeScript errors
+- `grep -c "return null" src/pages/CategoriesPage.tsx src/pages/TemplatePage.tsx src/pages/QuickAddPage.tsx src/pages/SettingsPage.tsx` returns 0 for all files
+- `grep -c "size-3 rounded-full" src/pages/CategoriesPage.tsx src/pages/TemplatePage.tsx` returns 0 for both (old dot headers removed)
+- `grep -c "border-l-4" src/pages/CategoriesPage.tsx src/pages/TemplatePage.tsx` returns at least 1 for each (new accent headers applied)
+- `grep -c "PageShell" src/pages/CategoriesPage.tsx src/pages/QuickAddPage.tsx src/pages/SettingsPage.tsx` returns at least 1 for each
+
+
+
+- All four pages (Categories, Template, QuickAdd, Settings) show consistent PageShell-style headers
+- All four pages show skeleton loading states instead of blank screens
+- Categories and Template pages show left-border accent group headers
+- Settings page has exactly ONE "Settings" heading (via PageShell), not two
+- `bun run build` passes
+- No `return null` loading patterns remain in any of the four files
+
+
+
diff --git a/.planning/phases/04-full-app-design-consistency/04-03-PLAN.md b/.planning/phases/04-full-app-design-consistency/04-03-PLAN.md
new file mode 100644
index 0000000..f655a2d
--- /dev/null
+++ b/.planning/phases/04-full-app-design-consistency/04-03-PLAN.md
@@ -0,0 +1,448 @@
+---
+phase: 04-full-app-design-consistency
+plan: 03
+type: execute
+wave: 3
+depends_on: [04-02]
+files_modified:
+ - src/pages/BudgetListPage.tsx
+ - src/pages/BudgetDetailPage.tsx
+ - src/i18n/en.json
+ - src/i18n/de.json
+autonomous: true
+requirements: [UI-BUDGETS-01, UI-RESPONSIVE-01, UI-DESIGN-01]
+
+must_haves:
+ truths:
+ - "BudgetList page uses PageShell for header with title and New Budget button"
+ - "BudgetList page shows locale-aware month names (German month names when locale is de)"
+ - "BudgetList page shows skeleton loading state instead of blank screen"
+ - "BudgetList dialog month/year labels are translated (not hardcoded English)"
+ - "BudgetDetail page uses PageShell with locale-aware month heading"
+ - "BudgetDetail page shows left-border accent group headers matching dashboard style"
+ - "BudgetDetail page uses semantic color tokens (text-over-budget/text-on-budget) instead of text-green-600/text-red-600"
+ - "BudgetDetail page uses direction-aware diff logic (spending over when actual > budgeted; income/saving/investment over when actual < budgeted)"
+ - "BudgetDetail page shows skeleton loading state instead of blank screen"
+ - "No hardcoded 'en' locale string remains in any budget page"
+ - "Navigating between all pages produces no jarring visual discontinuity"
+ artifacts:
+ - path: "src/pages/BudgetListPage.tsx"
+ provides: "PageShell adoption, locale-aware months, skeleton, i18n labels"
+ contains: "PageShell"
+ - path: "src/pages/BudgetDetailPage.tsx"
+ provides: "PageShell, semantic tokens, direction-aware diff, group headers, skeleton"
+ contains: "text-over-budget"
+ - path: "src/i18n/en.json"
+ provides: "Budget month/year dialog labels and group total i18n key"
+ contains: "budgets.month"
+ - path: "src/i18n/de.json"
+ provides: "German budget translations"
+ contains: "budgets.month"
+ key_links:
+ - from: "src/pages/BudgetDetailPage.tsx"
+ to: "semantic CSS tokens"
+ via: "text-over-budget / text-on-budget classes"
+ pattern: "text-over-budget|text-on-budget"
+ - from: "src/pages/BudgetListPage.tsx"
+ to: "i18n.language"
+ via: "Intl.DateTimeFormat locale parameter"
+ pattern: "Intl\\.DateTimeFormat"
+ - from: "src/pages/BudgetDetailPage.tsx"
+ to: "i18n.language"
+ via: "Intl.DateTimeFormat locale parameter"
+ pattern: "Intl\\.DateTimeFormat"
+---
+
+
+Upgrade BudgetListPage and BudgetDetailPage with PageShell, semantic color tokens, direction-aware diff logic, locale-aware month formatting, and skeleton loading states.
+
+Purpose: These are the most complex pages in the app. BudgetDetailPage currently uses hardcoded `text-green-600`/`text-red-600` color classes that bypass the design token system, a simplified `isIncome` boolean that mishandles saving/investment types, and a hardcoded `"en"` locale for month formatting. BudgetListPage has a hardcoded English MONTHS array. This plan migrates both to the established design system patterns from Phases 1-3.
+
+Output: Two fully upgraded budget pages with consistent visual language, correct semantic tokens, and locale-aware formatting.
+
+
+
+@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
+@/home/jlmak/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/04-full-app-design-consistency/04-CONTEXT.md
+@.planning/phases/04-full-app-design-consistency/04-RESEARCH.md
+
+
+From src/components/shared/PageShell.tsx:
+```tsx
+interface PageShellProps {
+ title: string
+ description?: string
+ action?: React.ReactNode
+ children: React.ReactNode
+}
+export function PageShell({ title, description, action, children }: PageShellProps)
+```
+
+From src/components/dashboard/CategorySection.tsx (direction-aware diff logic to replicate):
+```tsx
+const SPENDING_TYPES: CategoryType[] = ["bill", "variable_expense", "debt"]
+
+function isSpendingType(type: CategoryType): boolean {
+ return SPENDING_TYPES.includes(type)
+}
+
+function computeDiff(budgeted: number, actual: number, type: CategoryType): { diff: number; isOver: boolean } {
+ if (isSpendingType(type)) {
+ return { diff: budgeted - actual, isOver: actual > budgeted }
+ }
+ return { diff: actual - budgeted, isOver: actual < budgeted }
+}
+```
+
+Semantic color classes (from index.css Phase 1):
+- `text-over-budget` -- red, for amounts exceeding budget
+- `text-on-budget` -- green, for amounts within budget
+- `text-muted-foreground` -- neutral, for zero difference
+
+Group header pattern (established in Plan 02):
+```tsx
+
+ {t(`categories.types.${type}`)}
+
+```
+
+Locale-aware month formatting pattern:
+```tsx
+const { i18n } = useTranslation()
+const locale = i18n.language
+// Replace hardcoded MONTHS array:
+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]
+)
+// Replace hardcoded "en" in toLocaleDateString:
+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)
+ )
+}
+```
+
+
+
+
+
+
+ Task 1: Upgrade BudgetListPage with PageShell, locale-aware months, skeleton, and i18n labels
+ src/pages/BudgetListPage.tsx, src/i18n/en.json, src/i18n/de.json
+
+**BudgetListPage.tsx changes:**
+
+1. **Import PageShell, Skeleton, useMemo:** Add:
+ ```tsx
+ import { useState, useMemo } from "react"
+ import { PageShell } from "@/components/shared/PageShell"
+ import { Skeleton } from "@/components/ui/skeleton"
+ ```
+
+2. **Remove hardcoded MONTHS array:** Delete the entire `const MONTHS = [...]` constant (lines 36-49).
+
+3. **Add locale-aware month generation:** Inside the component, after the existing hooks and state, add:
+ ```tsx
+ const { t, i18n } = useTranslation()
+ const locale = i18n.language
+
+ 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]
+ )
+ ```
+ Update the existing `useTranslation()` call to also destructure `i18n`: change `const { t } = useTranslation()` to `const { t, i18n } = useTranslation()`.
+
+ **Rules of Hooks:** The `useMemo` must be declared BEFORE the `if (loading)` check. Since `useTranslation` is already before it, just place `useMemo` right after the state declarations and before `if (loading)`.
+
+4. **Fix budgetLabel to use locale:** Replace the `budgetLabel` helper function to use locale:
+ ```tsx
+ function budgetLabel(budget: Budget, locale: string): string {
+ const [year, month] = budget.start_date.split("-").map(Number)
+ return new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }).format(
+ new Date(year ?? 0, (month ?? 1) - 1, 1)
+ )
+ }
+ ```
+ Update all call sites to pass `locale`: `budgetLabel(budget, locale)` and `budgetLabel(result, locale)`.
+
+5. **Replace MONTHS usage in dialog:** In the month Select, replace `MONTHS.map((m) =>` with `monthItems.map((m) =>`. The shape is identical (`{ value, label }`).
+
+6. **Replace hardcoded "Month" and "Year" labels:** Replace the `` and `` in the new budget dialog with:
+ ```tsx
+
+ // and
+
+ ```
+
+7. **Replace header with PageShell:** Remove the `
+
+ )
+ ```
+
+**i18n additions (en.json):** Add inside the "budgets" object:
+```json
+"month": "Month",
+"year": "Year",
+"total": "{{label}} Total"
+```
+
+**i18n additions (de.json):** Add inside the "budgets" object:
+```json
+"month": "Monat",
+"year": "Jahr",
+"total": "{{label}} Gesamt"
+```
+
+
+ cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build
+
+ BudgetListPage uses PageShell, shows locale-aware month names via Intl.DateTimeFormat (no hardcoded English MONTHS array), dialog labels use i18n keys, skeleton replaces null loading state, budgetLabel uses i18n.language locale. Both en.json and de.json have month/year/total keys. Build passes.
+
+
+
+ Task 2: Upgrade BudgetDetailPage with semantic tokens, direction-aware diff, PageShell, group headers, and skeleton
+ src/pages/BudgetDetailPage.tsx
+
+**BudgetDetailPage.tsx changes:**
+
+1. **Import additions:** Add:
+ ```tsx
+ import { cn } from "@/lib/utils"
+ import { PageShell } from "@/components/shared/PageShell"
+ import { Skeleton } from "@/components/ui/skeleton"
+ ```
+
+2. **Add direction-aware diff logic:** At module level (above the component), add the same SPENDING_TYPES pattern from CategorySection:
+ ```tsx
+ const SPENDING_TYPES: CategoryType[] = ["bill", "variable_expense", "debt"]
+
+ function isSpendingType(type: CategoryType): boolean {
+ return SPENDING_TYPES.includes(type)
+ }
+ ```
+
+3. **Rewrite DifferenceCell:** Replace the entire DifferenceCell component. Change its props: remove `isIncome`, add `type: CategoryType`:
+ ```tsx
+ function DifferenceCell({
+ budgeted,
+ actual,
+ currency,
+ type,
+ }: {
+ budgeted: number
+ actual: number
+ currency: string
+ type: CategoryType
+ }) {
+ const isOver = isSpendingType(type)
+ ? actual > budgeted
+ : actual < budgeted
+ const diff = isSpendingType(type)
+ ? budgeted - actual
+ : actual - budgeted
+
+ return (
+
+ {formatCurrency(Math.abs(diff), currency)}
+ {diff < 0 ? " over" : ""}
+
+ )
+ }
+ ```
+
+4. **Update DifferenceCell call sites:** In the grouped.map render:
+ - Remove the `const isIncome = type === "income"` line.
+ - Change `` to `` in BOTH places (per-item row and group footer).
+
+5. **Remove TierBadge from BudgetDetailPage:** Per research recommendation, remove the tier column from BudgetDetailPage to reduce visual noise and align with CategorySection display. This is Claude's discretion per CONTEXT.md.
+ - Remove the TierBadge component definition from BudgetDetailPage (keep it in TemplatePage where it belongs).
+ - Remove the `{t("categories.type")}` column from the table header.
+ - Remove the `` from each table row.
+ - Update the TableFooter `colSpan` accordingly: the first footer cell changes from `colSpan={2}` to no colSpan (or `colSpan={1}`), and the last footer cell changes appropriately.
+ - Remove the `Badge` import if no longer used elsewhere in this file.
+
+6. **Group header upgrade:** Replace the dot+h2 pattern in grouped.map with:
+ ```tsx
+
+ {t(`categories.types.${type}`)}
+
+ ```
+
+7. **Fix locale for headingLabel:** Update the `headingLabel` function. Destructure `i18n` from `useTranslation`: change `const { t } = useTranslation()` to `const { t, i18n } = useTranslation()`. Then:
+ ```tsx
+ function headingLabel(): string {
+ if (!budget) return ""
+ const [year, month] = budget.start_date.split("-").map(Number)
+ return new Intl.DateTimeFormat(i18n.language, { month: "long", year: "numeric" }).format(
+ new Date(year ?? 0, (month ?? 1) - 1, 1)
+ )
+ }
+ ```
+
+8. **Fix overall totals section:** The overall totals box at the bottom uses hardcoded `text-green-600`/`text-red-600`. Replace with semantic tokens:
+ ```tsx
+
= 0 ? "text-on-budget" : "text-over-budget"
+ )}
+ >
+ ```
+ This replaces the inline ternary with `text-green-600 dark:text-green-400` / `text-red-600 dark:text-red-400`.
+
+9. **Fix group footer "Total" label:** The group footer currently has hardcoded English ` Total`:
+ ```tsx
+
+ {t(`categories.types.${type}`)} Total
+
+ ```
+ Replace with i18n:
+ ```tsx
+
+ {t("budgets.total", { label: t(`categories.types.${type}`) })}
+
+ ```
+ The `budgets.total` key was added in Task 1's i18n step: `"total": "{{label}} Total"` / `"total": "{{label}} Gesamt"`.
+
+10. **Replace header with PageShell:** Replace the back link + header section. Keep the back link as a child of PageShell:
+ ```tsx
+
+
+ {t("budgets.addItem")}
+
+ }
+ >
+
+
+ {t("budgets.title")}
+
+ {/* rest of content */}
+
+ ```
+ The `-mt-4` on the back link compensates for PageShell's `gap-6`, pulling it closer to the header.
+
+11. **Skeleton loading:** Replace `if (loading) return null` with:
+ ```tsx
+ if (loading) return (
+
+
+
+ {[1, 2, 3].map((i) => (
+
+
+
+
+ {[1, 2].map((j) => (
+
+
+
+
+
+
+ ))}
+
+ ))}
+
+
+
+ )
+ ```
+
+**IMPORTANT VERIFICATION after changes:** Ensure NO instances of `text-green-600`, `text-red-600`, `text-green-400`, or `text-red-400` remain in BudgetDetailPage.tsx. All color coding must use `text-over-budget`, `text-on-budget`, or `text-muted-foreground`.
+
+
+ cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build && grep -c "text-green-600\|text-red-600\|text-green-400\|text-red-400" src/pages/BudgetDetailPage.tsx || echo "CLEAN: no hardcoded color classes"
+
+ BudgetDetailPage uses semantic color tokens (text-over-budget/text-on-budget) with zero instances of text-green-600 or text-red-600. Direction-aware diff logic handles all 6 category types correctly (spending types over when actual > budgeted, income/saving/investment over when actual < budgeted). Left-border accent group headers replace dot headers. Tier badge column removed for cleaner display. Locale-aware month heading. Skeleton loading state. PageShell wraps the page. Overall totals box uses semantic tokens. Group footer total label uses i18n interpolation. Build passes.
+
+
+
+
+
+- `bun run build` compiles without TypeScript errors
+- `bun run lint` passes (or pre-existing errors only)
+- `grep -c "text-green-600\|text-red-600" src/pages/BudgetDetailPage.tsx` returns 0 (semantic tokens only)
+- `grep -c "text-over-budget\|text-on-budget" src/pages/BudgetDetailPage.tsx` returns at least 2
+- `grep -c "return null" src/pages/BudgetListPage.tsx src/pages/BudgetDetailPage.tsx` returns 0 for both
+- `grep -c 'toLocaleDateString("en"' src/pages/BudgetDetailPage.tsx src/pages/BudgetListPage.tsx` returns 0 (no hardcoded English locale)
+- `grep -c "Intl.DateTimeFormat" src/pages/BudgetListPage.tsx src/pages/BudgetDetailPage.tsx` returns at least 1 for each
+- `grep -c "PageShell" src/pages/BudgetListPage.tsx src/pages/BudgetDetailPage.tsx` returns at least 1 for each
+- `grep "budgets.month" src/i18n/en.json src/i18n/de.json` returns matches in both
+
+
+
+- BudgetListPage: PageShell header, locale-aware month names in dialog and table, skeleton loading, i18n month/year labels
+- BudgetDetailPage: PageShell header, semantic color tokens (no hardcoded green/red), direction-aware diff for all 6 category types, left-border accent group headers, no tier column, locale-aware heading, skeleton loading, i18n group total label
+- No hardcoded English locale strings ("en") remain in budget page formatting
+- No hardcoded Tailwind color classes (text-green-600, text-red-600) remain
+- All 9 app pages now use consistent header layout (PageShell or equivalent)
+- German locale shows fully translated text on both pages
+- `bun run build` passes
+
+
+