Skip to main content

UX Standards — 01: Layout Shell

Governs: The full-page layout structure every dashboard page lives inside. Parent rules: See 00-OVERVIEW-AND-CSS-RULES.md first.


Visual Anatomy

The sidebar is a full-height left rail that spans the entire viewport from top to bottom. The header, content area, and footer live entirely within the right column (SidebarInset) — they never span over the sidebar.

Note the bottom alignment: the sidebar's user card and the footer in the right column sit at the same vertical level — both pinned to the bottom of their respective columns.

Expanded State (w-64 — default)

┌────────────────────┬──────────────────────────────────────────────────────┐
│ SIDEBAR (w-64) │ HEADER (sticky, h-16, border-b bg-background) │
│ │ [Page Title] [Subtitle] [Co.Logo][Anna][Srch][More] │
│ ┌────────────────┐ ├──────────────────────────────────────────────────────┤
│ │ App Name │ │ MAIN CONTENT AREA (flex-1, overflow-y-auto, p-6) │
│ │ App subtitle ⌄ │ │ ┌──────────────────────────────────────────────────┐ │
│ └────────────────┘ │ │ [Stats Cards — optional] │ │
│ ───────────────── │ ├──────────────────────────────────────────────────┤ │
│ [ico] Nav item │ │ [Tab Navigation — optional] │ │
│ [ico] Nav item │ ├──────────────────────────────────────────────────┤ │
│ [ico] Nav item ▶ │ │ [UDT Toolbar] │ │
│ (scrollable) │ ├──────────────────────────────────────────────────┤ │
│ · │ │ [Universal Data Table] (flex-1, min-h-0) │ │
│ · │ └──────────────────────────────────────────────────┘ │
│ ───────────────── ├──────────────────────────────────────────────────────┤
│ Username │ FOOTER (h-12, border-t) │
│ user@email... ⌄ │ [N row(s) total.] [← ‹ 1 › → ][Page 1 of N][Rows] │
└────────────────────┴──────────────────────────────────────────────────────┘

Note: The user card at the bottom of the sidebar and the footer in the right column are horizontally aligned — both anchored to the bottom of the viewport.

Collapsed State (w-16 — icons only)

When collapsed, only icons are visible — no text. The app switcher trigger contracts from the app name row to the shield icon only. Nav items show icon-only with tooltip on hover.

┌──────┬────────────────────────────────────────────────────────────────────┐
│ w16 │ HEADER (sticky, h-16, border-b bg-background) │
│ │ [Page Title] [Subtitle] [Co.Logo][Anna][Srch][Theme][More] │
│ [🛡] ├────────────────────────────────────────────────────────────────────┤
│ ──── │ MAIN CONTENT AREA (flex-1, overflow-y-auto, p-6) │
│ [ico]│ ┌──────────────────────────────────────────────────────────────┐ │
│ [ico]│ │ [Stats Cards — optional] │ │
│ [ico]│ ├──────────────────────────────────────────────────────────────┤ │
│ · │ │ [Tab Navigation — optional] │ │
│ · │ ├──────────────────────────────────────────────────────────────┤ │
│ ──── │ │ [UDT Toolbar] │ │
│ [SU] ├──────────────────────────────────────────────────────────────────┤ │
│ │ FOOTER (h-12, border-t) │ │
│ │ [N row(s) total.] [← ‹ 1 › →][Page 1 of N][Rows ▾] │ │
└──────┴────────────────────────────────────────────────────────────────────┘

App Switcher (accessible in both states)

The app switcher lives at the top of the sidebar, not in the header. Clicking the trigger opens a dropdown listing all Anshin applications.

┌───────────────────────┐
│ Anshin Applications │ ← DropdownMenuLabel
│ ───────────────── │
│ ✓ Anshin Orchestrate │ ← current app (checkmark)
│ Anshin MCP │
│ Anshin Comply │
│ Anshin Predict │
│ Anshin Admin Portal │
│ Anshin Client Portal│
│ Anshin Engage │
│ ───────────────── │
│ [Settings] → │ ← workspace settings
└───────────────────────┘
Sidebar stateSwitcher triggerDropdown anchors to
Expanded[🛡 App Name / subtitle ⌄] row at topBelow the trigger row
CollapsedShield icon only at topBelow/beside the shield

