docs(04): create phase plan — 3 plans for full-app design consistency

This commit is contained in:
2026-03-17 16:04:04 +01:00
parent c16f3302c8
commit fbe01b7372
4 changed files with 1069 additions and 4 deletions

View File

@@ -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"'
---
<objective>
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.
</objective>
<execution_context>
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
@/home/jlmak/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
<!-- PageShell is NOT used here -- auth pages are standalone, outside AppLayout -->
From src/pages/LoginPage.tsx (current structure to modify):
- Root: `<div className="flex min-h-screen items-center justify-center bg-background p-4">`
- Card: `<Card className="w-full max-w-sm">`
- 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 `<img src="/favicon.svg">`.
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Redesign LoginPage with brand presence and OAuth icons</name>
<files>src/pages/LoginPage.tsx, src/i18n/en.json, src/i18n/de.json</files>
<action>
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
<img src="/favicon.svg" alt="" className="mx-auto mb-3 size-10" aria-hidden="true" />
```
4. **Subtitle:** Below the CardTitle, add:
```tsx
<p className="text-sm text-muted-foreground">{t("auth.loginSubtitle")}</p>
```
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
<svg className="size-4" viewBox="0 0 24 24" aria-hidden="true">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.27-4.74 3.27-8.1z" fill="#4285F4"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
</svg>
```
For GitHub button, add before "GitHub" text:
```tsx
<svg className="size-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2z"/>
</svg>
```
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.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
</verify>
<done>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.</done>
</task>
<task type="auto">
<name>Task 2: Redesign RegisterPage to match LoginPage treatment</name>
<files>src/pages/RegisterPage.tsx</files>
<action>
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
<img src="/favicon.svg" alt="" className="mx-auto mb-3 size-10" aria-hidden="true" />
```
4. **Subtitle:** Below the CardTitle, add:
```tsx
<p className="text-sm text-muted-foreground">{t("auth.registerSubtitle")}</p>
```
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.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
</verify>
<done>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.</done>
</task>
</tasks>
<verification>
- `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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/04-full-app-design-consistency/04-01-SUMMARY.md`
</output>

View File

@@ -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'
---
<objective>
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 `<h1>` + 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.
</objective>
<execution_context>
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
@/home/jlmak/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
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: <Skeleton className="h-4 w-32" />
```
From src/lib/palette.ts:
```tsx
export const categoryColors: Record<CategoryType, string>
// 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
<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>
```
Current pattern in all CRUD pages to replace:
```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>
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Upgrade CategoriesPage and TemplatePage with PageShell, skeletons, and group headers</name>
<files>src/pages/CategoriesPage.tsx, src/pages/TemplatePage.tsx, src/i18n/en.json, src/i18n/de.json</files>
<action>
**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 `<div className="mb-6 flex items-center justify-between">` block containing the `<h1>` and `<Button>`. Wrap the entire return content in:
```tsx
<PageShell
title={t("categories.title")}
action={
<Button onClick={openCreate} size="sm">
<Plus className="mr-1 size-4" />
{t("categories.add")}
</Button>
}
>
{/* existing content (empty state check + grouped sections) */}
</PageShell>
```
3. **Skeleton loading:** Replace `if (loading) return null` with:
```tsx
if (loading) return (
<PageShell title={t("categories.title")}>
<div className="space-y-6">
{[1, 2, 3].map((i) => (
<div key={i} className="space-y-2">
<div className="flex items-center gap-3 rounded-sm border-l-4 border-muted bg-muted/30 px-3 py-2">
<Skeleton className="h-4 w-28" />
</div>
{[1, 2].map((j) => (
<div key={j} 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>
))}
</div>
))}
</div>
</PageShell>
)
```
4. **Group header upgrade:** Replace the plain dot group header pattern in the `grouped.map` with the left-border accent pattern:
```tsx
<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>
```
Remove the old `<div className="mb-2 flex items-center gap-2">` block with the `size-3 rounded-full` dot and `<h2>`.
**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
<div>
{/* Header */}
<div className="mb-6 flex items-center justify-between gap-4">
<TemplateName ... />
<Button ...>...</Button>
</div>
...
</div>
```
With:
```tsx
<PageShell
title={template?.name ?? t("template.title")}
action={
<div className="flex items-center gap-2">
<Button onClick={openCreate} size="sm" disabled={isSaving}>
<Plus className="mr-1 size-4" />
{t("template.addItem")}
</Button>
</div>
}
>
...
</PageShell>
```
**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
<div className="flex flex-col gap-6">
<div className="flex items-start justify-between gap-4">
<TemplateName
name={template?.name ?? t("template.title")}
onSave={handleNameSave}
/>
<div className="shrink-0">
<Button onClick={openCreate} size="sm" disabled={isSaving}>
<Plus className="mr-1 size-4" />
{t("template.addItem")}
</Button>
</div>
</div>
{/* rest of content */}
</div>
```
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 `<h1>` 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 (
<div className="flex flex-col gap-6">
<div className="flex items-start justify-between gap-4">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-9 w-24" />
</div>
<div className="space-y-6">
{[1, 2].map((i) => (
<div key={i} className="space-y-2">
<div className="flex items-center gap-3 rounded-sm border-l-4 border-muted bg-muted/30 px-3 py-2">
<Skeleton className="h-4 w-28" />
</div>
{[1, 2, 3].map((j) => (
<div key={j} 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-4 w-20" />
<Skeleton className="h-7 w-7 rounded-md" />
</div>
))}
</div>
))}
</div>
</div>
)
```
4. **Group header upgrade:** Same left-border accent pattern as CategoriesPage. Replace the dot+h2 pattern in grouped.map with:
```tsx
<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>
```
**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.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
</verify>
<done>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.</done>
</task>
<task type="auto">
<name>Task 2: Upgrade QuickAddPage and SettingsPage with PageShell and skeletons</name>
<files>src/pages/QuickAddPage.tsx, src/pages/SettingsPage.tsx</files>
<action>
**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 `<div className="mb-6 flex items-center justify-between">` header block. Wrap the entire return in:
```tsx
<PageShell
title={t("quickAdd.title")}
action={
<Button onClick={openCreate} size="sm">
<Plus className="mr-1 size-4" />
{t("quickAdd.add")}
</Button>
}
>
{/* empty state + table + dialog */}
</PageShell>
```
Remove the wrapping `<div>` root since PageShell provides the outer container.
3. **Skeleton loading:** Replace `if (loading) return null` with:
```tsx
if (loading) return (
<PageShell title={t("quickAdd.title")}>
<div className="space-y-1">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="flex items-center gap-4 px-4 py-2.5 border-b border-border">
<Skeleton className="h-5 w-10 rounded-full" />
<Skeleton className="h-4 w-36" />
<Skeleton className="ml-auto h-7 w-7 rounded-md" />
</div>
))}
</div>
</PageShell>
)
```
**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 `<h1 className="mb-6 text-2xl font-semibold">{t("settings.title")}</h1>` on line 67. This creates a double heading since the Card below also has a CardTitle with "Settings".
3. **Wrap with PageShell:** Replace the `<div className="max-w-lg">` root with:
```tsx
<PageShell title={t("settings.title")}>
<div className="max-w-lg">
<Card>
{/* Remove CardHeader with CardTitle since PageShell provides the title.
Keep CardContent as-is. */}
<CardContent className="space-y-4 pt-6">
{/* existing form fields unchanged */}
</CardContent>
</Card>
</div>
</PageShell>
```
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 (
<PageShell title={t("settings.title")}>
<div className="max-w-lg">
<Card>
<CardContent className="space-y-4 pt-6">
{[1, 2, 3].map((i) => (
<div key={i} className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-10 w-full" />
</div>
))}
<Skeleton className="h-10 w-20" />
</CardContent>
</Card>
</div>
</PageShell>
)
```
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.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
</verify>
<done>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.</done>
</task>
</tasks>
<verification>
- `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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/04-full-app-design-consistency/04-02-SUMMARY.md`
</output>

View File

@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
@/home/jlmak/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
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
<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>
```
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)
)
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Upgrade BudgetListPage with PageShell, locale-aware months, skeleton, and i18n labels</name>
<files>src/pages/BudgetListPage.tsx, src/i18n/en.json, src/i18n/de.json</files>
<action>
**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 `<Label>Month</Label>` and `<Label>Year</Label>` in the new budget dialog with:
```tsx
<Label>{t("budgets.month")}</Label>
// and
<Label>{t("budgets.year")}</Label>
```
7. **Replace header with PageShell:** Remove the `<div className="mb-6 flex items-center justify-between">` header block. Wrap the return in:
```tsx
<PageShell
title={t("budgets.title")}
action={
<Button onClick={openDialog} size="sm">
<Plus className="mr-1 size-4" />
{t("budgets.newBudget")}
</Button>
}
>
{/* empty state + table + dialog */}
</PageShell>
```
8. **Skeleton loading:** Replace `if (loading) return null` with:
```tsx
if (loading) return (
<PageShell title={t("budgets.title")}>
<div className="space-y-1">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="flex items-center gap-4 px-4 py-3 border-b border-border">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-4 w-12" />
<Skeleton className="ml-auto h-4 w-4" />
</div>
))}
</div>
</PageShell>
)
```
**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"
```
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
</verify>
<done>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.</done>
</task>
<task type="auto">
<name>Task 2: Upgrade BudgetDetailPage with semantic tokens, direction-aware diff, PageShell, group headers, and skeleton</name>
<files>src/pages/BudgetDetailPage.tsx</files>
<action>
**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 (
<TableCell
className={cn(
"text-right tabular-nums",
isOver ? "text-over-budget" : diff !== 0 ? "text-on-budget" : "text-muted-foreground"
)}
>
{formatCurrency(Math.abs(diff), currency)}
{diff < 0 ? " over" : ""}
</TableCell>
)
}
```
4. **Update DifferenceCell call sites:** In the grouped.map render:
- Remove the `const isIncome = type === "income"` line.
- Change `<DifferenceCell budgeted={...} actual={...} currency={currency} isIncome={isIncome} />` to `<DifferenceCell budgeted={...} actual={...} currency={currency} type={type} />` 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 `<TableHead>{t("categories.type")}</TableHead>` column from the table header.
- Remove the `<TableCell><TierBadge tier={item.item_tier} /></TableCell>` 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
<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>
```
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
<p
className={cn(
"text-lg font-semibold tabular-nums",
totalBudgeted - totalActual >= 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
<TableCell colSpan={2} className="font-medium">
{t(`categories.types.${type}`)} Total
</TableCell>
```
Replace with i18n:
```tsx
<TableCell className="font-medium">
{t("budgets.total", { label: t(`categories.types.${type}`) })}
</TableCell>
```
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
<PageShell
title={headingLabel()}
action={
<Button onClick={openAddDialog} size="sm">
<Plus className="mr-1 size-4" />
{t("budgets.addItem")}
</Button>
}
>
<Link
to="/budgets"
className="-mt-4 inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="size-4" />
{t("budgets.title")}
</Link>
{/* rest of content */}
</PageShell>
```
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 (
<PageShell title="">
<div className="space-y-6">
<Skeleton className="h-4 w-24" />
{[1, 2, 3].map((i) => (
<div key={i} className="space-y-2">
<div className="flex items-center gap-3 rounded-sm border-l-4 border-muted bg-muted/30 px-3 py-2">
<Skeleton className="h-4 w-28" />
</div>
{[1, 2].map((j) => (
<div key={j} className="flex items-center gap-4 px-4 py-2.5 border-b border-border">
<Skeleton className="h-4 w-32" />
<Skeleton className="ml-auto h-4 w-20" />
<Skeleton className="h-4 w-20" />
<Skeleton className="h-4 w-16" />
</div>
))}
</div>
))}
<Skeleton className="h-20 w-full rounded-md" />
</div>
</PageShell>
)
```
**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`.
</action>
<verify>
<automated>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"</automated>
</verify>
<done>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.</done>
</task>
</tasks>
<verification>
- `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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/04-full-app-design-consistency/04-03-SUMMARY.md`
</output>