# Live rows

The rows of a list whose data changes under the person: Inbox's items and ApprovalQueue's requests. A row added after the list first drew rises in; one that leaves the data stays, inert and hidden from screen readers, for its out moment in the place it had; a real re-sort slides each row to its new place. With `selection`, each live row has a checkbox in a 24px target before it. The item draws each row's content (`row`) and its empty state (`empty`), and passes its own motion moments, so each item's motion is named for it.

Key it by the view it shows, so changing the view starts it over: its rows are there, not arriving or leaving. An internal part of Inbox and ApprovalQueue, installed with them.

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

## Install

```bash
npx candor-ui add live-rows
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/live-rows`.
It brings in: motion, tokens, utils.

## Example

```tsx
"use client"

import * as React from "react"
import { LiveRows } from "@/components/candor/live-rows"
import { motion } from "@/lib/candor/motion"

/** The example's own moments, as Inbox and ApprovalQueue name theirs. */
const rowMotion = {
  arrive: { style: motion("live-rows-example", "arrive", "arrive"), className: "cd-arrive" },
  leave: { style: motion("live-rows-example", "leave", "reveal"), className: "cd-reveal-out calm:hidden" },
  reorder: { style: motion("live-rows-example", "reorder", "move"), className: "data-[reorder]:cd-move" },
}

type Task = { id: string; title: string }

export default function Example() {
  const [tasks, setTasks] = React.useState<Task[]>([
    { id: "a", title: "Review the migration" },
    { id: "b", title: "Answer the region question" },
  ])
  const [sel, setSel] = React.useState<Set<string>>(() => new Set())
  const next = React.useRef(3)
  return (
    <div className="flex max-w-md flex-col gap-3">
      <div className="flex flex-wrap gap-2">
        <button
          type="button"
          className="rounded-md border px-2 py-1 text-sm"
          onClick={() => {
            const n = next.current++
            setTasks((list) => [{ id: `t${n}`, title: `New task ${n}` }, ...list])
          }}
        >
          Add one
        </button>
        <button type="button" className="rounded-md border px-2 py-1 text-sm" onClick={() => setTasks((list) => list.slice(1))}>
          Clear the first
        </button>
        <button type="button" className="rounded-md border px-2 py-1 text-sm" onClick={() => setTasks((list) => [...list].reverse())}>
          Re-sort
        </button>
      </div>
      <div className="rounded-xl border bg-card">
        <LiveRows
          items={tasks}
          keyOf={(task) => task.id}
          motion={rowMotion}
          className="relative divide-y"
          rowClassName="flex items-center"
          selection={{
            sel,
            toggle: (id) =>
              setSel((s) => {
                const out = new Set(s)
                if (out.has(id)) out.delete(id)
                else out.add(id)
                return out
              }),
            label: (task) => `Select ${task.title}`,
            className: "flex shrink-0 items-center ps-3",
          }}
          empty={<p className="px-4 py-8 text-center text-sm text-muted-foreground">Nothing left.</p>}
          row={(task) => (
            <button type="button" className="flex-1 px-3 py-2.5 text-start text-sm outline-none hover:bg-accent focus-visible:bg-accent">
              {task.title}
            </button>
          )}
        />
      </div>
    </div>
  )
}
```

## <LiveRows>

Props:

- `items: T[]`: The rows to draw, in order.
- `keyOf: (item: T) => string`: A row's id: its key, `data-row-id`, and what `selection` holds.
- `motion: LiveRowsMotion`: The item's own motion moments.
- `row: (item: T) => React.ReactNode`: A row's content, after its checkbox: usually the button that opens it.
- `empty: React.ReactNode`: Drawn in place of the list while there are no rows, leaving ones included.
- `selection?: LiveRowsSelection<T> | undefined`: Adds a checkbox before each live row.
- `className?: string | undefined`: Classes for the list.
- `rowClassName?: string | undefined`: Classes for the box inside each row that holds its checkbox and content.

## Types

### LiveRowsMoment

One of the item's motion moments: its chain, made with `motion(item, moment, family)` in the item's file, and the family utility that plays it, named there too, so each item's motion stays its own.

```ts
{
  /** The moment's chain, for the row's style. */
  style: React.CSSProperties;
  /** The family utility (and any classes it needs), for the row's classes. */
  className: string;
}
```

### LiveRowsMotion

The item's own motion moments.

```ts
{
  /** A row added after the list first drew rises in. */
  arrive: LiveRowsMoment;
  /** A row that left the data collapses out. */
  leave: LiveRowsMoment;
  /** A row slides to its new place when the data re-sorts the list; the part sets and applies the row's offset. */
  reorder: LiveRowsMoment;
}
```

### LiveRowsSelection

Selection on the rows: a checkbox before each one that isn't leaving.

```ts
{
  /** The ids selected. */
  sel: Set<string>;
  /** Ticks or clears a row by its id. */
  toggle: (id: string) => void;
  /** The checkbox's name for a row, in the item's words ("Select {title}"). */
  label: (item: T) => string;
  /** Classes for the box's 24px target's holder, which places it beside the row. */
  className: string;
}
```

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