Skip to content

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.

The commands

  1. 01: 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.

    Look first
  2. 02: 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.

    Update
  3. 03: 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.

    Run the codemods
  4. 04: 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.

    Finish by hand, then check

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.