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

24 KiB
Raw Blame History

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

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.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

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