406 lines
17 KiB
Markdown
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>
|