--- 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 `
` header block. Wrap the return in: ```tsx {t("budgets.newBudget")} } > {/* empty state + table + dialog */} ``` 8. **Skeleton loading:** Replace `if (loading) return null` with: ```tsx if (loading) return (
{[1, 2, 3, 4].map((i) => (
))}
) ``` **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 After completion, create `.planning/phases/04-full-app-design-consistency/04-03-SUMMARY.md`