# Split view

A list and the detail of what is open in it: side by side when there is room, one at a time when there isn't (a phone, a narrow panel). It responds to its own width, not the window's, so it behaves the same full-screen or embedded.

Narrower than `at`, the detail takes the whole width while something is open, with a Back button that says where it goes ("Back to Inbox"); Back calls `onBack`, and the app clears what is open, so `detail` becomes null. The list stays mounted while it is hidden, so its state (a search, a filter, an open group) is kept; its scroll position is put back when it shows again, and focus goes back to the element in the list that had it when the detail opened (the list itself if that element is gone). Focus never falls to the page when the detail closes, at any width.

The list pane scrolls (it is the scroller), so let your list grow to its content inside it.

- Registry name: `split-view` (`@candor/split-view`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/split-view

## Install

```bash
npx candor-ui add split-view
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/split-view`.
It brings in: button, locale, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { SplitView } from "@/components/candor/split-view"

const messages = [
  { id: "m1", from: "Billing agent", subject: "Invoice 2041 is overdue", body: "The invoice for Acme is 12 days overdue. I drafted a reminder; it is waiting for you in Approvals." },
  { id: "m2", from: "Deploy agent", subject: "Storefront deployed to Staging", body: "Build 418 is on Staging. The smoke tests passed in 2 minutes 10 seconds." },
  { id: "m3", from: "Access agent", subject: "Sam asked for admin on Billing", body: "Sam asked for admin on Billing until Friday. Nothing changes until you approve." },
]

export default function Example() {
  const [open, setOpen] = React.useState<string | null>(null)
  const [narrow, setNarrow] = React.useState(false)
  const message = messages.find((m) => m.id === open)
  return (
    <div className="flex flex-col gap-3">
      <label className="flex items-center gap-1.5 text-xs text-muted-foreground">
        <input type="checkbox" checked={narrow} onChange={(e) => setNarrow(e.target.checked)} />
        Phone width
      </label>
      <div className={narrow ? "h-80 w-80 overflow-hidden rounded-lg border" : "h-80 overflow-hidden rounded-lg border"}>
        <SplitView
          // Below `at` (720 by default) one pane shows at a time; this preview is narrower, so it's set lower.
          at={480}
          listWidth={240}
          listLabel="Inbox"
          detailLabel="Message"
          // Back clears what's open; the list comes back where the person left it.
          onBack={() => setOpen(null)}
          list={
            <ul className="flex flex-col p-1">
              {messages.map((m) => (
                <li key={m.id}>
                  <button
                    type="button"
                    aria-current={m.id === open ? "true" : undefined}
                    onClick={() => setOpen(m.id)}
                    className="flex w-full flex-col items-start rounded-md px-2 py-1.5 text-start text-sm hover:bg-accent aria-[current]:bg-accent"
                  >
                    <span className="font-medium">{m.subject}</span>
                    <span className="text-xs text-muted-foreground">{m.from}</span>
                  </button>
                </li>
              ))}
            </ul>
          }
          detail={
            message ? (
              <article className="flex flex-col gap-2 p-4 text-sm">
                <h3 className="font-semibold">{message.subject}</h3>
                <p className="text-xs text-muted-foreground">From {message.from}</p>
                <p>{message.body}</p>
              </article>
            ) : null
          }
        />
      </div>
    </div>
  )
}
```

## <SplitView>

Props:

- `list: React.ReactNode`: The list: a SessionList, an Inbox, rows of your own. It stays mounted when it is hidden behind the detail.
- `detail: React.ReactNode | null`: What is open, or null when nothing is.
- `listLabel: string`: The list pane's accessible name ("Inbox"); Back says it: "Back to Inbox".
- `detailLabel: string`: The detail pane's accessible name ("Message").
- `onBack: () => void`: Called by Back when only one pane fits. Clear what is open, so `detail` becomes null and the list shows again.
- `at?: number | undefined` (default `720`): The frame width in pixels below which one pane shows at a time. Defaults to 720.
- `listWidth?: number | undefined` (default `320`): The list pane's width in pixels when both show. Defaults to 320.
- `defaultWidth?: number | undefined` (default `0`): The frame width to lay out for until it can be measured: on the server, and in test environments like jsdom that measure everything as 0. Defaults to 0, one pane at a time.
- `className?: string | undefined`: Classes for the frame, merged with Candor's.

## Strings (`splitView`)

The fixed words, English by default. Override any of them under `splitView` in `<LocaleProvider strings={{ splitView: { … } }}>`; the table is exported as `splitViewStrings`.

- `back`: Back to {list}

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
