Change list
ProChanges, newest first: what changed, who made it (an agent through its mark and the name the app gives it, or a person), when, and how many lines were added and removed when that's known. A choice above the list shows "Since you last reviewed" or "Everything". The first needs the app's record of the person's last review (lastReviewedAt): without one it stays in the list, locked, saying "No review recorded yet", and is never guessed. A change whose date can't be read is kept under "Since you last reviewed", as it can't be shown to be older.
Each title is text, as the agent or the app wrote it; onOpen makes it a button that opens the change. Choosing says how many changes are shown, once, in a polite announcement.
Its strings are overridden under changeList in LocaleProvider's strings.
Version 5.4.0· other versions
This page is for 5.4.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, 4.3.0, 4.4.0, 4.5.0, 5.0.0, 5.1.0 and 5.2.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
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/change-list
Brings in: agent-avatar, bidi, chat, data, heading-level, heading-scope, locale, locked, page-state, relative-time, tokens, use-announcer, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import * as React from "react"
import { ChangeList, type ChangeListCompare, type ChangeListEntry } from "@/components/candor/change-list"
const ago = (hours: number) => new Date(Date.now() - hours * 3_600_000).toISOString()
const agent = { id: "dev", name: "Dev agent", role: "coder" }
const entries: ChangeListEntry[] = [
{ id: "c4", title: "Show the region on the customer header", by: { kind: "agent", agent }, at: ago(1), added: 14, removed: 3 },
{ id: "c3", title: "Add a region column to customers", by: { kind: "agent", agent }, at: ago(5), added: 6, removed: 0 },
{ id: "c2", title: "Retire the partner tier field", by: { kind: "person", name: "Priya" }, at: ago(30), added: 0, removed: 22 },
{ id: "c1", title: "Rename the billing settings page", by: { kind: "person", name: "Maya" }, at: ago(72) },
]
export default function Example() {
const [compare, setCompare] = React.useState<ChangeListCompare>("since")
const [recorded, setRecorded] = React.useState(true)
return (
<div className="flex max-w-xl flex-col gap-3">
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={recorded} onChange={(e) => setRecorded(e.target.checked)} />
The app has a record of the last review
</label>
{/* Without `lastReviewedAt`, "Since you last reviewed" is locked and says why; the app's record is never guessed. */}
<ChangeList
entries={entries}
lastReviewedAt={recorded ? ago(12) : null}
compare={compare}
onCompareChange={setCompare}
onOpen={() => {}}
/>
</div>
)
}<ChangeList>
| Prop | Type | Default | Description |
|---|---|---|---|
| entriesrequired | ChangeListEntry[] | — | The changes, in any order: they are drawn newest first. |
| lastReviewedAt | string | null | undefined | — | When the person last reviewed, as an ISO date, from the app's own record. Without it (or with a date that can't be read) "Since you last reviewed" is locked and says why. |
| compare | ChangeListCompare | null | undefined | — | The compare choice, when the app holds it: pressed in the list and changed only by the app (`onCompareChange` says what the person chose). `null` presses neither and shows every change. |
| defaultCompare | ChangeListCompare | null | undefined | — | The choice at first, when the list holds it: given this and no `compare`, the list changes it when the person chooses, starting here, and still calls `onCompareChange`. `null` starts with neither pressed. With neither prop the list holds no choice: it shows every change, presses neither, and calls `onCompareChange`. |
| onCompareChange | ((compare: ChangeListCompare) => void) | undefined | — | Called with the choice the person makes. |
| onOpen | ((id: string) => void) | undefined | — | Makes each title a button; called with the change's id. |
| state | ListState | undefined | — | What the list is showing: its rows (ready, the default), loading, empty or error with the reason, or partial with a note over the rows. Loading, empty and error draw in place of the rows, in this component's own words. |
| onRetry | (() => void) | undefined | — | Shows Try again when `state` is an error; called when it's pressed. Pass it only when it loads the list again. |
| headingLevel | HeadingLevel | undefined | — | The level of the heading. Defaults to one below the nearest `HeadingScope`, else 2. |
| className | string | undefined | — | Classes for the list, merged with Candor's. |
Strings
The fixed words, in English by default. Change any of them under changeList in <LocaleProvider strings={{ … }}>; the table is exported as changeListStrings.
| Key | English |
|---|---|
| label | Changes |
| compare | Show |
| since | Since you last reviewed |
| everything | Everything |
| noReview | No review recorded yet |
| byAgent | Agent: {name} |
| byPerson | Person: {name} |
| linesAdded | { one: "{count} line added", other: "{count} lines added" } |
| linesRemoved | { one: "{count} line removed", other: "{count} lines removed" } |
| whenUnknown | Time not known |
| empty | No changes yet. |
| nothingSince | Nothing has changed since you last reviewed. |
| showing | { one: "{count} change shown", other: "{count} changes shown" } |
| loading | Loading changes |
| error | Changes couldn’t be loaded. |
Motion
| Moment | Family | What it does |
|---|---|---|
| tint | tint | A compare choice's text colour changes when the pointer is over it, and its background and ring change when the person chooses it; a ring fades in on keyboard focus. A locked choice doesn't change on hover. |
Set with --cd-motion-change-list-<moment>-* in your brand file; the families are in the design reference.
Types
- ChangeListAuthor
- Who made a change: an agent, drawn with its mark and the name the app gives it, or a person by name. The same shape as Digest's `DigestActor`.
{ kind: "agent"; agent: AgentIdentity } | { kind: "person"; name: string }- ChangeListEntry
- One change in a ChangeList.
{ id: string; /** What changed, in a line. Often the agent's words: drawn as text, never as HTML or a link. */ title: string; by: ChangeListAuthor; /** When it was made, as an ISO date. */ at: string; /** Lines added, when known. Left out (or `null`, or not a finite number), none is drawn: never 0 for unknown. */ added?: number | null | undefined; /** Lines removed, when known. Left out (or `null`), none is drawn. */ removed?: number | null | undefined; }- ChangeListCompare
- ChangeList's compare choice: the changes since the person last reviewed, or every change.
"since" | "all"
Types from other items
The props above take these. Each is defined by another item; import it from the path shown.
- ListState from @/lib/candor/data
- What a list or table is showing, the same shape as `ChartState` and as page-state's `ListState` (which the free tier ships, so it declares its own rather than import this file): its rows (ready), the shape of rows still coming (loading), nothing and why (empty), the rows it has with a note on what's missing (partial), or why it couldn't load (error). Lists without a `state` show their rows.
ChartState- HeadingLevel from @/lib/candor/heading-level
- A heading's level in the page's outline: `1` is `<h1>`, `6` is `<h6>`. Every Candor item that draws a heading takes its `headingLevel` as this type; the level is where the heading sits in the outline, never how large it looks.
1 | 2 | 3 | 4 | 5 | 6- AgentIdentity from @/lib/candor/chat
{ id: string; /** The displayed name, e.g. "Architect agent". Never derived from model output. */ name: string; role: AgentRole; specialty?: string | undefined; }- ChartState from @/lib/candor/data
{ kind: "ready"; } | { kind: "loading"; } /** Nothing to plot, and why. */ | { kind: "empty"; reason: string; } /** Some of the range is missing. */ | { kind: "partial"; note: string; } | { kind: "error"; reason: string; }- AgentRole from @/lib/candor/chat
- What an agent does: "assistant", "coder", "reviewer"… The app registers each role's glyph and hue (AgentRolesProvider).
string