17 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04-full-app-design-consistency | 02 | execute | 2 |
|
|
true |
|
|
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.
<execution_context> @/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md @/home/jlmak/.claude/get-shit-done/templates/summary.md </execution_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 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:
// 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:
export const categoryColors: Record<CategoryType, string>
// Maps category type to CSS variable string like "var(--color-income)"
Group header upgrade pattern (from RESEARCH.md):
// 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:
<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>
-
Import PageShell: Add
import { PageShell } from "@/components/shared/PageShell"andimport { Skeleton } from "@/components/ui/skeleton". -
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:<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> -
Skeleton loading: Replace
if (loading) return nullwith: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> ) -
Group header upgrade: Replace the plain dot group header pattern in the
grouped.mapwith the left-border accent pattern:<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 thesize-3 rounded-fulldot and<h2>.
TemplatePage.tsx changes:
-
Import PageShell and Skeleton: Same imports as CategoriesPage.
-
Replace header: The TemplatePage header has an inline-editable
TemplateNamecomponent. Wrap with PageShell, putting TemplateName as the title area. Since PageShell accepts atitlestring but TemplateName is a component, use PageShell differently here:Instead of wrapping with PageShell using
titleprop, 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:
<div> {/* Header */} <div className="mb-6 flex items-center justify-between gap-4"> <TemplateName ... /> <Button ...>...</Button> </div> ... </div>With:
<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
titleprop 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 customactionthat 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:
<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 useclassName="text-2xl font-semibold tracking-tight"(addtracking-tightto match PageShell's h1 styling). -
Skeleton loading: Replace
if (loading) return nullwith a skeleton that mirrors the template page layout: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> ) -
Group header upgrade: Same left-border accent pattern as CategoriesPage. Replace the dot+h2 pattern in grouped.map with:
<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.
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.
-
Import PageShell and Skeleton: Add
import { PageShell } from "@/components/shared/PageShell"andimport { Skeleton } from "@/components/ui/skeleton". -
Replace header: Remove the
<div className="mb-6 flex items-center justify-between">header block. Wrap the entire return in:<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. -
Skeleton loading: Replace
if (loading) return nullwith: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:
-
Import PageShell and Skeleton: Add
import { PageShell } from "@/components/shared/PageShell"andimport { Skeleton } from "@/components/ui/skeleton". -
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". -
Wrap with PageShell: Replace the
<div className="max-w-lg">root with:<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-6to CardContent's className since without CardHeader the content needs top padding. -
Skeleton loading: Replace
if (loading) return nullwith: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> ) -
Clean up unused imports: After removing CardHeader and CardTitle usage, update the import to:
import { Card, CardContent } from "@/components/ui/card". RemoveCardHeaderandCardTitlefrom 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.
<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 buildpasses- No
return nullloading patterns remain in any of the four files </success_criteria>