candor-ui
v3.1.0
Published
Set up Candor UI in a shadcn/ui project: registry, licence key, theme and checks.
Maintainers
Readme
candor-ui
Sets up Candor UI in a shadcn/ui project. It edits components.json, .env.local
and your CSS for you, and leaves installs to the shadcn CLI.
This is candor-ui 3.0.0. It needs Node 20 or later and a project with components.json, or none: init runs
shadcn's setup first. CLI 3.0 works with Candor UI 4.0 and later; update, add and migrate say so on an older
project and change nothing. Which CLI for which kit:
| Your project | CLI | To move it on |
|---|---|---|
| Candor 4.x or 5.x | npx candor-ui@3 (this one) | npx candor-ui@3 update --to 5.0.0, then npx candor-ui@3 migrate 5.0.0 |
| Candor 3.x | npx candor-ui@2 | npx candor-ui@2 update --to 4.0.0, then npx candor-ui@2 migrate 4.0.0 |
| Candor 1.x or 2.x | npx candor-ui@1 | npx candor-ui@1 update --to 3.0.0, then npx candor-ui@1 migrate 3.0.0 |
CLI 2 and CLI 1 stay on npm. A lockfile that records the layout CLI 2.0.0 nested (web/candor/candor/) is folded by
npx candor-ui@2 update --fold first, and items candor-lock.json doesn't record are recorded by
npx candor-ui@2 update <items> --from <version>. Without a candor-lock.json, migrate takes a project that sets
up Candor's registry with nothing in a candor/ folder for 2.x.
npx candor-ui search approval # find an item
npx candor-ui list --tier free # every free item, by area
npx candor-ui info plan # an item's docs: tier, install line, exports
npx candor-ui@latest init # free items
npx candor-ui@latest init --key <key> # Pro and Premium items
npx candor-ui add thread message composer
npx candor-ui mcp claude # the docs MCP server, for your coding tool
npx candor-ui lint # check your app against the honesty rules
npx candor-ui theme check # your theme's contrast, motion values and sound files
npx candor-ui theme watch # the same, again on every save, until Ctrl-C
npx candor-ui theme from app/globals.css --out candor-brand.css
npx candor-ui theme apply candor-brand.css # install a brand file from the theme builder
npx candor-ui audit http://localhost:3000 # axe and the honesty rules, on a running page
npx candor-ui doctorCommands
list [--tier free|pro|premium] [--group <group>] [--installed] [--json]prints every item by area, with its tier and a one-line description, from the site's docs index (/docs/index.json).--grouptakes an area's name or its slug (agent-work).--installedlists only whatcandor-lock.jsonrecords, with each item's version and whether a newer one exists (from/r/versions.json; if that can't be read, it says "latest unknown"). When the kit has moved on, it compares the item's own files at your version and at the latest (/r/v/<version>/…) and says which it is:kit 2.2.0 available; this item unchanged since 2.1.0, or2.2.0 available; this item changed. If an item can't be compared (a paid item without a licence key, a retired item) it says only2.2.0 available. The kit's version number is not a claim about the item. Principle: unknown stays unknown.--jsonprints the same for machines, including each item'sexportsandinstallline and, for installed items,itemChanged(true,falseornullfor unknown).search <words...>looks in the index for items by name, exported component and description, and prints the best ten with their tier and install command. A name match ranks above an exported component, which ranks above the description; an item that matches more of the words ranks above one that matches fewer. With no match it says so and points tolist.--jsonprints the matches.info <item>prints the item's docs page (/docs/<item>.md), with the tier, install line and exports first. No pager. Given a component's name (ApprovalDecision), it says which item has it; given a near miss, it suggests names. From 0.13 it shows the page for the version you have: the onecandor-lock.jsonrecords (the item's own, else the kit's), from/r/v/<version>/docs/<item>.md, and it says which version that is. When that version kept no page for the item (pages are kept from 1.3.0, and an item newer than your kit has none), it shows the current page and says so.These three read public pages, so they send no licence key (the one exception:
list --installedsends it with its reads of your installed items' snapshots, asupdatedoes, so it can compare paid items), and they cache nothing on disk. They use the site that the@candorregistry incomponents.jsonpoints to (so a proxy works), else candor.themethodic.com. If the site can't be reached they say which address failed and exit 1.initadds the@candorregistry tocomponents.json, installs@candor/theme, and importscandor.cssin the CSS filecomponents.jsonnames, after Tailwind. From 0.13, whencomponents.jsonhas nolibalias,initadds one, the folder of yourutilsalias (@/lib/utilsgives@/lib), but only whentsconfig.json's paths take it to the folder shadcn already writes lib files to; otherwise it changes nothing and says what to add. With a licence key (--key, orCANDOR_LICENSE_KEYalready in.env.local) the registry sends it as a header; without one it uses the plain URL, because shadcn stops when a header names a variable that isn't set. If your CSS sets--font-sans: var(--font-sans)after the import (shadcn's init does),initsays so, andinit --yespoints it atvar(--cd-font). It also adds Inter (@candor/font-inter) and imports it next tocandor.css;--no-fontleaves it out, so text uses the system font or your own--cd-font. Running it again changes nothing. The theme and the font install asaddinstalls an item (from 0.11.2): at the versioncandor-lock.jsonrecords (--latestfor the newest), and recorded in it; with no lockfile, at the newest release, and the lockfile starts with them. Once the theme is recorded, a missing font installs at the theme's version,--latestor not (from 0.12; before,init --latestput it at the newest release, ahead of the theme). A theme from before 1.6, which loads Inter itself, doesn't get the font item.init --overwritereinstalls the theme over existing files, but not one the lockfile records: it stops, changes nothing, and points tonpx candor-ui update theme, which moves the theme to another release with your changes kept and the lockfile with it (to put the recorded version's files back over yours,npx candor-ui add theme --overwrite --replace-edits). Acandor-lock.jsonthat can't be read (not JSON, or a lockfile version this CLI doesn't know) doesn't stopinit(from 0.12; 0.11.2 stopped with exit 1): it says so, sets up the registry, the CSS import and the font settings, and installs nothing, since installs are recorded there. If the theme still needs installing, it stops there with exit 1. The lockfile is left as it is, anddoctorreports it with the fix.add <items...>runsnpx shadcn@~4.23.0 add @candor/<item>...(the pinned shadcn; see Which shadcn). Names work with or without@candor/, and shadcn flags such as--overwritepass through. It then records each installed item (and the@candoritems it pulled in) incandor-lock.json: the version, and a hash of every file as installed, with each file where it is (a stylesheet you moved and recorded is written where you put it, and the move stays recorded). From 2.2.1addandupdatehash the same text the same way: line endings and trailing spaces left out, and, without RSC, the"use client"line shadcn drops from only some files, so one item at one version has one hash whichever recorded it. A hash an earlier CLI recorded still matches. Commit the lockfile. With--overwrite, it stops before shadcn replaces an installed item you've changed (it names the files); bring that item up to date withupdate, which merges, or pass--replace-editsto replace it anyway. When shadcn writes nothing (it stops if a file it would update has your changes),addsays so, names that file, and exits 1.Seeing what replacing would change (0.12).
--dry-run(shadcn's flag, passed through) writes nothing. Where the add would stop on your edits, it shows instead, file by file, what--replace-editswould do (without--overwrite, what--overwrite --replace-editswould do): a diff from your file to the version the add installs, with your own edits since you installed it marked<(lost if you replace), the old version's lines-and the new version's+; then whatupdatewould do instead, a clean merge that keeps your changes or the number of conflicts it would leave. It uses the same three versions asupdate --dry-run(the release the lockfile records, your file, the release the add installs), each rendered the way shadcn's install writes it (see How files are worked out), so it needs the registry but no shadcn run. When the files are pulled in by others, the whole install is listed first (from 0.13; before, only the items with your changes). At the version you installed it saysupdatehas nothing to merge (before, it suggested anupdate --tothat would do nothing). It leavescandor-lock.jsonas it is. From 0.13 a dry run exits as the add would: where the add would stop (--overwriteover your edits without--replace-edits, or moving installed items without--yes) it says so and exits 1; before, it exited 0. With nothing of yours in the way, it lists each file the install would write and what would happen to it:create,same(yours is identical and stays),kept(in a monorepo app whoseuialias is in a shared package, yours differs only in its import lines, which shadcn doesn't compare there: its imports name different modules, and shadcn leaves the file as it is; from 2.1.0),differs(shadcn asks before replacing it) or, with--overwrite,replace. From 0.13 that list is worked out in-process and shadcn isn't run, socomponents.jsonis left alone even for a dry run at another version (before, it was pointed at that version's snapshot for the run and put back, which a dev server saw as a change). In a project the CLI doesn't cover (see How files are worked out) the dry run goes to shadcn, as before. Nothing is recorded either way.Without a terminal (a coding agent, CI, a pipe), shadcn can't ask before replacing a file of yours that differs from what it would write. It stops at that question part-way through, exits 0, and leaves the rest unwritten: it has installed the npm packages and written the files that came before it. From 0.14, where the CLI can work out the files (as for the dry run),
addchecks first: if any file differs and--overwriteisn't given, it installs nothing, names the files, and exits 1, andadd --dry-runsays the add would stop and exits 1 too. Autils.tswithout cn is such a file; from CLI 3.1, where the utils alias already resolves to a file that exports cn (autils.tsfrom before shadcn 4.23, with clsx and tailwind-merge, among them), shadcn's own utils isn't installed at all: nolib/utils.tsis written and nocnpackage added, and the list says so. From 3.1 the list also says, for each npm package, whether package.json's specifier already keeps it inside the item's range, in which case shadcn isn't given it and the specifier (an exact pin) stays as it is. Add--overwriteto replace them (with--replace-editswhere they hold your changes to a Candor item), or runaddat a terminal and answer shadcn. Without RSC (rsc: false), a file that differs only by its"use client"line counts as the same: shadcn keeps that line in some files and drops it from others, so when nothing else differsaddruns shadcn with--overwrite, which changes at most that line, instead of letting it stop to ask. Before 0.14 the dry run said shadcn kept your file without a terminal; it doesn't, and the add wrote only the files before the question and exited 1 with "Nothing was written". In a project the CLI doesn't cover, that is still what happens, and the message now says why.--diffand--view. shadcn's own previews:--diff [path]shows what the install would change in a file,--view [path]a file's contents. They write nothing and record nothing. From 0.14, at a version other than the newest (the lockfile's, or--version), they leavecomponents.jsonalone, as the dry run does: shadcn reads that release's items from temporary local copies, each@candordependency pointing at its own copy, so its first line names a temporary file where it would say@candor/<item>; the copies are removed afterwards. Before, the@candorentry was pointed at the snapshot for the run and put back. A shadcn dry run in a project the CLI doesn't cover works the same way. From 0.14 the path after--diffor--viewis passed to shadcn as their value; before, it was taken for an item name.What it prints (0.11.2). Before shadcn runs, a line per item it installs, per shared item they use, and per npm package (from 0.12 with no lockfile too, read from the newest release; if the registry can't be read, the named items alone); then shadcn's own output; then what it recorded in the lockfile. From 0.13 the check of what shadcn wrote is worked out in-process and takes no time to speak of; in a project it doesn't cover (see How files are worked out) it runs a shadcn preview per item, a few seconds each, and prints a line per item as it goes. When the output isn't a terminal (a coding agent, CI, a log file), shadcn runs with
CI=1, so its spinners don't redraw or repeat in the log, and a preview's own output is shown only if it fails.Which version it installs. With a
candor-lock.json,addinstalls at the version the lockfile records (the theme's, else the newest item's), and so do the shared items the item uses (locale,motion,render-prop,status): it points shadcn at that release's snapshot (/r/v/<version>/) for the run and putscomponents.jsonback after (a preview at another version doesn't touch it; see--diffand--viewabove). Adding one item doesn't move the others.--latestinstalls the newest release;--version <x.y.z>installs that release, and a version the registry doesn't list is refused with the list. With no lockfile it installs the newest release, as before. The last line says what was installed and at which version, and the lockfile records that version. If the release's snapshot can't be read, nothing is installed and it says why: a 404 means the item isn't in that release (it was added later;--latestinstalls the newest), and anything else (offline, a proxy, a refusal, a server error) is named with the URL it tried. If the CLI is interrupted while shadcn runs (Ctrl-C, or a SIGTERM or SIGHUP sent to the CLI alone, as a coding tool's timeout does), it passes the signal on to shadcn and to what it started (npxruns shadcn as its own process, which can outlivenpx), waits for all of them to stop, putscomponents.jsonback, and exits 128 + the signal's number (130 for SIGINT, 143 for SIGTERM). One still running after 10 s is killed, with everything under it: on macOS and Linux each process listed undernpx(from 0.12; before, onlynpxwas, and a shadcn that ignored the signal kept running), on Windows the whole tree withtaskkill /T /F. From 0.13, on macOS and Linux, the list is taken again every 100 ms while it waits, so what shadcn starts after the signal is told and waited for too, and after the kill it waits at most 2 s more. shadcn runs in the CLI's process group, so a signal to the whole group (Ctrl-C at a terminal,timeout -s KILL, a coding tool that kills its command's group) reaches shadcn and what it starts, even aSIGKILL, which the CLI can't catch or pass on. What it can't follow: when the signal is sent to the CLI alone (akill <pid>), a process whose parent exits between two of the CLI's checks has no parent undernpxany more and is missed. Finding it would need shadcn in a process group of its own, which would take it off the terminal it asks its questions on, and out of reach of a kill to the CLI's group.When it would move items you have. If the add would write an item that is already in the lockfile at another version (a locked project, with
--latestor--version, or a lockfile whose items are at different versions), it prints them and stops before shadcn runs, for example:This would move locale, render-prop and status from 2.1.0 to 2.6.1 and overwrite your edits to locale. npx candor-ui update is the way to move them on purpose.Add--yesto go ahead; the same text is printed as a notice. Items that aren't installed yet are installed without a question. To move installed items on purpose, with your changes merged, useupdate(below).update [items...]moves installed items to the newest release in the majorcandor-lock.jsonrecords (or--to <version>), keeping your changes. From CLI 1.0 it doesn't cross into a new major without--to: on 4.x it stays on 4.x and prints one line naming the newer major and theupdate --to <version> --dry-runthat shows its breaking changes. With no lockfile, or on the newest major already, it takes the newest release. A file you haven't touched is replaced. A file you changed gets a three-way merge between the version you installed, your file and the new version; where both changed the same lines it writes conflict markers (<<<<<<< yours,>>>>>>> candor 3.0.1) and exits 1. Both versions come from the registry's snapshots (/r/v/<version>/…), and are rendered for your project first as shadcn's install would write them, so paths, aliases and"use client"match an install (see How files are worked out). With no names it updates everything in the lockfile. Items the lockfile doesn't record (installed before it) are recorded by CLI 2,npx candor-ui@2 update <items> --from <the version you installed>; CLI 3 names that command and changes nothing.updateis how installed items move to another version;addinstalls at the version the lockfile records and stops, naming them, before it would move any (seeadd). From 0.15 it installs the Candor items the new versions need that the project doesn't have, at the target version, asaddwould, says which, and records them incandor-lock.json: one the old version didn't need (2.10'slocaleimported the newfill-strings), and one both versions need that isn't in the lockfile or the project (the theme's tokens, in a project whose theme predates them).--no-depsnames them with theaddcommand instead, as before 0.15. Where installing them would move an item the update wasn't asked to (the shared tokens, when only some items are named), it stops before writing anything and says what to update with them. Where the install would replace a file of yours that differs from what it writes (a shared item you've changed, such as the tokens, or alib/utils.tsof your own, which the lockfile doesn't record), shadcn asks at a terminal; from 2.2.1, without one,updateworks out every such file the wayadddoes, stops before writing anything and names each one, and--differing keepkeeps every one of them (and their records incandor-lock.json) while--differing replacetakes what the install writes, naming them either way. Where the project is oneaddcan't work out itself (tsx: false, no tsconfig paths), from 2.3.1--differing keepkeeps every file on disk that shadcn's own dry run lists for the install, and both name the files the install changed; 2.2.1 to 2.3.0 kept only the files the lockfile recorded as changed.--differing keepholds your files in.candor-keep/(git ignores it) while shadcn runs, so they are put back even when the CLI is stopped with Ctrl-C, and if they can't be put back it says where they are. Where the registry can't be read to tell, it stops before writing anything too (without a terminal, or with--differing keep).--differingwith--no-depsis refused,--dry-runsays where the real run would stop and which files (and exits 1 if it would), and when nothing needs installingupdatesays--differingchanged nothing. If the install doesn't go through,updatesays so, gives theaddcommand to run, and exits 1. From 2.0 it also installs the npm packages the new versions declare that the old ones didn't and the project's package.json doesn't list (4.0's primitives need@base-ui/react), with the project's package manager;--no-depsnames the command instead. From CLI 3.1 it names an npm package the updated items no longer import that no other recorded item needs ("No longer needed: cmdk"), and leaves it in package.json; and the string keys the new versions add, per item, read from the items' strings tables, which a translated app shows in English until it gives them.--dry-runsays what would change and writes nothing, and names the install it would run. A file you moved is followed when it's still exactly as installed; otherwise say where it is with--moved <installed path>=<where it is now>(repeat it for several), and the lockfile remembers. An item with a file it can't find stays at its old version in the lockfile until the file is found. From 0.15,updatefirst prints the breaking changes between the version each item is at and the target, read from the items' entries in the registry's snapshots: what breaks, the migration, and themigratecodemod when there is one (or that it is done by hand).update --to <version>before that version is released prints the entries for the items in your lockfile and changes nothing, so you can see what it asks of your project. When a release installs an item's file somewhere else (an item it renames, as 4.0 renames the presets'*-kindsfiles*-preset),updatemakes the move first: it moves the file there, points every import in the project at the new place, records the move incandor-lock.json, then merges as usual.--dry-runshows the moves and the merges and writes nothing. A file moved by--moved(or by CLI 1.x'smigrate paths) is followed, and stays where you put it; a new file that isn't code goes beside the files the lockfile records as moved together out of its folder (the theme's stylesheets), andaddplaces such files the same way (from 2.0.1). Where components.json'scomponentsorlibalias is already a folder namedcandor(@/candor), Candor's files go in that folder, not in acandor/inside it; the lockfile decides which a project has, so a teammate's nested folder doesn't change it (from 2.0.1, as CLI 1.0.1 for 3.0). Where the lockfile records files both in that folder and in acandor/inside it (anaddby an older CLI), the files in the folder decide, andupdatemoves the nested ones up, points their imports there and records them where they are, also for an item already at the version it updates to;addanddoctorsay when the lockfile records such files, and to runupdate(from 2.1.0). An item the target release doesn't have is said, left in place as your own and no longer recorded. Where the lockfile records every Candor file in such a nestedcandor/(web/candor/candor/…, what CLI 2.0.0 installed under a@/candoralias), CLI 3.0 doesn't update the project:doctorandupdatesay so and name CLI 2'snpx candor-ui@2 update --fold, which folds it up a folder (each file, the lockfile's paths and the imports that name them);update --foldhere says the same and changes nothing. Then run this CLI'supdate.verifyis for CI: it checkscandor-lock.jsonagainst the registry's versioned snapshots and the files on disk, without running shadcn. Every recorded item must exist at its version with the same number of files, and every file must be there (following recorded moves). Files you've changed are listed, not failed. From 2.2.1 it also names a stale entry, whose hash matches neither the file nor Candor's text at that version (rendered for your project in process, where that's covered), which makesaddandupdatetake the file for one you've changed; where the file is Candor's as released,verify --recordrecords its hash again, changing onlycandor-lock.json(no file is written and shadcn isn't run). Without React Server Components, an entry CLI 2.2.0 wrote for a file shadcn kept the"use client"line in matches Candor's text with that line, and isn't called stale. Exits 1 on anything missing or unknown; paid items needCANDOR_LICENSE_KEY.migrate <codemod>(from 0.15) rewrites your source for a release's breaking changes;update --to <version>names the codemod each entry has.migrate <version>(from 1.0;migrate 5.0.0,migrate 4.0.0) runs, in order, every codemod that release names for the items you have, each under its ownmigrate <codemod>:heading, and from 2.0.1 names the release's codemods it leaves out (Not run:), since no entry on an installed item names them;migrate <codemod>runs one anyway. It prints every change, file by file, and lists what it couldn't change with why;--dry-runprints the same and writes nothing. From CLI 3.1,migrate strings-keysalso names the string keys the release adds to the recorded items, and the line about Candor's installed files says which it read (the ones with your changes) and which it didn't (the ones as installed);--verboselists their paths. Candor's own installed files (the onescandor-lock.jsonrecords) are left toupdate, which brings the release's versions of them and keeps your changes when it merges; in a file of Candor's you've changed, what a codemod finds is listed under "Left for you" withfile:line, not rewritten. Without a lockfile, a file named like one of Candor's (from the registry's public lists) in a folder Candor installs to (components.json's components, ui, lib and hooks) is taken to be Candor's, left alone and named; if the lists can't be read, nothing is changed. In a monorepo app whoseuialias points into a shared package (shadcn's monorepo template), the codemods read that package's source too, andtokensreads the stylesheet components.json names intailwind.csswherever it is (../../packages/ui/src/styles/globals.css). It parses your source with the project's own TypeScript or, where that has no JavaScript API (TypeScript 7) or there is none, from CLI 3.0 with the TypeScript 5 candor-ui depends on, so there is nothing to install for the run. Each table row is made in a release:migrate <version>makes that release's rows, andmigrate <codemod>every row up to the release the project is going to (the first with codemods after the oldest versioncandor-lock.jsonrecords). The codemods for 5.0 (migrate 5.0.0runs them in this order, from CLI 3.0):types-data:Message,ToolCall,ApprovalRequest,ResultTable,FindingandRunReceiptimported from Candor'slib/candor/{chat,work,data,ops}(through your alias, a relative path or a monorepo's shared package) toMessageData,ToolCallData,ApprovalRequestData,ResultTableData,FindingDataandRunReceiptData, with their uses; an aliased import (Message as M) changes only the imported name. A re-export from a file of yours, and a file with its ownMessageData, are listed.@candor-ui/reactalready has the new names.props, 5.0's rows: TabBar's and NavRail'scurrent, AppSidebar'sactiveNavand SettingsLayout'sactivetoactiveId; SettingsLayout's and DocumentViewer'stitletolabel; PersonMenu's and ScopeSwitcher'slayerClasstopopupClassName. Tree's and EnvironmentBand'sonSelect, which passes the id from 5.0, is listed with what to write. Stepper'scurrentand PromptSuggestions'titlestay.strings-keys, 5.0's rows: an override of one of the eleven*Separatorkeys moves tolocale.separator(two different values are listed for you to choose); KIT-89's four chart keys, unread since 4.2, are deleted; a removed sentence key (scheduleEditor.announceNext) is listed with the key to translate and its placeholders;standardCardsused inside a component becomesconst standardCards = useStandardCards()at its top, andstatusLabel,environmentLabel,approvalStateLabelandreviewLevelsimports are listed with what to use.headings: lists each use of a component whose default heading level 5.0 changes, withoutheadingLevel, with the level it drew in 4.x (pass headingLevel={3} to keep it). It changes nothing.
The codemods for 4.0 (
migrate 4.0.0runs them in this order):as-child:asChildto Base UI'srenderon Candor's components only, the ones imported from a filecandor-lock.jsonrecords (without a lockfile, a file in acandor/folder) or from@candor-ui/react:<Button asChild><a href="/docs">Docs</a></Button>becomes<Button render={<a href="/docs" />} nativeButton={false}>Docs</Button>.nativeButton={false}goes on any of Candor's components whose Base UI part renders a button (Button, DialogTrigger, PopoverTrigger, DropdownMenuTrigger, SelectTrigger, TabsTrigger and the others that take it) when the element it renders isn't a<button>; TooltipTrigger takes nonativeButton. shadcn's own components incomponents/uiand anasChildon anything else stay. It lists what it can't follow: anasChildwith no element child, with more than the child inside, or with a value other thantrue.props: the JSX props 4.0 renames, on Candor's components only, from a table:loading,pending,applyingand the other waiting props tobusy(Combobox, DataTable, PermissionMode, ChangeSetReview …),onChangetoonValueChange(PermissionMode, DictationButton), and TooltipProvider'sdelayDuration,skipDelayDurationanddisableHoverableContentto Base UI'sdelay,timeoutanddisableHoverablePopup. AnonChangeon your own elements is a DOM event and stays. DictationButton'sdisabledis listed, not changed: 4.0'slockedis the reason the person sees, which only you can write. So is CommandPalette'sonCloseAutoFocus, which becomes Base UI'sfinalFocusand returns the element focus goes to.tokens: Candor's own tokens to their--cd-names (--warningto--cd-warning,--sunkento--cd-sunken, 23 colour roles in all) and the chart series onto shadcn's (--series-1to--chart-1…--series-6to--cd-chart-6), where your stylesheets set them and in everyvar(). In your source it renames only where a string reads or sets a token: insidevar(), in Tailwind's arbitrary values (bg-[var(--sunken)],bg-(--sunken),[--sunken:white]), as a style object's key ({ "--warning": … }) and insetProperty; a string that only looks like one, such as a script's--chromeargument, stays. Tailwind classes such asbg-sunkendon't change. A file that sets both--series-Nand--chart-Nis listed for you. Candor's own stylesheets come withupdate, and on a 4.x projecttheme checkreports an old name as an error.
CLI 2.0 dropped the codemods only a 2.x project needs (
orgdata,metadata-diff,card,terms,paths,refusal: CLI 1.x has them), and CLI 3.0 drops 3.0'smotion-aliases(CLI 2.x has it:npx candor-ui@2 migrate motion-aliases).theme checkstill reports 2.2's motion tokens as errors.strings-keysalso makes 4.0'smetadataDiffnamespacechangeDiffand themetadataDiffStringstablechangeDiffStrings(inmigrate 4.0.0), in a provider'sstringsor an object typedStringOverridesorCandorConfig; CLI 3.0 drops its 3.0 rows (message.thinking,useAnsweringApprovals), which CLI 2.x has.key <key>saves the key to.env.localand switches the registry to send it.key --removetakes it out and switches back. The key is never printed, and.env.localis added to.gitignoreif it isn't covered.mcp <tools...>adds Candor's docs MCP server to a coding tool's project-level config, so the tool can search the docs and pick components while it works. Tools:claude,cursor,codex,vscode,opencode; several at once with commas or spaces.init --mcp <tool>does the same as part of init. See below for the files it writes.lint [paths...]checks your.tsx,.ts,.jsxand.jsfiles against Candor's honesty rules and printspath:line:col rule messagefor each finding. Exits 1 on an error;--strictmakes warnings fail too;--jsonprints the findings for machines. It skipsnode_modules,dist,build,coverageand every directory whose name starts with a dot (.git,.next,.claude/worktrees). The rules are below.theme check [file]checks your theme and changes nothing: colour contrast (the next paragraph), then, from 0.10, the motion and sound values, and from 0.13 token names anddata-slotselectors (after it). It measures the contrast of your theme. It reads the CSS overrides in the filecomponents.jsonnames (pluscandor-brand.cssif there is one), or the file you give, on top of Candor's own tokens (the installedcandor-theme.css; else/r/theme.json), follows@imported files andvar(), and resolves each colour role the way the browser would inlight,dark(data-theme="dark") andhc(data-theme="hc"), by selector specificity and then source order, ascandor.cssis written. Dark by the system setting with nodata-themeis a fourth state, and is listed only when your overrides make it differ fromdata-theme="dark"(candor.css's own rule for it is more specific than:root). Every pairing the theme builder andnpm run themecheck (44 text and edge pairings, plus the focus ring at 80% on 6 surfaces: 50 per theme) is printed with its measured ratio andok,FAILorn/a, with totals. Text needs 4.5:1 (7:1 in high contrast), edges and the focus ring 3:1. For each failure it suggests the nearest lightness that would pass, and applies nothing. Exits 1 on any failure;--jsonprints the report (themes[].rows[],suggestions,notes,ok). From 2.1.0 each brand scope ([data-brand="<name>"], as the theme builder writes several brands in one file) is checked as a theme of its own, after the base: the scope's values over the page's in each of the states above, listed asLight in [data-brand="acme"]and so on (themes[].brandin the JSON), and counted in the totals. Before, a scope's rules were noted as not applied. A scope is measured as the browser draws it on an element inside<html>: a value the page works out at:rootfrom another (--primary: var(--brand-ink)) keeps the root's--brand-inkthere unless the scope sets--primaryagain, as the theme builder's scopes do. A scope whose rules can only be on<html>(:root[data-brand="acme"],[data-theme="dark"][data-brand="acme"]) is measured there, where its values and the page's are worked out together. A scope that changes no colour (one that sets only--radius) isn't measured again, since it draws in the base's colours: a note names it. A scope's selector with anything else in it (.card[data-brand="acme"]) is still named in a note. Principle: unknown stays unknown. A colour it can't read (transparency,color-mix(), a missing variable) makes its pairingsn/a, counted as "not measured" and neither passed nor failed; selectors it doesn't model (.dark,@layerorder, nesting) are named in a note, never guessed at. Motion and sound (0.10). The same files are searched for--cd-motion-*and--cd-sound-*values, which are checked against the motion manifest of the kit you have installed, so a range is the one your kit enforces and a name a newer kit added isn't called a typo. The manifest is found in this order: amotion.jsonbeside yourcandor.css; else/r/v/<version>/motion-manifest.jsonon the registry, for the versioncandor-lock.jsonrecords (the theme's, else the newest item's); else the registry's latest (/r/motion-manifest.json), with a note that it may be newer than your kit. Kits before 2.4.0 have no manifest, and if none can be had the motion checks don't run: it says so and what was left unchecked, which is never a pass. Sound checks need no manifest and always run. Each finding is one line,ERROR,WARNorn/a, with the file, the line and the fix after it;--jsonadds amotionAndSoundobject (findings,sounds,manifest,motionChecked,schema,notes) andokincludes its errors. If motion values were found and the motion checks couldn't run (no manifest),okisfalseandmotionAndSound.motionCheckedisfalse, and the exit code is 2 ("nothing measured"), unless colour or sound already failed, which is 1. Exit 1 on any error, in colour or in motion and sound.The same checks run in the docs MCP server (0.11). The checks are one function, shared with the server's
theme_checktool athttps://candor.themethodic.com/mcp: give it the contents ofcandor-brand.cssand it returns the same findings, so a coding agent can check its own edits without the builder or this CLI. The difference is what each can read: the server checks the CSS it is sent against the current kit's colours, motion manifest and token manifest (an older@candor-kitin the file is checked against the current names, with a note), follows no@import, fetches nothing, and can't see yourpublic/folder, so a sound file's length, size and format are "not measured" there.theme checkhere reads your files and the manifest of the kit you have installed, and measures the sounds.| Check | Level | | --- | --- | | A name the installed kit doesn't read (with "did you mean", or "known from 2.5.0: upgrade the kit" when the file says it was made for a newer kit) | warning | | A level outside 0 to 2; a duration outside its family's range;
--cd-motion-speedoutside 0.5 to 1.5 | error | | A spring on a family or moment that can't overshoot (tint,value,mark, a fade) | error | | Attention: count × duration over 5 s, or rings faster than 3 a second | error | | A level below 1 on theclockfamily or a clock moment (a hold or deadline is real time passing) | error, with why | | A rawanimation,transitionor@keyframes cd-*aimed at a Candor slot ([data-slot],.cd-*) | warning: outside the motion layer, not checked | | A sound path that isn't underpublic/orstatic/; a path not written as a quoted string | warning | | A sound over 1.5 s; a volume outside 0 to 1 | error | | A sound over 100 KB; not MP3, M4A, Ogg or WebM; an MP3 slot holding another format | warning | | A sound whose length can't be read from the file (MP3 frames, the M4A movie header, Ogg's last page, WebM's segment info); a remote sound |n/a: not measured, never a pass | | A@candor-themenewer than the CLI reads | error: update the CLI | | An easing that isn't one of Candor's four, a variant outside the family's set,--cd-motion-cap(the person's setting) | warning |A file with no
@candor-thememarker is a 2.0 to 2.2 colour-only file: if it has no motion or sound values it says so, and does not touch the network; if it has some (a hand-written file), they are checked anyway and it says that. Sound paths are looked for under the project'spublic/andstatic/; nothing is fetched. Colour checks are unchanged.Token names and
data-slotselectors (0.13). The same files' other--cd-*values and their[data-slot]selectors are checked against the token manifest of the kit you have installed: atokens-manifest.jsonbeside yourcandor.css; else/r/v/<version>/tokens-manifest.jsonfor the versioncandor-lock.jsonrecords; else, for a release before 2.8.0 (which has none) or with no version recorded, the current/r/tokens-manifest.json, and the report says so. If no manifest can be read the checks don't run, and it says that too. Each finding is a warning with the file, the line and the fix:| Code | What it means | | --- | --- | |
token-unknown| A--cd-*name the kit doesn't read, with the nearest one it does, or the shadcn name for a colour (colours go by--primary, not--cd-primary) | |token-range| A value outside a token's range, such as a control or row height under 24px | |token-internal| A--cd-*name the kit's CSS sets for itself, which a brand file shouldn't set: it changes how a component works, and can change in any release | |motion-value| A--cd-show-*or--cd-draw-*value the Pro motion-drawings item doesn't draw (it plays nothing), with the values it takes;noneandvar()aren't judged | |slot-unknown| A[data-slot]selector that matches nothing a component renders |A value that depends on another (
var(),calc(),em) isn/a, not measured. A--cd-*name your own CSS both sets and reads is your variable and isn't flagged, and neither is a selector that a slot pattern could match. These warnings don't change the exit code.--jsonadds atokensobject (manifest,checked,declared,selectors,warnings,findings,notes).theme watch [file](0.12) runstheme checknow and again whenever a file it reads changes, until Ctrl-C (exit 130): the CSS filecomponents.jsonnames (or the one given) and every file it imports,candor-brand.css, the installedcandor-theme.cssand motion manifest besidecandor.css,components.jsonandcandor-lock.json. The list is worked out again after each run, so a new@importis followed, and a file that isn't there yet (a brand file you're about to save) is watched for. Files are polled every quarter of a second rather than watched by events, so an editor that saves by writing a new file and renaming it is still seen; saves that arrive together run once. A save made while a check is running is seen once it ends (from 0.13; before, it was missed until the next save). Each run prints exactly whattheme checkprints, after a line naming what changed. A file that can't be read is said and watching goes on. With--json, stdout has one report per run and the rest goes to stderr. It changes nothing.theme from <file> [--out <file>] [--format shadcn|tailwind|figma]turns brand tokens you already have into acandor-brand.css, in the shape of a brand file (candor-brand.css): the overrides to import aftercandor.css, one block per theme (:root,[data-theme="dark"], the system-dark media rule,[data-theme="hc"]), and a header that says what was checked and the result. A role changed in any theme is written in every theme, so no theme picks up another's value. Sources: a shadcnglobals.css(or any CSS that sets--primary,--background…;:root,.darkand@themeare read,var()is followed; colours asoklch(),hsl(), hex orrgb(), and Tailwind v3's bare HSL channels,221.2 83.2% 53.3%); a Tailwind config (theme.colorsortheme.extend.colors, colours written out in the file, light theme only, read as text, never run); a Figma variables JSON: the REST API'sGET /v1/files/:key/variables/localresponse (meta.variablesandmeta.variableCollections, with modes and aliases), or avariablesarray of{ name, type, valuesByMode }(also insidecollections); modes named like light, dark or high contrast become the matching theme. The header lists the roles derived from your primary and any stated colour that was adjusted to fit sRGB. Figma has no JSON export of its own, so export with the REST API or a plugin that writes one of those shapes. Only what the source states is carried over. Where it states nothing (no dark block, no high-contrast mode) the theme keeps Candor's colours, and the primary follows yours by Candor's own lightness steps, as in the builder. Not carried over, and said:--input(in shadcn a soft border; in Candor the control edge, held to 3:1),*-foregroundtokens other than--primary-foreground,--secondary, sidebar and chart tokens, and colours with transparency. The result is then checked with the same engine astheme check; failures are printed with the nearest passing lightness as a suggestion, written into the header, and never applied. Nothing is written without--out(the CSS is printed, and the report goes to stderr so> candor-brand.cssworks); an existing file needs--overwrite. Exits 1 when any check fails (the file is still written). Principle: unknown stays unknown: a brand isn't shown as passing when it was measured as failing.theme apply <file | url> [--overwrite] [--json]installs a brand file, such as the one the theme builder's Download gives. It runstheme checkon the file in memory first (colour, motion and sound), and an error stops it with the findings, writing nothing. Then: it writescandor-brand.cssnext tocandor.css(in a monorepo, in the shared package whereinitput the theme); if a differentcandor-brand.cssis there it shows what replacing it changes and asks (--overwriteskips the question; with no terminal and no flag it stops); it adds@import "./candor-brand.css";after thecandor.cssimport in the CSS filecomponents.jsonnames, if that isn't there (and tells you the line to add if the file doesn't importcandor.css); it lists the sound files the theme names and whether each is present underpublic/orstatic/, with where a missing one goes (it never downloads them: your app hosts them); and says what changed in words:Motion: 7 values (pop expressive everywhere; copy-field's tick still; …). Sounds: 3 named, 1 missing.A URL must return CSS. A theme builder share link can't be applied in 0.10: open it in the builder, press Download, and apply the file.audit [path|url]islintplus a look at a running page. With a path (default: the current directory) it runs the lint rules over it. With a URL it loads the page in a browser and runs axe and the honesty rules that show in the DOM: adisabledcontrol with notitle,aria-describedbytext,Lockedor text beside it (a control that can't be used says why); a pre-checked radio or checkbox inside anything named approve, deny, allow, reject, delete or confirm (nothing is pre-selected in a decision); the calm rule (motion never says something false): the page is loaded as someone who asked for reduced motion, so aprefers-reduced-motion: reduceblock in your CSS has already done its work, andinfinite-animationnames anything that still loops (animation-iteration-count: infinite), andcalm-motionanything that still animates or transitions atransform,translate,scale,rotateorheightfor more than a frame. Candor's calm level lets only opacity and colour change, so a fade is fine. The kit's live indicators are left alone (aStatusDot, aSkeleton, anything built from thelivemotion family, the streaming caret, the live dot, a shimmer, a spinner inaria-busyorrole="status"). Without a browser that can emulate the setting, a page whose CSS has a reduced-motion block gets the loop check only, since the audit can't see what the block does; a page whose CSS has none gets both. Motion started from script (the Web Animations API) is read when the browser reports it; and an absolute<a href>insidedata-slot="message",message-content,message-bodyor an assistant message, other than a citation, the header, footer or actions (model output is text, never markup). Playwright (playwrightorplaywright-core, with a browser) andaxe-coreare your project's: candor-ui finds them from the project and never installs them. If either is missing it says "page not audited: install …" with the exact command (for your package manager), runs the path lint over the project, and exits 2; AUDIT.md says the page was not audited and--jsonhaspageAudited: false. The report is written toAUDIT.md(or--report <file>), grouped by principle, each finding with where (file and line, or a selector), what, why (linked to the principle) and how to fix. axe findings withcriticalorseriousimpact are errors, the rest warnings; the lint and DOM rules keep the severities above (model-htmland a model link are errors; the others warnings). Exits 1 on any error;--jsonprints the findings. These checks find some problems; a clean report doesn't mean the page is accessible or honest.doctorchecks the setup and says how to fix anything that's off: the registry and key, the theme and its import, the font override, React 19, Tailwind 4, your primitives library (see below),cn, Next.js App Router, guidance for coding agents, and which tools have the docs MCP server (from 0.12, and whether it is told the versioncandor-lock.jsonrecords; see the docs MCP server). From 0.11.2 it also notes@candor-ui/reactinstalled beside copied items (acandor-lock.jsonwith items, or a copiedlocalefile): each brings its ownLocaleProviderandTermsProvider, so a provider imported from the package doesn't reach the copied components (Pro items among them), which stay in English with the default terms and no error. It says to use one source, or, while you have both, to wrap the app in the copied providers as well. From 0.13 the package is a fix, not a note, when the project has Pro or Premium items (incandor-lock.json) or a licence key is set: the package is the free tier on npm, for trying Candor, and Pro items can't import from it. Either waydoctorlists every import of the package by file and line, with the items to add and the import each becomes. It also notes the package's stylesheet imported besidecandor.css(two copies of the theme), and both of its stylesheets (styles.css, which loads Inter, andstyles-no-font.css) imported at once. And it names acomponents.jsonwith nolibalias: shadcn then writes Candor's imports of its lib files as component paths that don't exist, so items that import one don't build. It is a fix when items are installed, a note before. From 0.14, since Candor 2.9'scandor.cssimportscandor-tokens.cssfrom beside it, acandor.csswhosecandor-tokens.cssisn't there is a fix (npx candor-ui add tokens, which writes it instyles/; for acandor.cssyou moved from there, it says to move the file beside it), and your CSS importingcandor-tokens.cssas well ascandor.cssis a note (the tokens are defined twice; remove that import). Fix commands use your package manager (npm, pnpm, yarn or bun, from the lockfile).
The docs MCP server
https://candor.themethodic.com/mcp is a streamable HTTP MCP server that serves Candor's docs: free, read-only, no
licence key, and it never serves paid source. npx candor-ui mcp <tool> writes it, as a server named candor, into
the tool's project-level config, in the shape that tool's documentation gives:
| Tool | File | Entry |
| --- | --- | --- |
| claude (Claude Code) | .mcp.json | mcpServers.candor = { "type": "http", "url": … } |
| cursor | .cursor/mcp.json | mcpServers.candor = { "url": … } |
| vscode | .vscode/mcp.json | servers.candor = { "type": "http", "url": … } |
| codex | .codex/config.toml | [mcp_servers.candor] with url = … (Codex reads it in trusted projects) |
| opencode | opencode.json (or opencode.jsonc when that's the file you have) | mcp.servers.candor = { "type": "remote", "url": … } |
An existing file is merged into: other servers, comments and formatting stay, and running it again changes nothing. A
candor entry that's already there in another shape (say, pointing at a local server) is left alone and reported. For
Codex that includes an inline table, dotted keys (mcp_servers.candor.url = …) and a sub-table such as
[mcp_servers.candor.env]; a second [mcp_servers.candor] would make the file invalid TOML, so none is written.
In a monorepo the files go at the repository root, where these tools read them. Zed, Windsurf and Cline aren't
offered because their documentation doesn't describe a project-level MCP file; add the URL in their settings
yourself.
The project's version (0.12). With a candor-lock.json, the URL it writes carries the kit version the lockfile
records (the theme's, else the newest item's): https://candor.themethodic.com/mcp?kit=2.6.0. The server's tools that
take a version start from it when the agent gives none; today that is item_changes, which says what changed in one
item between your version and another (props, exports and string keys added, removed or retyped, files touched, and
the CHANGELOG entries that name it). When the lockfile moves, run mcp <tool> again: it updates the version in the
entry it wrote and leaves everything else in the file as it is. Without a lockfile the URL has no version, as before.
With a lockfile that can't be read (from 0.13) the URL has no version either, and mcp (and init --mcp) says why
and to run it again once the lockfile is fixed; doctor says the entries' version can't be checked until then.
doctor says when an entry passes a different version from the lockfile's, or none, and the command that fixes it.
Lint rules
lint is a static check: it reads files, never runs them, and only looks at files that import Candor components
(from @candor-ui/react, or by name from your own components folder). When a pattern often means something harmless,
the rule skips it: it would rather miss a case than flag correct code. Each rule names the principle it serves, from
https://candor.themethodic.com/principles.
model-html(error; model output is text, never markup). In a file that imports a Candor chat component (Thread,Message,MessageContent,Composer,ToolCall,Reasoning,Citation, or anything from@candor-ui/react):dangerouslySetInnerHTML, an assignment toinnerHTML, or an import of a markdown-to-HTML renderer (react-markdown,marked,markdown-it,remark-html). A model can be talked into writing a link, a script or a fake button; if the interface renders what it writes as HTML, whoever controls the prompt controls the page. Show model text withMessageContentorSafeMarkdown, which read a safe subset of markdown.disabled-without-reason(warning; a control that can't be used says why). A JSXdisabledattribute in a file that imports Candor components but never usesLockedoraria-disabled. A greyed-out control with no explanation sends people looking for help, or around the rule. Wrap it inLockedwith the reason, or setaria-disabledand say why. Skipped when the value is a transient state (isPending,submitting), explains itself (!onSave,!draft.trim(),page === 0,!hasDetail, alockedorreasonvariable), isfalseor a passed-throughdisabled, or when the element hasaria-busy,titleoraria-describedby.preselected-decision(warning; nothing is pre-selected in a decision). In a file that importsPermissionPrompt,ApprovalRequest,ApprovalDecisionorTypeToConfirm:defaultCheckedon an element whose attributes or text say approve, deny, allow, reject, delete or confirm;autoFocuson a button, radio, checkbox or menu item that says one of those;defaultValueset to one of those words, or on aRadioGroup,SelectorToggleGroupwhose options say them. A default in an approval nudges people to agree without reading.autoFocuson a text field or a Cancel button, anddefaultValueon tabs (whatever their labels say), aren't flagged.estimate-as-measured(warning; unknown stays unknown). A prop namedduration,cost,countortokens(or ending inDuration,Cost,Count,Tokens,MsorSeconds) on a Candor component, given a string that reads as an estimate ("~$0.40","about 3s","approx."), a rounded product (Math.round(tokens * rate)) or a template literal that interpolates a rate applied to a value (${tokens * 0.002}inside the backticks). A guessed number looks exactly like a measured one, and people decide on numbers. Pass what the service reported, or leave the prop out so the component says it wasn't measured. Props whose name says estimate, budget, limit, expected, timeout, hold, delay or stale, and theRunEstimatecomponent, aren't flagged.motion-hardcoded(warning; motion never says something false). In a file that imports Candor components, a class that sets motion without going through a motion family: a raw duration (duration-200,duration-[250ms]), one of tw-animate's classes (animate-in,animate-out,animate-accordion-downand its relatives), ortransition-all, which transitions properties that move. These skip the settings a brand file and the person's own reduced-motion choice apply, so they behave differently from Candor's components beside them. Take the timing from the family that fits the change (reveal,arrive,pop,mark,fill,value,move,overlay,press,tint,refuse,attention,liveorclock). A duration that comes from a token (duration-(--cd-dur-base)), Tailwind's other animations (animate-spin),transition-colorsand anything in a comment aren't flagged.
Upgrading to 5.0
npx candor-ui@3 update --to 5.0.0 --dry-run # the entries and the merges; writes nothing
npx candor-ui@3 update --to 5.0.0 # updates every item, merging your changes
npx candor-ui@3 migrate 5.0.0 # types-data, props, strings-keys and headings, in that order
npx tsc --noEmit && npm run buildmigrate 5.0.0 lists under "Left for you" what no codemod can do: Tree's and EnvironmentBand's onSelect, which
passes the id; each heading item whose level changes, where your page or a test needs 4.x's (headingLevel={n}, or a
<HeadingScope> round the section); two different separators to choose between; a removed sentence key to translate
again; and standardCards or another removed English export used outside a component.
Upgrading to 4.0
With CLI 2, which CLI 3 sends a 3.x project to (CLI 3 keeps 4.0's codemods for a 4.x project that still has what they change):
npx candor-ui@2 update --to 4.0.0 --dry-run # the entries, the moves and the merges; writes nothing
npx candor-ui@2 update --to 4.0.0 # renames the presets' files (github-kinds.ts to github-preset.ts …), then updates
npx candor-ui@2 migrate 4.0.0 # as-child, props, strings-keys and tokens, in that order
npx tsc --noEmit && npx candor-ui@2 lint --strictmigrate 4.0.0 lists under "Left for you" what no codemod can do: DictationButton's disabled, which becomes locked
with the reason in your words; CommandPalette's onCloseAutoFocus, which becomes finalFocus, returning the element
focus goes to in place of moving it yourself; and anything a codemod couldn't follow.
From 2.x
A project on 2.x moves to 3.0 with CLI 1.x, which has the 2.x codemods and the v3-* lint rules, then to 4.0 with
CLI 2.x:
npx candor-ui@1 update --to 3.0.0 --dry-run # the entries, the moves and the merges; writes nothing
npx candor-ui@1 update --to 3.0.0 # moves Candor's files to components/candor/ and lib/candor/, then updates
npx candor-ui@1 migrate 3.0.0 # every codemod 3.0 names for your items, in order
npx candor-ui@1 lint --strict3.0.0's and 2.10.0's registries stay installable at /r/v/3.0.0/ and /r/v/2.10.0/.
Base UI, Radix and React Aria
From 4.0, Candor's primitives (button, dialog, popover and the others) are built on Base UI. An item that needs one
installs it into components/candor/ui/ with the @base-ui/react package, beside your own components in your ui
folder, which it leaves alone; Candor's components use Candor's primitives and your code keeps using yours.
- Base UI (
base-*styles, and whatinit --yessets up): supported, the same library as Candor's primitives. - Radix (
new-york,radix-*styles): supported. The app carries both libraries: its own Radix components incomponents/ui/, Candor's Base UI ones incomponents/candor/ui/. Candor's dialogs, popovers and tooltips work inside and beside the app's Radix ones (focus, portals, stacking and Escape are tested in a Radix app).doctorsays where Candor's go. - React Aria (
aria-*styles,"base": "aria", orreact-aria-componentsinpackage.json): not supported as a base yet.doctorsays so, and lists any of your React Aria primitives that sit incomponents/candor/ui/, where Candor's install. If none do, Candor's primitives sit alongside yours.
cn
Candor's components import cn from your utils alias (@/lib/utils, or aliases.utils in components.json).
After shadcn's migrate cn, that file re-exports cn from the cn package (export { cn } from "cn"), which works
as long as the cn package is installed. doctor follows the alias (through tsconfig.json paths, or a workspace
package's exports) and checks the file exports cn, and that what it imports (cn, or clsx and
tailwind-merge) is in package.json. If it doesn't, it tells you the line to add and the install command.
Monorepos (pnpm, Turborepo, npm, yarn and bun workspaces)
Laid out like shadcn's monorepo template: apps/web/components.json points tailwind.css and the ui and utils
aliases into a shared package (packages/ui, with its own components.json).
- Run candor-ui from the app:
npx candor-ui@latest init --cwd apps/web(orcd apps/webfirst). From the root,init,add,updateandkeystop and name the workspaces to use instead of setting shadcn up at the root.doctorat the root checks every workspace that has acomponents.json. initfrom the app adds the registry to bothcomponents.jsonfiles (shadcn reads the shared package's when it writes there), installs the theme in the shared package next to the CSS file that imports it (packages/ui/src/styles/candor.css, imported as./candor.css), so every app that uses the package gets it. The key stays in the app's.env.localand is passed to shadcn when it runs in the shared package. A.gitignoreat the repository root that covers.env.localcounts.addfrom the app installs as shadcn does: Candor's components and primitives into the app (components/candor/), shadcn's own primitives into the shared package.candor-lock.jsonsits next to the app'scomponents.jsonand records both, with paths from the app (../../packages/ui/src/styles/candor.css). Candor's stylesheets stay beside the CSS in the shared package that imports them: from 2.1.0, a stylesheet an item needs (tokens) that init installed there isn't written a second time into the app'sstyles/, andupdateupdates the theme where the lockfile records it. A project set up by a CLI before 2.1.0 hastokensrecorded at a copy in the app'sstyles/that nothing imports: from 2.1.0updaterecords and updates the copy beside the sharedcandor.cssinstead, and removes the app's copy if it's unchanged since it was installed (it says so when it leaves one you changed), anddoctornames such a copy. Runupdatefrom the app too; from the shared package it tells you where the lockfile is.- What type-checks in shadcn's template. With the template's
components.jsonas shadcn writes it (the app'suialias inpackages/ui),initandaddwork and the app builds, with one change: the template's sharedtsconfigsetslibto ES2022, anddata-tableuses ES2023'stoReversed, so add"lib": ["es2023", "DOM", "DOM.Iterable"]to the app'scompilerOptions. Measured with shadcn 4.23.0, Candor 4.1.0 andthread composer change-set-review card-registry use-attention approval-request data-tablein shadcn's Base UI template (shadcn init --monorepo --base base). (With Candor 2.10, Candor's primitives went into the shared package and 16 imports didn't resolve, so this section said to set the app'suialias to@/components/uifirst. That is no longer needed.) - Dependencies: React and Tailwind are found in the app, the shared package or the root, including pnpm
catalog:versions from `p
