@keenmate/web-components-devkit 1.0.0-rc06 → 1.0.0-rc08

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,98 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.0-rc08] - 2026-11-04 [PUBLISHED]
4
+
5
+ ### Added
6
+
7
+ - **Doctor: hard-coded colours in page CSS** (warning). A colour literal (`#hex`, `rgb()` / `hsl()` /
8
+ `oklch()`, a named colour) on a colour property in a page `<style>`, a `style="…"` or
9
+ `examples-local.css` stays the same when the theme switch flips the kit to dark. web-treeview's
10
+ migration kept 10 pages' own `<style>` blocks, and 8 of them had search bars, status bars, badges and
11
+ panels that didn't follow dark mode. rc07 only flagged rules that restyle a kit class, so nothing
12
+ reported them. `var()` fallbacks and `light-dark()` pairs pass. A block that shows fixed colours on
13
+ purpose (a theme gallery, an iOS mockup) opts out with `<style data-kmwc-fixed-colors>`, or one
14
+ rule with a `/* kmwc-fixed-colors: why */` comment before it (the playground's e2e fixture red).
15
+ - **Doctor: the loader class matches the installed core** (error). Core 6d6b768 renamed the default
16
+ loader's class from `<prefix>-loader` to `<prefix>__loader`, with no release that emits both. A
17
+ component that renames its selector before the core bump, or forgets to after it, ships an unstyled
18
+ loader. No rulebook check sees that, and a VALIDATION-NOTES `## C-NC-8` hides a leftover `.x-loader`
19
+ (drp has one for other classes). Doctor reads which class the installed core's `render-loader.js`
20
+ draws and fails on a `src/**/*.css` selector for the other one.
21
+
22
+ ### Fixed
23
+
24
+ - **Inline `<code>` in dark mode** was the accent colour on a neutral `#2b2b2b` chip: about 3:1, and
25
+ web-treeview's `#059669` green read dim in prose and tables. The chip is now a step up from the main
26
+ surface (`oklch(from --kmwc-main-bg calc(l + 0.07) …)`, slate on slate), and the text is a new
27
+ `--kmwc-inline-code-color`: the accent hue at a lightness of at least 0.8 in dark, the accent as-is in
28
+ light. An e2e spec checks 4.5:1 for the default, green, navy and amber accents.
29
+
30
+ ## [1.0.0-rc07] - 2026-11-03 [PUBLISHED]
31
+
32
+ Found migrating web-treeview — each item is something doctor missed or got wrong there.
33
+
34
+ ### Added
35
+
36
+ - **`kmwc check-pages`** — loads every example page in Chromium at 1440 / 390 / 320px and
37
+ reports page and console errors, failed requests, custom elements that never get defined and a
38
+ page wider than the viewport (with the widest elements named). treeview's iOS-Files mockup
39
+ (`width: 420px`) and theme gallery (`minmax(500px, 1fr)`) pushed a phone sideways and only a
40
+ browser could tell. Starts the repo's dev server, or `--url` for one already running (the built
41
+ site on the preview port); `--widths`. It looks inside shadow roots and at text (a long word
42
+ sticking out of a box that fits), and skips what can't widen the page (code blocks and other
43
+ scrolling boxes, hidden or fixed layers such as a closed chapter-nav panel). `kmwc sync` adds a
44
+ `check:pages` script and a `make check-pages` target.
45
+ - **Doctor: accepted deviations** — a rulebook failure whose id heads a section of
46
+ `VALIDATION-NOTES.md` (`## C-CST-4 / C-CST-10 — …`) is an architectural decision recorded per the
47
+ rulebook and counts as a pass (`--verbose` lists them). treeview's documented C-CST-4 / C-CSS-1 /
48
+ C-NC-6 showed as errors.
49
+ - **Doctor: library outputs** — every `./dist/…` target in package.json (`main`, `module`, `types`,
50
+ `style`, `exports`) must be a file the library build emits, and before the switch the old
51
+ `lib.name` must equal `kmwc.library.globalName`. The preset's defaults would have renamed
52
+ treeview's UMD global (`WebTreeView` → `WebTreeview`) and its CSS (`web-treeview.css` →
53
+ `style.css`) without a word.
54
+ - **Doctor: ports** — a script that pins its own `--port`, a config / Makefile / doc port outside the
55
+ repo's registry block, and a sibling checkout listening inside it are reported. treeview ran on
56
+ 21111 (docs too), and web-daterangepicker hardcoded treeview's 12300; the check also finds
57
+ web-multiselect and web-dropzone on 12200 and web-component-framework-test on 12300.
58
+ - **Doctor: analytics** — a Plausible site id another repo also uses is an error (treeview's
59
+ snippet borrowed web-multiselect's, so its traffic was counted as multiselect's).
60
+ - **Doctor: page CSS the kit covers** — page `<style>` / `examples-local.css` rules on kit-owned
61
+ selectors (`.kbd`, `kbd`, `mark`, `.pill` …) and one-declaration inline styles a kit class covers
62
+ (`width` on a field → `.w-*`, `margin-left: auto` → `.push-end`, `margin-top: 1rem` → `.mt-1`)
63
+ are warnings.
64
+
65
+ ### Changed
66
+
67
+ - **check-vars: input variables** — in the default mode a component variable is present when it is
68
+ declared **or** read. A knob that is read but never declared is an input, unset by design:
69
+ treeview forwards `--wtv-context-menu-*` to core's private `--_km-menu-*` so a page-wide
70
+ `--km-menu-*` still applies, and declaring them would break that chain — check-vars called all 26
71
+ dead. A read-with-fallback that isn't in the manifest is now reported as undocumented.
72
+ `cssVariables.sources` also takes a package path (`@keenmate/web-components-core/dist/menu/styles.js`)
73
+ for the `--base-*` reads of a core module the component renders.
74
+
75
+ ### Fixed
76
+
77
+ - **The text "&lt;head&gt;" / "&lt;/body&gt;" on a page no longer receives the injected snippets** —
78
+ the plugin put the theme / page-config / analytics block after the first `<head…>` match,
79
+ which also matched `<header>` (a page without `<head>`), and the kit's module script before
80
+ the first `</body>`, which can be text inside a page's inline script (the kit then landed
81
+ in that script and broke it). Now: the first real `<head>` tag outside HTML comments, and
82
+ the last `</body>`. Same failure as web-multiselect's live action-buttons page, where its
83
+ own Dockerfile's `sed '/<head>/r …'` put the Plausible snippet (an HTML comment) into the
84
+ page's module script, so no picker got its options.
85
+ - **Headings break instead of widening a phone** — a chapter heading naming an API outside
86
+ `<code>` (drp's `EV05 · getMonthDataCallback & beforeMonthChangeCallback`) and the examples index
87
+ title ("Web DateRangePicker" at 2.5rem) had no break opportunity at 320px. `h1`–`h4` get
88
+ `overflow-wrap: anywhere`; the index title is 2rem on a phone.
89
+ - **A 250px widget fits a 320px phone** — body + card padding left 240px, so an inline calendar
90
+ widened the page. Below 400px the body pads 0.75rem, cards and example sections 1rem, demo cards
91
+ and demo areas 0.75rem (264px inside a card).
92
+ - **`mark` no longer splits a word** — the 2px side padding cut a match inside a word apart
93
+ (core's Search page: "Town send"); `padding: 0`, and a match that wraps keeps its look on both
94
+ lines (`box-decoration-break: clone`).
95
+
3
96
  ## [1.0.0-rc06] - 2026-11-01 [PUBLISHED]
4
97
 
5
98
  ### Added
package/README.md CHANGED
@@ -19,19 +19,18 @@ them in the same shape: `@keenmate/web-components-devkit`.
19
19
  > positioning) lives in [`@keenmate/web-components-core`](../web-components-core).
20
20
  > Nothing here ships inside a component's bundle.
21
21
 
22
- ## What's New in v1.0.0-rc06
22
+ ## What's New in v1.0.0-rc08
23
23
 
24
- - **Example pages — `<kbd>`, `<mark>`, field widths and `.push-end`** — Every component documents a keyboard map and most highlight search matches. Pages either left `<kbd>` in the browser's look (core's Interaction page had 18 bare keys) or styled `<mark>` themselves. A `<kbd>` now looks like the spotlight's key hints and a `<mark>` like its match highlight. Native fields are about 20 characters wide, so pages set inline widths; core had 14. A number field is now 7rem by default. `.w-xs` / `.w-sm` / `.w-md` / `.w-lg` (4 / 6 / 9 / 12rem) size a short field, and a `.form-group` still stretches it to the row. `.push-end` moves an item to the end of a `.controls` row. See the playground's WG08 and FC12.
25
- - **Example pages — phone and contrast fixes** — Inline `<code>` now breaks anywhere. Before, a long identifier such as `resolvePresentation(mode, env, map)` in a heading made a page 350px wide on a 320px screen; code inside a `<pre>` keeps its lines. Muted log text on the light `.log-panel` (`--muted` lines, the seq / time / source cells, the empty placeholder) measured 2.83:1 in the light theme. It now uses `--kmwc-text-color-2`, and a spec checks every tone on both panels in both themes against WCAG AA's 4.5:1. Timeline rows wrap, so on a phone or with a long source name the detail drops under the cells instead of showing one letter per line. `.css-readout` gets a top margin after the demo it measures.
26
- - **CLI — `kmwc <command> --help` no longer runs the command** — Only a top-level `--help` was handled, and unknown flags were silently ignored, so `kmwc sync --help` did a full sync and overwrote core's hand-written Makefile. Every command now lists the flags it accepts. `--help` / `-h` after any command prints that command's usage and exits 0, and an unknown option is an error (exit 2). A boolean flag no longer takes the next word as its value.
27
- - **Libraries — `library: false`, `cssVariables: false`, and a doctor that knows the kind** — A repo like web-components-core builds its package with `tsc`. A plain `vite build` (the devkit's library mode) would empty its `dist/`, so `"kmwc": { "library": false }` now makes it refuse to run, while `vite build --mode examples` still builds the site. `"cssVariables": false` turns off the variables-manifest check, so sync stops re-adding `check:vars`. Doctor reads the registry kind (or `kmwc.kind`): a `library` runs only naming-conventions, readme-structure, publish-command and example-pages. On core that drops 12 errors and 4 "suite could not run" warnings that didn't apply to a library.
24
+ - **Doctor — hard-coded colours in example pages** — web-treeview's devkit migration kept each page's own `<style>` block, and on 8 of its 14 pages the search bars, status bars, badges and panels kept their light colours when the theme switch went dark. rc07 only flagged page CSS that restyles a kit class, so nothing reported them. Doctor now warns about a colour literal (`#hex`, `rgb()` / `hsl()` / `oklch()`, a named colour) on any colour property in a page `<style>`, a `style="…"` attribute or `examples-local.css`. `var()` fallbacks, `light-dark()` pairs, custom properties and `rgb(from var(…))` pass, because they follow the theme. A block that shows fixed colours on purpose, such as a theme gallery or a mockup, opts out with `<style data-kmwc-fixed-colors>`, and a single rule with a `/* kmwc-fixed-colors: why */` comment before it.
25
+ - **Doctor — the loader class matches the installed core** — Core 6d6b768 renamed its default loader's class from `<prefix>-loader` to `<prefix>__loader`, with no release that emits both. A component that renames its selector before the core bump, or forgets to after it, ships a loader with no styles. No rulebook check sees that, and a `## C-NC-8` heading in VALIDATION-NOTES.md would hide a leftover `.x-loader` from the BEM check. Doctor reads which class the installed core's `dist/dom/render-loader.js` draws and fails on any selector in the component's `src/**/*.css` that styles the other one. The error names the file and says which side should change.
28
26
 
29
- ## What's New in v1.0.0-rc05
27
+ ## What's New in v1.0.0-rc07
30
28
 
31
- - **Log panels — one tone set for every log line** — web-components-core's example pages each coloured their log lines with their own classes (`.warn`, `.lv-error`, `data-kind="hook"` …), four schemes mixing severity with "what happened". A `.log-line` now takes one of five tones — `--ok`, `--info`, `--warn`, `--err`, `--muted`, the same names as `.pill` and `.status-dot` (which gain `--info` / `--muted`) — and `--action` marks a "▶ you clicked X" line. Kinds such as hook or callback map onto those tones; the kind itself goes in a cell as text. On `.log-panel--dark` the tones switch to fixed light tints (`--kmwc-console-ok|info|warn|err-color`), so they stay readable on the console in both themes, while a light `.log-panel` follows the page theme. Every line keeps its line breaks and indentation.
32
- - **Log panels — timeline columns, text logs and an empty placeholder** — The cells `.log-line__seq`, `__time`, `__source` and `__name` turn a line into aligned columns, with the detail after them wrapping in its own column; the name takes the line's tone. A log fed by `textContent` is a `<pre class="log-panel">`, `data-empty="(nothing yet)"` shows while a panel is empty, and `--log-max-height: none` lets a log grow. This replaces the page-local timeline CSS core's Lifecycle and Interaction pages carried.
33
- - **`.event-log` — the legacy console now renders as `.log-panel--dark`** — Many component pages (web-grid, web-dropzone, web-treeview, the showcases) still use `.event-log` / `.event-entry` / `.event-time` / `.event-name`, which differed slightly from the newer panel (grey base text, 0.8rem, a 200px cap, no columns). It is now the same panel, and an `.event-entry` holding `.event-time` / `.event-name` is a timeline row with an aligned time column and tones, so old and new logs look the same. Other `.event-log` markup — lines without `.event-entry`, page-own classes — only gets the panel look, so existing layouts don't break.
34
- - **Small helpers — `.mono`, `td.is-highlight`, `.check-list`** — `.mono` sets the monospace font without the code chip, and `textarea.mono` makes a JSON or config editor. `td.is-highlight` (or `th`) tints one table cell, where only whole rows could be highlighted before. `.check-list` stacks checkbox / radio options one per line, a long label wrapping beside its box, with room for a trailing `.pill`. The playground shows everything on the Widgets (WG01–WG07), Form controls (FC11) and Tables (TB06) pages.
29
+ - **CLI — `kmwc check-pages` finds what only a browser can see** — Migrating web-treeview turned up pages that pushed a phone sideways: an iOS-Files mockup 420px wide and a theme gallery built on `minmax(500px, 1fr)`. No static check could have caught that. `kmwc check-pages` loads every example page in Chromium at 1440, 390 and 320px. It reports page and console errors, failed requests, custom elements that never get defined, and pages wider than the viewport, naming the widest elements. It looks inside shadow roots and at long words that overflow their box. It ignores layers that can't widen a page: scrolling code blocks, hidden or fixed layers, a closed chapter-nav panel. It starts the repo's dev server, or `--url` points it at one already running (for example the built site on the preview port), and `--widths` changes the widths. `kmwc sync` adds a `check:pages` script and a `make check-pages` target.
30
+ - **Doctor — catches what the treeview and daterangepicker migrations got wrong** — Doctor now checks that every `./dist/…` target in package.json is a file the library build actually emits, and that the old UMD name matches `kmwc.library.globalName`. Without that, the preset's defaults would quietly have renamed treeview's global (`WebTreeView` → `WebTreeview`) and its CSS (`web-treeview.css` → `style.css`). Doctor also reports scripts that pin their own `--port`, config, Makefile and doc ports outside the repo's registry block, and sibling repos that listen inside it. This found web-daterangepicker on treeview's 12300 and web-multiselect / web-dropzone on daterangepicker's 12200. A Plausible site id that another repo also uses is an error, because treeview's traffic was being counted as web-multiselect's. Page CSS that duplicates kit classes (`.kbd`, `mark`, inline field widths, `margin-left: auto`) is a warning.
31
+ - **Doctor — `VALIDATION-NOTES.md` records accepted deviations** — The rulebook lets a component record an architectural decision instead of following a rule. Doctor still reported those as errors (treeview's C-CST-4, C-CSS-1 and C-NC-6). A rulebook check id used as a `##` heading in `VALIDATION-NOTES.md` (`## C-CST-4 / C-CST-10 — …`) now counts as a pass, and `--verbose` lists them. A mention in plain prose doesn't count.
32
+ - **check-vars — input variables are not dead** — Some component variables are public knobs that are read with a fallback and never declared. Treeview forwards `--wtv-context-menu-*` to core's private `--_km-menu-*` so a page-wide `--km-menu-*` still applies, and declaring them would break that chain. check-vars called all 26 of them dead. In the default mode a variable now counts as present when it is declared or read, and a read-with-fallback missing from the manifest is reported as undocumented. `cssVariables.sources` also takes a package path (`@keenmate/web-components-core/dist/menu/styles.js`) for the `--base-*` reads of a core module the component renders.
33
+ - **Example pages — fit a 320px phone, and injected scripts land in the right place** — A chapter heading that names an API outside `<code>` now breaks instead of widening the page. On a phone the index title is 2rem. Below 400px, body, card and demo padding shrink, so a 250px inline calendar fits inside a card. `mark` no longer splits a word. The Vite plugin now puts its head block after the real `<head>` tag (not `<header>` or the text "<head>"). It puts the kit script before the last `</body>`, so it can no longer land inside a page's inline script and break it. That's the failure web-multiselect's sed-based Dockerfile caused on its live action-buttons page.
35
34
 
36
35
  ## Catalogue
37
36
 
@@ -41,8 +40,8 @@ them in the same shape: `@keenmate/web-components-devkit`.
41
40
  | Component | Package | Prefix | Dev port | Links | Devkit | About |
42
41
  |---|---|---|---:|---|:---:|---|
43
42
  | **web-multiselect** | `@keenmate/web-multiselect` | `ms` | 12100 | [repo](https://github.com/keenmate/web-multiselect) · [examples](https://examples.web-multiselect.keenmate.dev) · [docs](https://web-multiselect.keenmate.dev) · `web-multiselect-showcase` · `web-multiselect-docs` | — | Lightweight multiselect web component with typeahead search, rich content support, and excellent keyboard… |
44
- | **web-daterangepicker** | `@keenmate/web-daterangepicker` | `drp` | 12200 | [repo](https://github.com/keenmate/web-daterangepicker) · [docs](https://web-daterangepicker.keenmate.dev) · `web-daterangepicker-showcase` | — | Lightweight date picker web component with excellent keyboard navigation and range selection |
45
- | **web-treeview** | `@keenmate/web-treeview` | `wtv` | 12300 | [repo](https://github.com/keenmate/web-treeview) · `web-treeview-showcase` | — | Lightweight treeview web component with hierarchical data display, search, expand/collapse, drag-drop, and… |
43
+ | **web-daterangepicker** | `@keenmate/web-daterangepicker` | `drp` | 12200 | [repo](https://github.com/keenmate/web-daterangepicker) · [docs](https://web-daterangepicker.keenmate.dev) · `web-daterangepicker-showcase` | ✅ | Lightweight date picker web component with excellent keyboard navigation and range selection |
44
+ | **web-treeview** | `@keenmate/web-treeview` | `wtv` | 12300 | [repo](https://github.com/keenmate/web-treeview) · `web-treeview-showcase` | ✅ | Lightweight treeview web component with hierarchical data display, search, expand/collapse, drag-drop, and… |
46
45
  | **web-commandpalette** | `@keenmate/web-commandpalette` | `cp` | 12400 | [repo](https://github.com/keenmate/web-commandpalette) · [examples](https://examples.web-commandpalette.keenmate.dev) | ✅ | Framework-agnostic command-palette web component — modes, multi-step commands, scoped + global search,… |
47
46
  | **web-dropzone** | `@keenmate/web-dropzone` | `dz` | 12500 | [repo](https://github.com/keenmate/web-dropzone) | — | Lightweight file dropzone web component with drag-drop, previews, validation, and multiple display modes. |
48
47
  | **web-grid** | `@keenmate/web-grid` | `wg` | 12600 | [repo](https://github.com/keenmate/web-grid) · [docs](https://web-grid.keenmate.com) · [showcase](https://web-grid.keenmate.com) | — | Framework-agnostic data grid web component with sorting, filtering, inline editing, and keyboard navigation |
@@ -168,9 +167,11 @@ Every field is optional. Defaults are derived from the package.
168
167
  "packageJsonPaths": ["package.json"] // monorepos: list workspace package.jsons too
169
168
  },
170
169
  "cssVariables": { // false: no variables manifest, no check:vars
171
- "sources": ["src/css"], // dirs (stylesheets) or explicit files (any type)
170
+ "sources": ["src/css"], // dirs (stylesheets) or explicit files (any type); a package
171
+ // path (e.g. a core module's CSS) resolves from node_modules
172
172
  "internalVariables": ["ms-input-current-width"], // runtime internals, not theming knobs
173
- "checkMode": "declared" // or "consumed" (var() reads)
173
+ "checkMode": "declared" // declared OR read (an input knob may be read, never set);
174
+ // "consumed": var() reads only
174
175
  },
175
176
  "rulebook": {
176
177
  "path": "../BlissFramework/guidelines/web-components", // relative to the component; default: the nearest
@@ -362,6 +363,10 @@ kmwc doctor [--json] devkit checks + package contents + the rulebook's
362
363
  (exit 1 on errors). --verbose lists passes, --no-rulebook skips the
363
364
  suites, --no-pack skips the package-contents check
364
365
  kmwc check-vars [--strict] component-variables.manifest.json ⇔ CSS
366
+ kmwc check-pages [--url u] every example page in Chromium at 1440 / 390 / 320px: page and console
367
+ errors, failed requests, never-defined elements, a page wider than the
368
+ viewport. Starts the dev server unless --url (e.g. the built site);
369
+ --widths 1440,390,320
365
370
  kmwc check-release --tag t latest ⇔ release version, rc ⇔ -rcN, version not yet on npm
366
371
  kmwc new web-<name> scaffold a component (--prefix, --title, --tag, --dir, --port, --minimal, --register)
367
372
  --adopt: scaffold an ALREADY registered component (its port/prefix) to migrate into
@@ -388,7 +393,41 @@ and example-pages; the others check what a custom element ships (its CSS,
388
393
  manifest, overlay).
389
394
  When the rulebook isn't checked out (or there's no bash) the suites are
390
395
  skipped with a warning — the rulebook stays the single source of truth; the
391
- devkit never copies its rules.
396
+ devkit never copies its rules. A failure whose check id heads a section of
397
+ `VALIDATION-NOTES.md` (`## C-CSS-1 — Lean strategy`, `## C-CST-4 / C-CST-10 — …`)
398
+ is an accepted deviation — an architectural decision recorded per the rulebook —
399
+ and counts as a pass (`--verbose` lists them).
400
+
401
+ **What else doctor checks.** Beyond the presets, legacy files, fragment pages and
402
+ generated files:
403
+
404
+ - **Library outputs** — every `./dist/…` target in package.json (`main`,
405
+ `module`, `types`, `style`, `exports`) is a file the library build emits. Before
406
+ the switch to the preset it also compares the old `lib.name` with
407
+ `kmwc.library.globalName` — a renamed UMD global or CSS file breaks consumers
408
+ silently.
409
+ - **Ports** — no script pins a `--port` of its own, the repo's configs, Makefile
410
+ and docs stay inside its registry block, and no sibling checkout (a registry
411
+ entry at `../<id>`) listens on a port inside it.
412
+ - **Analytics** — no other repo uses the same Plausible site id.
413
+ - **Page CSS** — a page `<style>` or `examples-local.css` rule on a kit-owned
414
+ selector (`.kbd`, `kbd`, `mark`, `.pill`, `.card` …) and single-declaration
415
+ inline styles a kit class covers (`width` on a field → `.w-*`,
416
+ `margin-left: auto` → `.push-end`, `margin-top: 1rem` → `.mt-1`) are warnings.
417
+ So is a hard-coded colour (`#hex`, `rgb()`, `white` …) in page CSS or a `style="…"`:
418
+ it stays put when the theme switch flips the kit to dark. `var()` fallbacks and
419
+ `light-dark()` pairs follow the theme. Fixed colours on purpose (a theme gallery, a
420
+ mockup, a test fixture) go in a `<style data-kmwc-fixed-colors>` block, or one rule
421
+ gets a `/* kmwc-fixed-colors: why */` comment right before it.
422
+ - **Loader class** — the component CSS styles the class the *installed* core gives
423
+ its default loader (`<prefix>__loader` from core 6d6b768 on, `<prefix>-loader`
424
+ before). Renaming the selector on one side of the core bump leaves the loader
425
+ unstyled, and a VALIDATION-NOTES `## C-NC-8` would hide the old class from the
426
+ rulebook.
427
+
428
+ `kmwc check-pages` is the browser half: it loads every example page and reports
429
+ what doctor can't see in the source — a script error, a 404, a fixed width that
430
+ pushes a phone sideways.
392
431
 
393
432
  **Package contents.** Doctor runs `npm pack --dry-run` and fails on anything
394
433
  dev-only that would reach a consumer's `node_modules`: `CLAUDE.md` at any
@@ -451,7 +490,11 @@ registry edits), and move the component's own code into that layout with
451
490
  runner from the old Makefile, the canonical origin from the old
452
491
  `examples-copy-link.js`, and internals from the old
453
492
  `check-variable-manifest.mjs` (`cssVariables.internalVariables`).
454
- 3. Replace `vite.config.ts` (and any `vite.examples.config.ts`) with
493
+ 3. Run `npx kmwc doctor` **before** replacing `vite.config.ts`: it compares the
494
+ old `lib.name` with `kmwc.library.globalName` and package.json's `dist/`
495
+ targets with the preset's outputs — set `library.globalName` / `fileName` /
496
+ `cssFileName` to the published names so nothing gets renamed. Then replace
497
+ `vite.config.ts` (and any `vite.examples.config.ts`) with
455
498
  `defineComponentConfig()`. Move genuinely specific bits to `plugins` and
456
499
  `overrides`. Do the same for `playwright.config.ts`.
457
500
  4. Pages: delete the `<link href="examples-shared.css">` and the
@@ -468,10 +511,11 @@ registry edits), and move the component's own code into that layout with
468
511
  7. Set `"check:vars": "kmwc check-vars"` and run it.
469
512
  8. `npx kmwc sync`, then review the diff. Repo-specific Makefile targets move
470
513
  to `Makefile.local`.
471
- 9. Check it runs: `npm run dev`, `npm run build`, `npm run build:examples`,
472
- `npm run preview:examples`, the e2e tests, and finally `npx kmwc doctor`
473
- (devkit + rulebook — rulebook failures are real findings to fix or to
474
- document under `## Known limitations`, per the rulebook).
514
+ 9. Check it runs: `npm run dev`, `npm run build` (compare `dist/` with the old
515
+ build), `npm run build:examples`, `npm run preview:examples`, the e2e tests,
516
+ `npx kmwc check-pages` (and `--url` on the preview port for the built site),
517
+ and finally `npx kmwc doctor` (devkit + rulebook — rulebook failures are real
518
+ findings to fix, or architectural decisions to record in `VALIDATION-NOTES.md`).
475
519
 
476
520
  ### Developing the devkit
477
521
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keenmate/web-components-devkit",
3
- "version": "1.0.0-rc06",
3
+ "version": "1.0.0-rc08",
4
4
  "description": "Shared dev, example-site and build infrastructure for Keenmate web components: Vite preset, examples kit (chapter nav, copy-link, controls store, shared styles), Playwright preset and the `kmwc` CLI (sync, doctor, check-vars, new, readme).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,189 @@
1
+ /**
2
+ * kmwc check-pages — load every example page in a real browser and report what
3
+ * a reader would hit: page errors, console errors, failed requests, custom
4
+ * elements that never get defined, and a page wider than the viewport (a fixed
5
+ * width or a long word that pushes a phone sideways).
6
+ *
7
+ * Runs against the repo's own dev server (started here on kmwc.ports.dev), or a
8
+ * server you already have (--url, e.g. the built site on the preview port).
9
+ * Widths default to desktop + two phones. Uses the component's own
10
+ * @playwright/test (see getPeerModule) — `npx playwright install chromium` once.
11
+ */
12
+ import { existsSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { getKmwcConfig, getPeerModule, loadPeerModule } from '../../config.js';
15
+ import { getExamplePages } from '../../vite/index.js';
16
+
17
+ export const DEFAULT_WIDTHS = [1440, 390, 320];
18
+ const VIEWPORT_HEIGHT = 900;
19
+ const SETTLE_MS = 600;
20
+ const VITE_CONFIG_FILES = ['vite.config.ts', 'vite.config.mts', 'vite.config.mjs', 'vite.config.js'];
21
+ const MAX_OFFENDERS = 3;
22
+ // Requests a page doesn't control (favicon probes, analytics in a built site).
23
+ const IGNORED_REQUEST_PATTERN = /favicon\.ico|stats\.keenmate\.services|plausible/;
24
+
25
+ /** `--widths 1440,390` → [1440, 390]. Pure. */
26
+ export function parseWidths(widthsText) {
27
+ if (!widthsText) return DEFAULT_WIDTHS;
28
+ const widths = String(widthsText).split(',').map((width) => Number(width.trim()));
29
+ if (!widths.length || widths.some((width) => !Number.isInteger(width) || width < 200)) throw new Error(`--widths: "${widthsText}" is not a list of pixel widths ≥ 200`);
30
+ return widths;
31
+ }
32
+
33
+ /**
34
+ * Problems in one page load's report → [string]. Pure — exported for tests.
35
+ * @param {{ width: number, errors: string[], undefinedElements: string[], scrollWidth: number, clientWidth: number, offenders: string[] }} pageReport
36
+ */
37
+ export function getPageProblems(pageReport) {
38
+ const problems = [...pageReport.errors];
39
+ if (pageReport.undefinedElements.length) problems.push(`never defined: <${pageReport.undefinedElements.join('>, <')}>`);
40
+ if (pageReport.scrollWidth > pageReport.clientWidth) {
41
+ const offenderText = pageReport.offenders.length ? ` — widest: ${pageReport.offenders.join(', ')}` : '';
42
+ problems.push(`page is ${pageReport.scrollWidth}px wide in a ${pageReport.clientWidth}px viewport${offenderText}`);
43
+ }
44
+ return problems;
45
+ }
46
+
47
+ /**
48
+ * Runs in the page: overflow, undefined custom elements. A culprit is the deepest
49
+ * element — or the text — past the right edge, outside anything that scrolls or
50
+ * clips on purpose (a code block, a table wrapper) and outside hidden or fixed
51
+ * layers (a closed nav panel, a toast), which don't widen the page.
52
+ */
53
+ function inspectPage(maxOffenders) {
54
+ const viewportWidth = document.documentElement.clientWidth;
55
+ // Shadow roots count: a component's own internals can widen the page too.
56
+ const getParent = (element) => element.parentElement || element.getRootNode().host || null;
57
+ const describeOne = (element) => `${element.tagName.toLowerCase()}${element.id ? `#${element.id}` : ''}${[...element.classList].slice(0, 2).map((name) => `.${name}`).join('')}`;
58
+ const describe = (element) => {
59
+ const host = element.getRootNode().host;
60
+ return host ? `${describe(host)} › ${describeOne(element)}` : describeOne(element);
61
+ };
62
+ const getAllElements = (root) => [...root.querySelectorAll('*')].flatMap((element) => (element.shadowRoot ? [element, ...getAllElements(element.shadowRoot)] : [element]));
63
+ const checkIsContained = (element) => {
64
+ for (let current = element; current && current !== document.body; current = getParent(current)) {
65
+ const style = getComputedStyle(current);
66
+ if (style.position === 'fixed' || style.visibility === 'hidden' || style.display === 'none') return true;
67
+ if (current !== element && ['auto', 'scroll', 'hidden', 'clip'].includes(style.overflowX)) return true;
68
+ }
69
+ return false;
70
+ };
71
+ const allElements = getAllElements(document.body);
72
+ const pastEdge = allElements.filter((element) => {
73
+ const box = element.getBoundingClientRect();
74
+ return box.width > 0 && box.right > viewportWidth + 1 && !checkIsContained(element);
75
+ });
76
+ const checkContainsDeep = (element, other) => {
77
+ for (let current = other; current; current = getParent(current)) if (current === element) return true;
78
+ return false;
79
+ };
80
+ const offenders = pastEdge.filter((element) => !pastEdge.some((other) => other !== element && checkContainsDeep(element, other))).map(describe);
81
+ // Text past the edge inside a box that fits: a long word with no break opportunity.
82
+ const textRoots = [document.body, ...allElements.filter((element) => element.shadowRoot).map((element) => element.shadowRoot)];
83
+ const textNodes = textRoots.flatMap((root) => {
84
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
85
+ const nodes = [];
86
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) nodes.push(node);
87
+ return nodes;
88
+ });
89
+ for (const textNode of textNodes) {
90
+ const parent = textNode.parentElement;
91
+ if (!textNode.textContent.trim() || !parent || pastEdge.includes(parent)) continue;
92
+ const range = document.createRange();
93
+ range.selectNodeContents(textNode);
94
+ const box = range.getBoundingClientRect();
95
+ const parentStyle = getComputedStyle(parent);
96
+ if (box.right > viewportWidth + 1 && !['hidden', 'clip', 'auto', 'scroll'].includes(parentStyle.overflowX) && !checkIsContained(parent)) {
97
+ offenders.push(`${describe(parent)} "${textNode.textContent.trim().slice(0, 40)}"`);
98
+ }
99
+ }
100
+ const undefinedElements = [...new Set([...document.querySelectorAll(':not(:defined)')].map((element) => element.localName).filter((name) => name.includes('-')))];
101
+ return {
102
+ scrollWidth: document.documentElement.scrollWidth,
103
+ clientWidth: viewportWidth,
104
+ offenders: [...new Set(offenders)].slice(0, maxOffenders),
105
+ undefinedElements,
106
+ };
107
+ }
108
+
109
+ async function loadPageReport(browser, pageUrl, width) {
110
+ const page = await browser.newPage({ viewport: { width, height: VIEWPORT_HEIGHT } });
111
+ const errors = [];
112
+ page.on('pageerror', (error) => errors.push(`page error: ${error.message.split('\n')[0]}`));
113
+ page.on('console', (message) => {
114
+ if (message.type() === 'error') errors.push(`console error: ${message.text().split('\n')[0]}`);
115
+ });
116
+ page.on('response', (response) => {
117
+ if (response.status() >= 400 && !IGNORED_REQUEST_PATTERN.test(response.url())) errors.push(`HTTP ${response.status()}: ${response.url()}`);
118
+ });
119
+ page.on('requestfailed', (request) => {
120
+ if (!IGNORED_REQUEST_PATTERN.test(request.url())) errors.push(`request failed: ${request.url()} (${request.failure()?.errorText})`);
121
+ });
122
+ try {
123
+ await page.goto(pageUrl, { waitUntil: 'networkidle', timeout: 30_000 });
124
+ await page.waitForTimeout(SETTLE_MS);
125
+ const inspection = await page.evaluate(inspectPage, MAX_OFFENDERS);
126
+ return { width, errors: [...new Set(errors)], ...inspection };
127
+ } catch (error) {
128
+ return { width, errors: [...new Set([...errors, `load failed: ${error.message.split('\n')[0]}`])], scrollWidth: 0, clientWidth: 0, offenders: [], undefinedElements: [] };
129
+ } finally {
130
+ await page.close();
131
+ }
132
+ }
133
+
134
+ async function startDevServer(config) {
135
+ const viteModule = await loadPeerModule(config.root, 'vite');
136
+ // Vite < 6 resolves to its CJS build here: the API sits on `default`.
137
+ const createServer = viteModule.createServer || viteModule.default?.createServer;
138
+ // Run like `npm run dev` in the repo: the preset reads package.json#kmwc from the cwd,
139
+ // and Vite looks for vite.config there too.
140
+ process.chdir(config.root);
141
+ const configFile = VITE_CONFIG_FILES.map((fileName) => join(config.root, fileName)).find((filePath) => existsSync(filePath));
142
+ const server = await createServer({ root: config.root, configFile: configFile || false, logLevel: 'error', server: { port: config.ports.dev, strictPort: true } });
143
+ try {
144
+ await server.listen();
145
+ } catch (error) {
146
+ await server.close();
147
+ throw new Error(
148
+ `port ${config.ports.dev} is busy (${error.code || error.message}). If that is this repo's dev server, run: kmwc check-pages --url http://localhost:${config.ports.dev}`
149
+ );
150
+ }
151
+ return server;
152
+ }
153
+
154
+ export async function run(args) {
155
+ const config = getKmwcConfig(args.root);
156
+ const widths = parseWidths(args.widths);
157
+ const pageFiles = Object.values(getExamplePages(config)).map((pagePath) => pagePath.replace(/\\/g, '/').split('/').pop());
158
+ if (!pageFiles.length) {
159
+ console.log('kmwc check-pages: no example pages — nothing to check.');
160
+ return 0;
161
+ }
162
+ let chromium;
163
+ try {
164
+ ({ chromium } = getPeerModule(config.root, '@playwright/test'));
165
+ } catch {
166
+ throw new Error('@playwright/test is not installed in this repo — npm i -D @playwright/test && npx playwright install chromium');
167
+ }
168
+
169
+ const server = args.url ? null : await startDevServer(config);
170
+ const baseUrl = (args.url || `http://localhost:${config.ports.dev}`).replace(/\/$/, '');
171
+ const browser = await chromium.launch();
172
+ let problemCount = 0;
173
+ try {
174
+ console.log(`kmwc check-pages — ${pageFiles.length} page(s) × ${widths.join(' / ')}px on ${baseUrl}\n`);
175
+ for (const pageFile of pageFiles) {
176
+ const pageProblems = [];
177
+ for (const width of widths) {
178
+ for (const problem of getPageProblems(await loadPageReport(browser, `${baseUrl}/${pageFile}`, width))) pageProblems.push(`@${width}px ${problem}`);
179
+ }
180
+ problemCount += pageProblems.length;
181
+ console.log(`${pageProblems.length ? '✗' : '✓'} ${pageFile}${pageProblems.map((problem) => `\n ${problem}`).join('')}`);
182
+ }
183
+ } finally {
184
+ await browser.close();
185
+ if (server) await server.close();
186
+ }
187
+ console.log(`\n${problemCount ? '✗' : '✓'} ${problemCount} problem(s)`);
188
+ return problemCount ? 1 : 0;
189
+ }
@@ -8,10 +8,12 @@
8
8
  *
9
9
  * Contract (prefix = kmwc.prefix or manifest.prefix, e.g. `ms`):
10
10
  * - every `--base-*` read via var() must be in baseVariables, and vice versa
11
- * - checkMode "declared" (default): every `--<prefix>-*` DECLARED (`--x:`)
12
- * must be in componentVariables, and vice versa
13
- * - checkMode "consumed": the same, but against `var(--<prefix>-*)` reads (for
14
- * components whose public knobs are read-with-fallback, never declared)
11
+ * - checkMode "declared" (default): every `--<prefix>-*` DECLARED (`--x:`) or
12
+ * READ (`var(--x)`) must be in componentVariables, and vice versa. A read
13
+ * without a declaration is an input variable — a public knob that is unset by
14
+ * design, e.g. one forwarded to core's menu so a page-wide value still applies
15
+ * - checkMode "consumed": only `var(--<prefix>-*)` reads count (for components
16
+ * whose knobs are all read-with-fallback; a declared-but-unread name is dead)
15
17
  * - names in kmwc.cssVariables.internalVariables are runtime internals — exempt
16
18
  *
17
19
  * Undocumented component variables are a warning unless --strict; everything else errors.
@@ -25,7 +27,9 @@ const STYLESHEET_EXTENSIONS = new Set(['.css', '.scss', '.sass', '.less', '.pcss
25
27
  /**
26
28
  * Directories are scanned for stylesheets; a file listed explicitly is read
27
29
  * whatever its type — e.g. a built bundle that inlines the compiled CSS, for
28
- * components whose SCSS generates variable names via interpolation.
30
+ * components whose SCSS generates variable names via interpolation, or a core
31
+ * module whose CSS the component renders (`@keenmate/web-components-core/dist/menu/styles.js`
32
+ * — a package path resolves from node_modules).
29
33
  */
30
34
  function getStylesheetTexts(sourcePath, stylesheetTexts, isExplicit = true) {
31
35
  if (!existsSync(sourcePath)) throw new Error(`cssVariables source not found: ${sourcePath}`);
@@ -37,6 +41,14 @@ function getStylesheetTexts(sourcePath, stylesheetTexts, isExplicit = true) {
37
41
  return stylesheetTexts;
38
42
  }
39
43
 
44
+ /** A source relative to the repo, else a package path (`@scope/pkg/dist/x.js`) under node_modules. */
45
+ function resolveSourcePath(root, sourcePath) {
46
+ const repoPath = resolve(root, sourcePath);
47
+ if (existsSync(repoPath) || sourcePath.startsWith('.')) return repoPath;
48
+ const packagePath = resolve(root, 'node_modules', sourcePath);
49
+ return existsSync(packagePath) ? packagePath : repoPath;
50
+ }
51
+
40
52
  const mapToRegExpSource = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
41
53
 
42
54
  /** Pure verdict — exported for tests. */
@@ -48,7 +60,7 @@ export function checkManifestVariables({ cssText, manifest, prefix, internalVari
48
60
  const consumedVariables = new Set([...cssText.matchAll(new RegExp(`var\\(\\s*--(${prefixSource}-[a-z0-9-]+)`, 'gi'))].map((match) => match[1]));
49
61
  const consumedBaseVariables = new Set([...cssText.matchAll(/var\(\s*--(base-[a-z0-9-]+)/gi)].map((match) => match[1]));
50
62
 
51
- const componentVariablesInCss = checkMode === 'consumed' ? consumedVariables : declaredVariables;
63
+ const componentVariablesInCss = checkMode === 'consumed' ? consumedVariables : new Set([...declaredVariables, ...consumedVariables]);
52
64
  const internalVariableSet = new Set(internalVariables);
53
65
  const manifestComponentVariables = new Set((manifest.componentVariables || []).map((entry) => entry.name));
54
66
  const manifestBaseVariables = new Set((manifest.baseVariables || []).map((entry) => entry.name));
@@ -81,7 +93,7 @@ export async function run(args) {
81
93
  const prefix = config.prefix || manifest.prefix;
82
94
  if (!prefix) throw new Error('no variable prefix — set kmwc.prefix or "prefix" in the manifest');
83
95
 
84
- const cssText = config.cssVariables.sources.flatMap((sourcePath) => getStylesheetTexts(resolve(config.root, sourcePath), [])).join('\n');
96
+ const cssText = config.cssVariables.sources.flatMap((sourcePath) => getStylesheetTexts(resolveSourcePath(config.root, sourcePath), [])).join('\n');
85
97
  const verdict = checkManifestVariables({
86
98
  cssText,
87
99
  manifest,
@@ -90,7 +102,7 @@ export async function run(args) {
90
102
  checkMode: config.cssVariables.checkMode,
91
103
  });
92
104
 
93
- const presenceWord = config.cssVariables.checkMode === 'consumed' ? 'consumed via var()' : 'declared';
105
+ const presenceWord = config.cssVariables.checkMode === 'consumed' ? 'consumed via var()' : 'declared or read';
94
106
  const renderSection = (title, names) => `${title} (${names.length}):\n` + names.map((name) => ` --${name}`).join('\n');
95
107
  const errors = [];
96
108
  const warnings = [];