npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

candor-ui

v3.1.0

Published

Set up Candor UI in a shadcn/ui project: registry, licence key, theme and checks.

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 doctor

Commands

  • 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). --group takes an area's name or its slug (agent-work). --installed lists only what candor-lock.json records, 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, or 2.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 only 2.2.0 available. The kit's version number is not a claim about the item. Principle: unknown stays unknown. --json prints the same for machines, including each item's exports and install line and, for installed items, itemChanged (true, false or null for 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 to list. --json prints 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 one candor-lock.json records (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 --installed sends it with its reads of your installed items' snapshots, as update does, so it can compare paid items), and they cache nothing on disk. They use the site that the @candor registry in components.json points to (so a proxy works), else candor.themethodic.com. If the site can't be reached they say which address failed and exit 1.

  • init adds the @candor registry to components.json, installs @candor/theme, and imports candor.css in the CSS file components.json names, after Tailwind. From 0.13, when components.json has no lib alias, init adds one, the folder of your utils alias (@/lib/utils gives @/lib), but only when tsconfig.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, or CANDOR_LICENSE_KEY already 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), init says so, and init --yes points it at var(--cd-font). It also adds Inter (@candor/font-inter) and imports it next to candor.css; --no-font leaves it out, so text uses the system font or your own --cd-font. Running it again changes nothing. The theme and the font install as add installs an item (from 0.11.2): at the version candor-lock.json records (--latest for 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, --latest or not (from 0.12; before, init --latest put 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 --overwrite reinstalls the theme over existing files, but not one the lockfile records: it stops, changes nothing, and points to npx 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). A candor-lock.json that can't be read (not JSON, or a lockfile version this CLI doesn't know) doesn't stop init (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, and doctor reports it with the fix.

  • add <items...> runs npx shadcn@~4.23.0 add @candor/<item>... (the pinned shadcn; see Which shadcn). Names work with or without @candor/, and shadcn flags such as --overwrite pass through. It then records each installed item (and the @candor items it pulled in) in candor-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.1 add and update hash 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 with update, which merges, or pass --replace-edits to replace it anyway. When shadcn writes nothing (it stops if a file it would update has your changes), add says 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-edits would do (without --overwrite, what --overwrite --replace-edits would 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 what update would do instead, a clean merge that keeps your changes or the number of conflicts it would leave. It uses the same three versions as update --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 says update has nothing to merge (before, it suggested an update --to that would do nothing). It leaves candor-lock.json as it is. From 0.13 a dry run exits as the add would: where the add would stop (--overwrite over 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 whose ui alias 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, so components.json is 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), add checks first: if any file differs and --overwrite isn't given, it installs nothing, names the files, and exits 1, and add --dry-run says the add would stop and exits 1 too. A utils.ts without cn is such a file; from CLI 3.1, where the utils alias already resolves to a file that exports cn (a utils.ts from before shadcn 4.23, with clsx and tailwind-merge, among them), shadcn's own utils isn't installed at all: no lib/utils.ts is written and no cn package 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 --overwrite to replace them (with --replace-edits where they hold your changes to a Candor item), or run add at 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 differs add runs 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.

    --diff and --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 leave components.json alone, as the dry run does: shadcn reads that release's items from temporary local copies, each @candor dependency 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 @candor entry 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 --diff or --view is 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, add installs 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 puts components.json back after (a preview at another version doesn't touch it; see --diff and --view above). Adding one item doesn't move the others. --latest installs 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; --latest installs 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 (npx runs shadcn as its own process, which can outlive npx), waits for all of them to stop, puts components.json back, 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 under npx (from 0.12; before, only npx was, and a shadcn that ignored the signal kept running), on Windows the whole tree with taskkill /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 a SIGKILL, which the CLI can't catch or pass on. What it can't follow: when the signal is sent to the CLI alone (a kill <pid>), a process whose parent exits between two of the CLI's checks has no parent under npx any 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 --latest or --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 --yes to 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, use update (below).

  • update [items...] moves installed items to the newest release in the major candor-lock.json records (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 the update --to <version> --dry-run that 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. update is how installed items move to another version; add installs at the version the lockfile records and stops, naming them, before it would move any (see add). From 0.15 it installs the Candor items the new versions need that the project doesn't have, at the target version, as add would, says which, and records them in candor-lock.json: one the old version didn't need (2.10's locale imported the new fill-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-deps names them with the add command 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 a lib/utils.ts of your own, which the lockfile doesn't record), shadcn asks at a terminal; from 2.2.1, without one, update works out every such file the way add does, stops before writing anything and names each one, and --differing keep keeps every one of them (and their records in candor-lock.json) while --differing replace takes what the install writes, naming them either way. Where the project is one add can't work out itself (tsx: false, no tsconfig paths), from 2.3.1 --differing keep keeps 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 keep holds 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). --differing with --no-deps is refused, --dry-run says where the real run would stop and which files (and exits 1 if it would), and when nothing needs installing update says --differing changed nothing. If the install doesn't go through, update says so, gives the add command 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-deps names 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-run says 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, update first 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 the migrate codemod 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' *-kinds files *-preset), update makes the move first: it moves the file there, points every import in the project at the new place, records the move in candor-lock.json, then merges as usual. --dry-run shows the moves and the merges and writes nothing. A file moved by --moved (or by CLI 1.x's migrate 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), and add places such files the same way (from 2.0.1). Where components.json's components or lib alias is already a folder named candor (@/candor), Candor's files go in that folder, not in a candor/ 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 a candor/ inside it (an add by an older CLI), the files in the folder decide, and update moves 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; add and doctor say when the lockfile records such files, and to run update (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 nested candor/ (web/candor/candor/…, what CLI 2.0.0 installed under a @/candor alias), CLI 3.0 doesn't update the project: doctor and update say so and name CLI 2's npx candor-ui@2 update --fold, which folds it up a folder (each file, the lockfile's paths and the imports that name them); update --fold here says the same and changes nothing. Then run this CLI's update.

  • verify is for CI: it checks candor-lock.json against 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 makes add and update take the file for one you've changed; where the file is Candor's as released, verify --record records its hash again, changing only candor-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 need CANDOR_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 own migrate <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-run prints the same and writes nothing. From CLI 3.1, migrate strings-keys also 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); --verbose lists their paths. Candor's own installed files (the ones candor-lock.json records) are left to update, 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" with file: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 whose ui alias points into a shared package (shadcn's monorepo template), the codemods read that package's source too, and tokens reads the stylesheet components.json names in tailwind.css wherever 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, and migrate <codemod> every row up to the release the project is going to (the first with codemods after the oldest version candor-lock.json records). The codemods for 5.0 (migrate 5.0.0 runs them in this order, from CLI 3.0):

    • types-data: Message, ToolCall, ApprovalRequest, ResultTable, Finding and RunReceipt imported from Candor's lib/candor/{chat,work,data,ops} (through your alias, a relative path or a monorepo's shared package) to MessageData, ToolCallData, ApprovalRequestData, ResultTableData, FindingData and RunReceiptData, 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 own MessageData, are listed. @candor-ui/react already has the new names.
    • props, 5.0's rows: TabBar's and NavRail's current, AppSidebar's activeNav and SettingsLayout's active to activeId; SettingsLayout's and DocumentViewer's title to label; PersonMenu's and ScopeSwitcher's layerClass to popupClassName. Tree's and EnvironmentBand's onSelect, which passes the id from 5.0, is listed with what to write. Stepper's current and PromptSuggestions' title stay.
    • strings-keys, 5.0's rows: an override of one of the eleven *Separator keys moves to locale.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; standardCards used inside a component becomes const standardCards = useStandardCards() at its top, and statusLabel, environmentLabel, approvalStateLabel and reviewLevels imports are listed with what to use.
    • headings: lists each use of a component whose default heading level 5.0 changes, without headingLevel, 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.0 runs them in this order):

    • as-child: asChild to Base UI's render on Candor's components only, the ones imported from a file candor-lock.json records (without a lockfile, a file in a candor/ 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 no nativeButton. shadcn's own components in components/ui and an asChild on anything else stay. It lists what it can't follow: an asChild with no element child, with more than the child inside, or with a value other than true.
    • props: the JSX props 4.0 renames, on Candor's components only, from a table: loading, pending, applying and the other waiting props to busy (Combobox, DataTable, PermissionMode, ChangeSetReview …), onChange to onValueChange (PermissionMode, DictationButton), and TooltipProvider's delayDuration, skipDelayDuration and disableHoverableContent to Base UI's delay, timeout and disableHoverablePopup. An onChange on your own elements is a DOM event and stays. DictationButton's disabled is listed, not changed: 4.0's locked is the reason the person sees, which only you can write. So is CommandPalette's onCloseAutoFocus, which becomes Base UI's finalFocus and returns the element focus goes to.
    • tokens: Candor's own tokens to their --cd- names (--warning to --cd-warning, --sunken to --cd-sunken, 23 colour roles in all) and the chart series onto shadcn's (--series-1 to --chart-1 … --series-6 to --cd-chart-6), where your stylesheets set them and in every var(). In your source it renames only where a string reads or sets a token: inside var(), in Tailwind's arbitrary values (bg-[var(--sunken)], bg-(--sunken), [--sunken:white]), as a style object's key ({ "--warning": … }) and in setProperty; a string that only looks like one, such as a script's --chrome argument, stays. Tailwind classes such as bg-sunken don't change. A file that sets both --series-N and --chart-N is listed for you. Candor's own stylesheets come with update, and on a 4.x project theme check reports 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's motion-aliases (CLI 2.x has it: npx candor-ui@2 migrate motion-aliases). theme check still reports 2.2's motion tokens as errors. strings-keys also makes 4.0's metadataDiff namespace changeDiff and the metadataDiffStrings table changeDiffStrings (in migrate 4.0.0), in a provider's strings or an object typed StringOverrides or CandorConfig; CLI 3.0 drops its 3.0 rows (message.thinking, useAnsweringApprovals), which CLI 2.x has.

  • key <key> saves the key to .env.local and switches the registry to send it. key --remove takes it out and switches back. The key is never printed, and .env.local is added to .gitignore if 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, .jsx and .js files against Candor's honesty rules and prints path:line:col rule message for each finding. Exits 1 on an error; --strict makes warnings fail too; --json prints the findings for machines. It skips node_modules, dist, build, coverage and 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 and data-slot selectors (after it). It measures the contrast of your theme. It reads the CSS overrides in the file components.json names (plus candor-brand.css if there is one), or the file you give, on top of Candor's own tokens (the installed candor-theme.css; else /r/theme.json), follows @imported files and var(), and resolves each colour role the way the browser would in light, dark (data-theme="dark") and hc (data-theme="hc"), by selector specificity and then source order, as candor.css is written. Dark by the system setting with no data-theme is a fourth state, and is listed only when your overrides make it differ from data-theme="dark" (candor.css's own rule for it is more specific than :root). Every pairing the theme builder and npm run theme check (44 text and edge pairings, plus the focus ring at 80% on 6 surfaces: 50 per theme) is printed with its measured ratio and ok, FAIL or n/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; --json prints 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 as Light in [data-brand="acme"] and so on (themes[].brand in 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 :root from another (--primary: var(--brand-ink)) keeps the root's --brand-ink there unless the scope sets --primary again, 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 pairings n/a, counted as "not measured" and neither passed nor failed; selectors it doesn't model (.dark, @layer order, 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: a motion.json beside your candor.css; else /r/v/<version>/motion-manifest.json on the registry, for the version candor-lock.json records (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, WARN or n/a, with the file, the line and the fix after it; --json adds a motionAndSound object (findings, sounds, manifest, motionChecked, schema, notes) and ok includes its errors. If motion values were found and the motion checks couldn't run (no manifest), ok is false and motionAndSound.motionChecked is false, 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_check tool at https://candor.themethodic.com/mcp: give it the contents of candor-brand.css and 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-kit in the file is checked against the current names, with a note), follows no @import, fetches nothing, and can't see your public/ folder, so a sound file's length, size and format are "not measured" there. theme check here 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-speed outside 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 the clock family or a clock moment (a hold or deadline is real time passing) | error, with why | | A raw animation, transition or @keyframes cd-* aimed at a Candor slot ([data-slot], .cd-*) | warning: outside the motion layer, not checked | | A sound path that isn't under public/ or static/; 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-theme newer 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-theme marker 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's public/ and static/; nothing is fetched. Colour checks are unchanged.

    Token names and data-slot selectors (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: a tokens-manifest.json beside your candor.css; else /r/v/<version>/tokens-manifest.json for the version candor-lock.json records; 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; none and var() aren't judged | | slot-unknown | A [data-slot] selector that matches nothing a component renders |

    A value that depends on another (var(), calc(), em) is n/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. --json adds a tokens object (manifest, checked, declared, selectors, warnings, findings, notes).

  • theme watch [file] (0.12) runs theme check now and again whenever a file it reads changes, until Ctrl-C (exit 130): the CSS file components.json names (or the one given) and every file it imports, candor-brand.css, the installed candor-theme.css and motion manifest beside candor.css, components.json and candor-lock.json. The list is worked out again after each run, so a new @import is 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 what theme check prints, 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 a candor-brand.css, in the shape of a brand file (candor-brand.css): the overrides to import after candor.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 shadcn globals.css (or any CSS that sets --primary, --background …; :root, .dark and @theme are read, var() is followed; colours as oklch(), hsl(), hex or rgb(), and Tailwind v3's bare HSL channels, 221.2 83.2% 53.3%); a Tailwind config (theme.colors or theme.extend.colors, colours written out in the file, light theme only, read as text, never run); a Figma variables JSON: the REST API's GET /v1/files/:key/variables/local response (meta.variables and meta.variableCollections, with modes and aliases), or a variables array of { name, type, valuesByMode } (also inside collections); 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), *-foreground tokens other than --primary-foreground, --secondary, sidebar and chart tokens, and colours with transparency. The result is then checked with the same engine as theme 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.css works); 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 runs theme check on the file in memory first (colour, motion and sound), and an error stops it with the findings, writing nothing. Then: it writes candor-brand.css next to candor.css (in a monorepo, in the shared package where init put the theme); if a different candor-brand.css is there it shows what replacing it changes and asks (--overwrite skips the question; with no terminal and no flag it stops); it adds @import "./candor-brand.css"; after the candor.css import in the CSS file components.json names, if that isn't there (and tells you the line to add if the file doesn't import candor.css); it lists the sound files the theme names and whether each is present under public/ or static/, 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] is lint plus 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: a disabled control with no title, aria-describedby text, Locked or 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 a prefers-reduced-motion: reduce block in your CSS has already done its work, and infinite-animation names anything that still loops (animation-iteration-count: infinite), and calm-motion anything that still animates or transitions a transform, translate, scale, rotate or height for 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 (a StatusDot, a Skeleton, anything built from the live motion family, the streaming caret, the live dot, a shimmer, a spinner in aria-busy or role="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> inside data-slot="message", message-content, message-body or an assistant message, other than a citation, the header, footer or actions (model output is text, never markup). Playwright (playwright or playwright-core, with a browser) and axe-core are 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 --json has pageAudited: false. The report is written to AUDIT.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 with critical or serious impact are errors, the rest warnings; the lint and DOM rules keep the severities above (model-html and a model link are errors; the others warnings). Exits 1 on any error; --json prints the findings. These checks find some problems; a clean report doesn't mean the page is accessible or honest.

  • doctor checks 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 version candor-lock.json records; see the docs MCP server). From 0.11.2 it also notes @candor-ui/react installed beside copied items (a candor-lock.json with items, or a copied locale file): each brings its own LocaleProvider and TermsProvider, 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 (in candor-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 way doctor lists 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 beside candor.css (two copies of the theme), and both of its stylesheets (styles.css, which loads Inter, and styles-no-font.css) imported at once. And it names a components.json with no lib alias: 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's candor.css imports candor-tokens.css from beside it, a candor.css whose candor-tokens.css isn't there is a fix (npx candor-ui add tokens, which writes it in styles/; for a candor.css you moved from there, it says to move the file beside it), and your CSS importing candor-tokens.css as well as candor.css is 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 to innerHTML, 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 with MessageContent or SafeMarkdown, which read a safe subset of markdown.
  • disabled-without-reason (warning; a control that can't be used says why). A JSX disabled attribute in a file that imports Candor components but never uses Locked or aria-disabled. A greyed-out control with no explanation sends people looking for help, or around the rule. Wrap it in Locked with the reason, or set aria-disabled and say why. Skipped when the value is a transient state (isPending, submitting), explains itself (!onSave, !draft.trim(), page === 0, !hasDetail, a locked or reason variable), is false or a passed-through disabled, or when the element has aria-busy, title or aria-describedby.
  • preselected-decision (warning; nothing is pre-selected in a decision). In a file that imports PermissionPrompt, ApprovalRequest, ApprovalDecision or TypeToConfirm: defaultChecked on an element whose attributes or text say approve, deny, allow, reject, delete or confirm; autoFocus on a button, radio, checkbox or menu item that says one of those; defaultValue set to one of those words, or on a RadioGroup, Select or ToggleGroup whose options say them. A default in an approval nudges people to agree without reading. autoFocus on a text field or a Cancel button, and defaultValue on tabs (whatever their labels say), aren't flagged.
  • estimate-as-measured (warning; unknown stays unknown). A prop named duration, cost, count or tokens (or ending in Duration, Cost, Count, Tokens, Ms or Seconds) 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 the RunEstimate component, 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-down and its relatives), or transition-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, live or clock). A duration that comes from a token (duration-(--cd-dur-base)), Tailwind's other animations (animate-spin), transition-colors and 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 build