Detailed app switcher spec → see 17-APPLICATION-SWITCHER.md


Critical layout rules:

  • Header and footer are always inside SidebarInset (right column). They never span the full page width or overlap the sidebar.
  • The app switcher is in the sidebar header, not the top header bar.
  • Footer and sidebar user card bottom-align at the same horizontal level.

Layer-by-Layer Specification

Outer Container

Orchestrate (canonical pattern) — SidebarProvider

// dashboard-layout-client.tsx
<SidebarProvider defaultOpen={true}>
<AppSidebar user={user} collapsible="icon" /> {/* full-height left rail */}
<SidebarInset> {/* right column — flex-col */}
<Header ... /> {/* h-16, top of right column */}
<main className="flex-1 overflow-y-auto overflow-x-hidden p-6">{children}</main>
<Footer /> {/* h-12, bottom of right column */}
</SidebarInset>
</SidebarProvider>

SidebarProvider handles all sizing, collapse state, and mobile sheet behavior automatically. Do not add an outer <div> wrapper around it.

Accelerate Modules (custom pattern) — <div> wrapper

// Used by fax-automation, referral-automation, and other Accelerate modules
<div className="min-h-screen w-full bg-background text-foreground flex flex-col lg:flex-row">
{/* sidebar + main content */}
</div>
  • min-h-screen — always full viewport height
  • w-full — full width
  • bg-background — respects dark/light theme
  • flex flex-col lg:flex-row — vertical stacking on mobile, horizontal on desktop

The sidebar is a full-height left rail — it spans from the very top to the very bottom of the viewport, independent of the header and footer. The header and footer live inside SidebarInset (the right column), not above/below the sidebar.

Collapse Behavior

StateSidebar widthLogoNav items
Expandedw-64 (256px)Full logo (shield + wordmark)Icon + text label
Collapsedw-16 (64px)Shield icon only (no wordmark)Icon only (no text) — tooltip on hover
  • collapsible="icon" — shadcn sidebar prop that switches between expanded and icon-only
  • The sidebar never fully disappears on desktop — it contracts to w-16, not hidden
  • On mobile (< lg): sidebar hides entirely; hamburger → Sheet slide-over
  1. App Switcher trigger (SidebarHeader) — expanded: [🛡 App Name / subtitle ⌄]; collapsed: shield icon only. Clicking opens the application switcher dropdown.
  2. Navigation groups (SidebarContent) — scrollable area; nav items with icons + text labels (expanded) or icons only with tooltips (collapsed)
  3. User card (SidebarFooter) — pinned to bottom; avatar + name + email [⌄] (expanded) or avatar-only (collapsed)

Detailed sidebar spec → see 03-SIDEBAR-NAVIGATION.md


Header Zone

The header lives inside SidebarInset — it is a right-column element only. It does NOT span the sidebar.

<header className="sticky top-0 z-50 flex h-16 w-full shrink-0 items-center justify-between gap-2 border-b bg-background px-4">
  • sticky top-0 z-50 — always visible, above all content
  • h-16 — exactly 64px. Non-negotiable.
  • border-b bg-background — consistent separation
  • px-4 — horizontal padding

Detailed header spec → see 02-HEADER.md


Main Content Area

<main className="flex-1 overflow-y-auto overflow-x-hidden p-6">
{children}
</main>
  • flex-1 — fills all remaining space after sidebar
  • overflow-y-auto — content scrolls, not the page
  • overflow-x-hidden — prevents horizontal scroll at page level (table uses internal scroll)
  • p-6 — 24px padding on all sides. This is the standard content padding.

Special cases:

  • Full-screen/full-canvas pages (e.g., chat, workflow builder): use p-0 instead
  • Mobile: padding reduces to p-4

The footer lives inside SidebarInset — it is a right-column element only. It does NOT span the sidebar.

<footer className="flex h-12 shrink-0 items-center justify-between border-t bg-background px-6 text-sm text-muted-foreground">
  • h-12 — exactly 48px. Non-negotiable.
  • shrink-0 — never compressed
  • border-t bg-background — consistent separation
  • px-6 — horizontal padding (slightly wider than header)

Detailed footer spec → see 13-FOOTER.md


Component Wiring — Orchestrate

// packages/shared/components/layouts/header.tsx
// packages/shared/components/layouts/app-sidebar.tsx
// packages/shared/components/layouts/footer.tsx

