docs: complete project research

This commit is contained in:
2026-03-16 11:45:14 +01:00
parent c960b1a504
commit b830d381db
5 changed files with 1480 additions and 0 deletions

View 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.650.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 1020 |
| 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*