# Upgrading to 3.0

How to take a Candor UI 2.x project to 3.0: the commands, then every change 3.0 makes with what breaks, where it shows, what to do and the codemod that does it. The same list is in each item's registry entry and is what `candor-ui update --to 3.0.0` prints. The release's notes are in the changelog: https://candor.themethodic.com/changelog#3-0-0.

## The commands

### 1. Look first

From the app's folder (the one with `components.json`), with your work committed. This prints what 3.0 changes for the items in `candor-lock.json`, with the moves and merges the update would make, and writes nothing.

```bash
npx candor-ui@1 update --to 3.0.0 --dry-run
```

### 2. Update

Moves Candor's files to `components/candor/` and `lib/candor/`, copies Candor's ui primitives to `components/candor/ui/`, points the imports in your project at the new places, records the moves in `candor-lock.json`, and then updates every item, merging your changes into Candor's. `components/ui/` stays, and is yours from now on. An item 3.0 no longer has (`terms`) is listed, left where it is and dropped from `candor-lock.json`.

```bash
npx candor-ui@1 update --to 3.0.0
```

### 3. Run the codemods

Runs, in this order, those of 3.0's codemods (`metadata-diff`, `paths`, `orgdata`, `card`, `terms`, `strings-keys`, `refusal`, `motion-aliases`) that an entry on one of your installed items names, and lists the ones it leaves out (all of them run where there's no `candor-lock.json`); `npx candor-ui@1 migrate <codemod>` runs one anyway. Each says what it changed and lists what it couldn't under "Left for you". Add `--dry-run` to look first.

```bash
npx candor-ui@1 migrate 3.0.0
```

### 4. Finish by hand, then check

Make the changes below marked "By hand", then type-check and build. Once `candor-lock.json` records 3.x, `lint` reports a name 3.0 removed that is still in your code as an error, naming its codemod.

```bash
npx tsc --noEmit && npm run build && npx candor-ui@1 lint --strict
```

## Before you start