// dashboard-layout-client.tsx:
<SidebarProvider defaultOpen={true}>
<AppSidebar user={user} collapsible='icon' />
<SidebarInset>
<Header user={user} fetchCompanyLogo={fetchCompanyLogoFn} notificationSlot={<NotificationPopover />} />
<main className="flex-1 overflow-y-auto overflow-x-hidden p-6">
{children}
</main>
<Footer />
</SidebarInset>
</SidebarProvider>

Component Wiring — Accelerate Modules

// modules/fax-automation/app/web-fax-dashboard/components/dashboard-layout.tsx
// modules/referral-automation/web-outreach-dashboard/components/ (layout TBD)

<div className="min-h-screen w-full bg-background text-foreground flex flex-col lg:flex-row">
{/* Mobile header (lg:hidden) */}
<header className="lg:hidden flex items-center justify-between px-4 py-3 border-b bg-card">
<Sheet>...</Sheet> {/* Hamburger menu → sidebar in Sheet */}
<span className="font-semibold text-lg">{APP_NAME}</span>
<NotificationBell />
</header>

{/* Desktop sidebar */}
<aside className="hidden lg:flex flex-col w-64 border-r bg-card shrink-0">
{/* Logo header */}
{/* Nav content */}
</aside>

<main className="flex-1 flex flex-col overflow-hidden">
{children}
</main>
</div>

Mobile Breakpoint Behavior

ElementMobile (< lg)Desktop (lg+)
SidebarHidden; hamburger → Sheet slide-overFixed 256px left column
HeaderSimplified: hamburger + app name + bellFull three-zone header
Content paddingp-4p-6
FooterHidden (optional)h-12 always visible
TablesHorizontal scroll within overflow-x-auto wrapperFull width

When to Create a New Layout Shell

Never create a new layout from scratch. Always extend the existing shared layout. If a new module needs:

  1. A different sidebar structure → add a new <aside> variant to the shared layout using a variant prop
  2. No sidebar (e.g., a kiosk view) → use className="flex-1" on main without the aside
  3. A popup/detached window → use the popup layout (see 12-POPOUT-AND-SLIDE-OVER-WINDOWS.md)

Page Content Inner Structure

Within the <main> content area, every page follows this exact vertical stack:

[Stats Cards] ← optional, above tabs
[Tab Navigation] ← optional, full-width
[UDT Toolbar] ← always present on list pages
[Data Table] ← flex-1, fills remaining space
<div className="flex h-full flex-col space-y-4">
{/* 1. Stats cards (optional) */}
{stats && <div className={`grid gap-4 ${getGridCols(stats.length)}`}>...</div>}

{/* 2. Tab navigation (optional) — see 05-TAB-NAVIGATION.md */}
{preToolbarContent}

{/* 3. Toolbar (see 08-UDT-TOOLBAR.md) */}
<DataTableToolbar ... />

{/* 4. Table (flex-1 fills the rest) */}
<div className="flex min-h-0 flex-1 flex-col">
<DataTableSortable ... />
</div>
</div>

space-y-4 (16px gap) between all major zones. Do not use margin or padding for zone separation — always space-y-4 on the parent.


Violation Checklist

Before submitting any layout change, verify:

  • Orchestrate: Outer container is SidebarProvider — NOT a <div> wrapper
  • Accelerate: Outer container uses min-h-screen w-full bg-background text-foreground
  • Sidebar is a full-height left rail — AppSidebar or <aside w-64 border-r bg-card shrink-0>
  • Header is inside SidebarInset — NOT a sibling of AppSidebar
  • Footer is inside SidebarInset — NOT at root level spanning sidebar
  • Header is exactly h-16 sticky top-0 z-50 border-b bg-background
  • Footer is exactly h-12 border-t bg-background
  • Main content uses p-6 (not p-4, p-8, or px-6 py-4)
  • No hardcoded colors in any layout wrapper
  • Sidebar is hidden lg:flex (not always visible, not always hidden)
  • flex-1 overflow-y-auto overflow-x-hidden on main content
  • Inner content stack uses flex h-full flex-col space-y-4
  • App switcher trigger is in SidebarHeader (top of sidebar) — NOT in the header bar
  • Sidebar user card and footer are visually bottom-aligned at the same level