# App sidebar

The left rail: the brand, a new-thread button, the main places (threads, inbox, approvals…), the thread list, and the signed-in person at the foot.

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

## Install

```bash
npx candor-ui add app-sidebar
```

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

## Example

```tsx
"use client"

import * as React from "react"
import { BookOpen, Inbox, MessagesSquare, Play, Settings, ShieldCheck } from "lucide-react"
import { AppSidebar, type NavItem } from "@/components/app-sidebar"
import { SessionList } from "@/components/session-list"
import type { Session } from "@/lib/team"

const minutesAgo = (m: number) => new Date(Date.now() - m * 60_000).toISOString()

const nav: NavItem[] = [
  { id: "threads", label: "Threads", Icon: MessagesSquare },
  { id: "inbox", label: "Inbox", Icon: Inbox, count: 2, urgent: true },
  { id: "approvals", label: "Approvals", Icon: ShieldCheck, count: 1, urgent: true },
  { id: "runs", label: "Runs", Icon: Play, count: 4 },
  { id: "docs", label: "Docs", Icon: BookOpen },
  { id: "settings", label: "Settings", Icon: Settings },
]

const sessions: Session[] = [
  { id: "s1", title: "Add a region column to orders", updatedAt: minutesAgo(2), status: "running" },
  { id: "s2", title: "Why is checkout slow on Staging?", updatedAt: minutesAgo(40) },
  { id: "s3", title: "Clean up stale feature flags", updatedAt: minutesAgo(60 * 26) },
]

export default function Example() {
  const [place, setPlace] = React.useState("threads")
  const [thread, setThread] = React.useState("s1")
  return (
    <div className="h-[560px] w-64 overflow-hidden rounded-lg border">
      <AppSidebar
        brand={<span className="px-1 text-sm font-semibold">Keel</span>}
        nav={nav}
        activeNav={place}
        onNavigate={setPlace}
        onNewThread={() => {}}
        user={{ name: "Sam", email: "sam@acme.example", role: "Owner · Acme" }}
        usage={{ label: "Agent hours this month", used: 0.62 }}
        onProfile={() => {}}
        onShortcuts={() => {}}
        onSignOut={() => {}}
      >
        <SessionList sessions={sessions} activeId={place === "threads" ? thread : undefined} onOpen={(id) => (setPlace("threads"), setThread(id))} />
      </AppSidebar>
    </div>
  )
}
```

## <AppSidebar>

Props:

- `brand: React.ReactNode`: The product's logo or name, at the top.
- `nav: NavItem[]`: The main places, each with an icon and an optional count.
- `activeNav?: string | undefined`: The id of the current place, marked as the current page.
- `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 (for an app's own router); other clicks open the link as usual.
- `onNewThread?: (() => void) | undefined`: Shows a new-thread button by the brand; called when it's pressed.
- `user: { name: string; email: string; role?: string | undefined }`: The signed-in person, at the foot; the role shows in place of the email when given.
- `usage?: { label: string; used: number } | undefined`: e.g. "62% of this month’s agent hours".
- `onSignOut?: (() => void) | undefined`: Adds Sign out to the person menu; called when it's chosen.
- `onProfile?: (() => void) | undefined`: Adds Profile to the person menu; called when it's chosen.
- `onShortcuts?: (() => void) | undefined`: Adds Keyboard shortcuts to the person menu; called when it's chosen.
- `search?: React.ReactNode | undefined`: Above the places; usually a CommandTrigger.
- `children?: React.ReactNode | undefined`: Usually a SessionList.
- `help?: { href?: string | undefined; onOpen?: (() => void) | undefined; label?: string | undefined } | undefined`: Where people get help, in the same place on every page (WCAG 3.2.6): a link (`href`) or your own handler (`onOpen`), at the foot above the person menu. `label` defaults to "Help".
- `className?: string | undefined`

## Strings (`appSidebar`)

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

- `newThread`: New thread
- `main`: Main
- `waitingOnYou`: waiting on you
- `profile`: Profile
- `keyboardShortcuts`: Keyboard shortcuts
- `signOut`: Sign out
- `help`: Help

## Types

### NavItem

```ts
{ id: string label: string Icon: LucideIcon /** A count shown only when the service supplied it. */ count?: number | undefined /** Count means "needs you": shown in the accent, not grey. */ urgent?: boolean | undefined /** Where the place lives. Makes it a real link, so it opens in a new tab and works without JavaScript. */ href?: string | undefined }
```

## 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` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
