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 |
|
true |
|
|
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
}
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
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,CollapsibleContentfrom@/components/ui/collapsible - Import
Table,TableBody,TableCell,TableFooter,TableHead,TableHeader,TableRowfrom@/components/ui/table - Import
Badgefrom@/components/ui/badge - Import
ChevronRightfromlucide-react - Import
categoryColorsfrom@/lib/palette - Import
formatCurrencyfrom@/lib/format - Import
cnfrom@/lib/utils - Import
CategoryType,BudgetItemfrom@/lib/types
Header (CollapsibleTrigger):
<Collapsible open={open} onOpenChange={onOpenChange}>- Trigger is a
<button>withasChildon 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"witharia-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
- Badge variant="outline" className="tabular-nums":
Difference logic (direction-aware per user decision):
- Spending categories (bill, variable_expense, debt):
diff = budgeted - actual, isOver whenactual > budgeted - Income/saving/investment:
diff = actual - budgeted, isOver whenactual < 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"withtext-over-budgetwhen item is over, elsetext-muted-foreground
- Name cell:
- 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)
- First cell:
Per-item difference direction awareness:
- Each item's direction depends on its category type (which is
typefor 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
CategorySectionfrom./CategorySection - Import
CategoryType,BudgetItemfrom@/lib/types - Render a
<div className="space-y-3">wrappinggroups.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
<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>