# Phase 3: Collapsible Dashboard Sections - Research **Researched:** 2026-03-17 **Domain:** React collapsible UI, Radix UI Collapsible, Tailwind CSS animation, ResizeObserver, dashboard data grouping **Confidence:** HIGH --- ## User Constraints (from CONTEXT.md) ### Locked Decisions **Section header design** - Badge-style chips for totals: two small colored badges showing `[Budget $X]` and `[Actual $X]` right-aligned in the header row - Left border accent using the category's CSS variable color (thick colored left border on the header row) - Difference shown in header with color coding: green (`--color-on-budget`) when under/on budget, red (`--color-over-budget`) when over budget - Chevron-right icon that rotates to chevron-down when expanded (standard Radix Collapsible pattern) - Group label (from `categoryLabels` in palette.ts) on the left, badges and difference on the right **Line-item table columns** - Four columns: Item Name, Budgeted, Actual, Difference - No tier badge — keep it clean for the dashboard summary view - No notes column — full detail lives on BudgetDetailPage - Difference column uses red text when over budget (`--color-over-budget`), no full-row tint - Footer row with bold group totals summing Budget, Actual, and Diff columns - Read-only — no clickable rows, no navigation links to BudgetDetailPage **Default expand/collapse behavior** - Smart default: over-budget sections auto-expand on load, on/under-budget sections start collapsed - Over-budget logic is direction-aware: - Spending categories (bill, variable_expense, debt): actual > budget = over budget (expand) - Income category: actual < budget = under-earned (expand) - Savings/investment categories: actual < budget = under-saved (expand) - Empty category groups (no items of that type) are hidden entirely — only show sections with at least one budget item - Expand/collapse state resets on month navigation — smart defaults recalculate per month **Carryover display** - Subtitle line below balance amount on the balance StatCard: "Includes $X carryover" when non-zero - Carryover is included in the balance calculation: Balance = Income Actual - Expenses Actual + Carryover - When carryover is zero, the subtitle line is hidden entirely (clean card for the common case) - Negative carryover is supported: shown with red styling (e.g., "Includes -$150 carryover"), deducts from balance ### Claude's Discretion - Smooth collapse/expand CSS animation details (timing, easing) - Preventing ResizeObserver loop errors when toggling rapidly (success criteria #3) - Preventing chart resize jank when sections toggle (success criteria #3) - Exact spacing between section headers and between sections and the chart grid above - Table cell alignment and typography within line items - DashboardSkeleton updates for the collapsible sections area - How to derive and memoize per-group data from budget items ### Deferred Ideas (OUT OF SCOPE) None — discussion stayed within phase scope. --- ## Phase Requirements | ID | Description | Research Support | |----|-------------|-----------------| | UI-DASH-01 | Redesign dashboard with hybrid layout — summary cards, charts, and collapsible category sections (income, bills, expenses, debt, savings) with budget/actual columns | Collapsible sections inserted between chart grid and QuickAdd in DashboardContent; all grouping/totals derived from existing `items` array via useMemo | | UI-COLLAPSE-01 | Add collapsible inline sections on dashboard for each category group showing individual line items | Radix Collapsible v1.1.12 already installed and wrapped in collapsible.tsx; exposes `--radix-collapsible-content-height` CSS variable for height animation | --- ## Summary Phase 3 adds collapsible per-category sections to the dashboard between the chart grid and the QuickAdd button. The codebase is well-prepared: `collapsible.tsx` wraps Radix Collapsible v1.1.12, the `Badge`, `Table`, and `StatCard` primitives are ready, `categoryColors`/`categoryLabels` from `palette.ts` map cleanly to section styling, and `CATEGORY_TYPES` + `EXPENSE_TYPES` constants already define the display order. `BudgetDetailPage.tsx` already implements identical grouping logic (group items by `category.type`, derive per-group totals, render a `Table` with a `TableFooter` row) — the dashboard sections are a read-only, collapsible variant of that pattern. The primary technical considerations are: (1) animating the Radix `CollapsibleContent` height smoothly using the `--radix-collapsible-content-height` CSS variable it exposes, (2) preventing `ResizeObserver loop` errors that Recharts can trigger when layout shifts affect chart container dimensions, and (3) threading `budget.carryover_amount` through `SummaryStrip` → `StatCard` to display the carryover subtitle on the balance card. **Primary recommendation:** Build a `CategorySection` component that wraps `Collapsible`/`CollapsibleTrigger`/`CollapsibleContent` with inline `border-l-4` styling, derive all section data in a single `useMemo` in `DashboardContent`, and isolate Recharts charts in a stable wrapper div to prevent ResizeObserver jank. --- ## Standard Stack ### Core | Library | Version | Purpose | Why Standard | |---------|---------|---------|--------------| | `@radix-ui/react-collapsible` (via `radix-ui`) | 1.1.12 | Accessible expand/collapse primitive | Already installed; exposes `data-state`, `aria-expanded`, `--radix-collapsible-content-height` | | `tailwindcss` | 4.2.x | Utility classes for animation, border accent, spacing | Already in use; v4 `@theme inline` CSS variable pattern used throughout | | `lucide-react` | 0.577.x | ChevronRight / ChevronDown icons | Already in use | ### Supporting | Library | Version | Purpose | When to Use | |---------|---------|---------|-------------| | `Badge` (ui/badge.tsx) | — | Budget/Actual chip badges in section header | Used for the two right-aligned total chips | | `Table` / `TableBody` / `TableCell` / `TableFooter` (ui/table.tsx) | — | Line-item rows and group total footer | Used for the expanded content table | | `StatCard` (dashboard/StatCard.tsx) | — | Balance card needing carryover subtitle | Needs a new optional `subtitle` prop | ### Alternatives Considered | Instead of | Could Use | Tradeoff | |------------|-----------|----------| | Radix Collapsible | HTML `
`/`` | No animation support; no `data-state` for CSS targeting; not Radix-integrated | | CSS height animation via `--radix-collapsible-content-height` | framer-motion `AnimatePresence` | framer-motion not in the stack; adding it would violate the "no new major dependencies" constraint | **Installation:** No new packages needed. All primitives are already installed. --- ## Architecture Patterns ### Recommended Project Structure New files for this phase: ``` src/components/dashboard/ ├── CategorySection.tsx # Collapsible section: header + table ├── CollapsibleSections.tsx # Renders ordered list of CategorySection ``` Modified files: ``` src/components/dashboard/StatCard.tsx # Add optional subtitle prop src/components/dashboard/SummaryStrip.tsx # Thread carryover subtitle to balance StatCard src/pages/DashboardPage.tsx # DashboardContent: add grouped sections data, pass carryover to SummaryStrip src/components/dashboard/DashboardSkeleton.tsx # Add skeleton rows for sections area src/i18n/en.json # New keys: dashboard.sections.*, dashboard.carryoverIncludes src/i18n/de.json # German equivalents ``` ### Pattern 1: Radix Collapsible with CSS Height Animation **What:** `CollapsibleContent` exposes `--radix-collapsible-content-height` as an inline CSS variable on the content div. A Tailwind keyframe animation reads this variable to animate `max-height` from `0` to the measured natural height. **When to use:** Any time the Radix Collapsible content needs a smooth open/close height transition without a JS animation library. **How Radix sets the variable (from source):** ```typescript // @radix-ui/react-collapsible source (CollapsibleContentImpl) style: { [`--radix-collapsible-content-height`]: height ? `${height}px` : undefined, [`--radix-collapsible-content-width`]: width ? `${width}px` : undefined, ...props.style } ``` The `height` value is measured from `getBoundingClientRect()` in a `useLayoutEffect` — it is the element's natural height when fully open. The variable is set on the content div itself. **Tailwind v4 animation pattern (index.css addition):** ```css @theme inline { /* ... existing tokens ... */ --animate-collapsible-open: collapsible-open 200ms ease-out; --animate-collapsible-close: collapsible-close 200ms ease-out; } @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; } } ``` **CollapsibleContent usage:** ```tsx // Source: Radix Collapsible docs pattern + project CSS variable system {/* table content */} ``` **Key detail:** `data-[state=open]` and `data-[state=closed]` are set by Radix on the `CollapsibleContent` div (from `getState(context.open)` — returns `"open"` or `"closed"`). Tailwind v4's arbitrary variant syntax `data-[state=open]:` works directly against these attributes. ### Pattern 2: Controlled Collapsible with Smart Defaults **What:** Drive open/close state with local `useState` in `DashboardContent` (not inside `CategorySection`). Compute initial state from budget data, reset when `budgetId` changes (i.e., on month navigation). **When to use:** When expand state needs to be computed from data (smart defaults) and reset on navigation. **Example:** ```typescript // In DashboardContent — after all item-derived useMemos const CATEGORY_TYPES_ALL: CategoryType[] = [ "income", "bill", "variable_expense", "debt", "saving", "investment" ] 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) // Direction-aware over-budget check const isOverBudget = type === "income" || type === "saving" || type === "investment" ? actual < budgeted // under-earned / under-saved : actual > budgeted // overspent return { type, items: groupItems, budgeted, actual, isOverBudget } }) .filter(Boolean), [items] ) // Initial expand state: over-budget sections open, others closed // Key on budgetId so state resets when month changes const [openSections, setOpenSections] = useState>(() => Object.fromEntries( (groupedSections ?? []).map((g) => [g!.type, g!.isOverBudget]) ) ) // Reset when budgetId (month) changes useEffect(() => { setOpenSections( Object.fromEntries( (groupedSections ?? []).map((g) => [g!.type, g!.isOverBudget]) ) ) }, [budgetId]) // budgetId is the stable dependency; groupedSections flows from it ``` **Note on Rules of Hooks:** `useState` initializer runs once. The `useEffect` driven by `budgetId` handles the reset-on-navigation requirement without violating hooks rules. All `useMemo` hooks for `groupedSections` must be declared before any early returns (established pattern from Phase 2). ### Pattern 3: CategorySection Component **What:** A pure presentational component — accepts pre-computed group data and delegates all state management to the parent via `open` / `onOpenChange` props (controlled pattern). **Example:** ```tsx // Source: project conventions + Radix Collapsible controlled pattern 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 } export function CategorySection({ type, label, items, budgeted, actual, currency, open, onOpenChange, t }: CategorySectionProps) { const diff = /* direction-aware difference */ const isOver = /* direction-aware over-budget flag */ const accentColor = categoryColors[type] // "var(--color-income)" etc. return ( {/* Table with items + footer */} ) } ``` **Chevron rotation note:** The `[[data-state=open]_&]:rotate-90` class uses Tailwind v4 ancestor-state targeting. An ancestor element with `data-state="open"` (the `CollapsibleTrigger` button itself has `data-state` set by Radix) rotates the icon. Alternative: target the trigger's own `data-state` with `group-data-[state=open]:rotate-90` if a `group` class is applied to the trigger. ### Pattern 4: StatCard Carryover Subtitle **What:** Add an optional `subtitle` string prop to `StatCard`. When provided, renders below the value with small muted text. The balance card passes "Includes $X carryover" when `budget.carryover_amount !== 0`. **Modified StatCard interface:** ```typescript interface StatCardProps { title: string value: string valueClassName?: string subtitle?: string // NEW — optional small text below value subtitleClassName?: string // NEW — optional class override (for negative carryover red) variance?: { ... } } ``` **SummaryStrip carryover prop threading:** ```typescript interface SummaryStripProps { income: { value: string; budgeted: string } expenses: { value: string; budgeted: string } balance: { value: string; isPositive: boolean; carryoverSubtitle?: string; carryoverIsNegative?: boolean } t: (key: string) => string } ``` ### Anti-Patterns to Avoid - **Storing expand state inside `CategorySection`:** Breaks the reset-on-navigation requirement. All expand state must live in `DashboardContent` keyed to `budgetId`. - **Computing grouped data inside `CategorySection`:** Items should be pre-grouped in `DashboardContent` via `useMemo`. `CategorySection` is purely presentational. - **Using `overflow: hidden` on the outer `Collapsible` root:** Only apply `overflow: hidden` to `CollapsibleContent` (animated element), not the outer root, to avoid clipping box-shadows on the header. - **Declaring `useState`/`useMemo` after early returns:** Violates React hooks rules. All hooks must be declared before `if (loading) return `. - **Animating with `max-height: 9999px`:** Produces visible animation lag. Use `--radix-collapsible-content-height` (exact measured height) with `height` animation instead. --- ## Don't Hand-Roll | Problem | Don't Build | Use Instead | Why | |---------|-------------|-------------|-----| | Accessible expand/collapse | Custom `aria-expanded` + DOM toggle | `Collapsible` / `CollapsibleTrigger` / `CollapsibleContent` from `ui/collapsible.tsx` | Radix handles `aria-expanded`, `aria-controls`, `id` linkage, keyboard (Enter/Space), and disabled state | | Height measurement for animation | `ResizeObserver` + state | `--radix-collapsible-content-height` CSS variable from Radix | Radix measures height in `useLayoutEffect` and sets the variable; no custom measurement needed | | Category color accent border | Tailwind color class | Inline `style={{ borderLeftColor: categoryColors[type] }}` | `categoryColors` maps to CSS custom property strings (`"var(--color-income)"`); Tailwind can't generate arbitrary CSS variable values without JIT config | **Key insight:** The Radix `CollapsibleContent` implementation already handles the tricky edge cases: mount-animation prevention on initial render (the `isMountAnimationPreventedRef.current` flag), measurement timing (uses `useLayoutEffect` to measure before paint), and `hidden` attribute management (element is `hidden` when closed, preventing tab focus and screen reader access to invisible content). --- ## Common Pitfalls ### Pitfall 1: ResizeObserver Loop Errors from Recharts **What goes wrong:** When collapsible sections open/close, the document layout shifts. Recharts' `ResponsiveContainer` uses a `ResizeObserver` internally. If the observer callback fires mid-layout and triggers a chart re-render that itself changes layout, the browser fires `ResizeObserver loop limit exceeded` console errors. **Why it happens:** Recharts charts are already rendered above the sections. The layout shift from section expand/collapse propagates upward through the document flow if the chart grid is not height-stable. **How to avoid:** 1. Give the chart grid a stable height by ensuring the three chart cards have `min-h-[xxx]` or fixed `h-[xxx]` Tailwind classes. The existing card + `ChartContainer` with `min-h-[250px]` (from Phase 2) already creates a floor. 2. Wrap the chart grid in a div with `overflow: hidden` or `contain: layout` to prevent section expand/collapse from reflowing chart dimensions. 3. Use CSS `contain: layout style` on the chart grid container: ```tsx {/* 3-column chart grid — isolated from section-toggle reflow */}
...charts...
``` **Warning signs:** `ResizeObserver loop limit exceeded` or `ResizeObserver loop completed with undelivered notifications` in the browser console after toggling sections. **Confidence:** MEDIUM — the `min-h-[250px]` on ChartContainer from Phase 2 may already be sufficient. Add `contain` only if errors appear in testing. ### Pitfall 2: Mount-Time Animation Flicker **What goes wrong:** Sections that start expanded (over-budget auto-expand) animate open on first render even though they should appear pre-opened. **Why it happens:** The Radix `CollapsibleContent` animation keyframe fires on mount if `defaultOpen={true}`. **How to avoid:** Radix handles this internally: `isMountAnimationPreventedRef.current` is initialized to `isOpen` (true if open on mount), and the `animationName` is set to `"none"` during the initial layout effect, then restored after a `requestAnimationFrame`. This means the animation is suppressed on mount automatically. No additional handling needed. ### Pitfall 3: Chevron Rotation Targeting **What goes wrong:** The chevron icon doesn't rotate because the Tailwind ancestor-state class references the wrong ancestor's `data-state`. **Why it happens:** In Radix Collapsible, the `data-state` attribute is set on both the `CollapsibleTrigger` button element AND the root `Collapsible` div. The icon is a child of the trigger button. **How to avoid:** Use the trigger's own `data-state` as the ancestor for rotation. The simplest approach — add `group` class to the `CollapsibleTrigger` (or its `asChild` element) and use `group-data-[state=open]:rotate-90` on the icon: ```tsx ``` ### Pitfall 4: `useEffect` Reset Dependency Array **What goes wrong:** Expand state doesn't reset on month navigation, or resets on every render. **Why it happens:** Wrong dependency in the `useEffect` that resets `openSections`. **How to avoid:** Depend on `budgetId` (the string prop from `DashboardContent`), not on `groupedSections` (which changes reference on every render due to `useMemo`). `budgetId` changes exactly when the user navigates months. ### Pitfall 5: i18n Key Interpolation for Carryover Subtitle **What goes wrong:** Carryover subtitle shows `"Includes {{amount}} carryover"` as literal text. **Why it happens:** i18next interpolation requires `t("key", { amount: "..." })` and the JSON to use `{{amount}}` syntax. **How to avoid:** ```json // en.json "dashboard": { "carryoverIncludes": "Includes {{amount}} carryover" } ``` ```typescript t("dashboard.carryoverIncludes", { amount: formatCurrency(carryover, currency) }) ``` --- ## Code Examples Verified patterns from source inspection and project conventions: ### Collapsible Controlled Pattern ```tsx // Source: @radix-ui/react-collapsible v1.1.12 type definitions + project conventions import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible" {/* table */} ``` ### CSS Keyframe Animation (index.css addition) ```css /* Source: Radix docs pattern + project @theme inline convention */ @theme inline { --animate-collapsible-open: collapsible-open 200ms ease-out; --animate-collapsible-close: collapsible-close 200ms ease-out; } @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; } } ``` ### Direction-Aware Over-Budget Logic ```typescript // Source: CONTEXT.md locked decisions function isOverBudget(type: CategoryType, budgeted: number, actual: number): boolean { if (type === "income" || type === "saving" || type === "investment") { return actual < budgeted // under-earned / under-saved = problem } return actual > budgeted // overspent = problem } ``` ### Carryover Subtitle in StatCard ```typescript // Modified StatCard — add optional subtitle prop interface StatCardProps { title: string value: string valueClassName?: string subtitle?: string // e.g. "Includes €150.00 carryover" subtitleClassName?: string // e.g. "text-over-budget" for negative carryover variance?: { amount: string; direction: "up" | "down" | "neutral"; label: string } } // In render: {subtitle && (

