docs: complete project research
This commit is contained in:
452
.planning/research/ARCHITECTURE.md
Normal file
452
.planning/research/ARCHITECTURE.md
Normal file
@@ -0,0 +1,452 @@
|
||||
# Architecture Research
|
||||
|
||||
**Domain:** Personal finance dashboard UI — React SPA overhaul
|
||||
**Researched:** 2026-03-16
|
||||
**Confidence:** HIGH (existing codebase is fully inspected; patterns are grounded in Radix/shadcn/Recharts official docs)
|
||||
|
||||
---
|
||||
|
||||
## Standard Architecture
|
||||
|
||||
### System Overview
|
||||
|
||||
The existing three-tier architecture (Pages → Hooks → Supabase) is sound and must be preserved. The UI overhaul introduces a new layer of **dashboard-specific view components** that sit between pages and the primitive shadcn/ui atoms. Nothing touches hooks or the library layer.
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ Pages Layer │
|
||||
│ DashboardPage CategoriesPage BudgetDetailPage ... │
|
||||
│ (routing, data loading, layout composition) │
|
||||
├───────────────────────────────────────────────────────────────┤
|
||||
│ View Components Layer [NEW] │
|
||||
│ ┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐ │
|
||||
│ │ DashboardContent│ │CategorySection│ │ ChartPanel │ │
|
||||
│ │ (hybrid layout) │ │(collapsible) │ │ (chart wrappers)│ │
|
||||
│ └─────────────────┘ └──────────────┘ └─────────────────┘ │
|
||||
│ ┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐ │
|
||||
│ │ SummaryStrip │ │ BudgetTable │ │ PageShell │ │
|
||||
│ │ (KPI cards row) │ │ (line items) │ │ (consistent │ │
|
||||
│ └─────────────────┘ └──────────────┘ │ header+CTA) │ │
|
||||
│ └─────────────────┘ │
|
||||
├───────────────────────────────────────────────────────────────┤
|
||||
│ Primitive UI Layer (shadcn/ui) │
|
||||
│ Card Button Table Dialog Select Collapsible Badge ... │
|
||||
├───────────────────────────────────────────────────────────────┤
|
||||
│ Hooks Layer [UNCHANGED] │
|
||||
│ useBudgets useBudgetDetail useCategories useAuth ... │
|
||||
├───────────────────────────────────────────────────────────────┤
|
||||
│ Library Layer [UNCHANGED] │
|
||||
│ supabase.ts types.ts format.ts palette.ts utils.ts │
|
||||
│ index.css (@theme tokens — EXTEND for new color tokens) │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**The constraint is strict:** hooks and library are read-only during this milestone. All UI overhaul changes land in `src/pages/`, `src/components/`, and `src/index.css` only.
|
||||
|
||||
---
|
||||
|
||||
### Component Responsibilities
|
||||
|
||||
| Component | Responsibility | Typical Implementation |
|
||||
|-----------|----------------|------------------------|
|
||||
| `DashboardPage` | Find current month budget, render shell | Unchanged outer page; delegates to `DashboardContent` |
|
||||
| `DashboardContent` | Hybrid layout orchestration | Calls `useBudgetDetail`; computes derived data with `useMemo`; renders SummaryStrip + charts + CategorySections |
|
||||
| `SummaryStrip` | Three KPI cards (income, expenses, balance) | Grid of `StatCard` components; color-coded balance |
|
||||
| `StatCard` | Single KPI display unit | shadcn Card with title, large number, optional trend indicator |
|
||||
| `ChartPanel` | Houses all charts in responsive grid | Two-column grid on desktop: income bar chart left, expense donut right, spend horizontal bar full-width below |
|
||||
| `IncomeBarChart` | Budgeted vs actual income vertical bar | Recharts `BarChart` wrapped in `ChartContainer` with `ChartConfig` |
|
||||
| `ExpenseDonutChart` | Expense category breakdown donut | Recharts `PieChart` with `innerRadius`/`outerRadius` + custom legend |
|
||||
| `SpendBarChart` | Horizontal budget vs actual by category type | Recharts `BarChart layout="vertical"` |
|
||||
| `CategorySection` | Collapsible group for one category type | Radix `Collapsible.Root` wrapping a header row + `BudgetLineItems` |
|
||||
| `CategorySectionHeader` | Always-visible row: type label, color dot, group totals, chevron | Trigger for the collapsible; shows budgeted/actual/diff inline |
|
||||
| `BudgetLineItems` | Table of individual line items inside a section | shadcn `Table`; thin wrapper around existing `InlineEditCell` / `DifferenceCell` atoms |
|
||||
| `PageShell` | Consistent page header with title + primary CTA | Reusable wrapper used by every page; enforces padding, heading size, CTA slot |
|
||||
| `AppLayout` | Sidebar navigation shell | Minor visual refresh only; structure unchanged |
|
||||
|
||||
---
|
||||
|
||||
## Recommended Project Structure
|
||||
|
||||
The existing structure is well-organized. The overhaul adds a `dashboard/` subfolder and a `shared/` subfolder under components — no reorganization of hooks or lib.
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── ui/ # shadcn primitives (do not modify)
|
||||
│ │ └── collapsible.tsx # ADD — Radix Collapsible primitive
|
||||
│ ├── dashboard/ # ADD — dashboard-specific view components
|
||||
│ │ ├── DashboardContent.tsx # hybrid layout orchestrator
|
||||
│ │ ├── SummaryStrip.tsx # KPI cards row
|
||||
│ │ ├── StatCard.tsx # single KPI card
|
||||
│ │ ├── ChartPanel.tsx # chart grid container
|
||||
│ │ ├── IncomeBarChart.tsx # budgeted vs actual income bar
|
||||
│ │ ├── ExpenseDonutChart.tsx # donut + legend
|
||||
│ │ ├── SpendBarChart.tsx # horizontal budget vs actual
|
||||
│ │ ├── CategorySection.tsx # collapsible category group
|
||||
│ │ └── BudgetLineItems.tsx # line-item table inside section
|
||||
│ ├── shared/ # ADD — cross-page reusable components
|
||||
│ │ └── PageShell.tsx # consistent page header + CTA slot
|
||||
│ ├── AppLayout.tsx # MODIFY — visual refresh only
|
||||
│ └── QuickAddPicker.tsx # unchanged
|
||||
├── pages/ # MODIFY — swap DashboardContent import; apply PageShell
|
||||
│ ├── DashboardPage.tsx
|
||||
│ ├── BudgetDetailPage.tsx
|
||||
│ ├── BudgetListPage.tsx
|
||||
│ ├── CategoriesPage.tsx
|
||||
│ ├── TemplatePage.tsx
|
||||
│ ├── QuickAddPage.tsx
|
||||
│ ├── SettingsPage.tsx
|
||||
│ ├── LoginPage.tsx
|
||||
│ └── RegisterPage.tsx
|
||||
├── hooks/ # UNCHANGED
|
||||
├── lib/
|
||||
│ ├── palette.ts # UNCHANGED — CSS vars already defined
|
||||
│ └── ... # everything else unchanged
|
||||
├── i18n/
|
||||
│ ├── en.json # ADD new translation keys
|
||||
│ └── de.json # ADD new translation keys
|
||||
└── index.css # ADD semantic color tokens if needed
|
||||
```
|
||||
|
||||
### Structure Rationale
|
||||
|
||||
- **`components/dashboard/`:** All dashboard-specific view components are co-located. They have no meaning outside the dashboard, so they do not belong in `shared/`. Avoids polluting the top-level components directory.
|
||||
- **`components/shared/`:** `PageShell` is the one genuinely cross-page component introduced by this milestone. Keeping it separate signals that it is intentionally reusable, not page-specific.
|
||||
- **`components/ui/collapsible.tsx`:** The Radix Collapsible primitive is not yet in the project (inspected file list confirms absence). It must be added via `npx shadcn@latest add collapsible` before building `CategorySection`.
|
||||
|
||||
---
|
||||
|
||||
## Architectural Patterns
|
||||
|
||||
### Pattern 1: Derived Data via `useMemo` in DashboardContent
|
||||
|
||||
**What:** All computed values — category group totals, chart data arrays, KPI numbers — are derived in one place (`DashboardContent`) using `useMemo`, then passed as plain props to presentational child components. Child components never call hooks or perform calculations themselves.
|
||||
|
||||
**When to use:** Any time a value depends on `items` array from `useBudgetDetail`. Centralizing derivation means one cache invalidation (after a budget item update) triggers one recalculation, and all children rerender from the same consistent snapshot.
|
||||
|
||||
**Trade-offs:** Slightly more props-passing verbosity. Benefit: children are trivially testable pure components.
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// DashboardContent.tsx
|
||||
const { budget, items } = useBudgetDetail(budgetId)
|
||||
|
||||
const totals = useMemo(() => {
|
||||
const income = items
|
||||
.filter(i => i.category?.type === "income")
|
||||
.reduce((sum, i) => sum + i.actual_amount, 0)
|
||||
const expenses = items
|
||||
.filter(i => i.category?.type !== "income")
|
||||
.reduce((sum, i) => sum + i.actual_amount, 0)
|
||||
return { income, expenses, balance: income - expenses + (budget?.carryover_amount ?? 0) }
|
||||
}, [items, budget?.carryover_amount])
|
||||
|
||||
const groupedItems = useMemo(() =>
|
||||
CATEGORY_TYPES.map(type => ({
|
||||
type,
|
||||
items: items.filter(i => i.category?.type === type),
|
||||
budgeted: items.filter(i => i.category?.type === type).reduce((s, i) => s + i.budgeted_amount, 0),
|
||||
actual: items.filter(i => i.category?.type === type).reduce((s, i) => s + i.actual_amount, 0),
|
||||
})).filter(g => g.items.length > 0),
|
||||
[items])
|
||||
|
||||
// Pass totals and groupedItems as props; child components are pure
|
||||
```
|
||||
|
||||
### Pattern 2: Collapsible Category Sections via Radix Collapsible
|
||||
|
||||
**What:** Each category group (income, bills, variable expenses, debt, savings, investment) is wrapped in a `Collapsible.Root`. The always-visible trigger row shows the category label, color dot, and group-level budget/actual/difference totals. The collapsible content reveals the individual line-item table.
|
||||
|
||||
**When to use:** The dashboard hybrid view — users need the summary at a glance without scrolling through every line item. Opening a section is an explicit drill-down action.
|
||||
|
||||
**Trade-offs:** Adds `open` state per section. Use `useState` per section (not global state — there are at most 6 sections). Do not persist open state in localStorage for v1; the sections should open fresh on each visit so the summary view is the default.
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// CategorySection.tsx
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger }
|
||||
from "@/components/ui/collapsible"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
interface CategorySectionProps {
|
||||
type: CategoryType
|
||||
budgeted: number
|
||||
actual: number
|
||||
items: BudgetItem[]
|
||||
currency: string
|
||||
}
|
||||
|
||||
export function CategorySection({ type, budgeted, actual, items, currency }: CategorySectionProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button className="flex w-full items-center gap-3 rounded-lg border px-4 py-3 hover:bg-muted/40">
|
||||
<span className="size-3 shrink-0 rounded-full"
|
||||
style={{ backgroundColor: categoryColors[type] }} />
|
||||
<span className="font-medium">{t(`categories.types.${type}`)}</span>
|
||||
<span className="ml-auto tabular-nums text-sm text-muted-foreground">
|
||||
{formatCurrency(actual, currency)} / {formatCurrency(budgeted, currency)}
|
||||
</span>
|
||||
<ChevronDown className={`size-4 transition-transform ${open ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<BudgetLineItems items={items} currency={currency} type={type} />
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: shadcn ChartContainer + ChartConfig for All Charts
|
||||
|
||||
**What:** Wrap every Recharts chart in shadcn's `ChartContainer` component. Define colors and labels in a `ChartConfig` object that references existing CSS variable tokens from `index.css` (`var(--color-income)`, `var(--color-bill)`, etc.). Do not hardcode hex values inside chart components.
|
||||
|
||||
**When to use:** All three chart types (bar, horizontal bar, donut). This ensures charts automatically theme with the design system and dark mode works at zero extra cost.
|
||||
|
||||
**Trade-offs:** Requires adding the shadcn `chart` component (`npx shadcn@latest add chart`). Minor wrapper overhead, but the CSS variable binding and tooltip consistency is worth it.
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// IncomeBarChart.tsx
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent }
|
||||
from "@/components/ui/chart"
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid } from "recharts"
|
||||
|
||||
const chartConfig = {
|
||||
budgeted: { label: "Budgeted", color: "var(--color-income)" },
|
||||
actual: { label: "Actual", color: "var(--color-income)" },
|
||||
} satisfies ChartConfig
|
||||
|
||||
// data: [{ month: "March", budgeted: 3000, actual: 2850 }]
|
||||
export function IncomeBarChart({ data, currency }: Props) {
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
|
||||
<BarChart data={data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis dataKey="month" />
|
||||
<YAxis tickFormatter={v => formatCurrency(v, currency)} />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="budgeted" fill="var(--color-income)" radius={4} />
|
||||
<Bar dataKey="actual" fill="var(--color-income)" fillOpacity={0.6} radius={4} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: PageShell for Consistent Page Headers
|
||||
|
||||
**What:** A `PageShell` component accepts `title`, `description` (optional), and `action` (optional ReactNode slot for a primary CTA button). Every page wraps its top section in `PageShell`. This enforces a consistent heading size, spacing, and CTA placement across the entire app refresh.
|
||||
|
||||
**When to use:** All 9 pages in the overhaul. Any new page added in future milestones should also use it.
|
||||
|
||||
**Trade-offs:** Adds one wrapper per page. The benefit is that a single visual change to the page header propagates everywhere without hunting through 9 files.
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// shared/PageShell.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="text-sm text-muted-foreground mt-1">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
{action && <div className="shrink-0">{action}</div>}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Dashboard Read Flow
|
||||
|
||||
```
|
||||
DashboardPage renders
|
||||
↓
|
||||
useBudgets() → finds current month budget → passes budgetId prop
|
||||
↓
|
||||
DashboardContent mounts
|
||||
↓
|
||||
useBudgetDetail(budgetId) → TanStack Query cache or Supabase fetch
|
||||
↓ data arrives
|
||||
useMemo recalculates: totals, groupedItems, chartData
|
||||
↓
|
||||
Props flow DOWN to pure presentational children:
|
||||
SummaryStrip(totals)
|
||||
ChartPanel(chartData, currency)
|
||||
├── IncomeBarChart(barData)
|
||||
├── ExpenseDonutChart(pieData)
|
||||
└── SpendBarChart(horizontalData)
|
||||
CategorySection[] (groupedItems, per-type items)
|
||||
└── BudgetLineItems(items, currency)
|
||||
```
|
||||
|
||||
### Budget Item Edit Flow (unchanged, flows back up)
|
||||
|
||||
```
|
||||
InlineEditCell: user types new actual_amount
|
||||
↓
|
||||
onCommit → updateItem.mutateAsync({ id, budgetId, actual_amount })
|
||||
↓
|
||||
Supabase updates budget_items row
|
||||
↓
|
||||
onSuccess: queryClient.invalidateQueries(["budgets", budgetId, "items"])
|
||||
↓
|
||||
useBudgetDetail re-fetches items
|
||||
↓
|
||||
DashboardContent useMemo recalculates all derived values
|
||||
↓
|
||||
ALL children rerender with consistent new data
|
||||
```
|
||||
|
||||
### State Management (what lives where)
|
||||
|
||||
| State | Location | Why |
|
||||
|-------|----------|-----|
|
||||
| Budget and items data | TanStack Query cache | Server state, must survive component unmounts |
|
||||
| Collapsible open/closed | `useState` in each `CategorySection` | Purely local UI state; 6 booleans maximum |
|
||||
| Chart tooltip hover | Recharts internal | Library-managed interaction state |
|
||||
| Dialog open/closed | `useState` in page components | Unchanged from current pattern |
|
||||
| Currency, locale | `Profile` via Supabase → hooks | Read from `budget.currency`; no separate state |
|
||||
|
||||
---
|
||||
|
||||
## Scaling Considerations
|
||||
|
||||
This is a personal finance app for a single authenticated user at a time. Scale is not a concern for rendering. The relevant concern is **perceived performance** on the dashboard when `items` is large (100+ line items).
|
||||
|
||||
| Scale | Architecture Adjustment |
|
||||
|-------|--------------------------|
|
||||
| <50 items (normal) | No optimization needed — current useMemo pattern is fast |
|
||||
| 50-200 items | useMemo already handles this — O(n) passes are negligible |
|
||||
| 200+ items | Consider React.memo on CategorySection to skip unchanged sections; still no virtualization needed |
|
||||
| Dashboard load time | TanStack Query 5-min staleTime means instant rerender on navigate-back; no change required |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Anti-Pattern 1: Deriving Chart Data Inside Chart Components
|
||||
|
||||
**What people do:** Put `items.filter(...).reduce(...)` directly inside `IncomeBarChart` or `ExpenseDonutChart`, passing the raw `items` array from `useBudgetDetail` as a prop.
|
||||
|
||||
**Why it's wrong:** Each chart component recalculates from scratch. If `items` reference changes (after a mutation), all three charts recalculate independently. Chart components become impure, harder to test, and cannot be reused with different data shapes without changing their internals.
|
||||
|
||||
**Do this instead:** Derive all chart data in `DashboardContent` with `useMemo`. Pass prepared `barData`, `pieData`, `horizontalData` arrays to each chart. Charts receive typed data arrays and render only.
|
||||
|
||||
---
|
||||
|
||||
### Anti-Pattern 2: Hardcoding Colors Inside Chart Components
|
||||
|
||||
**What people do:** Paste hex values like `fill="#4ade80"` into `<Bar>` and `<Cell>` components to match the design.
|
||||
|
||||
**Why it's wrong:** The existing `index.css` already defines category colors as OKLCH CSS variables (`--color-income`, `--color-bill`, etc.) and `palette.ts` maps them to `var(--color-income)` etc. Hardcoding breaks dark mode adaptation, creates a second source of truth, and means a palette change requires editing multiple files.
|
||||
|
||||
**Do this instead:** Always reference `categoryColors[type]` from `palette.ts` (which returns the CSS variable string) for both chart fills and UI color dots. For shadcn `ChartContainer`, pass `color: categoryColors[type]` in the `ChartConfig` object.
|
||||
|
||||
---
|
||||
|
||||
### Anti-Pattern 3: One Monolithic DashboardContent Component
|
||||
|
||||
**What people do:** Add all new dashboard sections — summary cards, three charts, six collapsible sections, QuickAdd button — directly into one large `DashboardContent.tsx` that becomes 400+ lines.
|
||||
|
||||
**Why it's wrong:** The existing `DashboardContent` is already 200 lines with just two charts and progress bars. A full hybrid dashboard with three chart types and six collapsible sections will exceed 600 lines inline, making it impossible to review, test, or modify individual sections without reading the whole file.
|
||||
|
||||
**Do this instead:** Extract into the component tree defined above. `DashboardContent` orchestrates layout and owns derived data. Each distinct visual section (`SummaryStrip`, `ChartPanel`, `CategorySection`) is its own file. The rule: if a block of JSX has a distinct visual purpose, it gets its own component.
|
||||
|
||||
---
|
||||
|
||||
### Anti-Pattern 4: Using shadcn Accordion Instead of Collapsible for Category Sections
|
||||
|
||||
**What people do:** Reach for the `Accordion` component (which is already in some shadcn setups) to build collapsible category sections because it looks similar.
|
||||
|
||||
**Why it's wrong:** Accordion by default enforces "only one open at a time" (type="single") or requires explicit `type="multiple"` with `collapsible` prop to allow free open/close. For budget sections, users may want to compare two categories side-by-side with both open simultaneously. Using individual `Collapsible` per section gives full independent control without fighting Accordion's root-state coordination.
|
||||
|
||||
**Do this instead:** Use Radix `Collapsible` (shadcn wrapper) on each `CategorySection` with independent `useState`. Six independent booleans are trivially managed.
|
||||
|
||||
---
|
||||
|
||||
### Anti-Pattern 5: Modifying Hooks or Supabase Queries
|
||||
|
||||
**What people do:** Add derived fields or aggregations to the `useBudgetDetail` return value, or add new Supabase query fields, because it seems convenient during the UI overhaul.
|
||||
|
||||
**Why it's wrong:** Explicitly out of scope (PROJECT.md: "No Supabase schema changes — UI-only modifications"). Any hook changes risk breaking `BudgetDetailPage` and other consumers. The data model is sufficient — all needed aggregations can be computed with `useMemo` from the existing `items` array.
|
||||
|
||||
**Do this instead:** Keep hooks read-only. All computation lives in the presentation layer via `useMemo`.
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### External Services
|
||||
|
||||
| Service | Integration | Notes |
|
||||
|---------|-------------|-------|
|
||||
| Supabase | Unchanged — hooks layer handles all DB calls | No new RPC calls, no schema changes |
|
||||
| Recharts | Chart primitives — wrap with `ChartContainer` | Requires adding shadcn `chart` component |
|
||||
| Radix UI | `Collapsible` primitive — add via shadcn CLI | Not yet in project; must be added before CategorySection work |
|
||||
| i18next | All new UI text needs keys in `en.json` and `de.json` | Add keys before rendering any new text; no runtime fallback acceptable |
|
||||
|
||||
### Internal Boundaries
|
||||
|
||||
| Boundary | Communication | Notes |
|
||||
|----------|---------------|-------|
|
||||
| `DashboardContent` ↔ chart components | Props only — typed data arrays + currency string | Charts are pure; they do not call hooks |
|
||||
| `DashboardContent` ↔ `CategorySection` | Props only — grouped items, budgeted/actual totals, currency, mutation handlers | Mutation handlers passed down from `DashboardContent` which owns `useBudgets()` mutations |
|
||||
| `CategorySection` ↔ `BudgetLineItems` | Props only — items array, currency, type | `BudgetLineItems` is a thin table wrapper; all mutations are callbacks from parent |
|
||||
| `PageShell` ↔ all pages | Props (title, action slot, children) | No state shared; purely compositional |
|
||||
| `index.css` @theme tokens ↔ components | CSS variables via `var(--color-X)` and `palette.ts` | Single source of truth for all color; never duplicate in component style props |
|
||||
|
||||
---
|
||||
|
||||
## Build Order Implications
|
||||
|
||||
Components have dependencies that dictate implementation order:
|
||||
|
||||
1. **`components/ui/collapsible.tsx`** — Add via `npx shadcn@latest add collapsible`. Required by `CategorySection`. Do first.
|
||||
2. **`components/ui/chart.tsx`** — Add via `npx shadcn@latest add chart`. Required by all chart components. Do first.
|
||||
3. **`shared/PageShell.tsx`** — No dependencies. Build early; apply to all pages as each is refreshed.
|
||||
4. **`StatCard` + `SummaryStrip`** — Only depends on `formatCurrency` and Tailwind. Build second after primitives.
|
||||
5. **Chart components** (`IncomeBarChart`, `ExpenseDonutChart`, `SpendBarChart`) — Depend on `ChartContainer`. Build after step 2.
|
||||
6. **`ChartPanel`** — Composes the three chart components. Build after step 5.
|
||||
7. **`BudgetLineItems`** — Refactored from existing `BudgetDetailPage` table code; existing `InlineEditCell` and `DifferenceCell` are reusable as-is.
|
||||
8. **`CategorySection`** — Depends on `Collapsible` primitive and `BudgetLineItems`. Build after steps 1 and 7.
|
||||
9. **`DashboardContent`** — Orchestrates everything. Build last, wiring together all children. Replace the existing `DashboardContent` function in `DashboardPage.tsx`.
|
||||
10. **Non-dashboard page refreshes** — Apply `PageShell` + visual refresh to remaining 7 pages. Independent of dashboard work; can be done in any order.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- Radix UI Collapsible: https://www.radix-ui.com/primitives/docs/components/collapsible (HIGH confidence — official docs)
|
||||
- shadcn/ui Chart component: https://ui.shadcn.com/docs/components/radix/chart (HIGH confidence — official docs)
|
||||
- shadcn/ui Accordion: https://ui.shadcn.com/docs/components/radix/accordion (HIGH confidence — official docs)
|
||||
- Tailwind CSS v4 @theme tokens: https://tailwindcss.com/docs/theme (HIGH confidence — official docs)
|
||||
- React useMemo: https://react.dev/reference/react/useMemo (HIGH confidence — official docs)
|
||||
- Existing codebase: inspected `src/` fully — `DashboardPage.tsx`, `BudgetDetailPage.tsx`, `CategoriesPage.tsx`, `AppLayout.tsx`, `palette.ts`, `types.ts`, `index.css`, `useBudgets.ts` (HIGH confidence — primary source)
|
||||
|
||||
---
|
||||
|
||||
*Architecture research for: SimpleFinanceDash UI overhaul — React + Tailwind + shadcn/ui + Recharts*
|
||||
*Researched: 2026-03-16*
|
||||
244
.planning/research/FEATURES.md
Normal file
244
.planning/research/FEATURES.md
Normal file
@@ -0,0 +1,244 @@
|
||||
# Feature Research
|
||||
|
||||
**Domain:** Personal finance budget dashboard (UI presentation layer overhaul)
|
||||
**Researched:** 2026-03-16
|
||||
**Confidence:** HIGH — grounded in competitor analysis (YNAB, Empower), industry design guides, and direct codebase inspection
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
This research covers the **presentation layer** only. Backend schema and data model are frozen. The question is: what UI/UX features make a personal finance dashboard feel polished rather than basic, and which of those apply to this project's existing data model (income, bills, variable expenses, debt, savings, investments — budgeted vs actual)?
|
||||
|
||||
The existing stack is React 19 + Tailwind CSS 4 + Recharts + shadcn/ui. All chart capabilities already exist in Recharts; this research identifies how to use them well.
|
||||
|
||||
---
|
||||
|
||||
## Feature Landscape
|
||||
|
||||
### Table Stakes (Users Expect These)
|
||||
|
||||
Features users assume exist. Missing these = product feels incomplete.
|
||||
|
||||
| Feature | Why Expected | Complexity | Notes |
|
||||
|---------|--------------|------------|-------|
|
||||
| Summary KPI cards at the top | Every finance dashboard leads with top-line numbers (income, total spend, balance). Users scan these first — it's the entry point to understanding the month. | LOW | Already exists but needs richer visual treatment: larger numbers, colored icons, clearer hierarchy |
|
||||
| Green/red semantic color coding for over/under budget | Finance convention: green = on track, red = over budget. Violating this creates cognitive friction. | LOW | Already exists on progress bars and difference cells; must be consistent across all new chart types |
|
||||
| Donut/pie chart for expense category breakdown | Standard visualization for "how is spending distributed." Users expect to see the breakdown at a glance. | LOW | Existing pie is plain; needs inner radius (donut), center total label, and richer color fills |
|
||||
| Budget vs actual per category | The core comparison every budget tool offers. Users come here specifically to compare what they planned to what they actually spent. | MEDIUM | Existing progress bars satisfy this minimally; new grouped/horizontal bar charts will fulfill it properly |
|
||||
| Inline value editing on budget detail | Users need to log actuals inline — no one wants a separate form for every line item. | LOW | Already implemented in BudgetDetailPage as InlineEditCell; keep and refine |
|
||||
| Loading state that reflects page structure | If the page renders blank or flashes, users distrust the data or think it's broken. | LOW | Currently returns `null` during load — skeleton cards matching the final layout are expected |
|
||||
| Empty state with actionable guidance | A blank dashboard for a new month should tell the user what to do next, not just show nothing. | LOW | Currently shows plain muted text; needs a CTA card pattern |
|
||||
| Tabular-numeral formatting throughout | Financial amounts must use tabular (monospaced number) alignment so columns read correctly. | LOW | Already applied via `tabular-nums`; extend to all new components |
|
||||
| Color-coded category identity | Users build a mental map: bills are orange, savings are blue. Consistent color per category type is required everywhere — charts, tables, badges. | LOW | Palette already defined in `index.css` and `palette.ts`; extend consistently to new components |
|
||||
| Collapsible / grouped sections per category type | Budget detail and dashboard both group items by type. Users expect to expand/collapse groups, especially with many line items. | MEDIUM | BudgetDetailPage already groups by type; dashboard needs collapsible inline sections as new feature |
|
||||
| Month navigation on the dashboard | Users want to check a prior month without navigating away. "Looking at last month" is a top-3 use case for any budget tool. | MEDIUM | Not in current dashboard (auto-resolves current month only); needs month selector |
|
||||
| Totals row / footer per section | Standard pattern from spreadsheets. Each category group needs a sub-total visible without counting rows. | LOW | Already in BudgetDetailPage table footer; needs equivalent on dashboard sections |
|
||||
| Consistent design language across all pages | Auth pages, settings, categories, and budget list must feel like the same product as the dashboard. Inconsistency signals amateur work. | HIGH | Currently only dashboard is styled; all other pages need the same card/color/typography treatment |
|
||||
|
||||
### Differentiators (Competitive Advantage)
|
||||
|
||||
Features that set the product apart. Not required, but valued.
|
||||
|
||||
| Feature | Value Proposition | Complexity | Notes |
|
||||
|---------|-------------------|------------|-------|
|
||||
| Grouped bar chart: income budgeted vs actual | Directly answers "Did I earn what I expected this month?" in a single visual. Most basic dashboards skip this, showing only expense charts. | MEDIUM | Recharts `BarChart` with two `Bar` components (budgeted / actual); use category colors |
|
||||
| Horizontal bar chart: spend by category type (budget vs actual) | Lets users scan over-budget categories at a glance. Horizontal orientation makes labels readable without rotation. | MEDIUM | Recharts `BarChart layout="vertical"` with `XAxis type="number"` and `YAxis type="category"` |
|
||||
| Donut chart with center total label | Center label turns the chart from decorative into informational — shows total spend while the ring shows distribution. YNAB and Empower both do this. | LOW | Recharts `Label` component inside `Pie` using `viewBox` coordinates for positioned text |
|
||||
| Active sector hover highlight on donut | Hovering a slice expands it slightly and shows the category name + amount in a tooltip. Feels interactive and polished vs a static chart. | LOW | Recharts `activeShape` with expanded `outerRadius` — supported natively |
|
||||
| Variance indicator (delta arrows) | Show "▲ 12% over" or "▼ 5% under" beside actual amounts on summary cards and section headers. Transforms raw numbers into directional insight. | LOW | Computed from budgeted/actual; render as small colored badge or arrow icon |
|
||||
| Accent-colored card borders / icon containers | Rich visual style: each category section gets its palette color as a left border accent or icon container fill. Breaks the sea-of-grey-cards look. | LOW | Tailwind `border-l-4` or `bg-[color]/10` container with category CSS variable |
|
||||
| Section collapse persisted in localStorage | Remembering which sections are open avoids users re-expanding the same sections every visit. Small touch that signals quality. | LOW | Simple `useState` + `localStorage` read/write; wrap in a `useCollapse` hook |
|
||||
| Carryover amount visible on dashboard | The budget model already tracks `carryover_amount`. Showing it alongside the available balance makes the "running total" story clear. | LOW | Surface as a fourth summary card or sub-label on the balance card |
|
||||
| Skeleton loading that mirrors the real layout | Animated skeleton cards/charts during load feel like the app is fast and predictable. Blank screens feel broken. | LOW | shadcn/ui `Skeleton` component already available; wrap summary cards and chart containers |
|
||||
| Page header with budget month name | Every page in the app should show context: "March 2026 Budget." Users confirm they're looking at the right period immediately. | LOW | Format `budget.start_date` as "Month YYYY" in the page heading |
|
||||
|
||||
### Anti-Features (Commonly Requested, Often Problematic)
|
||||
|
||||
Features that seem good but create problems in this context.
|
||||
|
||||
| Feature | Why Requested | Why Problematic | Alternative |
|
||||
|---------|---------------|-----------------|-------------|
|
||||
| Trend / multi-month charts | Users always ask "can I see my spending over 6 months?" | Requires multi-budget data aggregation, a new query shape, and a new chart layout — out of scope for a UI-only overhaul. Adding it half-baked is worse than not adding it. | Explicitly defer to a future milestone. Put a placeholder card with "Coming soon" if needed. |
|
||||
| Dark mode toggle | Professional apps always have a dark mode. | Tailwind CSS 4 dark mode via class strategy is possible, but requires auditing every component's contrast, chart colors, and skeleton states. Doing it properly doubles testing surface. The OKLCH color system is set up with dark mode in mind, but no dark palette is currently defined. | Define as a future phase deliverable. Keep the existing light-only approach consistent and polished first. |
|
||||
| Real-time sync / live updates | "What if two tabs are open?" | No backend changes allowed this milestone. Supabase realtime would require schema changes and subscription setup. The current TanStack Query polling pattern is sufficient. | Rely on query invalidation on mutation — already in place. |
|
||||
| Drag-to-reorder line items | Power users want custom sort order | Category `sort_order` field exists, but updating it requires PATCH calls and animation logic. No backend changes in scope. | Keep current `sort_order` from DB; do not expose drag handles this milestone. |
|
||||
| Glassmorphism / heavy blur effects | Trendy, visually striking | `backdrop-blur` on many elements is GPU-heavy and can degrade performance on lower-end machines. Also risks reducing text readability on charts. Misapplied, it obscures data rather than enhancing it. | Use solid cards with subtle colored-border accents instead. Reserve `backdrop-blur` for a single hero element if at all. |
|
||||
| AI / natural language budget insights | Feels modern, seen in apps like Copilot | Requires an AI backend, a new API integration, and user data being sent to a third-party service — none of which are part of a UI-only overhaul. | Surface static insights from computed data instead: "You spent 15% more on bills than budgeted this month." Computed in the frontend, no AI needed. |
|
||||
| Infinite scroll on budget list | "I have many months of budgets" | Adds complexity to the budget list query and requires scroll position management. The budget list is unlikely to have more than 24 entries in a year of use. | Standard paginated or full list with a search/filter input is sufficient. |
|
||||
|
||||
---
|
||||
|
||||
## Feature Dependencies
|
||||
|
||||
```
|
||||
Donut Chart with Center Label
|
||||
└──requires──> Category color system (already exists)
|
||||
└──requires──> Recharts Label component in Pie (Recharts native)
|
||||
|
||||
Horizontal Bar Chart (budget vs actual by type)
|
||||
└──requires──> Category color system (already exists)
|
||||
└──requires──> Grouped data by category type (already in DashboardContent)
|
||||
|
||||
Grouped Bar Chart (income budget vs actual)
|
||||
└──requires──> Income items filtered from BudgetItems (already in DashboardContent)
|
||||
|
||||
Collapsible Dashboard Sections
|
||||
└──requires──> Category group data (already derived)
|
||||
└──enhances──> Section collapse persistence (localStorage)
|
||||
└──enhances──> Totals row per section (already exists in BudgetDetailPage)
|
||||
|
||||
Skeleton Loading
|
||||
└──requires──> Final layout structure (must design layout first)
|
||||
└──depends-on──> Summary cards layout (build cards first, then wrap in Skeleton)
|
||||
|
||||
Month Navigation on Dashboard
|
||||
└──requires──> Budgets list query (already in useBudgets)
|
||||
└──conflicts──> Auto-resolve current month (replace with explicit selection)
|
||||
|
||||
Consistent Design Language (all pages)
|
||||
└──depends-on──> Dashboard redesign being finished first (establishes the design token/component pattern)
|
||||
└──applies-to──> LoginPage, RegisterPage, CategoriesPage, TemplatePage, BudgetListPage, BudgetDetailPage, QuickAddPage, SettingsPage
|
||||
|
||||
Variance Indicators (delta arrows)
|
||||
└──requires──> budgeted_amount and actual_amount both available (already on BudgetItem)
|
||||
└──enhances──> Summary cards (add % change sub-label)
|
||||
└──enhances──> Collapsible section headers (show group variance at a glance)
|
||||
```
|
||||
|
||||
### Dependency Notes
|
||||
|
||||
- **Skeleton loading requires final layout:** Don't implement skeleton states until the target card/chart layout is finalized — skeleton shape must mirror real content shape.
|
||||
- **All-pages redesign depends on dashboard:** The dashboard establishes the color system application patterns (accent borders, card styles, typography scale) that all other pages will inherit.
|
||||
- **Month navigation conflicts with auto-resolve:** The current pattern auto-finds the budget for the current calendar month. Adding a selector means the dashboard must hold local `selectedBudgetId` state rather than computing it.
|
||||
- **Donut center label requires Recharts Label:** shadcn/ui's chart system supports this natively via the `Label` component inside `Pie` using `viewBox` coordinates. No new library needed.
|
||||
|
||||
---
|
||||
|
||||
## MVP Definition
|
||||
|
||||
This is a UI overhaul milestone, not a greenfield product. "MVP" here means: what must ship for the redesign to feel complete and polished?
|
||||
|
||||
### Launch With (v1 — Dashboard Overhaul)
|
||||
|
||||
- [ ] Summary cards with richer visual treatment (larger numbers, semantic color on balance, variance badge) — foundation of the dashboard
|
||||
- [ ] Donut chart with center total label and active sector hover — replaces existing flat pie
|
||||
- [ ] Horizontal bar chart (budget vs actual by category type) — new chart, satisfies the key "am I on track" question
|
||||
- [ ] Grouped bar chart (income budget vs actual) — completes the three-chart suite from the reference
|
||||
- [ ] Collapsible inline sections per category group on dashboard (with line items + group totals) — replaces current progress bars
|
||||
- [ ] Skeleton loading states for cards and charts — removes the jarring blank-then-rendered experience
|
||||
- [ ] Month navigation control (budget period selector) on dashboard — without this the dashboard is locked to current month only
|
||||
|
||||
### Add After Dashboard Phase (v1.x — Full App Polish)
|
||||
|
||||
- [ ] Consistent card/color design applied to BudgetDetailPage — users navigate here from the dashboard; it must match
|
||||
- [ ] Consistent design applied to BudgetListPage — entry point from nav, feels disconnected from dashboard without polish
|
||||
- [ ] Consistent design applied to CategoriesPage and TemplatePage — setup pages; lower urgency but visible gap
|
||||
- [ ] Consistent design applied to LoginPage and RegisterPage — first impression; polish matters
|
||||
- [ ] Consistent design applied to QuickAddPage and SettingsPage — utility pages; can ship slightly after core flows
|
||||
- [ ] Section collapse state persisted to localStorage — nice-to-have UX polish
|
||||
|
||||
### Future Consideration (v2+)
|
||||
|
||||
- [ ] Trend charts over multiple months — deferred explicitly in PROJECT.md out-of-scope
|
||||
- [ ] Dark mode — foundational work (OKLCH variables) exists but needs full audit and dark palette definition
|
||||
- [ ] AI-derived spending insights — requires backend changes; no scope here
|
||||
- [ ] Drag-to-reorder categories — requires sort_order mutation support
|
||||
|
||||
---
|
||||
|
||||
## Feature Prioritization Matrix
|
||||
|
||||
| Feature | User Value | Implementation Cost | Priority |
|
||||
|---------|------------|---------------------|----------|
|
||||
| Summary cards — richer visual treatment | HIGH | LOW | P1 |
|
||||
| Donut chart with center label + active hover | HIGH | LOW | P1 |
|
||||
| Horizontal bar chart (spend vs budget by type) | HIGH | MEDIUM | P1 |
|
||||
| Grouped bar chart (income budget vs actual) | HIGH | MEDIUM | P1 |
|
||||
| Collapsible dashboard sections with line items | HIGH | MEDIUM | P1 |
|
||||
| Skeleton loading states | MEDIUM | LOW | P1 |
|
||||
| Month navigation on dashboard | HIGH | MEDIUM | P1 |
|
||||
| Consistent design on BudgetDetailPage | HIGH | MEDIUM | P1 |
|
||||
| Consistent design on BudgetListPage | MEDIUM | LOW | P2 |
|
||||
| Variance indicators (delta arrows/badges) | MEDIUM | LOW | P2 |
|
||||
| Accent-colored category section borders | MEDIUM | LOW | P2 |
|
||||
| Carryover amount visible on dashboard | MEDIUM | LOW | P2 |
|
||||
| Consistent design on auth/settings/utility pages | LOW | MEDIUM | P2 |
|
||||
| Section collapse persisted in localStorage | LOW | LOW | P3 |
|
||||
| Empty state with action guidance | MEDIUM | LOW | P2 |
|
||||
|
||||
**Priority key:**
|
||||
- P1: Must have for the overhaul to feel complete
|
||||
- P2: Should have; include in same milestone if effort allows
|
||||
- P3: Nice to have; add as polish after core delivery
|
||||
|
||||
---
|
||||
|
||||
## Competitor Feature Analysis
|
||||
|
||||
| Feature | YNAB | Empower (formerly Personal Capital) | Our Approach |
|
||||
|---------|------|--------------------------------------|--------------|
|
||||
| Summary KPI cards | Yes — "Available" balance prominent | Yes — net worth headline | 3 cards: Income / Expenses / Balance |
|
||||
| Budget vs actual bars | Yes — horizontal category bars with color (green/yellow/red) | Cash flow bars | Horizontal bar chart per category type |
|
||||
| Donut / pie chart | No (YNAB uses different visualization) | Yes — allocation donut for investments | Donut with center total, colored by category type |
|
||||
| Collapsible grouped sections | Yes — master categories expand to show sub-categories | No — flat list | Collapsible per category type (income, bills, etc.) |
|
||||
| Inline editing | Yes — click amount to edit | No | Keep existing InlineEditCell pattern, refine styling |
|
||||
| Color-coded categories | Yes — status colors (green/yellow/red) | Category colors for accounts | Per-type semantic color (income=green, debt=red/pink, etc.) |
|
||||
| Month/period navigation | Yes — budget period selector in sidebar | Yes — date range selector | Month selector on dashboard |
|
||||
| Skeleton loading | Yes — YNAB shows skeleton on load | Yes | Skeleton cards + chart placeholders |
|
||||
| Variance / delta indicators | Yes — shows "over by $X" inline | Yes — shows gain/loss % | Variance badge on summary cards and section headers |
|
||||
|
||||
---
|
||||
|
||||
## Chart Design Notes (Recharts-Specific)
|
||||
|
||||
These translate research findings into concrete implementation guidance for the Recharts stack.
|
||||
|
||||
**Donut chart:**
|
||||
- Use `innerRadius={60}` to `innerRadius={72}` for a modern ring look (not too thin)
|
||||
- Place center total using `<Label>` with `content` prop inside `<Pie>` using `viewBox` cx/cy coordinates
|
||||
- Use `activeShape` with `outerRadius + 10` for hover expansion
|
||||
- Add `isAnimationActive` to respect `prefers-reduced-motion`
|
||||
- Legend as a separate `<ul>` below the chart (not Recharts built-in legend) for full styling control
|
||||
|
||||
**Horizontal bar chart (budget vs actual by type):**
|
||||
- `<BarChart layout="vertical">` with `<XAxis type="number">` and `<YAxis type="category">`
|
||||
- Two `<Bar>` components — one for budgeted (muted/secondary color) and one for actual (category color)
|
||||
- Set `barSize` to keep bars compact; use `barCategoryGap` for breathing room
|
||||
- `<LabelList position="right">` for actual amounts visible on bar ends
|
||||
- Never start X axis at a non-zero value
|
||||
|
||||
**Grouped bar chart (income budget vs actual):**
|
||||
- Standard `<BarChart>` with `layout="horizontal"` (default)
|
||||
- Two `<Bar>` groups: budgeted (muted fill) and actual (category color)
|
||||
- `<CartesianGrid vertical={false}>` to reduce visual noise
|
||||
- Custom `<ChartTooltip>` showing both values with currency formatting
|
||||
|
||||
**Color system:**
|
||||
- All charts must consume CSS variables from the existing OKLCH palette (`var(--color-income)`, etc.)
|
||||
- Never hardcode hex values in chart components — use the `categoryColors` map from `palette.ts`
|
||||
- Green/red semantic colors for over/under-budget states must be distinct from category colors
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- [Fintech design guide with patterns that build trust — Eleken (2026)](https://www.eleken.co/blog-posts/modern-fintech-design-guide)
|
||||
- [Finance Dashboard Design Best Practices — F9 Finance](https://www.f9finance.com/dashboard-design-best-practices/)
|
||||
- [Budget vs Actual Dashboard — Bold BI](https://www.boldbi.com/dashboard-examples/finance/budget-vs-actual-dashboard/)
|
||||
- [7 Essential Financial Charts for Personal Finance Visualization — Syncfusion](https://www.syncfusion.com/blogs/post/financial-charts-visualization)
|
||||
- [Fintech dashboard design — Merge Rocks](https://merge.rocks/blog/fintech-dashboard-design-or-how-to-make-data-look-pretty)
|
||||
- [Dashboard Design UX Patterns Best Practices — Pencil & Paper](https://www.pencilandpaper.io/articles/ux-pattern-analysis-data-dashboards)
|
||||
- [shadcn/ui Charts — Donut with center text, Bar charts](https://ui.shadcn.com/charts)
|
||||
- [Bar Charts Best Practices — Nastengraph / Medium](https://nastengraph.medium.com/bar-charts-best-practices-5e81ebc7b340)
|
||||
- [Best Color Palettes for Financial Dashboards — Phoenix Strategy Group](https://www.phoenixstrategy.group/blog/best-color-palettes-for-financial-dashboards)
|
||||
- [The Role of Color Theory in Finance Dashboard Design — Extej / Medium](https://medium.com/@extej/the-role-of-color-theory-in-finance-dashboard-design-d2942aec9fff)
|
||||
- [Skeleton loading screen design — LogRocket](https://blog.logrocket.com/ux-design/skeleton-loading-screen-design/)
|
||||
- [Empty state UX examples — Eleken](https://www.eleken.co/blog-posts/empty-state-ux)
|
||||
- [YNAB / Mint / Empower comparison — The State of Personal Finance Apps 2025](https://bountisphere.com/blog/personal-finance-apps-2025-review)
|
||||
- [Recharts documentation — recharts.org](https://recharts.github.io/en-US/api/Bar/)
|
||||
|
||||
---
|
||||
|
||||
*Feature research for: SimpleFinanceDash UI overhaul — presentation layer*
|
||||
*Researched: 2026-03-16*
|
||||
295
.planning/research/PITFALLS.md
Normal file
295
.planning/research/PITFALLS.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# Pitfalls Research
|
||||
|
||||
**Domain:** Personal finance dashboard UI overhaul (React SPA)
|
||||
**Researched:** 2026-03-16
|
||||
**Confidence:** HIGH (most findings verified against official docs, Recharts issues tracker, and Tailwind v4 official docs)
|
||||
|
||||
---
|
||||
|
||||
## Critical Pitfalls
|
||||
|
||||
### Pitfall 1: Recharts Re-renders Every Parent State Change
|
||||
|
||||
**What goes wrong:**
|
||||
The existing `DashboardPage.tsx` computes `pieData` and `progressGroups` directly in the render body — no `useMemo`. Every unrelated state change (e.g., a tooltip hover, a loading flag flip, QuickAddPicker opening) re-runs these array transforms and causes Recharts to diff its entire virtual DOM tree. Adding two more charts (bar chart, horizontal bar) multiplies this cost by 3x. With a large budget, each O(n) filter+reduce runs six times per render for each chart type.
|
||||
|
||||
**Why it happens:**
|
||||
The existing single-chart dashboard is fast enough that the missing memoization is invisible. During the overhaul, three chart instances are added to the same render tree, and the dashboard becomes visually complex enough that parent re-renders happen more frequently (collapsible state toggles, hover events on multiple chart tooltips).
|
||||
|
||||
**How to avoid:**
|
||||
- Wrap all chart data derivations in `useMemo` with explicit deps arrays:
|
||||
```tsx
|
||||
const pieData = useMemo(() =>
|
||||
EXPENSE_TYPES.map(type => { ... }).filter(d => d.value > 0),
|
||||
[items, t]
|
||||
)
|
||||
```
|
||||
- Wrap formatter callbacks passed to `<Tooltip formatter={...}>` in `useCallback`. New function references on every render force Recharts to re-render tooltip internals.
|
||||
- Extract each chart into its own memoized sub-component (`React.memo`) so only the chart whose data changed re-renders.
|
||||
- Create a category index Map once (in a hook or useMemo) and look up by key rather than `.filter()` per item across three charts.
|
||||
|
||||
**Warning signs:**
|
||||
- React DevTools Profiler shows `DashboardContent` re-rendering on tooltip mouseover
|
||||
- Multiple chart tooltips fighting each other (Recharts issues #281 / #1770)
|
||||
- "ResizeObserver loop limit exceeded" console errors when multiple `ResponsiveContainer` instances mount simultaneously
|
||||
|
||||
**Phase to address:** Dashboard redesign phase (charts implementation)
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 2: CSS Variable Scope — Recharts Cannot Read Tailwind `@theme inline` Variables
|
||||
|
||||
**What goes wrong:**
|
||||
The existing `palette.ts` already uses `var(--color-income)` etc. as chart fill values. This works correctly because Tailwind v4's `@theme inline` inlines these as true CSS custom properties on `:root`. However, the risk is subtle: if any chart color is set via a Tailwind utility class string (e.g., `fill-[var(...)]`) rather than through the `style` prop or a direct CSS variable reference, Recharts SVG elements — which render outside the standard DOM paint context — may not resolve them correctly in all browsers.
|
||||
|
||||
Additionally, dark mode: the current CSS has no dark-mode overrides for `--color-income` through `--color-investment`. If the overhaul adds a dark mode toggle, all chart colors will remain light-mode pastel on dark backgrounds, creating very poor contrast.
|
||||
|
||||
**Why it happens:**
|
||||
Tailwind v4's `@theme inline` block defines variables at `:root` scope, but the dark mode variant uses `.dark` class scoping. Theme variables defined only in `@theme inline` are not automatically duplicated under `.dark {}`. Dark-mode overrides for semantic colors (background, foreground) exist in shadcn's generated block, but category colors were custom-added without dark variants.
|
||||
|
||||
**How to avoid:**
|
||||
- Pass all Recharts color values via JavaScript (the `fill={categoryColors[type]}` pattern is correct — maintain it).
|
||||
- If dark mode is added in the overhaul: add a `.dark {}` block in `index.css` that overrides `--color-income`, `--color-bill`, etc. with darker/brighter variants appropriate for dark backgrounds.
|
||||
- Never attempt to pass Tailwind utility class strings as Recharts `fill` props. Recharts `Cell`, `Bar`, `Line` props require resolved color values (hex, oklch string, or `var()` reference).
|
||||
- Test chart colors in both light and dark modes before marking a phase complete.
|
||||
|
||||
**Warning signs:**
|
||||
- Chart fills show as `oklch(...)` literal text in DOM attributes instead of resolved colors
|
||||
- Colors are invisible or white-on-white on one theme variant
|
||||
- Browser DevTools shows SVG `fill` as unresolved `var(--color-income)` with no computed value
|
||||
|
||||
**Phase to address:** Design token / theming phase (early); dashboard charts phase (verification)
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 3: Collapsible Sections Causing Layout Shift and CLS Jank
|
||||
|
||||
**What goes wrong:**
|
||||
The planned hybrid dashboard includes collapsible inline sections for each category group (income, bills, variable expenses, debt, savings). If these are implemented by toggling `display: none` / `display: block` or by animating the `height` CSS property from `0` to `auto`, the result is either: (a) instant snap with no animation, or (b) jank where the browser triggers full layout recalculations on every animation frame. With five collapsible sections and charts above them, collapsing a section causes the charts to resize (their `ResponsiveContainer` detects parent width change), triggering a cascade of resize events.
|
||||
|
||||
**Why it happens:**
|
||||
Animating `height` from `0` to `auto` is a known browser limitation — you cannot CSS-transition to `height: auto`. Common naive workarounds (JavaScript measuring `scrollHeight` on every frame) cause layout thrashing. Radix UI's `Collapsible` component handles this correctly via CSS custom properties for height, but requires the `data-state` attribute pattern and the correct CSS transition on the inner `CollapsibleContent` element.
|
||||
|
||||
**How to avoid:**
|
||||
- Use Radix UI `Collapsible` (already available via shadcn) — it sets `--radix-collapsible-content-height` as a CSS variable during open/close, enabling smooth CSS-only transition:
|
||||
```css
|
||||
[data-state='open'] > .collapsible-content {
|
||||
animation: slideDown 200ms ease-out;
|
||||
}
|
||||
[data-state='closed'] > .collapsible-content {
|
||||
animation: slideUp 200ms ease-out;
|
||||
}
|
||||
@keyframes slideDown {
|
||||
from { height: 0 }
|
||||
to { height: var(--radix-collapsible-content-height) }
|
||||
}
|
||||
```
|
||||
- Add `debounce={50}` to all `ResponsiveContainer` instances to prevent rapid resize recalculations when parent containers animate.
|
||||
- Use `padding` instead of `margin` inside collapsible children — margin collapsing causes jump artifacts on some browsers.
|
||||
- Never animate `height`, `padding`, or `margin` directly with JavaScript setInterval; use CSS animations or Radix primitives.
|
||||
|
||||
**Warning signs:**
|
||||
- Charts visually "snap" to a narrower width and back when a section above them is toggled
|
||||
- Frame rate drops to under 30fps when expanding/collapsing sections (visible in DevTools Performance panel)
|
||||
- `ResizeObserver loop limit exceeded` errors spike after section toggle
|
||||
|
||||
**Phase to address:** Dashboard collapsible sections phase
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 4: Color Accessibility Failures in Financial Semantic Colors
|
||||
|
||||
**What goes wrong:**
|
||||
The most dangerous pattern in the existing dashboard is using pure green / red to signal positive / negative balance:
|
||||
```tsx
|
||||
const balanceColor = availableBalance >= 0
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: "text-red-600 dark:text-red-400"
|
||||
```
|
||||
And in the progress bar:
|
||||
```tsx
|
||||
const barColor = group.overBudget
|
||||
? "bg-red-500 dark:bg-red-400"
|
||||
: "bg-green-500 dark:bg-green-400"
|
||||
```
|
||||
|
||||
Pure green (#00FF00) has a contrast ratio of only 1.4:1 against white — catastrophically below WCAG AA's 4.5:1 minimum for text. Even `green-600` (#16a34a) must be verified. Additionally, pie chart adjacent slice colors must maintain 3:1 contrast against each other (WCAG 2.1 SC 1.4.11 Non-text Contrast) — the existing six category colors are all similar lightness (OKLCH L ≈ 0.65–0.72), meaning they may be hard to distinguish for colorblind users or when printed.
|
||||
|
||||
**Why it happens:**
|
||||
Designers focus on making the palette "look rich" during an overhaul without running contrast checks. The redesign goal is "rich, colorful visual style" — this is a direct risk factor for accessibility regressions. Color-alone encoding (green = good, red = bad) violates WCAG 1.4.1 Use of Color, which requires that color not be the sole means of conveying information.
|
||||
|
||||
**How to avoid:**
|
||||
- Run every text color against its background through a WCAG contrast checker (target 4.5:1 for normal text, 3:1 for large text and UI components). Use the Tailwind oklch values — compute with tools like https://webaim.org/resources/contrastchecker/
|
||||
- For semantic colors (balance positive/negative, over-budget): supplement color with an icon or text label, not color alone. Example: a checkmark icon + green for positive balance; an exclamation icon + red for over-budget.
|
||||
- For pie/donut chart slices: ensure adjacent colors have at least 3:1 contrast, or add a visible stroke separator (1px white/black stroke between slices provides a natural contrast boundary).
|
||||
- For the 6 category colors: vary hue and lightness, not just hue. Consider making the OKLCH lightness values more spread (e.g., 0.55 to 0.80 range) so colors are distinguishable at reduced color sensitivity.
|
||||
- Do not rely on `dark:text-green-400` having passed contrast automatically — verify each dark mode color pair independently.
|
||||
|
||||
**Warning signs:**
|
||||
- All six category pie slices are clearly distinguishable to the developer but indistinguishable when the browser's "Emulate vision deficiency" filter is applied in DevTools
|
||||
- Running `window.getComputedStyle` on a colored element and checking its OKLCH L value — if multiple semantic colors cluster at the same lightness, colorblind users see identical grays
|
||||
|
||||
**Phase to address:** Design token / visual language phase (establish accessible palette before building components); then verify in each component phase
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 5: i18n Key Regressions When Renaming or Adding UI Sections
|
||||
|
||||
**What goes wrong:**
|
||||
The current translation files (`en.json`, `de.json`) have flat keys per page section. The overhaul adds new dashboard sections (bar chart, horizontal bar chart, collapsible income/bill/expense groups, richer donut legend). Each new label, section header, tooltip, and ARIA label needs a corresponding key in both files. During rapid UI iteration, developers commonly add `t("dashboard.incomeSection")` to the JSX, forget to add it to `de.json`, and only notice when the German locale shows the raw key string — or worse, the i18next fallback silently shows the English value and the German regression goes undetected.
|
||||
|
||||
**Why it happens:**
|
||||
`i18next` with `fallbackLng: 'en'` (the existing config) silently falls back to English when a German key is missing. There is no visible failure. The project has no i18n linting step and no build-time key extraction check. The `debug: false` production config makes this invisible.
|
||||
|
||||
**How to avoid:**
|
||||
- When adding a new UI section, add its keys to **both** `en.json` and `de.json` in the same commit — never split across commits.
|
||||
- Use `i18next-cli` or `i18next-scanner` (npm package) to extract all `t("...")` call keys from source and diff against the JSON files. Run this as a pre-commit check or part of the build.
|
||||
- In development, consider setting `debug: true` in `i18n.ts` — i18next logs `missingKey` warnings to console for every untranslated key in the non-fallback language.
|
||||
- Use a TypeScript-typed i18n setup (e.g., `i18next-typescript`) so that `t("dashboard.nonExistentKey")` produces a type error at compile time.
|
||||
- Before marking any phase complete, manually switch locale to German and click through every changed screen.
|
||||
|
||||
**Warning signs:**
|
||||
- German UI text contains raw dot-notation strings (e.g., `dashboard.barChart.title`)
|
||||
- `console.warn` messages containing `i18next::translator: missingKey de`
|
||||
- `de.json` has fewer top-level keys than `en.json` after a phase's changes
|
||||
|
||||
**Phase to address:** Every phase that adds new UI text; establish the key-parity check process in the first phase of the overhaul
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 6: Design Inconsistency Across Page Refreshes (The "Island Redesign" Problem)
|
||||
|
||||
**What goes wrong:**
|
||||
The overhaul covers all pages, but if phases are structured page-by-page, early pages get the richest design attention and later pages get inconsistency or fatigue-driven shortcuts. The most common failure mode: the dashboard uses a specific card style (colored header accent, icon in corner), but the Categories page — redesigned two weeks later — uses a subtly different card variant. Buttons on the Budget Detail page use different spacing than on the Template page. The result is a design that looks cohesive in screenshots of individual pages but feels broken when navigating between them.
|
||||
|
||||
**Why it happens:**
|
||||
There is no design system enforced at the component level. shadcn/ui components are used directly in pages without project-specific wrappers. When the overhaul introduces new visual patterns (e.g., a colored icon badge, a section divider style), they are implemented inline in the first page that needs them and then drift in subsequent pages as developers make minor "feels close enough" adaptations.
|
||||
|
||||
**How to avoid:**
|
||||
- Before redesigning any pages, establish a small shared component library of new visual primitives (e.g., `<SectionHeader>`, `<StatCard>`, `<CategoryBadge>`) with fixed prop interfaces. All page redesigns consume these components — they never re-implement them inline.
|
||||
- Define the full color palette and spacing scale in the first phase, not incrementally. The `index.css` `@theme` block is the single source of truth — no hardcoded hex values anywhere else.
|
||||
- Create a visual spec (even a quick screenshot grid) of what each page will look like before coding begins, so inconsistencies are caught at design time not code review time.
|
||||
- In code review: any new Tailwind classes that don't use design tokens (e.g., `text-[#6b21a8]`, `p-[13px]`) should be flagged as violations.
|
||||
|
||||
**Warning signs:**
|
||||
- Two pages use different visual patterns for "section with a list of items" (one uses a table, one uses cards)
|
||||
- Color values appear as raw hex or oklch literals in component files instead of semantic tokens
|
||||
- shadcn Card component is used in 3 different ways across 3 pages (no wrapper abstraction)
|
||||
|
||||
**Phase to address:** Design foundation phase (first phase of overhaul) before any page work begins
|
||||
|
||||
---
|
||||
|
||||
## Technical Debt Patterns
|
||||
|
||||
| Shortcut | Immediate Benefit | Long-term Cost | When Acceptable |
|
||||
|----------|-------------------|----------------|-----------------|
|
||||
| Inline chart data transforms in render | Faster to write | Re-renders on every state change; adding more charts multiplies the cost | Never — always `useMemo` for chart data |
|
||||
| Hardcoded color classes `text-green-600` for semantic states | Familiar Tailwind pattern | Dark mode variants need to be manually maintained in two places; fails contrast checks | Only for truly non-semantic colors (e.g., a green checkmark icon that is always green) |
|
||||
| Copying shadcn component patterns across pages without abstraction | Faster per-page development | Design drift guarantees inconsistency; changes to shared patterns require hunting all usages | MVP only, with explicit note to abstract before more pages are built |
|
||||
| Adding English i18n keys without German equivalent | Unblocks development | Silent regression in German locale; accumulates debt that is hard to audit later | Never in this project — add both languages in the same commit |
|
||||
| Using `ResponsiveContainer` without `debounce` | Default behavior, no extra code | Multiple containers trigger simultaneous resize cascades when sections open/close | Acceptable for single-chart pages; set `debounce={50}` on all multi-chart layouts |
|
||||
| Implementing collapsible with `display: none` toggle | Simplest implementation | No animation; abrupt layout shift; screen readers cannot detect intermediate states | Never for primary dashboard sections; acceptable for debug/dev-only UI |
|
||||
|
||||
---
|
||||
|
||||
## Integration Gotchas
|
||||
|
||||
| Integration | Common Mistake | Correct Approach |
|
||||
|-------------|----------------|------------------|
|
||||
| Recharts + Tailwind CSS variables | Passing Tailwind utility strings (e.g., `"text-blue-500"`) as chart `fill` or `stroke` props | Pass `var(--color-income)` or a resolved hex/oklch string via JS. Recharts SVG does not process Tailwind class strings. |
|
||||
| Recharts + `ResponsiveContainer` | Placing `ResponsiveContainer` inside a flex/grid parent without an explicit height | The container measures `0px` height. Always wrap in a `<div style={{ height: 240 }}>` or give the parent an explicit height class. |
|
||||
| shadcn `Collapsible` + Recharts | Placing charts inside collapsible sections without `debounce` | When the collapsible opens, the chart container resizes and triggers `ResizeObserver` rapidly. Add `debounce={50}` to `ResponsiveContainer`. |
|
||||
| Tailwind v4 `@theme inline` + dark mode | Defining category colors only in `@theme inline` without a `.dark {}` override block | Category colors remain light-mode pastels on dark backgrounds. Define dark variants in a `.dark {}` block in `index.css`. |
|
||||
| i18next + new UI sections | Adding `t("new.key")` JSX without adding the key to `de.json` | Silently falls back to English in German locale. Always update both files simultaneously. |
|
||||
|
||||
---
|
||||
|
||||
## Performance Traps
|
||||
|
||||
| Trap | Symptoms | Prevention | When It Breaks |
|
||||
|------|----------|------------|----------------|
|
||||
| Unmemoized chart data on a multi-chart dashboard | Tooltip hover causes all three charts to re-render simultaneously; visible lag | `useMemo` for all data transforms, `useCallback` for formatter props, `React.memo` on chart sub-components | Noticeable with 50+ budget items; subtle with 10–20 |
|
||||
| O(n) category lookup per budget item across 3 charts | CPU spike when dashboard loads; slow initial paint | Build a `Map<id, Category>` once in the hook layer, O(1) lookup in components | Becomes visible at ~100 budget items |
|
||||
| Multiple `ResponsiveContainer` without `debounce` | Rapid resize loop on section toggle; "ResizeObserver loop" errors | Add `debounce={50}` to all `ResponsiveContainer` instances on the dashboard | Any time two or more containers are mounted simultaneously |
|
||||
| Animating collapsible height with JavaScript | Dropped frames during expand/collapse; chart reflow cascade | Use Radix `Collapsible` with CSS `@keyframes` on `--radix-collapsible-content-height` | Every interaction on the dashboard |
|
||||
| TanStack Query unbounded cache growth | Long browser session consumes excessive memory | Set `gcTime` and `staleTime` on QueryClient config | After ~30 minutes of active use without page reload |
|
||||
|
||||
---
|
||||
|
||||
## UX Pitfalls
|
||||
|
||||
| Pitfall | User Impact | Better Approach |
|
||||
|---------|-------------|-----------------|
|
||||
| Color as the only signal for budget overrun (red progress bar) | Colorblind users cannot distinguish over-budget from on-track | Add an icon (exclamation mark) or text indicator alongside the color change |
|
||||
| Hiding line items behind collapsible sections with no affordance | Users don't discover that sections are expandable; they think the dashboard is incomplete | Use an explicit chevron icon with visible state change; consider first-time-open hint |
|
||||
| Chart tooltips showing raw numbers without currency formatting | Users see "1234.5" instead of "$1,234.50" — especially jarring in a financial app | Always pass `formatCurrency(value, currency)` in the Recharts `formatter` prop |
|
||||
| Summary cards showing totals without context (no comparison to last month or budget) | Numbers without context are harder to interpret; users don't know if $1,200 expenses is good or bad | Show budget vs actual delta or a "vs budget" sub-label on cards |
|
||||
| Five-section collapsible dashboard that defaults to all-collapsed | Users land on a nearly empty dashboard and are confused | Default the primary sections (income, bills) to open on first load |
|
||||
| Progress bar clamped to 100% without showing actual overage amount | Users cannot see how much they are over budget from the bar alone | Show the actual percentage (e.g., "132%") in the label even when the bar is clamped |
|
||||
|
||||
---
|
||||
|
||||
## "Looks Done But Isn't" Checklist
|
||||
|
||||
- [ ] **Charts:** Verify all three chart types (donut, bar, horizontal bar) render correctly when `items` is empty — Recharts renders a blank SVG that can overlap other content if dimensions are not handled
|
||||
- [ ] **Dark mode:** Switch to dark theme (if implemented) and verify all category colors, chart fills, and semantic state colors (green/red) have sufficient contrast — light-mode testing only is the most common gap
|
||||
- [ ] **German locale:** Navigate every redesigned page in German and verify no raw key strings appear — `de.json` parity check
|
||||
- [ ] **Collapsible sections:** Toggle each collapsible section open and closed rapidly three times — verify no layout shift in charts above/below, no "ResizeObserver loop" errors in console
|
||||
- [ ] **Empty states:** Load the dashboard with a budget that has zero actual amounts — verify charts handle `pieData.length === 0` and empty bar data without rendering broken empty SVGs
|
||||
- [ ] **Long category names:** Enter a category named "Wiederkehrende monatliche Haushaltsausgaben" (long German string) — verify it doesn't overflow card boundaries or truncate without tooltip
|
||||
- [ ] **Currency formatting:** Verify EUR formatting (€1.234,56) and USD formatting ($1,234.56) both display correctly in chart tooltips and summary cards when locale is switched in Settings
|
||||
- [ ] **Responsive at 1024px:** View the dashboard at exactly 1024px viewport width — the breakpoint where desktop layout switches — verify no horizontal overflow or chart sizing issues
|
||||
|
||||
---
|
||||
|
||||
## Recovery Strategies
|
||||
|
||||
| Pitfall | Recovery Cost | Recovery Steps |
|
||||
|---------|---------------|----------------|
|
||||
| Chart re-render performance discovered in production | LOW | Add `useMemo` wrappers to chart data transforms; wrap chart components in `React.memo`; requires no visual changes |
|
||||
| Color accessibility failures discovered post-launch | MEDIUM | Audit all semantic color uses with WCAG checker; update `index.css` CSS variable values; may require minor component changes for icon-supplement approach |
|
||||
| i18n key regressions across multiple pages | MEDIUM | Run `i18next-scanner` to enumerate all missing German keys; systematically add translations; no code changes needed |
|
||||
| Design inconsistency across pages after all pages are shipped | HIGH | Requires extracting shared component abstractions retroactively and refactoring all pages — avoid by establishing components in first phase |
|
||||
| `height: 0 → auto` collapsible causing jank discovered mid-phase | LOW | Replace toggle logic with Radix `Collapsible` and add CSS keyframe animation — isolated to the collapsible component, no broader refactor needed |
|
||||
| `ResponsiveContainer` ResizeObserver loop in multi-chart layout | LOW | Add `debounce={50}` prop to each `ResponsiveContainer` — one-line fix per chart instance |
|
||||
|
||||
---
|
||||
|
||||
## Pitfall-to-Phase Mapping
|
||||
|
||||
| Pitfall | Prevention Phase | Verification |
|
||||
|---------|------------------|--------------|
|
||||
| Recharts re-render on every parent state change | Dashboard charts phase — add memoization before wiring data | React DevTools Profiler: chart sub-components should not appear in flame graph on tooltip hover |
|
||||
| CSS variable scope / dark mode chart color gaps | Design tokens phase (first) | Inspect SVG `fill` in DevTools; toggle dark mode and visually verify all chart colors resolve |
|
||||
| Collapsible layout shift and ResizeObserver cascade | Dashboard collapsible sections phase | Toggle all sections rapidly; check console for ResizeObserver errors; check DevTools Performance for layout thrash |
|
||||
| Color accessibility failures | Design tokens phase — define accessible palette upfront | Run each color pair through WCAG contrast checker; use DevTools "Emulate vision deficiency" filter |
|
||||
| i18n key regressions | Every phase — enforce dual-language commit rule from the start | Run `i18next-scanner` before marking any phase complete; switch to German locale and navigate all changed pages |
|
||||
| Design inconsistency across page refreshes | Design foundation phase — create shared components before any page work | Code review: flag any Tailwind color/spacing classes that are not semantic tokens; check that all pages use shared `<SectionHeader>` / `<StatCard>` etc. |
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- [Recharts performance guide (official)](https://recharts.github.io/en-US/guide/performance/)
|
||||
- [Recharts deep-compare issue #281](https://github.com/recharts/recharts/issues/281)
|
||||
- [Recharts ResizeObserver loop issue #1770](https://github.com/recharts/recharts/issues/1770)
|
||||
- [Recharts ResponsiveContainer API](https://recharts.github.io/en-US/api/ResponsiveContainer/)
|
||||
- [Improving Recharts performance (Belchior)](https://belchior.hashnode.dev/improving-recharts-performance-clp5w295y000b0ajq8hu6cnmm)
|
||||
- [Tailwind v4 dark mode docs](https://tailwindcss.com/docs/dark-mode)
|
||||
- [shadcn/ui Tailwind v4 guide](https://ui.shadcn.com/docs/tailwind-v4)
|
||||
- [Tailwind v4 migration breaking changes discussion](https://github.com/tailwindlabs/tailwindcss/discussions/16517)
|
||||
- [shadcn theming with Tailwind v4 and CSS variables (Goins)](https://medium.com/@joseph.goins/theming-shadcn-with-tailwind-v4-and-css-variables-d602f6b3c258)
|
||||
- [WCAG 2.1 SC 1.4.11 Non-text Contrast](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html)
|
||||
- [WCAG SC 1.4.3 Contrast Minimum](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html)
|
||||
- [WebAIM contrast checker](https://webaim.org/resources/contrastchecker/)
|
||||
- [Radix UI Collapsible primitive](https://www.radix-ui.com/primitives/docs/components/collapsible)
|
||||
- [i18next missing key detection discussion](https://github.com/i18next/i18next/discussions/2088)
|
||||
- [Fintech dashboard design (Merge Rocks)](https://merge.rocks/blog/fintech-dashboard-design-or-how-to-make-data-look-pretty)
|
||||
- [Dashboard UX design principles (Smashing Magazine)](https://www.smashingmagazine.com/2025/09/ux-strategies-real-time-dashboards/)
|
||||
- Existing codebase analysis: `src/pages/DashboardPage.tsx`, `src/lib/palette.ts`, `src/index.css`, `.planning/codebase/CONCERNS.md`
|
||||
|
||||
---
|
||||
|
||||
*Pitfalls research for: SimpleFinanceDash UI overhaul (React + Recharts + Tailwind v4 + shadcn/ui)*
|
||||
*Researched: 2026-03-16*
|
||||
289
.planning/research/STACK.md
Normal file
289
.planning/research/STACK.md
Normal file
@@ -0,0 +1,289 @@
|
||||
# Stack Research
|
||||
|
||||
**Domain:** Personal finance dashboard UI overhaul — React SPA
|
||||
**Researched:** 2026-03-16
|
||||
**Confidence:** HIGH
|
||||
|
||||
---
|
||||
|
||||
## Context: What Already Exists
|
||||
|
||||
This is a subsequent-milestone research document. The stack is **locked**. The project uses:
|
||||
|
||||
| Package | Installed Version | Status |
|
||||
|---------|------------------|--------|
|
||||
| React | 19.2.4 | Locked |
|
||||
| Vite | 8.0.0 | Locked |
|
||||
| TypeScript | ~5.9.3 | Locked |
|
||||
| Tailwind CSS | 4.2.1 | Locked |
|
||||
| Recharts | **3.8.0** | Locked — already on latest |
|
||||
| Radix UI (via `radix-ui`) | 1.4.3 | Locked |
|
||||
| Lucide React | 0.577.0 | Locked |
|
||||
| next-themes | 0.4.6 | Locked |
|
||||
| TanStack Query | 5.90.21 | Locked |
|
||||
|
||||
The `index.css` already defines a complete OKLCH `@theme inline` color system with category-specific colors (`--color-income`, `--color-bill`, etc.) and chart colors (`--color-chart-1` through `--color-chart-5`). The project uses the `radix-ui` unified package (post-June 2025 migration).
|
||||
|
||||
**No new frameworks or backend dependencies.** This research covers only what to add or lean into for the UI overhaul.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Stack: Additions and Patterns
|
||||
|
||||
### shadcn/ui Components to Add
|
||||
|
||||
The project has shadcn/ui components already (card, button, input, etc.) but is missing two critical primitives for the redesign.
|
||||
|
||||
| Component | Install Command | Purpose | Why |
|
||||
|-----------|----------------|---------|-----|
|
||||
| `chart` | `npx shadcn@latest add chart` | `ChartContainer` + `ChartTooltipContent` wrappers | Provides theme-aware chart wrappers that read from CSS variables. Eliminates boilerplate for consistent chart theming across all chart types. Copy-paste into `src/components/ui/chart.tsx`. |
|
||||
| `accordion` | `npx shadcn@latest add accordion` | Collapsible category sections on dashboard | Built on Radix UI Accordion primitive. WAI-ARIA compliant, keyboard navigable, supports `type="multiple"` to keep multiple groups open. |
|
||||
| `collapsible` | `npx shadcn@latest add collapsible` | Single collapsible toggle pattern | Alternative to Accordion when only one independent section needs to expand/collapse (simpler than full Accordion). |
|
||||
|
||||
**CRITICAL: Recharts v3 + shadcn chart.tsx compatibility.** The project is on Recharts 3.8.0. The official shadcn/ui chart PR (#8486) for Recharts v3 support is not yet merged (as of March 2026). After running `npx shadcn@latest add chart`, the generated `chart.tsx` may need manual fixes:
|
||||
|
||||
1. The `ChartContainer` may log `"width(-1) and height(-1) of chart should be greater than 0"` warnings — fix by adding `initialDimension={{ width: 320, height: 200 }}` to the `ResponsiveContainer` inside `chart.tsx`.
|
||||
2. If TypeScript errors appear on `TooltipProps`, update the import to use `TooltipProps` from `recharts` directly.
|
||||
3. Community-verified workaround exists in shadcn-ui/ui issue #9892.
|
||||
|
||||
**Confidence: HIGH** — PR #8486 and issue #9892 are the authoritative sources; the fix is a small patch to one file.
|
||||
|
||||
---
|
||||
|
||||
### Core Charting Patterns (Recharts 3.8.0)
|
||||
|
||||
No new charting libraries are needed. Recharts 3.8.0 is the current stable release and supports all required chart types natively. Three chart types are needed for the dashboard redesign:
|
||||
|
||||
**1. Donut Chart (expense category breakdown)**
|
||||
|
||||
```tsx
|
||||
// Already in use. Enrich by:
|
||||
// - Increasing innerRadius to ~60, outerRadius to ~100 for visual weight
|
||||
// - Adding paddingAngle={3} for visible segment separation
|
||||
// - Using stroke="none" (replaces removed blendStroke in v3)
|
||||
// - Rendering a center label via custom activeShape or absolute-positioned div
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={pieData}
|
||||
dataKey="value"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={100}
|
||||
paddingAngle={3}
|
||||
stroke="none"
|
||||
>
|
||||
{pieData.map((entry) => (
|
||||
<Cell key={entry.type} fill={categoryColors[entry.type]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip content={<ChartTooltipContent />} />
|
||||
</PieChart>
|
||||
```
|
||||
|
||||
Note: In Recharts v3, `Cell` is deprecated in favor of the `shape` prop and `activeShape`/`inactiveShape` are deprecated in favor of `shape`. Existing `Cell` usage still works; migration is not required for this milestone.
|
||||
|
||||
**2. Vertical Bar Chart (income budget vs actual)**
|
||||
|
||||
```tsx
|
||||
<BarChart data={incomeData}>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Bar dataKey="budgeted" radius={[4, 4, 0, 0]} fill="var(--color-muted)" />
|
||||
<Bar dataKey="actual" radius={[4, 4, 0, 0]} fill="var(--color-income)" />
|
||||
<Tooltip content={<ChartTooltipContent />} />
|
||||
</BarChart>
|
||||
```
|
||||
|
||||
**3. Horizontal Bar Chart (spend by category type)**
|
||||
|
||||
```tsx
|
||||
// layout="vertical" turns BarChart into a horizontal bar chart
|
||||
<BarChart data={categoryData} layout="vertical">
|
||||
<XAxis type="number" hide />
|
||||
<YAxis type="category" dataKey="name" width={130} />
|
||||
<Bar dataKey="budgeted" radius={[0, 4, 4, 0]} fill="var(--color-muted)" />
|
||||
<Bar dataKey="actual" radius={[0, 4, 4, 0]} fill="var(--color-primary)" />
|
||||
<Tooltip content={<ChartTooltipContent />} />
|
||||
</BarChart>
|
||||
```
|
||||
|
||||
**Confidence: HIGH** — All three patterns verified against Recharts official examples and docs.
|
||||
|
||||
---
|
||||
|
||||
### Tailwind CSS 4 Color System Patterns
|
||||
|
||||
The existing `index.css` is already correctly structured with `@theme inline`. The overhaul needs to extend it with:
|
||||
|
||||
**1. Richer category colors (more vibrant for charts)**
|
||||
|
||||
The current category colors are defined but conservative. For the rich visual style target, increase chroma:
|
||||
|
||||
```css
|
||||
/* Recommended: bump chroma from ~0.14 to ~0.18+ for visual richness */
|
||||
--color-income: oklch(0.68 0.19 145); /* vivid green */
|
||||
--color-bill: oklch(0.65 0.19 25); /* vivid orange-red */
|
||||
--color-variable-expense: oklch(0.70 0.18 55); /* vivid amber */
|
||||
--color-debt: oklch(0.62 0.20 355); /* vivid rose */
|
||||
--color-saving: oklch(0.68 0.18 220); /* vivid blue */
|
||||
--color-investment: oklch(0.65 0.18 285); /* vivid purple */
|
||||
```
|
||||
|
||||
**2. Semantic status tokens for budget comparison**
|
||||
|
||||
```css
|
||||
--color-over-budget: oklch(0.62 0.20 25); /* red-orange for overspend */
|
||||
--color-on-budget: oklch(0.68 0.19 145); /* green for on-track */
|
||||
--color-budget-bar-bg: oklch(0.92 0.01 260); /* neutral track for progress bars */
|
||||
```
|
||||
|
||||
**3. Dark theme token set**
|
||||
|
||||
The current CSS has no dark mode overrides. With `next-themes` v0.4.6 already installed, dark mode works by toggling `.dark` on `<html>`. Add a dark block:
|
||||
|
||||
```css
|
||||
.dark {
|
||||
@theme inline {
|
||||
--color-background: oklch(0.13 0.02 260);
|
||||
--color-foreground: oklch(0.95 0.005 260);
|
||||
--color-card: oklch(0.18 0.02 260);
|
||||
/* ...etc — mirror the light tokens with dark values */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is optional for the current milestone (desktop-first, dark mode not listed as a goal), but the token structure is already set up for it.
|
||||
|
||||
**Confidence: HIGH** — Tailwind CSS 4 `@theme inline` pattern verified against official Tailwind v4 docs.
|
||||
|
||||
---
|
||||
|
||||
### Typography Patterns
|
||||
|
||||
Already uses Inter via `--font-sans`. No new font installations needed. Apply these patterns consistently:
|
||||
|
||||
| Pattern | CSS / Tailwind | Where to Use |
|
||||
|---------|---------------|--------------|
|
||||
| Tabular numbers | `tabular-nums` (already used in DashboardPage) | All currency amounts, percentages |
|
||||
| Monospace amounts | `font-mono` | Dense data tables where column alignment matters |
|
||||
| Numeric font features | `font-feature-settings: "tnum" 1` | When `tabular-nums` alone is insufficient |
|
||||
| Large metric emphasis | `text-3xl font-bold tracking-tight` | Summary card primary values |
|
||||
| Muted labels | `text-sm text-muted-foreground` | Section headers, stat labels |
|
||||
|
||||
The codebase already uses `tabular-nums` on currency values — this is correct and should be applied everywhere financial numbers appear.
|
||||
|
||||
**Confidence: HIGH** — Pattern matches current codebase and Shopify Polaris / Datawrapper typography standards.
|
||||
|
||||
---
|
||||
|
||||
### Layout Patterns
|
||||
|
||||
**Summary card grid:**
|
||||
|
||||
```tsx
|
||||
// 4 cards on desktop, 2x2 on tablet, stacked on mobile
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
```
|
||||
|
||||
**Chart row:**
|
||||
|
||||
```tsx
|
||||
// Charts side-by-side on desktop, stacked on mobile
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
```
|
||||
|
||||
**Collapsible category section:**
|
||||
|
||||
```tsx
|
||||
// Using shadcn Accordion — one group per CategoryType
|
||||
<Accordion type="multiple" defaultValue={["income", "bill"]}>
|
||||
<AccordionItem value="income">
|
||||
<AccordionTrigger>
|
||||
{/* Summary row: label + budgeted + actual + delta */}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
{/* Line item rows */}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
```
|
||||
|
||||
Use `type="multiple"` so users can expand multiple sections simultaneously. Default expand income and variable_expense as the most-checked categories.
|
||||
|
||||
**Confidence: HIGH** — Standard shadcn/ui pattern, verified against official docs.
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Use
|
||||
|
||||
| Avoid | Why | Use Instead |
|
||||
|-------|-----|-------------|
|
||||
| Nivo | Adds a large bundle, different component API, requires adapting all existing chart code | Recharts 3.8.0 — already installed, sufficient |
|
||||
| ApexCharts / ECharts | New library dependency, no benefit over Recharts for this scope | Recharts 3.8.0 |
|
||||
| react-financial-charts | Designed for candlestick / OHLC trading charts, not budget dashboards | Recharts 3.8.0 |
|
||||
| Recharts 2.x | Project is already on v3.8.0 — never downgrade | Stay on 3.8.0 |
|
||||
| shadcn `npx shadcn@latest add` for chart without reading the output | The generated chart.tsx requires a manual Recharts v3 patch | Run the add command, then apply the `initialDimension` fix to `ChartContainer` |
|
||||
| CSS-in-JS for theming | Tailwind v4 `@theme inline` already handles all theming via CSS variables | Extend `index.css` `@theme inline` block |
|
||||
| Custom progress bar libraries | Raw Tailwind divs with `style={{ width: X% }}` are already used and are sufficient | Keep existing progress bar pattern, improve styling only |
|
||||
| Framer Motion | Adds bundle weight; CSS transitions via `transition-all duration-200` cover needed animations | Tailwind transition utilities |
|
||||
| React Grid Layout | Drag/resize dashboards are out of scope for this milestone | Standard CSS Grid |
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
| Recommended | Alternative | When to Use Alternative |
|
||||
|-------------|-------------|-------------------------|
|
||||
| Recharts 3.8.0 (stay) | Nivo | If you needed animated, visually opinionated charts with server-side rendering — not applicable here |
|
||||
| shadcn Accordion | KendoReact PanelBar | If the project used KendoReact's broader component suite — it doesn't |
|
||||
| Tailwind `tabular-nums` | Geist Mono font | Use Geist Mono only if building a dense trading-style table where column alignment is a core feature, not a budget dashboard |
|
||||
| shadcn chart.tsx (copy-paste) | Direct Recharts usage | Acceptable — the project's existing dashboard already uses Recharts directly. shadcn wrappers add theme-aware tooltips and config-based colors but are not mandatory |
|
||||
|
||||
---
|
||||
|
||||
## Version Compatibility
|
||||
|
||||
| Package | Version | Compatible With | Notes |
|
||||
|---------|---------|-----------------|-------|
|
||||
| Recharts | 3.8.0 | React 19 | Requires `react-is` peer dependency override for React 19 |
|
||||
| shadcn chart.tsx | CLI generated | Recharts 3.8.0 | Apply `initialDimension` fix to `ChartContainer` after generation |
|
||||
| Tailwind CSS | 4.2.1 | `@theme inline` | Use `@theme inline` (not bare `@theme`) when referencing other CSS vars |
|
||||
| next-themes | 0.4.6 | React 19 | Compatible; dark mode toggled via `.dark` class on `<html>` |
|
||||
| radix-ui | 1.4.3 | Post-June 2025 migration | shadcn CLI add commands now generate imports from `radix-ui`, not `@radix-ui/react-*` |
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Add shadcn chart primitives (then apply Recharts v3 fix to chart.tsx)
|
||||
npx shadcn@latest add chart
|
||||
|
||||
# Add Accordion for collapsible dashboard sections
|
||||
npx shadcn@latest add accordion
|
||||
|
||||
# Add Collapsible if single-section toggles are needed
|
||||
npx shadcn@latest add collapsible
|
||||
```
|
||||
|
||||
No npm/bun package installs needed — Recharts 3.8.0 is already installed and sufficient.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- [Recharts GitHub Releases](https://github.com/recharts/recharts/releases) — version 3.8.0 confirmed latest stable (March 2025)
|
||||
- [Recharts 3.0 Migration Guide](https://github.com/recharts/recharts/wiki/3.0-migration-guide) — breaking changes from v2 to v3
|
||||
- [shadcn/ui Chart Docs](https://ui.shadcn.com/docs/components/chart) — ChartContainer, ChartConfig, ChartTooltip patterns
|
||||
- [shadcn-ui/ui PR #8486](https://github.com/shadcn-ui/ui/pull/8486) — Recharts v3 chart.tsx upgrade (open as of March 2026)
|
||||
- [shadcn-ui/ui Issue #9892](https://github.com/shadcn-ui/ui/issues/9892) — Community-verified Recharts v3 chart.tsx fix
|
||||
- [shadcn/ui Accordion Docs](https://ui.shadcn.com/docs/components/radix/accordion) — component API verified
|
||||
- [Tailwind CSS v4 Theme Docs](https://tailwindcss.com/docs/theme) — `@theme inline`, CSS variables, OKLCH palette
|
||||
- [npm recharts](https://www.npmjs.com/package/recharts) — 3.8.0 current, 7M weekly downloads, healthy maintenance
|
||||
|
||||
---
|
||||
|
||||
*Stack research for: SimpleFinanceDash UI Overhaul*
|
||||
*Researched: 2026-03-16*
|
||||
200
.planning/research/SUMMARY.md
Normal file
200
.planning/research/SUMMARY.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Project Research Summary
|
||||
|
||||
**Project:** SimpleFinanceDash — UI/UX Overhaul Milestone
|
||||
**Domain:** Personal finance budget dashboard (React SPA, UI-only redesign)
|
||||
**Researched:** 2026-03-16
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This milestone is a pure UI/UX overhaul of an existing working application. The backend (Supabase schema, hooks, queries) is frozen. The stack is already in place — React 19, Vite 8, Tailwind CSS 4, Recharts 3.8.0, shadcn/ui, TanStack Query — and no new framework dependencies are needed. The overhaul adds three shadcn/ui primitives (`chart`, `accordion`/`collapsible`), extends the existing OKLCH color system, and introduces a structured component hierarchy for the dashboard. The core transformation is from a flat single-chart dashboard to a rich hybrid layout: summary KPI cards, three chart types (donut, vertical bar, horizontal bar), and collapsible per-category line-item sections.
|
||||
|
||||
The recommended approach is to establish design foundations first (tokens, shared components, build order primitives) before touching any page. Every visual decision — color, spacing, card style — must originate from `index.css` `@theme inline` tokens and a small set of shared components (`PageShell`, `StatCard`, `CategorySection`). Without this discipline, a multi-page overhaul will produce "island redesign" inconsistency: each page looks polished individually but the app feels fragmented during navigation. The dashboard phase comes second, followed by extending the design system to all remaining pages.
|
||||
|
||||
The highest-risk technical area is the Recharts + shadcn `chart.tsx` integration: the official shadcn PR for Recharts v3 is not yet merged, so the generated `chart.tsx` requires a known manual patch (add `initialDimension` to `ResponsiveContainer`). A close second risk is collapsible section layout shift — Recharts `ResponsiveContainer` instances react to parent height changes, and multiple instances on one page require `debounce={50}` and Radix `Collapsible` (not raw `display:none` toggle) to prevent ResizeObserver cascade errors. Both risks have documented fixes and are LOW recovery cost if addressed proactively.
|
||||
|
||||
---
|
||||
|
||||
## Key Findings
|
||||
|
||||
### Recommended Stack
|
||||
|
||||
The stack is locked; no new npm packages are required. Three shadcn/ui components must be installed via CLI: `chart` (Recharts theme-aware wrappers), `collapsible` (Radix primitive for category sections), and optionally `accordion`. After running `npx shadcn@latest add chart`, a manual one-line fix to `chart.tsx` is required: add `initialDimension={{ width: 320, height: 200 }}` to the inner `ResponsiveContainer` (see shadcn-ui/ui issue #9892). The existing OKLCH color palette in `index.css` needs only two additions: richer chroma on category colors (bump from ~0.14 to ~0.18+) and semantic status tokens (`--color-over-budget`, `--color-on-budget`, `--color-budget-bar-bg`).
|
||||
|
||||
**Core technologies and their roles for this milestone:**
|
||||
- **Recharts 3.8.0** — all three chart types (donut, bar, horizontal bar) are supported natively; stay on current version, do not introduce alternative chart libraries
|
||||
- **shadcn `chart.tsx`** — `ChartContainer` + `ChartTooltipContent` wrappers provide CSS-variable-aware theming for all Recharts instances; required patch documented in issue #9892
|
||||
- **shadcn `Collapsible`** — Radix UI primitive that animates height via CSS custom property `--radix-collapsible-content-height`; the correct tool for per-category collapsible sections
|
||||
- **Tailwind CSS 4 `@theme inline`** — single source of truth for all color tokens; all component color values must reference CSS variables, never hardcoded hex
|
||||
- **`useMemo` in DashboardContent** — all chart data derivations must be memoized centrally; child chart components receive pre-computed data arrays and render only
|
||||
|
||||
**Critical version note:** Recharts 3 deprecated `Cell` in favor of `shape` prop, and removed `blendStroke` — use `stroke="none"` instead. Existing `Cell` usage still works but should not be extended.
|
||||
|
||||
### Expected Features
|
||||
|
||||
Research grounded in competitor analysis (YNAB, Empower) and fintech UX standards. The full feature list lives in `FEATURES.md`.
|
||||
|
||||
**Must have (table stakes — users expect these):**
|
||||
- Summary KPI cards (income / expenses / balance) with colored semantics and variance badges
|
||||
- Donut chart with center total label and active hover expand
|
||||
- Horizontal bar chart: budget vs actual per category type
|
||||
- Grouped bar chart: income budgeted vs actual
|
||||
- Collapsible per-category sections with line items and group totals
|
||||
- Skeleton loading states that mirror the real layout structure
|
||||
- Month navigation control on the dashboard (currently locked to current month)
|
||||
- Consistent design language across all pages — the dashboard sets the pattern, all other pages must inherit it
|
||||
|
||||
**Should have (differentiators, high value for low cost):**
|
||||
- Variance indicators (delta arrows/badges) on summary cards and section headers
|
||||
- Accent-colored category section borders (`border-l-4` with category CSS variable)
|
||||
- Empty state with actionable CTA for new months
|
||||
- Carryover amount surfaced on the dashboard balance card
|
||||
- Section collapse state preserved in localStorage (P3 polish)
|
||||
|
||||
**Defer to v2+:**
|
||||
- Trend / multi-month charts — requires new query shape and chart layout; explicitly out of scope
|
||||
- Dark mode — OKLCH infrastructure is ready but requires full contrast audit; doubles testing surface
|
||||
- AI-derived insights — backend dependency; no scope here
|
||||
- Drag-to-reorder categories — requires `sort_order` mutation support
|
||||
|
||||
### Architecture Approach
|
||||
|
||||
The existing three-tier architecture (Pages → Hooks → Supabase) is preserved intact. The overhaul introduces a **View Components layer** that sits between pages and shadcn/ui primitives, and a small **Shared Components layer** for cross-page patterns. Hooks and library files are read-only during this milestone; all computation lives in the presentation layer via `useMemo`.
|
||||
|
||||
**New components and their responsibilities:**
|
||||
|
||||
| Component | Location | Responsibility |
|
||||
|-----------|----------|----------------|
|
||||
| `DashboardContent` | `components/dashboard/` | Orchestrator: owns `useMemo` derivations, passes typed props to all children |
|
||||
| `SummaryStrip` + `StatCard` | `components/dashboard/` | KPI cards row with semantic color and variance badge |
|
||||
| `ChartPanel` | `components/dashboard/` | Two-column responsive grid containing all three chart instances |
|
||||
| `IncomeBarChart` | `components/dashboard/` | Budgeted vs actual income vertical bar, wrapped in `ChartContainer` |
|
||||
| `ExpenseDonutChart` | `components/dashboard/` | Expense breakdown donut with center total and custom legend |
|
||||
| `SpendBarChart` | `components/dashboard/` | Horizontal budget vs actual by category type |
|
||||
| `CategorySection` | `components/dashboard/` | Radix `Collapsible` wrapping header row + `BudgetLineItems` |
|
||||
| `BudgetLineItems` | `components/dashboard/` | Line-item table reusing existing `InlineEditCell` / `DifferenceCell` atoms |
|
||||
| `PageShell` | `components/shared/` | Cross-page consistent header with title, optional description, CTA slot |
|
||||
|
||||
**Build order is strictly dependency-driven** (see Architecture section for full sequence): install shadcn primitives first, then `PageShell` and `StatCard`, then chart components, then `CategorySection`, then `DashboardContent` as final orchestrator, then all remaining page refreshes.
|
||||
|
||||
### Critical Pitfalls
|
||||
|
||||
All six pitfalls in `PITFALLS.md` are rated HIGH confidence with verified fixes. The top five to actively prevent:
|
||||
|
||||
1. **Unmemoized chart data triggers triple re-renders** — every `items.filter().reduce()` in `DashboardContent` must be wrapped in `useMemo`. Adding three chart instances to a non-memoized render body multiplies re-render cost 3x and causes tooltip hover lag. Recovery cost is LOW but disrupts delivered work if caught late.
|
||||
|
||||
2. **shadcn `chart.tsx` incompatibility with Recharts 3** — the generated `chart.tsx` from `npx shadcn@latest add chart` will produce `width(-1) and height(-1)` warnings and potential layout failures. Apply the `initialDimension` fix to `ChartContainer` immediately after generation. This is a one-line fix with a documented workaround (issue #9892), but if skipped it causes all charts to silently render at zero dimensions.
|
||||
|
||||
3. **Collapsible sections trigger ResizeObserver cascade on charts** — using `display:none` toggle or raw `height` animation on category sections causes Recharts `ResponsiveContainer` instances above/below to rapidly resize. Fix: use Radix `Collapsible` with CSS `@keyframes` on `--radix-collapsible-content-height`, and add `debounce={50}` to all `ResponsiveContainer` instances.
|
||||
|
||||
4. **Color accessibility regression during "rich visual" overhaul** — the redesign goal of rich colors is a direct risk factor for WCAG contrast failures. The existing `text-green-600` / `text-red-600` pattern must be audited (green-600 is borderline). All six category pie slice colors cluster at similar OKLCH lightness (~0.65–0.72) — vary lightness as well as hue. Supplement color with icons for status indicators; color alone is never sufficient.
|
||||
|
||||
5. **i18n key regressions** — `i18next` silently falls back to English when German keys are missing (`fallbackLng: 'en'`). Every new UI text key must be added to both `en.json` and `de.json` in the same commit. Run `i18next-scanner` or manually switch to German locale before marking any phase complete.
|
||||
|
||||
**Sixth pitfall to prevent at the outset:** Design inconsistency across page refreshes. If shared components (`StatCard`, `CategorySection` header pattern, card border accents) are not extracted before page work begins, each page will develop subtle visual drift that is expensive to correct retroactively.
|
||||
|
||||
---
|
||||
|
||||
## Implications for Roadmap
|
||||
|
||||
Research points to a clear four-phase structure. The ordering is driven by: (1) the build dependency chain in ARCHITECTURE.md, (2) the "design foundation before page work" principle from PITFALLS.md, and (3) the feature dependency graph in FEATURES.md.
|
||||
|
||||
### Phase 1: Design Foundation and Primitives
|
||||
**Rationale:** Multiple pitfalls converge on the same root cause — design tokens and shared components must exist before any page work begins. Design drift (Pitfall 6) and color accessibility failures (Pitfall 4) both require the palette and shared component abstractions to be established first. This phase has no external dependencies and produces the building blocks every subsequent phase consumes.
|
||||
**Delivers:** Extended `index.css` color tokens (richer chroma, semantic status tokens), installed shadcn primitives (`chart.tsx` with Recharts v3 patch applied, `collapsible.tsx`), `PageShell`, `StatCard` / `SummaryStrip` components.
|
||||
**Addresses:** Summary KPI cards (table stakes), accent-colored category borders, semantic color system, skeleton components
|
||||
**Avoids:** Design inconsistency pitfall, color accessibility regression, CSS variable scope issues
|
||||
**Research flag:** Standard patterns — no additional research needed. Tailwind v4 `@theme inline` and WCAG contrast requirements are well-documented.
|
||||
|
||||
### Phase 2: Dashboard Charts and Layout
|
||||
**Rationale:** Charts depend on `chart.tsx` from Phase 1. The dashboard is the highest-value page and establishes the full visual language for all subsequent pages. Month navigation must be included here (not deferred) because it changes how `DashboardContent` holds state — retrofitting it after collapsible sections are built is more disruptive than including it upfront.
|
||||
**Delivers:** `DashboardContent` orchestrator with `useMemo` data derivations, `ChartPanel` with all three chart types (`IncomeBarChart`, `ExpenseDonutChart`, `SpendBarChart`), month navigation control, skeleton loading for charts and cards.
|
||||
**Addresses:** Donut chart (table stakes), horizontal bar chart (P1), grouped income bar chart (P1), skeleton loading (P1), month navigation (P1)
|
||||
**Avoids:** Unmemoized chart data pitfall (use `useMemo` from the start), `chart.tsx` Recharts v3 patch (apply in Phase 1 before this phase begins)
|
||||
**Research flag:** Standard patterns — Recharts 3.8.0 chart implementations are fully documented. The specific `chart.tsx` fix is documented in issue #9892.
|
||||
|
||||
### Phase 3: Collapsible Dashboard Sections
|
||||
**Rationale:** `CategorySection` depends on `Collapsible` primitive (Phase 1) and `BudgetLineItems` (which reuses existing `InlineEditCell` atoms). This phase brings the dashboard to full feature completeness. Separating it from Phase 2 keeps each phase focused and ensures chart layout is stable before section collapse animations interact with chart resize behavior.
|
||||
**Delivers:** `CategorySection` components with Radix `Collapsible`, `BudgetLineItems` table, group totals row, variance indicators in section headers, carryover amount on balance card.
|
||||
**Addresses:** Collapsible sections with line items (P1), totals per section (table stakes), variance indicators (P2), carryover amount (P2)
|
||||
**Avoids:** Collapsible layout shift pitfall — use Radix `Collapsible` with CSS keyframe animation and `debounce={50}` on all `ResponsiveContainer` instances; never `display:none` toggle
|
||||
**Research flag:** Standard patterns — Radix Collapsible API is well-documented. The `--radix-collapsible-content-height` animation pattern is the established approach.
|
||||
|
||||
### Phase 4: Full-App Design Consistency
|
||||
**Rationale:** The dashboard establishes the design token application patterns. All other pages inherit from it. This phase is sequenced last because it depends on `PageShell` and the card/color patterns being proven on the dashboard first. FEATURES.md dependency graph makes this explicit: "all-pages redesign depends on dashboard being finished first."
|
||||
**Delivers:** `PageShell` applied to all 9 pages, consistent card/color/typography treatment on `BudgetDetailPage` (highest urgency — users navigate here directly from dashboard), `BudgetListPage`, `CategoriesPage`, `TemplatePage`, `LoginPage`, `RegisterPage`, `QuickAddPage`, `SettingsPage`. Empty state pattern for new months.
|
||||
**Addresses:** Consistent design language (table stakes), BudgetDetailPage polish (P1), remaining page polish (P2), empty states (P2)
|
||||
**Avoids:** i18n key regressions — switch to German locale and run key parity check before completing each sub-page
|
||||
**Research flag:** Standard patterns — `PageShell` wrapper is straightforward. The main risk is thoroughness (9 pages), not technical complexity.
|
||||
|
||||
### Phase Ordering Rationale
|
||||
|
||||
- **Foundation before features:** Pitfall 6 (design inconsistency) has a HIGH recovery cost. Establishing `index.css` tokens and shared components in Phase 1 prevents the most expensive failure mode.
|
||||
- **Dashboard before other pages:** FEATURES.md dependency graph is explicit — the dashboard establishes the patterns all pages inherit. Building it second (after foundation) lets Phase 4 apply proven patterns.
|
||||
- **Charts before collapsibles:** `ChartPanel` layout must be stable before collapsible sections are added beneath it. The ResizeObserver pitfall (Pitfall 3) is easiest to test and fix when charts are the only moving part.
|
||||
- **Sections in their own phase:** The collapsible + ResizeObserver interaction is the trickiest technical integration. Isolating it in Phase 3 limits blast radius if issues arise.
|
||||
|
||||
### Research Flags
|
||||
|
||||
Phases with standard, well-documented patterns (skip `/gsd:research-phase`):
|
||||
- **Phase 1:** Tailwind v4 `@theme inline`, OKLCH color tokens, and WCAG contrast requirements are all from official documentation with no ambiguity.
|
||||
- **Phase 2:** Recharts 3.8.0 chart implementations and the `chart.tsx` fix are fully documented. Month navigation via `useBudgets` hook is a straightforward state change in `DashboardContent`.
|
||||
- **Phase 3:** Radix Collapsible primitive is well-documented. The animation pattern with `--radix-collapsible-content-height` is the standard approach.
|
||||
- **Phase 4:** `PageShell` application and page-by-page refresh are repetitive pattern application, not novel implementation.
|
||||
|
||||
No phases require pre-execution research. All major decisions are resolved by this research.
|
||||
|
||||
---
|
||||
|
||||
## Confidence Assessment
|
||||
|
||||
| Area | Confidence | Notes |
|
||||
|------|------------|-------|
|
||||
| Stack | HIGH | Stack is locked and fully inspected. The one uncertainty (shadcn `chart.tsx` + Recharts v3 compatibility) has a documented fix in issue #9892. |
|
||||
| Features | HIGH | Grounded in competitor analysis (YNAB, Empower) and fintech UX standards. Feature prioritization is opinionated and defensible. |
|
||||
| Architecture | HIGH | Based on full codebase inspection. Component boundaries and build order are derived from actual file structure and import dependencies. |
|
||||
| Pitfalls | HIGH | Most pitfalls verified against official docs and open Recharts/shadcn issues. Recovery strategies and warning signs documented for each. |
|
||||
|
||||
**Overall confidence: HIGH**
|
||||
|
||||
### Gaps to Address
|
||||
|
||||
- **shadcn `chart.tsx` patch (issue #9892):** The `initialDimension` fix is community-verified but not yet merged into the official shadcn CLI output. Apply manually after generation and verify with a smoke test (render one chart, confirm no `width(-1)` warnings in console) before proceeding with Phase 2.
|
||||
- **WCAG contrast on category colors:** The recommended OKLCH values in STACK.md (chroma bumped to ~0.18+) have not been run through a contrast checker. During Phase 1, verify each category color pair against its background at the target luminance levels. Adjust chroma or lightness if any pair fails 3:1 (non-text) or 4.5:1 (text) thresholds.
|
||||
- **i18n key count baseline:** Before starting the overhaul, run `i18next-scanner` (or manually audit `en.json` vs `de.json`) to establish a baseline parity count. This makes regression detection in each subsequent phase mechanical rather than manual.
|
||||
- **`BudgetListPage` data shape:** Research did not inspect `BudgetListPage` implementation in detail. Phase 4 may uncover layout decisions that conflict with the dashboard's card pattern. Plan for one iteration pass on `BudgetListPage` specifically.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence — official documentation)
|
||||
- [Recharts 3.0 Migration Guide](https://github.com/recharts/recharts/wiki/3.0-migration-guide) — v3 breaking changes, `Cell` deprecation, `blendStroke` removal
|
||||
- [Recharts API: ResponsiveContainer](https://recharts.github.io/en-US/api/ResponsiveContainer/) — `debounce` prop, dimension requirements
|
||||
- [shadcn/ui Chart Docs](https://ui.shadcn.com/docs/components/chart) — `ChartContainer`, `ChartConfig`, `ChartTooltipContent`
|
||||
- [shadcn/ui Accordion + Collapsible Docs](https://ui.shadcn.com/docs/components/radix/accordion) — component API, `type="multiple"`, independent state
|
||||
- [Radix UI Collapsible](https://www.radix-ui.com/primitives/docs/components/collapsible) — `--radix-collapsible-content-height` animation pattern
|
||||
- [Tailwind CSS v4 Theme Docs](https://tailwindcss.com/docs/theme) — `@theme inline`, CSS variable scoping, dark mode class strategy
|
||||
- [WCAG 2.1 SC 1.4.11 Non-text Contrast](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html) — 3:1 minimum for UI components and chart elements
|
||||
- [WCAG SC 1.4.3 Contrast Minimum](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html) — 4.5:1 for normal text
|
||||
- [React useMemo](https://react.dev/reference/react/useMemo) — memoization patterns for derived state
|
||||
- Existing codebase: full inspection of `src/pages/`, `src/components/`, `src/hooks/`, `src/lib/`, `src/index.css`
|
||||
|
||||
### Secondary (MEDIUM confidence — community sources, multiple corroborating)
|
||||
- [shadcn-ui/ui PR #8486](https://github.com/shadcn-ui/ui/pull/8486) — Recharts v3 chart.tsx upgrade (open as of March 2026)
|
||||
- [shadcn-ui/ui Issue #9892](https://github.com/shadcn-ui/ui/issues/9892) — Community-verified `initialDimension` fix for Recharts v3
|
||||
- [Recharts performance guide](https://recharts.github.io/en-US/guide/performance/) — memoization guidance
|
||||
- [Recharts ResizeObserver loop issue #1770](https://github.com/recharts/recharts/issues/1770) — confirmed bug and `debounce` workaround
|
||||
- [YNAB / Empower competitor analysis](https://bountisphere.com/blog/personal-finance-apps-2025-review) — feature comparison basis
|
||||
- [Fintech dashboard design best practices — Eleken, Merge Rocks, F9 Finance](https://merge.rocks/blog/fintech-dashboard-design-or-how-to-make-data-look-pretty) — visual design conventions
|
||||
- [shadcn/ui Charts examples](https://ui.shadcn.com/charts) — Donut with center text, bar chart patterns
|
||||
|
||||
### Tertiary (informing but not authoritative)
|
||||
- [Skeleton loading UX — LogRocket](https://blog.logrocket.com/ux-design/skeleton-loading-screen-design/) — skeleton mirrors real layout
|
||||
- [Empty state UX — Eleken](https://www.eleken.co/blog-posts/empty-state-ux) — CTA pattern for empty states
|
||||
- [Color theory in finance dashboards — Extej/Medium](https://medium.com/@extej/the-role-of-color-theory-in-finance-dashboard-design-d2942aec9fff) — palette chroma recommendations
|
||||
|
||||
---
|
||||
|
||||
*Research completed: 2026-03-16*
|
||||
*Ready for roadmap: yes*
|
||||
Reference in New Issue
Block a user