Repository navigation
Releases: dgesteves/onsystem
Release list
v0.10.0
onsystem's GitHub Action at 0.10.0. Pin it with uses: dgesteves/onsystem@v0.10.0, or use @v0 to follow each release.
Release notes: https://github.com/dgesteves/onsystem/releases/tag/onsystem%400.10.0
v0.9.1
onsystem's GitHub Action at 0.9.1. Pin it with uses: dgesteves/onsystem@v0.9.1, or use @v0 to follow each release.
Release notes: https://github.com/dgesteves/onsystem/releases/tag/onsystem%400.9.1
onsystem@0.9.1
onsystem 0.9.1 reads tokens the way your stylesheets apply them, and no longer slows down on very long values.
- shadcn/ui's Tailwind v4 codemod theme (
--color-destructive: hsl(var(--destructive))) is read as the tokendestructive, sotext-red-500is fixed totext-destructive, nottext-[hsl(var(--destructive))]. Stylesheets imported one after another apply in CSS order, and whencomponents.jsonnames a stylesheet in another package (as cal.com's does), the app's own stylesheet is read first. - A Tailwind v3 config keeps Tailwind's default radius scale:
rounded-[12px]is fixed torounded-xl, notrounded-lg. In a v3 project with no radius tokens of its own,no-hardcoded-radiusnow runs against that scale, socheckcan report warnings it did not before. - A class with an arbitrary length thousands of digits long (
p-[999…9x]) took about 25 s to check at 100,000 characters, incheck,check_ui, the ESLint plugin and the Action; it now takes a few milliseconds. Avar()whose fallback holdscalc()or anothervar()now has the fallback's value.onsystem export --format dtcgkeeps a__proto__token and no longer adds properties to every object in the process. - Thanks to @Aaqibhafeezkhan (
get_componentexplains when a project has no components, #234) and @Golden007-prog (a failed warning cap is explained in every CLI output format, and JSON and SARIF stay parseable, #235).
Patch Changes
-
#234
643bc6fThanks @Aaqibhafeezkhan! - Explain when a project has no design-system components available. -
#235
65f91f5Thanks @Golden007-prog! - Explain a failed warning cap in every CLI output format: a GitHub error annotation, or a message on stderr that leaves JSON and SARIF output parseable. -
#232
a965061- A long length or color no longer slows onsystem down. A class with an arbitrary length thousands of digits long, such asp-[999…9x], took a second to check at 20,000 characters and about 25 seconds at 100,000, incheck, the MCPcheck_uitool, the ESLint plugin and the GitHub Action: the pattern that reads a length tried every way to split the digits. It now takes a few milliseconds. The same goes for token values: a length, avar()with a fallback, or bare HSL or RGB channels in the design system's stylesheets or Tailwind config, and typography, spacing and radius values inonsystem export. Avar()whose fallback holdscalc()or anothervar()now has the fallback's value, insidecalc()too:calc(2 * var(--gap, var(--space-2)))is read as a length, andcalc(var(--gap, var(--space-2)) + 1px)no longer drops the+ 1px.onsystem export --format dtcgkeeps a token whose path has a__proto__part, such as--color-__proto__, under a"__proto__"key of its group. It was left out, and a name such as--__proto__.pollutedadded apollutedproperty to every object in the process that ran the export. -
#201
ee231a8- Tokens are read the way the stylesheets and the Tailwind config apply them, in four cases where they were not.- An
@themecolor that wraps a variable of bare channels, as shadcn/ui's Tailwind v4 codemod leaves a v3 theme (--color-destructive: hsl(var(--destructive))with--destructive: 0 84.2% 60.2%), is the tokendestructivewith its class. The class and the color were on two tokens, so every fix was an arbitrary value:text-red-500was fixed totext-[hsl(var(--destructive))], and now totext-destructive.rgb(var(--x))over RGB channels counts too. An@themevalue that names another variable takes its color: with--color-brand: var(--color-blue-600),bg-blue-600suggestsbg-brand. - Stylesheets imported one after another apply as CSS applies them: a later
@importoverrides an earlier one, and the importing stylesheet's own rules override both. With@import "./base.css"; @import "./brand.css";,--primarycame from base.css, sobg-[[#171717](https://github.com/dgesteves/onsystem/issues/171717)]was fixed tobg-primary, which renders brand.css's color. - A Tailwind v3 config that extends
borderRadius(as shadcn/ui's does, withlg,mdandsm), or sets none, keeps Tailwind's default scale:rounded-[12px]is fixed torounded-xl, notrounded-lg(8px), androunded-b-[24px]torounded-b-3xl, notrounded-b-full. Atheme.borderRadiusoutsideextendreplaces the scale, unless it spreadsdefaultTheme.borderRadiusinto it. No default step is added when the scale cannot be read: a step of the config has no known size, because no stylesheet read sets the variable it uses (var(--radius)); the config takes a preset, a theme or the whole config from an installed package (presets: [require("@acme/preset")],theme: require("@acme/theme"),module.exports = require("@acme/tailwind-config"),{ ...base, content }), which may replace the scale; orborderRadiusis set from a variable, a function or a spread other thandefaultTheme.borderRadius. Tailwind's own modules (tailwindcss/defaultConfig) do not count. Athemethat is a module of the project, or spreads one (theme: require("./theme"),theme: { ...require("./theme") }), replaces the scale with its ownborderRadius, andpresets: []leaves out Tailwind's default config, so neither keeps the default steps unless atheme.borderRadiusspreads them back in. Nor is one added in a Tailwind v4 project that reads the config through@config, which has v4's scale. In a Tailwind v3 project with no radius tokens of its own, such as Dub,no-hardcoded-radiusnow runs against that scale, socheckcan report warnings it did not before. - When
components.jsonnames a stylesheet in another package, as cal.com's names coss/ui's, zero config also reads the app's own stylesheet first (styles/globals.css,app/globals.cssand the others it looks for withoutcomponents.json), and the named one through it when the app's stylesheet imports it. cal.com's own tokens (text-subtle,bg-emphasis,rounded-cal) were not read, and color fixes named coss/ui's tokens instead. When the app's stylesheet imports Tailwind v4, the spacing and radius rules use Tailwind's scale, as in any Tailwind v4 app.
- An
onsystem@0.10.0
onsystem 0.10.0 layers an app's config over the one its design-system package declares, instead of replacing it.
- An app's own
onsystem.config.jsonnow builds on its design-system package's config: itsrulesapply over the package's, rule by rule, and what it leaves out (components,importPath, docs, tokens) comes from the package, unless it setscomponentsof its own. Before, a config that only turned one rule to"warn"lost the package's components and tokens, andchecksaidNo design system foundand passed."extends": falsereads a config alone, as before. - A monorepo root's config does the same when you check from the root, so its
rulesandoverridesapply to every project without a config of its own. initno longer writes pnpm's version-named paths (node_modules/.pnpm/@acme+ui@1.0.0…) into a config, which brokecheckafter an upgrade.init --forceon a package that declares a config writes only$schema, keeps the components of an app config that names its own, and keeps atokenslist that names the app's own stylesheet beside the package's.inspectnames both configs it reads, andinspect --explainsays where the components come from.onsystem export --format design-mdkeeps each table row on one line when a comment or value wraps over lines (#237).
This can change what check reports in an app that has a config and a design-system dependency that declares one: it now gets the package's rules where its config leaves them out.
Minor Changes
-
#202
3798fb7- An app's own config builds on the config its design-system package declares, instead of replacing it. What the app's config leaves out comes from the package's, unless it setscomponentsof its own:components, with the package'simportPath,docs,elements,tokensandlibraries. A config that setscomponentsreads its tokens as before: withouttokens, from the app's own stylesheet (app/globals.css) and the ones it imports. Itsrulesapply over the package's, rule by rule, either way, and, with the package's components, itselementselement by element; the package'sexcludepatterns are added to the app's; itsoverrides,tsconfigandincludeDesignSystemare its own. Before, an app config that only turned one rule to"warn"lost the package's components and color tokens: in an app that installs the package,checksaidNo design system foundand passed.inspectnames both configs (config onsystem.config.json, over node_modules/@acme/ui/onsystem.config.json (declared by @acme/ui))."extends": falsein the app's config reads it alone, as before. This can change whatcheckreports in an app with a config of its own whose dependency declares one: it now gets the package's rules where its config leaves them out, and the package's components and tokens when it sets nocomponentsof its own. A workspace package whose"onsystem"field cannot be read is a warning in such an app, which then reads its own config alone, and still an error in an app without one.A monorepo root's config builds on a project's design-system package config the same way in
checkandinspectfrom the root, so itsrulesandoverridesapply to every project without a config of its own. Before, the config a project's design-system package declared took their place, without a word. From the project's folder, the root's config is still not read.init --forcein an app on such a package, without a config or with one whosecomponentsmatch no file outside the package, writes a config with only$schema, for the app's own rules and overrides, and keeps the package's components,importPath, tokens and rules. Before, it wrote the package's paths into the app's config, or a config that lost the package's components. In an app whose config names components of its own, it writes them again, as before.initin an app whose design system zero config reads from an installed package leavescomponentsandtokensto zero config. It wrote paths into the package's folder, which pnpm names by version (node_modules/.pnpm/@acme+ui@1.0.0_react@19.0.0/node_modules/@acme/ui/…), so after an upgradecheckfailed withToken file not found, and the component files it listed matched nothing.In a design-system package whose
filesleave out its component sources,initsays that apps installing it read its components from the type declarations it publishes, when itspackage.jsonnames some. It said they get no component checks, which holds only without type declarations.
Patch Changes
- #237
5a10990-onsystem export --format design-mdkeeps each table row on one line. A token whose comment or value the stylesheet wraps over several lines, such as a two-part--shadow-lg, broke its row: GitHub showed the rest of it as a code block and every later row in that table as a paragraph. The lines are now joined with spaces.
v0.9.0
onsystem 0.9.0 reads design systems published as compiled packages, keeps baselines right when files move, and gives agents clearer MCP answers.
- An app on a design-system package installed as
dist/plus type declarations now gets every rule: components, props and variants come from its.d.tsfiles (@dub/uifrom npm gives 427 components). - The baseline follows a renamed or moved file, and in the GitHub Action a copied baselined line is reported where it was added. The Action runs the onsystem version the project installed, so the lockfile decides; without an install it runs its own version as before.
no-hardcoded-colorwarns when achart-*token stands in for a status color outside a chart, andinitsays which status colors the theme has no token for.- MCP:
search_componentssays when nothing clearly matches instead of returning a poor best guess,list_componentsfilters and collapses icons, andget_tokenssets chart and sidebar colors apart.
onsystem's GitHub Action at 0.9.0. Pin it with uses: dgesteves/onsystem@v0.9.0, or use @v0 to follow each release.
Release notes: https://github.com/dgesteves/onsystem/releases/tag/onsystem%400.9.0
onsystem@0.9.0
onsystem 0.9.0 reads design systems published as compiled packages, keeps baselines right when files move, and gives agents clearer MCP answers.
- An app on a design-system package installed as
dist/plus type declarations now gets every rule: components, props and variants come from its.d.tsfiles (@dub/uifrom npm gives 427 components). - The baseline follows a renamed or moved file, and in the GitHub Action a copied baselined line is reported where it was added. The Action runs the onsystem version the project installed, so the lockfile decides; without an install it runs its own version as before.
no-hardcoded-colorwarns when achart-*token stands in for a status color outside a chart, andinitsays which status colors the theme has no token for.- MCP:
search_componentssays when nothing clearly matches instead of returning a poor best guess,list_componentsfilters and collapses icons, andget_tokenssets chart and sidebar colors apart.
Minor Changes
-
#192
eeb3af0- The baseline follows a file that is renamed or moved. Its entries are keyed by path, so aftergit mvevery finding of the file was new tocheck .and to the GitHub Action withonly-changed-lines: false, and nothing was said about the entries left under the old path. Now a file with no entries of its own, new or not, takes over those of a file that no longer exists when at least half of its findings match them, and half of them match its findings, as git counts a rename from 50% similarity. A rename that changes only the case of a name counts too, also on macOS and Windows, where the old spelling still opens the file.checksays which file it took for moved, and--prune-baselinerecords the new path. Each file that no longer exists is taken over by one file at most, so copies of a file are still new, and a finding the move adds is still reported. In the GitHub Action, a file the pull request did not change keeps the entries it took over, so a copy the pull request adds is the new one, even when the rename was merged without--prune-baseline;checkalone prefers the file whose findings match the old entries most closely, then a file that kept the old name, then one in the old folder. The entries of deleted files in the folders a run checks now count in the "no longer occurs" hint.The GitHub Action tells
checkwhich lines a pull request changed, and the baseline accepts the findings on the other lines first. A pull request that copied a baselined line above the original passed before: the baseline accepted the copy, and the original, reported instead, was on a line the pull request did not change. Now the copy is reported, on the line it was added, and fails the check.checkwithout the Action still accepts findings in file order.The GitHub Action runs the onsystem the project installed, found in
working-directoryor the nearest folder above it up to the repository root, so the version the lockfile pins checks the pull request, and the log says when it is not the action's own version. Without an install, it runsnpx onsystem@<the action's version>as before. With an install, the action's results now follow the installed version, so this can change the result of a job that passed before: an older install can have fewer rules, and one older than this release accepts baselined findings in file order, whatever lines the pull request changed. A newer one can find more. -
#193
b8e0853-no-hardcoded-colorwarns on achart-*token used outside a chart, such astext-chart-2on a green "Connected" label in a theme with no success color. The class is a token, so it was counted as clean, but it changes with the chart palette and does not say what a status color says: in benchmark v2's two tasks that ask for a small green label in such a theme, all 24 runs with onsystem's skill, MCP server and hook together used a chart token for the green, and 0 of 24 without onsystem did. The warning does not failcheckor block the hook, and has no fix: it says to add a status token for a status color (--success, as docs/tokens.md#status-colors shows) or to use the token that fits. Chart tokens are left alone in a file that draws a chart (it imports a chart library, renders a tag such as<ChartContainer>or<BarChart>, declares a name such aschartData, or is named for charts), in an element inside an<svg>, a<figure>or arole="img", and everywhere when the design system's own components use them outside a chart. The real-world corpus has no such finding; sidebar tokens, which apps use for their frame (bg-sidebaron a shell or header), are not reported.onsystem initsays which of success, warning and info the theme has no color for, after the dry run, with a link to the CSS that adds them.icon-button-accessible-nametakes a button's name from a<label htmlFor>(or the design system's<Label htmlFor>) whose value is the button'sid, as for<label htmlFor={id}>Autopilot</label>next to<button id={id} role="switch">. A button is a labelable element, so that label is its accessible name, and the button was reported without one. -
#194
a5eafef- An app on a design-system package installed from a registry as compileddist/and type declarations alone now gets every rule: its components, props, variant values, parts and the elements they render are read from its.d.tsfiles, as MUI's are. Before, such an app got the token rules andchecksaidNo design-system components found: the component rules did not run.This applies to a package that declares its onsystem config (set up withinit), whatever its name; to one without a config that is named like a design system in the app's own npm scope (@acme/uifor@acme/web); and, in any app, to the dependency the app's config names as itsimportPath:{ "importPath": "@dub/ui" }in an app on@dub/uifrom npm reads its 427 components. A kit from another scope named like one (@sanity/ui,@react-email/components) is not read unless the config names it, and a package whose.tsxsources the app reads is read from them as before; one that publishessrc/besideexportsintodist/is read from its declarations. A package with no entry of its own is read from each subpath it exports (@acme/ui/button).inspectlists it on itslibraryline (Design-system package (@acme/ui 1.0.0): 10 components from its type declarations),inspect --explainsays so, or what would make it read,"libraries": falseturns it off, and the app's@importof its stylesheets brings its tokens.Reading declarations also got more exact for MUI and any package: a capitalised export is a component only when it renders (a function returning a string, or a class without
render, is not); variants that tsup inlines with cva'sClassPropare kept, since itsclass-variance-authority/dist/typesimport now resolves by path when the app's"moduleResolution": "bundler"does not; a function component's element comes from its DOM attributes when it takes noref; props that depend on a genericcomponentthrough a conditional type are left open, so they are not reported as unknown; andTabsnext toTab, or a loneCheckboxGroupnext toCheckbox, is no longer taken for its part. In a bundle's oneindex.d.ts, parts go by name only in families of two or more, and icons have none:@dub/ui'sUserCheckis not a part ofUser, norButtonTooltipofButton. -
#198
6bd19f5- The MCP tools answer better where the design system is thin, large, or has no component for what the agent wants.search_componentssays when no component clearly matches: "a dropdown menu to pick a country" in a design system with no select used to come back asCardAction, best first, because its description mentions a menu. A result now matches clearly when a word of the query is in its name, or when it has more than half of the query's words (itself or through a synonym). When some do, the others are left out; when none does, the answer starts "No component clearly matches", lists the nearest and marks each resultweak: truein the structured content.- A shadcn/ui component without JSDoc, laid out as
npx shadcn addwrites it (Badgeincomponents/ui/badge.tsx: the file named for it, in auifolder), is described by what shadcn/ui's component of that name is for, said as shadcn/ui's (shadcn/ui's Badge: A small label for a status, a count or a category.), inlist_components,get_component, resources and the search index, so "status label" finds Badge first in a stock app. A component of the same name elsewhere, such as a design system'ssrc/sheet.tsx, gets no borrowed description. A component with no description is listed without one, not as "No description." list_componentstakesquery,kind("component"or"icon") andlimit. Icons, the components that render an<svg>, are named on one line when there are five or more from one import, or from one folder for an icon set with a module per icon (components/icons/check.tsx, each named with its module), instead of one entry each, in the text and in the structured content, which has them undericonsand leaves out emptyvariantsandsubcomponents.kind: "icon"lists them one by one.get_componentlists the parts before the props, cuts a prop's description lon...
v0.8.0
onsystem 0.8.0 reads more of the design system you actually use, reports more of the hardcoded styling agents write, and checks Codex edits. It is a minor release: code that passed can get new findings, so run onsystem check . --update-baseline once after upgrading if you use a baseline.
- More findings, safer fixes: Tailwind
whiteandblackare reported, class strings kept in variables in the same file are followed, and icon-onlyasChildlinks are checked. A background is never fixed to a token where the text on it would stop standing out in dark mode. - Zero config reads more of your design system: an installed package named like one (
@acme/ui) that publishes its.tsxsources, and the foldersnpx shadcn addcopies other registries' code into, read-only next tocomponents/ui.initwrites those registry folders into the config. excludenow adds to the defaults (tests, stories, mocks and.d.tsfiles stay out) and leaves out the same files incheck, at a monorepo root and in the ESLint plugin.checkandcheck_uiread only JS and TS files, socheck $(git diff --name-only)no longer parses CSS or Markdown as TSX, and--require-design-systemfails when nothing was checked.- The plugin's hook now checks edits in Codex, which installs the Claude Code plugin and runs its hook on
apply_patch.
onsystem's GitHub Action at 0.8.0. Pin it with uses: dgesteves/onsystem@v0.8.0, or use @v0 to follow each release.
Release notes: https://github.com/dgesteves/onsystem/releases/tag/onsystem%400.8.0
onsystem@0.8.0
onsystem 0.8.0 reads more of the design system you actually use, reports more of the hardcoded styling agents write, and checks Codex edits. It is a minor release: code that passed can get new findings, so run onsystem check . --update-baseline once after upgrading if you use a baseline.
- More findings, safer fixes: Tailwind
whiteandblackare reported, class strings kept in variables in the same file are followed, and icon-onlyasChildlinks are checked. A background is never fixed to a token where the text on it would stop standing out in dark mode. - Zero config reads more of your design system: an installed package named like one (
@acme/ui) that publishes its.tsxsources, and the foldersnpx shadcn addcopies other registries' code into, read-only next tocomponents/ui.initwrites those registry folders into the config. excludenow adds to the defaults (tests, stories, mocks and.d.tsfiles stay out) and leaves out the same files incheck, at a monorepo root and in the ESLint plugin.checkandcheck_uiread only JS and TS files, socheck $(git diff --name-only)no longer parses CSS or Markdown as TSX, and--require-design-systemfails when nothing was checked.- The plugin's hook now checks edits in Codex, which installs the Claude Code plugin and runs its hook on
apply_patch.
Minor Changes
-
#180
9f40cac-checkandcheck_uiread only JS and TS files. A CSS, Markdown or JSON file named outright or matched by a glob, ascheck $(git diff --name-only), lint-staged's"*"orcheck "**/*"passes them, was parsed as TSX and gave hundreds of syntax errors (209 for a shadcn/uiapp/globals.css), so the run failed on files with no UI code in them.checknow skips such a file and counts it in the summary line (3 files that are not JS or TS skipped).check_uianswers with an error for apathorfilenamethat is not.tsx,.jsx,.tsor.js(or.mts,.cts,.mjs,.cjs): a.vueor.cssfile was checked as TSX before.--require-design-systemalso fails (exit code 2) when the folders and globs it is given leave no file to check, so CI cannot pass by checking nothing: a JavaScript app whose pages are.jsfiles, a job that runs in a design-system package, whose files are all its own components, or a glob that matches only styles. Files named outright that are not JS or TS still pass with nothing to check. This can fail a job that passed before.Folders still stand for their
.tsxand.jsxfiles, andchecknow names the.jsfiles in them that have JSX (Next.js's JavaScript template writesapp/page.js) with the command that checks them, such ascheck . "**/*.js", in place of "no files match" for a folder of.jspages, and as the first line of the--require-design-systemfailure. In the GitHub Action,paths: . **/*.jschecks them.A run with 50 findings or more ends, in the default output, with the count per rule and the ten Tailwind palette classes it found most, each with the token to use.
--format json,sarifandgithubprint the findings alone, as before. -
#183
7159b5b-excludeadds to the defaults instead of replacing them. A config whoseexcludelists folders without repeating the defaults now also leaves out tests and stories (*.test.tsx,*.stories.tsx,__tests__,__stories__and__mocks__folders) and.d.tsfiles, which it checked before. To check one of them again, add the default with!in front ("!**/*.{test,spec,stories}.{ts,tsx,js,jsx}"), or pass--include-teststo check tests and stories for one run; a!pattern that names no default gets a warning. A design-system package'sexcludeadds to the defaults for finding its own components too. -
#176
e7335d8- Zero config now reads a design-system package installed from a registry when it is named like one (@acme/ui,@acme/design-system) and itsexportspoint at the.tsxor.jsxsources it publishes, directly or through barrels, as it reads a workspace package, with no config in the app or the package. Before, such an app got no components and no tokens: a package that publishessrc/, such as one whoseexportspoint at./src/index.ts, was skipped as compiled. Its exported stylesheets are its tokens, with the ones of the package they@import, and an app stylesheet's@import "@acme/ui/globals.css"is followed intonode_moduleswhen the app's config names notokens. Other installed packages, such astailwindcss, are not read. Itsexportsare read through the conditions an app resolves (import,require,module-sync,node,module,browser,style,default), and the first of them decides, for its components and its stylesheets alike, so a package whosesourceorexpo-sourcecondition points atsrc/before adefaultthat points at its build, such as@expo/uior one built with react-native-builder-bob, is not read either. Unlike in a workspace package, exports that point atdist/are not followed back tosrc/, and nothing compiled in the package is read (JavaScript, JSX or CSS indist/,build/,lib/,out/,esm/orcjs/, or an export that points at JavaScript anywhere, such as./index.jsor./es/index.mjs, whose re-exports are not followed), so a component kit named like a design system that publishessrc/next todist/for its source maps is not read as a second design system. With pnpm the package is read innode_modules/.pnpm, so the props its components take from their own dependencies resolve. A package whoseexportspoint at build output is still not read, andinspect --explainnow says why:its exports point at build output (node_modules/@acme/ui/dist/index.mjs), not at component sources, instead of calling every installed package compiled. An app whose own config leavescomponentsunset now picks up such a package too. -
#183
83a40a0- Zero config reads the foldersnpx shadcn addcopies other registries' code into as read-only design systems, next tocomponents/ui: for each registry in theregistriesofcomponents.json, the folder of that name under thecomponentsalias (@signoff-ui→components/signoff-ui,@ai-elements→components/ai-elements). Before, that code was checked as the app's own: in VoltAgent's chatbot starter, which the corpus now checks, 11 of the 14 findings ofcheck .were incomponents/ai-elements, and the nextshadcn addcould bring findings in registry files nobody edited, which failedcheckand the GitHub Action. Nowcheck, the Claude Code hook, the ESLint plugin and the Action skip those files, as they skipcomponents/ui;search_components,get_componentandlist_componentsfind their components, andget_componentsays which registry one was installed from and that changes to it belong upstream; and the app's code is checked against them, so<DiffReview tone="danger">or<Message.Header>is a finding, and so is an icon-only registry button without a name. None of them is suggested in place of a native element, and the helpers in a folder'slib/(signoff-ui'sImgand icons) are left out oflist_componentsandsearch_components.inspectnames the folders on itsdetectedline, andinspect --explainsays why each was read or not. A folder is not read when the app's components come from a workspace package imported through itsexports, and code a registry copies loose intocomponents/is still checked as the app's.initnames those folders on its "Found the design system" line instead of a note of its own.A config that lists such a folder in
components, asinitwrites it, reads it the same way: its components are no longer suggested for native elements (signoff-ui'sImgfor an<img>); map one inelementsto have it suggested. A folder thatcomponents.jsonnames in noregistriesentry is read as before. -
#174
005d204-check,check_ui, the hook and the ESLint plugin report more of the hardcoded styling agents write, so code that passed can get new findings:no-hardcoded-colorreports Tailwind'swhiteandblack(bg-white,text-black,border-white), which passed whilebg-[white],style={{ color: "white" }}andfill="white"were reported, sobg-white dark:bg-zinc-900had only its dark half fixed. They are fixed only to a token of exactly that color (bg-white→bg-backgroundin a stock shadcn/ui theme), and suggested without a fix otherwise. A theme that defines--color-whiteor--color-blackmakes them its tokens, which are not reported. White or black text, fill or stroke is a warning without a fix, since its token depends on the surface under it. A hardcoded background (bg-white,bg-zinc-900,bg-[[#111](https://github.com/dgesteves/onsystem/issues/111)]) under hardcoded text that keeps no fix (white or black,text-black/80, or a color such astext-neutral-800held back as another shade), on it, inside it or inherited from around it, is not fixed to a token unless the text still stands out from it in every mode, by 0.25 of lightness or 3:1 contrast as it did on the hardcoded color:bg-black text-whitewould becomebg-primary text-white, white on near white in dark mode, an...
onsystem@0.7.0
onsystem 0.7.0 reads more of your theme, makes its fixes safer and closes a file-read hole. It is a minor release: read the upgrade note under Minor Changes before you update.
- Security: component, token and docs files are read only when their real path is in the project, its workspace or git repository, or an installed package. A cloned repository can no longer make
get_componentreturn a file elsewhere on the machine through a committed link or a../path. - Zero config now finds color tokens kept in a shared Tailwind config package, as Dub keeps them, so such projects get color findings for the first time. Run
onsystem check . --update-baselineonce after upgrading, and pin the GitHub Action to a commit SHA if you need a stable check:@v0now runs 0.7.0. - A color whose hue has no token in your theme is now a warning that names two ways out, not an error that pushed agents to a gray or a chart token.
- Fixes no longer change what code means:
eslint --fixleaves files that compile, color fixes keep hover states, opacity and status colors, guessed props and variant values are suggested, not applied, icon-button labels are fixed only when the icon says what the button does, and 1px hairlines are left alone. check --prune-baselinedrops fixed findings without accepting new ones, andcheck,check_uiandinspectnow say when a component library is not read yet or the dependencies are not installed. The Claude Code hook holds an Edit to the lines it changed and uses a monorepo root's config and baseline. Thanks to @CRYPTONIKAV for the SVG mask fix (#110) and the hairline spacing fix (#153).
Minor Changes
-
#107
9ecb46dThanks @dgesteves! - Zero config now finds color tokens kept in a shared Tailwind config package, as Dub keeps them inpackages/tailwind-config: it follows the@imports of the stylesheets it finds, by a path or into a workspace package (@import "@acme/theme"in Tailwind v4), reads presets that a package exposes only throughexports, takes the stylesheets of a shared preset's package that set the colors its config maps, and reads bare RGB channels (--content-error: 220 38 38) as colors where the config reads them withrgb().Upgrading from 0.6.0: a project that keeps its color tokens in such a package got no color findings from 0.6.0, which said "No color tokens found: no-hardcoded-color did not run". Now the rule runs and reports the colors it skipped: in Dub's
apps/web, 10,024 errors and 164 warnings that a baseline written with 0.6.0 does not hold, socheckexits 1 there. Runonsystem check . --update-baselineonce after upgrading to accept them; in Dub the 1,052 findings 0.6.0 recorded still match, andcheckexits 0 again. The warnings are colors whose hue has no token in the theme, and they do not failcheckunless you set--max-warnings(orfail-on: warningin the Action). The GitHub Action at@v0moves to each release when it is published, so pin it to a commit SHA if you need a stable check, and move the pin when you update the baseline.
Patch Changes
-
#170
57aef3bThanks @dgesteves! -check --prune-baselinedrops the baseline entries that no longer occur and accepts nothing new, and it is the commandchecknow prints when baselined findings get fixed. The command it printed before,--update-baseline, also recorded every new finding of the run, so following it next to a new error accepted the error. The printed command keeps--baseline,--root,--config,--components,--tokens,--docs,--include-design-systemand--include-tests, so it rewrites the same file from the same findings; before, with--baseline <file>it wrote a second baseline at the root.--update-baselinecreates the baseline's folder, and with--format json,sariforgithubprints its summary on stderr, so stdout stays in the format.The baseline now records the onsystem version that wrote it and the rules that ran. When a rule that did not run then reports findings, as when an upgrade finds tokens an older version missed,
checksays they are new to the baseline rather than to the code, and how many other new findings--update-baselinewould accept with them. A baseline without these fields is read as before, and older versions of onsystem read the new ones without complaint. -
#147
a19115dThanks @dgesteves! -no-hardcoded-colorno longer offers two kinds of fix that changed what a color means, whicheslint --fixandapplyFixesapplied unasked. A class under a condition (hover:,focus-visible:,md:) is not fixed to the token the same class list has or gets without it, nor the other way round, unless the two already have exactly the same color:bg-[#2563eb] hover:bg-[#1d4ed8]with a blueprimarybecamebg-primary hover:bg-primary, and the button lost its hover. A slight change counts too:bg-[#fff] hover:bg-[#fafafa]keeps its hover. The base class is still fixed, and the message suggests keeping the two apart, such as withhover:bg-primary/90;dark:classes still get the same token, and so do two hardcoded classes of exactly the same color (border-red-500 focus:border-red-500), whose condition changed nothing before either, and classes for a pseudo-element or other elements (placeholder:,prose-a:,[&_svg]:). A fix to a token that is not a status color is held back when a status color of the color's hue is close too and at most ΔE 0.03 further: with a rosebrandnext to a reddestructive,bg-[#ef4444]on a payment-failed alert becamebg-brand, so the error would follow the next rebrand. The message now offers both (bg-destructiveif the color shows a status, otherwisebg-brand). The brand's own color, and reds thatdestructivematches, are still fixed. -
#141
a83de7cThanks @dgesteves! -no-hardcoded-colorcounts opacity when it matches a color to a token. It used to compare only lightness, chroma and hue, so a translucent color matched its opaque token and the fix made it opaque:bg-[#e7000b33](the red at 20%) becamebg-destructive,rgba(255, 255, 255, 0)became the opaque background, and in shadcn/ui's dark themedark:border-[#ffffff]becamedark:border-border, which is 10% white. Now a token more than 0.02 apart in opacity is never a fix, and the message gives both opacities (destructive is opaque, and this color is 20% opaque.), so the color can keep its opacity with a modifier (bg-destructive/20). Colors and tokens of the same opacity still match (dark:border-[rgba(255,255,255,0.1)]→dark:border-border, and 15% white →dark:border-input), an opacity modifier still stays on the token (bg-[#ffffff]/40→bg-background/40), and "same value as" lists only tokens of the same opacity. -
#126
7e11d9bThanks @dgesteves! -no-hardcoded-colorreads tokens whose names start withon-(Material's) as text colors, and those that start withcontent-(Dub's) too, unless the name goes on to say border or background (content-border,content-background).text-neutral-400now matches Dub'scontent-muted, andborder-*,ring-*andbg-*classes are no longer fixed to a text color. When the only close token is made for another role, the message names it ("content-subtle has this color, but it is meant for text") instead of saying that no token is close. A Tailwind palette class is now fixed only to a token that matches it (ΔE under 0.02), or to one that is nearest the class's own shade and within ΔE 0.037 of it, the most Tailwind v3 and v4 differ on one shade, so tokens that copy v3's palette (text-red-600to a token holding v3's red-600) keep their fix. Other close tokens are still suggested, without a fix, soeslint --fixleaves them, and the message says why and what to do:content-defaultfortext-neutral-600is one shade darker (neutral-700), and a token at neutral-500 fortext-slate-500is closest to neutral-500, not slate-500. -
#157
efb3888Thanks @dgesteves! - In an app whose design-system package publishes only compileddist/, not its component sources,no-unknown-componentno longer reports every component the app imports from the package ("Button" is not a design-system component (imported from "@acme/ui")) when the package's config setsimportPath, asinitwrites it for a package apps import by one name. Those errors blocked the Claude Code hook and failed CI on real components, whilechecksaid in the same run that the component rules did not run. With no component to check against, imports from the package are now left alone, by the CLI, the MCP server, the ESLint plugin, the hook and the Action. The token rules andicon-button-accessible-nameon native elements still run. To check the package's components in those apps, addsrc/tofilesin itspackage.json. -
#131 [
43b66c9](https://github.com/dgesteves/onsystem/com...
onsystem@0.6.0
Minor Changes
-
#92
ebc7dbcThanks @dgesteves! -onsystem initin a design-system package now declares its config in its package.json ("onsystem": "./onsystem.config.json") and writes ONSYSTEM.md for the teams of the apps that use it, and an app with no config of its own takes on the config a dependency declares, from the workspace or fromnode_modules, in the CLI, the MCP server and the ESLint plugin alike.In a monorepo, an app with no config of its own that depends on such a package reads the package's config rather than the monorepo root's, so the root config's rules no longer apply to it.
-
#87
6c20ad8Thanks @dgesteves! - Import and export design tokens as DTCG 2025.10 and DESIGN.md. DTCG files now read$roottokens and groups that$extendsanother, which were left out before. A DESIGN.md's front matter is a token source, which zero config reads when it finds no stylesheet or DTCG file.onsystem export --format dtcg|design-mdprints the tokens it found, or writes them with--out: the DTCG file keeps colors as color objects, sizes as{ value, unit }, aliases as references, and the token's name, the custom property, the Tailwind theme variable and the dark mode under$extensions["com.github.dgesteves.onsystem"], so reading it back gives the same findings, with the same token names. -
#86
1d8000eThanks @dgesteves! - MUI apps get a design system with no config: when a project depends on@mui/materialand it is installed, onsystem reads MUI's components from the package's type declarations with the TypeScript checker, for MUI 5 to 9. It reads each component's props (Button'shrefsignature included), the values of its string-literal props as variants (variant,color,size, TextField's genericvariant), values the app adds by module augmentation or in its theme (teal: { main },components.MuiChip.variants), the element it renders and the module that default-exports it.check,check_uiand the hook then report invented MUI components, props and variant values, skip props whencomponentis set, and count aTooltipwith a stringtitleas its child's accessible name. Palette colors, the spacing unit, border radius and font family come from thecreateThemeoptions the app writes out, following constants it imports by a relative path. Where the app has components of its own, those win a shared name in what the tools offer and suggest, while code that imports MUI's is checked against MUI's.inspectshows the library on alibraryline, and a workspace package that uses MUI is a project of its monorepo root."libraries": falsein the config turns this off, and a list (["mui"]) reads only the libraries it names.check --require-design-system, and the GitHub Action, which turns it on by default, count MUI's components as the app's design system: when the theme's colors cannot be read, as with a palette from Sass variables, the check runs and says thatno-hardcoded-colordid not. Libraries are adapters (src/libraries), so Chakra UI, Mantine, HeroUI and Radix Themes can follow.
Patch Changes
-
#79
ebb0fa8Thanks @dgesteves! - The GitHub Action's description is shorter than 125 characters, so the release can be published to the GitHub Marketplace. -
#80
037b19cThanks @dgesteves! -initwrites a config wherecheckandinspectalready find the design system without detecting one: an app withcomponents/uiand nocomponents.json. It spells out the default globs that match and the token files found, with--yes, without a terminal, and in one (where it no longer asks where the components are). Before,init --yesandinitwithout a terminal exited 2 with "zero config found no design system". -
#96
a380586Thanks @dgesteves! -initnamesonsystem.baseline.jsonin its next steps only when it wrote one. Without a terminal, it says that it skipped the baseline question and thatonsystem check . --update-baselinerecords one.inspect --explainlists eachdocsglob once, where a shadcn/ui app listedcomponents/ui/**/*.{md,mdx}twice. The MCP server's load line counts components asinspectdoes:loaded 4 components (+6 parts), notloaded 10 components. -
#84
7042a12Thanks @dgesteves! - The package's npm page links to the website, onsystem.vercel.app, rather than the README on GitHub. -
#90
8d62016Thanks @dgesteves! - A Tailwind v3borderRadius.DEFAULTtoken is suggested asrounded, notrounded-DEFAULT. -
#89
58e6b63Thanks @CRYPTONIKAV! - Tailwind v3 projects: radius values set intailwind.configborderRadius, or in a preset it imports, are read, so4pxin a project whoserounded-smiscalc(var(--radius) - 4px)suggestsrounded-sm. Thanks @CRYPTONIKAV. -
#80
8549f8fThanks @dgesteves! -npx onsystemwith no command, typed in a terminal, says on stderr what it is before it starts the MCP server, which otherwise sits waiting on stdin and looks hung: where to add it to an agent, and thatinspectorcheck .is what to run by hand. An MCP client starts it with a pipe, not a terminal, so clients see nothing new.