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 |
|
|
true |
|
|
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)
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-tightmatches existing DashboardPage headingactionis 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,CardTitlefrom@/components/ui/card - Import
TrendingUp,TrendingDown,Minusfromlucide-react - Import
cnfrom@/lib/utils - Use
text-2xl font-bold tabular-nums tracking-tightfor the value (upgraded from existingfont-semiboldfor 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
StatCardfrom./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 labelt("budgets.budgeted") - Expenses card:
title={t("dashboard.totalExpenses")},valueClassName="text-destructive", variance with direction "neutral" and labelt("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
Skeletonfrom@/components/ui/skeleton - Import
Card,CardContent,CardHeaderfrom@/components/ui/card - Renders a
<div className="flex flex-col gap-6">with:- 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) - 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)
- Summary cards skeleton:
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:
-
Remove the inline SummaryCard component (lines 45-66). Delete the entire
SummaryCardPropsinterface andSummaryCardfunction. These are replaced byStatCard/SummaryStrip. -
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" -
Replace loading states with DashboardSkeleton:
- In
DashboardContent: Replaceif (loading) return null(line 76) withif (loading) return <DashboardSkeleton /> - In
DashboardPage: Replaceif (loading) return null(line 291) with:if (loading) return ( <PageShell title={t("dashboard.title")}> <DashboardSkeleton /> </PageShell> )
- In
-
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"
- BEFORE:
-
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"
- BEFORE:
-
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"
- BEFORE:
-
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) -
Replace the page header with PageShell in the
DashboardPagecomponent'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_TYPESconstant - The
currentMonthStarthelper - The
DashboardContentcomponent structure (budgetId prop, hooks, derived totals, pie chart, progress groups) - The
QuickAddPickerusage - 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
SummaryCardPropsinterface andSummaryCardfunction component - The hardcoded
text-green-600,text-red-600,bg-red-500,bg-green-500color classes - The
if (loading) return nullpatterns (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.
<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>