Files
SimpleFinanceDash/.planning/phases/01-design-foundation-and-primitives/01-02-PLAN.md

16 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
01-design-foundation-and-primitives 02 execute 2
01-01
src/components/shared/PageShell.tsx
src/components/dashboard/StatCard.tsx
src/components/dashboard/SummaryStrip.tsx
src/components/dashboard/DashboardSkeleton.tsx
src/pages/DashboardPage.tsx
true
UI-DASH-01
UI-DESIGN-01
UI-RESPONSIVE-01
truths artifacts key_links
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
path provides exports min_lines
src/components/shared/PageShell.tsx Consistent page header wrapper
PageShell
15
path provides exports min_lines
src/components/dashboard/StatCard.tsx KPI display card with variance badge
StatCard
30
path provides exports min_lines
src/components/dashboard/SummaryStrip.tsx Responsive row of 3 StatCards
SummaryStrip
20
path provides exports min_lines
src/components/dashboard/DashboardSkeleton.tsx Skeleton loading placeholder for dashboard
DashboardSkeleton
20
path provides contains
src/pages/DashboardPage.tsx Refactored dashboard page using new components PageShell
from to via pattern
src/components/dashboard/SummaryStrip.tsx src/components/dashboard/StatCard.tsx import and composition import.*StatCard
from to via pattern
src/pages/DashboardPage.tsx src/components/shared/PageShell.tsx import and wrapping import.*PageShell
from to via pattern
src/pages/DashboardPage.tsx src/components/dashboard/SummaryStrip.tsx import replacing inline SummaryCard import.*SummaryStrip
from to via pattern
src/pages/DashboardPage.tsx src/components/dashboard/DashboardSkeleton.tsx import replacing null loading state import.*DashboardSkeleton
from to via pattern
src/pages/DashboardPage.tsx src/index.css semantic token classes 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.

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

export type CategoryType = "income" | "bill" | "variable_expense" | "debt" | "saving" | "investment"

From src/lib/format.ts:

export function formatCurrency(amount: number, currency?: string): string

From src/lib/palette.ts:

export const categoryColors: Record<CategoryType, string>

From src/components/ui/card.tsx:

export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

From src/components/ui/badge.tsx:

export { Badge, badgeVariants }

From src/components/ui/skeleton.tsx:

export { Skeleton }

From src/hooks/useBudgets.ts:

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:

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

interface PageShellProps {
  title: string
  description?: string
  action?: React.ReactNode
  children: React.ReactNode
}

export function PageShell({ title, description, action, children }: PageShellProps) {
  return (
    <div className="flex flex-col gap-6">
      <div className="flex items-start justify-between gap-4">
        <div>
          <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
          {description && (
            <p className="mt-1 text-sm text-muted-foreground">{description}</p>
          )}
        </div>
        {action && <div className="shrink-0">{action}</div>}
      </div>
      {children}
    </div>
  )
}

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:

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:

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 <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> 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 <div className="flex flex-col gap-6"> with:
    1. Summary cards skeleton: <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> 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: <div className="grid gap-6 lg:grid-cols-2"> 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:

    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 <DashboardSkeleton />
    • In DashboardPage: Replace if (loading) return null (line 291) with:
      if (loading) return (
        <PageShell title={t("dashboard.title")}>
          <DashboardSkeleton />
        </PageShell>
      )
      
  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 <div className="grid gap-4 sm:grid-cols-3"> block (lines 135-149) with:

    <SummaryStrip
      income={{
        value: formatCurrency(totalIncome, currency),
        budgeted: formatCurrency(
          items.filter((i) => 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:

    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:

    <div>
      <div className="mb-6 flex items-center justify-between">
        <h1 className="text-2xl font-semibold">{t("dashboard.title")}</h1>
      </div>
      {/* content */}
    </div>
    

    With:

    <PageShell title={t("dashboard.title")}>
      {/* content */}
    </PageShell>
    

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 <div className="mb-6 flex items-center justify-between"> 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

<success_criteria>

  • 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) </success_criteria>
After completion, create `.planning/phases/01-design-foundation-and-primitives/01-02-SUMMARY.md`