--- phase: 01-design-foundation-and-primitives plan: 02 type: execute wave: 2 depends_on: - 01-01 files_modified: - src/components/shared/PageShell.tsx - src/components/dashboard/StatCard.tsx - src/components/dashboard/SummaryStrip.tsx - src/components/dashboard/DashboardSkeleton.tsx - src/pages/DashboardPage.tsx autonomous: true requirements: - UI-DASH-01 - UI-DESIGN-01 - UI-RESPONSIVE-01 must_haves: truths: - "PageShell renders a consistent page header with title, optional description, and optional CTA slot" - "StatCard renders a KPI card with title, large formatted value, optional semantic color, and optional variance badge with directional icon" - "SummaryStrip renders 3 StatCards in a responsive grid (1 col mobile, 2 cols tablet, 3 cols desktop)" - "DashboardSkeleton mirrors the real summary card grid and chart card layout with pulse animations" - "DashboardPage uses PageShell instead of inline h1 header" - "DashboardPage uses SummaryStrip instead of inline SummaryCard components" - "DashboardPage shows DashboardSkeleton during loading instead of returning null" - "Balance card uses semantic text-on-budget/text-over-budget classes instead of hardcoded text-green-600/text-red-600" artifacts: - path: "src/components/shared/PageShell.tsx" provides: "Consistent page header wrapper" exports: ["PageShell"] min_lines: 15 - path: "src/components/dashboard/StatCard.tsx" provides: "KPI display card with variance badge" exports: ["StatCard"] min_lines: 30 - path: "src/components/dashboard/SummaryStrip.tsx" provides: "Responsive row of 3 StatCards" exports: ["SummaryStrip"] min_lines: 20 - path: "src/components/dashboard/DashboardSkeleton.tsx" provides: "Skeleton loading placeholder for dashboard" exports: ["DashboardSkeleton"] min_lines: 20 - path: "src/pages/DashboardPage.tsx" provides: "Refactored dashboard page using new components" contains: "PageShell" key_links: - from: "src/components/dashboard/SummaryStrip.tsx" to: "src/components/dashboard/StatCard.tsx" via: "import and composition" pattern: "import.*StatCard" - from: "src/pages/DashboardPage.tsx" to: "src/components/shared/PageShell.tsx" via: "import and wrapping" pattern: "import.*PageShell" - from: "src/pages/DashboardPage.tsx" to: "src/components/dashboard/SummaryStrip.tsx" via: "import replacing inline SummaryCard" pattern: "import.*SummaryStrip" - from: "src/pages/DashboardPage.tsx" to: "src/components/dashboard/DashboardSkeleton.tsx" via: "import replacing null loading state" pattern: "import.*DashboardSkeleton" - from: "src/pages/DashboardPage.tsx" to: "src/index.css" via: "semantic token classes" pattern: "text-(on-budget|over-budget)" --- Build the shared components (PageShell, StatCard, SummaryStrip, DashboardSkeleton) and integrate them into DashboardPage, replacing the inline SummaryCard, null loading state, and hardcoded color classes. Purpose: Deliver the visual foundation components that all subsequent phases consume. After this plan, the dashboard has semantic KPI cards with variance badges, skeleton loading, and a consistent page header pattern ready for reuse across all 9 pages. Output: 4 new component files, refactored DashboardPage.tsx. @/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md @/home/jlmak/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/01-design-foundation-and-primitives/01-RESEARCH.md @.planning/phases/01-design-foundation-and-primitives/01-01-SUMMARY.md @src/pages/DashboardPage.tsx @src/components/ui/card.tsx @src/components/ui/badge.tsx @src/components/ui/skeleton.tsx @src/lib/format.ts @src/lib/palette.ts @src/lib/types.ts @src/i18n/en.json From src/lib/types.ts: ```typescript export type CategoryType = "income" | "bill" | "variable_expense" | "debt" | "saving" | "investment" ``` From src/lib/format.ts: ```typescript export function formatCurrency(amount: number, currency?: string): string ``` From src/lib/palette.ts: ```typescript export const categoryColors: Record ``` From src/components/ui/card.tsx: ```typescript export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } ``` From src/components/ui/badge.tsx: ```typescript export { Badge, badgeVariants } ``` From src/components/ui/skeleton.tsx: ```typescript export { Skeleton } ``` From src/hooks/useBudgets.ts: ```typescript export function useBudgets(): { budgets: Budget[], loading: boolean, ... } export function useBudgetDetail(id: string): { budget: Budget | null, items: BudgetItem[], loading: boolean } ``` From existing DashboardPage.tsx (lines 45-66) - the SummaryCard being REPLACED: ```typescript interface SummaryCardProps { title: string value: string valueClassName?: string } function SummaryCard({ title, value, valueClassName }: SummaryCardProps) { ... } ``` CSS tokens available from Plan 01 (src/index.css): - `text-on-budget` (maps to --color-on-budget) - `text-over-budget` (maps to --color-over-budget) - `text-income` (maps to --color-income) - `text-destructive` (maps to --color-destructive) Task 1: Create PageShell, StatCard, SummaryStrip, and DashboardSkeleton components src/components/shared/PageShell.tsx, src/components/dashboard/StatCard.tsx, src/components/dashboard/SummaryStrip.tsx, src/components/dashboard/DashboardSkeleton.tsx Create 4 new component files. Create directories `src/components/shared/` and `src/components/dashboard/` if they do not exist. **File 1: 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) { return (

{title}

{description && (

{description}

)}
{action &&
{action}
}
{children}
) } ``` Key decisions: - Named export (not default) per convention for shared components - `text-2xl font-semibold tracking-tight` matches existing DashboardPage heading - `action` is a ReactNode slot, not a button-specific prop - No padding baked in -- AppLayout.tsx already provides `p-6` - No i18n dependency -- title comes from the caller via `t()` at the page level **File 2: src/components/dashboard/StatCard.tsx** Follow the pattern from research (Pattern 2) exactly. Named export `StatCard`. Props interface: ```typescript interface StatCardProps { title: string value: string valueClassName?: string variance?: { amount: string direction: "up" | "down" | "neutral" label: string } } ``` Implementation: - Import `Card`, `CardContent`, `CardHeader`, `CardTitle` from `@/components/ui/card` - Import `TrendingUp`, `TrendingDown`, `Minus` from `lucide-react` - Import `cn` from `@/lib/utils` - Use `text-2xl font-bold tabular-nums tracking-tight` for the value (upgraded from existing `font-semibold` for more visual weight) - Variance section renders a directional icon (size-3) + amount text + label in `text-xs text-muted-foreground` - Do NOT import Badge -- the variance display uses inline layout, not a badge component **File 3: src/components/dashboard/SummaryStrip.tsx** Follow the pattern from research (Pattern 3). Named export `SummaryStrip`. Props interface: ```typescript interface SummaryStripProps { income: { value: string; budgeted: string } expenses: { value: string; budgeted: string } balance: { value: string; isPositive: boolean } t: (key: string) => string } ``` Implementation: - Import `StatCard` from `./StatCard` - Renders a `
` with 3 StatCards - Income card: `title={t("dashboard.totalIncome")}`, `valueClassName="text-income"`, variance with direction "neutral" and label `t("budgets.budgeted")` - Expenses card: `title={t("dashboard.totalExpenses")}`, `valueClassName="text-destructive"`, variance with direction "neutral" and label `t("budgets.budgeted")` - Balance card: `title={t("dashboard.availableBalance")}`, `valueClassName={balance.isPositive ? "text-on-budget" : "text-over-budget"}`, no variance prop Note: The `t` function is passed as a prop to keep SummaryStrip as a presentational component that does not call `useTranslation()` internally. The parent (DashboardContent) already has `t` from `useTranslation()`. **File 4: src/components/dashboard/DashboardSkeleton.tsx** Follow the pattern from research (Pattern 4). Named export `DashboardSkeleton`. Implementation: - Import `Skeleton` from `@/components/ui/skeleton` - Import `Card`, `CardContent`, `CardHeader` from `@/components/ui/card` - Renders a `
` with: 1. Summary cards skeleton: `
` with 3 skeleton cards matching StatCard layout (Skeleton h-4 w-24 for title, Skeleton h-8 w-32 for value, Skeleton h-3 w-20 for variance) 2. Chart area skeleton: `
` with 2 skeleton cards (Skeleton h-5 w-40 for chart title, Skeleton h-[240px] w-full rounded-md for chart area) This mirrors the real dashboard grid exactly so there is no layout shift when data loads. All 4 files use named exports. Follow import order convention: React first, third-party, internal types, internal utilities, components. npm run build All 4 component files exist, export the correct named exports, follow project conventions, and build passes. PageShell accepts title/description/action/children. StatCard accepts title/value/valueClassName/variance. SummaryStrip renders 3 StatCards in responsive grid with semantic color classes. DashboardSkeleton mirrors the real layout structure. Task 2: Integrate new components into DashboardPage src/pages/DashboardPage.tsx Refactor `src/pages/DashboardPage.tsx` to use the new shared components. This is a MODIFY operation -- preserve all existing logic (derived totals, pie chart, progress groups) while replacing the presentation layer. **Changes to make:** 1. **Remove the inline SummaryCard component** (lines 45-66). Delete the entire `SummaryCardProps` interface and `SummaryCard` function. These are replaced by `StatCard`/`SummaryStrip`. 2. **Add new imports** at the appropriate positions in the import order: ```typescript import { PageShell } from "@/components/shared/PageShell" import { SummaryStrip } from "@/components/dashboard/SummaryStrip" import { DashboardSkeleton } from "@/components/dashboard/DashboardSkeleton" ``` 3. **Replace loading states with DashboardSkeleton:** - In `DashboardContent`: Replace `if (loading) return null` (line 76) with `if (loading) return ` - In `DashboardPage`: Replace `if (loading) return null` (line 291) with: ```tsx if (loading) return ( ) ``` 4. **Replace hardcoded balance color** (lines 95-98): - BEFORE: `const balanceColor = availableBalance >= 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"` - AFTER: `const balanceColor = availableBalance >= 0 ? "text-on-budget" : "text-over-budget"` 5. **Replace hardcoded progress bar colors** (lines 219-221): - BEFORE: `const barColor = group.overBudget ? "bg-red-500 dark:bg-red-400" : "bg-green-500 dark:bg-green-400"` - AFTER: `const barColor = group.overBudget ? "bg-over-budget" : "bg-on-budget"` 6. **Replace hardcoded progress text color** (lines 235-239): - BEFORE: `group.overBudget ? "text-red-600 dark:text-red-400" : "text-muted-foreground"` - AFTER: `group.overBudget ? "text-over-budget" : "text-muted-foreground"` 7. **Replace inline summary cards with SummaryStrip** in DashboardContent's return JSX. Replace the `
` block (lines 135-149) with: ```tsx i.category?.type === "income").reduce((sum, i) => sum + i.budgeted_amount, 0), currency ), }} expenses={{ value: formatCurrency(totalExpenses, currency), budgeted: formatCurrency( items.filter((i) => i.category?.type !== "income").reduce((sum, i) => sum + i.budgeted_amount, 0), currency ), }} balance={{ value: formatCurrency(availableBalance, currency), isPositive: availableBalance >= 0, }} t={t} /> ``` To avoid recomputing budgeted totals inline, derive them alongside the existing totalIncome/totalExpenses calculations: ```typescript const budgetedIncome = items .filter((i) => i.category?.type === "income") .reduce((sum, i) => sum + i.budgeted_amount, 0) const budgetedExpenses = items .filter((i) => i.category?.type !== "income") .reduce((sum, i) => sum + i.budgeted_amount, 0) ``` 8. **Replace the page header with PageShell** in the `DashboardPage` component's return. Replace: ```tsx

