# Environment list

The environments an app is connected to, one row each: its name and kind (production marked as a warning), its connection in words ("Connected", "Connection expired", "Not connected"), when it last synced ("Never synced" when it hasn't), and a line about it. The active one says "Active" and has `aria-current`.

Set active is on the others; on one that isn't connected it stays, locked, saying it needs reconnecting. Reconnect is on an expired or disconnected one (open `EnvironmentConnect` from `onReconnect`). Disconnect asks first, in the row: what stops and what stays, with "Disconnect" and "Keep connected" and neither chosen for the person; focus moves to the question, so it is read once. Disconnecting the active one adds that nothing is active until another is set. "Disconnected" shows only once `onDisconnect`'s promise resolves, until the app passes the new state; a rejection says why and that it's still connected. When a control leaves the row (Set active once it's active, Disconnect once it's disconnected), focus moves to the row's next control, never to the page's body.

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

## Install

```bash
npx candor-ui add environment-list
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/environment-list`.
It brings in: bidi, button, environment-band, freshness-stamp, locale, locked, team, tokens, use-announcer, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { EnvironmentList, type ConnectedEnvironment } from "@/components/candor/environment-list"

const hoursAgo = (h: number) => new Date(Date.now() - h * 3_600_000).toISOString()

const start: ConnectedEnvironment[] = [
  { id: "prod", name: "Shop", production: true, hue: 1, connection: "connected", active: true, lastSyncAt: hoursAgo(0.2), summary: "1,667 items" },
  { id: "staging", name: "Shop", detail: "staging", production: false, hue: 2, connection: "connected", lastSyncAt: hoursAgo(26), summary: "1,640 items" },
  { id: "old", name: "Shop", detail: "dev-2", production: false, hue: 3, connection: "not-connected", lastSyncAt: null },
]

export default function Example() {
  const [environments, setEnvironments] = React.useState(start)
  return (
    <EnvironmentList
      className="max-w-2xl"
      environments={environments}
      onSetActive={(id) => setEnvironments((all) => all.map((e) => ({ ...e, active: e.id === id })))}
      onReconnect={() => {}}
      // Stands in for your service: it answers after a moment, then the list shows what it reports.
      onDisconnect={async (id) => {
        await new Promise((resolve) => setTimeout(resolve, 900))
        setEnvironments((all) => all.map((e) => (e.id === id ? { ...e, connection: "not-connected", active: false } : e)))
      }}
      disconnect={{
        stops: ["Syncing stops", "Agents can no longer read or change anything there"],
        stays: ["Everything already synced, and the history of what agents did"],
      }}
      onConnect={() => {}}
    />
  )
}
```

## <EnvironmentList>

Props:

- `environments: ConnectedEnvironment[]`: The environments, in order.
- `onSetActive?: ((id: string) => void) | undefined`: Makes one the active one. The app passes the list back with it `active`.
- `onReconnect?: ((id: string) => void) | undefined`: Starts reconnecting an expired or disconnected one: open `EnvironmentConnect`.
- `onDisconnect?: ((id: string) => Promise<void> | void) | undefined`: Disconnects one, after the person confirms. "Disconnected" shows once the promise resolves; a rejection's message says why it failed.
- `disconnect: EnvironmentDisconnect`: What disconnecting stops and what stays, shown before the person confirms.
- `onConnect?: (() => void) | undefined`: Connects another: shown as "Connect another".
- `label?: string | undefined`: The list's name. Defaults to "Connected environments", in the app's noun.
- `className?: string | undefined`

## Strings (`environmentList`)

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

- `label`: Connected {environments}
- `empty`: No {environments} connected yet.
- `production`: Production
- `nonProduction`: Non-production
- `connected`: Connected
- `expired`: Connection expired
- `notConnected`: Not connected
- `disconnected`: Disconnected
- `active`: Active
- `synced`: Synced
- `setActive`: Set active
- `setActiveLocked`: Reconnect it before setting it active.
- `reconnect`: Reconnect
- `disconnect`: Disconnect
- `setActiveName`: Set active: {name}
- `reconnectName`: Reconnect {name}
- `disconnectName`: Disconnect {name}
- `connectAnother`: Connect another
- `disconnectTitle`: Disconnect {name}?
- `stops`: What stops
- `stays`: What stays
- `noneActive`: Nothing will be active until you set another.
- `keep`: Keep connected
- `confirmDisconnect`: Disconnect
- `disconnecting`: Disconnecting…
- `busy`: Wait for the disconnect to finish.
- `doneAnnounce`: {name} disconnected.
- `failed`: Couldn’t disconnect {name}: {reason}. It’s still connected.
- `noReason`: the service didn’t say why

## Types

### ConnectedEnvironment

An environment as the list shows it: which one is active, when it last synced, and a line about it.

```ts
Environment & {
  /** The one the app acts on now. One at most. */
  active?: boolean | undefined;
  /** When it last synced (ISO date); null when it never has. */
  lastSyncAt: string | null;
  /** A line about it in the app's words ("1,667 items"), drawn as plain text. */
  summary?: string | undefined;
}
```

### EnvironmentDisconnect

What disconnecting stops and what stays, in the app's words, as `IntegrationConnect` says it.

```ts
{
  /** What stops: "Syncing stops", "Agents lose access". */
  stops: string[];
  /** What stays: "History and reports stay". */
  stays: string[];
}
```

## 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";
}
```

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