Files

406 lines
17 KiB
Markdown

---
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>