chore: archive v1.0 phase directories
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
---
|
||||
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)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jlmak/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||
|
||||
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<CategoryType, string>
|
||||
```
|
||||
|
||||
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)
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create PageShell, StatCard, SummaryStrip, and DashboardSkeleton components</name>
|
||||
<files>src/components/shared/PageShell.tsx, src/components/dashboard/StatCard.tsx, src/components/dashboard/SummaryStrip.tsx, src/components/dashboard/DashboardSkeleton.tsx</files>
|
||||
<action>
|
||||
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 (
|
||||
<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:
|
||||
```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 `<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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npm run build</automated>
|
||||
</verify>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Integrate new components into DashboardPage</name>
|
||||
<files>src/pages/DashboardPage.tsx</files>
|
||||
<action>
|
||||
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 <DashboardSkeleton />`
|
||||
- In `DashboardPage`: Replace `if (loading) return null` (line 291) with:
|
||||
```tsx
|
||||
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:
|
||||
```tsx
|
||||
<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:
|
||||
```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
|
||||
<div>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h1 className="text-2xl font-semibold">{t("dashboard.title")}</h1>
|
||||
</div>
|
||||
{/* content */}
|
||||
</div>
|
||||
```
|
||||
With:
|
||||
```tsx
|
||||
<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
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npm run build && npm run lint</automated>
|
||||
</verify>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-design-foundation-and-primitives/01-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user