> ## Documentation Index
> Fetch the complete documentation index at: https://docs.switchbord.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Navigation & page layout

> How Switchbord's app shell is organised — the sidebar groups, sub-routes, breadcrumbs, filter tabs, and when to reach for a Card versus a plain section.

Switchbord's app shell is built around a collapsible sidebar, an auto-derived breadcrumb in the top bar, and a small set of composable page primitives (`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.

<CardGroup cols={2}>
  <Card title="Inbox" icon="inbox">
    Flat — no sub-group. Land directly on the operator inbox.
  </Card>

  <Card title="Contacts" icon="address-book">
    Flat. Filters and segments live inside the page, not the sidebar.
  </Card>

  <Card title="Agents" icon="robot">
    Collapsible group. Children: `Overview`, `Templates`, and per-agent detail routes.
  </Card>

  <Card title="Campaigns" icon="bullhorn">
    Collapsible group. Children: `All campaigns`, `Audiences`, `Schedules`.
  </Card>

  <Card title="Journeys" icon="diagram-project">
    Collapsible group. Children: `All journeys`, `Templates`, `Runs`.
  </Card>

  <Card title="Settings" icon="gear">
    Flat; internal tabs handle the many sub-pages.
  </Card>
</CardGroup>

Groups persist their open/closed state per user in `localStorage` so the shell doesn't re-collapse on every navigation.

<Tip>
  If you're adding a new top-level destination, prefer a **flat** entry first. Only promote to a collapsible group once you have three or more sibling routes that benefit from being visible at once.
</Tip>

## Sub-routes under a parent record

Detail pages for Agents, Campaigns, and Journeys use a consistent URL shape:

```
/agents/[id]/config
/agents/[id]/tools
/agents/[id]/mcp
/agents/[id]/runs

/campaigns/[id]/overview
/campaigns/[id]/audience
/campaigns/[id]/schedule
/campaigns/[id]/runs

/journeys/[id]/graph
/journeys/[id]/triggers
/journeys/[id]/runs
```

These sub-routes are rendered as **FilterTabs** at the top of the record page, immediately below the `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.

<Info>
  Sub-route tabs are not the same primitive as in-page `Tabs`. See [Filter tabs vs content tabs](#filter-tabs-vs-content-tabs) below.
</Info>

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

```tsx theme={null}
// No prop needed — auto is the default.
<PageHeader title="Runs" />
```

For `/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

Pass `breadcrumbs` as an array when the auto-derived trail would be wrong or missing context:

```tsx theme={null}
<PageHeader
  title="Reply drafting"
  breadcrumbs={[
    { label: "Inbox", href: "/inbox" },
    { label: contact.displayName, href: `/inbox/${conv.id}` },
    { label: "Draft" },
  ]}
/>
```

Use explicit mode when:

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

<Warning>
  Don't mix both. If you pass `breadcrumbs`, the auto resolver is skipped entirely — partial overrides are not supported on purpose, to keep the trail predictable.
</Warning>

## 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 / Approved` on templates).
* You want a segmented-control look that doesn't imply separate "pages".
* Sub-route navigation for a detail record (`config | tools | mcp | runs`).

```tsx theme={null}
<FilterTabs
  value={status}
  onValueChange={setStatus}
  options={[
    { value: "all", label: "All" },
    { value: "draft", label: "Draft" },
    { value: "approved", label: "Approved" },
  ]}
/>
```

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

<Tip>
  Rule of thumb: if switching the tab should change the **URL**, use `FilterTabs` with sub-routes. If switching the tab only changes **what's inside this card / section**, use `Tabs`.
</Tip>

## Cards vs sections — page composition

The most common mistake in new screens is wrapping everything in a `Card`. Our page grammar is:

1. **`PageHeader`** at the top — title, description, breadcrumbs, primary action.
2. **Plain sections** underneath — an `<h2>` (or a small section header component) followed by content directly on the page background.
3. **`Card`** only for side panels, modular pickers, or things that genuinely need a bordered container.

### Prefer plain sections

```tsx theme={null}
<PageHeader title="Agent configuration" />

<section className="space-y-4">
  <h2 className="text-lg font-semibold">Prompt</h2>
  <PromptEditor ... />
</section>

<section className="space-y-4">
  <h2 className="text-lg font-semibold">Tools</h2>
  <ToolList ... />
</section>
```

This reads as one page with two concerns, which is usually what you mean. Stacking two full `Card`s 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.

```tsx theme={null}
// Good: side panel
<div className="grid grid-cols-[1fr_320px] gap-6">
  <AgentRunsTable ... />
  <Card>
    <CardHeader>
      <CardTitle>Recent alerts</CardTitle>
    </CardHeader>
    <CardContent>...</CardContent>
  </Card>
</div>
```

<Warning>
  If you find yourself nesting a `Card` inside a `Card`, stop. That's the signal that the outer container should be a plain section, or the inner one should be a plain `<div>`.
</Warning>

## Quick checklist for new pages

Before you open a PR for a new screen, confirm:

* [ ] `PageHeader` is the first element; `breadcrumbs` is either omitted (auto) or an explicit array — never both.
* [ ] Sub-routes under a record use `FilterTabs` wired to real URLs, not in-page `Tabs`.
* [ ] 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. `Card` appears 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 | runs` and friends) ✅
* **BORD-331** — auto-derived breadcrumbs with a small registry + explicit override escape hatch ✅
* **BORD-332** — page-grammar refactor: `PageHeader` + sections, `Card` demoted to side-panel duty ✅
* **BORD-333** — this doc + a design-token cleanup on the `/web` landing CTA ✅
