Files
SimpleFinanceDash/.planning/phases/03-collapsible-dashboard-sections/03-01-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
03-collapsible-dashboard-sections 01 execute 1
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
true
UI-DASH-01
UI-COLLAPSE-01
truths artifacts key_links
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
path provides contains
src/index.css Collapsible animation keyframes and tokens collapsible-open
path provides contains
src/i18n/en.json Section and carryover i18n keys carryoverIncludes
path provides contains
src/i18n/de.json German section and carryover i18n keys carryoverIncludes
path provides contains
src/components/dashboard/StatCard.tsx Optional subtitle prop for carryover display subtitle
path provides contains
src/components/dashboard/SummaryStrip.tsx Carryover subtitle threading to balance StatCard carryoverSubtitle
path provides contains
src/pages/DashboardPage.tsx Carryover subtitle computed and passed to SummaryStrip carryoverSubtitle
path provides exports
src/components/dashboard/CategorySection.tsx Collapsible section with header badges and line-item table
CategorySection
path provides exports
src/components/dashboard/CollapsibleSections.tsx Container rendering ordered CategorySection list
CollapsibleSections
from to via pattern
src/pages/DashboardPage.tsx src/components/dashboard/SummaryStrip.tsx carryoverSubtitle prop on balance object carryoverSubtitle.*formatCurrency.*carryover
from to via pattern
src/components/dashboard/SummaryStrip.tsx src/components/dashboard/StatCard.tsx subtitle prop subtitle.*carryoverSubtitle
from to via pattern
src/components/dashboard/CollapsibleSections.tsx src/components/dashboard/CategorySection.tsx renders CategorySection per group CategorySection
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.

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

From src/lib/types.ts:

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:

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:

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

From src/components/ui/collapsible.tsx:

export { Collapsible, CollapsibleTrigger, CollapsibleContent }

From src/components/dashboard/StatCard.tsx (current):

interface StatCardProps {
  title: string
  value: string
  valueClassName?: string
  variance?: { amount: string; direction: "up" | "down" | "neutral"; label: string }
}

From src/components/dashboard/SummaryStrip.tsx (current):

interface SummaryStripProps {
  income: { value: string; budgeted: string }
  expenses: { value: string; budgeted: string }
  balance: { value: string; isPositive: boolean }
  t: (key: string) => string
}
Task 1: Add CSS animation tokens, i18n keys, and carryover display 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 **1. CSS animation tokens (src/index.css):**

Add to the existing @theme inline block, after the --radius line:

/* Collapsible animation */
--animate-collapsible-open: collapsible-open 200ms ease-out;
--animate-collapsible-close: collapsible-close 200ms ease-out;

Add after the @layer base block:

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

"sections": {
  "itemName": "Item",
  "groupTotal": "{{label}} Total"
},
"carryoverIncludes": "Includes {{amount}} carryover"

3. i18n keys (src/i18n/de.json):

Add under the "dashboard" object:

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

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:

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

balance: {
  value: string
  isPositive: boolean
  carryoverSubtitle?: string      // NEW
  carryoverIsNegative?: boolean   // NEW
}

Pass to the balance StatCard:

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

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:

balance={{
  value: formatCurrency(availableBalance, currency),
  isPositive: availableBalance >= 0,
  carryoverSubtitle,
  carryoverIsNegative,
}}

Note: The t function used in DashboardContent is from useTranslation() — it already supports interpolation. cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run lint && bun run build

  • 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
Task 2: Build CategorySection and CollapsibleSections components src/components/dashboard/CategorySection.tsx, src/components/dashboard/CollapsibleSections.tsx **1. Create src/components/dashboard/CategorySection.tsx:**

A pure presentational component. Accepts pre-computed group data, controlled open/onOpenChange, and t() for i18n.

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.

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. cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run lint && bun run build

  • 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
- `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/

<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>
After completion, create `.planning/phases/03-collapsible-dashboard-sections/03-01-SUMMARY.md`