# Nav rail

A narrow rail of the app's main places, each an icon above its word (never icon-only), for AppLayout's `sidebar`, in either frame; with `frame="app"` it is the L's vertical arm, under the brand. The current place is a tinted rounded block marked as the current page (in forced colours, a `Highlight` border, so it isn't carried by a background). Every place is at least 44 px tall.

A count sits on the icon's top-end corner only when the place has one: filled in the accent only when it is `urgent` (things waiting on the person), quiet otherwise, and always read in the place's name ("Approvals, 3 waiting"), so it is never colour alone. `groups` split the places with a divider; `pinned` places sit at the foot (a System or Settings place). A place with an `href` is a link (a plain click calls `onNavigate`, for your router; the app's link render draws it when one is set), else a button, as AppSidebar's places are.

In AppLayout's sheet (a narrow frame) the same places draw as rows, icon beside word, with the group labels shown. On a phone, `navRailTabs` gives a TabBar the first four from the same list. The rail is 5rem wide; a brand may set `--cd-nav-rail-width`. With the Pro motion-drawings item's `--cd-draw-highlight`, a box travels behind the place under the pointer (`GlideHighlight`).

- Registry name: `nav-rail` (`@candor/nav-rail`)
- 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/nav-rail

## Install

```bash
npx candor-ui add nav-rail
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/nav-rail`.
It brings in: app-layout, app-sidebar, glide-highlight, link, locale, motion, tab-bar, tokens, utils.

## Example

```tsx
"use client"

import * as React from "react"
import { Activity, Bell, Home, MessagesSquare, Settings, ShieldCheck, SquareKanban, Workflow } from "lucide-react"
import { AppLayout } from "@/components/candor/app-layout"
import { NavRail, useNavRailTabs, type NavRailGroup } from "@/components/candor/nav-rail"
import { TabBar } from "@/components/candor/tab-bar"

const groups: NavRailGroup[] = [
  {
    id: "work",
    label: "Work",
    places: [
      { id: "home", label: "Home", Icon: Home },
      { id: "chat", label: "Chat", Icon: MessagesSquare },
      { id: "board", label: "Board", Icon: SquareKanban },
      // An urgent count is filled and read in the name: "Approvals, 3 waiting".
      { id: "approvals", label: "Approvals", Icon: ShieldCheck, count: 3, urgent: true },
    ],
  },
  {
    id: "watch",
    label: "Watch",
    places: [
      { id: "flows", label: "Flows", Icon: Workflow },
      { id: "activity", label: "Activity", Icon: Activity, count: 8 },
      { id: "alerts", label: "Alerts", Icon: Bell },
    ],
  },
]
const pinned = [{ id: "settings", label: "Settings", Icon: Settings }]
const all = [...groups.flatMap((g) => g.places), ...pinned]

export default function Example() {
  const [place, setPlace] = React.useState("home")
  // The phone's bar takes the places the app picks, from the same list (or the first four without ids); More opens the
  // sheet with the rest. A short rail scrolls its places, the pinned foot kept in view.
  const { destinations, more } = useNavRailTabs(all, { ids: ["home", "chat", "approvals", "alerts"] })
  // One frame at a time: a page has one main. The phone's frame is narrower than tabBarAt, so the bar shows.
  const [size, setSize] = React.useState<"wide" | "phone">("wide")
  return (
    <div className="flex flex-col gap-3">
      <label className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
        Frame
        <select value={size} onChange={(e) => setSize(e.target.value as "wide" | "phone")} className="max-w-full min-w-0 rounded-md border bg-background px-2 py-1 text-foreground">
          <option value="wide">Wide: the rail</option>
          <option value="phone">Phone: the tab bar, the rest behind More</option>
        </select>
      </label>
      {size === "wide" ? (
        <div className="h-[520px] w-full max-w-[420px] overflow-hidden rounded-lg border">
          <AppLayout defaultWidth={420} sidebarAt={400} sidebar={<NavRail places={[]} groups={groups} pinned={pinned} activeId={place} onNavigate={setPlace} />}>
            <div className="p-4 text-sm">
              <h2 className="font-semibold capitalize">{place}</h2>
            </div>
          </AppLayout>
        </div>
      ) : (
        <div className="h-[520px] w-[340px] overflow-hidden rounded-lg border">
          <AppLayout
            defaultWidth={340}
            sidebar={<NavRail places={[]} groups={groups} pinned={pinned} activeId={place} onNavigate={setPlace} />}
            tabBar={<TabBar destinations={destinations} activeId={place} onNavigate={setPlace} more={more} />}
          >
            <div className="p-4 text-sm">
              <h2 className="font-semibold capitalize">{place}</h2>
            </div>
          </AppLayout>
        </div>
      )}
    </div>
  )
}
```

## <NavRail>

Props:

- `places: NavItem[]`: The places, in order. Left out of the drawing when `groups` is given.
- `groups?: NavRailGroup[] | undefined`: The places in groups, drawn in place of `places`, a divider between groups.
- `pinned?: NavItem[] | undefined`: Places pinned to the foot of the rail, below the rest.
- `activeId?: string | undefined`: The id of the place the person is in, marked as the current page, when you control it.
- `defaultActiveId?: string | undefined`: The place to start in, when the rail keeps it itself: with this and no `activeId`, a place chosen in this page (not opened in a new tab) becomes the current one, and `onNavigate` is still called. Without either, none is marked.
- `onNavigate?: ((id: string) => void) | undefined`: Called with a place's id when it's chosen. For a place with an `href`, a plain click calls this instead of following the link; other clicks open the link as usual.
- `label?: string | undefined`: The navigation's accessible name. Defaults to "Main".
- `className?: string | undefined`: Classes for the rail, merged with Candor's.

## Functions

### navRailTabs

```ts
navRailTabs(places: NavItem[], { ids, max = 4, pinned, waiting = (label: string, count: number) => `${label}, ${count} waiting`, }: { /** The ids of the places for the bar, in the bar's order (a pinned place's id included); an id with no place is skipped. */ ids?: string[] | undefined; /** * The rail's `pinned` places (Settings, Help). Not on the bar unless `ids` names one, but counted for `more`: a * pinned place left off the bar makes `more` true, so it can still be reached on a phone. */ pinned?: NavItem[] | undefined; /** Without `ids`, how many of the first places go on the bar. Defaults to 4. */ max?: number | undefined; /** The name of a destination with an urgent count. */ waiting?: ((label: string, count: number) => string) | undefined; } = {})
```

A phone's TabBar destinations from the same places the rail draws. With `ids`, the places the app chose for the phone, in that order (a place the rail doesn't show can be one: pass it in `places` too, and leave it out of the rail's); without, the first `max` (four by default). `more` is true when places are left out of the bar, the rail's `pinned` ones included when they are passed as `pinned` (a pinned Settings that isn't on the bar can still be reached): give it to TabBar's `more`, whose button opens the sidebar sheet with the rest, or leave More off. An urgent count becomes the destination's badge, read by `badgeLabel` in the rail's words ("Approvals, 3 waiting"); a quiet count is left off the bar, which has no quiet badge. `waiting` makes that name (it gets the place's label and its count); it defaults to the English "{label}, {count} waiting" with the count unformatted. In a component, use `useNavRailTabs`, which makes the name from LocaleProvider (`navRail.waiting`, the count through its number formatter), as the rail does.

### useNavRailTabs

```ts
useNavRailTabs(places: NavItem[], options: Parameters<typeof navRailTabs>[1] = {})
```

`navRailTabs` in a component: the same destinations, each counted name made as the rail makes its own, from LocaleProvider's `navRail.waiting` with the count through its number formatter, so the bar and the rail read alike in every language. `waiting`, given, still wins.

## Strings (`navRail`)

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

- `main`: Main
- `waiting`: {label}, {count} waiting
- `counted`: {label}, {count}

## Types

### NavRailGroup

A group of places in the rail, with a divider before it; its label is read by screen readers, and shown in a sheet.

```ts
{
  /** A key for the group. */
  id: string;
  /** The group's name: visually hidden in the rail, shown above its places in a sheet. */
  label: string;
  /** The group's places, in order. */
  places: NavItem[];
}
```

## Motion

- **tint** (tint): A place's background and text colour change when the pointer is over it and for the current page, and a ring fades in on keyboard focus. The place that is current when the rail first appears is drawn current at once.

Set with `--cd-motion-nav-rail-<moment>-*` in your brand file; the families are in the design reference.

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