Files

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
04-01
src/pages/CategoriesPage.tsx
src/pages/TemplatePage.tsx
src/pages/QuickAddPage.tsx
src/pages/SettingsPage.tsx
src/i18n/en.json
src/i18n/de.json
true
UI-CATEGORIES-01
UI-TEMPLATE-01
UI-QUICKADD-01
UI-SETTINGS-01
UI-DESIGN-01
truths artifacts key_links
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
path provides contains
src/pages/CategoriesPage.tsx PageShell adoption, skeleton, group header upgrade PageShell
path provides contains
src/pages/TemplatePage.tsx PageShell adoption, skeleton, group header upgrade PageShell
path provides contains
src/pages/QuickAddPage.tsx PageShell adoption, skeleton PageShell
path provides contains
src/pages/SettingsPage.tsx PageShell adoption, skeleton, no double heading PageShell
from to via pattern
src/pages/CategoriesPage.tsx src/components/shared/PageShell.tsx import and render import.*PageShell.*from.*shared/PageShell
from to via pattern
src/pages/SettingsPage.tsx src/components/shared/PageShell.tsx import and render — replacing redundant h1 import.*PageShell.*from.*shared/PageShell
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.

<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>
Task 1: Upgrade CategoriesPage and TemplatePage with PageShell, skeletons, and group headers src/pages/CategoriesPage.tsx, src/pages/TemplatePage.tsx, src/i18n/en.json, src/i18n/de.json **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:

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

    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:

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

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

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

    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:

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

Task 2: Upgrade QuickAddPage and SettingsPage with PageShell and skeletons src/pages/QuickAddPage.tsx, src/pages/SettingsPage.tsx **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:

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

    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:

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

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

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

<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>
After completion, create `.planning/phases/04-full-app-design-consistency/04-02-SUMMARY.md`