docs: complete project research
This commit is contained in:
410
.planning/research/ARCHITECTURE.md
Normal file
410
.planning/research/ARCHITECTURE.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# Architecture Patterns: Design System
|
||||
|
||||
**Domain:** shadcn/ui + Tailwind CSS 4 design system for personal finance dashboard
|
||||
**Researched:** 2026-03-11
|
||||
**Confidence:** HIGH (based on direct codebase inspection + framework documentation patterns)
|
||||
|
||||
---
|
||||
|
||||
## Recommended Architecture
|
||||
|
||||
The goal is a pastel design system layered on top of shadcn/ui without replacing it. The architecture has three tiers:
|
||||
|
||||
1. **Token layer** — CSS custom properties in `index.css` (already exists, needs pastel values)
|
||||
2. **Variant layer** — CVA-based component variants in `components/ui/` (shadcn files, lightly patched)
|
||||
3. **Composition layer** — Feature components in `components/` that assemble ui primitives with domain semantics
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
index.css ← Token layer: ALL CSS variables live here
|
||||
lib/
|
||||
utils.ts ← cn() helper (already exists)
|
||||
components/
|
||||
ui/ ← Variant layer: shadcn primitives (owned, patchable)
|
||||
button.tsx
|
||||
card.tsx
|
||||
badge.tsx
|
||||
...
|
||||
category-badge.tsx ← Composition layer: domain-specific wrapper
|
||||
stat-card.tsx ← Composition layer: reusable financial card pattern
|
||||
progress-bar.tsx ← Composition layer: budget vs actual bar
|
||||
page-header.tsx ← Composition layer: consistent page headers
|
||||
pages/ ← Page layer: route views that compose components
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Boundaries
|
||||
|
||||
| Component | Responsibility | Communicates With |
|
||||
|-----------|---------------|-------------------|
|
||||
| `index.css` | All CSS variables and `@theme inline` mappings | Tailwind engine only |
|
||||
| `components/ui/*` | Headless + styled primitives (shadcn-owned) | Tailwind classes, CVA variants |
|
||||
| `components/*.tsx` | Domain-aware compositions (app-owned) | ui primitives, hooks, i18n |
|
||||
| `pages/*.tsx` | Route-level views | components, hooks, router |
|
||||
| `hooks/*.ts` | Data fetching + state | API client only |
|
||||
|
||||
The key boundary: `components/ui/` components must not import domain types from `lib/api.ts`. Only `components/*.tsx` and `pages/*.tsx` know about API shapes.
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
CSS Variables (index.css)
|
||||
↓ consumed by
|
||||
Tailwind @theme inline → utility classes (bg-primary, text-muted-foreground, etc.)
|
||||
↓ applied in
|
||||
shadcn ui primitives (button, card, badge, input)
|
||||
↓ assembled into
|
||||
Domain components (stat-card, category-badge, progress-bar)
|
||||
↓ composed into
|
||||
Pages (DashboardPage, LoginPage, CategoriesPage, SettingsPage)
|
||||
↑ data from
|
||||
Hooks (useAuth, useBudgets) → API client (lib/api.ts) → Go REST API
|
||||
```
|
||||
|
||||
Styling flows **downward** (tokens → primitives → components → pages). Data flows **upward** (API → hooks → pages → components via props).
|
||||
|
||||
---
|
||||
|
||||
## Patterns to Follow
|
||||
|
||||
### Pattern 1: Token-First Color System
|
||||
|
||||
**What:** Define all palette values as CSS custom properties in `:root`, then map them to Tailwind via `@theme inline`. Never write raw color values (`oklch(0.88 0.06 310)`) in component files.
|
||||
|
||||
**When:** Any time a new color is needed for the pastel palette.
|
||||
|
||||
**How it works in this codebase:**
|
||||
|
||||
The project uses Tailwind 4 which reads tokens exclusively from `@theme inline {}` in index.css. The two-step pattern already present in the codebase is correct — add raw values to `:root {}`, then expose them as Tailwind utilities in `@theme inline {}`.
|
||||
|
||||
```css
|
||||
/* Step 1: Define semantic tokens in :root */
|
||||
:root {
|
||||
/* Pastel palette — raw values */
|
||||
--pastel-pink: oklch(0.92 0.04 340);
|
||||
--pastel-blue: oklch(0.92 0.04 220);
|
||||
--pastel-green: oklch(0.92 0.04 145);
|
||||
--pastel-amber: oklch(0.92 0.06 80);
|
||||
--pastel-violet: oklch(0.92 0.04 290);
|
||||
--pastel-sky: oklch(0.94 0.04 215);
|
||||
|
||||
/* Semantic role tokens — reference palette */
|
||||
--color-income: var(--pastel-green);
|
||||
--color-bill: var(--pastel-blue);
|
||||
--color-expense: var(--pastel-amber);
|
||||
--color-debt: var(--pastel-pink);
|
||||
--color-saving: var(--pastel-violet);
|
||||
--color-investment: var(--pastel-sky);
|
||||
|
||||
/* Override shadcn semantic tokens with pastel values */
|
||||
--primary: oklch(0.55 0.14 255); /* soft indigo */
|
||||
--background: oklch(0.985 0.005 240); /* barely-blue white */
|
||||
--card: oklch(1 0 0);
|
||||
--muted: oklch(0.96 0.01 240);
|
||||
}
|
||||
|
||||
/* Step 2: Expose as Tailwind utilities inside @theme inline */
|
||||
@theme inline {
|
||||
/* Existing shadcn bridge (keep as-is) */
|
||||
--color-primary: var(--primary);
|
||||
--color-background: var(--background);
|
||||
/* ... */
|
||||
|
||||
/* New pastel category utilities */
|
||||
--color-income: var(--color-income);
|
||||
--color-bill: var(--color-bill);
|
||||
--color-expense: var(--color-expense);
|
||||
--color-debt: var(--color-debt);
|
||||
--color-saving: var(--color-saving);
|
||||
--color-investment: var(--color-investment);
|
||||
}
|
||||
```
|
||||
|
||||
Components then use `bg-income`, `text-bill`, `bg-saving/30` etc. as Tailwind classes.
|
||||
|
||||
### Pattern 2: Category Color Mapping via CVA
|
||||
|
||||
**What:** A single `categoryVariants` CVA definition maps category types to their pastel color classes. Any component that needs category coloring imports this one function.
|
||||
|
||||
**When:** CategoryBadge, table rows in FinancialOverview, chart color arrays, category icons.
|
||||
|
||||
```typescript
|
||||
// components/category-badge.tsx
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
export const categoryVariants = cva(
|
||||
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
|
||||
{
|
||||
variants: {
|
||||
type: {
|
||||
income: "bg-income/20 text-income-foreground",
|
||||
bill: "bg-bill/20 text-bill-foreground",
|
||||
variable_expense:"bg-expense/20 text-expense-foreground",
|
||||
debt: "bg-debt/20 text-debt-foreground",
|
||||
saving: "bg-saving/20 text-saving-foreground",
|
||||
investment: "bg-investment/20 text-investment-foreground",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This is the single source of truth for category-to-color mapping. Chart colors derive from the same CSS variables (not hardcoded hex).
|
||||
|
||||
### Pattern 3: Wrapper Components Over shadcn Modification
|
||||
|
||||
**What:** When domain semantics need to be expressed (e.g., a "stat card" showing budget vs. actual), create a wrapper component in `components/` that uses shadcn Card internally. Do not modify `components/ui/card.tsx` for domain logic.
|
||||
|
||||
**When:** Any component that appears more than twice with the same structure across pages.
|
||||
|
||||
```typescript
|
||||
// components/stat-card.tsx — app-owned, domain-aware
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface StatCardProps {
|
||||
title: string
|
||||
value: string
|
||||
subtext?: string
|
||||
accent?: "income" | "bill" | "expense" | "debt" | "saving" | "investment"
|
||||
}
|
||||
|
||||
export function StatCard({ title, value, subtext, accent }: StatCardProps) {
|
||||
return (
|
||||
<Card className={cn(accent && `border-l-4 border-l-${accent}`)}>
|
||||
<CardHeader><CardTitle>{title}</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold">{value}</p>
|
||||
{subtext && <p className="text-sm text-muted-foreground">{subtext}</p>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: Extend shadcn Primitives via className Override Only
|
||||
|
||||
**What:** When a shadcn component needs a slight visual adjustment, pass a `className` prop using `cn()` to override. Do not fork the component unless adding a new CVA variant that belongs at the primitive level.
|
||||
|
||||
**When:** One-off adjustments in page/feature components.
|
||||
|
||||
**When to actually patch `components/ui/`:** Adding a CVA variant used in 3+ places (e.g., adding a `pastel` button variant). Keep patches minimal and document them.
|
||||
|
||||
```typescript
|
||||
// Good — className override in consumer
|
||||
<Button className="bg-primary/90 hover:bg-primary">Save Budget</Button>
|
||||
|
||||
// Good — new CVA variant if used everywhere
|
||||
// In button.tsx, add to variants.variant:
|
||||
pastel: "bg-primary/15 text-primary hover:bg-primary/25 border-primary/20",
|
||||
```
|
||||
|
||||
### Pattern 5: Recharts Color Consistency
|
||||
|
||||
**What:** Recharts color arrays must reference the same CSS variables used in Tailwind classes. Use `getComputedStyle` to read the variable at render time, not hardcoded hex values.
|
||||
|
||||
**When:** Any chart component (donut, bar, pie in ExpenseBreakdown, FinancialOverview).
|
||||
|
||||
```typescript
|
||||
// In chart component or a lib/colors.ts utility
|
||||
function getCategoryColor(type: CategoryType): string {
|
||||
return getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(`--color-${type}`)
|
||||
.trim()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### Anti-Pattern 1: Inline Color Values in Components
|
||||
|
||||
**What:** Writing `bg-sky-50`, `bg-emerald-50`, `bg-amber-50` directly in component files (already present in `FinancialOverview.tsx`).
|
||||
|
||||
**Why bad:** The color assignment is scattered across every component. Changing a category color requires hunting every file. The existing code in `FinancialOverview.tsx` rows array hardcodes `color: 'bg-sky-50'` — this must be replaced by the category variant system.
|
||||
|
||||
**Instead:** All category-to-color mappings live in `categoryVariants` (CVA, single file). Components only pass the category type.
|
||||
|
||||
### Anti-Pattern 2: Separate CSS Files per Component
|
||||
|
||||
**What:** Creating `StatCard.css`, `Dashboard.css` etc.
|
||||
|
||||
**Why bad:** The project uses Tailwind 4's `@theme inline` — all custom styles belong in `index.css` or as Tailwind utilities. Split CSS files fragment the token system.
|
||||
|
||||
**Instead:** All tokens in `index.css`. All component-specific styles as Tailwind classes or CVA variants.
|
||||
|
||||
### Anti-Pattern 3: Dark Mode Parallel Pastel Palette
|
||||
|
||||
**What:** Trying to create a full pastel dark mode in the same milestone.
|
||||
|
||||
**Why bad:** High complexity for low value (budgeting is primarily a desktop daytime activity). The PROJECT.md explicitly defers custom themes.
|
||||
|
||||
**Instead:** Keep `.dark {}` block in index.css as-is (it's already defined). Focus dark mode on the existing neutral dark values. Pastel = light mode only for this milestone.
|
||||
|
||||
### Anti-Pattern 4: Tailwind `tailwind.config.js` for Custom Tokens
|
||||
|
||||
**What:** Creating a `tailwind.config.js` or `tailwind.config.ts` to add custom colors.
|
||||
|
||||
**Why bad:** This project uses Tailwind CSS 4 (`@import "tailwindcss"` in index.css, `@tailwindcss/vite` plugin). Tailwind 4 configures everything through CSS — `tailwind.config.js` is a Tailwind 3 pattern. Using both causes conflicts and confusion.
|
||||
|
||||
**Instead:** All custom tokens go in `@theme inline {}` inside `index.css`.
|
||||
|
||||
---
|
||||
|
||||
## CSS Variable Organization
|
||||
|
||||
The existing `index.css` should be organized into clearly labeled sections:
|
||||
|
||||
```css
|
||||
/* 1. FRAMEWORK IMPORTS */
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
@import "@fontsource-variable/geist";
|
||||
|
||||
/* 2. CUSTOM DARK VARIANT */
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* 3. LIGHT MODE TOKENS */
|
||||
:root {
|
||||
/* === PASTEL PALETTE (raw values) === */
|
||||
--pastel-pink: ...;
|
||||
--pastel-blue: ...;
|
||||
/* ... */
|
||||
|
||||
/* === CATEGORY SEMANTIC TOKENS === */
|
||||
--color-income: var(--pastel-green);
|
||||
/* ... */
|
||||
|
||||
/* === SHADCN SEMANTIC TOKENS (override defaults) === */
|
||||
--background: ...;
|
||||
--primary: ...;
|
||||
/* ... all existing shadcn vars ... */
|
||||
|
||||
/* === CHART TOKENS === */
|
||||
--chart-1: ...;
|
||||
/* ... */
|
||||
}
|
||||
|
||||
/* 4. DARK MODE OVERRIDES */
|
||||
.dark {
|
||||
/* shadcn dark vars only — no dark pastel */
|
||||
}
|
||||
|
||||
/* 5. TAILWIND BRIDGE (@theme inline) */
|
||||
@theme inline {
|
||||
/* font */
|
||||
--font-sans: 'Geist Variable', sans-serif;
|
||||
|
||||
/* shadcn token bridge (existing) */
|
||||
--color-background: var(--background);
|
||||
--color-primary: var(--primary);
|
||||
/* ... */
|
||||
|
||||
/* category token bridge (new) */
|
||||
--color-income: var(--color-income);
|
||||
--color-bill: var(--color-bill);
|
||||
/* ... */
|
||||
|
||||
/* radius scale (existing) */
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
/* ... */
|
||||
}
|
||||
|
||||
/* 6. BASE STYLES */
|
||||
@layer base {
|
||||
* { @apply border-border outline-ring/50; }
|
||||
body { @apply bg-background text-foreground; }
|
||||
html { @apply font-sans; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure: Design System
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
index.css ← SINGLE SOURCE: all tokens, @theme inline, base
|
||||
lib/
|
||||
utils.ts ← cn() (keep as-is)
|
||||
colors.ts ← NEW: getCategoryColor() for Recharts, CATEGORY_TYPES map
|
||||
components/
|
||||
ui/ ← shadcn primitives (patchable with CVA variants)
|
||||
button.tsx ← Add: pastel variant
|
||||
badge.tsx ← Add: category type variants via CVA
|
||||
card.tsx ← Keep as-is (className override is sufficient)
|
||||
input.tsx ← May need focus ring color patch
|
||||
...
|
||||
category-badge.tsx ← NEW: domain badge using badge.tsx + categoryVariants
|
||||
stat-card.tsx ← NEW: financial metric card
|
||||
progress-bar.tsx ← NEW: budget vs actual with color coding
|
||||
page-header.tsx ← NEW: consistent page header with title + actions slot
|
||||
empty-state.tsx ← NEW: consistent empty state for lists
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scalability Considerations
|
||||
|
||||
| Concern | Current scope | Future (theming milestone) |
|
||||
|---------|--------------|---------------------------|
|
||||
| Color tokens | Pastel light mode only | Add `data-theme="ocean"` etc. on `<html>`, swap `:root` vars |
|
||||
| Dark mode | Neutral dark (existing) | Pastel dark values in `.dark {}` |
|
||||
| Component variants | Per-category CVA | Per-theme variant maps |
|
||||
| Chart colors | CSS variable lookup | Same — already theme-aware |
|
||||
|
||||
The CSS variable architecture supports future theming without structural change. When custom themes are added, only `:root` values change; components need zero modification.
|
||||
|
||||
---
|
||||
|
||||
## Build Order: Foundation to Pages
|
||||
|
||||
This is the correct sequence because each layer depends on the previous:
|
||||
|
||||
**Phase A: Token foundation**
|
||||
1. Define pastel palette in `index.css` `:root` (raw oklch values)
|
||||
2. Define category semantic tokens in `index.css`
|
||||
3. Add category tokens to `@theme inline` bridge
|
||||
4. Define pastel overrides for shadcn semantic tokens (background, primary, etc.)
|
||||
|
||||
**Phase B: Primitive polish**
|
||||
5. Patch `button.tsx` — add `pastel` variant, adjust default hover states
|
||||
6. Patch `badge.tsx` — add category type variants
|
||||
7. Patch `input.tsx` — ensure focus ring uses `--ring` pastel value
|
||||
8. Create `lib/colors.ts` — getCategoryColor() and category type constants
|
||||
|
||||
**Phase C: Domain components**
|
||||
9. Create `category-badge.tsx` — wraps badge with category semantics
|
||||
10. Create `stat-card.tsx` — financial metric display card
|
||||
11. Create `progress-bar.tsx` — budget vs actual visual
|
||||
12. Create `page-header.tsx` — consistent header slot
|
||||
13. Create `empty-state.tsx` — consistent empty list state
|
||||
|
||||
**Phase D: Page-by-page polish**
|
||||
14. Login + Register pages — full branded treatment (no generic shadcn defaults)
|
||||
15. Dashboard — replace hardcoded color strings with category variants, add stat cards
|
||||
16. Categories page — category badge integration, polished CRUD UI
|
||||
17. Settings page — form layout, preference toggles
|
||||
|
||||
**Phase E: Charts**
|
||||
18. ExpenseBreakdown — donut chart with CSS variable colors
|
||||
19. FinancialOverview chart — bar chart with category palette
|
||||
20. AvailableBalance — progress indicator
|
||||
|
||||
Build order rationale: Pages cannot be polished until domain components exist. Domain components cannot use consistent tokens until the token layer is established. Charts come last because they depend on both the token system and the data components being stable.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- Direct inspection of `frontend/src/index.css` — confirms Tailwind 4 (`@import "tailwindcss"`, `@theme inline`) and oklch color space already in use
|
||||
- Direct inspection of `frontend/components.json` — confirms shadcn/ui radix-nova style, `cssVariables: true`, Tailwind 4 CSS-only config (no `tailwind.config.ts`)
|
||||
- Direct inspection of `frontend/src/components/ui/button.tsx` — confirms CVA pattern with `class-variance-authority` already in use
|
||||
- Direct inspection of `frontend/src/components/FinancialOverview.tsx` — identifies hardcoded color anti-pattern (`bg-sky-50`, `bg-emerald-50`) that needs migration
|
||||
- `.planning/codebase/STACK.md` — confirms Tailwind CSS 4.2.1, shadcn/ui 4.0.0, CVA 0.7.1
|
||||
- `.planning/PROJECT.md` — confirms pastel spreadsheet aesthetic goal, CSS variable customization approach, desktop-first target
|
||||
156
.planning/research/FEATURES.md
Normal file
156
.planning/research/FEATURES.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Feature Landscape — UI/UX Polish
|
||||
|
||||
**Domain:** Personal finance dashboard (pastel spreadsheet aesthetic)
|
||||
**Researched:** 2026-03-11
|
||||
**Milestone scope:** Existing backend is complete. This pass is purely about making what exists look and feel premium.
|
||||
|
||||
---
|
||||
|
||||
## What "Premium" Means for This App
|
||||
|
||||
The stated vision is "opening the app should feel like opening a beautifully designed personal spreadsheet." That is a specific aesthetic contract: data-dense but not cluttered, color-coded but soft, functional but delightful. A premium personal finance tool is not a banking dashboard (dark, heavy, corporate) — it is closer to Notion or Linear: calm, spacious, consistent, with color that communicates meaning rather than decoration.
|
||||
|
||||
The gap today: The CSS variables are pure neutral (zero chroma in all OKLCH values), the card headers have correct pastel gradient classes but no actual pastel primary/accent colors, the login screen is a plain white card on a plain white background, and the sidebar has no brand identity. Everything works but nothing _looks_ intentional.
|
||||
|
||||
---
|
||||
|
||||
## Table Stakes
|
||||
|
||||
Features users expect. Missing = product feels unfinished.
|
||||
|
||||
| Feature | Why Expected | Current State | Complexity | Notes |
|
||||
|---------|--------------|---------------|------------|-------|
|
||||
| **Pastel color system in CSS variables** | The entire visual identity depends on it — currently all chroma: 0 (neutral grey) | Missing — `--primary`, `--accent`, `--chart-*` use zero-chroma values | Low | Single change in `index.css` unlocks color across every shadcn component. The card header gradient classes are already correct; they just have no color to work with. |
|
||||
| **Login/Register: branded background** | Auth screen is the first impression. White card on white screen communicates nothing | Just `bg-background` (white) | Low | Add a soft pastel gradient or subtle pattern to the `min-h-screen` wrapper |
|
||||
| **Login/Register: app logo or wordmark** | Users expect a visual identity at the entry point | `CardDescription` shows `app.title` as plain text | Low | Even a styled typographic treatment works without designing an icon |
|
||||
| **Sidebar: visual brand identity** | Sidebar is always visible — it anchors the whole app's look | Plain white sidebar, `h2` text for title, no color differentiation | Low–Med | Pastel sidebar background, heavier app name treatment |
|
||||
| **Active nav item: clear visual indicator** | Users need to know where they are. shadcn `isActive` prop exists but needs styled colors | `isActive` is wired, unstyled | Low | CSS variable for sidebar-primary needs a real color |
|
||||
| **Card hover states on inline-editable rows** | The edit trigger is `hover:bg-muted` — with no muted color it's invisible | No visible hover feedback | Low | Requires a non-neutral `--muted` value |
|
||||
| **Loading state: spinner/pulse on form submission** | Buttons say "disabled" during async calls but give no visual motion feedback | `disabled` only | Low | Add Tailwind `animate-pulse` or a lucide `Loader2` spinner inside submit buttons |
|
||||
| **Page-level loading skeleton for dashboard** | Dashboard has `<Skeleton>` calls but skeleton is unstyled (neutral grey) | Skeleton exists, no pastel color | Low | Style skeletons to match section colors |
|
||||
| **Empty state: first-time experience** | The `t('dashboard.noBudgets')` card is just grey text. First-time users need direction | Bare `text-muted-foreground` text | Low–Med | Illustrative empty state with a CTA to create the first budget |
|
||||
| **Category empty state on CategoriesPage** | If no categories exist (possible state), nothing renders — `grouped.filter` could produce empty array | Returns silently empty | Low | Add empty state card with create CTA |
|
||||
| **Error feedback on form failures** | Auth errors show raw error strings; no visual differentiation from normal text | `text-destructive` paragraph, but destructive color needs styling | Low | Pair with error icon and styled alert block |
|
||||
| **Consistent section headers** | Every dashboard card uses a slightly different gradient (sky-to-indigo, blue-to-indigo, pink-to-rose, etc.) — good start but the overall palette looks fragmented | Each card picks ad-hoc gradient colors | Low | Align gradients to a single palette family derived from the pastel color system |
|
||||
| **Typography hierarchy** | Dashboard page has no visual hierarchy — all cards look equal weight. The `FinancialOverview` and `AvailableBalance` should feel like the hero items | All cards same shadow/border/weight | Low–Med | Use subtle elevation (shadow-sm vs shadow) or card size differences to establish hierarchy |
|
||||
| **Positive/negative amount coloring** | `VariableExpenses` colors negative remaining red via `text-destructive` — good. `FinancialOverview` and `BillsTracker` do not | Inconsistent | Low | Extend the pattern to all currency display: negative = destructive, over-budget = warning amber |
|
||||
| **Responsive sidebar: collapsible on smaller screens** | SidebarProvider supports it, but no toggle button is present in the layout | Not surfaced | Low | Add hamburger/collapse trigger to SidebarInset header area |
|
||||
|
||||
---
|
||||
|
||||
## Differentiators
|
||||
|
||||
Features that are not expected but elevate the experience from "usable" to "delightful."
|
||||
|
||||
| Feature | Value Proposition | Current State | Complexity | Notes |
|
||||
|---------|-------------------|---------------|------------|-------|
|
||||
| **Donut chart center label shows month + year** | The `AvailableBalance` donut already has an amount in the center. Adding context (month name) makes it more meaningful at a glance | Only the amount | Low | Render `budget.name` or derived month/year below the amount |
|
||||
| **Budget health indicator on header** | A small color-coded badge next to the budget selector (green = on track, amber = tight, red = over) derived from `totals.available` vs total income | Not present | Low–Med | Purely computed from existing data, no backend change |
|
||||
| **Inline edit: visual affordance (pencil icon on hover)** | Current inline edit is discoverable only by hovering and noticing the background change. A pencil icon makes it explicit | Background hint only | Low | Add a `lucide-react` `Pencil` icon that fades in on row hover |
|
||||
| **Save confirmation: row flash on successful inline edit** | After blur/Enter saves a value, nothing acknowledges the save. A brief green background flash (100ms) confirms the action succeeded | No feedback | Low–Med | CSS `transition` + toggled class; requires tracking save success in row state |
|
||||
| **Category creation inside budget item add flow** | Users currently must leave the dashboard, go to Categories, create a category, then return. Linking category management directly from a "add item" action would reduce friction | No cross-page flow | High | Requires dialog-within-dialog or slide-over; warrants its own milestone item |
|
||||
| **Month navigator: prev/next arrows beside budget selector** | Instead of only a dropdown, allow quick sequential navigation through budgets. Most months are adjacent | Dropdown only | Med | Would need sorting budgets by date and prev/next pointer logic |
|
||||
| **Chart tooltips: formatted with currency** | Recharts `<Tooltip />` renders raw numbers. Formatting with the budget's currency makes tooltips professional | Raw number in tooltip | Low | Custom `Tooltip` formatter using the existing `formatCurrency` util |
|
||||
| **Progress bars in BillsTracker for actual vs budget** | Seeing 85% of a bill paid vs just raw numbers adds spatial meaning | Tables only | Med | Bar per row using `actual/budgeted` ratio with color based on threshold |
|
||||
| **Animated number transitions** | When inline edits are saved and totals recompute, numbers jump. A short count animation (200ms) makes the update feel responsive | Hard jump | Med | Use a lightweight counter animation hook; only applies to summary numbers in FinancialOverview and AvailableBalance |
|
||||
| **Savings/Investments: visual progress toward goal** | Savings and investment items have both budgeted and actual amounts. A thin progress arc per item communicates progress toward monthly target | Table rows only | Med | Small inline arc per row using SVG or Recharts |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Features
|
||||
|
||||
Things to explicitly NOT build during this polish pass.
|
||||
|
||||
| Anti-Feature | Why Avoid | What to Do Instead |
|
||||
|--------------|-----------|-------------------|
|
||||
| **Dark mode toggle** | CSS variables for dark are already in `index.css` but the pastel system must be defined first. Dark pastel is a distinct design problem. Adding a toggle now couples two unfinished systems. | Get the light mode color system right first; dark mode in a future milestone |
|
||||
| **Custom color picker / theme selector** | PROJECT.md calls this out explicitly as out of scope. Complex state with no current value. | The color tokens are centralized — theme variants can be added later cleanly |
|
||||
| **Animated page transitions** | Route-level enter/exit animations (Framer Motion) add JS weight and complexity for marginal benefit in a data-first app | Keep transitions limited to within-page state changes (inline edits, loading states) |
|
||||
| **Toast notifications for every action** | Inline save feedback (row flash) is more contextual. Global toasts for every data save are noisy in a dashboard that supports rapid sequential edits | Use toast only for errors and destructive actions (delete), not saves |
|
||||
| **Drag-to-reorder categories on the dashboard** | `sort_order` exists on categories but reordering on the dashboard itself is a UX scope expansion. The Categories page is the right place for sort management | Accept keyboard-ordered sort for this milestone |
|
||||
| **Charts outside of existing sections** | Do not add new chart types (e.g., multi-month trend line, sparklines per category) in this pass. The goal is polishing existing charts, not adding new data views | Polish the existing Pie, Donut, and BarChart; trend analysis is a future milestone |
|
||||
| **Onboarding wizard / guided tour** | First-run experience improvements are valuable but a multi-step wizard is high complexity for a single-user self-hosted app | An improved empty state with a clear CTA is sufficient |
|
||||
| **Keyboard shortcuts / command palette** | Power-user feature that belongs after the visual foundation is established | Out of scope for this milestone |
|
||||
|
||||
---
|
||||
|
||||
## Feature Dependencies
|
||||
|
||||
```
|
||||
Pastel CSS variable system
|
||||
→ active nav item colors (sidebar-primary needs chroma)
|
||||
→ card hover states (muted needs chroma)
|
||||
→ skeleton background color
|
||||
→ button loading spinners (match primary color)
|
||||
→ progress bars in BillsTracker (use existing chart color tokens)
|
||||
→ budget health badge colors
|
||||
|
||||
Login branded background
|
||||
→ requires background to have a color, which needs the pastel system
|
||||
|
||||
Inline edit save confirmation (row flash)
|
||||
→ requires inline edit mechanism (already exists)
|
||||
→ requires knowing save succeeded (currently onSave returns a Promise — can use resolution)
|
||||
|
||||
Chart tooltip currency formatting
|
||||
→ requires access to budget.currency (already in component scope)
|
||||
→ no other dependencies
|
||||
|
||||
Month navigator (prev/next)
|
||||
→ requires budget list sorted by date
|
||||
→ requires selectBudget to be called (already available in useBudgets hook)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MVP Recommendation for This Polish Milestone
|
||||
|
||||
These should be done in order because earlier items unlock later items visually.
|
||||
|
||||
**First (foundation — everything else depends on it):**
|
||||
1. Redefine `--primary`, `--accent`, `--muted`, `--sidebar`, `--chart-1` through `--chart-5` in `index.css` with actual pastel OKLCH values. This single change transforms the entire app.
|
||||
2. Apply branded login/register background — a soft pastel gradient wrapping the card.
|
||||
|
||||
**Second (structural polish — visible on every page load):**
|
||||
3. Sidebar brand treatment — pastel sidebar background, heavier app name, colored active nav.
|
||||
4. Typography hierarchy on dashboard — make FinancialOverview + AvailableBalance visually the hero row.
|
||||
5. Consistent card section header palette — unify the gradient choices across all dashboard cards.
|
||||
|
||||
**Third (interaction quality):**
|
||||
6. Loading spinners inside submit buttons (login, save, budget create).
|
||||
7. Inline edit affordance — pencil icon on row hover.
|
||||
8. Save confirmation flash on inline edit success.
|
||||
9. Positive/negative amount coloring applied consistently across all tables.
|
||||
|
||||
**Fourth (completeness — guards against "unfinished" feeling):**
|
||||
10. Empty state for first-time dashboard (no budgets yet).
|
||||
11. Empty state for CategoriesPage.
|
||||
12. Chart tooltips formatted with currency.
|
||||
13. Collapsible sidebar toggle for smaller screens.
|
||||
|
||||
**Defer:**
|
||||
- Progress bars in BillsTracker: useful but higher complexity; tackle if time allows.
|
||||
- Animated number transitions: medium complexity, medium payoff.
|
||||
- Category creation inline from dashboard: high complexity, belongs in a future milestone.
|
||||
- Month navigator: medium complexity; polish-pass bonus if foundation is solid.
|
||||
|
||||
---
|
||||
|
||||
## Confidence Assessment
|
||||
|
||||
| Area | Confidence | Notes |
|
||||
|------|------------|-------|
|
||||
| Gap analysis (what's missing) | HIGH | Based on direct code review — all findings are grounded in the actual source |
|
||||
| Table stakes categorization | HIGH | Standard UI/UX craft, no library-specific claims |
|
||||
| Differentiators | MEDIUM | Based on domain experience with personal finance tools; no external sources were available during this research session |
|
||||
| Anti-features | HIGH | Grounded in PROJECT.md constraints and direct complexity analysis |
|
||||
|
||||
**Note on research method:** WebSearch and Context7 were not available in this session. All findings are derived from direct codebase analysis (all frontend source files read) and domain knowledge of finance dashboard UX patterns. The gap analysis is HIGH confidence because it is based on code inspection. The "premium differentiators" section carries MEDIUM confidence because it reflects design judgment rather than verified external benchmarks. Recommend treating differentiators as starting proposals for the roadmap rather than firm requirements.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- Direct review: all files under `/frontend/src/` (pages, components, hooks, App.tsx, index.css)
|
||||
- Project constraints: `.planning/PROJECT.md`
|
||||
- Design intent: `CLAUDE.md` architecture notes
|
||||
280
.planning/research/PITFALLS.md
Normal file
280
.planning/research/PITFALLS.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# Domain Pitfalls: UI Polish for SimpleFinanceDash
|
||||
|
||||
**Domain:** React + shadcn/ui frontend overhaul — pastel design system on an existing functional app
|
||||
**Researched:** 2026-03-11
|
||||
**Confidence:** HIGH (grounded in direct codebase inspection)
|
||||
|
||||
---
|
||||
|
||||
## Critical Pitfalls
|
||||
|
||||
Mistakes that cause rewrites, visual regressions across the whole app, or consistency breakdown.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 1: Hardcoded Color Values Bypassing the Token System
|
||||
|
||||
**What goes wrong:** The codebase already mixes two color approaches. Some components use shadcn CSS variable tokens (`bg-muted`, `text-destructive`, `hover:bg-muted`) while others hardcode Tailwind palette values (`bg-emerald-50`, `bg-violet-100 text-violet-800`, `fill="#fcd34d"`, `#93c5fd`, `#f9a8d4`). If the pastel palette is defined purely as CSS variable tokens in `index.css` but chart fills and badge color classes remain as inline hex strings and raw Tailwind utilities, changing the theme or adjusting a single color requires hunting down every call site.
|
||||
|
||||
**Why it happens:** shadcn components use CSS variables natively. But Recharts does not — `fill` props require string hex or named colors. Developers add those inline and then replicate the same approach in badge class strings, gradient headers, and row highlights.
|
||||
|
||||
**Consequences:**
|
||||
- A "pastel blue" may be `#93c5fd` in one chart, `blue-300` in a gradient, and `--chart-1` in another chart. They look similar but are not the same.
|
||||
- Adjusting saturation for accessibility requires touching 10+ places instead of 1.
|
||||
- Dark mode (even if not this milestone) is impossible to add cleanly later.
|
||||
|
||||
**Detection warning signs:**
|
||||
- `grep` finds hex color strings (`#`) in component `.tsx` files
|
||||
- Tailwind color classes like `bg-pink-50`, `text-emerald-800` in component files alongside `bg-card`, `text-muted-foreground` in the same codebase
|
||||
- `PASTEL_COLORS` arrays defined per-component (`AvailableBalance.tsx` and `ExpenseBreakdown.tsx` each define their own separate array with partially overlapping values)
|
||||
|
||||
**Prevention:**
|
||||
1. Define the full pastel palette as CSS variables in `index.css` — one variable per semantic purpose: `--color-income`, `--color-bills`, `--color-expenses`, etc.
|
||||
2. Map those variables into Tailwind's `@theme inline` block so `bg-income`, `text-bills` etc. work as utility classes.
|
||||
3. For Recharts fills, create a single exported constant array that references the CSS variable resolved values (use `getComputedStyle` or a constant palette object that is the single source of truth).
|
||||
4. Replace per-component `PASTEL_COLORS` arrays with imports from a central `lib/palette.ts`.
|
||||
|
||||
**Phase:** Must be addressed in Phase 1 (design token foundation) before touching any components. Fixing it mid-polish causes re-work.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 2: Overriding shadcn Components by Editing Their Source Files
|
||||
|
||||
**What goes wrong:** shadcn/ui is not a dependency — the components live in `src/components/ui/`. When developers want to change the default appearance of `Button`, `Card`, `Input`, etc., the temptation is to edit those source files directly (add default className values, change variant definitions, etc.).
|
||||
|
||||
**Why it happens:** It feels like the "right" place — the code is right there, you own it, and `className` overrides in every call site feel verbose.
|
||||
|
||||
**Consequences:**
|
||||
- If `shadcn add` is run later to add a new component or update an existing one, it overwrites your customizations or creates merge conflicts.
|
||||
- The diff between the upstream shadcn component and your version becomes invisible — future developers don't know what was intentionally changed vs. what is default shadcn behavior.
|
||||
- You lose the ability to reference shadcn docs accurately.
|
||||
|
||||
**Detection warning signs:**
|
||||
- Any `className` default changed in `src/components/ui/*.tsx` files
|
||||
- Variant maps in `button.tsx`, `badge.tsx`, `card.tsx` expanded beyond the shadcn defaults
|
||||
|
||||
**Prevention:**
|
||||
- Customize appearance exclusively through CSS variables in `index.css`. shadcn components read `--primary`, `--card`, `--border`, `--radius` etc. — those are the intended extension points.
|
||||
- For structural changes (adding a new button variant, a custom card subcomponent), create wrapper components like `src/components/ui/pastel-card.tsx` that compose shadcn primitives rather than modifying them.
|
||||
- Treat `src/components/ui/` as vendor code. Only modify it if the change would be appropriate to upstream.
|
||||
|
||||
**Phase:** Establish this rule before any component work begins. A short note in `CLAUDE.md` prevents accidental violations.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 3: Duplicated InlineEditRow Logic Diverging During Polish
|
||||
|
||||
**What goes wrong:** `BillsTracker.tsx`, `VariableExpenses.tsx`, and `DebtTracker.tsx` all contain a local `InlineEditRow` function with identical logic and structure. The visual polish work will need to change the edit affordance (hover state, active state, input styling, focus ring) in all three. If they remain separate, each will be polished independently and end up slightly different.
|
||||
|
||||
**Why it happens:** The component was created inline as a local helper and never extracted. Since each file is self-contained it doesn't feel wrong — until the polish pass.
|
||||
|
||||
**Consequences:**
|
||||
- Three components get slightly different hover colors, different input widths, different transition timings.
|
||||
- A bug fix or interaction change (e.g., adding an ESC key handler or optimistic update) must be applied three times.
|
||||
- Reviewers approve them one at a time and miss inconsistencies.
|
||||
|
||||
**Detection warning signs:**
|
||||
- `grep -r "InlineEditRow"` returns multiple files
|
||||
- Visual comparison of Bills, Variable Expenses, and Debt tables reveals small inconsistencies in edit behavior
|
||||
|
||||
**Prevention:**
|
||||
- Extract `InlineEditRow` to `src/components/InlineEditCell.tsx` before the polish phase begins.
|
||||
- Unify props interface; handle all three use cases (bills, variable expenses, debts are all "click to edit actual amount").
|
||||
- Polish the one component once.
|
||||
|
||||
**Phase:** Phase 1 or early Phase 2, before any visual work on those table components.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 4: Chart Colors Not Connected to the Semantic Color System
|
||||
|
||||
**What goes wrong:** Recharts charts (`PieChart` in `AvailableBalance`, `PieChart` in `ExpenseBreakdown`, `BarChart` in `VariableExpenses`) all use hardcoded hex palettes. The donut chart in `AvailableBalance` slices map to budget categories (bills, expenses, debts, savings, investments). If the color for "debts" in the donut is different from the color for "Debts" row in the `FinancialOverview` table, the user sees two different colors for the same concept on the same screen.
|
||||
|
||||
**Why it happens:** The table rows use Tailwind classes (`bg-red-50`) while the chart uses an index-based array (`PASTEL_COLORS[index]`). There is no mapping from category type to a single canonical color.
|
||||
|
||||
**Consequences:**
|
||||
- Cognitive load: user cannot use color as a navigation cue across widgets.
|
||||
- "Bills" is blue in the overview table header gradient, blue-300 in one chart, and whatever position it lands at in the pie.
|
||||
- The donut in `AvailableBalance` has no legend — users must already be confused about which slice is which.
|
||||
|
||||
**Detection warning signs:**
|
||||
- Side-by-side view of `AvailableBalance` donut and `FinancialOverview` table with different colors for the same categories
|
||||
- `PASTEL_COLORS` arrays that do not reference a category-to-color map
|
||||
|
||||
**Prevention:**
|
||||
- Define a `CATEGORY_COLORS` map in `lib/palette.ts`: `{ income: '#...', bill: '#...', variable_expense: '#...', debt: '#...', saving: '#...', investment: '#...' }`
|
||||
- All chart `fill` props, all badge class strings, all row background classes derive from this map.
|
||||
- Do not use index-based color arrays for category data — always key by category type.
|
||||
|
||||
**Phase:** Phase 1 (design token foundation). Required before polishing any chart component.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 5: Polish Feels "Off" Because Layout Spacing Is Inconsistent, Not Just Colors
|
||||
|
||||
**What goes wrong:** A common mistake in UI polish projects is focusing on colors and typography while leaving spacing inconsistent. Looking at the current layout: `DashboardPage` uses `p-6` and `gap-6`. `CardContent` sometimes uses `p-0` (for tables), sometimes `pt-4`, sometimes `pt-6`. `CardHeader` has gradient backgrounds but no consistent padding treatment relative to card content. Auth forms (`LoginPage`) have no background treatment — just `bg-background` which is white.
|
||||
|
||||
**Why it happens:** Spacing decisions were made locally per component during initial development. The Tailwind utility model makes it easy to add `p-4` or `p-6` without thinking about global rhythm.
|
||||
|
||||
**Consequences:**
|
||||
- Even if colors are right, the layout feels amateur because card headers have different internal padding, section gaps vary, and the auth page has no visual brand presence.
|
||||
- Dashboard looks like a collection of separate components rather than a unified screen.
|
||||
|
||||
**Detection warning signs:**
|
||||
- `CardHeader` padding varies across components
|
||||
- `CardContent` uses `p-0` in some cards and `pt-4`/`pt-6` in others without a clear rule
|
||||
- Auth pages have no background color or brand element beyond the card itself
|
||||
- Sidebar lacks any visual weight or brand mark beyond plain text
|
||||
|
||||
**Prevention:**
|
||||
- Define a spacing scale decision early: what is the standard gap between dashboard sections? Between card header and content? Between form fields?
|
||||
- Encode those decisions in either Tailwind config (spacing tokens) or a layout component.
|
||||
- Treat the auth pages as a first-class design surface — they are the first screen users see.
|
||||
|
||||
**Phase:** Phase 2 (layout and structure), before polishing individual components.
|
||||
|
||||
---
|
||||
|
||||
## Moderate Pitfalls
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 6: i18n Keys Missing for New UI Text Introduced During Polish
|
||||
|
||||
**What goes wrong:** The polish phase will add new UI elements — empty states, tooltips, chart legends, aria labels, section descriptions, helper text. Each piece of text needs both `en.json` and `de.json` entries. During fast iteration it is easy to hardcode English strings directly in JSX.
|
||||
|
||||
**Why it happens:** Adding `t('some.new.key')` requires updating two JSON files. Under time pressure developers skip it and plan to "add translations later."
|
||||
|
||||
**Consequences:**
|
||||
- German users see raw English strings or key names like `dashboard.expenseTooltip`
|
||||
- Text added without translation keys becomes invisible debt — only discovered when someone switches the language
|
||||
|
||||
**Detection warning signs:**
|
||||
- String literals in JSX that are not wrapped in `t()`: `<p>No transactions yet</p>`
|
||||
- `t()` calls referencing keys that exist in `en.json` but not `de.json`
|
||||
|
||||
**Prevention:**
|
||||
- As a discipline: never commit a new UI string without both translation files updated.
|
||||
- At the start of the milestone, run a diff between `en.json` and `de.json` key sets to verify they're in sync.
|
||||
- For every new UI element added during polish, add the translation key immediately, even if the German translation is a placeholder.
|
||||
|
||||
**Phase:** Ongoing across all phases. Establish the discipline at the start.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 7: Recharts Tooltip and Legend Styling Not Customized — Default Gray Box Breaks Pastel Aesthetic
|
||||
|
||||
**What goes wrong:** Recharts renders its `<Tooltip>` as a white box with gray border by default. Its `<Legend>` uses its own internal color swatches. Neither reads from CSS variables. The `VariableExpenses` chart has `<Tooltip />` and `<Legend />` without any custom styling. The `ExpenseBreakdown` pie chart uses inline label rendering with `{name} {percent}%` which overflows on small slices and has no connection to the design system.
|
||||
|
||||
**Why it happens:** Recharts customization requires passing `content` prop with a custom component, which feels like extra work when the default "works."
|
||||
|
||||
**Consequences:**
|
||||
- Charts look designed but tooltips and legends look like placeholder UI — this is one of the most noticeable quality signals in data dashboards.
|
||||
- Pie chart labels on small slices (`ExpenseBreakdown`) collide and become unreadable.
|
||||
|
||||
**Prevention:**
|
||||
- Create a shared `ChartTooltip` component that uses the design system's card/border/text tokens.
|
||||
- For pie charts, remove inline labels and use a separate legend or tooltip instead.
|
||||
- The `shadcn/ui` chart component (`src/components/ui/chart.tsx`) wraps Recharts and provides `ChartTooltipContent` — check if it covers the use case before building custom.
|
||||
|
||||
**Phase:** Phase 3 (chart polish), but the decision to use `chart.tsx` vs. raw Recharts should be made in Phase 1.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 8: The "No Data" State Is Not Designed
|
||||
|
||||
**What goes wrong:** Several components conditionally return `null` when data is absent (`ExpenseBreakdown` returns `null` if no expenses, `DebtTracker` returns `null` if no debts). This means sections of the dashboard silently disappear rather than showing a welcoming empty state. On first use (no budget created yet), the whole dashboard shows only a budget selector and a card saying "No budgets yet."
|
||||
|
||||
**Why it happens:** Empty states are not considered until user testing — during initial development, returning `null` is the simplest valid behavior.
|
||||
|
||||
**Consequences:**
|
||||
- First-run experience feels broken — user sees a mostly-empty screen and doesn't know what to do.
|
||||
- A polished product feels abandoned when sections disappear instead of explaining why.
|
||||
|
||||
**Prevention:**
|
||||
- Design empty states for each major section during the polish phase.
|
||||
- At minimum: an icon, a short explanation, and a call-to-action where appropriate.
|
||||
- Consider whether conditional `null` returns are the right pattern or whether a section should always be present with an empty state.
|
||||
|
||||
**Phase:** Phase 2 or Phase 3. Must be addressed before considering any screen "polished."
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 9: Delete Operations Have No Confirmation — Silent Destructive Actions
|
||||
|
||||
**What goes wrong:** In `CategoriesPage`, the Delete button calls `handleDelete(cat.id)` directly with no confirmation dialog. Categories are reused across budget periods — deleting one could orphan or break historical budget data.
|
||||
|
||||
**Why it happens:** Confirmation dialogs feel like UI polish work but are actually a correctness concern. Easy to defer.
|
||||
|
||||
**Consequences:**
|
||||
- User accidentally deletes a category they use in 12 months of budgets.
|
||||
- The backend may or may not cascade delete budget items — either way, the user has no warning.
|
||||
|
||||
**Prevention:**
|
||||
- Add a confirmation step to all delete actions during the polish phase.
|
||||
- The `Dialog` component is already in use in `CategoriesPage` — the pattern is available.
|
||||
- Consider showing the impact ("This category is used in 3 budgets") as part of the confirmation.
|
||||
|
||||
**Phase:** Phase 3 (page polish), but should be flagged as a correctness concern, not just cosmetic.
|
||||
|
||||
---
|
||||
|
||||
## Minor Pitfalls
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 10: `formatCurrency` Hardcodes `de-DE` Locale Regardless of User Language Preference
|
||||
|
||||
**What goes wrong:** `lib/format.ts` uses `new Intl.NumberFormat('de-DE', ...)`. Users with the English locale preference will still see `1.234,56 €` (German decimal notation) instead of `1,234.56 €`.
|
||||
|
||||
**Detection:** Switch to English in settings, observe currency formatting in the dashboard.
|
||||
|
||||
**Prevention:** Pass the user's locale to `formatCurrency` or read it from the i18n context. The user's `preferred_locale` is available from the settings API.
|
||||
|
||||
**Phase:** Can be addressed during any phase where currency display is touched, but should be caught before final review.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 11: The Sidebar Has No Visual Hierarchy or Brand Identity
|
||||
|
||||
**What goes wrong:** `AppLayout.tsx` renders the sidebar with a plain text `<h2>Budget Dashboard</h2>` and no logo, icon, or brand color. The active nav item uses shadcn's default `isActive` styling which is just a background highlight. In the pastel design vision, the sidebar is a primary brand surface.
|
||||
|
||||
**Prevention:** Treat the sidebar header as a branding opportunity — a simple icon or wordmark with the pastel primary color makes the application feel intentional. Active nav items should use a pastel accent, not the default dark highlight.
|
||||
|
||||
**Phase:** Phase 2 (layout).
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 12: Budget Selector in the Dashboard Header Has No Visual Context
|
||||
|
||||
**What goes wrong:** The budget selector (`Select` component in `DashboardPage`) floats in the top-left with just a "Create Budget" button beside it. There is no visual framing — no page header, no date context, no quick-stat summary near the selector. Users can't tell at a glance which month they are viewing.
|
||||
|
||||
**Prevention:** The budget has a name (which presumably includes the month). Displaying it prominently with supporting context (date range, currency) as part of a proper page header would substantially improve orientation. This is a layout decision, not a data model change.
|
||||
|
||||
**Phase:** Phase 2 (layout).
|
||||
|
||||
---
|
||||
|
||||
## Phase-Specific Warnings
|
||||
|
||||
| Phase Topic | Likely Pitfall | Mitigation |
|
||||
|-------------|----------------|------------|
|
||||
| Design token foundation | Defining CSS variables but not connecting them to Recharts fills (Pitfall 1, 4) | Create `lib/palette.ts` as the single source of truth for all colors including chart fills |
|
||||
| Component extraction | Leaving InlineEditRow duplicated (Pitfall 3) | Extract before any polish touches the table components |
|
||||
| Auth page polish | Treating it as "just style the card" vs. a brand surface (Pitfall 5) | Design the full viewport, not just the card |
|
||||
| Chart polish | Default Recharts tooltip/legend styling (Pitfall 7) | Decide early whether to use `chart.tsx` wrapper or custom tooltip component |
|
||||
| Dashboard layout | Relying on component visibility returning `null` (Pitfall 8) | Design empty states for every conditional section |
|
||||
| Categories page polish | No delete confirmation (Pitfall 9) | Treat confirmation as a requirement, not a stretch |
|
||||
| Any new UI text | Missing i18n translations (Pitfall 6) | Two-JSON rule: never commit text without both `en.json` and `de.json` |
|
||||
| Currency display | Hardcoded `de-DE` locale in formatCurrency (Pitfall 10) | Pass user locale when touching any numeric display |
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- Direct inspection of `frontend/src/` codebase (HIGH confidence — grounded in actual code)
|
||||
- Observed patterns: `index.css` CSS variable definitions vs. inline hex in component files
|
||||
- Observed duplication: `InlineEditRow` in `BillsTracker.tsx`, `VariableExpenses.tsx`, `DebtTracker.tsx`
|
||||
- Observed color divergence: `PASTEL_COLORS` arrays in `AvailableBalance.tsx` and `ExpenseBreakdown.tsx` are different arrays
|
||||
- shadcn/ui design: components in `src/components/ui/` read from CSS variables — `--primary`, `--card`, `--border`, `--muted`, etc.
|
||||
- Recharts constraint: `fill`, `stroke` props accept strings only, no CSS variable resolution natively
|
||||
292
.planning/research/STACK.md
Normal file
292
.planning/research/STACK.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# Technology Stack
|
||||
|
||||
**Project:** SimpleFinanceDash — UI Polish Milestone
|
||||
**Researched:** 2026-03-11
|
||||
**Confidence:** HIGH (stack already locked; all versions confirmed from package.json and source files)
|
||||
|
||||
---
|
||||
|
||||
## Current Stack (Already Installed — Do Not Change)
|
||||
|
||||
| Technology | Version | Role |
|
||||
|------------|---------|------|
|
||||
| React | 19.2.0 | UI runtime |
|
||||
| TypeScript | 5.9.3 | Type safety |
|
||||
| Vite | 7.3.1 | Build tool |
|
||||
| Tailwind CSS | 4.2.1 | Utility styling |
|
||||
| shadcn/ui | 4.0.0 | Component library |
|
||||
| radix-ui | 1.4.3 | Headless primitives (single package, not individual `@radix-ui/*`) |
|
||||
| Recharts | 2.15.4 | Charts |
|
||||
| tw-animate-css | 1.4.0 | CSS animation utilities |
|
||||
| Geist Variable | 5.2.8 | Primary font (already loaded, set as `--font-sans`) |
|
||||
| lucide-react | 0.577.0 | Icon set |
|
||||
|
||||
**Critical context:** The project is on the new shadcn v4 architecture (single `radix-ui` package, `@theme inline` in CSS, `oklch` color space). All recommendations below are scoped to this exact setup.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Stack — UI Polish Layer
|
||||
|
||||
### 1. CSS Variable Strategy for Pastel Theme
|
||||
|
||||
**Technique: Replace oklch values in `:root` inside `index.css`**
|
||||
|
||||
The current `index.css` uses default shadcn black/white tokens:
|
||||
- `--background: oklch(1 0 0)` → pure white
|
||||
- `--primary: oklch(0.205 0 0)` → near-black
|
||||
- `--secondary: oklch(0.97 0 0)` → near-white gray
|
||||
|
||||
**Replace with pastel-tinted equivalents.** oklch is ideal for pastel work because the `C` (chroma) channel controls saturation independently of lightness. A pastel is high-L, low-C:
|
||||
|
||||
```
|
||||
Soft lavender background: oklch(0.97 0.015 280)
|
||||
Soft blue primary: oklch(0.55 0.12 250)
|
||||
Rose secondary: oklch(0.94 0.025 350)
|
||||
Amber accent: oklch(0.94 0.04 85)
|
||||
Soft green: oklch(0.94 0.04 145)
|
||||
```
|
||||
|
||||
**Finance category color tokens** (add as custom vars alongside shadcn defaults):
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Category semantic tokens — used in chart fills and row backgrounds */
|
||||
--color-income: oklch(0.88 0.08 145); /* soft green */
|
||||
--color-bill: oklch(0.88 0.07 250); /* soft blue */
|
||||
--color-expense: oklch(0.90 0.08 85); /* soft amber */
|
||||
--color-debt: oklch(0.90 0.08 15); /* soft red/rose */
|
||||
--color-saving: oklch(0.88 0.07 280); /* soft violet */
|
||||
--color-investment: oklch(0.90 0.07 320); /* soft pink */
|
||||
}
|
||||
```
|
||||
|
||||
Expose them in the `@theme inline` block so Tailwind generates utility classes (`bg-income`, `bg-bill`, etc.).
|
||||
|
||||
**Confidence: HIGH** — This is how shadcn v4 + Tailwind v4 theming works. The `@theme inline` bridging pattern is confirmed by the existing codebase structure.
|
||||
|
||||
---
|
||||
|
||||
### 2. shadcn/ui Component Customization Strategy
|
||||
|
||||
**Pattern: Variant extension via `class-variance-authority` (CVA) — already available**
|
||||
|
||||
Do not fork shadcn component source files unless necessary. Instead:
|
||||
|
||||
1. **CSS variable changes** cover 80% of polish (background, border, primary, radius).
|
||||
2. **className overrides at the call site** via `cn()` cover the remaining 20%.
|
||||
3. For components that need a recurring custom style (e.g., Card with always-pastel header), wrap in a thin local component: `PastelCard`, `StatCard`. This avoids touching `ui/card.tsx`.
|
||||
|
||||
**Radius token:** The current `--radius: 0.625rem` is fine. For a softer look, increase to `0.75rem` or `1rem`.
|
||||
|
||||
**Border softness:** Replace `--border: oklch(0.922 0 0)` with a tinted, slightly lower-opacity token like `oklch(0.88 0.02 250 / 60%)` for a barely-there border that reads as "pastel spreadsheet cell divider."
|
||||
|
||||
**Confidence: HIGH**
|
||||
|
||||
---
|
||||
|
||||
### 3. Chart Customization with Recharts + shadcn ChartContainer
|
||||
|
||||
The codebase already has shadcn's `ChartContainer` / `ChartTooltipContent` wrapper in `ui/chart.tsx`. This is the correct foundation.
|
||||
|
||||
**Current gap:** `ExpenseBreakdown.tsx` uses raw `<PieChart>` without `ChartContainer`. It should be migrated to use `ChartContainer` + `ChartConfig` so chart colors come from CSS variables rather than hardcoded hex strings.
|
||||
|
||||
**Pattern to follow for all charts:**
|
||||
|
||||
```typescript
|
||||
const chartConfig = {
|
||||
bills: { label: 'Bills', color: 'var(--color-bill)' },
|
||||
expenses: { label: 'Expenses', color: 'var(--color-expense)' },
|
||||
savings: { label: 'Savings', color: 'var(--color-saving)' },
|
||||
investments: { label: 'Investments', color: 'var(--color-investment)' },
|
||||
} satisfies ChartConfig
|
||||
```
|
||||
|
||||
Then use `fill="var(--color-bills)"` on Recharts `<Cell>` elements. The `ChartStyle` component injects these as scoped CSS vars so dark mode works automatically.
|
||||
|
||||
**Tooltip customization:** Use `<ChartTooltipContent>` with a `formatter` prop to render currency-formatted values. The existing `ChartTooltipContent` already handles the indicator dot pattern — just pass `formatter={(value) => formatCurrency(value, currency)}`.
|
||||
|
||||
**Recharts animation:** Recharts has built-in entrance animations on charts (`isAnimationActive={true}` is the default). The animation duration can be tuned via `animationDuration` (prop on `<Bar>`, `<Pie>`, `<Line>`). Set to `600`–`800ms` for a polished feel. Do NOT disable animations.
|
||||
|
||||
**Chart types in use / recommended:**
|
||||
- `PieChart` — expense breakdown (already exists, needs ChartContainer migration)
|
||||
- `BarChart` — budget vs actual comparison (recommended addition for FinancialOverview)
|
||||
- `RadialBarChart` — AvailableBalance progress indicator (recommended to replace plain number display)
|
||||
|
||||
**Confidence: HIGH** — Recharts 2.x API is stable and well-understood. The ChartContainer pattern is confirmed from the existing `ui/chart.tsx` source.
|
||||
|
||||
---
|
||||
|
||||
### 4. Animation and Transition Strategy
|
||||
|
||||
**Primary tool: `tw-animate-css` (already installed)**
|
||||
|
||||
`tw-animate-css` provides Tailwind v4-compatible utility classes wrapping Animate.css keyframes. Use it for:
|
||||
- Page entrance animations: `animate-fade-in` on dashboard sections
|
||||
- Dialog/sheet enters: `animate-in slide-in-from-bottom-4` (shadcn already applies these via `data-[state=open]` variants)
|
||||
- Loading skeletons: `animate-pulse` (Tailwind built-in, no extra library needed)
|
||||
|
||||
**Do NOT add Motion/Framer Motion for this milestone.** Reason: the interactions needed (inline edit toggle, form submission feedback, card entrance) are all achievable with CSS transitions and `tw-animate-css`. Framer Motion adds ~45KB to the bundle and its React 19 compatibility had rough edges in early 2025. Revisit for a future milestone if complex drag-and-drop or physics animations are needed.
|
||||
|
||||
**Inline edit transitions** (e.g., BillsTracker `InlineEditRow`): Use CSS `transition-all duration-150` on the toggle between display and input state. This is zero-dependency and already idiomatic in the codebase.
|
||||
|
||||
**Recommended micro-interaction patterns:**
|
||||
- Input focus: `focus-visible:ring-2 focus-visible:ring-primary/40` for a soft glow
|
||||
- Row hover: `hover:bg-muted/60 transition-colors duration-150`
|
||||
- Button press: shadcn Button already has `active:scale-[0.98]` equivalent via its CVA variants — verify in `ui/button.tsx`
|
||||
- Number updates (balance changing): CSS `@keyframes` count-up is overkill; a simple `transition-opacity` flash on value change via a `key` prop reset is sufficient
|
||||
|
||||
**Confidence: MEDIUM** — tw-animate-css is confirmed installed. Framer Motion React 19 compatibility claim is based on training data; the recommendation to skip it is defensive but well-justified on bundle size grounds alone.
|
||||
|
||||
---
|
||||
|
||||
### 5. Typography
|
||||
|
||||
**Current state:** Geist Variable is already installed (`@fontsource-variable/geist`) and set as `--font-sans`. This is the correct font for this project.
|
||||
|
||||
**Geist Variable characteristics:** Clean, geometric, excellent for data/numbers, neutral without being cold. Used by Vercel's dashboard products. Well-suited for finance UIs.
|
||||
|
||||
**Do NOT add a second typeface.** The spreadsheet aesthetic is served by a single well-chosen sans. A display font pairing would clash with the data-dense layout.
|
||||
|
||||
**Typography scale recommendations:**
|
||||
|
||||
```css
|
||||
/* Add to @theme inline */
|
||||
--font-mono: 'Geist Mono Variable', ui-monospace, monospace;
|
||||
```
|
||||
|
||||
Install `@fontsource-variable/geist-mono` alongside the existing font. Use `font-mono tabular-nums` on all currency amounts. The existing `ChartTooltipContent` already does this (`font-mono tabular-nums` class on values) — extend this pattern to table cells showing currency.
|
||||
|
||||
**Text hierarchy for finance:**
|
||||
- Card titles: `text-sm font-semibold tracking-wide text-muted-foreground uppercase` — reads as a spreadsheet column header
|
||||
- Primary numbers (balance): `text-3xl font-bold tabular-nums`
|
||||
- Secondary numbers (line items): `text-sm tabular-nums`
|
||||
- Labels: `text-xs text-muted-foreground`
|
||||
|
||||
**Confidence: HIGH** — Font is already installed. Geist Mono is the natural companion and from the same @fontsource-variable namespace.
|
||||
|
||||
---
|
||||
|
||||
### 6. Icon Usage (lucide-react)
|
||||
|
||||
**Already installed at 0.577.0.** lucide-react is the correct choice — it's the shadcn default and provides consistent stroke-width icons that match the component style.
|
||||
|
||||
**Pattern for finance icons:**
|
||||
|
||||
| Concept | Lucide Icon |
|
||||
|---------|-------------|
|
||||
| Income | `TrendingUp` |
|
||||
| Bills | `Receipt` |
|
||||
| Variable expenses | `ShoppingCart` |
|
||||
| Debt | `CreditCard` |
|
||||
| Savings | `PiggyBank` |
|
||||
| Investments | `BarChart2` |
|
||||
| Settings | `Settings2` |
|
||||
| Categories | `Tag` |
|
||||
| Budget period | `CalendarDays` |
|
||||
|
||||
Use `size={16}` consistently for inline icons, `size={20}` for standalone icon buttons.
|
||||
|
||||
**Confidence: HIGH**
|
||||
|
||||
---
|
||||
|
||||
### 7. Layout Pattern for Dashboard
|
||||
|
||||
The current `DashboardPage.tsx` uses a flat `flex flex-col gap-6 p-6` layout. For polish, the recommended evolution is:
|
||||
|
||||
**Header area:** Sticky top bar with budget selector + month navigation + key stat (available balance). Removes the selector from inline flow.
|
||||
|
||||
**Grid approach (keep existing):** The `lg:grid-cols-3` + `lg:col-span-2` pattern for FinancialOverview + AvailableBalance is correct. Continue using CSS Grid for two-column chart sections.
|
||||
|
||||
**Card anatomy standard:**
|
||||
- `CardHeader`: Always has a soft background gradient using two adjacent pastel tokens
|
||||
- `CardTitle`: Uses the uppercase label style (see Typography section)
|
||||
- `CardContent`: `p-0` for tables, `pt-4` for charts/other content
|
||||
|
||||
This pattern already exists in the codebase (BillsTracker, FinancialOverview headers use `bg-gradient-to-r from-blue-50 to-indigo-50`). The task is to make it consistent and driven by category-semantic tokens rather than hardcoded Tailwind color names.
|
||||
|
||||
**Do NOT use Tailwind's `bg-blue-50` directly.** Reason: when the CSS variable values are updated for theming, hardcoded `bg-blue-50` won't change. Use `bg-(--color-bill)/20` syntax to reference semantic tokens.
|
||||
|
||||
**Confidence: HIGH**
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
| Category | Recommended | Alternative | Why Not |
|
||||
|----------|-------------|-------------|---------|
|
||||
| Animation | tw-animate-css (installed) | Framer Motion | Bundle cost (~45KB), React 19 edge cases, overkill for this scope |
|
||||
| Animation | tw-animate-css (installed) | react-spring | Same — overkill, no benefit over CSS transitions for this UI |
|
||||
| Charts | Recharts (installed) | Victory / Nivo | Recharts is already embedded, ChartContainer wrapper is done, switching has zero upside |
|
||||
| Charts | Recharts (installed) | D3 directly | Too low-level; maintenance cost far exceeds polish benefit |
|
||||
| Fonts | Geist Variable (installed) | Inter | Geist is already there; Inter would be a downgrade in distinctiveness |
|
||||
| Fonts | Single font | Font pairing | Finance dashboards read better with single-family discipline |
|
||||
| Color format | oklch (current) | HSL / HEX | oklch is perceptually uniform, better for generating harmonious pastel palettes |
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Use
|
||||
|
||||
**Do NOT install:**
|
||||
|
||||
- `framer-motion` — React 19 had compatibility issues in early 2025; bundle cost not justified
|
||||
- `@radix-ui/*` individual packages — The project is on the new `radix-ui` unified package; mixing them causes version conflicts
|
||||
- `react-select` / custom dropdown libraries — shadcn's `Select` covers the need
|
||||
- A CSS-in-JS solution — Tailwind v4 + CSS variables is the correct pattern; CSS-in-JS would fight the build setup
|
||||
- A color picker library — custom themes are explicitly out of scope
|
||||
- `date-fns` or `dayjs` — only needed if date formatting becomes complex; standard `Intl.DateTimeFormat` is sufficient
|
||||
- `react-hook-form` — the forms in scope (login, budget creation) are simple enough for controlled inputs; adding a form library is premature
|
||||
|
||||
---
|
||||
|
||||
## Key Tailwind v4 Syntax Notes
|
||||
|
||||
**Important: Tailwind v4 changed syntax.** The codebase correctly uses the new forms but contributors must be aware:
|
||||
|
||||
| v3 syntax | v4 syntax |
|
||||
|-----------|-----------|
|
||||
| `bg-opacity-50` | `bg-black/50` or `oklch(.../50%)` |
|
||||
| `text-opacity-75` | `text-foreground/75` |
|
||||
| `bg-[--color-bill]` | `bg-(--color-bill)` (parentheses, not brackets) |
|
||||
| `@apply border-border` | Same (works in v4) |
|
||||
| Plugin-based config | `@theme inline` in CSS |
|
||||
|
||||
Use `bg-(--color-bill)/20` to reference a CSS variable with opacity. This is the correct v4 syntax.
|
||||
|
||||
**Confidence: HIGH** — Confirmed by reading the existing `index.css` which uses the `@theme inline` pattern correctly.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
No new packages are required for the core polish milestone. All tools are already installed.
|
||||
|
||||
**Optional — only if currency formatting needs locale support beyond what exists:**
|
||||
```bash
|
||||
# Nothing to add — Intl.NumberFormat is native
|
||||
```
|
||||
|
||||
**If Geist Mono is desired (recommended for tabular numbers):**
|
||||
```bash
|
||||
cd frontend && bun add @fontsource-variable/geist-mono
|
||||
```
|
||||
|
||||
Then in `index.css`:
|
||||
```css
|
||||
@import "@fontsource-variable/geist-mono";
|
||||
```
|
||||
And in `@theme inline`:
|
||||
```css
|
||||
--font-mono: 'Geist Mono Variable', ui-monospace, monospace;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- Confirmed from codebase: `/frontend/package.json`, `/frontend/src/index.css`, `/frontend/src/components/ui/chart.tsx`
|
||||
- shadcn v4 `@theme inline` + `oklch` color pattern: confirmed from existing `index.css` structure (HIGH confidence)
|
||||
- Tailwind v4 CSS variable reference syntax (`bg-(--var)`): confirmed from Tailwind v4 docs in training data, consistent with codebase patterns (HIGH confidence)
|
||||
- Recharts ChartContainer pattern: confirmed from existing `ui/chart.tsx` implementation (HIGH confidence)
|
||||
- tw-animate-css v4 compatibility: confirmed from import in `index.css` (`@import "tw-animate-css"`) (HIGH confidence)
|
||||
- Framer Motion React 19 compatibility concerns: training data (MEDIUM confidence — may have improved in later 2025 patch releases, but avoidance recommendation stands on bundle-size grounds alone)
|
||||
190
.planning/research/SUMMARY.md
Normal file
190
.planning/research/SUMMARY.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# Project Research Summary
|
||||
|
||||
**Project:** SimpleFinanceDash — UI Polish Milestone
|
||||
**Domain:** Personal finance dashboard (pastel spreadsheet aesthetic)
|
||||
**Researched:** 2026-03-11
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Executive Summary
|
||||
|
||||
SimpleFinanceDash is a functional, fully-deployed personal finance dashboard built on Go + React + shadcn/ui. The backend is complete. This milestone is a pure frontend polish pass: making an app that already works look and feel intentionally designed. The core problem is that the existing UI has all the structural scaffolding for a pastel theme — correct gradient classes on card headers, a Tailwind 4 `@theme inline` token bridge, and shadcn components that read from CSS variables — but the CSS variables themselves have zero chroma (neutral grey/white values). A single targeted change to `index.css` unlocks the entire visual identity across every shadcn component simultaneously.
|
||||
|
||||
The recommended approach is strictly layered: define the pastel token system first (Phase 1), then polish structural layout surfaces (Phase 2), then address interaction quality and empty states (Phase 3), then finalize charts (Phase 4). This order is non-negotiable because later phases depend on earlier ones — chart colors must reference the semantic token system, and component polish cannot happen consistently until shared components (InlineEditCell, StatCard, EmptyState) are extracted first. Skipping the token foundation and going component-by-component is the most common failure mode for this type of polish project.
|
||||
|
||||
The key risk is color fragmentation: the codebase already has three conflicting color systems (hardcoded hex in Recharts fills, raw Tailwind palette classes like `bg-emerald-50`, and the CSS variable token system). If the pastel token system is added as a fourth system without migrating the existing approaches, the app will look inconsistent regardless of effort spent. The mitigation is a `lib/palette.ts` file as the single source of truth, and a strict rule that no color value other than a CSS variable reference belongs in a component file.
|
||||
|
||||
---
|
||||
|
||||
## Key Findings
|
||||
|
||||
### Recommended Stack
|
||||
|
||||
The stack is already locked — no new dependencies are required for the core milestone. All tools are present: React 19 + TypeScript 5, Tailwind CSS 4.2.1, shadcn/ui 4.0.0 (on the new single `radix-ui` package architecture), Recharts 2.15.4, tw-animate-css, lucide-react, and Geist Variable font. The one optional addition is `@fontsource-variable/geist-mono` for tabular currency numbers (one `bun add` command).
|
||||
|
||||
The project is on the new shadcn v4 architecture which uses `@theme inline` in CSS and `oklch` color space. This is the correct foundation for a pastel palette — oklch's independent chroma channel (C) makes pastel generation straightforward: high L, low C values produce clean pastels without the perceptual distortion of HSL.
|
||||
|
||||
**Core technologies:**
|
||||
- `Tailwind CSS 4.2.1`: Utility styling via `@theme inline` — all tokens live in `index.css`, zero `tailwind.config.js` needed
|
||||
- `shadcn/ui 4.0.0`: Component library that reads 100% from CSS variables — theme changes flow through tokens automatically
|
||||
- `radix-ui 1.4.3`: Single unified package (not individual `@radix-ui/*`); mixing them causes version conflicts
|
||||
- `Recharts 2.15.4`: Charts via shadcn's `ChartContainer` wrapper; `fill` props require string values, not CSS variables — requires `getComputedStyle` or a `lib/palette.ts` constant map
|
||||
- `tw-animate-css 1.4.0`: Already imported in `index.css`; sufficient for all loading states and micro-interactions without Framer Motion
|
||||
- `lucide-react 0.577.0`: Icon library matching shadcn's stroke-width style; consistent `size={16}` / `size={20}` discipline needed
|
||||
- Geist Variable (already installed): Single typeface; do not add a second. Geist Mono companion is the only optional addition.
|
||||
|
||||
### Expected Features
|
||||
|
||||
The FEATURES.md analysis is based on direct codebase inspection and identifies a concrete, ordered list of gaps.
|
||||
|
||||
**Must have (table stakes):**
|
||||
- Pastel CSS variable system — all other polish depends on it; currently `--primary` and `--accent` have zero chroma
|
||||
- Branded login/register background — first impression is white card on white screen
|
||||
- App wordmark/logo treatment on login and sidebar — no visual identity currently
|
||||
- Active nav item color — `isActive` is wired but unstyled due to missing `--sidebar-primary` value
|
||||
- Card hover states — `hover:bg-muted` is invisible because `--muted` has no chroma
|
||||
- Loading spinner on form submit buttons — `disabled` state only, no visual motion
|
||||
- Page-level loading skeletons styled to match section colors
|
||||
- Empty state for first-time dashboard (no budgets) and CategoriesPage
|
||||
- Consistent section header palette — currently each card picks ad-hoc gradient colors
|
||||
- Positive/negative amount coloring applied uniformly across all tables
|
||||
|
||||
**Should have (differentiators):**
|
||||
- Pencil icon affordance on inline-editable rows (currently only a background hover hint)
|
||||
- Save confirmation flash after inline edit (brief green background, 100ms)
|
||||
- Chart tooltips formatted with `formatCurrency` (currently raw numbers)
|
||||
- Budget health badge (green/amber/red) near the budget selector
|
||||
- Donut chart center label showing month + year context
|
||||
- Month navigator (prev/next arrows) alongside budget selector
|
||||
- Progress bars in BillsTracker for actual vs. budgeted amount
|
||||
- Collapsible sidebar toggle for smaller screens
|
||||
|
||||
**Defer to v2+:**
|
||||
- Dark mode pastel palette — define light mode fully first; dark pastel is a distinct design problem
|
||||
- Custom color picker / theme selector — out of scope per PROJECT.md
|
||||
- Route-level animated page transitions — adds Framer Motion weight for marginal benefit
|
||||
- Toast notifications for saves — noisy in a dashboard with rapid sequential edits; use contextual row flash instead
|
||||
- Drag-to-reorder categories on dashboard
|
||||
- New chart types (multi-month trend, sparklines)
|
||||
- Onboarding wizard / guided tour
|
||||
- Category creation inline from dashboard — high complexity, separate milestone
|
||||
|
||||
### Architecture Approach
|
||||
|
||||
The design system follows a strict three-tier layered architecture: Token layer (all CSS custom properties in `index.css`) → Variant layer (CVA-based component variants in `components/ui/`) → Composition layer (domain-aware wrapper components in `components/`). Styling flows strictly downward; data flows upward from hooks through pages into components via props. The component boundary rule is critical: `components/ui/` must not import domain types from `lib/api.ts`.
|
||||
|
||||
**Major components:**
|
||||
1. `index.css` — single source of all CSS tokens, `@theme inline` Tailwind bridge, and base styles; no other CSS files
|
||||
2. `lib/palette.ts` (new) — single source of truth for category-to-color mapping, used by both Tailwind classes and Recharts `fill` strings
|
||||
3. `lib/colors.ts` (new) — `getCategoryColor()` utility for runtime CSS variable resolution in chart components
|
||||
4. `components/category-badge.tsx` (new) — domain badge using CVA `categoryVariants` as single source of category-to-color mapping
|
||||
5. `components/stat-card.tsx` (new) — financial metric display card wrapping shadcn Card
|
||||
6. `components/progress-bar.tsx` (new) — budget vs. actual visual with color-coded threshold states
|
||||
7. `components/empty-state.tsx` (new) — consistent empty state for all conditional sections
|
||||
8. `components/InlineEditCell.tsx` (extract) — extract from three duplicated local definitions in BillsTracker, VariableExpenses, DebtTracker
|
||||
|
||||
### Critical Pitfalls
|
||||
|
||||
1. **Hardcoded color values bypassing the token system** — The codebase has hex strings in Recharts fills, raw Tailwind palette classes (`bg-emerald-50`), and per-component `PASTEL_COLORS` arrays alongside the CSS variable system. Adding a fourth system (the pastel tokens) without migrating the existing approaches produces a visually inconsistent result regardless of effort. Prevention: `lib/palette.ts` as the single source of truth; ban inline hex values in component files.
|
||||
|
||||
2. **Editing shadcn source files directly** — `src/components/ui/` is effectively vendor code. Editing it risks overwrite by future `shadcn add` commands and makes the diff from upstream invisible. Prevention: customize exclusively via CSS variables in `index.css`; create wrapper components (`stat-card.tsx`, `category-badge.tsx`) for domain logic rather than modifying `card.tsx`, `badge.tsx`.
|
||||
|
||||
3. **InlineEditRow duplicated in three components** — `BillsTracker.tsx`, `VariableExpenses.tsx`, and `DebtTracker.tsx` each contain a local `InlineEditRow` function. The polish pass will make them diverge further if not extracted first. Prevention: extract to `components/InlineEditCell.tsx` before any visual work touches these tables.
|
||||
|
||||
4. **Chart colors not connected to the semantic color system** — `AvailableBalance` donut slice colors and `FinancialOverview` table row colors use different approaches for the same category types. A user sees two different blues for "Bills" on the same screen. Prevention: all chart `fill` props must derive from `CATEGORY_COLORS` in `lib/palette.ts`, keyed by category type — never index-based arrays.
|
||||
|
||||
5. **Spacing inconsistency overlooked in favor of color work** — `CardContent` padding varies (`p-0` vs `pt-4` vs `pt-6`) with no governing rule. Even correct colors will feel amateur if layout rhythm is inconsistent. Prevention: define spacing standards early (dashboard section gaps, card anatomy) and treat auth pages as first-class design surfaces rather than an afterthought.
|
||||
|
||||
---
|
||||
|
||||
## Implications for Roadmap
|
||||
|
||||
Based on combined research, a four-phase structure is strongly indicated by the dependency graph in FEATURES.md and the build order in ARCHITECTURE.md.
|
||||
|
||||
### Phase 1: Design Token Foundation
|
||||
**Rationale:** Everything else depends on this. The CSS variable system must have real pastel values before any component work produces consistent results. Establishing `lib/palette.ts` here prevents the color fragmentation pitfall from propagating through all subsequent phases.
|
||||
**Delivers:** Pastel color system live across all shadcn components, category semantic tokens available as Tailwind utilities, `lib/palette.ts` and `lib/colors.ts` in place, `InlineEditCell.tsx` extracted (dependency clearance before Phase 2).
|
||||
**Addresses:** Table stakes #1 (pastel CSS variable system); Pitfalls 1, 3, 4.
|
||||
**Avoids:** Starting component polish before the token layer is stable — the most common failure mode for this type of project.
|
||||
|
||||
### Phase 2: Layout and Brand Identity
|
||||
**Rationale:** Once tokens exist, the surfaces users see on every page load (login, sidebar, card headers, page headers) can be polished. These are high-visibility, high-leverage changes that establish the overall visual tone before touching individual feature components.
|
||||
**Delivers:** Branded login/register page, polished sidebar with visual hierarchy and colored active nav, consistent card header palette, typography hierarchy on dashboard (FinancialOverview + AvailableBalance as hero row), budget selector with proper page header framing.
|
||||
**Uses:** Pastel tokens from Phase 1, Geist Mono for tabular numbers.
|
||||
**Implements:** `page-header.tsx`, `stat-card.tsx` composition components.
|
||||
**Addresses:** Table stakes #2–5 (login background, logo, sidebar, active nav, typography).
|
||||
**Avoids:** Pitfall 5 (spacing inconsistency) — establish spacing standards here.
|
||||
|
||||
### Phase 3: Interaction Quality and Completeness
|
||||
**Rationale:** With color and layout established, the focus shifts to states and interactions: loading, empty, error, and edit feedback. These guard against the "unfinished" feeling that persists even in visually polished apps.
|
||||
**Delivers:** Loading spinners on form submit buttons, empty states for dashboard first-run and CategoriesPage, inline edit pencil icon affordance, save confirmation flash, positive/negative amount coloring uniformly applied, delete confirmation dialog on CategoriesPage, i18n keys for all new text.
|
||||
**Implements:** `empty-state.tsx`, interaction patterns on `InlineEditCell.tsx`.
|
||||
**Addresses:** Table stakes #6–14; Pitfalls 6 (i18n), 8 (empty states), 9 (delete confirmation).
|
||||
**Avoids:** New feature scope creep — defer month navigator, progress bars, budget health badge to optional stretch if foundation is solid.
|
||||
|
||||
### Phase 4: Chart Polish
|
||||
**Rationale:** Charts come last because they depend on both the token system (Phase 1) and the domain components (Phase 2-3) being stable. Polishing charts before tokens are final means redoing the work.
|
||||
**Delivers:** `ExpenseBreakdown` migrated to `ChartContainer` pattern, all chart fills referencing `CATEGORY_COLORS` from `lib/palette.ts`, custom `ChartTooltipContent` with `formatCurrency` formatting, donut center label with month/year context, `formatCurrency` locale bug fixed.
|
||||
**Uses:** `ChartContainer`, `ChartTooltipContent` from existing `ui/chart.tsx`.
|
||||
**Implements:** Recharts color consistency via `getCategoryColor()`.
|
||||
**Addresses:** Differentiators (chart tooltips, donut context label); Pitfalls 7 (Recharts tooltip styling), 10 (formatCurrency locale).
|
||||
|
||||
### Phase Ordering Rationale
|
||||
|
||||
- Phase 1 must precede all others because CSS token values are consumed by every subsequent change. Polish done without live tokens requires rework.
|
||||
- Phase 2 before Phase 3 because layout surfaces (sidebar, login, card headers) are always visible and set the perceptual quality bar; interaction states are noticed only when triggered.
|
||||
- Phase 3 before Phase 4 because empty states (Phase 3) affect what charts render against; fixing chart styling before the empty-state context is designed produces mismatched polish.
|
||||
- Extraction of `InlineEditCell.tsx` is placed at the start of Phase 1 (before any visual work) to prevent the three-component divergence pitfall.
|
||||
|
||||
### Research Flags
|
||||
|
||||
Phases with standard patterns (research not needed):
|
||||
- **Phase 1:** Tailwind 4 `@theme inline` token system is well-documented; patterns confirmed directly from codebase inspection. No research needed.
|
||||
- **Phase 2:** shadcn layout patterns are established and fully confirmed. No research needed.
|
||||
- **Phase 3:** All interaction patterns (loading spinner, row flash, empty state) are straightforward CSS transitions and existing shadcn Dialog usage. No research needed.
|
||||
- **Phase 4:** Recharts + `ChartContainer` integration pattern is confirmed from existing `ui/chart.tsx`. No research needed.
|
||||
|
||||
No phases require `/gsd:research-phase` — this is a well-scoped polish pass on an existing functional codebase with a fully locked, known stack.
|
||||
|
||||
---
|
||||
|
||||
## Confidence Assessment
|
||||
|
||||
| Area | Confidence | Notes |
|
||||
|------|------------|-------|
|
||||
| Stack | HIGH | All versions confirmed from `package.json`; no new dependencies needed |
|
||||
| Features | HIGH (table stakes) / MEDIUM (differentiators) | Table stakes from direct code inspection; differentiators from design domain experience without external benchmark sources |
|
||||
| Architecture | HIGH | Confirmed from direct inspection of `index.css`, `components.json`, `button.tsx`, `FinancialOverview.tsx`, `ui/chart.tsx` |
|
||||
| Pitfalls | HIGH | All pitfalls grounded in observed code — specific files and line-level patterns cited |
|
||||
|
||||
**Overall confidence:** HIGH
|
||||
|
||||
### Gaps to Address
|
||||
|
||||
- **formatCurrency locale bug (Pitfall 10):** The `de-DE` hardcode in `lib/format.ts` is a correctness issue, not just cosmetic. Confirm the `preferred_locale` field is available on the settings API response before implementing the fix in Phase 4.
|
||||
- **Delete cascade behavior on categories (Pitfall 9):** Before adding a confirmation dialog in Phase 3, verify what the Go backend does when a category with associated budget items is deleted. The confirmation copy should reflect the actual impact.
|
||||
- **Differentiator priority (budget health badge, month navigator, progress bars):** These are MEDIUM-confidence additions based on design judgment. Treat them as stretch goals within Phase 3 rather than committed scope. Re-evaluate after Phase 2 is complete.
|
||||
- **Geist Mono availability:** Confirm `@fontsource-variable/geist-mono` is available on the deployment host or can be bundled before adding the `bun add` step.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- Direct codebase inspection: `/frontend/src/index.css` — confirms Tailwind 4 `@theme inline`, oklch color space, shadcn CSS variable architecture
|
||||
- Direct codebase inspection: `/frontend/package.json` — all version numbers confirmed
|
||||
- Direct codebase inspection: `/frontend/components.json` — confirms shadcn v4, `cssVariables: true`, no `tailwind.config.ts`
|
||||
- Direct codebase inspection: `/frontend/src/components/ui/button.tsx`, `chart.tsx` — confirms CVA pattern, `ChartContainer` wrapper
|
||||
- Direct codebase inspection: `/frontend/src/components/FinancialOverview.tsx`, `AvailableBalance.tsx`, `ExpenseBreakdown.tsx` — identifies hardcoded color anti-patterns
|
||||
- Direct codebase inspection: `/frontend/src/` all pages and components — identifies `InlineEditRow` duplication, missing empty states, inconsistent amount coloring
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- Framer Motion React 19 compatibility: training data — recommendation to avoid is defensible on bundle-size grounds alone (~45KB) regardless of compatibility status
|
||||
- Differentiator features (budget health badge, progress bars, animated counters): domain experience with personal finance UI patterns; no external benchmark sources verified in this session
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
- None — all findings are grounded in codebase or framework documentation patterns
|
||||
|
||||
---
|
||||
|
||||
*Research completed: 2026-03-11*
|
||||
*Ready for roadmap: yes*
|
||||
Reference in New Issue
Block a user