# Scope switcher

Where the person is working, in one control for a top bar: a button with the primary level's name (a client, a workspace) over a line of facts (the environment, with production a warning in words, colour and an icon; the agent's mode as it last reported it, and "Changing to …" while another is asked for; the connection's state, a dot always with its word), and a menu with a choice of one per level. A level with nothing chosen says "Choose {kind}" and nothing is checked; a locked level stays, `aria-disabled`, with its reason. It shows a change only when the app's `levels` do: nothing optimistic. Names are other tenants' text, drawn in their own isolate.

An option can carry a second line (`detail`: "Production · 1,667 components"), and `actions` add the app's own entries at the menu's foot ("Connect another"), after a separator.

`compact` keeps the primary name, production's warning (its icon and the word "Production") and the connection's dot and word, for a narrow bar. `onChangeMode` adds a "Change mode" entry that opens the app's own `PermissionMode` (or settings); the mode's words are the app's, not the kit's. The menu opens through `LayerPortal`, so it sits above the app's own dialogs.

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

## Install

```bash
npx candor-ui add scope-switcher
```

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

## Example

```tsx
"use client"

import * as React from "react"
import { Plus } from "lucide-react"
import { ScopeSwitcher, type ScopeSwitcherLevel } from "@/components/candor/scope-switcher"
import type { Environment } from "@/lib/candor/team"

const clients = [
  { id: "northwind", label: "Northwind" },
  { id: "acme", label: "Acme Robotics" },
]
const environments: Record<string, Environment> = {
  prod: { id: "prod", name: "Northwind", production: true, connection: "connected" },
  uat: { id: "uat", name: "Northwind", detail: "UAT", production: false, connection: "expired" },
}

// The app keeps where the person is; the switcher shows only what the app says, so a change shows once the app has
// made it. "Recommends only" is the app's word for its agent's mode, as the agent last reported it.
export default function Example() {
  const [client, setClient] = React.useState<string | null>("northwind")
  const [env, setEnv] = React.useState("prod")
  const levels: ScopeSwitcherLevel[] = [
    { id: "client", kind: "client", value: clients.find((c) => c.id === client) ?? null, options: clients },
    {
      id: "env",
      kind: "environment",
      value: { id: env, label: env === "prod" ? "Production" : "UAT" },
      // A second line of facts under each option.
      options: [
        { id: "prod", label: "Production", detail: "1,667 components · synced 8 min ago" },
        { id: "uat", label: "UAT", detail: "1,662 components · connection expired" },
      ],
    },
  ]
  return (
    <ScopeSwitcher
      levels={levels}
      onChange={(level, option) => (level === "client" ? setClient(option) : setEnv(option))}
      environment={environments[env]}
      mode={{ label: "Recommends only" }}
      actions={[{ id: "connect", label: "Connect another environment", Icon: Plus, onSelect: () => {} }]}
    />
  )
}
```

## <ScopeSwitcher>

Props:

- `levels: ScopeSwitcherLevel[]`: The levels, outermost first; the first is the primary one, whose name the button shows. ScopeBreadcrumb's levels, an option able to carry a `detail` line.
- `onChange: (levelId: string, optionId: string) => void`: Called with a level's id and the option's id when the person moves to another place at that level.
- `environment?: Environment | undefined`: The environment in scope: its name or detail, production as a warning, and its connection's state.
- `mode?: ScopeSwitcherMode | undefined`: The agent's mode as it last reported it, and one asked for that it hasn't reported yet.
- `onChangeMode?: (() => void) | undefined`: Adds "Change mode" to the menu; called when it's chosen, to open the app's PermissionMode or settings.
- `actions?: ScopeSwitcherAction[] | undefined`: Actions at the menu's foot, after a separator ("Connect another"), each the app's words.
- `compact?: boolean | undefined` (default `false`): Only the primary name, production's warning in words and the connection's state, for a narrow bar.
- `layerClass?: string | undefined`: Classes for the menu's popup, which is portalled; Candor's own layer class is always kept.
- `className?: string | undefined`: Classes for the button, merged with Candor's.

## Strings (`scopeSwitcher`)

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

- `description`: Changes where you're working
- `production`: Production
- `nonProduction`: Non-production
- `connected`: Connected
- `expired`: Connection expired
- `notConnected`: Not connected
- `changingTo`: Changing to {mode}
- `choose`: Choose {kind}
- `changeMode`: Change mode

## Types

### ScopeSwitcherMode

The agent's mode as it last reported it, and the one asked for that it hasn't reported yet.

```ts
{
  /** The mode the agent last reported, in the app's words ("Recommends only"). */
  label: string;
  /** A mode asked for that the agent hasn't reported yet; shown as "Changing to {pending}" beside `label`. */
  pending?: string | undefined;
}
```

### ScopeSwitcherOption

A place a level can be set to, as ScopeBreadcrumb's, with a second line of facts about it.

```ts
ScopeOption & {
  /** A second line under the name, drawn as text ("Production · 1,667 components"). */
  detail?: string | undefined;
}
```

### ScopeSwitcherLevel

One level of a ScopeSwitcher: ScopeBreadcrumb's `ScopeLevel`, its options able to carry a `detail`.

```ts
Omit<ScopeLevel, "options"> & {
  /** What the level can be changed to. */
  options?: ScopeSwitcherOption[] | undefined;
}
```

### ScopeSwitcherAction

An action at the menu's foot, after the levels ("Connect another", in the app's words).

```ts
{
  /** A key for the action. */
  id: string;
  /** Its words, drawn as text. */
  label: string;
  /** An icon before the words. */
  Icon?: LucideIcon | undefined;
  /** Called when the person chooses it. */
  onSelect: () => void;
}
```

## Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

### Environment

Import: `import type { Environment } from "@/lib/candor/team"` (item `team`, https://candor.themethodic.com/docs/team.md)

Where agents act: a CRM instance, a database, a repository, a project. The app names them; LocaleProvider's `nouns` sets what they're called ("project", "workspace").

```ts
{
  id: string;
  /** The name shown everywhere, e.g. "Shop". */
  name: string;
  /** What tells same-named ones apart, e.g. "dev-2" or "staging". */
  detail?: string | undefined;
  /** Changes here reach real users, so components warn and ask for more care. */
  production: boolean;
  /** A chart slot (1–5) so each environment keeps one colour everywhere it appears. */
  hue?: 1 | 2 | 3 | 4 | 5 | undefined;
  connection: "connected" | "expired" | "not-connected";
}
```

## Motion

- **tint** (tint): The switcher's button changes colour when the pointer is over it, and a ring fades in on keyboard focus. Its menu opens as DropdownMenu's does.

Set with `--cd-motion-scope-switcher-<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`.
