24 KiB
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:
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:
// 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:
// 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:
// 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:
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:
<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.tsxdirectly: Project constraint — customize viaclassNameprops or CSS variable overrides only, never editsrc/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-*ortext-blue-*raw classes for wordmark: Use gradient text via inline style from token values, consistent with Phase 1 decisions. - Adding
BrowserRouterto auth pages: Auth pages render outside the router (App.tsxrenders them beforeBrowserRouter). Do not add routing-dependent hooks toLoginPageorRegisterPage.
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 <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
// 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
// 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
// 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
// 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)
// 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-primaryTailwind 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
-
Active sidebar item contrast adequacy
- What we know:
--sidebar-accentis 4 lightness points from--sidebar;SidebarMenuButtonusesdata-[active=true]:bg-sidebar-accentby 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-primaryclassName override
- What we know:
-
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
-
SidebarInset main content padding
- What we know: Current
AppLayout.tsxhas<main className="flex-1">{children}</main>with no padding - What's unclear: Whether adding a header bar with
SidebarTriggerrequires padding adjustments to existing page components - Recommendation: Add
p-4to main only if pages do not already manage their own padding; inspectDashboardPageto confirm
- What we know: Current
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-04frontend/src/pages/RegisterPage.test.tsx— covers AUTH-03frontend/src/components/AppLayout.test.tsx— covers NAV-01, NAV-02, NAV-03, NAV-04frontend/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.tsxandRegisterPage.tsx— confirms plainbg-backgroundwrapper 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-accenttoken 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)