docs(03): create phase plan

This commit is contained in:
2026-03-17 15:01:41 +01:00
parent 76f7299976
commit 1a4035bea1
3 changed files with 777 additions and 3 deletions

View File

@@ -0,0 +1,422 @@
---
phase: 03-collapsible-dashboard-sections
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/index.css
- src/i18n/en.json
- src/i18n/de.json
- src/components/dashboard/StatCard.tsx
- src/components/dashboard/SummaryStrip.tsx
- src/pages/DashboardPage.tsx
- src/components/dashboard/CategorySection.tsx
- src/components/dashboard/CollapsibleSections.tsx
autonomous: true
requirements:
- UI-DASH-01
- UI-COLLAPSE-01
must_haves:
truths:
- "Balance card shows 'Includes $X carryover' subtitle when carryover is non-zero"
- "Balance card has no subtitle when carryover is zero"
- "Negative carryover displays with red styling"
- "CategorySection renders with left border accent, chevron, label, badges, and difference"
- "CollapsibleSections renders an ordered list of CategorySection components"
- "Collapsible animation tokens are defined in CSS"
artifacts:
- path: "src/index.css"
provides: "Collapsible animation keyframes and tokens"
contains: "collapsible-open"
- path: "src/i18n/en.json"
provides: "Section and carryover i18n keys"
contains: "carryoverIncludes"
- path: "src/i18n/de.json"
provides: "German section and carryover i18n keys"
contains: "carryoverIncludes"
- path: "src/components/dashboard/StatCard.tsx"
provides: "Optional subtitle prop for carryover display"
contains: "subtitle"
- path: "src/components/dashboard/SummaryStrip.tsx"
provides: "Carryover subtitle threading to balance StatCard"
contains: "carryoverSubtitle"
- path: "src/pages/DashboardPage.tsx"
provides: "Carryover subtitle computed and passed to SummaryStrip"
contains: "carryoverSubtitle"
- path: "src/components/dashboard/CategorySection.tsx"
provides: "Collapsible section with header badges and line-item table"
exports: ["CategorySection"]
- path: "src/components/dashboard/CollapsibleSections.tsx"
provides: "Container rendering ordered CategorySection list"
exports: ["CollapsibleSections"]
key_links:
- from: "src/pages/DashboardPage.tsx"
to: "src/components/dashboard/SummaryStrip.tsx"
via: "carryoverSubtitle prop on balance object"
pattern: "carryoverSubtitle.*formatCurrency.*carryover"
- from: "src/components/dashboard/SummaryStrip.tsx"
to: "src/components/dashboard/StatCard.tsx"
via: "subtitle prop"
pattern: "subtitle.*carryoverSubtitle"
- from: "src/components/dashboard/CollapsibleSections.tsx"
to: "src/components/dashboard/CategorySection.tsx"
via: "renders CategorySection per group"
pattern: "CategorySection"
---
<objective>
Build the carryover display, CSS animation tokens, i18n keys, and the two new collapsible section components (CategorySection + CollapsibleSections) as pure presentational building blocks.
Purpose: Establish all the foundational pieces that Plan 02 will wire into DashboardContent. Carryover display is self-contained and ships immediately. Section components are built and tested in isolation.
Output: StatCard with subtitle, SummaryStrip with carryover, CSS animation tokens, i18n keys, CategorySection component, CollapsibleSections component.
</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/03-collapsible-dashboard-sections/03-CONTEXT.md
@.planning/phases/03-collapsible-dashboard-sections/03-RESEARCH.md
@.planning/phases/03-collapsible-dashboard-sections/03-VALIDATION.md
@src/pages/DashboardPage.tsx
@src/components/dashboard/StatCard.tsx
@src/components/dashboard/SummaryStrip.tsx
@src/components/ui/collapsible.tsx
@src/components/ui/table.tsx
@src/components/ui/badge.tsx
@src/lib/palette.ts
@src/lib/types.ts
@src/lib/format.ts
@src/index.css
@src/i18n/en.json
@src/i18n/de.json
<interfaces>
<!-- Key types and contracts the executor needs. -->
From src/lib/types.ts:
```typescript
export type CategoryType = "income" | "bill" | "variable_expense" | "debt" | "saving" | "investment"
export interface Budget { id: string; carryover_amount: number; currency: string; /* ... */ }
export interface BudgetItem { id: string; budgeted_amount: number; actual_amount: number; category?: Category; /* ... */ }
export interface Category { id: string; name: string; type: CategoryType; /* ... */ }
```
From src/lib/palette.ts:
```typescript
export const categoryColors: Record<CategoryType, string> // e.g. { income: "var(--color-income)" }
export const categoryLabels: Record<CategoryType, { en: string; de: string }>
```
From src/lib/format.ts:
```typescript
export function formatCurrency(amount: number, currency?: string, locale?: string): string
```
From src/components/ui/collapsible.tsx:
```typescript
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
```
From src/components/dashboard/StatCard.tsx (current):
```typescript
interface StatCardProps {
title: string
value: string
valueClassName?: string
variance?: { amount: string; direction: "up" | "down" | "neutral"; label: string }
}
```
From src/components/dashboard/SummaryStrip.tsx (current):
```typescript
interface SummaryStripProps {
income: { value: string; budgeted: string }
expenses: { value: string; budgeted: string }
balance: { value: string; isPositive: boolean }
t: (key: string) => string
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add CSS animation tokens, i18n keys, and carryover display</name>
<files>src/index.css, src/i18n/en.json, src/i18n/de.json, src/components/dashboard/StatCard.tsx, src/components/dashboard/SummaryStrip.tsx, src/pages/DashboardPage.tsx</files>
<action>
**1. CSS animation tokens (src/index.css):**
Add to the existing `@theme inline` block, after the `--radius` line:
```css
/* Collapsible animation */
--animate-collapsible-open: collapsible-open 200ms ease-out;
--animate-collapsible-close: collapsible-close 200ms ease-out;
```
Add after the `@layer base` block:
```css
@keyframes collapsible-open {
from { height: 0; overflow: hidden; }
to { height: var(--radix-collapsible-content-height); overflow: hidden; }
}
@keyframes collapsible-close {
from { height: var(--radix-collapsible-content-height); overflow: hidden; }
to { height: 0; overflow: hidden; }
}
```
**2. i18n keys (src/i18n/en.json):**
Add under the `"dashboard"` object:
```json
"sections": {
"itemName": "Item",
"groupTotal": "{{label}} Total"
},
"carryoverIncludes": "Includes {{amount}} carryover"
```
**3. i18n keys (src/i18n/de.json):**
Add under the `"dashboard"` object:
```json
"sections": {
"itemName": "Posten",
"groupTotal": "{{label}} Gesamt"
},
"carryoverIncludes": "Inkl. {{amount}} Übertrag"
```
**4. StatCard subtitle prop (src/components/dashboard/StatCard.tsx):**
Add two optional props to `StatCardProps`:
```typescript
subtitle?: string // e.g. "Includes EUR 150.00 carryover"
subtitleClassName?: string // e.g. "text-over-budget" for negative carryover
```
Add to the destructured props. Render below the value `<p>` and before the variance block:
```tsx
{subtitle && (
<p className={cn("mt-0.5 text-xs text-muted-foreground", subtitleClassName)}>
{subtitle}
</p>
)}
```
`cn` is already imported from `@/lib/utils`.
**5. SummaryStrip carryover prop (src/components/dashboard/SummaryStrip.tsx):**
Extend the `balance` prop type:
```typescript
balance: {
value: string
isPositive: boolean
carryoverSubtitle?: string // NEW
carryoverIsNegative?: boolean // NEW
}
```
Pass to the balance `StatCard`:
```tsx
<StatCard
title={t("dashboard.availableBalance")}
value={balance.value}
valueClassName={balance.isPositive ? "text-on-budget" : "text-over-budget"}
subtitle={balance.carryoverSubtitle}
subtitleClassName={balance.carryoverIsNegative ? "text-over-budget" : undefined}
/>
```
**6. DashboardContent carryover pass-through (src/pages/DashboardPage.tsx):**
In the `DashboardContent` function, after the `availableBalance` computation (line ~125) and before the `return`, compute the carryover subtitle:
```typescript
const carryover = budget.carryover_amount
const carryoverSubtitle = carryover !== 0
? t("dashboard.carryoverIncludes", { amount: formatCurrency(Math.abs(carryover), currency) })
: undefined
const carryoverIsNegative = carryover < 0
```
Update the `SummaryStrip` balance prop:
```tsx
balance={{
value: formatCurrency(availableBalance, currency),
isPositive: availableBalance >= 0,
carryoverSubtitle,
carryoverIsNegative,
}}
```
Note: The `t` function used in DashboardContent is from `useTranslation()` — it already supports interpolation.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run lint && bun run build</automated>
</verify>
<done>
- StatCard accepts optional subtitle/subtitleClassName props and renders subtitle text below value
- SummaryStrip accepts carryoverSubtitle/carryoverIsNegative on balance and passes to StatCard
- DashboardContent computes carryover subtitle from budget.carryover_amount and passes to SummaryStrip
- CSS animation tokens for collapsible-open/close defined in index.css
- i18n keys for sections and carryover added to both en.json and de.json
</done>
</task>
<task type="auto">
<name>Task 2: Build CategorySection and CollapsibleSections components</name>
<files>src/components/dashboard/CategorySection.tsx, src/components/dashboard/CollapsibleSections.tsx</files>
<action>
**1. Create src/components/dashboard/CategorySection.tsx:**
A pure presentational component. Accepts pre-computed group data, controlled open/onOpenChange, and `t()` for i18n.
```typescript
interface CategorySectionProps {
type: CategoryType
label: string
items: BudgetItem[]
budgeted: number
actual: number
currency: string
open: boolean
onOpenChange: (open: boolean) => void
t: (key: string, opts?: Record<string, unknown>) => string
}
```
Implementation details:
- Import `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` from `@/components/ui/collapsible`
- Import `Table`, `TableBody`, `TableCell`, `TableFooter`, `TableHead`, `TableHeader`, `TableRow` from `@/components/ui/table`
- Import `Badge` from `@/components/ui/badge`
- Import `ChevronRight` from `lucide-react`
- Import `categoryColors` from `@/lib/palette`
- Import `formatCurrency` from `@/lib/format`
- Import `cn` from `@/lib/utils`
- Import `CategoryType`, `BudgetItem` from `@/lib/types`
**Header (CollapsibleTrigger):**
- `<Collapsible open={open} onOpenChange={onOpenChange}>`
- Trigger is a `<button>` with `asChild` on CollapsibleTrigger
- Button has: `className="group flex w-full items-center gap-3 rounded-md border-l-4 bg-card px-4 py-3 text-left hover:bg-muted/40 transition-colors"`
- Inline style: `style={{ borderLeftColor: categoryColors[type] }}`
- ChevronRight icon: `className="size-4 shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-90"` with `aria-hidden`
- Label span: `className="font-medium"` showing `{label}`
- Right side (ml-auto flex items-center gap-2):
- Badge variant="outline" className="tabular-nums": `{t("budgets.budgeted")} {formatCurrency(budgeted, currency)}`
- Badge variant="secondary" className="tabular-nums": `{t("budgets.actual")} {formatCurrency(actual, currency)}`
- Difference span with color coding
**Difference logic (direction-aware per user decision):**
- Spending categories (bill, variable_expense, debt): `diff = budgeted - actual`, isOver when `actual > budgeted`
- Income/saving/investment: `diff = actual - budgeted`, isOver when `actual < budgeted`
- Color: `isOver ? "text-over-budget" : "text-on-budget"`
- Display `formatCurrency(Math.abs(diff), currency)`
**Content (CollapsibleContent):**
- `className="overflow-hidden data-[state=open]:animate-collapsible-open data-[state=closed]:animate-collapsible-close"`
- Contains a `<div className="pt-2">` wrapper for spacing
- Table with 4 columns per user decision: Item Name, Budgeted, Actual, Difference
- TableHeader: use `t("dashboard.sections.itemName")`, `t("budgets.budgeted")`, `t("budgets.actual")`, `t("budgets.difference")` — last 3 columns right-aligned
- TableBody: map items, each row:
- Name cell: `item.category?.name ?? item.category_id`, `className="font-medium"`
- Budgeted cell: `formatCurrency(item.budgeted_amount, currency)`, `className="text-right tabular-nums"`
- Actual cell: `formatCurrency(item.actual_amount, currency)`, `className="text-right tabular-nums"`
- Difference cell: direction-aware diff calculation (same isIncome logic as header), color-coded, `className="text-right tabular-nums"` with `text-over-budget` when item is over, else `text-muted-foreground`
- TableFooter: bold group totals row
- First cell: `t("dashboard.sections.groupTotal", { label })`, `className="font-medium"`
- Three total cells: budgeted, actual, diff — all `font-medium text-right tabular-nums`
- Footer diff uses same color coding as header (isOver ? text-over-budget : text-on-budget)
**Per-item difference direction awareness:**
- Each item's direction depends on its category type (which is `type` for all items in this section since they're pre-grouped)
- For spending types: item diff = `item.budgeted_amount - item.actual_amount`, isOver = `item.actual_amount > item.budgeted_amount`
- For income/saving/investment: item diff = `item.actual_amount - item.budgeted_amount`, isOver = `item.actual_amount < item.budgeted_amount`
**2. Create src/components/dashboard/CollapsibleSections.tsx:**
Container component that renders an ordered list of CategorySection components.
```typescript
interface GroupData {
type: CategoryType
label: string
items: BudgetItem[]
budgeted: number
actual: number
}
interface CollapsibleSectionsProps {
groups: GroupData[]
currency: string
openSections: Record<string, boolean>
onToggleSection: (type: string, open: boolean) => void
t: (key: string, opts?: Record<string, unknown>) => string
}
```
Implementation:
- Import `CategorySection` from `./CategorySection`
- Import `CategoryType`, `BudgetItem` from `@/lib/types`
- Render a `<div className="space-y-3">` wrapping `groups.map(...)`
- For each group, render `<CategorySection key={group.type} type={group.type} label={group.label} items={group.items} budgeted={group.budgeted} actual={group.actual} currency={currency} open={openSections[group.type] ?? false} onOpenChange={(open) => onToggleSection(group.type, open)} t={t} />`
This component is thin glue — its purpose is to keep DashboardContent clean and provide a clear interface boundary for Plan 02 to wire into.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run lint && bun run build</automated>
</verify>
<done>
- CategorySection.tsx exports a presentational collapsible section with header badges, chevron rotation, line-item table with 4 columns, and footer totals
- Direction-aware difference logic implemented per user decision (spending: over when actual > budget; income/saving/investment: over when actual < budget)
- CollapsibleSections.tsx exports a container that renders ordered CategorySection list with controlled open state
- Both components accept t() as prop (presentational pattern)
- Lint and build pass
</done>
</task>
</tasks>
<verification>
- `bun run lint` passes with no new errors
- `bun run build` succeeds (TypeScript compile + Vite bundle)
- StatCard renders subtitle when provided, hides when undefined
- CSS animation keyframes defined for collapsible-open and collapsible-close
- i18n keys present in both en.json and de.json
- CategorySection and CollapsibleSections importable from components/dashboard/
</verification>
<success_criteria>
- Carryover subtitle flows from DashboardContent through SummaryStrip to StatCard balance card
- CategorySection renders correct header layout: left border accent, chevron, label, badges, difference
- CategorySection renders correct table: 4 columns, direction-aware coloring, footer totals
- CollapsibleSections renders all groups with controlled open state
- No TypeScript errors, no lint errors, build succeeds
</success_criteria>
<output>
After completion, create `.planning/phases/03-collapsible-dashboard-sections/03-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,351 @@
---
phase: 03-collapsible-dashboard-sections
plan: 02
type: execute
wave: 2
depends_on:
- 03-01
files_modified:
- src/pages/DashboardPage.tsx
- src/components/dashboard/DashboardSkeleton.tsx
autonomous: false
requirements:
- UI-DASH-01
- UI-COLLAPSE-01
must_haves:
truths:
- "Each non-empty category group renders as a collapsible section between charts and QuickAdd"
- "Over-budget sections auto-expand on load (direction-aware: spending overspent, income/savings under-earned/saved)"
- "On/under-budget sections start collapsed"
- "Empty category groups are hidden entirely"
- "Expand/collapse state resets when navigating months"
- "Toggling sections does not produce ResizeObserver loop errors or chart resize jank"
- "Collapsible sections animate open/close smoothly with no flicker on mount"
- "DashboardSkeleton mirrors the sections area layout"
artifacts:
- path: "src/pages/DashboardPage.tsx"
provides: "groupedSections useMemo, openSections state, CollapsibleSections rendering"
contains: "groupedSections"
- path: "src/components/dashboard/DashboardSkeleton.tsx"
provides: "Skeleton placeholders for collapsible sections area"
contains: "Skeleton"
key_links:
- from: "src/pages/DashboardPage.tsx"
to: "src/components/dashboard/CollapsibleSections.tsx"
via: "renders CollapsibleSections with grouped data and open state"
pattern: "CollapsibleSections.*groups.*openSections"
- from: "src/pages/DashboardPage.tsx"
to: "useBudgetDetail items"
via: "groupedSections useMemo derives groups from items"
pattern: "groupedSections.*useMemo.*items\\.filter"
---
<objective>
Wire the collapsible sections into DashboardContent with smart expand/collapse defaults, month-navigation state reset, and chart isolation. Update DashboardSkeleton.
Purpose: Complete the dashboard hybrid view by integrating the CategorySection/CollapsibleSections components built in Plan 01 into the live dashboard page with all the required state management.
Output: Fully functional collapsible sections on the dashboard, DashboardSkeleton updated.
</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/03-collapsible-dashboard-sections/03-CONTEXT.md
@.planning/phases/03-collapsible-dashboard-sections/03-RESEARCH.md
@.planning/phases/03-collapsible-dashboard-sections/03-VALIDATION.md
@.planning/phases/03-collapsible-dashboard-sections/03-01-SUMMARY.md
@src/pages/DashboardPage.tsx
@src/components/dashboard/DashboardSkeleton.tsx
@src/components/dashboard/CollapsibleSections.tsx
@src/components/dashboard/CategorySection.tsx
<interfaces>
<!-- Interfaces created by Plan 01 that this plan consumes. -->
From src/components/dashboard/CollapsibleSections.tsx (created in Plan 01):
```typescript
interface GroupData {
type: CategoryType
label: string
items: BudgetItem[]
budgeted: number
actual: number
}
interface CollapsibleSectionsProps {
groups: GroupData[]
currency: string
openSections: Record<string, boolean>
onToggleSection: (type: string, open: boolean) => void
t: (key: string, opts?: Record<string, unknown>) => string
}
export function CollapsibleSections(props: CollapsibleSectionsProps): JSX.Element
```
From src/pages/DashboardPage.tsx (current state after Plan 01):
```typescript
// DashboardContent receives { budgetId: string }
// Uses useBudgetDetail(budgetId) -> { budget, items, loading }
// Already has: totalIncome, totalExpenses, budgetedIncome, budgetedExpenses, pieData, incomeBarData, spendBarData useMemos
// Already has: carryover subtitle computation from Plan 01
// Layout: SummaryStrip -> chart grid -> QuickAdd
// Collapsible sections insert between chart grid and QuickAdd
```
From src/lib/types.ts:
```typescript
export type CategoryType = "income" | "bill" | "variable_expense" | "debt" | "saving" | "investment"
export interface BudgetItem { id: string; budgeted_amount: number; actual_amount: number; category?: Category }
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Wire collapsible sections into DashboardContent with smart defaults</name>
<files>src/pages/DashboardPage.tsx, src/components/dashboard/DashboardSkeleton.tsx</files>
<action>
**1. Add imports to DashboardPage.tsx:**
```typescript
import { useState, useMemo, useEffect } from "react" // add useState, useEffect
import { CollapsibleSections } from "@/components/dashboard/CollapsibleSections"
```
**2. Add CATEGORY_TYPES_ALL constant (near top, alongside existing EXPENSE_TYPES):**
```typescript
const CATEGORY_TYPES_ALL: CategoryType[] = [
"income",
"bill",
"variable_expense",
"debt",
"saving",
"investment",
]
```
**3. Add isOverBudget helper function at module level (near constants):**
```typescript
function isOverBudget(type: CategoryType, budgeted: number, actual: number): boolean {
if (type === "income" || type === "saving" || type === "investment") {
return actual < budgeted // under-earned / under-saved
}
return actual > budgeted // overspent
}
```
**4. Add groupedSections useMemo in DashboardContent:**
Place after the existing `spendBarData` useMemo and BEFORE the early returns (`if (loading)` / `if (!budget)`). This follows the established hooks-before-returns pattern from Phase 2.
```typescript
const groupedSections = useMemo(() =>
CATEGORY_TYPES_ALL
.map((type) => {
const groupItems = items.filter((i) => i.category?.type === type)
if (groupItems.length === 0) return null
const budgeted = groupItems.reduce((s, i) => s + i.budgeted_amount, 0)
const actual = groupItems.reduce((s, i) => s + i.actual_amount, 0)
return {
type,
label: t(`categories.types.${type}`),
items: groupItems,
budgeted,
actual,
}
})
.filter((g): g is NonNullable<typeof g> => g !== null),
[items, t]
)
```
**5. Add openSections state and reset effect (after groupedSections, before early returns):**
```typescript
const [openSections, setOpenSections] = useState<Record<string, boolean>>(() =>
Object.fromEntries(
groupedSections.map((g) => [g.type, isOverBudget(g.type, g.budgeted, g.actual)])
)
)
// Reset expand state when month (budgetId) changes
useEffect(() => {
setOpenSections(
Object.fromEntries(
groupedSections.map((g) => [g.type, isOverBudget(g.type, g.budgeted, g.actual)])
)
)
}, [budgetId]) // budgetId changes on month navigation; groupedSections flows from it
```
IMPORTANT: `useState` and `useEffect` must be called before any early returns (hooks rules). The ordering in DashboardContent should be:
1. All existing useMemo hooks (totalIncome, etc.)
2. groupedSections useMemo (new)
3. openSections useState (new)
4. openSections useEffect (new)
5. Early returns (loading, !budget)
6. Computed values and JSX
**6. Add handleToggleSection callback (after early returns, before JSX return):**
```typescript
const handleToggleSection = (type: string, open: boolean) => {
setOpenSections((prev) => ({ ...prev, [type]: open }))
}
```
**7. Update the JSX layout in DashboardContent:**
Insert `CollapsibleSections` between the chart grid `</div>` and the QuickAdd `<div>`:
```tsx
{/* Collapsible category sections */}
{groupedSections.length > 0 && (
<CollapsibleSections
groups={groupedSections}
currency={currency}
openSections={openSections}
onToggleSection={handleToggleSection}
t={t}
/>
)}
```
The final DashboardContent JSX order becomes:
1. SummaryStrip
2. Chart grid (3-column)
3. CollapsibleSections (new)
4. QuickAdd button
**8. Update DashboardSkeleton (src/components/dashboard/DashboardSkeleton.tsx):**
Add skeleton placeholders for the collapsible sections area. After the chart grid skeleton div, add:
```tsx
{/* Collapsible sections skeleton */}
<div className="space-y-3">
{[1, 2, 3].map((i) => (
<div key={i} className="flex items-center gap-3 rounded-md border-l-4 border-muted bg-card px-4 py-3">
<Skeleton className="size-4" />
<Skeleton className="h-4 w-32" />
<div className="ml-auto flex items-center gap-2">
<Skeleton className="h-5 w-24 rounded-full" />
<Skeleton className="h-5 w-24 rounded-full" />
<Skeleton className="h-4 w-16" />
</div>
</div>
))}
</div>
```
This mirrors 3 collapsed section headers (the most common default state), matching the real CategorySection header structure.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run lint && bun run build</automated>
</verify>
<done>
- DashboardContent derives groupedSections from items via useMemo (filters empty groups, computes totals)
- openSections state initializes with direction-aware smart defaults (over-budget expanded, others collapsed)
- openSections resets via useEffect keyed on budgetId (month navigation)
- CollapsibleSections renders between chart grid and QuickAdd
- All hooks declared before early returns (Rules of Hooks compliance)
- DashboardSkeleton includes section header placeholders
- Lint and build pass
</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 2: Verify collapsible sections and carryover display</name>
<files>none</files>
<action>
Human verification of the complete Phase 3 feature set. No code changes — this is a visual/functional verification checkpoint.
**What was built across Plan 01 and Plan 02:**
- Collapsible per-category sections between charts and QuickAdd
- Smart expand/collapse defaults (over-budget sections auto-expand)
- Carryover subtitle on balance card when non-zero
- Smooth expand/collapse animation
- Direction-aware difference coloring
- DashboardSkeleton updated with section placeholders
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
</verify>
<done>Human approves all 11 verification checks pass</done>
<what-built>
Complete dashboard hybrid view with:
- Collapsible per-category sections between charts and QuickAdd
- Smart expand/collapse defaults (over-budget sections auto-expand)
- Carryover subtitle on balance card when non-zero
- Smooth expand/collapse animation
- Direction-aware difference coloring
</what-built>
<how-to-verify>
1. Run `cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run dev` and open http://localhost:5173
2. **Collapsible sections visible:** Navigate to a month with budget items. Verify collapsible sections appear below the chart grid and above the QuickAdd button.
3. **Section header design:** Each section should have:
- Thick colored left border (category color)
- Chevron icon on the left
- Category group label (e.g., "Income", "Bills")
- Two badges on the right: "Budgeted $X" and "Actual $X"
- Color-coded difference (green for on-budget, red for over-budget)
4. **Smart defaults:** If any category group is over-budget (e.g., spending actual > budget), that section should be expanded on page load. On-budget sections should be collapsed.
5. **Expand/collapse animation:** Click a section header. It should expand with a smooth 200ms animation. Click again to collapse. No layout jank in the charts above.
6. **Line-item table:** Expanded sections show a 4-column table: Item Name, Budgeted, Actual, Difference. Footer row with bold group totals.
7. **Empty groups hidden:** If a category type has zero budget items, it should not appear at all.
8. **Month navigation reset:** Expand/collapse some sections, then navigate to a different month. Smart defaults should recalculate.
9. **Carryover display:** If the budget has a non-zero `carryover_amount`, the balance card should show "Includes $X carryover" in small text below the balance value. If carryover is zero, no subtitle should appear.
10. **Rapid toggle:** Toggle sections open/closed rapidly 10+ times. Check browser console (F12) for "ResizeObserver loop" errors.
11. **Chevron rotation:** When a section is expanded, the chevron should rotate 90 degrees (pointing down). When collapsed, it should point right.
</how-to-verify>
<resume-signal>Type "approved" or describe any issues found</resume-signal>
</task>
</tasks>
<verification>
- `bun run lint` passes
- `bun run build` succeeds
- Dashboard renders collapsible sections for all non-empty category groups
- Over-budget sections auto-expand, on-budget sections start collapsed
- Section headers show correct badges, left border accent, and difference
- Line-item tables have 4 columns with footer totals
- Carryover subtitle displays on balance card when non-zero
- Expand/collapse animation is smooth, no ResizeObserver errors
- Month navigation resets expand/collapse state
</verification>
<success_criteria>
- All 4 ROADMAP success criteria for Phase 3 are met:
1. Category groups render as collapsible sections with color-accented headers, budgeted/actual totals, and difference
2. Expanding reveals line-item table, collapsing hides it with smooth animation, no chart jank
3. Rapid toggling produces no ResizeObserver loop errors
4. Carryover amount visible on balance card when non-zero
- Human verification checkpoint passes
</success_criteria>
<output>
After completion, create `.planning/phases/03-collapsible-dashboard-sections/03-02-SUMMARY.md`
</output>