- **Use CLI 1.0.1 or later.** `npx candor-ui@1` runs the newest 1.x, and everyone working on the project should run the upgrade with 1.0.1 or later: 1.0.0 put files where 1.0.1 doesn't, and a lockfile both have written can disagree with the files on disk. 1.0.1 doesn't nest Candor's files again where components.json's `components` (or `lib`) alias is already a folder named `candor` (`@/candor`: `web/candor/thread.tsx`, not `web/candor/candor/thread.tsx`), keeps a file whose move `candor-lock.json` records where you put it (the theme's stylesheets) and installs a new stylesheet beside it, takes the release's imports in a Candor file you changed that stays where it is, and lists the codemods `migrate 3.0.0` leaves out.
- `update`, when it moves files, and `migrate` read your source with the project's own TypeScript 5. On TypeScript 7, which has no JavaScript API yet, install 5 for the run and put 7 back after; candor-ui never installs it itself.
- **Stored messages are data, not source.** No codemod reaches a database or a saved conversation: change a stored block's `kind` from `orgData` to `measured` once, where it is stored.
- **2.x stays installable.** `https://candor.themethodic.com/r/v/2.10.0/<name>.json` serves every item as 2.10.0 shipped it, with the same licence check, and `npx candor-ui add <item> --version 2.10.0` installs from there.
- **Monorepos need CLI 1.0.** 3.0 installs Candor's primitives in the app (`components/candor/ui/`), not in the shared ui package, and only CLI 1.0 follows that.
- **`add` at 3.0 on a 2.x project stops** and names `update --to 3.0.0`, so a 3.0 item never lands among 2.x files.

## What changes (16)

### orgdata

The `orgData` inline node is removed. It was renamed `measured` in 1.0 and has been drawn as `measured` since.

- **On:** `chat`, `message-content`
- **What breaks:** A message block that carries `{ kind: "orgData", … }` fails the type check. Untyped data (a server's JSON, stored messages) draws nothing where the node was, though the message's plain-text copy still has its text.
- **Shows in:** the type check, what is drawn
- **What to do:** Change `kind: "orgData"` to `kind: "measured"` wherever the blocks are built (the app's adapter or the server's output); the fields are the same. Stored messages are data: update their `kind` once.
- **Codemod:** `npx candor-ui@1 migrate orgdata`, which `migrate 3.0.0` runs.

### metadata-diff

`MetadataDiff`, the old name of `ChangeDiff`, is removed, and the item is renamed `change-diff`, so its file installs as `change-diff.tsx`.

- **On:** `metadata-diff` (renamed `change-diff`)
- **What breaks:** `import { MetadataDiff } from "@/components/metadata-diff"` fails the type check, and imports of the item's old path stop resolving once its file has moved.
- **Shows in:** the type check, imports
- **What to do:** Import `ChangeDiff` instead of `MetadataDiff` and rename the tag; the props are the same, so you can do it by hand on 2.x now. As you move to 3.0, `npx candor-ui update --to 3.0.0` moves the installed file to `change-diff.tsx`, and then `npx candor-ui migrate metadata-diff` renames the import and the tag. Before the update it refuses without `--for 3.0.0`, since adding an item that uses metadata-diff installs `metadata-diff.tsx` again. From 2.10, MetadataDiff logs a console warning in development, once per page.
- **Codemod:** `npx candor-ui@1 update --to 3.0.0` moves metadata-diff.tsx to change-diff.tsx; then `npx candor-ui@1 migrate metadata-diff` (part of `migrate 3.0.0`) renames MetadataDiff to ChangeDiff in your files.

### attention-kinds

`AttentionKind` gains `reply` and `silent`: `signal({ kind: "reply" })` plays the `reply` sound without a `sound` option, and `counts` has a count for each.

- **On:** `use-attention`
- **What breaks:** A `Record<AttentionKind, number>` built by hand misses two keys, and an exhaustive `switch` on the kind with `never` in its `default` no longer compiles. Both fail at the type check.
- **Shows in:** the type check
- **What to do:** Add `reply: 0, silent: 0` to a record built by hand (or build it from a list of kinds), and handle the two new kinds in a `switch` or give it a `default`. Code that only reads `counts.approval` and the like changes nothing.
- **Codemod:** By hand: no codemod can make this change safely.

### attention-sound-when

`useAttention`'s `sound.when` defaults to "away": left out, a sound plays only while the window isn't in front of the person.

- **On:** `use-attention`
- **What breaks:** Nothing fails to compile. An app that never passes `when` stops playing sounds while the person is looking at the page.
- **Shows in:** behaviour
- **What to do:** Pass `when` in `useAttention`'s `sound` option: "always" keeps 2.x's behaviour, "away" is 3.0's default. `sound-setting` already lets the person choose. From 2.10, useAttention logs a console warning in development, once per page, for a `sound` without `when`.
- **Codemod:** By hand: no codemod can make this change safely.

### motion-aliases

The 2.2 motion tokens `--cd-pop-from` and `--cd-motion-shake`, kept through 2.x as the last fallback of their families, are no longer read, and `data-motion="expressive"` no longer sets their presets.

- **On:** `tokens`, `theme`
- **What breaks:** A brand file or the app's own CSS that sets `--cd-pop-from` or `--cd-motion-shake` stops having an effect.
- **Shows in:** styling
- **What to do:** `--cd-pop-from` becomes `--cd-motion-pop-scale`, which applies from the expressive level only. `--cd-motion-shake` set a whole animation at every level and has no single replacement: set the refuse family's `--cd-motion-refuse-distance`, `-dur` and `-ease`, which move at the expressive level only.
- **Codemod:** `npx candor-ui@1 migrate motion-aliases`, which `migrate 3.0.0` runs.

### legacy-animations

The 2.2 animation utilities (`animate-shimmer`, `animate-pulse-dot`, `animate-rise-in`, `animate-caret`, `animate-collapse-down`, `animate-collapse-up`, `animate-pop-in`, `animate-pop-out`, `animate-tick-draw`, `animate-fill`, `animate-shake`, `animate-attention`, `animate-attention-wait`, `animate-nudge`) are removed, with the unprefixed keyframes only they ran (`pulse-dot`, `rise-in`, `caret`, `collapse-down`, `collapse-up`, `pop-in`, `pop-out`, `tick-draw`, `fill`, `shake`, `attention`). The `shimmer` keyframes stay: the `shimmer-text` utility runs them. Candor's own components use none of these utilities.

- **On:** `tokens`, `theme`
- **What breaks:** A class such as `animate-pop-in` in the app's own markup stops animating, and CSS that names one of the keyframes (`animation: pop-in …`) finds none.
- **Shows in:** styling
- **What to do:** Use the motion families' utilities (`cd-pop`, `cd-reveal`, `cd-arrive`, `cd-refuse`, `cd-attention`, `cd-live-pulse` and the rest), which a brand file and `data-motion` reach; the Design page lists them. For `animate-shimmer`, use `shimmer-text` on text, or write the animation with the `shimmer` keyframes, which stay. The codemod renames `pulse-dot`, `rise-in`, `caret`, `pop-in`, `tick-draw`, `fill` and `attention` keyframes to `cd-pulse-dot`, `cd-rise`, `cd-caret`, `cd-pop`, `cd-draw`, `cd-grow` and `cd-ring` (same frames), and lists `pop-out`, `collapse-down`, `collapse-up` and `shake`, which need a direction or a size or distance variable besides the name.
- **Codemod:** `npx candor-ui@1 migrate motion-aliases`, which `migrate 3.0.0` runs.

### paths

Candor's files install in folders of their own, `components/candor/` and `lib/candor/`, instead of among the app's files in `components/`, `components/ui/` and `lib/`.

- **On:** every item that installs at the default paths: components, hooks, libs, primitives and presets
- **What breaks:** Imports of an item at its 2.x path (`@/components/thread`, `@/lib/chat`) stop resolving once its file has moved.
- **Shows in:** imports
- **What to do:** Nothing to do on 2.x: `add` and `update` still install Candor's files in the old places. From CLI 1.0, `npx candor-ui update --to 3.0.0` makes the moves: it moves the installed files into `components/candor/` and `lib/candor/` (Candor's ui primitives are copied to `components/candor/ui/`, and `components/ui/` stays the app's), rewrites the project's imports of the old paths, records the moves and then updates. `--dry-run` lists the moves first; `npx candor-ui migrate paths --for 3.0.0` makes them on their own. A monorepo needs CLI 1.0 or later. (`update --moved` is only for files someone moved by hand.)
- **Codemod:** `npx candor-ui@1 update --to 3.0.0` makes the moves (`--dry-run` lists them first); `npx candor-ui@1 migrate paths --for 3.0.0` makes them on their own.

### terms

`TermsProvider`, `useTerms` and `capitalise` are folded into LocaleProvider: the app's nouns become arguments of its messages, with plural and select forms, set where the interface's words are set.

- **On:** `terms`, `candor-provider`, `github-kinds`, `hubspot-kinds`, `jira-kinds`, `linear-kinds`, `locale`
- **What breaks:** Code that imports `TermsProvider`, `useTerms`, `capitalise` or `Terms` from `terms`, or passes `terms` to `CandorProvider` or uses a `…Terms` preset, fails the type check. The `terms` item is gone from the registry.
- **Shows in:** the type check
- **What to do:** Run `npx candor-ui migrate terms`. It rewrites `<TermsProvider terms={…}>` to `<LocaleProvider nouns={…}>`, `useTerms()` to `useNouns()`, `capitalise(` to `startOf(`, `Terms` to `NounTable`/`Nouns`, `CandorConfig`'s `terms` to `nouns` and `githubTerms` to `githubNouns`, importing from `@/components/candor/locale`, and lists what it can't follow. Then delete the installed `terms` file (the codemod names its path). Your 2.x words work as they are; add plural forms where a message counts them.
- **Codemod:** `npx candor-ui@1 migrate terms`, which `migrate 3.0.0` runs.

### card

`Card`, card-registry's first name for `RegistryCard`, is removed.

- **On:** `card-registry`
- **What breaks:** `import { Card } from "@/components/card-registry"` fails the type check.
- **Shows in:** the type check
- **What to do:** Import `RegistryCard` from the same file and rename the tag; the props are the same. shadcn's own `Card` in `@/components/ui/card` is not affected.
- **Codemod:** `npx candor-ui@1 migrate card`, which `migrate 3.0.0` runs.

### message-thinking

Message's `thinking` string, renamed `working` in 2.9.0, is removed: the working line reads only `message.working`.

- **On:** `message`
- **What breaks:** An app that overrides `message.thinking` in LocaleProvider's `strings`, and not `message.working`, sees Candor's own "Working" again on a reply that hasn't started.
- **Shows in:** what is drawn
- **What to do:** Rename the key in LocaleProvider's (or CandorProvider's) `strings` from `message.thinking` to `message.working`; the text can stay. 2.9 and later already read `working` first. `npx candor-ui migrate strings-keys` renames it, unless `working` is already there.
- **Codemod:** `npx candor-ui@1 migrate strings-keys`, which `migrate 3.0.0` runs.

### answering-approvals-strings

useAnsweringApprovals' words are read under `answeringApprovals` in LocaleProvider's `strings`, the name its docs give, and no longer under `useAnsweringApprovals`.

- **On:** `use-answering-approvals`
- **What breaks:** An app that still overrides the hook's words under `useAnsweringApprovals` sees Candor's own sentence again when an answer fails to send.
- **Shows in:** what is drawn
- **What to do:** Move your override from `strings.useAnsweringApprovals` to `strings.answeringApprovals`; 2.9 and later read the new key first, and 3.0 reads only the new key. The keys inside (`failed`) are the same. `npx candor-ui migrate strings-keys` renames it, in an object given as `strings` or typed `StringOverrides` or `CandorConfig`.
- **Codemod:** `npx candor-ui@1 migrate strings-keys`, which `migrate 3.0.0` runs.

### block-viewer-of

The blocks' and templates' `viewerOf(request)`, kept in each store since 2.7 when who is signed in moved to the server foundation, is removed.

- **On:** `agent-admin`, `approval-gated-change`, `approve-from-anywhere`, `cost-guard`, `eval-dashboard`, `eval-review-loop`, `human-handoff`, `knowledge-base`, `plan-build-review`, `run-console`, `agent-ops-console`, `approvals-inbox`, `project-workspace`, `support-copilot`
- **What breaks:** Code that imports `viewerOf` from a block's or template's `store` fails the type check.
- **Shows in:** the type check
- **What to do:** Use `getViewer(request)` (the viewer, or null when nobody is signed in) or `viewerName(request)` from the server foundation's `candor-server.ts` (`@/lib/candor/candor-server` in a default install, or under your project's own `lib` alias); both are async, so await them, and handle null. `viewerOf` read the demo cookie only, and gave the demo person (Riley) when nobody was signed in; these read the source given to `setViewerSource()`. In approvals-inbox, `signedInContext(request)` from its store also gives the viewer.
- **Codemod:** By hand: no codemod can make this change safely.

