Files
SimpleFinanceDash/.planning/phases/02-layout-and-brand-identity/02-RESEARCH.md
2026-03-11 21:38:05 +01:00

441 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 2: Layout and Brand Identity - Research
**Researched:** 2026-03-11
**Domain:** React UI polish — auth screen branding, shadcn sidebar customization, Tailwind CSS v4 token-based styling
**Confidence:** HIGH
---
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|-----------------|
| AUTH-01 | Login screen has a branded pastel gradient background (not plain white) | Replace `bg-background` wrapper with gradient using palette tokens; full-bleed gradient div wrapping the Card |
| AUTH-02 | Login screen displays a styled app wordmark/logo treatment | Replace plain `CardTitle` text with a styled typographic mark using font weight + letter-spacing or gradient text fill |
| AUTH-03 | Register screen matches login screen's branded look | Same structural changes as AUTH-01/02 applied to `RegisterPage.tsx` |
| AUTH-04 | Auth form errors display with styled alert blocks and error icons | Install shadcn `alert` component; replace `<p className="text-sm text-destructive">` with `<Alert variant="destructive">` |
| NAV-01 | Sidebar has a pastel background color distinct from the main content area | `--sidebar` token already set to `oklch(0.97 0.012 280)` in index.css; sidebar.tsx uses `bg-sidebar` natively — need to verify contrast reads in browser |
| NAV-02 | Sidebar app name has a branded typographic treatment (not plain h2) | Replace `<h2 className="text-lg font-semibold">` with richer markup: gradient text, tracking-wide, or a custom wordmark span |
| NAV-03 | Active navigation item has a clearly visible color indicator using sidebar-primary token | `SidebarMenuButton` receives `isActive` prop — need to verify the `isActive` styling is visible; may need to override via className |
| NAV-04 | Sidebar is collapsible via a toggle button for smaller screens | `SidebarTrigger` component already exists and exported from `ui/sidebar.tsx`; needs to be placed in the layout (currently missing from `AppLayout.tsx`) |
</phase_requirements>
---
## Summary
Phase 2 focuses on two surface areas: the auth screens (LoginPage, RegisterPage) and the app shell (AppLayout with the shadcn Sidebar). Both are self-contained files with minimal external dependencies, so the risk profile is low.
The auth screens currently render a plain white Card centered on `bg-background`. Both pages use the same pattern and can be updated in parallel. The wordmark treatment (AUTH-02) is pure CSS — no new libraries needed, just a styled span with gradient text or tracked lettering using existing font/color tokens. The error display (AUTH-04) requires installing the shadcn `alert` component, which does not exist in `frontend/src/components/ui/` yet.
The sidebar is already fully wired with the shadcn Sidebar primitives including the `--sidebar` CSS token (set to a distinct pastel `oklch(0.97 0.012 280)`). The main gap is NAV-04: `SidebarTrigger` is exported from `ui/sidebar.tsx` but is not rendered anywhere in `AppLayout.tsx`. The active-state indicator (NAV-03) is passed via `isActive` to `SidebarMenuButton` — the shadcn component has built-in active styling, but visual verification is needed since the token values may produce low contrast.
**Primary recommendation:** Add shadcn Alert, apply gradient backgrounds to auth pages, add `SidebarTrigger` to AppLayout, and style the sidebar wordmark using CSS gradient text — all within existing files with no new routing or data fetching.
---
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| shadcn/ui | 4.0.0 (installed) | UI component primitives via CSS variables | Project constraint: all UI via shadcn |
| Tailwind CSS v4 | 4.2.1 (installed) | Utility classes; `@theme inline` exposes CSS vars as utilities | Project constraint |
| lucide-react | 0.577.0 (installed) | Icon set including `AlertCircle`, `PanelLeft` | Already used throughout |
| tw-animate-css | 1.4.0 (installed) | Animation utilities (for any transitions) | Out-of-scope alternative to Framer Motion |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| class-variance-authority | 0.7.1 (installed) | Variant-based className logic | If extracting a reusable wordmark component |
| clsx + tailwind-merge | installed | Conditional class merging | Standard in this project via `cn()` utility |
### Not Needed for This Phase
- No new npm installs required except `npx shadcn add alert` (fetches from registry, not a new npm dep)
- No animation library installs — tw-animate-css handles any needed transitions
- No routing changes
**Installation:**
```bash
cd frontend && bunx --bun shadcn add alert
```
---
## Architecture Patterns
### Recommended File Scope
All work is confined to these existing files:
```
frontend/src/
├── pages/
│ ├── LoginPage.tsx # AUTH-01, AUTH-02, AUTH-04
│ └── RegisterPage.tsx # AUTH-03, AUTH-04
├── components/
│ ├── AppLayout.tsx # NAV-01, NAV-02, NAV-03, NAV-04
│ └── ui/
│ └── alert.tsx # NEW — installed via shadcn CLI
└── index.css # No changes needed (tokens already set)
```
### Pattern 1: Gradient Background Wrapper (AUTH-01, AUTH-03)
**What:** Replace the `bg-background` wrapper div on auth pages with a full-bleed gradient using existing palette tokens via inline style or Tailwind arbitrary values.
**When to use:** Full-screen auth layouts where background IS the brand statement.
**Example:**
```tsx
// Replace:
<div className="flex min-h-screen items-center justify-center bg-background">
// With (using CSS custom properties from index.css):
<div
className="flex min-h-screen items-center justify-center"
style={{
background: `linear-gradient(135deg, oklch(0.96 0.03 280), oklch(0.94 0.04 260), oklch(0.96 0.04 320))`,
}}
>
```
The gradient values should pull from the `palette.ts` saving/bill/investment light shades to stay within the established pastel family. Alternatively, add a dedicated `--auth-gradient` CSS custom property to `index.css` and reference it with a Tailwind arbitrary value `bg-[var(--auth-gradient)]`.
### Pattern 2: Styled Wordmark (AUTH-02, NAV-02)
**What:** A CSS gradient text treatment using `background-clip: text` — a standard technique with full browser support.
**When to use:** When the app name needs brand identity treatment without SVG or image assets.
**Example:**
```tsx
// Auth page wordmark (inside CardHeader, replacing or supplementing CardDescription)
<span
className="text-2xl font-bold tracking-tight"
style={{
background: `linear-gradient(to right, oklch(0.50 0.12 260), oklch(0.50 0.12 320))`,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
}}
>
Budget Dashboard
</span>
// Sidebar app name (replacing <h2 className="text-lg font-semibold">)
<span
className="text-base font-semibold tracking-wide"
style={{
background: `linear-gradient(to right, oklch(0.50 0.12 260), oklch(0.50 0.12 300))`,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
}}
>
{t('app.title')}
</span>
```
The gradient values come from `--primary` (`oklch(0.50 0.12 260)`) shifted slightly in hue — no new tokens needed.
### Pattern 3: shadcn Alert for Error Display (AUTH-04)
**What:** Replace `<p className="text-sm text-destructive">` with a structured Alert component.
**When to use:** Form-level errors that need icon + message treatment.
**Example:**
```tsx
// After: bunx --bun shadcn add alert
import { Alert, AlertDescription } from '@/components/ui/alert'
import { AlertCircle } from 'lucide-react'
// In LoginPage and RegisterPage CardContent:
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
```
The shadcn `alert` component uses `--destructive` and `--destructive-foreground` tokens which are already defined in `index.css`.
### Pattern 4: SidebarTrigger Placement (NAV-04)
**What:** Add a visible toggle button that calls `toggleSidebar()` from the sidebar context. `SidebarTrigger` is already implemented and exported from `ui/sidebar.tsx`.
**When to use:** The current `AppLayout.tsx` renders no trigger — collapsing is only possible via the keyboard shortcut `Ctrl+B`.
**Example:**
```tsx
import { SidebarTrigger, /* existing imports */ } from '@/components/ui/sidebar'
// In AppLayout, inside SidebarInset, add a header bar:
<SidebarInset>
<header className="flex h-12 items-center gap-2 border-b px-4">
<SidebarTrigger />
</header>
<main className="flex-1 p-4">{children}</main>
</SidebarInset>
```
The `SidebarTrigger` renders a `PanelLeftIcon` button and calls `toggleSidebar()` internally. The current `collapsible` prop on `<Sidebar>` defaults to `"offcanvas"`, which means on mobile it slides in as a Sheet and on desktop it shifts the content. This behavior is already fully implemented in `sidebar.tsx`.
### Pattern 5: Active Nav Indicator (NAV-03)
**What:** Verify and if needed, reinforce the `isActive` visual state on `SidebarMenuButton`.
**Current state:** `AppLayout.tsx` already passes `isActive={location.pathname === item.path}` to `SidebarMenuButton`. The shadcn sidebar component applies `data-active` attribute and styles active items with `bg-sidebar-accent` and `text-sidebar-accent-foreground` by default.
**Potential gap:** The current `--sidebar-accent` is `oklch(0.93 0.020 280)` against `--sidebar` of `oklch(0.97 0.012 280)` — that is a 4-point lightness difference. This may render as visually insufficient contrast. If so, the fix is to update `SidebarMenuButton` via a `className` override or adjust `--sidebar-accent` in index.css to use `--sidebar-primary` for the active state.
**Verification test:** Render in browser, click nav items, confirm visible selection change. If not visible, apply:
```tsx
<SidebarMenuButton
asChild
isActive={location.pathname === item.path}
className="data-[active=true]:bg-sidebar-primary data-[active=true]:text-sidebar-primary-foreground"
>
```
### Anti-Patterns to Avoid
- **Editing `ui/sidebar.tsx` directly:** Project constraint — customize via `className` props or CSS variable overrides only, never edit `src/components/ui/` source files.
- **Hardcoded hex or oklch values in component files:** All colors must come from design tokens (`--primary`, `palette.ts`) or explicit inline style with values from the established token system.
- **Adding `text-green-*` or `text-blue-*` raw classes for wordmark:** Use gradient text via inline style from token values, consistent with Phase 1 decisions.
- **Adding `BrowserRouter` to auth pages:** Auth pages render outside the router (`App.tsx` renders them before `BrowserRouter`). Do not add routing-dependent hooks to `LoginPage` or `RegisterPage`.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Styled error alerts | Custom div with icon + border CSS | `shadcn add alert` | Handles destructive variant, screen-reader role, proper token use |
| Sidebar toggle button | Custom button + `useState` for open/closed | `SidebarTrigger` from `ui/sidebar` | Already wired to sidebar context; persists state in cookie |
| Sidebar collapse state | Manual `useState` + CSS width transitions | `SidebarProvider` + `collapsible="icon"` | Full collapse behavior with keyboard shortcut already built |
| Gradient text wordmark | SVG logo | CSS `background-clip: text` with inline style | No asset needed; uses existing tokens; responsive |
**Key insight:** The shadcn Sidebar component is unusually complete — toggle, collapse, cookie persistence, mobile Sheet, and keyboard shortcut are all pre-built. The only missing piece is exposing `SidebarTrigger` in the rendered layout.
---
## Common Pitfalls
### Pitfall 1: Alert Component Not Installed
**What goes wrong:** Importing `@/components/ui/alert` fails at build time — the file does not exist yet.
**Why it happens:** shadcn components are opt-in; `alert.tsx` is not in `frontend/src/components/ui/`.
**How to avoid:** Run `bunx --bun shadcn add alert` before writing the import. Verify the file appears at `frontend/src/components/ui/alert.tsx`.
**Warning signs:** TypeScript error `Cannot find module '@/components/ui/alert'`.
### Pitfall 2: Sidebar Active State Low Contrast
**What goes wrong:** The `isActive` indicator renders but is nearly invisible due to minimal lightness difference between `--sidebar` and `--sidebar-accent`.
**Why it happens:** `--sidebar: oklch(0.97 0.012 280)` vs `--sidebar-accent: oklch(0.93 0.020 280)` — 4 lightness points difference in a low-chroma space.
**How to avoid:** After implementing, visually verify active state. If insufficient, override with `data-[active=true]:bg-sidebar-primary data-[active=true]:text-sidebar-primary-foreground` in the `SidebarMenuButton` className. The `--sidebar-primary` token `oklch(0.50 0.12 260)` provides strong contrast.
**Warning signs:** Clicking nav items produces no perceptible visual change.
### Pitfall 3: Auth Gradient Feels Jarring
**What goes wrong:** A high-saturation gradient makes the login screen feel loud rather than refined.
**Why it happens:** Pastels require low chroma (0.020.06) at high lightness (0.92+). Using chart or header gradient values (medium shades at 0.88 lightness) will appear oversaturated as full-screen backgrounds.
**How to avoid:** Use the `light` shades from `palette.ts` (lightness 0.950.97, chroma 0.030.04), not `medium` or `base` shades. Keep the gradient subtle — it should feel like tinted paper, not a colorful splash screen.
**Warning signs:** The gradient overwhelms the card form element visually.
### Pitfall 4: Wordmark Gradient Text Fallback
**What goes wrong:** `WebkitTextFillColor: 'transparent'` with `backgroundClip: 'text'` renders as invisible text in some edge environments.
**Why it happens:** The `-webkit-` prefix technique requires both properties to be set correctly.
**How to avoid:** Always pair `WebkitBackgroundClip: 'text'`, `WebkitTextFillColor: 'transparent'`, AND `backgroundClip: 'text'`. The non-prefixed version is needed for Firefox compatibility.
**Warning signs:** Wordmark text disappears or renders as black.
### Pitfall 5: SidebarTrigger Outside SidebarProvider
**What goes wrong:** `useSidebar()` throws "useSidebar must be used within a SidebarProvider."
**Why it happens:** `SidebarTrigger` calls `useSidebar()` internally and will throw if placed outside `<SidebarProvider>`.
**How to avoid:** `SidebarTrigger` must be placed inside the `<SidebarProvider>` tree — either inside `<Sidebar>`, `<SidebarInset>`, or any other child. In `AppLayout.tsx` the current structure has both `<Sidebar>` and `<SidebarInset>` inside `<SidebarProvider>`, so placing it in `<SidebarInset>` is safe.
### Pitfall 6: RegisterPage Missing from Auth Page Update
**What goes wrong:** AUTH-03 is missed — RegisterPage still renders the plain white card after LoginPage is polished.
**Why it happens:** The two pages are separate files with identical structure; easy to forget to update both.
**How to avoid:** Treat AUTH-01/02/04 and AUTH-03 as one logical task that touches both files simultaneously. Register page should be a near-exact structural mirror of Login page.
---
## Code Examples
### Full-Screen Gradient Auth Wrapper
```tsx
// Source: palette.ts light shades — keeping within established token system
<div
className="flex min-h-screen items-center justify-center"
style={{
background: `linear-gradient(135deg, ${palette.saving.light}, ${palette.bill.light}, ${palette.investment.light})`,
}}
>
<Card className="w-full max-w-md shadow-lg">
{/* ... */}
</Card>
</div>
```
### Alert Destructive Error Block
```tsx
// Source: shadcn alert component (installed via bunx shadcn add alert)
import { Alert, AlertDescription } from '@/components/ui/alert'
import { AlertCircle } from 'lucide-react'
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
```
### Sidebar Header with Branded Wordmark
```tsx
// Source: index.css --primary token; no new tokens needed
<SidebarHeader className="border-b px-4 py-3">
<span
className="text-base font-semibold tracking-wide"
style={{
background: `linear-gradient(to right, var(--color-primary), oklch(0.50 0.12 300))`,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
}}
>
{t('app.title')}
</span>
{auth.user && (
<p className="text-sm text-muted-foreground">{auth.user.display_name}</p>
)}
</SidebarHeader>
```
### SidebarTrigger in Layout Header
```tsx
// Source: ui/sidebar.tsx — SidebarTrigger already exported
import { SidebarTrigger, /* ... */ } from '@/components/ui/sidebar'
<SidebarInset>
<header className="flex h-12 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
</header>
<main className="flex-1 p-4">{children}</main>
</SidebarInset>
```
### Active Nav Item Override (if default contrast insufficient)
```tsx
// Source: AppLayout.tsx — className addition only, no sidebar.tsx edit
<SidebarMenuButton
asChild
isActive={location.pathname === item.path}
className="data-[active=true]:bg-sidebar-primary data-[active=true]:text-sidebar-primary-foreground"
>
<Link to={item.path}>
<item.icon />
<span>{item.label}</span>
</Link>
</SidebarMenuButton>
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Manual sidebar toggle with useState | shadcn Sidebar with SidebarProvider context | shadcn 2.x | Built-in toggle, cookie persistence, keyboard shortcut |
| Tailwind v3 `theme.extend.colors` for custom tokens | CSS custom properties in `:root` + `@theme inline` in Tailwind v4 | Tailwind v4 | Tokens are pure CSS, not Tailwind-config-dependent |
| `lucide-react` named imports | Same — no change | - | Lucide is still the icon standard for shadcn |
**Deprecated/outdated:**
- `bg-primary` Tailwind class approach for backgrounds: Now use CSS variable direct references (`var(--color-primary)`) or token-based inline styles for gradient backgrounds — Tailwind v4 exposes all custom properties as utilities.
---
## Open Questions
1. **Active sidebar item contrast adequacy**
- What we know: `--sidebar-accent` is 4 lightness points from `--sidebar`; `SidebarMenuButton` uses `data-[active=true]:bg-sidebar-accent` by default
- What's unclear: Whether this is visually sufficient without browser testing
- Recommendation: Plan task includes a visual verification step; if insufficient, apply the `data-[active=true]:bg-sidebar-primary` className override
2. **Wordmark: gradient text vs. font-weight/tracking only**
- What we know: Gradient text works cross-browser with correct CSS properties
- What's unclear: Whether the design intent is a text-gradient wordmark or just tracked/bold typography
- Recommendation: Gradient text is the stronger brand treatment; the planner should implement gradient text as the default and treat simpler alternatives as a fallback
3. **SidebarInset main content padding**
- What we know: Current `AppLayout.tsx` has `<main className="flex-1">{children}</main>` with no padding
- What's unclear: Whether adding a header bar with `SidebarTrigger` requires padding adjustments to existing page components
- Recommendation: Add `p-4` to main only if pages do not already manage their own padding; inspect `DashboardPage` to confirm
---
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Vitest 4.0.18 + @testing-library/react 16.3.2 |
| Config file | `frontend/vite.config.ts` (test section present) |
| Quick run command | `cd frontend && bun vitest run --reporter=verbose` |
| Full suite command | `cd frontend && bun vitest run` |
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|--------------|
| AUTH-01 | Login page renders gradient background wrapper | unit | `bun vitest run src/pages/LoginPage.test.tsx` | No — Wave 0 |
| AUTH-02 | Login page renders wordmark with gradient text style | unit | `bun vitest run src/pages/LoginPage.test.tsx` | No — Wave 0 |
| AUTH-03 | Register page renders same gradient/wordmark as login | unit | `bun vitest run src/pages/RegisterPage.test.tsx` | No — Wave 0 |
| AUTH-04 | Error string renders Alert with destructive variant | unit | `bun vitest run src/pages/LoginPage.test.tsx` | No — Wave 0 |
| NAV-01 | Sidebar renders with bg-sidebar class | unit | `bun vitest run src/components/AppLayout.test.tsx` | No — Wave 0 |
| NAV-02 | Sidebar header contains branded wordmark element | unit | `bun vitest run src/components/AppLayout.test.tsx` | No — Wave 0 |
| NAV-03 | Active nav item receives data-active attribute | unit | `bun vitest run src/components/AppLayout.test.tsx` | No — Wave 0 |
| NAV-04 | SidebarTrigger button is rendered in layout | unit | `bun vitest run src/components/AppLayout.test.tsx` | No — Wave 0 |
**Note on visual requirements:** AUTH-01 (gradient background), AUTH-02 (wordmark appearance), NAV-01 (sidebar color distinction), and NAV-03 (visible color indicator) have a visual correctness dimension that unit tests cannot fully capture. Unit tests verify structural presence (element rendered, class present, inline style set); visual correctness requires browser verification. Plan tasks should include explicit browser check steps alongside automated tests.
### Sampling Rate
- **Per task commit:** `cd frontend && bun vitest run`
- **Per wave merge:** `cd frontend && bun vitest run && cd frontend && bun run build`
- **Phase gate:** Full suite green + production build green before `/gsd:verify-work`
### Wave 0 Gaps
- [ ] `frontend/src/pages/LoginPage.test.tsx` — covers AUTH-01, AUTH-02, AUTH-04
- [ ] `frontend/src/pages/RegisterPage.test.tsx` — covers AUTH-03
- [ ] `frontend/src/components/AppLayout.test.tsx` — covers NAV-01, NAV-02, NAV-03, NAV-04
- [ ] `frontend/src/components/ui/alert.tsx` — must exist before any test imports it (install via shadcn CLI)
---
## Sources
### Primary (HIGH confidence)
- Direct file inspection: `frontend/src/components/AppLayout.tsx` — confirms SidebarTrigger not yet rendered
- Direct file inspection: `frontend/src/pages/LoginPage.tsx` and `RegisterPage.tsx` — confirms plain `bg-background` wrapper and `<p className="text-sm text-destructive">` error display
- Direct file inspection: `frontend/src/components/ui/sidebar.tsx` — confirms SidebarTrigger exported, collapsible behavior fully implemented
- Direct file inspection: `frontend/src/index.css` — confirms `--sidebar`, `--sidebar-primary`, `--sidebar-accent` token values
- Direct file inspection: `frontend/src/lib/palette.ts` — confirms light/medium/base shades for gradient construction
### Secondary (MEDIUM confidence)
- shadcn/ui Alert component documentation pattern — standard destructive variant with AlertCircle icon is the established pattern for form error alerts in shadcn projects
### Tertiary (LOW confidence)
- None
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — all libraries verified as installed via package.json; no new npm deps needed beyond `bunx shadcn add alert`
- Architecture: HIGH — all files inspected directly; changes are confined to known files with no new routing
- Pitfalls: HIGH — active state contrast gap identified by reading actual token values; alert install gap confirmed by directory listing
**Research date:** 2026-03-11
**Valid until:** 2026-04-10 (stable shadcn/Tailwind ecosystem; 30-day window)