# Details list

Titled groups of label-and-value rows for a card where a person reads and confirms how something will be done: the commands that will run, the checks and their time limits, the services and their ports, the paths that carry risk. Each group is a real description list (`dl`, `dt`, `dd`), so a screen reader reads it as one, and its heading is a button that folds the rows away (`aria-expanded`), with an optional `summary` shown while it is folded. A folded group's rows stay in the page, hidden until found, so the browser's find-in-page can reach them and opens the group when it lands on one. A `command` value is monospace, wraps, and has a copy control that says "Copied" only once the clipboard took it. Nothing is hidden from the person who has to confirm: groups start open unless you say otherwise, and an unknown value shows as "Not known". A group the person must read before confirming stays open: leave its `defaultOpen` alone, or set `collapsible` to false so it can't be folded.

`AttributesRail` is the flat sibling, one list of facts about a record with no groups, folding or commands. It draws no border or padding of its own, so it sits in a `Surface` or a card.

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

## Install

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

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/details-list`.
It brings in: collapsible, locale, motion, tokens, use-copy, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import { DetailsList } from "@/components/candor/details-list"

export default function Example() {
  return (
    <div className="max-w-xl rounded-xl border bg-card p-4">
      <p className="text-sm font-semibold">How the billing service will be built and checked</p>
      <p className="mt-0.5 text-xs text-muted-foreground">Read each part, then confirm below.</p>
      <DetailsList
        className="mt-2"
        groups={[
          {
            id: "install",
            title: "Install",
            summary: "2 commands",
            rows: [
              { label: "Dependencies", value: "npm ci", kind: "command" },
              { label: "Database", value: "npm run db:migrate -- --env staging", kind: "command" },
            ],
          },
          {
            id: "checks",
            title: "Checks",
            summary: "3 checks, 2 with time limits",
            rows: [
              { label: "Unit tests", value: "npm test — stops after 5 minutes" },
              { label: "Types", value: "npm run typecheck — stops after 2 minutes" },
              { label: "Lint", value: "npm run lint — no time limit" },
            ],
          },
          {
            id: "services",
            title: "Services",
            summary: "1 service",
            defaultOpen: false, // Folded only to show `summary`; a group the person must read before confirming stays open.
            rows: [
              { label: "api", value: "registry.example.com/billing-api:4.2 on port 8080", kind: "code" },
              { label: "Health check", value: "GET /healthz every 10 seconds" },
              { label: "Runbook", value: "Billing on-call", kind: "link", href: "/runbooks/billing" },
            ],
          },
          {
            id: "risk",
            title: "Risky paths",
            summary: "2 paths",
            rows: [
              { label: "Writes to", value: "migrations/, db/seed.sql", kind: "code" },
              { label: "Rollback", value: null },
            ],
          },
        ]}
      />
    </div>
  )
}
```

## <DetailsList>

Props:

- `groups: DetailGroup[]`: The groups, in order.
- `headingLevel?: 2 | 3 | 4 | 5 | 6 | undefined` (default `3`): The heading level of each group's title, so the card fits the page's outline. Defaults to 3.
- `className?: string | undefined`

## Strings (`detailsList`)

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

- `empty`: No details.
- `notKnown`: Not known
- `copy`: Copy {label}
- `copied`: Copied
- `failed`: Couldn’t copy

## Types

### DetailKind

How a row's value is shown: "text" (the default), "command" (monospace, wraps, with a copy control), "code" (monospace, no control) or "link" (inside the app only).

```ts
"text" | "command" | "code" | "link"
```

### DetailRow

One label and its value.

```ts
{
  /** What the value is ("Install", "Port"). It is the row's term, and names the copy control. */
  label: string;
  /** The value, as text (never HTML). A missing, null, empty or blank value shows as "Not known". */
  value: string | null | undefined;
  /** How the value is shown. Defaults to `"text"`. */
  kind?: DetailKind | undefined;
  /** Where a `"link"` row goes. Only an address inside the app is followed (starting `/`, `#` or `?`, not `//`); anything else shows as plain text. */
  href?: string | undefined;
}
```

### DetailGroup

A titled group of rows.

```ts
{
  /** Stable key for the group. */
  id: string;
  /** The group's heading. */
  title: string;
  /** The group's rows, in order. */
  rows: DetailRow[];
  /** Whether the heading folds the rows away. Defaults to true. */
  collapsible?: boolean | undefined;
  /** Whether a collapsible group starts open. Defaults to true. */
  defaultOpen?: boolean | undefined;
  /** A line shown beside the heading while the group is folded, so it can be skimmed shut ("3 checks, 2 with time limits"). */
  summary?: string | undefined;
}
```

## Motion

- **open** (reveal, size): The chevron beside a group turns when the person opens it and turns back when they close it. Under reduced motion it turns at once.
- **tick** (pop, scale): After the person copies a command, a tick (or a cross, if the browser refused the clipboard) pops in on the button once the browser has answered. Nothing shows before the answer, and under reduced motion it fades in.

Set with `--cd-motion-details-list-<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`.
