Upgrading to 5.0
How to take a Candor UI 4.x project to 5.0: the commands, then every change 5.0 makes with what breaks, where it shows, what to do and the codemod that does it, or the edit you make by hand. The same list is in each item’s registry entry and is what candor-ui update --to 5.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 5.0 changes for the items incandor-lock.json, with the merges the update would make, and writes nothing.Look first02: Update
Updates every item, merging your changes into Candor's as
updatealways has. Ifcandor-lock.jsonstill records the layout CLI 2.0.0 nested (candor/candor/), CLI 3 stops and namesnpx candor-ui@2 update --fold, which folds it first.Update03: Run the codemods
Runs, in this order, those of 5.0's codemods (
types-data,props,strings-keys,headings) 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@3 migrate <codemod>runs one anyway. They change your own files only: Candor's installed files come with the update, and in one you've changed, what a codemod finds is listed. Each says what it changed and lists what it couldn't under "Left for you".headingschanges nothing: it lists each heading item whose level 5.0 changes. Add--dry-runto look first.Run the codemods04: Finish by hand, then check
Make the changes
migratelists under "Left for you": Tree's and EnvironmentBand'sonSelect, which passes the id; aheadingLevelwhere your page or a test needs 4.x's level (or a<HeadingScope>round the section, except round the freePermissionPrompt, which reads none and takesheadingLevel={3}); two different separators to choose between (and aFormatteryou build yourself, a test double foruseFormat, addsseparatorandjoin); a removed sentence key to translate again; and an English export used outside a component. Then the ones below marked "By hand" that no codemod lists, which the type check finds. Then type-check, build and run your tests: one that usestoBeDisabled()on a control that waits checksaria-disabled="true"now.Finish by hand, then check
Before you start
update --to 5.0.0prints 5.0.0's text. It reads the 5.0.0 snapshot, which stays as it was released, so the entries 5.1.0 corrected (heading-default,strings-separator,strings-sentences,locked-not-disabledandprops-active-id-nav, and the migration lines'npx candor-ui@3) print there as they were. This page has the corrected text, andupdateto 5.1.0 or later prints it.migrateparses your source with the project's TypeScript or, on TypeScript 7 (which has no JavaScript API) or with none, with the TypeScript 5 that CLI 3 brings: there is nothing to install for the run.@candor-ui/react5.0.0 has the same type names as before (they were already*Data); it dropsstatusLabeland follows the heading, separator and naming changes below for its free components. Update it with your package manager.- A removed string key still compiles.
StringOverridestakes any key, so the type check doesn't find an override of a key 5.0 removes: it just isn't read.migrate strings-keysmoves it or lists it, for astringsobject it can see (onLocaleProviderorCandorProvider, in aCandorConfig, or typedStringOverrides). - Stored strings are data, not source. No codemod reaches translations kept in a database or a CMS: move a stored
*Separatoroverride tolocale.separator, and translate a removed sentence key's replacement, once, where it is stored. - 4.x stays installable.
https://candor.themethodic.com/r/v/4.5.0/<name>.jsonserves every item as 4.5.0 shipped it, with the same licence check, andnpx candor-ui add <item> --version 4.5.0installs from there. - A 3.x project goes to 4.0 first, with CLI 2:
npx candor-ui@2 update --to 4.0.0, thennpx candor-ui@2 migrate 4.0.0(Upgrading to 4.0). CLI 3 doesn't change a 3.x project; it names those commands.@2stays on npm for 3.x and 4.x.
What changes (14)
types-data
The six data types are named for what they hold: Message and ToolCall (lib/candor/chat), ApprovalRequest and ResultTable (lib/candor/work), Finding (lib/candor/data) and RunReceipt (lib/candor/ops) are MessageData, ToolCallData, ApprovalRequestData, ResultTableData, FindingData and RunReceiptData, as @candor-ui/react already named them. The old names aren't kept.
- On
chat,work,data,ops- What breaks
- An import of an old name from lib/candor fails the type check (
has no exported member 'Message'). Imports from@candor-ui/reactdon't change. - Shows in
- the type check, imports
- What to do
npx candor-ui@3 migrate types-datarenames each import and its uses in your files; an import you aliased (Message as M) keeps its local name. It lists a re-export from a file of yours, and a file that already has its ownMessageData, for you to change.- Codemod
npx candor-ui@3 migrate types-data, whichmigrate 5.0.0runs.
heading-default
Every item that draws a heading takes headingLevel (1 to 6) and, without it, draws its top heading one level below the nearest HeadingScope, else at <h2>, so a page's outline skips no level. Dialogs, sheets, drawers, alert dialogs, settings sections, dashboard tiles and artifact panels set a scope over their content. PageHeader and EmailLayout stay at <h1>; the free PermissionPrompt and PageState default to <h2> and read no scope.
- On
artifact-panel,app-layout,dock-window,settings-layout,mcp-connect,model-key,stop-all,entity-disambiguation,permission-prompt,thread-header,form-error-summary,dialog,sheet,drawer,alert-dialog,data-table,reorderable-list,board,details-list,reconcile-table,column-mapper,finding,narrative-block,portfolio-card,report-sheet,monitor-card,run-log,run-estimate,failure-clusters,review-queue,dashboard-tile,approval-request,result-card,plan,environment-promotion,blast-radius,agent-screen,checkpoint-list,clarifying-questions,change-set-review,document-progress-queue,file-list,proposal,workflow-editor,comment-thread,drift-change,ship-review,run-recovery,knowledge-sources,memory-manager,prose-diff,retrieval-inspector,version-history,away-digest,escalation,extraction-review,api-keys,channel-setup,data-region,data-retention,device-sessions,environment-connect,environment-list,integration-connect,schedule-editor,schedule-list,sso-setup,webhooks,account-create,consent-ceremony,invite-chooser,setup-checklist,sign-in,terms-consent,billing-details,invoice-history,plan-gate- What breaks
- Where you don't pass
headingLevel, a heading can be a different level than in 4.x: most that were<h3>or<h4>are<h2>outside a scope. It looks the same; a test or an outline check that expects 4.x's level sees the change. - Shows in
- what is drawn
- What to do
npx candor-ui@3 migrate headingslists each use withoutheadingLevel, with the level it drew in 4.x, and changes nothing. Where your page or a test needs that level, passheadingLevel={n}; or wrap a section in<HeadingScope level={n}>(lib/candor/heading-scope, Pro) so the items in it sit one level below. The freePermissionPromptdraws<h2>by default since 5.0 and reads noHeadingScope: passheadingLevel={3}to keep 4.x's<h3>.- Codemod
npx candor-ui@3 migrate headings, whichmigrate 5.0.0runs.
heading-level-type
headingLevel is typed HeadingLevel (1 to 6) on every item, where nine props took 2 | 3 | 4 | 5 | 6. lib/candor/heading-level exports the type, and layout/heading still does.
- On
environment-list,dashboard-tile,board,data-table,reorderable-list,monitor-card,narrative-block,portfolio-card- What breaks
- Nothing you pass stops type-checking. A type of yours read from the prop (
ComponentProps<typeof DataTable>["headingLevel"]) now includes 1. - Shows in
- the type check
- What to do
- Nothing for a level you pass. Where you type a level of your own, import
HeadingLevelfrom lib/candor/heading-level. - Codemod
- None needed for a level you pass. By hand: import
HeadingLevelwhere you type a level of your own.
props-label-name
SettingsLayout's and DocumentViewer's title, which is their accessible name and isn't drawn, is label; title is Candor's word for a heading that is drawn.
- On
settings-layout,document-viewer- What breaks
titlefails the type check; in code that isn't type-checked, the settings region or the viewer has no name for a screen reader.- Shows in
- the type check, behaviour
- What to do
npx candor-ui@3 migrate propsrenames it on both.- Codemod
npx candor-ui@3 migrate props, whichmigrate 5.0.0runs.
props-popup-class
PersonMenu's and ScopeSwitcher's layerClass prop, the classes for the menu's popup, is popupClassName; layerClass is the name of foundation/layer's constant.
- On
person-menu,scope-switcher- What breaks
layerClassfails the type check; in code that isn't type-checked, the popup loses your classes.- Shows in
- the type check, styling
- What to do
npx candor-ui@3 migrate propsrenames it on both.- Codemod
npx candor-ui@3 migrate props, whichmigrate 5.0.0runs.
on-select-id
Tree's and EnvironmentBand's onSelect passes the id of what was chosen, as RunTree, SessionList, Plan and Candor's other lists do, not the node or the environment.
- On
tree,environment-band- What breaks
- A handler that takes the object fails the type check; in code that isn't type-checked, it reads properties of a string.
- Shows in
- the type check, behaviour
- What to do
- Write
onSelect={(id) => …}and look the node or the environment up by id where the handler needs it.npx candor-ui@3 migrate propslists eachonSelecton them with its file and line, and the type check finds the rest. - Codemod
- By hand: no codemod can make this change safely.
strings-compat-exports
The English exports kept for compatibility are removed: statusLabel (lib/candor/status), environmentLabel (lib/candor/team), approvalStateLabel (approval-request), reviewLevels (review-ladder) and standardCards (standard-cards). SessionList's statusLabel prop is a different thing and stays.
- On
status,team,approval-request,review-ladder,standard-cards- What breaks
- An import of one fails the type check and the build.
- Shows in
- the type check, imports
- What to do
npx candor-ui@3 migrate strings-keysrewritesstandardCardsused inside a component toconst standardCards = useStandardCards()at its top, and lists the others with what to use:useStatusLabel(), EnvironmentBand's strings (production,nonProduction) joined byuseFormat().join,useStrings("approvalRequest", approvalRequestStrings)anduseStrings("reviewLadder", reviewLadderStrings). Each follows the locale, where the export was English.- Codemod
npx candor-ui@3 migrate strings-keys, whichmigrate 5.0.0runs.
chart-strings
BarChart's byCategory, LineChart's over and chartValues and Heatmap's heatmap strings, which nothing has read since 4.2, are removed.
- On
bar-chart,line-chart,heatmap- What breaks
- An override of one is left in place and isn't read, as it hasn't been since 4.2, so nothing draws differently. It still compiles:
StringOverridestakes any key. - Shows in
- what is drawn
- What to do
npx candor-ui@3 migrate strings-keysdeletes the overrides.- Codemod
npx candor-ui@3 migrate strings-keys, whichmigrate 5.0.0runs.
strings-separator
The " · " between parts is one string, locale.separator, read through useFormat().separator and useFormat().join, where eleven items each had a key for it (message.metaSeparator, composer.hintSeparator, reasoning.summarySeparator …) and about forty typed it. SubscoreCard's separator and TableScrubberMap's kindSeparator, which joined lists, are removed: a list follows the locale's list format (useFormat().list). Strings that held the separator at their start or in their middle no longer do (integrationConnect.connectedSince is since {when}, membersTable.invitedBy is Invited by {name}, columnMapper.suggested is Suggested, with percentSure beside it, and the others the changelog lists): Candor joins the parts. Four that held it between parts are removed: reconcileTable.summary (now openCount, resolvedCount and matchedCount), evalScorecard.passedFailed (passedCount, failedCount), reviewQueue.progress (position, gradedCount) and dataTable.aggregateLine (nothing to translate).
- On
locale,message,composer,permission-prompt,reasoning,payload,search-trace,change-set-review,task-tray,workflow-editor,review-queue,subscore-card,table-scrubber-map,integration-connect,subscription-status,members-table,away-digest,escalation,blast-radius,environment-promotion,clarifying-questions,composer-scope-picker,app-sidebar,inbox,session-list,agent-roster,reconcile-table,eval-scorecard,data-table,column-mapper- What breaks
- An override of a removed key isn't read any more: the separator is whatever
locale.separatorsays. The override still compiles, sinceStringOverridestakes any key, somigrate strings-keysis what finds it. An object you build as aFormatteryourself (a test double foruseFormat) fails the type check: it has noseparatororjoin. - Shows in
- what is drawn, the type check
- What to do
npx candor-ui@3 migrate strings-keysmoves an override of one of the eleven keys tostrings.locale.separatorand removes the old keys; where two give different values, it leaves them and lists them for you to choose one. It lists an override ofsubscoreCard.separatorortableScrubberMap.kindSeparator, which has nothing to move to, and ofreconcileTable.summary,evalScorecard.passedFailed,reviewQueue.progressordataTable.aggregateLine, with the keys that say it now. An override of a string that no longer holds the separator, written with its own " · ", shows it twice: take the mark out. The codemod lists one that still has it.clarifyingQuestions.before("Before I plan") is new, the wordsclarifyingQuestions.progressheld before its separator: a translated app translates it, since no codemod lists it.Formatterhas two new members,separatorandjoin: an object you build as aFormatteryourself (a test double foruseFormat) adds them, or it fails the type check.- Codemod
npx candor-ui@3 migrate strings-keys, whichmigrate 5.0.0runs.
strings-sentences
Sentences that were built by joining strings and numbers in code are one string each, with {placeholders}, so a translation can put the words in its own order; numbers go through the locale's formatter. The keys they replace are removed: scheduleEditor.announceNext (say it with announceRule and announceRuleAlone) and the others the changelog lists by item. Some keys stay but no longer fill a placeholder, since Candor joins that part on with locale.separator: environmentPromotion.requestedBy, promoted and declined, and escalation.sentLine and acknowledged ({when}); blastRadius.group and awayDigest.group ({count}); taskTray.started ({status}); and membersTable.invitedBy ({expires}, now expiresIn). Five sentences have new keys that replace none, so nothing is removed: modelKey.keyFor, progressButton.measuredFailed and failedWithFiles, runTree.verdictNote and presence.hereStale.
- On
schedule-editor,data-retention,environment-connect,subscription-status,invite-chooser,dual-listbox,subscore-card,freshness-stamp,memory-manager,blast-radius,change-set-review,document-progress-queue,drift-change,environment-promotion,log-stream,plan,ship-review,workflow-editor,escalation,away-digest,task-tray,members-table,composer,composer-scope-picker,search-trace,app-sidebar,inbox,session-list,model-key,progress-button,run-tree,presence- What breaks
- An override of a removed key isn't read any more: the item says the sentence in English until you translate its new key. Nothing fails to compile, since
StringOverridestakes any key, somigrate strings-keysis what lists it. An override of a kept key that still has its dropped placeholder shows it as written ({when}on screen), with its own " · " beside Candor's. A translated app also says the five new keys' sentences in English (ModelKey's field label for one provider, ProgressButton's description and announcement with failed files, RunTree's verdict note, Presence's stale summary) until you translate them; no codemod lists those, since no key was removed. - Shows in
- what is drawn
- What to do
npx candor-ui@3 migrate strings-keyslists each override of a removed key with the key that says the sentence now and its placeholders. Translate that key: your words can't be moved for you, since the sentence is different. It also lists an override of a kept key that still has its dropped placeholder or a " · ": take them out. Translate the five new keys too, where your app is translated.- Codemod
npx candor-ui@3 migrate strings-keys, whichmigrate 5.0.0runs.
card-deck-backspace
CardDeck no longer dismisses a card on Backspace; Delete still does, and the hint names Delete and the arrow keys. A dismissal is announced only once the app has answered: on the redraw after onDismiss, or when the promise onDismiss returns settles; a card still in items then was refused, and isn't announced when it leaves later.
- On
card-deck- What breaks
- Backspace on a card does nothing. An app that takes the card out later, after an
await, without returning that promise fromonDismiss, has its dismissal go unannounced, where 4.x announced it when the card left. - Shows in
- behaviour
- What to do
- Where your own help names Backspace for CardDeck, name Delete. Where
onDismisssaves before it takes the card out, return the promise (anasynchandler does). - Codemod
- None needed: it changes what a key does, not your code.
locked-not-disabled
A control that waits on something (a save on its way, a condition not yet met) stays focusable and is aria-disabled, with the reason, through Locked, where it was natively disabled; a press does nothing until it can be used. A ToolCall or SearchTrace row that has finished with nothing behind it, which can never open, is text rather than a button.
- On
session-expired,webhooks,api-keys,data-controls,schedule-editor,account-create,profile-settings,terms-consent,invite-members,invite-chooser,step-walker,sign-in,step-up-dialog,tool-call,search-trace,permission-prompt,composer,message-branches,approval-request,ship-review,clarifying-questions,steer-composer,restore-dialog,plan,type-to-confirm,source-ingest,memory-manager,memory-mode,data-import,import-review,billing-details,plan-picker- What breaks
- A test that checks such a control with
toBeDisabled()fails, since it isn'tdisabled, and one that finds a finished ToolCall or SearchTrace row with no details as a button finds none. For a person nothing is pressed that wasn't before, and the control can be reached and says why it waits. A translated app says those reasons in English until it translates their keys (50locked…keys, on 31 of these items: RestoreDialog's reasons were keys already, or are the app's). - Shows in
- behaviour, what is drawn
- What to do
- In a test, check
aria-disabled="true"(toHaveAttribute("aria-disabled", "true")) in place oftoBeDisabled(), and find a finished ToolCall or SearchTrace row with no details by its text. Nothing changes in the app's code. A translated app translates the reasons the waiting controls give, which are new string keys and say their English words until it does:accountCreate.lockedCreating,apiKeys.lockedCreating,approvalDecision.lockedRecording,approvalRequest.lockedEditChecking,approvalRequest.lockedEditInvalid,approvalRequest.lockedRecording,billingDetails.lockedSaving,clarifyingQuestions.lockedSending,clarifyingQuestions.lockedUnanswered,composer.lockedEmpty,composer.lockedHeld,composer.lockedSending,composer.lockedStreaming,composer.lockedUnavailable,dataControls.lockedPreparing,dataImport.lockedReading,importReview.lockedLoading,inviteChooser.lockedChoose,inviteChooser.lockedJoining,inviteMembers.lockedSending,memoryManager.lockedNoMemory,memoryMode.lockedSaving,messageBranches.lockedFirst,messageBranches.lockedLast,permissionPrompt.lockedBusy,permissionPrompt.lockedInvalid,permissionPrompt.lockedUnanswered,plan.lockedAllSkipped,plan.lockedChoose,planPicker.lockedChoose,planPicker.lockedUnchanged,profileSettings.lockedClean,profileSettings.lockedSaving,scheduleEditor.lockedSaving,searchTrace.lockedNoDetail,sessionExpired.lockedSigningIn,shipReview.lockedShipping,signIn.lockedBusy,signIn.lockedNeedsCode,signIn.lockedNeedsEmail,sourceIngest.lockedNoLink,sourceIngest.lockedNoText,steerComposer.lockedEmpty,stepUpDialog.lockedChecking,stepWalker.lockedAhead,termsConsent.lockedRecording,toolCall.lockedNoDetail,typeToConfirm.lockedWorking,webhooks.lockedAddingandwebhooks.lockedTesting. - Codemod
- None needed in the app's code. By hand: a test that uses
toBeDisabled()on one of these controls checksaria-disabled="true", and a translated app translates the newlocked…keys listed above.
cli-3
CLI 3.0.0 (npx candor-ui@3) takes a project to 5.0, with migrate 5.0.0, and works with Candor 4.0 and later. It no longer takes a 3.x project, folds the layout CLI 2.0.0 nested (update --fold), or records items installed before the lockfile (update --from); npx candor-ui@2 stays on npm for those.
- On
- every item that installs at the default paths: components, hooks, libs, primitives and presets
- What breaks
npx candor-ui@2doesn't have 5.0's codemods.npx candor-ui@3stops on a 3.x project, and on a lockfile that records CLI 2.0.0's nested layout, naming the@2command to run.- Shows in
- behaviour
- What to do
- Use
npx candor-ui@3from 5.0. A 3.x project goes to 4.0 first withnpx candor-ui@2 update --to 4.0.0andnpx candor-ui@2 migrate 4.0.0; a lockfile with the nested layout is folded withnpx candor-ui@2 update --foldfirst. - Codemod
- None needed: run the commands above with
npx candor-ui@3.