# 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 | 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 `

` with `` | | 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 `

` 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`) | --- ## 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:
// With (using CSS custom properties from index.css):
``` 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) Budget Dashboard // Sidebar app name (replacing

) {t('app.title')} ``` 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 `

` 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 && ( {error} )} ``` 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:

{children}
``` The `SidebarTrigger` renders a `PanelLeftIcon` button and calls `toggleSidebar()` internally. The current `collapsible` prop on `` 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 ``` ### 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.02–0.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.95–0.97, chroma 0.03–0.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 ``. **How to avoid:** `SidebarTrigger` must be placed inside the `` tree — either inside ``, ``, or any other child. In `AppLayout.tsx` the current structure has both `` and `` inside ``, so placing it in `` 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
{/* ... */}
``` ### 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 && ( {error} )} ``` ### Sidebar Header with Branded Wordmark ```tsx // Source: index.css --primary token; no new tokens needed {t('app.title')} {auth.user && (

{auth.user.display_name}

)}
``` ### SidebarTrigger in Layout Header ```tsx // Source: ui/sidebar.tsx — SidebarTrigger already exported import { SidebarTrigger, /* ... */ } from '@/components/ui/sidebar'
{children}
``` ### Active Nav Item Override (if default contrast insufficient) ```tsx // Source: AppLayout.tsx — className addition only, no sidebar.tsx edit {item.label} ``` --- ## 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 `
{children}
` 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 `

` 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)