# Change tree

Changed files as a tree of folders, on Tree: each file with a chip for its change in ChangeDiff's words (N new, C changed, D deleted, and a dot for unchanged, which is no letter; the letters and words are strings, so a translation has its own), the word read before its name ("Deleted: billing.ts"), deleted files struck through and still listed, each folder with how many of its files changed, and a folder that holds only one folder joined to it in one row (`src/billing`). The colour is never alone: the letter, the word and the strike say the change. Paths read left to right in a right-to-left page. A file's name draws in `--cd-font-name` when the app sets it (the token ChangeDiff's file names read, so the two match); unset, it keeps the tree's own font.

Selecting a file calls `onSelect` with its path, so one list drives the tree and a ChangeDiff beside it: pass the same `selected` to both. A `FileDiff[]` can be passed as `files` as it is. The keyboard is Tree's: ↑↓ move, → opens, ← closes, Enter selects.

Its strings are overridden under `changeTree` in LocaleProvider's `strings`.

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

## Install

```bash
npx candor-ui@3 add change-tree
```

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

## Example

```tsx
"use client"

import * as React from "react"
import type { FileDiff } from "@/lib/candor/work"
import { ChangeDiff } from "@/components/candor/change-diff"
import { ChangeTree } from "@/components/candor/change-tree"

const line = (text: string, newNo: number) => ({ kind: "add" as const, text, newNo })

const files: FileDiff[] = [
  { path: "web/src/customers/CustomerHeader.tsx", change: "update", language: "tsx", hunks: [{ lines: [{ kind: "remove", text: '<Fact label="Partner tier" />', oldNo: 44 }, line('<Fact label="Region" />', 44)] }] },
  { path: "web/src/customers/RegionBadge.tsx", change: "create", language: "tsx", hunks: [{ lines: [line("export function RegionBadge() {", 1), line("  return null", 2), line("}", 3)] }] },
  { path: "web/src/customers/PartnerTier.tsx", change: "delete", language: "tsx", hunks: [{ lines: [{ kind: "remove", text: "export function PartnerTier() {}", oldNo: 1 }] }] },
  { path: "db/migrations/0142_add_customer_region.sql", change: "create", language: "sql", hunks: [{ lines: [line("ALTER TABLE customers ADD COLUMN region text;", 1)] }] },
]

export default function Example() {
  // One selection drives both: the tree marks the file the diff shows, and picking either moves the other.
  const [path, setPath] = React.useState<string | null>(files[0]!.path)
  return (
    <div className="flex flex-wrap items-start gap-3">
      <div className="w-72 rounded-xl border bg-card p-1.5">
        <ChangeTree files={files} selected={path} onSelect={setPath} />
      </div>
      <ChangeDiff className="min-w-0 flex-1 basis-96" files={files} selected={path} onSelect={setPath} />
    </div>
  )
}
```

## <ChangeTree>

Props:

- `files: readonly ChangeTreeFile[]`: The files, each with its path and how it changed. A ChangeDiff's `files` can be passed as they are.
- `selected?: string | null | undefined`: The path of the selected file, when the app holds the selection: marked in the tree, and changed only by the app (`onSelect` says what the person picked). `null` marks none.
- `defaultSelected?: string | null | undefined`: The path of the file selected at first, when the tree holds the selection: given this and no `selected`, the tree marks the file the person picks, starting here, and still calls `onSelect`. `null` starts with none. With neither prop the tree holds no selection and marks none; `onSelect` is still called.
- `onSelect?: ((path: string, file: ChangeTreeFile) => void) | undefined`: Called with a file's path, and the file, when the person clicks it or presses Enter or Space on it. Folders aren't selected.
- `label?: string | undefined`: The tree's accessible name. "Changed files" by default.
- `className?: string | undefined`: Classes for the tree, merged with Candor's.

## Strings (`changeTree`)

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

- `label`: Changed files
- `create`: New
- `update`: Changed
- `delete`: Deleted
- `unchanged`: Unchanged
- `createLetter`: N
- `updateLetter`: C
- `deleteLetter`: D
- `unchangedMark`: ·
- `changeName`: {change}: 
- `folderChanges`: { one: "{count} change", other: "{count} changes" }
- `empty`: No changed files.

## Types

### ChangeTreeFile

A file in a ChangeTree: its path, with `/` between folders, and how it changed. A `FileDiff` is one, so the list that drives ChangeDiff drives ChangeTree too; `"unchanged"` lists a file the change didn't touch.

```ts
{ path: string; change: "create" | "update" | "delete" | "unchanged" }
```

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