### block-refusal-export

The `Refusal` re-export from these blocks' and templates' stores is removed; it is the server foundation's `Refusal`.

- **On:** `plan-build-review`, `run-console`, `agent-ops-console`, `project-workspace`
- **What breaks:** `import { Refusal } from "./store"` (or the store's installed path) fails the type check.
- **Shows in:** the type check
- **What to do:** Import `Refusal` from the server foundation's `candor-server.ts` instead (`@/lib/candor/candor-server` in a default install, or under your project's own `lib` alias); it is the same class. `npx candor-ui migrate refusal` changes the import.
- **Codemod:** `npx candor-ui@1 migrate refusal`, which `migrate 3.0.0` runs.

### block-db-connect

Each block's and template's `db/connect.ts` (`connectRunDb`, `connectInboxDb` and the like) is removed; the store opens its database through the server foundation's `database()`.

- **On:** `agent-admin`, `approval-gated-change`, `approve-from-anywhere`, `audit-data-controls`, `cost-guard`, `eval-dashboard`, `eval-review-loop`, `human-handoff`, `knowledge-base`, `plan-build-review`, `run-console`, `agent-ops-console`, `approvals-inbox`, `project-workspace`, `support-copilot`
- **What breaks:** Code that imports a `connect…Db` function or `db/connect` fails the type check, and a database driver swapped in `db/connect.ts` is no longer used.
- **Shows in:** the type check, behaviour
- **What to do:** Use `database()` from the server foundation's `candor-database.ts` (`@/lib/candor/candor-database` in a default install, or under your project's own `lib` alias), which opens one pool for every block and template. If you swapped the driver in `db/connect.ts`, give your database to `setDatabase()` at startup instead, which applies it to every block. If you passed a URL other than DATABASE_URL, give that database to `setDatabase()` too.
- **Codemod:** By hand: no codemod can make this change safely.

### block-context-of

`contextOf(request)` in audit-data-controls' and approvals-inbox's stores is removed.

- **On:** `audit-data-controls`, `approvals-inbox`
- **What breaks:** Code that imports `contextOf` from either store fails the type check.
- **Shows in:** the type check
- **What to do:** Use `signedInContext(request)` from the same store: it is async and answers null when nobody is signed in (where `contextOf` fell back to the demo person, Riley), so await it and handle null.
- **Codemod:** By hand: no codemod can make this change safely.

### inbox-post

approvals-inbox's `post(body)` in its shell is removed.

- **On:** `approvals-inbox`
- **What breaks:** Code that imports `post` from the template's `shell` fails the type check.
- **Shows in:** the type check
- **What to do:** Use `usePost()` from the same file in a component: it returns the same function, and says its errors in the words given to LocaleProvider rather than in English only.
- **Codemod:** By hand: no codemod can make this change safely.
