The index of Keenmate's web components and the shared tooling that keeps
them in the same shape: @keenmate/web-components-devkit.
- Catalogue: every component, library, showcase and test
harness, with CSS prefixes, dev ports and links. It's
generated from
components.json. - Devkit: one Vite preset, one examples kit, one Playwright
preset and the
kmwcCLI, instead of a diverged copy of each in every repo.
The rules — component structure, naming, CSS, theming, README, Custom Elements Manifest, publishing — live in the BlissFramework rulebook (
BlissFramework/guidelines/web-components/, plus the JS coding guidelines inBlissFramework/web/docs/coding-guidelines-javascript/). The devkit generates what the rulebook asks for (kmwc new) and checks it (kmwc doctorruns the rulebook's*.checks.shsuites). Runtime code shared between components (BlissElement, the input table, converters, logging, positioning) lives in@keenmate/web-components-core. Nothing here ships inside a component's bundle.
- Examples kit —
appendLogLine()writes event-log lines — The kit styled log panels and.log-linetimelines but gave pages no way to write a line, so about 20 pages across web-daterangepicker, web-multiselect, web-components-core and web-dropzone each built entries from their owninnerHTMLtemplate with hard-coded colours.appendLogLine(panel, { name, detail, variant })from/examplesappends one timeline line (time, name, then the detail, with anything that isn't text pretty-printed as JSON) and scrolls it into view.varianttakes the contract's role names (info|success|warning|danger, rulebook D-NC-23) and maps onto the existing.log-line--info|ok|warn|errmodifiers. An unknown variant throws, so a stray'error'is caught at once. Line breaks are now kept only on log lines and text-only elements, so an older entry built from an indented template no longer shows that template's whitespace as gaps. - Examples kit — the theming control panel and
mapToEscapedHtml— Three components' theming pages carried the same ~30-line<style>block for their settings card..controls-header,.control-section(with itsh3) and.section-descare now kit CSS under the names those pages already use.mapToEscapedHtml(), the kit's own HTML escaper, is now exported from/examples. Pages had written their ownesc/escapeHtml14 times across four repos. The kit's index page and footer now use the shared copy instead of private duplicates. - Examples kit — note variants are the contract's role names (breaking) —
.note.erroris now.note.danger, with no alias, so notes use the same four words as the messagevariantand the--base-*status roles:.info,.success,.warning,.danger. Only web-treeview's tree-editor page used the old name. - Doctor — hand-copied demo snippets, and the published variable-manifest schema — Doctor now warns when a
<pre>in a<details>or.code-blockcopies a live demo on the same page. It catches a live element's id, a reference to it, a matching tag, or repeated lines; the fix is<kmwc-demo>, anddata-kmwc-snippetmarks a reference snippet on purpose. The first run found 23 copies in web-dropzone, several of which had already drifted from their demo. Doctor also warns whencomponent-variables.manifest.jsondoesn't point at the published schema,https://schemas.keenmate.dev/component-variables/v1.schema.json. The old raw-GitHub URL never resolved.kmwc newwrites the new one. A--base-*entry marked"readBy"(read by a core module rather than the component's own CSS) is no longer reported as dead. - CLI —
kmwc portschecks every registry repo at once — Doctor sees one repo and the siblings inside its own port block, so a repo that was never migrated only showed up when the repo it collided with ran doctor.kmwc ports, run in the devkit checkout, reads everycomponents.jsonentry checked out next to it: kmwc.ports, script--portflags, Vite and Playwright configs, and Makefiles. It lists every port outside the entry's block, names whose block it lands in, and exits 1 if there are any. Its first run found web-grid's docs on web-commandpalette's 12400 and web-component-framework-test on web-treeview's 12300. - Composer — a comment inside
<kmwc-demo>no longer stops its expansion — The page composer split pages at HTML comments before matching demos, so a<!-- … -->inside a demo left the raw<kmwc-demo>on the page, with no code block and no error. It now scans once, left to right. A comment inside a demo stays in both its live markup and its shown code.
- Doctor — monorepos:
kmwc.packageslists what the repo publishes — Doctor used to assume the repo root was the package. In web-dropzone (npm workspaces: a private root that holds the example site, the component inpackages/web-dropzone, a headless library inpackages/web-dropzone-core) that produced 113 errors that said little.npm packon the private root "leaked" 101 files npm never publishes, six rulebook suites found no component at the root, and the overlay suite scannednode_modules. Meanwhile the component's real findings never showed. Now"packages": [{ "path", "kind": "component" | "library" }]inpackage.json#kmwclists the published packages. Doctor packs each one and runs the rulebook suites for its kind inside it (reported asrulebook:<suite>@<path>). The publish-command and example-pages suites stay repo-wide, and the loader check runs per component package. Doctor warns when npm workspaces aren't all listed, errors on a listed path without apackage.json, and never packs a private workspace root. A repo withoutpackagesis still one package at its root. - Examples kit — every example page ends with the footer — Only the index page had the "Created by Keenmate s.r.o. | GitHub | web-components-devkit vX" footer; the other example pages just stopped. The kit now appends the same line to every page except the index (
mountPageFooter(),.kmwc-page-footer), at the end of the layout's container. The index footer and the page footer are rendered by one function,renderFooterLineHtml(), so they can't drift apart. The index keeps its own extra links (README, API docs); the page footer carries Keenmate, the repo's GitHub link and the devkit version.
| Component | Package | Prefix | Dev port | Links | Devkit | About |
|---|---|---|---|---|---|---|
| web-multiselect | @keenmate/web-multiselect |
ms |
12100 | repo · examples · docs · web-multiselect-showcase · web-multiselect-docs |
✅ | Lightweight multiselect web component with typeahead search, rich content support, and excellent keyboard… |
| web-daterangepicker | @keenmate/web-daterangepicker |
drp |
12200 | repo · docs · web-daterangepicker-showcase |
✅ | Lightweight date picker web component with excellent keyboard navigation and range selection |
| web-treeview | @keenmate/web-treeview |
wtv |
12300 | repo · web-treeview-showcase |
✅ | Lightweight treeview web component with hierarchical data display, search, expand/collapse, drag-drop, and… |
| web-commandpalette | @keenmate/web-commandpalette |
cp |
12400 | repo · examples | ✅ | Framework-agnostic command-palette web component — modes, multi-step commands, scoped + global search,… |
| web-dropzone | @keenmate/web-dropzone |
dz |
12500 | repo | ✅ | Lightweight file dropzone web component with drag-drop, previews, validation, and multiple display modes. |
| web-grid | @keenmate/web-grid |
wg |
12600 | repo · docs · showcase | — | Framework-agnostic data grid web component with sorting, filtering, inline editing, and keyboard navigation |
| web-editor | @keenmate/web-editor |
km |
12700 | examples | ✅ | Keenmate Web Editor — a framework-agnostic WYSIWYG / Markdown editor built on ProseMirror. |
| web-switch | @keenmate/web-switch |
— | 12800 | repo | — | Framework-agnostic web component wrapper for @keenmate/svelte-switch. Works with React, Vue, Angular, or… |
| web-timepicker | — | — | 12900 | — | — | Vanilla-JS prototypes (spiral / slider time pickers); no package yet. |
| web-wysiwyg | — | — | 13000 | — | — | ProseMirror exploration; superseded by web-editor. |
| Library | Package | Dev port | About |
|---|---|---|---|
| web-components-core | @keenmate/web-components-core |
15000 | Runtime foundation: BlissElement, reactive input table, converters, logging, positioning, CEM preset. |
| web-components | @keenmate/web-components-devkit |
15100 | This repo — dev/demo/build tooling (Vite preset, examples kit, kmwc CLI). |
| pure-css | @keenmate/pure-css |
15200 | CSS framework mirroring the base-css-variables token contract (demo site + e2e on the dev port). |
| base-css-variables | @keenmate/base-css-variables |
15300 | The shared --base-* token contract (base-variables.css) + base-contract-check; examples site + e2e on the dev port. |
| Repo | Kind | For | Dev port | About |
|---|---|---|---|---|
| web-multiselect-showcase | showcase | web-multiselect | 16000 | |
| web-daterangepicker-showcase | showcase | web-daterangepicker | 16100 | |
| web-treeview-showcase | showcase | web-treeview | 16200 | |
| web-grid-showcase | showcase | web-grid | 16300 | https://web-grid.keenmate.com |
| web-multiselect-docs | docs | web-multiselect | 16400 | Content-only docs (keendocs). |
| web-component-framework-test | test | — | 17000 | Smoke + contrast tests inside CSS host frameworks (plain / Bootstrap / …). |
| web-multiselect-test | test | web-multiselect | 17100 | npm-install vs direct-file consumption checks before publishing. |
| web-multiselect-positioning-demo | test | web-multiselect | 17200 | Single-page positioning repro. |
| web-playground | test | web-components | 17300 | Devkit e2e test bed — a component scaffolded with kmwc new, linked to this repo. |
Ports — a block of 100 per repo: dev server = Dev port, preview = +1, examples image = +10. Each kind numbers its blocks by 100 inside its own range (component 12100–14900 · library 15000–15900 · showcase & docs 16000–16900 · test 17000–17900), so any number of repos run side by side without collisions and a new component lands next to the others. kmwc doctor checks a repo against its block; kmwc new takes the next free one.
To add or change a row, edit components.json and run
npm run readme. Descriptions and the Devkit ✅ are read from the
sibling checkouts next to this repo. npm run readme:check fails when the
table is stale. Prefixes mirror the reserved-prefix table in the rulebook's
base-variables.md, which stays the authority.
Before the devkit, every component repo carried its own copy of
examples-shared.css (six copies, six different versions),
examples-chapter-nav.js (three versions), examples-copy-link.js,
examples-controls-store.js, plausible-snippet.html,
check-variable-manifest.mjs, a vite.config.ts, a Dockerfile, an
nginx.conf and a Makefile. A fix made in one repo never reached the others.
web-treeview forked the whole 980-line stylesheet just to change purple to
green. Two components shared a dev port and three shared an image port.
The devkit owns all of that. A component states what's specific to it once,
in package.json#kmwc, and gets the rest.
| Piece | Import / command | Replaces |
|---|---|---|
| Vite preset | @keenmate/web-components-devkit/vite → defineComponentConfig() |
per-repo vite.config.ts, vite.examples.config.ts |
| Examples kit (auto-injected) | shared styles, chapter nav, copy-link buttons, theme switch, version badge, page footer, analytics | examples-shared.css, examples-chapter-nav.js, examples-copy-link.js, plausible-snippet.html, Dockerfile sed |
| Fragment pages | <meta name="kmwc:title"> pages + <kmwc-include> partials + <kmwc-demo> (live markup = snippet) |
the head / header / container boilerplate copied into every page, hand-copied .code-block snippets |
| Examples helpers | @keenmate/web-components-devkit/examples → attachControlsPersistence, renderExamplesIndex, mountExamplesKit |
examples-controls-store.js, 150 lines of inline index-page CSS |
| Playwright preset | @keenmate/web-components-devkit/playwright → definePlaywrightConfig() |
per-repo playwright.config.ts |
| Generated files | kmwc sync |
hand-edited Dockerfile, nginx.conf, Makefile, .dockerignore |
| Standard check | kmwc doctor (devkit checks + the rulebook's *.checks.sh) |
— |
| Manifest check | kmwc check-vars |
3 diverged check-variable-manifest.mjs |
| Release gate | kmwc check-release (used by make publish / publish-rc) |
— |
| Scaffolder | kmwc new web-<name> --prefix <p> |
copy-pasting the last repo |
npm i -D @keenmate/web-components-devkit vite@^7
To develop against a local checkout of this repo, keep the semver range in
package.json (Docker builds install from the registry) and link it:
cd ../web-components && npm link
cd ../web-<component> && npm link @keenmate/web-components-devkit
Peer dependencies (vite, @playwright/test) are always resolved from the
component's node_modules, so a linked devkit doesn't load a second copy.
Every field is optional. Defaults are derived from the package.
A library (registry kind: "library", e.g. web-components-core) builds its
package itself (tsc) or ships none. "library": false (or { "shouldBuild": false, "entry": … } to keep the pages' entry script) makes plain vite build
refuse to run — it is the devkit's library mode and would empty dist/ — while
vite build --mode examples still builds the site, and kmwc sync no longer
proposes a build: vite build script. "cssVariables": false turns off the
manifest check (no check:vars script, kmwc check-vars skips), for variables
the checker can't see, such as names built by string interpolation. Doctor runs
only the rulebook suites that apply to a library (see below).
A monorepo (npm workspaces, e.g. web-dropzone: a private root with the example
site, packages/web-dropzone + packages/web-dropzone-core) lists its published
packages in kmwc.packages, each with its kind. The root keeps the devkit's
example site, generated files, ports and registry checks; doctor packs each
package (npm pack), runs the rulebook suites for its kind in the package
(reported as rulebook:<suite>@<path>), and checks the loader class in each
component package. publish-command and example-pages stay repo-wide, at the root.
Without packages the root is the one package — doctor warns when npm workspaces
exist but aren't listed, and doesn't pack a private workspace root.
// vite.config.ts
import { defineComponentConfig } from '@keenmate/web-components-devkit/vite';
export default defineComponentConfig();
// with repo-specific bits
export default defineComponentConfig({
plugins: [shadowCssPlugin()],
overrides: { optimizeDeps: { entries: ['index.html', 'examples-*.html'] } },
getOverridesCallback: (env, config) => ({ /* env-dependent bits */ }),
});| Command | Result |
|---|---|
vite |
dev server on kmwc.ports.dev (strict, never hops ports) |
vite build |
library: dist/<fileName>.js, .umd.js, style.css, rolled-up .d.ts |
vite build --mode examples |
static examples site → dist-examples/ (every root *.html is a page) |
vite preview --mode examples |
serves the built site on the preview port (ports.dev + 1) |
The preset defines __VERSION__, __PACKAGE_NAME__, __AUTHOR__,
__LICENSE__, __REPOSITORY__ and __HOMEPAGE__. For a hand-assembled config
(monorepos, showcases), use the plugin on its own: createExamplesPlugin().
The plugin injects the kit into every root-level *.html page. Pages under
test/ (Playwright fixtures) are left alone, and any page can opt out with
<html data-kmwc="off">.
A page without <html> is a fragment: it carries only its own content,
and the plugin wraps it in the devkit layout (head, entry script, header with
back link / title / subtitle, container). The whole file:
<meta name="kmwc:title" content="Tree of Options">
<meta name="kmwc:subtitle" content="Hierarchical options via a materialized path-member…">
<link rel="stylesheet" href="examples-local.css"> <!-- moved into <head> -->
<kmwc-include src="partials/no-collapse-note.html" title="No collapse"></kmwc-include>
<div class="card"> <!-- each h2 = a chapter -->
<h2>TR01 · Basic tree</h2>
<kmwc-demo>
<web-multiselect id="tree" path-member="path"></web-multiselect>
<script type="module" data-kmwc-show>
document.getElementById('tree').options = [{ value: 'fruit', label: 'Fruit', path: '1' }];
</script>
</kmwc-demo>
</div>
<script type="module">
import { attachControlsPersistence } from '@keenmate/web-components-devkit/examples';
attachControlsPersistence('tree');
</script>| Construct | Does |
|---|---|
<meta name="kmwc:title"> (required), kmwc:subtitle, kmwc:body-class |
header + <title>, body classes |
<meta name="kmwc:layout" content="bare"> |
head only, no header/container (the index page) |
<meta name="kmwc:entry" content="none"> |
skip the component entry script (default /<kmwc.library.entry>, loaded in <head> so the element is defined before page scripts run) |
<link> / <style> anywhere in a fragment |
moved into <head> |
<kmwc-include src="partials/x.html" some-param="v">inner</kmwc-include> |
the partial with {{someParam}} and {{content}} filled in; nests; cycles are an error; unknown {{…}} stay as written |
<kmwc-demo>markup</kmwc-demo> |
the markup live and as a code block of exactly that markup (copy button included) — a snippet can't drift from its demo. <script data-kmwc-show> inside runs and is shown too. Attributes: preview-class, markup-hidden, code-hidden |
data-kmwc-snippet on a hand-written <details> / .code-block (or its <pre>) |
a reference snippet that mirrors no demo on purpose (an API listing that happens to name a live id) — doctor's hand-copied-snippet check skips it |
Tags inside HTML comments are never expanded; a comment inside a
<kmwc-demo> stays in its live markup and shown code. A <kmwc-demo> without
its own </kmwc-demo> (or nested in another) fails the page. Partials live in the repo's
partials/ folder (a change reloads the dev server page); shared demo data
belongs in examples-data/*.js modules imported by pages. Full documents
(<html>…) pass through untouched, so pages migrate one at a time — kmwc doctor lists the ones still repeating the head/header boilerplate, and warns
about every hand-written code block that copies a live demo on its page (wrap
the demo in <kmwc-demo> instead).
- Styles live in
@layer kmwc, so any unlayered rule beats them regardless of order (and the layered reset can never clobber a component). Tokens (--kmwc-*) read through the rulebook's--base-*taxonomy withlight-dark()fallbacks, so a theme-designer theme restyles the demo page together with the component; colour literals live only intokens.css. Re-theme a site withkmwc.theme.accentColor. The page vocabulary is unchanged from the old shared CSS —.card,.example-section,.controls,.demo-area,.demo-card,.code-block,.note(.info|.success|.warning|.danger),.grid/.card-grid,.reference-table(+.table-*modifiers),.log-panel,.pill,.status-dot,.control-group, the theming control panel (.controls-header,.control-section,.section-desc), buttons (.btn-secondary,.btn-outline,.btn-danger),<kbd>keys and<mark>search matches (the spotlight's look),.push-end(to the end of a flex row) — while everything the kit generates is prefixed BEM (kmwc-chapter-nav__item--active,kmwc-copy-link--copied, …). - Form controls look the same on every page without a class: text fields,
selects and list boxes (
multiple/size), textareas, checkboxes, radios, switches (<input type="checkbox" role="switch">), ranges, colour and file inputs, fieldsets — modelled on pure-admin's forms and read through the--base-input-*/--base-checkbox-*/--base-icon-checkcontract. Inputs, selects and buttons share--kmwc-control-height, so a row of them lines up in any container, not only.controls. The rules are zero-specificity (:where());kmwc-nativeon an element or an ancestor keeps the browser's own controls (a light-DOM component that draws its own inputs). A number field is 7rem wide;.w-xs/.w-sm/.w-md/.w-lg(4 / 6 / 9 / 12rem) size a short field without an inline style. The playground'sexamples-forms.htmlshows them all. - Tables: any
<table>gets a header fill, row lines and a hover tint;.reference-tableis the full-width, fully bordered API table. Modifiers combine —.table-full,.table-bordered,.table-striped,.table-compact(tighter cells; the controls in it shrink via--kmwc-control-height) — plustr.is-highlightand a.numcell (right-aligned, tabular figures). On a phone a wide table scrolls inside its card. See the playground'sexamples-tables.html. - Widgets:
.log-panel(light, following the theme;--darkfor a console stream; the legacy.event-log/.event-entrymarkup renders the same). Lines keep their indentation; a.log-linetakes one tone —--ok,--info,--warn,--err,--muted, the same five as.pill/.status-dot— and--actionmarks a "you clicked X" line. Timeline cells (.log-line__seq,__time,__source,__name) turn a line into aligned columns. Write event-log lines withappendLogLine(panel, { name, detail, variant })(/examples): one timeline line (time, name, detail — anything that isn't text is pretty-printed JSON),variant=info|success|warning|danger. Don't build entries withinnerHTMLtemplates. A<pre class="log-panel">is a text-fed log;data-emptyshows while a panel is empty. Also.mono,.check-list(options one per line),td.is-highlight,.display-slidersand.css-readout. The playground'sexamples-widgets.htmlshows them all. - Theme switch (top right) cycles light → dark → auto and sets
data-themeon<html>— the same signal components react to via:host-context([data-theme]), so every example page is a dark-mode smoke test. The saved choice is applied before first paint. - Chapter nav and copy-link share one section/slug algorithm, so the jump-list hashes and copied links always match. Floating UI is bundled (no CDN). At the bottom of a short page, the last chapter gets highlighted.
- Spotlight (Ctrl/⌘+K) searches every chapter of every example page: a
chapter matches when its section code or heading text contains the query
(
th7findsTH07; uncodedSummary/ reference sections are found by title), results are grouped by page with the current page first, ↑↓ / PgUp / PgDn / Home / End pick, Enter jumps, Ctrl/⌘+Enter opens a new tab. The Vite plugin builds the index from every page's<h2>s (includes resolved) asvirtual:kmwc-chapters; the current page's chapters are read live, so script-rendered sections count too. Off withkmwc.examples.isSpotlightEnabled: false(a component that owns Ctrl+K itself; a page handler callingpreventDefault()also keeps the key). It mirrors web-commandpalette — markup, matching, keyboard map, open/close, CSS — without depending on it; see Developing the devkit. - Short chapter links:
examples-theming.html#th07(or#th7, or the slug of an uncoded heading,#summary) scrolls to that chapter on arrival — the codes from the rulebook'sexample-pages.mdwork as URLs. - Version badge: any
[data-kmwc-version](or legacy#version-badge) showsv1.2.3, orv1.2.3 (built …)in a built site. - Landing page:
renderExamplesIndex({ pages })renders the header, cards, footer and cross-page nav from the registry pluskmwcconfig. - Page footer: every other example page ends with the same line as the
index footer — Keenmate, the repo's GitHub link, the devkit version — inside
the layout's container (
.kmwc-page-footer). - Late content: the kit mounts one task after DOM-ready. Pages that render
sections after a fetch call
mountExamplesKit(). - Analytics: the Plausible snippet goes into built pages only, and only
when
kmwc.analytics.plausibleSiteIdis set.
import { definePlaywrightConfig, devices } from '@keenmate/web-components-devkit/playwright';
export default definePlaywrightConfig({
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] }, testIgnore: ['**/mobile.spec.ts'] },
{ name: 'mobile', use: { ...devices['Pixel 7'] }, testMatch: ['**/mobile.spec.ts'] },
],
});baseURL and the npm run dev web server come from kmwc.ports.dev.
kmwc sync [--check] regenerate Dockerfile, nginx.conf, Makefile, .dockerignore, the
CLAUDE.md devkit block, .gitignore entries, missing standard scripts
kmwc doctor [--json] devkit checks + package contents + the rulebook's *.checks.sh suites
(exit 1 on errors). --verbose lists passes, --no-rulebook skips the
suites, --no-pack skips the package-contents check
kmwc check-vars [--strict] component-variables.manifest.json ⇔ CSS
kmwc check-pages [--url u] every example page in Chromium at 1440 / 390 / 320px: page and console
errors, failed requests, never-defined elements, a page wider than the
viewport, page CSS that only matches inside a shadow root. Starts the dev server unless --url (e.g. the built site);
--widths 1440,390,320
kmwc check-release --tag t latest ⇔ release version, rc ⇔ -rcN, version not yet on npm
kmwc new web-<name> scaffold a component (--prefix, --title, --tag, --dir, --port, --minimal, --register)
--adopt: scaffold an ALREADY registered component (its port/prefix) to migrate into
kmwc readme [--check] regenerate this README's catalogue (devkit repo only)
kmwc ports every registry repo checked out next to the devkit keeps to its port
block: kmwc.ports, script flags, vite / playwright configs, Makefile
(devkit repo only; doctor checks one repo)
kmwc <command> --help that command's usage — never runs the command
An unknown option is an error (exit 2), never ignored — a typo can't run a command with its defaults.
Generated files start with a GENERATED by kmwc sync header. Repo-specific
make targets go in Makefile.local, which the generated Makefile includes at
the end. Per-developer overrides (DOCKER_RUNNER, IMAGE_PORT) go in
.makefile.env. make publish / make publish-rc run kmwc check-release
and the e2e suite before building and publishing.
kmwc doctor and the rulebook. Doctor runs every suite in the rulebook
(component-structure, naming-conventions, css-structure, theme-container,
base-variables, color-scheme, readme-structure, custom-elements-manifest,
responsive-overlay, publish-command, example-pages) with bash, passing kmwc.prefix, and
reports each FAIL as an error pointing at the matching .checks.md entry.
On Windows that is Git for Windows' bash (a bare bash may be WSL's, which can't
read C:/ paths); KMWC_BASH overrides it. A suite that exits with anything but 0,
or 1 with FAIL lines, never ran — a warning, never a pass.
A library runs only naming-conventions, readme-structure, publish-command
and example-pages; the others check what a custom element ships (its CSS,
manifest, overlay).
When the rulebook isn't checked out (or there's no bash) the suites are
skipped with a warning — the rulebook stays the single source of truth; the
devkit never copies its rules. A failure whose check id heads a section of
VALIDATION-NOTES.md (## C-CSS-1 — Lean strategy, ## C-CST-4 / C-CST-10 — …)
is an accepted deviation — an architectural decision recorded per the rulebook —
and counts as a pass (--verbose lists them).
What else doctor checks. Beyond the presets, legacy files, fragment pages and generated files:
- Library outputs — every
./dist/…target in package.json (main,module,types,style,exports) is a file the library build emits. Before the switch to the preset it also compares the oldlib.namewithkmwc.library.globalName— a renamed UMD global or CSS file breaks consumers silently. - Ports — no script pins a
--portof its own, the repo's configs, Makefile and docs stay inside its registry block, and no sibling checkout (a registry entry at../<id>) listens on a port inside it. A doc that names other repos' ports on purpose (a dated plan, a cross-repo review) carries<!-- kmwc-other-ports: why -->and isn't scanned. - Analytics — no other repo uses the same Plausible site id.
- Page CSS — a page
<style>orexamples-local.cssrule on a kit-owned selector (.kbd,kbd,mark,.pill,.card…) and single-declaration inline styles a kit class covers (widthon a field →.w-*,margin-left: auto→.push-end,margin-top: 1rem→.mt-1) are warnings. So is a hard-coded colour (#hex,rgb(),white…) in page CSS or astyle="…": it stays put when the theme switch flips the kit to dark.var()fallbacks andlight-dark()pairs follow the theme. Fixed colours on purpose (a theme gallery, a mockup, a test fixture) go in a<style data-kmwc-fixed-colors>block, or one rule gets a/* kmwc-fixed-colors: why */comment right before it. An element whosestyle="…"sets them on purpose carries the same attribute — markup a render callback draws inside a shadow root can't be reached by page CSS at all. - Hand-copied demo snippets — a
<pre>in a<details>or.code-blockthat copies the page's live content is a warning (one per page, with the lines): it carries a live element'sid, reaches one withgetElementById('x')/querySelector('#x'), repeats a live tag with ≥ 2 of its attribute values, or repeats ≥ 3 non-trivial lines of the page's markup / scripts (2 when they're ≥ 30 % of the snippet). Generic reference code (a data shape,el.open(), an API listing) doesn't match. The fix is<kmwc-demo>; a reference snippet that trips it on purpose carriesdata-kmwc-snippet. - Loader class — the component CSS styles the class the installed core gives
its default loader (
<prefix>__loaderfrom core 6d6b768 on,<prefix>-loaderbefore). Renaming the selector on one side of the core bump leaves the loader unstyled, and a VALIDATION-NOTES## C-NC-8would hide the old class from the rulebook. On a core that still draws<prefix>-loader, CSS that styles both classes has an element of its own called<prefix>__loader: a warning, because core's loader takes that name (and those rules) with the bump. - Variable manifest — the same verdict as
kmwc check-vars, which otherwise only runs insidenpm run build:--base-*reads and the manifest agree, no dead--<prefix>-*entries (errors), no undocumented ones (warning). A--base-*entry marked"readBy"(read by a core module, not the component's CSS) is not dead. The manifest's$schemamust be the published schema,https://schemas.keenmate.dev/component-variables/v1.schema.json(warning).
kmwc check-pages is the browser half: it loads every example page and reports
what doctor can't see in the source — a script error, a 404, a fixed width that
pushes a phone sideways, and page CSS that can never apply: a rule (in the page's
<style> or examples-local.css) whose element exists only inside a component's
shadow root, such as the rows a renderNodeCallback draws. Page CSS stops at the
shadow boundary — style those through a CSS variable, ::part or the component's
styles hook (getCustomStylesCallback). An element a demo leaves undefined on
purpose (registered on a button press) carries data-kmwc-undefined.
Package contents. Doctor runs npm pack --dry-run and fails on anything
dev-only that would reach a consumer's node_modules: CLAUDE.md at any
depth and .claude/ (AI-assistant guidance, which a directory entry such as
src/css in files sweeps in), test/ / e2e/ / *.spec.* / *.test.*,
example pages and partials/, build/test output, and local make config. Keep
"!**/CLAUDE.md" and "!**/.claude" at the end of files; the scaffold adds them.
npx kmwc new web-colorpicker --prefix cpk --title "Web Color Picker" --register
kmwc new runs the rulebook intake: the npm name must be free and the CSS
prefix must be 2–4 lowercase letters not reserved in base-variables.md or
components.json (it's derived only when unambiguous — hyphenated words, or a
short single word — otherwise pass --prefix). The result passes every
rulebook suite on its first commit:
src/web-component.ts(ColorpickerElement, Element layer,ATTRIBUTE_TABLE,changeevent withonChangetwin) ·src/colorpicker.ts(WebColorpicker, logic class) ·src/types.ts·src/logger.ts·src/index.tssrc/css/— the canonical eight files,@layer variables, component, overrides,--cpk-*⇢--base-*⇢light-dark()chains, Strategy-B dark mode, FOUC rule- Custom Elements Manifest config + package.json
customElements/web-types,component-variables.manifest.json(exported as/manifest) - README in the canonical shape,
docs/{usage,theming,examples,accessibility}.md, CHANGELOG (## [0.1.0] - <date>), LICENSE,.claude/commands/publish.md - a
filesallowlist (dist, docs, manifests, editor-integration JSON) ending in!**/CLAUDE.mdand!**/.claude, so AI-assistant guidance never ships - examples (index, basic, theming), fixtures + smoke and dark-mode e2e specs,
and the
kmwc syncfiles
By default the component is a small working sample (a toggle with label /
active inputs and a change event) to rename or replace. Add --minimal
for an empty skeleton instead: the same files and structure, but an empty
ATTRIBUTE_TABLE, no events, a root that renders its slotted content, and
stub feature CSS — it passes the same rulebook suites.
Reserve the new prefix in the rulebook's base-variables.md afterwards — the
command prints the reminder.
kmwc doctor lists everything that's left, so run it after each step.
Repo in a different shape (a one-package monorepo, a non-standard layout)?
Don't teach the devkit the old shape: back the repo up, run kmwc new web-<name> --adopt --dir <scratch> (same files, with the registered port and prefix, no
registry edits), and move the component's own code into that layout with
git mv. web-editor was flattened this way.
npm i -D @keenmate/web-components-devkit vite@^7(plus@playwright/testif the repo has e2e tests).- Add
package.json#kmwc. Take the port block (ports.dev= block,ports.image= block + 10) and prefix fromcomponents.json, the Plausible id from the oldplausible-snippet.html(analytics.plausibleSiteId), the image name and runner from the old Makefile, the canonical origin from the oldexamples-copy-link.js, and internals from the oldcheck-variable-manifest.mjs(cssVariables.internalVariables). - Run
npx kmwc doctorbefore replacingvite.config.ts: it compares the oldlib.namewithkmwc.library.globalNameand package.json'sdist/targets with the preset's outputs — setlibrary.globalName/fileName/cssFileNameto the published names so nothing gets renamed. Then replacevite.config.ts(and anyvite.examples.config.ts) withdefineComponentConfig(). Move genuinely specific bits topluginsandoverrides. Do the same forplaywright.config.ts. - Pages: delete the
<link href="examples-shared.css">and theexamples-chapter-nav.js/examples-copy-link.jsscript tags. Changeimport { persistControls } from './examples-controls-store.js'toimport { attachControlsPersistence } from '@keenmate/web-components-devkit/examples'(same storage keys — saved demo state survives). Move component-only CSS intoexamples-local.css. - Swap
index.htmlforrenderExamplesIndex({ pages }), keeping the page registry. The inline<style>and header markup go away. - Delete the legacy files:
examples-chapter-nav.js,examples-copy-link.js,examples-controls-store.js,examples-shared.css,plausible-snippet.html,scripts/check-variable-manifest.mjs. - Set
"check:vars": "kmwc check-vars"and run it. npx kmwc sync, then review the diff. Repo-specific Makefile targets move toMakefile.local.- Check it runs:
npm run dev,npm run build(comparedist/with the old build),npm run build:examples,npm run preview:examples, the e2e tests,npx kmwc check-pages(and--urlon the preview port for the built site), and finallynpx kmwc doctor(devkit + rulebook — rulebook failures are real findings to fix, or architectural decisions to record inVALIDATION-NOTES.md).
npm install
npm test # unit tests (config, registry, templates, sync, checks, intake, plugin, kit)
npm run test:e2e # playground: the kit + scaffold in a real browser, dev server AND built site
npm run test:e2e:coverage # the same specs on the dev server + line coverage of the kit's browser code
npm run readme # regenerate the catalogue
npm run check:spotlight-mirror # what changed in web-commandpalette since the spotlight was aligned
make dev # the playground's dev server
The spotlight mirrors web-commandpalette. src/examples/spotlight.js,
chapter-search.js and css/spotlight.css keep wcp's structure block for
block (devkit naming), each marked mirrors <file> <function>;
scripts/spotlight-mirror.json lists every
pair and the wcp / core commit they're aligned to. A bug fixed in one gets
ported to the other: run npm run check:spotlight-mirror (add -- --diff
for the patches), port what applies, bump alignedCommit.
playground/ is a real component scaffolded with kmwc new and
linked to this checkout. It's the reference for "what a component looks like",
the e2e target for every kit feature, and npx kmwc doctor inside it must
report every rulebook suite green. When you change the kit or a template, add
a spec there.
Releases are prepared with the /publish Claude command
(.claude/commands/publish.md, the rulebook's canonical /publish with devkit
steps: unit + e2e + playground doctor + catalogue check, entry-point smoke
test, npm pack review). It never publishes or pushes. That's yours.