Skip to content
Tier

Scope switcher

Pro

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.

Markdown
Version 5.2.0· other versions

This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).

Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2, 4.1.0, 4.2.0 and 4.3.0: the page came later, or had another name then.

Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.

Preview

Loading the preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/scope-switcher

Brings in: bidi, dropdown-menu, link, locale, motion, scope-breadcrumb, team, tokens, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"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>

PropTypeDefaultDescription
levelsrequiredScopeSwitcherLevel[]—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.
onChangerequired(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. An option with `href` is a link: a plain click (or Enter) calls this and also follows the link, through the app's link render (`CandorProvider`'s `link`), so don't navigate here as well; a click that opens it in a new tab or window (a modifier key, the middle button) only opens the link, since this page's place doesn't change.
environmentEnvironment | undefined—The environment in scope: its name or detail, production as a warning, and its connection's state.
modeScopeSwitcherMode | 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.
actionsScopeSwitcherAction[] | undefined—Actions at the menu's foot, after a separator ("Connect another"), each the app's words.
compactboolean | undefinedfalseOnly the primary name, production's warning in words and the connection's state, for a narrow bar.
popupClassNamestring | undefined—Classes for the menu's popup, which is portalled; Candor's own layer class (`foundation/layer`'s `layerClass`) is always kept.
classNamestring | undefined—Classes for the button, merged with Candor's.

Strings

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

KeyEnglish
descriptionChanges where you're working
productionProduction
nonProductionNon-production
connectedConnected
expiredConnection expired
notConnectedNot connected
changingToChanging to {mode}
chooseChoose {kind}
changeModeChange mode

Motion

MomentFamilyWhat it does
tinttintThe 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.

Types

ScopeSwitcherMode
The agent's mode as it last reported it, and the one asked for that it hasn't reported yet.
{ /** 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.
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`.
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).
{ /** 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 from @/lib/candor/team
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").
{ 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"; }