PageHeader, FilterTabs, Tabs, Card, plain sections). This page documents what’s where, and — for contributors — when to use each primitive so new screens look and feel like the rest of the product.
Sidebar structure
The left sidebar uses collapsible groups. Each group is a parent route; its children are a flat list of first-class destinations underneath.Inbox
Flat — no sub-group. Land directly on the operator inbox.
Contacts
Flat. Filters and segments live inside the page, not the sidebar.
Agents
Collapsible group. Children:
Overview, Templates, and per-agent detail routes.Campaigns
Collapsible group. Children:
All campaigns, Audiences, Schedules.Journeys
Collapsible group. Children:
All journeys, Templates, Runs.Settings
Flat; internal tabs handle the many sub-pages.
localStorage so the shell doesn’t re-collapse on every navigation.
Sub-routes under a parent record
Detail pages for Agents, Campaigns, and Journeys use a consistent URL shape:PageHeader. Each tab is a real route — deep-linkable, shareable, and preserved across reloads. Tab switches use Next.js shallow navigation so scroll position and transient state are not blown away.
Sub-route tabs are not the same primitive as in-page
Tabs. See Filter tabs vs content tabs below.Breadcrumbs
The top bar renders a breadcrumb trail that is auto-derived from the current route by default. The system walks the pathname segments, looks each up in a small registry (/apps/app/lib/breadcrumbs.ts) for a human label, and renders clickable crumbs for every segment except the last.
Auto mode (the default)
Every page gets auto-derived crumbs without doing anything:/agents/abc123/runs this renders: Agents › Acme support bot › Runs. The middle label is resolved by the registry’s [id] loader (it fetches the agent’s display name from cache).
Use auto mode when:
- The route segments map cleanly to human labels (the common case).
- You’re inside a parent/child record hierarchy already wired into the registry.
Explicit mode
Passbreadcrumbs as an array when the auto-derived trail would be wrong or missing context:
- The URL doesn’t reflect the user’s mental location (e.g. modal routes, wizards).
- You need to surface a label that isn’t in the registry yet and you don’t want to add it.
- You’re rendering a one-off page (migrations, admin tools) where registry maintenance isn’t worth it.
Filter tabs vs content tabs
Switchbord ships two tab primitives that look similar but serve different roles.FilterTabs — segmented control
A compact, segmented-button row. Picks which slice of the same list you’re viewing. No content panels of its own — it just controls the query/filter state below.
Use FilterTabs when:
- Switching between views of the same underlying dataset (e.g.
All / Draft / Pending / Approvedon templates). - You want a segmented-control look that doesn’t imply separate “pages”.
- Sub-route navigation for a detail record (
config | tools | mcp | runs).
Tabs — content regions
The full Radix-backed Tabs primitive with TabsList, TabsTrigger, and TabsContent. Each tab owns a distinct content region with its own header, its own data, and potentially its own mutations.
Use Tabs when:
- The tabs render structurally different UIs (e.g. Overview vs JSON vs Logs).
- Each tab has substantial content — tables, forms, charts.
- You’d otherwise have to split into sibling routes but the screens are too tightly coupled to warrant URL-level navigation.
Cards vs sections — page composition
The most common mistake in new screens is wrapping everything in aCard. Our page grammar is:
PageHeaderat the top — title, description, breadcrumbs, primary action.- Plain sections underneath — an
<h2>(or a small section header component) followed by content directly on the page background. Cardonly for side panels, modular pickers, or things that genuinely need a bordered container.
Prefer plain sections
Cards produces heavy, boxy pages that don’t scale past three sections.
Reach for Card when
- Side panels — a right-rail summary next to the main content.
- Modular pickers — model selector, template picker, audience filter box. Self-contained widgets that can live anywhere on a page.
- Empty states & onboarding prompts — a single, centred affordance on an otherwise empty page.
- Dashboard tiles — metric cards on overview pages.
Quick checklist for new pages
Before you open a PR for a new screen, confirm:-
PageHeaderis the first element;breadcrumbsis either omitted (auto) or an explicit array — never both. - Sub-routes under a record use
FilterTabswired to real URLs, not in-pageTabs. - The sidebar entry is flat unless there are 3+ sibling routes, in which case it’s a collapsible group with a sensible icon.
- Top-level structure is
PageHeader+ sections.Cardappears only for side panels, pickers, empty states, or dashboard tiles. - Any new
[id]segment has a label loader registered in/apps/app/lib/breadcrumbs.ts(or explicit crumbs are passed at every call site).
Shipped in this project
The navigation + UI polish project (BORD-329 → BORD-333) delivered:- BORD-329 — collapsible sidebar groups for Agents / Campaigns / Journeys ✅
- BORD-330 — per-record sub-route tabs (
config | tools | mcp | runsand friends) ✅ - BORD-331 — auto-derived breadcrumbs with a small registry + explicit override escape hatch ✅
- BORD-332 — page-grammar refactor:
PageHeader+ sections,Carddemoted to side-panel duty ✅ - BORD-333 — this doc + a design-token cleanup on the
/weblanding CTA ✅