{t("dashboard.title")}

{/* content */}
``` With: ```tsx {/* content */} ``` **What to preserve:** - All imports for Recharts (PieChart, Pie, Cell, ResponsiveContainer, Tooltip) - The `EXPENSE_TYPES` constant - The `currentMonthStart` helper - The `DashboardContent` component structure (budgetId prop, hooks, derived totals, pie chart, progress groups) - The `QuickAddPicker` usage - The entire pie chart + legend section - The entire category progress section (but with updated color classes) - The no-budget empty state with Link to /budgets **What to remove:** - The `SummaryCardProps` interface and `SummaryCard` function component - The hardcoded `text-green-600`, `text-red-600`, `bg-red-500`, `bg-green-500` color classes - The `if (loading) return null` patterns (both in DashboardContent and DashboardPage) - The inline `
` header npm run build && npm run lint DashboardPage imports and uses PageShell, SummaryStrip, and DashboardSkeleton. No more inline SummaryCard component. Loading states show skeleton instead of null. All hardcoded green/red color classes replaced with semantic token classes (text-on-budget, text-over-budget, bg-on-budget, bg-over-budget). Build and lint pass. 1. `npm run build && npm run lint` passes 2. `src/components/shared/PageShell.tsx` exports `PageShell` 3. `src/components/dashboard/StatCard.tsx` exports `StatCard` 4. `src/components/dashboard/SummaryStrip.tsx` exports `SummaryStrip` and imports `StatCard` 5. `src/components/dashboard/DashboardSkeleton.tsx` exports `DashboardSkeleton` 6. `src/pages/DashboardPage.tsx` imports PageShell, SummaryStrip, DashboardSkeleton 7. No occurrences of `text-green-600`, `text-red-600`, `bg-red-500`, `bg-green-500` remain in DashboardPage.tsx 8. No occurrences of `SummaryCard` remain in DashboardPage.tsx 9. No `return null` for loading states in DashboardPage.tsx - All 4 new component files exist and are well-typed - DashboardPage uses PageShell for header, SummaryStrip for KPI cards, DashboardSkeleton for loading - Zero hardcoded green/red color values in DashboardPage - Build and lint pass cleanly - Summary cards display in responsive grid (1/2/3 columns by breakpoint) After completion, create `.planning/phases/01-design-foundation-and-primitives/01-02-SUMMARY.md`