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

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,85 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.0-rc06] - 2026-11-01 [PUBLISHED]
4
+
5
+ ### Added
6
+
7
+ - **`<kbd>` and `<mark>`** — every component documents a keyboard map and most highlight
8
+ search matches, and pages either left them in the browser's look (core's Interaction page:
9
+ 18 bare `<kbd>`s) or styled them locally (core's Search page). A `<kbd>` now looks like the
10
+ spotlight's key hints and a `<mark>` like its match highlight. Widgets page WG08.
11
+ - **Field widths** — a native field is ~20 characters wide, so pages set inline widths (core:
12
+ 14). A number field is now 7rem (six digits and the spin buttons; a `.form-group` still
13
+ stretches it), and `.w-xs` / `.w-sm` / `.w-md` / `.w-lg` (4 / 6 / 9 / 12rem) size a short
14
+ field. `.push-end` moves an item to the end of a `.controls` row. Form controls page FC12.
15
+ - **`kmwc.library: false`** — for a repo that builds its package itself (core: `tsc`) or ships
16
+ none. Plain `vite build`, the devkit's library mode, refuses to run instead of emptying
17
+ `dist/`; `vite build --mode examples` still builds the site, and sync no longer proposes a
18
+ `build: vite build` script. `{ "shouldBuild": false, "entry": … }` keeps the pages' entry.
19
+ - **`kmwc.cssVariables: false`** (or `checkMode: "off"`) — sync stopped re-adding `check:vars`
20
+ only when there was no manifest; now it can be switched off (core builds its variable names
21
+ by interpolation, which the checker can't see). `kmwc check-vars` then skips.
22
+ - **Doctor knows libraries** — a repo whose registry entry (or `kmwc.kind`) is `library` runs
23
+ only the rulebook suites that apply to it: naming-conventions, readme-structure,
24
+ publish-command, example-pages. The CSS, Custom Elements Manifest, overlay and
25
+ component-structure suites check what a custom element ships; on core they gave 12 errors
26
+ and 4 "suite could not run" warnings. No prefix warning for a library.
27
+
28
+ ### Fixed
29
+
30
+ - **`kmwc <command> --help` ran the command** — only a top-level `--help` was handled and
31
+ unknown flags were ignored, so `kmwc sync --help` did a full sync (it overwrote core's
32
+ Makefile). `--help` / `-h` after any command now prints that command's usage and exits 0,
33
+ and an unknown option is an error (exit 2). Every command declares its flags; a boolean
34
+ flag no longer swallows the next word as its value.
35
+ - **Inline `<code>` widened a phone page** — a long identifier has no break opportunity, so
36
+ `resolvePresentation(mode, env, map)` in a heading made core's page 350px wide at 320px.
37
+ Inline code now breaks anywhere; code in a `<pre>` keeps its lines.
38
+ - **Muted log text failed contrast on the light panel** — `--muted` lines, the seq / time /
39
+ source cells and the empty placeholder used `--kmwc-text-color-3`: 2.83:1 on the light
40
+ `.log-panel` in the light theme (WCAG AA wants 4.5:1). They now use `--kmwc-text-color-2`.
41
+ A spec checks every tone on both panels in both themes.
42
+ - **`.css-readout` sat on the demo above it** — it had no top margin (core carried 8 inline
43
+ margins); it now has 0.75rem.
44
+
45
+ - **Timeline log lines on a phone** — the cells (`.log-line__seq`, `__time`, `__source`,
46
+ `__name`) never shrink and the row never wrapped, so on a narrow panel, or with a long
47
+ source name, the detail got the few pixels left over: one letter per line. The row now
48
+ wraps, and the detail wants ~14rem beside the cells; with less room it drops under them at
49
+ full width. Wide panels are unchanged. A cell is capped at the panel width.
50
+
51
+ ## [1.0.0-rc05] - 2026-11-01 [PUBLISHED]
52
+
53
+ ### Changed
54
+
55
+ - **`.event-log` is now the same panel as `.log-panel--dark`** — the legacy console differed
56
+ in small ways (grey base text, 0.8rem, 200px cap, no columns), so old and new logs looked
57
+ slightly different. It now renders exactly as `.log-panel--dark` (light base text,
58
+ 0.85rem, `--log-max-height`, default 220px), and an `.event-entry` holding `.event-time`
59
+ / `.event-name` is a timeline row: aligned time column, the detail wrapping in its own
60
+ column, tones via `.log-line--*`. Other `.event-log` markup (lines without
61
+ `.event-entry`, page-own classes) just gets the panel look.
62
+
63
+ ### Added
64
+
65
+ - **Log lines: one tone set, timeline columns, text logs, empty placeholder** —
66
+ web-components-core's pages each coloured their log lines (four schemes mixing severity
67
+ with "what happened"). A `.log-line` now takes one of the pill tones — `--ok`, `--info`,
68
+ `--warn`, `--err`, `--muted` — and `--action` marks a "you clicked X" line. On
69
+ `.log-panel--dark` and `.event-log` the tones switch to fixed light tints
70
+ (`--kmwc-console-ok|info|warn|err-color`), so they stay readable on the console in both
71
+ themes. Every line in a log keeps its line breaks and indentation. The cells
72
+ `.log-line__seq`, `__time`, `__source`, `__name` turn a line into aligned columns, with
73
+ the detail wrapping in its own column. `<pre class="log-panel">` is a log fed by
74
+ `textContent`; `data-empty` is shown while a panel is empty.
75
+ - **`.pill--info` / `--muted` and `.status-dot--info` / `--muted`**, so pills, dots and log
76
+ lines share the same five tones.
77
+ - **`.mono`** (monospace text without the code chip; `textarea.mono` for a JSON editor),
78
+ **`td.is-highlight`** (one highlighted cell) and **`.check-list`** (checkbox / radio
79
+ options one per line, a long label wrapping beside its box).
80
+ - Playground: WG01 and WG03 show the tones, new WG05 (timeline), WG06 (text log, empty
81
+ placeholder), WG07 (`.mono`), FC11 (check list), TB06 (highlighted cell); e2e specs.
82
+
3
83
  ## [1.0.0-rc04] - 2026-11-01 [PUBLISHED]
4
84
 
5
85
  ### Fixed
package/README.md CHANGED
@@ -19,20 +19,19 @@ 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-rc04
22
+ ## What's New in v1.0.0-rc06
23
23
 
24
- - **Example-page grids — wide content scrolls instead of widening the page** — A `<pre>` or table inside a `.grid`, `.grid-2` or `.card-grid` item still pushed the page wider than a phone: grid items default to `min-width: auto` (their min-content), so the single `1fr` column at ≤ 768 px grew to fit the content instead of letting it scroll. Every item of the three grids now gets `min-width: 0`, the same rule pure-css puts on its `.pc-col-*` columns. Found by two agents migrating web-components-core's example pages, which drop their page-local workaround with this release.
25
- - **`.controls` — long `<code>` in a button wraps on a phone** — rc03 wrapped `<code>` only inside `.controls` labels, so a button whose label was an unbroken API name (`setAttribute("…")`) still overflowed the row. The wrap rule (`overflow-wrap: anywhere`) now covers any `<code>` in a `.controls` row, and a `.controls` button is capped at the row width. An e2e spec injects both cases at 390 px and checks that the page stays inside the screen.
26
- - **Playground — a Widgets page for the kit's opt-in helpers** — `.log-panel` (light and `--dark`), the legacy `.event-log`, `.pill` / `.status-dot` tones, `.display-sliders` and `.css-readout` had no demo, so their look was only checked on component pages. The new `examples-widgets.html` (WG01–WG04) shows them as they are today, including the gaps the coming log-tone work fixes (no line tones, leading spaces collapsing in a log line), and gets its own e2e spec.
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.
27
28
 
28
- ## What's New in v1.0.0-rc03
29
+ ## What's New in v1.0.0-rc05
29
30
 
30
- - **Spotlight — Ctrl/⌘+K chapter search across every example page** — Example sites grew to dozens of chapters over several pages, and the per-page chapter nav only knew its own page. The Vite plugin now builds an index of every page's `<h2>`s (`virtual:kmwc-chapters`), coded and uncoded, and Ctrl/⌘+K opens a search that matches the section code or the heading text (`th7` finds `TH07`), grouped by page with the current one first. Enter jumps, Ctrl/⌘+Enter opens a new tab. It mirrors web-commandpalette's markup, matching, keyboard map and CSS without depending on it; `npm run check:spotlight-mirror` lists what changed there since the last alignment. Turn it off with `kmwc.examples.isSpotlightEnabled: false`.
31
- - **Form controls — one look for native inputs on every example page** — Pages styled their own selects and inputs, or got the browser default, so a select next to the kit's buttons came out shorter and in the platform's style (a `.controls` row was the only place the kit reached). Text fields, selects and list boxes, textareas, checkboxes, radios, `role="switch"` switches, ranges, colour and file inputs are now styled with no class, modelled on pure-admin's forms. Inputs, selects and buttons share `--kmwc-control-height`, so a mixed row lines up in any container. Focus, placeholder, disabled tone, checkbox size and the tick read the `--base-*` contract. The rules are zero-specificity, and `kmwc-native` opts an element out.
32
- - **Tables — every table styled, `.reference-table` plus modifiers** — Only `.reference-table` had a look; a bare table was unstyled and pages wrote their own table CSS with fixed light colours that break in dark mode. Any `<table>` now gets a header fill, row lines and a hover tint, and modifiers combine: `.table-full`, `.table-bordered`, `.table-striped`, and `.table-compact`, whose inputs and buttons shrink with the rows. Add `tr.is-highlight` and `.num` (tabular figures). Every wide table scrolls inside its card on a phone.
33
- - **Scaffold — component CSS no longer restyles the host page** — The `*, *::before, *::after { box-sizing: inherit }` reset in the `kmwc new` and `--minimal` templates also reached the page through the component's light-DOM stylesheet, in a layer that beats `@layer kmwc`, so page controls rendered content-box (a 36 px input came out 38 px). It is scoped to `:host *` now, and a unit test rejects any universal or bare-element selector in scaffold CSS. Components scaffolded on rc01/rc02 should make the same one-line change in `src/css/base.css`.
34
- - **Example pages — short chapter links, phones and the example-pages suite** — `#th07`, `#th7` or an uncoded heading's slug now scroll to that chapter on arrival. On a phone, example pages no longer grow wider than the screen, which made mobile browsers report a wrong viewport to pages that demo breakpoints. `kmwc doctor` runs the rulebook's new `example-pages` suite: every example `<h2>` carries a permanent section code.
35
- - **`make dev` at the devkit root** — the standard `dev` / `kill-port` / `build-examples` / `preview-examples` targets, run against the playground.
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.
36
35
 
37
36
  ## Catalogue
38
37
 
@@ -144,7 +143,8 @@ Every field is optional. Defaults are derived from the package.
144
143
  "tagline": "Feature-rich multiselect …", // default: package.json description
145
144
  "tag": "web-multiselect",
146
145
  "prefix": "ms", // reserved CSS prefix (rulebook base-variables.md)
147
- "library": {
146
+ "kind": "component", // default: the components.json entry's kind
147
+ "library": { // false: no library build (see below)
148
148
  "entry": "src/index.ts", // default
149
149
  "globalName": "MultiSelect", // UMD global
150
150
  "fileName": "multiselect", // → dist/multiselect.js + .umd.js
@@ -167,7 +167,7 @@ Every field is optional. Defaults are derived from the package.
167
167
  "runner": "podman",
168
168
  "packageJsonPaths": ["package.json"] // monorepos: list workspace package.jsons too
169
169
  },
170
- "cssVariables": {
170
+ "cssVariables": { // false: no variables manifest, no check:vars
171
171
  "sources": ["src/css"], // dirs (stylesheets) or explicit files (any type)
172
172
  "internalVariables": ["ms-input-current-width"], // runtime internals, not theming knobs
173
173
  "checkMode": "declared" // or "consumed" (var() reads)
@@ -181,6 +181,16 @@ Every field is optional. Defaults are derived from the package.
181
181
  }
182
182
  ```
183
183
 
184
+ **A library** (registry `kind: "library"`, e.g. web-components-core) builds its
185
+ package itself (`tsc`) or ships none. `"library": false` (or `{ "shouldBuild":
186
+ false, "entry": … }` to keep the pages' entry script) makes plain `vite build`
187
+ refuse to run — it is the devkit's library mode and would empty `dist/` — while
188
+ `vite build --mode examples` still builds the site, and `kmwc sync` no longer
189
+ proposes a `build: vite build` script. `"cssVariables": false` turns off the
190
+ manifest check (no `check:vars` script, `kmwc check-vars` skips), for variables
191
+ the checker can't see, such as names built by string interpolation. Doctor runs
192
+ only the rulebook suites that apply to a library (see below).
193
+
184
194
  ### Vite preset
185
195
 
186
196
  ```ts
@@ -267,7 +277,8 @@ doctor` lists the ones still repeating the head/header boilerplate.
267
277
  `.demo-area`, `.demo-card`, `.code-block`, `.note(.warning|.info|.success|.error)`,
268
278
  `.grid`/`.card-grid`, `.reference-table` (+ `.table-*` modifiers), `.log-panel`, `.pill`,
269
279
  `.status-dot`, `.control-group`, buttons (`.btn-secondary`, `.btn-outline`,
270
- `.btn-danger`) — while everything the kit **generates** is prefixed BEM
280
+ `.btn-danger`), `<kbd>` keys and `<mark>` search matches (the spotlight's look),
281
+ `.push-end` (to the end of a flex row) — while everything the kit **generates** is prefixed BEM
271
282
  (`kmwc-chapter-nav__item--active`, `kmwc-copy-link--copied`, …).
272
283
  - **Form controls** look the same on every page without a class: text fields,
273
284
  selects and list boxes (`multiple` / `size`), textareas, checkboxes, radios,
@@ -277,7 +288,9 @@ doctor` lists the ones still repeating the head/header boilerplate.
277
288
  selects and buttons share `--kmwc-control-height`, so a row of them lines up
278
289
  in any container, not only `.controls`. The rules are zero-specificity
279
290
  (`:where()`); `kmwc-native` on an element or an ancestor keeps the browser's
280
- own controls (a light-DOM component that draws its own inputs). The
291
+ own controls (a light-DOM component that draws its own inputs). A number field
292
+ is 7rem wide; `.w-xs` / `.w-sm` / `.w-md` / `.w-lg` (4 / 6 / 9 / 12rem) size a
293
+ short field without an inline style. The
281
294
  playground's `examples-forms.html` shows them all.
282
295
  - **Tables:** any `<table>` gets a header fill, row lines and a hover tint;
283
296
  `.reference-table` is the full-width, fully bordered API table. Modifiers
@@ -285,9 +298,15 @@ doctor` lists the ones still repeating the head/header boilerplate.
285
298
  (tighter cells; the controls in it shrink via `--kmwc-control-height`) — plus
286
299
  `tr.is-highlight` and a `.num` cell (right-aligned, tabular figures). On a phone
287
300
  a wide table scrolls inside its card. See the playground's `examples-tables.html`.
288
- - **Widgets** (`.log-panel` / `--dark`, `.event-log`, `.pill`, `.status-dot`,
289
- `.display-sliders`, `.css-readout`) are shown in the playground's
290
- `examples-widgets.html`.
301
+ - **Widgets:** `.log-panel` (light, following the theme; `--dark` for a console
302
+ stream; the legacy `.event-log` / `.event-entry` markup renders the same). Lines keep their indentation; a
303
+ `.log-line` takes one tone — `--ok`, `--info`, `--warn`, `--err`, `--muted`,
304
+ the same five as `.pill` / `.status-dot` — and `--action` marks a "you clicked
305
+ X" line. Timeline cells (`.log-line__seq`, `__time`, `__source`, `__name`)
306
+ turn a line into aligned columns. A `<pre class="log-panel">` is a text-fed
307
+ log; `data-empty` shows while a panel is empty. Also `.mono`, `.check-list`
308
+ (options one per line), `td.is-highlight`, `.display-sliders` and
309
+ `.css-readout`. The playground's `examples-widgets.html` shows them all.
291
310
  - **Theme switch** (top right) cycles light → dark → auto and sets
292
311
  `data-theme` on `<html>` — the same signal components react to via
293
312
  `:host-context([data-theme])`, so every example page is a dark-mode smoke
@@ -347,8 +366,12 @@ kmwc check-release --tag t latest ⇔ release version, rc ⇔ -rcN, version n
347
366
  kmwc new web-<name> scaffold a component (--prefix, --title, --tag, --dir, --port, --minimal, --register)
348
367
  --adopt: scaffold an ALREADY registered component (its port/prefix) to migrate into
349
368
  kmwc readme [--check] regenerate this README's catalogue (devkit repo only)
369
+ kmwc <command> --help that command's usage — never runs the command
350
370
  ```
351
371
 
372
+ An unknown option is an error (exit 2), never ignored — a typo can't run a
373
+ command with its defaults.
374
+
352
375
  Generated files start with a `GENERATED by kmwc sync` header. Repo-specific
353
376
  make targets go in `Makefile.local`, which the generated Makefile includes at
354
377
  the end. Per-developer overrides (`DOCKER_RUNNER`, `IMAGE_PORT`) go in
@@ -360,6 +383,9 @@ and the e2e suite before building and publishing.
360
383
  base-variables, color-scheme, readme-structure, custom-elements-manifest,
361
384
  responsive-overlay, publish-command, example-pages) with `bash`, passing `kmwc.prefix`, and
362
385
  reports each `FAIL` as an error pointing at the matching `.checks.md` entry.
386
+ A `library` runs only naming-conventions, readme-structure, publish-command
387
+ and example-pages; the others check what a custom element ships (its CSS,
388
+ manifest, overlay).
363
389
  When the rulebook isn't checked out (or there's no bash) the suites are
364
390
  skipped with a warning — the rulebook stays the single source of truth; the
365
391
  devkit never copies its rules.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keenmate/web-components-devkit",
3
- "version": "1.0.0-rc04",
3
+ "version": "1.0.0-rc06",
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",
package/src/cli/args.js CHANGED
@@ -1,16 +1,39 @@
1
- /** Tiny flag parser: --flag, --key value, --key=value, positionals. */
2
- export function parseArgs(args) {
1
+ /**
2
+ * Flag parser: --flag, --key value, --key=value, positionals. `flagSpec` names
3
+ * every flag a command takes — `booleans` never take a value, `values` always
4
+ * do. Anything else is an error: an ignored typo (or `--help` before it was
5
+ * handled) used to run the command for real.
6
+ *
7
+ * @param {string[]} args
8
+ * @param {{ booleans?: string[], values?: string[] }} [flagSpec]
9
+ */
10
+ export function parseArgs(args, flagSpec = {}) {
11
+ const booleanFlags = new Set(flagSpec.booleans || []);
12
+ const valueFlags = new Set(['root', ...(flagSpec.values || [])]);
3
13
  const out = { _: [] };
4
14
  for (let i = 0; i < args.length; i++) {
5
- const a = args[i];
6
- if (!a.startsWith('--')) {
7
- out._.push(a);
15
+ const arg = args[i];
16
+ if (!arg.startsWith('-') || arg === '-') {
17
+ out._.push(arg);
8
18
  continue;
9
19
  }
10
- const [key, inline] = a.slice(2).split(/=(.*)/s);
11
- if (inline !== undefined) out[key] = inline;
12
- else if (args[i + 1] && !args[i + 1].startsWith('--')) out[key] = args[++i];
13
- else out[key] = true;
20
+ if (!arg.startsWith('--')) throw new Error(`unknown option "${arg}"`);
21
+ const [key, inline] = arg.slice(2).split(/=(.*)/s);
22
+ if (booleanFlags.has(key)) {
23
+ if (inline !== undefined) throw new Error(`--${key} takes no value`);
24
+ out[key] = true;
25
+ } else if (valueFlags.has(key)) {
26
+ if (inline !== undefined) out[key] = inline;
27
+ else if (args[i + 1] !== undefined && !args[i + 1].startsWith('--')) out[key] = args[++i];
28
+ else throw new Error(`--${key} needs a value`);
29
+ } else {
30
+ throw new Error(`unknown option "--${key}"`);
31
+ }
14
32
  }
15
33
  return out;
16
34
  }
35
+
36
+ /** True when the arguments ask for help (`--help` or `-h` anywhere). */
37
+ export function checkIsHelpRequested(args) {
38
+ return args.some((arg) => arg === '--help' || arg === '-h');
39
+ }
@@ -68,6 +68,10 @@ export function checkManifestVariables({ cssText, manifest, prefix, internalVari
68
68
  export async function run(args) {
69
69
  const config = getKmwcConfig(args.root);
70
70
  const isStrict = !!args.strict;
71
+ if (!config.cssVariables.isEnabled) {
72
+ console.log('kmwc check-vars: off (kmwc.cssVariables is false) — nothing to check.');
73
+ return 0;
74
+ }
71
75
  const manifestPath = resolve(config.root, config.cssVariables.manifest);
72
76
  if (!existsSync(manifestPath)) {
73
77
  console.log(`kmwc check-vars: no ${config.cssVariables.manifest} — nothing to check.`);
@@ -25,21 +25,33 @@ const LEGACY_PAGE_REFERENCES = ['examples-shared.css', 'examples-chapter-nav.js'
25
25
  const VITE_CONFIG_FILES = ['vite.config.ts', 'vite.config.mjs', 'vite.config.js'];
26
26
  const PLAYWRIGHT_CONFIG_FILES = ['playwright.config.ts', 'playwright.config.mjs', 'playwright.config.js'];
27
27
 
28
- /** Rulebook suites in the rulebook's canonical reading order; the prefix is passed where the script takes one. */
28
+ /**
29
+ * Rulebook suites in the rulebook's canonical reading order; the prefix is
30
+ * passed where the script takes one. `isComponentOnly` suites check what a
31
+ * custom element ships (its CSS, manifest, overlay) — a `library` (registry
32
+ * kind, or kmwc.kind) skips them.
33
+ */
29
34
  export const RULEBOOK_SUITES = [
30
- { name: 'component-structure', isPrefixed: false },
31
- { name: 'naming-conventions', isPrefixed: true },
32
- { name: 'css-structure', isPrefixed: true },
33
- { name: 'theme-container', isPrefixed: true },
34
- { name: 'base-variables', isPrefixed: true },
35
- { name: 'color-scheme', isPrefixed: true },
36
- { name: 'readme-structure', isPrefixed: false },
37
- { name: 'custom-elements-manifest', isPrefixed: false },
38
- { name: 'responsive-overlay', isPrefixed: true },
39
- { name: 'publish-command', isPrefixed: false },
40
- { name: 'example-pages', isPrefixed: false },
35
+ { name: 'component-structure', isPrefixed: false, isComponentOnly: true },
36
+ { name: 'naming-conventions', isPrefixed: true, isComponentOnly: false },
37
+ { name: 'css-structure', isPrefixed: true, isComponentOnly: true },
38
+ { name: 'theme-container', isPrefixed: true, isComponentOnly: true },
39
+ { name: 'base-variables', isPrefixed: true, isComponentOnly: true },
40
+ { name: 'color-scheme', isPrefixed: true, isComponentOnly: true },
41
+ { name: 'readme-structure', isPrefixed: false, isComponentOnly: false },
42
+ { name: 'custom-elements-manifest', isPrefixed: false, isComponentOnly: true },
43
+ { name: 'responsive-overlay', isPrefixed: true, isComponentOnly: true },
44
+ { name: 'publish-command', isPrefixed: false, isComponentOnly: false },
45
+ { name: 'example-pages', isPrefixed: false, isComponentOnly: false },
41
46
  ];
42
47
 
48
+ const LIBRARY_KIND = 'library';
49
+
50
+ /** The suites that apply to a repo of `kind`. Pure — exported for tests. */
51
+ export function getRulebookSuites(kind) {
52
+ return RULEBOOK_SUITES.filter((suite) => kind !== LIBRARY_KIND || !suite.isComponentOnly);
53
+ }
54
+
43
55
  function getMajorVersion(versionRange) {
44
56
  const match = String(versionRange || '').match(/(\d+)/);
45
57
  return match ? Number(match[1]) : null;
@@ -85,7 +97,7 @@ export function checkComponent(config, doctorConfig = {}) {
85
97
  } else {
86
98
  addPass('config', 'package.json#kmwc present');
87
99
  if (!config.packageJson.kmwc.title) addWarning('config', 'kmwc.title not set (falls back to a title-cased id)', 'set kmwc.title');
88
- if (!config.prefix) addWarning('config', 'kmwc.prefix not set — rulebook suites run without a CSS prefix', 'set kmwc.prefix to the prefix reserved in base-variables.md');
100
+ if (!config.prefix && config.kind !== LIBRARY_KIND) addWarning('config', 'kmwc.prefix not set — rulebook suites run without a CSS prefix', 'set kmwc.prefix to the prefix reserved in base-variables.md');
89
101
  if (!config.packageJson.kmwc.ports?.dev) addWarning('config', 'kmwc.ports.dev not set (falls back to 5173)', 'take the port from the devkit components.json');
90
102
  }
91
103
 
@@ -248,7 +260,12 @@ function checkRulebook(config, { addPass, addWarning, addError }) {
248
260
  return;
249
261
  }
250
262
  const skippedSuites = new Set(config.rulebook.skippedChecks);
251
- for (const suite of RULEBOOK_SUITES) {
263
+ const suites = getRulebookSuites(config.kind);
264
+ if (suites.length < RULEBOOK_SUITES.length) {
265
+ const componentOnlySuiteNames = RULEBOOK_SUITES.filter((suite) => !suites.includes(suite)).map((suite) => suite.name);
266
+ addPass('rulebook', `${config.kind}: component-only suites not run (${componentOnlySuiteNames.join(', ')})`);
267
+ }
268
+ for (const suite of suites) {
252
269
  if (skippedSuites.has(suite.name)) continue;
253
270
  if (!existsSync(join(config.rulebook.path, `${suite.name}.checks.sh`))) continue;
254
271
  const suiteResult = runRulebookSuite(config, suite);
@@ -280,7 +297,7 @@ export async function run(args) {
280
297
  }
281
298
 
282
299
  const LEVEL_ICONS = { pass: '✓', warning: '⚠', error: '✗' };
283
- console.log(`kmwc doctor — ${config.packageName} (${config.root})\n`);
300
+ console.log(`kmwc doctor — ${config.packageName}${config.kind === 'component' ? '' : ` [${config.kind}]`} (${config.root})\n`);
284
301
  for (const result of results) {
285
302
  if (result.level === 'pass' && !args.verbose) continue;
286
303
  console.log(`${LEVEL_ICONS[result.level]} [${result.checkName}] ${result.message}${result.fixHint ? `\n → ${result.fixHint}` : ''}`);
package/src/cli/kmwc.js CHANGED
@@ -8,59 +8,97 @@
8
8
  * kmwc check-release --tag <tag> gate a publish (version ⇔ dist-tag, not yet on npm)
9
9
  * kmwc new <name> [options] scaffold a new component repo next to the devkit
10
10
  * kmwc readme [--check] regenerate the devkit README catalogue (devkit repo only)
11
+ * kmwc <command> --help that command's usage (never runs it)
11
12
  *
12
13
  * All commands act on the current directory (override with --root <dir>).
13
- * Each command module exports `run(args)` returning the process exit code.
14
+ * Each command module exports `run(args)` returning the process exit code; an
15
+ * unknown option is an error (exit 2), never ignored.
14
16
  */
15
17
  import { argv, exit } from 'node:process';
16
- import { parseArgs } from './args.js';
18
+ import { parseArgs, checkIsHelpRequested } from './args.js';
17
19
 
18
- const COMMAND_LOADERS = {
19
- sync: () => import('./commands/sync.js'),
20
- doctor: () => import('./commands/doctor.js'),
21
- 'check-vars': () => import('./commands/check-vars.js'),
22
- 'check-release': () => import('./commands/check-release.js'),
23
- new: () => import('./commands/new.js'),
24
- readme: () => import('./commands/readme.js'),
25
- };
26
-
27
- function renderUsage() {
28
- console.log(`kmwc — Keenmate web-components devkit
29
-
30
- Usage: kmwc <command> [options]
31
-
32
- sync [--check] Regenerate Dockerfile, nginx.conf, Makefile, .dockerignore,
20
+ /** Every command: its module, the flags it takes and its usage text. */
21
+ const COMMANDS = {
22
+ sync: {
23
+ load: () => import('./commands/sync.js'),
24
+ flags: { booleans: ['check'] },
25
+ usage: `sync [--check] Regenerate Dockerfile, nginx.conf, Makefile, .dockerignore,
33
26
  the CLAUDE.md devkit block, .gitignore entries and missing
34
- standard scripts. --check: report drift, write nothing.
35
- doctor [--json] Check this repo against the devkit standard and the
27
+ standard scripts. --check: report drift, write nothing.`,
28
+ },
29
+ doctor: {
30
+ load: () => import('./commands/doctor.js'),
31
+ flags: { booleans: ['json', 'verbose', 'no-rulebook', 'no-pack'] },
32
+ usage: `doctor [--json] Check this repo against the devkit standard and the
36
33
  BlissFramework rulebook (*.checks.sh), and that npm pack ships no
37
- dev-only files. --verbose, --no-rulebook, --no-pack.
38
- check-vars [--strict] Validate component-variables.manifest.json against the CSS.
39
- check-release --tag <t> Gate a publish: latest ⇔ release version, rc ⇔ -rcN,
40
- version not on npm yet. --offline skips the npm lookup.
41
- new <name> Scaffold a new component (e.g. \`kmwc new web-colorpicker --prefix cpk\`).
42
- --title "…" --tag … --prefix … --dir … --port … --register
34
+ dev-only files. --verbose, --no-rulebook, --no-pack.`,
35
+ },
36
+ 'check-vars': {
37
+ load: () => import('./commands/check-vars.js'),
38
+ flags: { booleans: ['strict'] },
39
+ usage: `check-vars [--strict] Validate component-variables.manifest.json against the CSS.`,
40
+ },
41
+ 'check-release': {
42
+ load: () => import('./commands/check-release.js'),
43
+ flags: { booleans: ['offline'], values: ['tag'] },
44
+ usage: `check-release --tag <t> Gate a publish: latest ⇔ release version, rc ⇔ -rcN,
45
+ version not on npm yet. --offline skips the npm lookup.`,
46
+ },
47
+ new: {
48
+ load: () => import('./commands/new.js'),
49
+ flags: { booleans: ['register', 'minimal', 'adopt', 'offline'], values: ['title', 'tagline', 'tag', 'prefix', 'dir', 'port'] },
50
+ usage: `new <name> Scaffold a new component (e.g. \`kmwc new web-colorpicker --prefix cpk\`).
51
+ --title "…" --tagline "…" --tag … --prefix … --dir … --port …
52
+ --register --offline
43
53
  --minimal: empty skeleton (no inputs/events) instead of the sample
44
- --adopt: an already registered component (migration scratch scaffold)
45
- readme [--check] Regenerate the README catalogue from components.json (devkit repo).
54
+ --adopt: an already registered component (migration scratch scaffold)`,
55
+ },
56
+ readme: {
57
+ load: () => import('./commands/readme.js'),
58
+ flags: { booleans: ['check'] },
59
+ usage: `readme [--check] Regenerate the README catalogue from components.json (devkit repo).`,
60
+ },
61
+ };
62
+
63
+ const GLOBAL_USAGE = `Global: --root <dir> Act on <dir> instead of the current directory.
64
+ --help, -h Show the usage (of one command: kmwc <command> --help).`;
46
65
 
47
- Global: --root <dir> Act on <dir> instead of the current directory.`);
66
+ function renderUsage(commandName) {
67
+ if (commandName) {
68
+ console.log(`Usage: kmwc ${COMMANDS[commandName].usage}\n\n${GLOBAL_USAGE}`);
69
+ return;
70
+ }
71
+ const commandLines = Object.values(COMMANDS).map((command) => ` ${command.usage}`).join('\n');
72
+ console.log(`kmwc — Keenmate web-components devkit\n\nUsage: kmwc <command> [options]\n\n${commandLines}\n\n${GLOBAL_USAGE}`);
48
73
  }
49
74
 
50
75
  async function main() {
51
76
  const [commandName, ...commandArgs] = argv.slice(2);
52
77
  if (!commandName || ['help', '--help', '-h'].includes(commandName)) {
53
- renderUsage();
78
+ renderUsage(COMMANDS[commandArgs[0]] ? commandArgs[0] : undefined);
54
79
  return 0;
55
80
  }
56
- const loadCommand = COMMAND_LOADERS[commandName];
57
- if (!loadCommand) {
81
+ const command = COMMANDS[commandName];
82
+ if (!command) {
58
83
  console.error(`kmwc: unknown command "${commandName}"\n`);
59
84
  renderUsage();
60
85
  return 2;
61
86
  }
62
- const command = await loadCommand();
63
- return (await command.run(parseArgs(commandArgs))) ?? 0;
87
+ // Help never runs the command (`kmwc sync --help` once overwrote a Makefile).
88
+ if (checkIsHelpRequested(commandArgs)) {
89
+ renderUsage(commandName);
90
+ return 0;
91
+ }
92
+ let args;
93
+ try {
94
+ args = parseArgs(commandArgs, command.flags);
95
+ } catch (error) {
96
+ console.error(`kmwc ${commandName}: ${error.message}\n`);
97
+ renderUsage(commandName);
98
+ return 2;
99
+ }
100
+ const commandModule = await command.load();
101
+ return (await commandModule.run(args)) ?? 0;
64
102
  }
65
103
 
66
104
  main().then(
@@ -69,14 +69,14 @@ const hasAnyFile = (root, fileNames) => fileNames.some((fileName) => existsSync(
69
69
  export function getStandardScripts(config) {
70
70
  const scripts = {
71
71
  dev: 'vite',
72
- build: 'vite build',
72
+ ...(config.library.shouldBuild ? { build: 'vite build' } : {}),
73
73
  'build:examples': 'vite build --mode examples',
74
74
  'preview:examples': 'vite preview --mode examples',
75
75
  'clean:dist': generateRemoveCommand(['dist']),
76
76
  clean: generateRemoveCommand(['dist', config.examples.outDir]),
77
77
  doctor: 'kmwc doctor',
78
78
  };
79
- if (existsSync(join(config.root, config.cssVariables.manifest))) scripts['check:vars'] = 'kmwc check-vars';
79
+ if (config.cssVariables.isEnabled && existsSync(join(config.root, config.cssVariables.manifest))) scripts['check:vars'] = 'kmwc check-vars';
80
80
  if (existsSync(join(config.root, CEM_CONFIG_FILE))) scripts.analyze = 'cem analyze';
81
81
  if (hasAnyFile(config.root, PLAYWRIGHT_CONFIG_FILES)) {
82
82
  Object.assign(scripts, {
package/src/config.d.ts CHANGED
@@ -10,7 +10,12 @@ export interface KmwcPackageConfig {
10
10
  tag?: string;
11
11
  /** Reserved CSS prefix (base-variables.md "Existing prefixes"), e.g. `ms`. */
12
12
  prefix?: string;
13
- library?: {
13
+ /** Registry kind; defaults to the components.json entry's kind (else `component`). A `library` runs only the doctor checks that apply to it. */
14
+ kind?: 'component' | 'library' | 'showcase' | 'docs' | 'test';
15
+ /** The library build (`vite build`). `false` = the repo builds its package itself or ships none: plain `vite build` refuses to run, only `--mode examples` builds. */
16
+ library?: false | {
17
+ /** `false` = same as `library: false`, keeping `entry` for the example pages. */
18
+ shouldBuild?: boolean;
14
19
  entry?: string;
15
20
  /** UMD global name. */
16
21
  globalName?: string;
@@ -51,14 +56,15 @@ export interface KmwcPackageConfig {
51
56
  packageJsonPaths?: string[];
52
57
  installCommand?: string;
53
58
  };
54
- cssVariables?: {
59
+ /** `false` (or `checkMode: 'off'`): no variables manifest check — sync adds no `check:vars`, `kmwc check-vars` skips. */
60
+ cssVariables?: false | {
55
61
  manifest?: string;
56
62
  /** Directories (stylesheets scanned) or explicit files (any type, e.g. a built bundle). */
57
63
  sources?: string[];
58
64
  /** Prefixed names (without `--`) that are runtime internals, not theming knobs. */
59
65
  internalVariables?: string[];
60
66
  /** `declared`: manifest ⇔ `--x:` declarations; `consumed`: manifest ⇔ `var(--x)` reads. */
61
- checkMode?: 'declared' | 'consumed';
67
+ checkMode?: 'declared' | 'consumed' | 'off';
62
68
  };
63
69
  rulebook?: {
64
70
  /** Path to BlissFramework `guidelines/web-components` (env KMWC_RULEBOOK_PATH wins). */
@@ -75,6 +81,7 @@ export interface KmwcConfig {
75
81
  packageJson: Record<string, any>;
76
82
  id: string;
77
83
  packageName: string;
84
+ kind: string;
78
85
  version: string;
79
86
  title: string;
80
87
  tagline: string;
@@ -85,6 +92,7 @@ export interface KmwcConfig {
85
92
  author: string;
86
93
  license: string;
87
94
  library: {
95
+ shouldBuild: boolean;
88
96
  entry: string;
89
97
  globalName: string;
90
98
  fileName: string;
@@ -118,7 +126,8 @@ export interface KmwcConfig {
118
126
  manifest: string;
119
127
  sources: string[];
120
128
  internalVariables: string[];
121
- checkMode: 'declared' | 'consumed';
129
+ checkMode: 'declared' | 'consumed' | 'off';
130
+ isEnabled: boolean;
122
131
  };
123
132
  rulebook: { path: string; skippedChecks: string[] };
124
133
  sync: { skippedFiles: string[] };
@@ -137,6 +146,7 @@ export const PORT_RANGES: Array<{ kinds: string[]; firstBlock: number; lastBlock
137
146
  export const PREVIEW_PORT_OFFSET: number;
138
147
  export const IMAGE_PORT_OFFSET: number;
139
148
  export function getPortsForBlock(portBlock: number): { dev: number; preview: number; image: number };
149
+ export function getRegistryKind(id: string, registry?: { components: Array<{ id: string; kind: string }> }): string;
140
150
  export function getPortRange(kind: string): { kinds: string[]; firstBlock: number; lastBlock: number };
141
151
  export function getNextPortBlock(registry?: { components: Array<Record<string, any>> }, kind?: string): number;
142
152
  export function getPeerModule(root: string, moduleId: string): any;
package/src/config.js CHANGED
@@ -139,6 +139,11 @@ export function getRegistry() {
139
139
  return getJsonFile(join(DEVKIT_ROOT, 'components.json'));
140
140
  }
141
141
 
142
+ /** The registry kind of `id` (`component`, `library` …); `component` when it isn't registered. */
143
+ export function getRegistryKind(id, registry = getRegistry()) {
144
+ return registry.components.find((entry) => entry.id === id)?.kind || 'component';
145
+ }
146
+
142
147
  /** Strip the npm scope: `@keenmate/web-multiselect` → `web-multiselect`. */
143
148
  export function getUnscopedName(packageName) {
144
149
  return String(packageName || '').replace(/^@[^/]+\//, '');
@@ -182,13 +187,16 @@ export function getKmwcConfig(root = process.cwd()) {
182
187
  export function mapKmwcConfig(packageJson, root = process.cwd()) {
183
188
  const packageConfig = packageJson.kmwc || {};
184
189
  const id = packageConfig.id || getUnscopedName(packageJson.name) || 'component';
185
- const library = packageConfig.library || {};
190
+ // `library: false` — the repo builds its package itself (tsc …) or ships none;
191
+ // Vite serves and builds only the examples site.
192
+ const library = packageConfig.library === false ? { shouldBuild: false } : packageConfig.library || {};
186
193
  const examples = packageConfig.examples || {};
187
194
  const ports = packageConfig.ports || {};
188
195
  const image = packageConfig.image || {};
189
196
  const theme = packageConfig.theme || {};
190
197
  const analytics = packageConfig.analytics || {};
191
- const cssVariables = packageConfig.cssVariables || {};
198
+ // `cssVariables: false` — no variables manifest to check (no check:vars script).
199
+ const cssVariables = packageConfig.cssVariables === false ? { checkMode: 'off' } : packageConfig.cssVariables || {};
192
200
  const rulebook = packageConfig.rulebook || {};
193
201
  const devPort = ports.dev ?? 5173;
194
202
 
@@ -198,6 +206,7 @@ export function mapKmwcConfig(packageJson, root = process.cwd()) {
198
206
  packageJson,
199
207
  id,
200
208
  packageName: packageJson.name || id,
209
+ kind: packageConfig.kind || getRegistryKind(id),
201
210
  version: packageJson.version || '0.1.0',
202
211
  title: packageConfig.title || mapToTitleCase(id),
203
212
  tagline: packageConfig.tagline || packageJson.description || '',
@@ -208,6 +217,7 @@ export function mapKmwcConfig(packageJson, root = process.cwd()) {
208
217
  author: typeof packageJson.author === 'string' ? packageJson.author : packageJson.author?.name || 'Keenmate s.r.o.',
209
218
  license: packageJson.license || 'MIT',
210
219
  library: {
220
+ shouldBuild: library.shouldBuild ?? true,
211
221
  entry: library.entry || 'src/index.ts',
212
222
  globalName: library.globalName || mapToPascalCase(id),
213
223
  fileName: library.fileName || id,
@@ -254,6 +264,7 @@ export function mapKmwcConfig(packageJson, root = process.cwd()) {
254
264
  sources: cssVariables.sources || ['src/css'],
255
265
  internalVariables: cssVariables.internalVariables || [],
256
266
  checkMode: cssVariables.checkMode || 'declared',
267
+ isEnabled: cssVariables.checkMode !== 'off',
257
268
  },
258
269
  rulebook: {
259
270
  path:
@@ -120,6 +120,9 @@
120
120
  font-size: 0.9em;
121
121
  color: var(--kmwc-accent-color);
122
122
  font-family: var(--kmwc-mono-font-family);
123
+ /* A long identifier has no break opportunity — break it anywhere
124
+ rather than widen the page on a phone. */
125
+ overflow-wrap: anywhere;
123
126
  }
124
127
 
125
128
  pre code,
@@ -130,6 +133,32 @@
130
133
  border-radius: 0;
131
134
  color: inherit;
132
135
  font-size: inherit;
136
+ overflow-wrap: inherit;
137
+ }
138
+
139
+ /* A key in a keyboard map — the spotlight's key-hint look. */
140
+ kbd {
141
+ display: inline-block;
142
+ padding: 0.1rem 0.35rem;
143
+ background-color: var(--kmwc-elevated-bg);
144
+ color: var(--kmwc-text-color-1);
145
+ border: 1px solid var(--kmwc-border-color);
146
+ border-bottom-width: 2px;
147
+ border-radius: var(--kmwc-radius-sm);
148
+ font-family: var(--kmwc-font-family);
149
+ font-weight: 600;
150
+ font-size: 0.8em;
151
+ line-height: 1.2;
152
+ white-space: nowrap;
153
+ }
154
+
155
+ /* A search match — the spotlight's match highlight. */
156
+ mark {
157
+ background-color: var(--kmwc-accent-light-bg-hover);
158
+ color: var(--kmwc-accent-color);
159
+ font-weight: 600;
160
+ padding: 0 2px;
161
+ border-radius: var(--kmwc-radius-sm);
133
162
  }
134
163
 
135
164
  /* ==========================================================================
@@ -214,10 +243,32 @@
214
243
  color: var(--kmwc-text-color-3);
215
244
  }
216
245
 
246
+ /* Monospace text that isn't code (folded values, code-unit dumps); on a
247
+ textarea, a JSON / config editor. */
248
+ .mono {
249
+ font-family: var(--kmwc-mono-font-family);
250
+ }
251
+
252
+ textarea.mono {
253
+ font-size: 0.8125rem;
254
+ }
255
+
217
256
  .mt-1 {
218
257
  margin-top: 1rem;
219
258
  }
220
259
 
260
+ /* Push an item to the end of a flex row (`.controls`, `.btn-group` …). */
261
+ .push-end {
262
+ margin-inline-start: auto;
263
+ }
264
+
265
+ /* Field widths: a native field is ~20 characters wide; these size a short
266
+ one (a page size, a caption) without an inline style. */
267
+ .w-xs { width: 4rem; max-width: 100%; }
268
+ .w-sm { width: 6rem; max-width: 100%; }
269
+ .w-md { width: 9rem; max-width: 100%; }
270
+ .w-lg { width: 12rem; max-width: 100%; }
271
+
221
272
  @media (max-width: 768px) {
222
273
  .grid,
223
274
  .grid-2,
@@ -86,6 +86,34 @@
86
86
  margin-bottom: 0.75rem;
87
87
  }
88
88
 
89
+ /* Check list — checkbox / radio options one per line; a long label wraps
90
+ beside its box, and anything after the text (a .pill) follows it. */
91
+ .check-list {
92
+ display: grid;
93
+ gap: 0.35rem;
94
+ margin-bottom: 1rem;
95
+ }
96
+
97
+ .check-list > label {
98
+ display: flex;
99
+ align-items: flex-start;
100
+ gap: 0.5rem;
101
+ cursor: pointer;
102
+ }
103
+
104
+ .check-list > label > input {
105
+ flex: none;
106
+ margin-top: calc((1lh - var(--kmwc-checkbox-size)) / 2);
107
+ }
108
+
109
+ .check-list > label > :not(input) {
110
+ min-width: 0;
111
+ }
112
+
113
+ .check-list code {
114
+ overflow-wrap: anywhere;
115
+ }
116
+
89
117
  /* Control group — a label stacked over an input/select; rows of them wrap
90
118
  via `.control-row`. */
91
119
  .control-row {
@@ -74,7 +74,11 @@
74
74
  cursor: not-allowed;
75
75
  }
76
76
 
77
+ /* A number is short: 7rem holds six digits and the spin buttons (a
78
+ .form-group still stretches it to the row; .w-* sizes it). */
77
79
  :where(input[type="number"]):not(:where(.kmwc-native, .kmwc-native *)) {
80
+ width: 7rem;
81
+ max-width: 100%;
78
82
  padding-inline-end: 0.25rem; /* room for the spin buttons */
79
83
  }
80
84
 
@@ -9,7 +9,7 @@
9
9
  .table-striped zebra rows
10
10
  .table-compact tighter cells; controls inside shrink with them
11
11
  .table-full full width
12
- tr.is-highlight an accent-tinted row
12
+ tr.is-highlight an accent-tinted row (td.is-highlight: one cell)
13
13
  .num a right-aligned, tabular-figure cell
14
14
 
15
15
  The base is zero-specificity (:where()), so a page or component rule wins;
@@ -103,7 +103,8 @@
103
103
  }
104
104
 
105
105
  tr.is-highlight,
106
- tr.is-highlight:hover {
106
+ tr.is-highlight:hover,
107
+ :is(th, td).is-highlight {
107
108
  background: var(--kmwc-accent-light-bg);
108
109
  }
109
110
 
@@ -113,6 +113,11 @@
113
113
  --kmwc-console-text-color: #e2e8f0;
114
114
  --kmwc-console-muted-color: #94a3b8;
115
115
  --kmwc-console-highlight-color: #68d391;
116
+ /* Log-line tones on the console (always dark, so fixed light tints). */
117
+ --kmwc-console-ok-color: #68d391;
118
+ --kmwc-console-info-color: #7dd3fc;
119
+ --kmwc-console-warn-color: #fcd34d;
120
+ --kmwc-console-err-color: #fca5a5;
116
121
  --kmwc-inline-code-bg: light-dark(#edf2f7, #2b2b2b);
117
122
 
118
123
  /* ======================================================================
@@ -7,11 +7,32 @@
7
7
  /* ==========================================================================
8
8
  LOG PANELS
9
9
  ==========================================================================
10
- `.log-panel` — monospace, scrollable box for streaming output; light by
11
- default, `.log-panel--dark` for the console look; height cap via
12
- `--log-max-height`. `.event-log` is the legacy dark console with
13
- `.event-*` entries — prefer `.log-panel` for new pages. */
14
- .log-panel {
10
+ `.log-panel` — monospace, scrollable box for streaming output. Light by
11
+ default (it follows the page theme); `.log-panel--dark` for a console
12
+ stream (always dark). `--log-max-height` caps the height (`none` lets it
13
+ grow).
14
+
15
+ Lines: any child keeps its line breaks and indentation. `.log-line` takes
16
+ one tone — `--ok`, `--info`, `--warn`, `--err`, `--muted` (the pill
17
+ tones) — and `--action` marks a "you clicked X" line. Timeline cells
18
+ `.log-line__seq`, `__time`, `__source`, `__name` turn a line into columns;
19
+ the element after them is the detail. A panel fed by textContent is a
20
+ `<pre class="log-panel">`. `data-empty="(nothing yet)"` shows while the
21
+ panel is empty.
22
+
23
+ Legacy markup: `.event-log` is a `.log-panel--dark`, an `.event-entry`
24
+ holding `.event-time` / `.event-name` is a timeline line with those two
25
+ cells. Prefer the `.log-*` classes in new pages. */
26
+ .log-panel,
27
+ .event-log {
28
+ --kmwc-log-ok-color: var(--kmwc-success-color);
29
+ --kmwc-log-info-color: var(--kmwc-info-color);
30
+ --kmwc-log-warn-color: var(--kmwc-warning-color);
31
+ --kmwc-log-err-color: var(--kmwc-danger-color);
32
+ /* text-color-3 is 2.8:1 on the light panel — under WCAG AA's 4.5. */
33
+ --kmwc-log-muted-color: var(--kmwc-text-color-2);
34
+ --kmwc-log-name-color: var(--kmwc-accent-color);
35
+ --kmwc-log-rule-color: var(--kmwc-border-color);
15
36
  font-family: var(--kmwc-mono-font-family);
16
37
  font-size: 0.85rem;
17
38
  line-height: 1.5;
@@ -24,31 +45,108 @@
24
45
  border: 1px solid var(--kmwc-border-color);
25
46
  }
26
47
 
27
- .log-panel--dark {
48
+ .log-panel--dark,
49
+ .event-log {
50
+ --kmwc-log-ok-color: var(--kmwc-console-ok-color);
51
+ --kmwc-log-info-color: var(--kmwc-console-info-color);
52
+ --kmwc-log-warn-color: var(--kmwc-console-warn-color);
53
+ --kmwc-log-err-color: var(--kmwc-console-err-color);
54
+ --kmwc-log-muted-color: var(--kmwc-console-muted-color);
55
+ --kmwc-log-name-color: var(--kmwc-console-highlight-color);
56
+ --kmwc-log-rule-color: color-mix(in srgb, var(--kmwc-console-text-color) 12%, transparent);
28
57
  background: var(--kmwc-console-bg);
29
58
  color: var(--kmwc-console-text-color);
30
59
  border-color: transparent;
31
60
  }
32
61
 
33
- .event-log {
34
- background: var(--kmwc-console-bg);
35
- color: var(--kmwc-console-muted-color);
36
- padding: 1rem;
37
- border-radius: var(--kmwc-radius-md);
38
- font-family: var(--kmwc-mono-font-family);
39
- font-size: 0.8rem;
40
- max-height: 200px;
41
- overflow-y: auto;
42
- line-height: 1.5;
62
+ pre.log-panel {
63
+ margin: 0;
64
+ white-space: pre-wrap;
65
+ overflow-wrap: anywhere;
43
66
  }
44
67
 
45
- .event-log .event-entry {
46
- padding: 0.15rem 0;
47
- border-bottom: 1px solid color-mix(in srgb, var(--kmwc-console-text-color) 6%, transparent);
68
+ :is(.log-panel, .event-log) > * {
69
+ white-space: pre-wrap;
70
+ overflow-wrap: anywhere;
48
71
  }
49
72
 
50
- .event-log .event-time { color: var(--kmwc-console-muted-color); }
51
- .event-log .event-name { color: var(--kmwc-console-highlight-color); font-weight: 600; }
73
+ :is(.log-panel, .event-log)[data-empty]:empty::before {
74
+ content: attr(data-empty);
75
+ color: var(--kmwc-log-muted-color);
76
+ }
77
+
78
+ .log-line,
79
+ .event-entry {
80
+ color: var(--kmwc-log-tone, currentColor);
81
+ }
82
+
83
+ .log-line--ok { --kmwc-log-tone: var(--kmwc-log-ok-color); }
84
+ .log-line--info { --kmwc-log-tone: var(--kmwc-log-info-color); }
85
+ .log-line--warn { --kmwc-log-tone: var(--kmwc-log-warn-color); }
86
+ .log-line--err { --kmwc-log-tone: var(--kmwc-log-err-color); }
87
+ .log-line--muted { --kmwc-log-tone: var(--kmwc-log-muted-color); }
88
+
89
+ .log-line--action {
90
+ font-weight: 600;
91
+ }
92
+
93
+ .log-line--action:not(:first-child) {
94
+ margin-top: 0.35rem;
95
+ }
96
+
97
+ /* Timeline: a line with cells is a row; the cells keep their width and the
98
+ detail after them wraps in its own column. When the cells leave the detail
99
+ less than ~14rem (a phone, a long source name) it drops under them at full
100
+ width instead of being squeezed to a letter per line. */
101
+ .log-line:has(> :is(.log-line__seq, .log-line__time, .log-line__source, .log-line__name)),
102
+ .event-entry:has(> :is(.event-time, .event-name)) {
103
+ display: flex;
104
+ column-gap: 0.6rem;
105
+ padding: 0.1rem 0;
106
+ border-bottom: 1px solid var(--kmwc-log-rule-color);
107
+ }
108
+
109
+ .log-line:has(> :is(.log-line__seq, .log-line__time, .log-line__source, .log-line__name)) {
110
+ flex-wrap: wrap;
111
+ }
112
+
113
+ .log-line:has(> :is(.log-line__seq, .log-line__time, .log-line__source, .log-line__name)) > :last-child:not(.log-line__name) {
114
+ flex: 1 1 14rem;
115
+ min-width: 0;
116
+ }
117
+
118
+ .log-line__seq,
119
+ .log-line__time,
120
+ .log-line__source,
121
+ .log-line__name,
122
+ .event-time,
123
+ .event-name {
124
+ flex: none;
125
+ max-width: 100%;
126
+ }
127
+
128
+ .log-line__seq,
129
+ .log-line__time,
130
+ .event-time {
131
+ text-align: end;
132
+ font-variant-numeric: tabular-nums;
133
+ }
134
+
135
+ .log-line__seq { min-width: 2.2rem; }
136
+ .log-line__time { min-width: 4.6rem; }
137
+
138
+ .log-line__seq,
139
+ .log-line__time,
140
+ .log-line__source,
141
+ .event-time {
142
+ color: var(--kmwc-log-muted-color);
143
+ }
144
+
145
+ .log-line__name,
146
+ .event-name {
147
+ color: var(--kmwc-log-tone, var(--kmwc-log-name-color));
148
+ font-weight: 600;
149
+ }
52
150
 
53
151
  /* ==========================================================================
54
152
  CSS-VARIABLE DEMO HELPERS
@@ -89,7 +187,7 @@
89
187
  display: flex;
90
188
  flex-wrap: wrap;
91
189
  gap: 0.4rem;
92
- margin: 0.25rem 0 1rem;
190
+ margin: 0.75rem 0 1rem; /* it follows the demo it measures */
93
191
  }
94
192
 
95
193
  .css-readout code { font-size: 0.78rem; }
@@ -98,7 +196,9 @@
98
196
 
99
197
  /* ==========================================================================
100
198
  STATUS DOTS & PILLS
101
- ========================================================================== */
199
+ ==========================================================================
200
+ One tone set with the log lines: --ok, --info, --warn, --err, --muted
201
+ (= the neutral default). */
102
202
  .status-dot {
103
203
  --kmwc-status-color: var(--kmwc-neutral-color);
104
204
  display: inline-block;
@@ -113,6 +213,8 @@
113
213
  .status-dot--ok { --kmwc-status-color: var(--kmwc-success-color); }
114
214
  .status-dot--warn { --kmwc-status-color: var(--kmwc-warning-color); }
115
215
  .status-dot--err { --kmwc-status-color: var(--kmwc-danger-color); }
216
+ .status-dot--info { --kmwc-status-color: var(--kmwc-info-color); }
217
+ .status-dot--muted { --kmwc-status-color: var(--kmwc-neutral-color); }
116
218
 
117
219
  .pill {
118
220
  --kmwc-status-color: var(--kmwc-neutral-color);
@@ -130,4 +232,6 @@
130
232
  .pill--ok { --kmwc-status-color: var(--kmwc-success-color); }
131
233
  .pill--warn { --kmwc-status-color: var(--kmwc-warning-color); }
132
234
  .pill--err { --kmwc-status-color: var(--kmwc-danger-color); }
235
+ .pill--info { --kmwc-status-color: var(--kmwc-info-color); }
236
+ .pill--muted { --kmwc-status-color: var(--kmwc-neutral-color); }
133
237
  }
package/src/vite/index.js CHANGED
@@ -199,9 +199,16 @@ export function defineComponentConfig(presetConfig = {}) {
199
199
  const config = getKmwcConfig(presetConfig.root);
200
200
  // The component's own vite — the one running this config (see loadPeerModule).
201
201
  const { mergeConfig, searchForWorkspaceRoot } = await loadPeerModule(config.root, 'vite');
202
- const library = { ...config.library, ...(presetConfig.library || {}) };
202
+ const library = { ...config.library, ...(presetConfig.library === false ? { shouldBuild: false } : presetConfig.library || {}) };
203
203
  const isExamplesMode = env.mode === EXAMPLES_MODE;
204
204
  const isLibraryBuild = env.command === 'build' && !isExamplesMode;
205
+ if (isLibraryBuild && !library.shouldBuild) {
206
+ // The library build empties dist/ — it would wipe a package the repo builds itself.
207
+ throw new Error(
208
+ `${config.id}: kmwc.library is false — plain \`vite build\` has nothing to build. ` +
209
+ 'Build the package with its own script (npm run build), the examples site with `vite build --mode examples`.'
210
+ );
211
+ }
205
212
 
206
213
  const plugins = [createExamplesPlugin({ root: config.root }), ...(presetConfig.plugins || [])];
207
214
 
@@ -25,7 +25,11 @@ locally — every local fork is how they drifted apart.
25
25
  controls (inputs, selects, checkboxes, radios, `role="switch"`, buttons) and
26
26
  tables are already styled by the kit (`.reference-table`, `.table-striped`,
27
27
  `.table-compact`, …) — don't restyle them per page; `kmwc-native` opts an
28
- element out. If a second component needs a pattern, it belongs in the devkit.
28
+ element out. Log lines take the kit tones (`.log-line--ok|info|warn|err|muted`,
29
+ `--action`, timeline cells `.log-line__time` …) — don't colour them per page.
30
+ `<kbd>`, `<mark>`, field widths (`.w-xs` … `.w-lg`) and `.push-end` are kit
31
+ too — no inline styles for them.
32
+ If a second component needs a pattern, it belongs in the devkit.
29
33
  - **Example pages are fragments:** no `<html>`/`<head>`/`<header>` — start with
30
34
  `<meta name="kmwc:title" content="…">`, the devkit layout adds the rest. Shared text
31
35
  goes in `partials/` via `<kmwc-include>`; show code with `<kmwc-demo>` (the live