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
|
||||
Reference in New Issue
Block a user