{subtitle}

)} ``` ### Read-Only Line-Item Table Pattern (Dashboard variant) ```tsx // Source: BudgetDetailPage.tsx pattern, adapted for read-only dashboard use {t("categories.name")} {t("budgets.budgeted")} {t("budgets.actual")} {t("budgets.difference")} {items.map((item) => ( {item.category?.name ?? item.category_id} {formatCurrency(item.budgeted_amount, currency)} {formatCurrency(item.actual_amount, currency)} {formatCurrency(Math.abs(diff), currency)} ))} {t(`categories.types.${type}`)} Total {formatCurrency(budgeted, currency)} {formatCurrency(actual, currency)} {formatCurrency(Math.abs(diff), currency)}
``` ### New i18n Keys Required ```json // en.json additions under "dashboard": { "dashboard": { "sections": { "itemName": "Item", "groupTotal": "{{label}} Total" }, "carryoverIncludes": "Includes {{amount}} carryover" } } ``` ```json // de.json additions: { "dashboard": { "sections": { "itemName": "Posten", "groupTotal": "{{label}} Gesamt" }, "carryoverIncludes": "Inkl. {{amount}} Übertrag" } } ``` --- ## State of the Art | Old Approach | Current Approach | When Changed | Impact | |--------------|------------------|--------------|--------| | `max-height: 9999px` CSS hack | `height` animation with `--radix-collapsible-content-height` | Radix ~v1.0 | Smooth animation with no lag | | Custom `aria-expanded` management | Radix `CollapsibleTrigger` manages `aria-expanded` automatically | Radix v1.x | Correct accessibility with zero effort | | Separate `@radix-ui/react-collapsible` install | Included in `radix-ui` v1.4.3 umbrella package | radix-ui monorepo consolidation | Already present in project — no install needed | **Deprecated/outdated:** - `defaultOpen` on `Collapsible` root: Still valid, but we use controlled `open` + `onOpenChange` for the reset-on-navigation requirement - `hidden` prop removed from `CollapsibleContent` in newer Radix: Radix manages `hidden` attribute internally; never pass it manually --- ## Open Questions 1. **CSS `contain` on chart grid to prevent ResizeObserver errors** - What we know: Recharts uses ResizeObserver; layout shifts from sections opening can trigger loop errors - What's unclear: Whether Phase 2's `min-h-[250px]` on charts is already sufficient to prevent jank - Recommendation: Implement without `contain` first; add `[contain:layout_style]` to chart grid div only if ResizeObserver errors appear in manual testing 2. **Tailwind v4 `data-[]` variant syntax for `group-data-[state=open]`** - What we know: Tailwind v4 supports arbitrary group variants; the project uses `group` pattern already (sidebar.tsx) - What's unclear: Whether Tailwind v4's JIT generates `group-data-[state=open]:` without explicit config - Recommendation: Use it — Tailwind v4 generates arbitrary variants JIT; if it doesn't compile, fall back to the `[[data-state=open]_&]:rotate-90` CSS selector approach --- ## Validation Architecture ### Test Framework | Property | Value | |----------|-------| | Framework | None installed — no test infrastructure in project | | Config file | None — Wave 0 would need `vitest.config.ts` | | Quick run command | N/A | | Full suite command | `bun run lint` (only automated check available) | ### Phase Requirements → Test Map | Req ID | Behavior | Test Type | Automated Command | File Exists? | |--------|----------|-----------|-------------------|-------------| | UI-DASH-01 | Collapsible sections render between charts and QuickAdd | manual-only | — | N/A | | UI-DASH-01 | Over-budget sections auto-expand on load | manual-only | — | N/A | | UI-DASH-01 | Carryover subtitle appears on balance card when non-zero | manual-only | — | N/A | | UI-COLLAPSE-01 | Section expands/collapses with smooth animation | manual-only | — | N/A | | UI-COLLAPSE-01 | No ResizeObserver loop errors on rapid toggle | manual-only | browser console check | N/A | | UI-COLLAPSE-01 | Empty category groups are hidden | manual-only | — | N/A | | UI-COLLAPSE-01 | State resets on month navigation | manual-only | — | N/A | **Justification for manual-only:** No test framework is installed. The project has no `vitest`, `jest`, `@testing-library/react`, or similar. Installing a test framework is out of scope for Phase 3 (the project's TESTING.md notes this explicitly). All validation will be manual browser testing. The primary automated check available is `bun run lint` (ESLint), which catches hooks rules violations, unused variables, and TypeScript errors. ### Sampling Rate - **Per task commit:** `bun run lint` (catches TypeScript errors and hooks violations) - **Per wave merge:** `bun run build` (full TypeScript compile + Vite bundle) - **Phase gate:** Manual browser testing of all 7 behaviors above before `/gsd:verify-work` ### Wave 0 Gaps None — no test infrastructure to create. Lint and build are the only automated gates. --- ## Sources ### Primary (HIGH confidence) - `node_modules/@radix-ui/react-collapsible/dist/index.mjs` — Full source inspection of `CollapsibleContentImpl`; confirmed `--radix-collapsible-content-height` CSS variable, `data-state` values `"open"`/`"closed"`, `hidden` attribute management, mount-animation prevention via `isMountAnimationPreventedRef` - `node_modules/@radix-ui/react-collapsible/dist/index.d.ts` — Confirmed type signatures: `CollapsibleProps.open`, `CollapsibleProps.onOpenChange`, `CollapsibleProps.defaultOpen` - `src/components/ui/collapsible.tsx` — Confirmed wrapper already in project, exports `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` - `src/pages/DashboardPage.tsx` — Confirmed existing `DashboardContent` structure, `useMemo` placement before early returns, `budget.carryover_amount` already in scope - `src/pages/BudgetDetailPage.tsx` — Confirmed grouping pattern (`CATEGORY_TYPES.map`, `items.filter`, per-group totals), `Table`/`TableFooter` pattern, `DifferenceCell` logic - `src/lib/palette.ts` — Confirmed `categoryColors` returns `"var(--color-income)"` etc.; `categoryLabels` for EN/DE display strings - `src/lib/types.ts` — Confirmed `Budget.carryover_amount: number`, `BudgetItem.category?.type` - `src/index.css` — Confirmed `--color-over-budget`, `--color-on-budget` semantic tokens; `@theme inline` pattern for CSS custom properties - `src/i18n/en.json` + `de.json` — Confirmed existing keys; identified gaps for new keys - `src/components/dashboard/StatCard.tsx` — Confirmed current interface (no subtitle prop); variance prop pattern ### Secondary (MEDIUM confidence) - `.planning/codebase/CONVENTIONS.md` — Component structure, hooks-before-returns rule, import ordering, TypeScript strict mode - `.planning/codebase/TESTING.md` — Confirmed no test framework installed; lint/build are only automated checks - `.planning/STATE.md` — Confirmed pre-existing lint errors in unrelated files; Phase 2 patterns (useMemo before early returns, QuickAdd position) ### Tertiary (LOW confidence) - None — all findings verified against installed source code --- ## Metadata **Confidence breakdown:** - Standard stack: HIGH — all libraries inspected from installed node_modules source - Architecture: HIGH — patterns derived directly from existing codebase files - Pitfalls: MEDIUM — ResizeObserver issue is known Recharts behavior; specific CSS `contain` fix is speculative pending testing - Animation pattern: HIGH — `--radix-collapsible-content-height` confirmed from source, Tailwind v4 `data-[]` variants confirmed from existing sidebar.tsx usage **Research date:** 2026-03-17 **Valid until:** 2026-04-17 (stable libraries; CSS variables are fixed in installed source)