migrate 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 --strict

migrate 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 --strict

3.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 what init --yes sets up): supported, the same library as Candor's primitives.
  • Radix (new-york, radix-* styles): supported. The app carries both libraries: its own Radix components in components/ui/, Candor's Base UI ones in components/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). doctor says where Candor's go.
  • React Aria (aria-* styles, "base": "aria", or react-aria-components in package.json): not supported as a base yet. doctor says so, and lists any of your React Aria primitives that sit in components/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 (or cd apps/web first). From the root, init, add, update and key stop and name the workspaces to use instead of setting shadcn up at the root. doctor at the root checks every workspace that has a components.json.
  • init from the app adds the registry to both components.json files (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.local and is passed to shadcn when it runs in the shared package. A .gitignore at the repository root that covers .env.local counts.
  • add from 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.json sits next to the app's components.json and 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's styles/, and update updates the theme where the lockfile records it. A project set up by a CLI before 2.1.0 has tokens recorded at a copy in the app's styles/ that nothing imports: from 2.1.0 update records and updates the copy beside the shared candor.css instead, and removes the app's copy if it's unchanged since it was installed (it says so when it leaves one you changed), and doctor names such a copy. Run update from 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.json as shadcn writes it (the app's ui alias in packages/ui), init and add work and the app builds, with one change: the template's shared tsconfig sets lib to ES2022, and data-table uses ES2023's toReversed, so add "lib": ["es2023", "DOM", "DOM.Iterable"] to the app's compilerOptions. Measured with shadcn 4.23.0, Candor 4.1.0 and thread composer change-set-review card-registry use-attention approval-request data-table in 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's ui alias to @/components/ui first. 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