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
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 incandor-lock.json, with the moves and merges the update would make, and writes nothing.Look first02: Update
Moves Candor's files to
components/candor/andlib/candor/, copies Candor's ui primitives tocomponents/candor/ui/, points the imports in your project at the new places, records the moves incandor-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 fromcandor-lock.json.Update03: 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 nocandor-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-runto look first.Run the codemods04: Finish by hand, then check
Make the changes below marked "By hand", then type-check and build. Once
candor-lock.jsonrecords 3.x,lintreports 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@1runs 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'scomponents(orlib) alias is already a folder namedcandor(@/candor:web/candor/thread.tsx, notweb/candor/candor/thread.tsx), keeps a file whose movecandor-lock.jsonrecords 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 codemodsmigrate 3.0.0leaves out. update, when it moves files, andmigrateread 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
kindfromorgDatatomeasuredonce, where it is stored. - 2.x stays installable.
https://candor.themethodic.com/r/v/2.10.0/<name>.jsonserves every item as 2.10.0 shipped it, with the same licence check, andnpx candor-ui add <item> --version 2.10.0installs 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. addat 3.0 on a 2.x project stops and namesupdate --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"tokind: "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 theirkindonce. - Codemod
npx candor-ui@1 migrate orgdata, whichmigrate 3.0.0runs.
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(renamedchange-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
ChangeDiffinstead ofMetadataDiffand 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.0moves the installed file tochange-diff.tsx, and thennpx candor-ui migrate metadata-diffrenames the import and the tag. Before the update it refuses without--for 3.0.0, since adding an item that uses metadata-diff installsmetadata-diff.tsxagain. From 2.10, MetadataDiff logs a console warning in development, once per page. - Codemod
npx candor-ui@1 update --to 3.0.0moves metadata-diff.tsx to change-diff.tsx; thennpx candor-ui@1 migrate metadata-diff(part ofmigrate 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 exhaustiveswitchon the kind withneverin itsdefaultno longer compiles. Both fail at the type check. - Shows in
- the type check
- What to do
- Add
reply: 0, silent: 0to a record built by hand (or build it from a list of kinds), and handle the two new kinds in aswitchor give it adefault. Code that only readscounts.approvaland 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
whenstops playing sounds while the person is looking at the page. - Shows in
- behaviour
- What to do
- Pass
wheninuseAttention'ssoundoption: "always" keeps 2.x's behaviour, "away" is 3.0's default.sound-settingalready lets the person choose. From 2.10, useAttention logs a console warning in development, once per page, for asoundwithoutwhen. - 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-fromor--cd-motion-shakestops having an effect. - Shows in
- styling
- What to do
--cd-pop-frombecomes--cd-motion-pop-scale, which applies from the expressive level only.--cd-motion-shakeset a whole animation at every level and has no single replacement: set the refuse family's--cd-motion-refuse-distance,-durand-ease, which move at the expressive level only.- Codemod
npx candor-ui@1 migrate motion-aliases, whichmigrate 3.0.0runs.
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-inin 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-pulseand the rest), which a brand file anddata-motionreach; the Design page lists them. Foranimate-shimmer, useshimmer-texton text, or write the animation with theshimmerkeyframes, which stay. The codemod renamespulse-dot,rise-in,caret,pop-in,tick-draw,fillandattentionkeyframes tocd-pulse-dot,cd-rise,cd-caret,cd-pop,cd-draw,cd-growandcd-ring(same frames), and listspop-out,collapse-down,collapse-upandshake, which need a direction or a size or distance variable besides the name. - Codemod
npx candor-ui@1 migrate motion-aliases, whichmigrate 3.0.0runs.
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:
addandupdatestill install Candor's files in the old places. From CLI 1.0,npx candor-ui update --to 3.0.0makes the moves: it moves the installed files intocomponents/candor/andlib/candor/(Candor's ui primitives are copied tocomponents/candor/ui/, andcomponents/ui/stays the app's), rewrites the project's imports of the old paths, records the moves and then updates.--dry-runlists the moves first;npx candor-ui migrate paths --for 3.0.0makes them on their own. A monorepo needs CLI 1.0 or later. (update --movedis only for files someone moved by hand.) - Codemod
npx candor-ui@1 update --to 3.0.0makes the moves (--dry-runlists them first);npx candor-ui@1 migrate paths --for 3.0.0makes 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,capitaliseorTermsfromterms, or passestermstoCandorProvideror uses a…Termspreset, fails the type check. Thetermsitem 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()touseNouns(),capitalise(tostartOf(,TermstoNounTable/Nouns,CandorConfig'stermstonounsandgithubTermstogithubNouns, importing from@/components/candor/locale, and lists what it can't follow. Then delete the installedtermsfile (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, whichmigrate 3.0.0runs.
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
RegistryCardfrom the same file and rename the tag; the props are the same. shadcn's ownCardin@/components/ui/cardis not affected. - Codemod
npx candor-ui@1 migrate card, whichmigrate 3.0.0runs.
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.thinkingin LocaleProvider'sstrings, and notmessage.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)
stringsfrommessage.thinkingtomessage.working; the text can stay. 2.9 and later already readworkingfirst.npx candor-ui migrate strings-keysrenames it, unlessworkingis already there. - Codemod
npx candor-ui@1 migrate strings-keys, whichmigrate 3.0.0runs.
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
useAnsweringApprovalssees Candor's own sentence again when an answer fails to send. - Shows in
- what is drawn
- What to do
- Move your override from
strings.useAnsweringApprovalstostrings.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-keysrenames it, in an object given asstringsor typedStringOverridesorCandorConfig. - Codemod
npx candor-ui@1 migrate strings-keys, whichmigrate 3.0.0runs.
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
viewerOffrom a block's or template'sstorefails the type check. - Shows in
- the type check
- What to do
- Use
getViewer(request)(the viewer, or null when nobody is signed in) orviewerName(request)from the server foundation'scandor-server.ts(@/lib/candor/candor-serverin a default install, or under your project's ownlibalias); both are async, so await them, and handle null.viewerOfread the demo cookie only, and gave the demo person (Riley) when nobody was signed in; these read the source given tosetViewerSource(). 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
Refusalfrom the server foundation'scandor-server.tsinstead (@/lib/candor/candor-serverin a default install, or under your project's ownlibalias); it is the same class.npx candor-ui migrate refusalchanges the import. - Codemod
npx candor-ui@1 migrate refusal, whichmigrate 3.0.0runs.
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…Dbfunction ordb/connectfails the type check, and a database driver swapped indb/connect.tsis no longer used. - Shows in
- the type check, behaviour
- What to do
- Use
database()from the server foundation'scandor-database.ts(@/lib/candor/candor-databasein a default install, or under your project's ownlibalias), which opens one pool for every block and template. If you swapped the driver indb/connect.ts, give your database tosetDatabase()at startup instead, which applies it to every block. If you passed a URL other than DATABASE_URL, give that database tosetDatabase()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
contextOffrom 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 (wherecontextOffell 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
postfrom the template'sshellfails 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.