@keenmate/web-components-devkit 1.0.0-rc08 → 1.0.0-rc10
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 +75 -0
- package/README.md +34 -15
- package/package.json +2 -1
- package/src/cli/commands/check-pages.js +83 -15
- package/src/cli/commands/check-vars.js +18 -11
- package/src/cli/commands/doctor.js +85 -20
- package/src/cli/repo-checks.js +34 -4
- package/src/examples/css/controls.css +2 -2
- package/src/examples/css/tokens.css +9 -3
- package/src/vite/page-composer.js +7 -2
- package/templates/claude-block.md +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,80 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.0-rc10] - 2026-11-05 [PUBLISHED]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Doctor runs the variable-manifest check.** `kmwc check-vars` only ran inside `npm run build`, so a
|
|
8
|
+
red manifest went unseen until a build: web-treeview's `check:vars` failed on core's
|
|
9
|
+
non-contract `--base-accent-color` while doctor passed. Doctor now reports the same verdict
|
|
10
|
+
(`vars`): missing / dead `--base-*` and dead `--<prefix>-*` entries are errors, undocumented
|
|
11
|
+
`--<prefix>-*` a warning. `loadManifestVerdict()` is shared by both.
|
|
12
|
+
- **Opt-outs for markup that is right on purpose:**
|
|
13
|
+
- an element whose own `style="…"` sets fixed colours carries `data-kmwc-fixed-colors` (doctor's
|
|
14
|
+
colour check). Markup a render callback draws inside a shadow root can't be styled by page CSS,
|
|
15
|
+
so inline is the only way there (web-treeview's context-menu role chips);
|
|
16
|
+
- `data-kmwc-undefined` on an element a demo leaves undefined on purpose — check-pages no longer
|
|
17
|
+
reports it (core's PG01 registers its element on a button press). The "never defined" problem
|
|
18
|
+
names the attribute;
|
|
19
|
+
- `<!-- kmwc-other-ports: why -->` in a doc that names other repos' ports on purpose (a dated
|
|
20
|
+
plan, a cross-repo review) — doctor's port scan skips it.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Doctor: the loader check tells a collision from a rename.** On a core that still draws
|
|
25
|
+
`<prefix>-loader`, CSS that styles both classes has its own element called `<prefix>__loader`
|
|
26
|
+
(web-multiselect's loading-state row). That was reported as a rename done too early (error); it is
|
|
27
|
+
now a warning that core's loader takes the name — and those rules — with the bump, so rename the
|
|
28
|
+
component's element in the same change.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **Fragment pages: `<link>` / `<style>` text inside an attribute value stayed put.** The composer
|
|
33
|
+
moved a `<link>` it found in a tooltip (`title="…the <link> in <head>"`) into `<head>` as an empty
|
|
34
|
+
element, and the tooltip lost its text (web-multiselect). It now reads whole start tags, quoted
|
|
35
|
+
attribute values included, and moves only real `<link>` / `<style>` elements.
|
|
36
|
+
- **Inline code in light mode** read at ~3.3:1 — the raw accent on the pale chip. The text keeps the
|
|
37
|
+
accent hue at most 0.5 lightness (≥ 4.5:1 for every accent the spec tries, light and dark).
|
|
38
|
+
- **Doctor: a rulebook suite that never ran counted as a pass.** Only exit 2 was read as "could
|
|
39
|
+
not run"; any other exit without `FAIL` lines passed. On Windows a bare `bash` can be WSL's, which
|
|
40
|
+
can't read `C:/…` paths — every suite exited 127 and doctor reported the rulebook green (found when
|
|
41
|
+
`make publish-rc` ran the tests from PowerShell). Doctor now runs Git for Windows' bash (from PATH
|
|
42
|
+
or the install root; `KMWC_BASH` overrides) and warns on any exit but 0, or 1 with `FAIL` lines.
|
|
43
|
+
|
|
44
|
+
### Internal
|
|
45
|
+
|
|
46
|
+
- **Tests on the critical paths.** Doctor end to end on fixture repos (a healthy repo is clean, each
|
|
47
|
+
broken one gets its finding; `doctor.js` 26% → 98% of lines); a real `vite build` of the playground
|
|
48
|
+
must emit exactly what doctor checks package.json against, with the UMD global and every export
|
|
49
|
+
`index.d.ts` declares; `kmwc new` (full and `--minimal`) scaffolds a repo doctor finds clean;
|
|
50
|
+
`kmwc sync` rewrites only what it owns and `--check` writes nothing; `check-release` keeps an rc off
|
|
51
|
+
`latest`. Composer cases for single-quoted values, `<style>` text and a `>` inside an attribute.
|
|
52
|
+
- **`npm run test:e2e:coverage`** (maintainer) — the dev-server e2e run with line coverage of the
|
|
53
|
+
kit's browser code (`src/examples/*.js`, 84%), snapshot before every navigation.
|
|
54
|
+
- **README:** a sentence about `data-kmwc-undefined` had landed inside the rc09 What's New bullet;
|
|
55
|
+
it's back in the check-pages docs.
|
|
56
|
+
|
|
57
|
+
## [1.0.0-rc09] - 2026-11-04 [PUBLISHED]
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- **check-pages: page CSS that only matches inside a shadow root.** A page's own rules (its `<style>`
|
|
62
|
+
blocks and `examples-local.css`) are matched against the loaded page. A rule whose element (the last
|
|
63
|
+
compound, pseudo-classes dropped) exists nowhere in the document but inside a component's shadow root
|
|
64
|
+
never applies — page CSS stops at the shadow boundary. web-treeview styled the rows its
|
|
65
|
+
`renderNodeCallback` draws that way (custom-layout: `.file-icon`, `.node-badge`, `.badge-new` …;
|
|
66
|
+
responsive: `.isco-row`, `.dev-badge` …, 18 rules); against its pre-fix pages the check reports
|
|
67
|
+
exactly those. Checked once per page (first width).
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **The grey secondary button in dark mode** (`button.secondary` / `.btn-secondary`) had white text on
|
|
72
|
+
`#9ca3af` at 2.54:1. That grey sits just under the on-colour formula's 0.34 luminance switch, so the
|
|
73
|
+
formula picked white. It showed on 9 web-treeview pages ("Clear", "Reset", "Move up" …) and on every
|
|
74
|
+
other site's grey buttons. The button now has its own fill, `--kmwc-neutral-fill-color`
|
|
75
|
+
(`#6b7280` light / `#4b5563` dark, white text ≥ 4.5:1 in both, hovered too), and
|
|
76
|
+
`--kmwc-neutral-fill-color-hover`. Muted dots and pills keep the light `--kmwc-neutral-color`.
|
|
77
|
+
|
|
3
78
|
## [1.0.0-rc08] - 2026-11-04 [PUBLISHED]
|
|
4
79
|
|
|
5
80
|
### Added
|
package/README.md
CHANGED
|
@@ -19,18 +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-
|
|
22
|
+
## What's New in v1.0.0-rc10
|
|
23
23
|
|
|
24
|
-
- **Doctor —
|
|
25
|
-
- **Doctor —
|
|
24
|
+
- **Doctor — runs the variable-manifest check** — `kmwc check-vars` only ran inside `npm run build`, so a red manifest stayed invisible until someone built. During the devkit pass, web-treeview's `check:vars` failed on core's `--base-accent-color` (a name outside the base-variables contract) while `kmwc doctor` reported nothing. Doctor now reports the same verdict under `vars`: `--base-*` reads missing from the manifest, manifest entries the CSS no longer reads, and dead `--<prefix>-*` entries are errors; undocumented `--<prefix>-*` variables are a warning. Both commands share one function, `loadManifestVerdict()`, so they can't disagree.
|
|
25
|
+
- **Doctor and check-pages — opt-outs for markup that is right on purpose** — The migrations of web-treeview, web-multiselect and web-components-core hit three findings that could never pass. Markup a render callback draws inside a shadow root can only be coloured inline, so an element whose `style="…"` sets fixed colours on purpose now carries `data-kmwc-fixed-colors` (treeview's context-menu role chips). An element a demo leaves undefined on purpose carries `data-kmwc-undefined`, and check-pages skips it (core's PG01 registers its element on a button press); the "never defined" message names the attribute. A doc that names other repos' ports on purpose, such as a dated plan or a cross-repo review, carries `<!-- kmwc-other-ports: why -->`, and doctor's port scan skips it.
|
|
26
|
+
- **Doctor — the loader check tells a collision from a rename** — web-multiselect has its own loading-state element called `.ms__loader`, next to core rc17's `.ms-loader`. Doctor read it as a rename done before the core bump and reported an error. On a core that still draws `<prefix>-loader`, CSS that styles both classes now gets a warning instead: from core 6d6b768 on, core's default loader takes that class name and picks up the component's rules. The fix is to rename the component's own element in the same change as the core bump.
|
|
27
|
+
- **Fragment pages — text inside attribute values stays put** — The page composer moves a page's `<link>` and `<style>` elements into `<head>`, but it also matched them inside attribute values. web-multiselect's tooltip `title="…the <link> in <head>"` turned into an empty `<link>` in the head, and the tooltip lost its text; a `<style>` written inside an attribute would even have been applied to the whole page. The composer now reads whole start tags, quoted values included, and moves only real elements.
|
|
28
|
+
- **Examples kit — inline code readable in light mode** — Inline `code` used the raw accent colour on its pale chip, which read at about 3.3:1 for the default accent. The text keeps the accent's hue at no more than 0.5 lightness. The e2e spec checks five accents in both themes for at least 4.5:1.
|
|
29
|
+
- **Devkit tests — the critical paths** — The suite now runs a real library build of the playground and checks that the emitted files, the UMD global and every export `index.d.ts` declares are exactly what package.json and doctor expect. It also checks that `kmwc new` scaffolds a repo doctor finds clean, that `kmwc sync` never rewrites a local file or an existing script, and that `check-release` keeps an rc off `latest`. Doctor itself is tested end to end on fixture repos.
|
|
26
30
|
|
|
27
|
-
## What's New in v1.0.0-
|
|
31
|
+
## What's New in v1.0.0-rc09
|
|
28
32
|
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
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.
|
|
33
|
+
- **check-pages — page CSS that can't reach into a shadow root** — web-treeview's custom-layout and responsive pages styled the rows their `renderNodeCallback` draws with page CSS (`.file-icon`, `.node-badge`, `.isco-row`, `.dev-badge` …, 18 rules). Those rows live in the tree's shadow root, and page CSS stops at the shadow boundary, so none of the rules ever applied. Nothing in the source shows where a render callback's output ends up, so this needs the loaded page. `kmwc check-pages` now reads the page's own rules (its `<style>` blocks and `examples-local.css`) and finds what each one styles: the last part of the selector, without pseudo-classes. It reports a rule when that element exists nowhere in the document but does exist inside a component's shadow root, and names the component. Run against treeview's pages from before its fix, it lists exactly those 18 rules. The fix is a CSS variable, `::part`, or the component's styles hook (`getCustomStylesCallback`).
|
|
34
|
+
- **Examples kit — the grey secondary button in dark mode** — `button.secondary` / `.btn-secondary` had white text on `#9ca3af` in dark mode, at 2.54:1. That grey sits just under the 0.34 luminance switch in the formula that picks a button's text colour, so the formula chose white. It showed on 9 web-treeview pages ("Clear", "Reset", "Move up" …) and on every other component site's grey buttons. The button now has its own fill, `--kmwc-neutral-fill-color` (`#6b7280` light / `#4b5563` dark), plus a hover token, so white text stays at 4.5:1 or better in both themes, hovered too. Muted status dots and pills keep the lighter `--kmwc-neutral-color`. An e2e spec checks the contrast in both themes.
|
|
34
35
|
|
|
35
36
|
## Catalogue
|
|
36
37
|
|
|
@@ -39,7 +40,7 @@ them in the same shape: `@keenmate/web-components-devkit`.
|
|
|
39
40
|
|
|
40
41
|
| Component | Package | Prefix | Dev port | Links | Devkit | About |
|
|
41
42
|
|---|---|---|---:|---|:---:|---|
|
|
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` |
|
|
43
|
+
| **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… |
|
|
43
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 |
|
|
44
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… |
|
|
45
46
|
| **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,… |
|
|
@@ -365,7 +366,7 @@ kmwc doctor [--json] devkit checks + package contents + the rulebook's
|
|
|
365
366
|
kmwc check-vars [--strict] component-variables.manifest.json ⇔ CSS
|
|
366
367
|
kmwc check-pages [--url u] every example page in Chromium at 1440 / 390 / 320px: page and console
|
|
367
368
|
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
|
+
viewport, page CSS that only matches inside a shadow root. Starts the dev server unless --url (e.g. the built site);
|
|
369
370
|
--widths 1440,390,320
|
|
370
371
|
kmwc check-release --tag t latest ⇔ release version, rc ⇔ -rcN, version not yet on npm
|
|
371
372
|
kmwc new web-<name> scaffold a component (--prefix, --title, --tag, --dir, --port, --minimal, --register)
|
|
@@ -388,6 +389,9 @@ and the e2e suite before building and publishing.
|
|
|
388
389
|
base-variables, color-scheme, readme-structure, custom-elements-manifest,
|
|
389
390
|
responsive-overlay, publish-command, example-pages) with `bash`, passing `kmwc.prefix`, and
|
|
390
391
|
reports each `FAIL` as an error pointing at the matching `.checks.md` entry.
|
|
392
|
+
On Windows that is Git for Windows' bash (a bare `bash` may be WSL's, which can't
|
|
393
|
+
read `C:/` paths); `KMWC_BASH` overrides it. A suite that exits with anything but 0,
|
|
394
|
+
or 1 with `FAIL` lines, never ran — a warning, never a pass.
|
|
391
395
|
A `library` runs only naming-conventions, readme-structure, publish-command
|
|
392
396
|
and example-pages; the others check what a custom element ships (its CSS,
|
|
393
397
|
manifest, overlay).
|
|
@@ -408,7 +412,9 @@ generated files:
|
|
|
408
412
|
silently.
|
|
409
413
|
- **Ports** — no script pins a `--port` of its own, the repo's configs, Makefile
|
|
410
414
|
and docs stay inside its registry block, and no sibling checkout (a registry
|
|
411
|
-
entry at `../<id>`) listens on a port inside it.
|
|
415
|
+
entry at `../<id>`) listens on a port inside it. A doc that names other repos'
|
|
416
|
+
ports on purpose (a dated plan, a cross-repo review) carries
|
|
417
|
+
`<!-- kmwc-other-ports: why -->` and isn't scanned.
|
|
412
418
|
- **Analytics** — no other repo uses the same Plausible site id.
|
|
413
419
|
- **Page CSS** — a page `<style>` or `examples-local.css` rule on a kit-owned
|
|
414
420
|
selector (`.kbd`, `kbd`, `mark`, `.pill`, `.card` …) and single-declaration
|
|
@@ -418,16 +424,28 @@ generated files:
|
|
|
418
424
|
it stays put when the theme switch flips the kit to dark. `var()` fallbacks and
|
|
419
425
|
`light-dark()` pairs follow the theme. Fixed colours on purpose (a theme gallery, a
|
|
420
426
|
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.
|
|
427
|
+
gets a `/* kmwc-fixed-colors: why */` comment right before it. An element whose
|
|
428
|
+
`style="…"` sets them on purpose carries the same attribute — markup a render
|
|
429
|
+
callback draws inside a shadow root can't be reached by page CSS at all.
|
|
422
430
|
- **Loader class** — the component CSS styles the class the *installed* core gives
|
|
423
431
|
its default loader (`<prefix>__loader` from core 6d6b768 on, `<prefix>-loader`
|
|
424
432
|
before). Renaming the selector on one side of the core bump leaves the loader
|
|
425
433
|
unstyled, and a VALIDATION-NOTES `## C-NC-8` would hide the old class from the
|
|
426
|
-
rulebook.
|
|
434
|
+
rulebook. On a core that still draws `<prefix>-loader`, CSS that styles both
|
|
435
|
+
classes has an element of its own called `<prefix>__loader`: a warning, because
|
|
436
|
+
core's loader takes that name (and those rules) with the bump.
|
|
437
|
+
- **Variable manifest** — the same verdict as `kmwc check-vars`, which otherwise
|
|
438
|
+
only runs inside `npm run build`: `--base-*` reads and the manifest agree, no
|
|
439
|
+
dead `--<prefix>-*` entries (errors), no undocumented ones (warning).
|
|
427
440
|
|
|
428
441
|
`kmwc check-pages` is the browser half: it loads every example page and reports
|
|
429
442
|
what doctor can't see in the source — a script error, a 404, a fixed width that
|
|
430
|
-
pushes a phone sideways
|
|
443
|
+
pushes a phone sideways, and page CSS that can never apply: a rule (in the page's
|
|
444
|
+
`<style>` or `examples-local.css`) whose element exists only inside a component's
|
|
445
|
+
shadow root, such as the rows a `renderNodeCallback` draws. Page CSS stops at the
|
|
446
|
+
shadow boundary — style those through a CSS variable, `::part` or the component's
|
|
447
|
+
styles hook (`getCustomStylesCallback`). An element a demo leaves undefined on
|
|
448
|
+
purpose (registered on a button press) carries `data-kmwc-undefined`.
|
|
431
449
|
|
|
432
450
|
**Package contents.** Doctor runs `npm pack --dry-run` and fails on anything
|
|
433
451
|
dev-only that would reach a consumer's `node_modules`: `CLAUDE.md` at any
|
|
@@ -523,6 +541,7 @@ registry edits), and move the component's own code into that layout with
|
|
|
523
541
|
npm install
|
|
524
542
|
npm test # unit tests (config, registry, templates, sync, checks, intake, plugin, kit)
|
|
525
543
|
npm run test:e2e # playground: the kit + scaffold in a real browser, dev server AND built site
|
|
544
|
+
npm run test:e2e:coverage # the same specs on the dev server + line coverage of the kit's browser code
|
|
526
545
|
npm run readme # regenerate the catalogue
|
|
527
546
|
npm run check:spotlight-mirror # what changed in web-commandpalette since the spotlight was aligned
|
|
528
547
|
make dev # the playground's dev server
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keenmate/web-components-devkit",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-rc10",
|
|
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",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"readme:check": "node src/cli/kmwc.js readme --check",
|
|
48
48
|
"test": "node --test \"test/**/*.test.js\"",
|
|
49
49
|
"test:e2e": "cd playground && npx playwright test",
|
|
50
|
+
"test:e2e:coverage": "node scripts/e2e-coverage.js",
|
|
50
51
|
"check:contract": "base-contract-check src templates playground",
|
|
51
52
|
"check:spotlight-mirror": "node scripts/check-spotlight-mirror.js",
|
|
52
53
|
"playground": "cd playground && vite",
|
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* kmwc check-pages — load every example page in a real browser and report what
|
|
3
3
|
* a reader would hit: page errors, console errors, failed requests, custom
|
|
4
|
-
* elements that never get defined,
|
|
5
|
-
* width or a long word that pushes a phone sideways)
|
|
4
|
+
* elements that never get defined, a page wider than the viewport (a fixed
|
|
5
|
+
* width or a long word that pushes a phone sideways), and page CSS that can
|
|
6
|
+
* never apply because what it targets lives only inside a shadow root (classes
|
|
7
|
+
* a render callback puts into the component).
|
|
6
8
|
*
|
|
7
9
|
* Runs against the repo's own dev server (started here on kmwc.ports.dev), or a
|
|
8
10
|
* server you already have (--url, e.g. the built site on the preview port).
|
|
9
11
|
* Widths default to desktop + two phones. Uses the component's own
|
|
10
12
|
* @playwright/test (see getPeerModule) — `npx playwright install chromium` once.
|
|
11
13
|
*/
|
|
12
|
-
import { existsSync } from 'node:fs';
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
15
|
import { join } from 'node:path';
|
|
14
16
|
import { getKmwcConfig, getPeerModule, loadPeerModule } from '../../config.js';
|
|
15
17
|
import { getExamplePages } from '../../vite/index.js';
|
|
18
|
+
import { getRuleSelectors } from '../repo-checks.js';
|
|
16
19
|
|
|
17
20
|
export const DEFAULT_WIDTHS = [1440, 390, 320];
|
|
18
21
|
const VIEWPORT_HEIGHT = 900;
|
|
19
22
|
const SETTLE_MS = 600;
|
|
20
23
|
const VITE_CONFIG_FILES = ['vite.config.ts', 'vite.config.mts', 'vite.config.mjs', 'vite.config.js'];
|
|
21
24
|
const MAX_OFFENDERS = 3;
|
|
25
|
+
const LOCAL_STYLESHEET_FILE = 'examples-local.css';
|
|
26
|
+
// An element a demo leaves undefined on purpose (registered on a button press) opts out.
|
|
27
|
+
export const UNDEFINED_ON_PURPOSE_ATTRIBUTE = 'data-kmwc-undefined';
|
|
22
28
|
// Requests a page doesn't control (favicon probes, analytics in a built site).
|
|
23
29
|
const IGNORED_REQUEST_PATTERN = /favicon\.ico|stats\.keenmate\.services|plausible/;
|
|
24
30
|
|
|
@@ -30,13 +36,69 @@ export function parseWidths(widthsText) {
|
|
|
30
36
|
return widths;
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The element a selector styles, as a selector of its own: the last compound with
|
|
41
|
+
* pseudo-classes / -elements dropped (`.tree .badge:hover::after` → `.badge`), or
|
|
42
|
+
* null when there's no class / id / attribute to look for (`:root`, `div`, `from`). Pure.
|
|
43
|
+
*/
|
|
44
|
+
export function getSelectorSubject(selector) {
|
|
45
|
+
let depth = 0;
|
|
46
|
+
let start = 0;
|
|
47
|
+
for (let index = 0; index < selector.length; index++) {
|
|
48
|
+
const character = selector[index];
|
|
49
|
+
if (character === '(' || character === '[') depth++;
|
|
50
|
+
else if (character === ')' || character === ']') depth--;
|
|
51
|
+
else if (depth === 0 && /[\s>+~]/.test(character)) start = index + 1;
|
|
52
|
+
}
|
|
53
|
+
const subject = selector.slice(start).replace(/::?[\w-]+(\([^()]*\))?/g, '').trim();
|
|
54
|
+
return /[.#[]/.test(subject) ? subject : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A page's own CSS rules (its `<style>` blocks + examples-local.css) → [{ selector, subject }]. */
|
|
58
|
+
export function loadPageRules(root, pagePath) {
|
|
59
|
+
const pageText = readFileSync(pagePath, 'utf8');
|
|
60
|
+
const localStylesheetPath = join(root, LOCAL_STYLESHEET_FILE);
|
|
61
|
+
const styleText = [
|
|
62
|
+
...[...pageText.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].map((match) => match[1]),
|
|
63
|
+
existsSync(localStylesheetPath) ? readFileSync(localStylesheetPath, 'utf8') : '',
|
|
64
|
+
].join('\n');
|
|
65
|
+
const selectors = [...new Set(getRuleSelectors(styleText))];
|
|
66
|
+
return selectors.map((selector) => ({ selector, subject: getSelectorSubject(selector) })).filter((rule) => rule.subject);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Runs in the page: page rules whose subject matches nothing in the document but
|
|
71
|
+
* something inside a shadow root → [{ selector, host }]. Page CSS stops at the
|
|
72
|
+
* shadow boundary, so such a rule never applies.
|
|
73
|
+
*/
|
|
74
|
+
export function findShadowOnlyRules(rules) {
|
|
75
|
+
const getShadowRoots = (root) =>
|
|
76
|
+
[...root.querySelectorAll('*')].filter((element) => element.shadowRoot).flatMap((element) => [element.shadowRoot, ...getShadowRoots(element.shadowRoot)]);
|
|
77
|
+
const shadowRoots = getShadowRoots(document);
|
|
78
|
+
const safeQuery = (root, selector) => {
|
|
79
|
+
try {
|
|
80
|
+
return root.querySelector(selector);
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
return rules.flatMap(({ selector, subject }) => {
|
|
86
|
+
if (safeQuery(document, subject)) return [];
|
|
87
|
+
const shadowRoot = shadowRoots.find((root) => safeQuery(root, subject));
|
|
88
|
+
return shadowRoot ? [{ selector, host: shadowRoot.host.localName }] : [];
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
33
92
|
/**
|
|
34
93
|
* 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
|
|
94
|
+
* @param {{ width: number, errors: string[], undefinedElements: string[], scrollWidth: number, clientWidth: number, offenders: string[], shadowOnlyRules?: { selector: string, host: string }[] }} pageReport
|
|
36
95
|
*/
|
|
37
96
|
export function getPageProblems(pageReport) {
|
|
38
97
|
const problems = [...pageReport.errors];
|
|
39
|
-
|
|
98
|
+
for (const { selector, host } of pageReport.shadowOnlyRules || []) {
|
|
99
|
+
problems.push(`page CSS "${selector}" only matches inside <${host}>'s shadow root — page styles can't reach it (use a CSS variable, ::part or the component's styles hook)`);
|
|
100
|
+
}
|
|
101
|
+
if (pageReport.undefinedElements.length) problems.push(`never defined: <${pageReport.undefinedElements.join('>, <')}> (left undefined on purpose? add ${UNDEFINED_ON_PURPOSE_ATTRIBUTE})`);
|
|
40
102
|
if (pageReport.scrollWidth > pageReport.clientWidth) {
|
|
41
103
|
const offenderText = pageReport.offenders.length ? ` — widest: ${pageReport.offenders.join(', ')}` : '';
|
|
42
104
|
problems.push(`page is ${pageReport.scrollWidth}px wide in a ${pageReport.clientWidth}px viewport${offenderText}`);
|
|
@@ -48,9 +110,10 @@ export function getPageProblems(pageReport) {
|
|
|
48
110
|
* Runs in the page: overflow, undefined custom elements. A culprit is the deepest
|
|
49
111
|
* element — or the text — past the right edge, outside anything that scrolls or
|
|
50
112
|
* 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.
|
|
113
|
+
* layers (a closed nav panel, a toast), which don't widen the page. Undefined elements
|
|
114
|
+
* carrying `undefinedAttribute` are left undefined on purpose and don't count.
|
|
52
115
|
*/
|
|
53
|
-
function inspectPage(maxOffenders) {
|
|
116
|
+
function inspectPage({ maxOffenders, undefinedAttribute }) {
|
|
54
117
|
const viewportWidth = document.documentElement.clientWidth;
|
|
55
118
|
// Shadow roots count: a component's own internals can widen the page too.
|
|
56
119
|
const getParent = (element) => element.parentElement || element.getRootNode().host || null;
|
|
@@ -97,7 +160,7 @@ function inspectPage(maxOffenders) {
|
|
|
97
160
|
offenders.push(`${describe(parent)} "${textNode.textContent.trim().slice(0, 40)}"`);
|
|
98
161
|
}
|
|
99
162
|
}
|
|
100
|
-
const undefinedElements = [...new Set([...document.querySelectorAll(
|
|
163
|
+
const undefinedElements = [...new Set([...document.querySelectorAll(`:not(:defined):not([${undefinedAttribute}])`)].map((element) => element.localName).filter((name) => name.includes('-')))];
|
|
101
164
|
return {
|
|
102
165
|
scrollWidth: document.documentElement.scrollWidth,
|
|
103
166
|
clientWidth: viewportWidth,
|
|
@@ -106,7 +169,7 @@ function inspectPage(maxOffenders) {
|
|
|
106
169
|
};
|
|
107
170
|
}
|
|
108
171
|
|
|
109
|
-
async function loadPageReport(browser, pageUrl, width) {
|
|
172
|
+
async function loadPageReport(browser, pageUrl, width, pageRules = []) {
|
|
110
173
|
const page = await browser.newPage({ viewport: { width, height: VIEWPORT_HEIGHT } });
|
|
111
174
|
const errors = [];
|
|
112
175
|
page.on('pageerror', (error) => errors.push(`page error: ${error.message.split('\n')[0]}`));
|
|
@@ -122,8 +185,9 @@ async function loadPageReport(browser, pageUrl, width) {
|
|
|
122
185
|
try {
|
|
123
186
|
await page.goto(pageUrl, { waitUntil: 'networkidle', timeout: 30_000 });
|
|
124
187
|
await page.waitForTimeout(SETTLE_MS);
|
|
125
|
-
const inspection = await page.evaluate(inspectPage, MAX_OFFENDERS);
|
|
126
|
-
|
|
188
|
+
const inspection = await page.evaluate(inspectPage, { maxOffenders: MAX_OFFENDERS, undefinedAttribute: UNDEFINED_ON_PURPOSE_ATTRIBUTE });
|
|
189
|
+
const shadowOnlyRules = pageRules.length ? await page.evaluate(findShadowOnlyRules, pageRules) : [];
|
|
190
|
+
return { width, errors: [...new Set(errors)], ...inspection, shadowOnlyRules };
|
|
127
191
|
} catch (error) {
|
|
128
192
|
return { width, errors: [...new Set([...errors, `load failed: ${error.message.split('\n')[0]}`])], scrollWidth: 0, clientWidth: 0, offenders: [], undefinedElements: [] };
|
|
129
193
|
} finally {
|
|
@@ -154,7 +218,8 @@ async function startDevServer(config) {
|
|
|
154
218
|
export async function run(args) {
|
|
155
219
|
const config = getKmwcConfig(args.root);
|
|
156
220
|
const widths = parseWidths(args.widths);
|
|
157
|
-
const
|
|
221
|
+
const pagePaths = Object.values(getExamplePages(config));
|
|
222
|
+
const pageFiles = pagePaths.map((pagePath) => pagePath.replace(/\\/g, '/').split('/').pop());
|
|
158
223
|
if (!pageFiles.length) {
|
|
159
224
|
console.log('kmwc check-pages: no example pages — nothing to check.');
|
|
160
225
|
return 0;
|
|
@@ -172,10 +237,13 @@ export async function run(args) {
|
|
|
172
237
|
let problemCount = 0;
|
|
173
238
|
try {
|
|
174
239
|
console.log(`kmwc check-pages — ${pageFiles.length} page(s) × ${widths.join(' / ')}px on ${baseUrl}\n`);
|
|
175
|
-
for (const pageFile of pageFiles) {
|
|
240
|
+
for (const [pageIndex, pageFile] of pageFiles.entries()) {
|
|
176
241
|
const pageProblems = [];
|
|
177
|
-
|
|
178
|
-
|
|
242
|
+
const pageRules = loadPageRules(config.root, pagePaths[pageIndex]);
|
|
243
|
+
for (const [widthIndex, width] of widths.entries()) {
|
|
244
|
+
// Shadow-only rules don't depend on the width: check them on the first load only.
|
|
245
|
+
const report = await loadPageReport(browser, `${baseUrl}/${pageFile}`, width, widthIndex === 0 ? pageRules : []);
|
|
246
|
+
for (const problem of getPageProblems(report)) pageProblems.push(`@${width}px ${problem}`);
|
|
179
247
|
}
|
|
180
248
|
problemCount += pageProblems.length;
|
|
181
249
|
console.log(`${pageProblems.length ? '✗' : '✓'} ${pageFile}${pageProblems.map((problem) => `\n ${problem}`).join('')}`);
|
|
@@ -77,18 +77,14 @@ export function checkManifestVariables({ cssText, manifest, prefix, internalVari
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
80
|
+
/**
|
|
81
|
+
* The repo's manifest checked against its CSS sources → { verdict, prefix }, or { skippedReason }
|
|
82
|
+
* when there's nothing to check. Shared by `kmwc check-vars` and doctor.
|
|
83
|
+
*/
|
|
84
|
+
export function loadManifestVerdict(config) {
|
|
85
|
+
if (!config.cssVariables.isEnabled) return { skippedReason: 'off (kmwc.cssVariables is false)' };
|
|
87
86
|
const manifestPath = resolve(config.root, config.cssVariables.manifest);
|
|
88
|
-
if (!existsSync(manifestPath)) {
|
|
89
|
-
console.log(`kmwc check-vars: no ${config.cssVariables.manifest} — nothing to check.`);
|
|
90
|
-
return 0;
|
|
91
|
-
}
|
|
87
|
+
if (!existsSync(manifestPath)) return { skippedReason: `no ${config.cssVariables.manifest}` };
|
|
92
88
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
93
89
|
const prefix = config.prefix || manifest.prefix;
|
|
94
90
|
if (!prefix) throw new Error('no variable prefix — set kmwc.prefix or "prefix" in the manifest');
|
|
@@ -101,6 +97,17 @@ export async function run(args) {
|
|
|
101
97
|
internalVariables: config.cssVariables.internalVariables,
|
|
102
98
|
checkMode: config.cssVariables.checkMode,
|
|
103
99
|
});
|
|
100
|
+
return { verdict, prefix };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function run(args) {
|
|
104
|
+
const config = getKmwcConfig(args.root);
|
|
105
|
+
const isStrict = !!args.strict;
|
|
106
|
+
const { verdict, prefix, skippedReason } = loadManifestVerdict(config);
|
|
107
|
+
if (skippedReason) {
|
|
108
|
+
console.log(`kmwc check-vars: ${skippedReason} — nothing to check.`);
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
104
111
|
|
|
105
112
|
const presenceWord = config.cssVariables.checkMode === 'consumed' ? 'consumed via var()' : 'declared or read';
|
|
106
113
|
const renderSection = (title, names) => `${title} (${names.length}):\n` + names.map((name) => ` --${name}`).join('\n');
|
|
@@ -19,6 +19,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
19
19
|
import { getKmwcConfig, getPortsForBlock, getRegistry, LEGACY_FILES, NON_PAGE_HTML_FILES, PACKAGE_LEAK_RULES, PACKAGE_FILES_EXCLUSIONS } from '../../config.js';
|
|
20
20
|
import { getSyncPlan, getScriptsPlan, CLAUDE_BLOCK_START } from '../sync-plan.js';
|
|
21
21
|
import { checkIsFragment } from '../../vite/page-composer.js';
|
|
22
|
+
import { loadManifestVerdict } from './check-vars.js';
|
|
22
23
|
import {
|
|
23
24
|
VALIDATION_NOTES_FILE,
|
|
24
25
|
loadAcceptedCheckIds,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
getPortBlockRange,
|
|
29
30
|
getScriptPortMismatches,
|
|
30
31
|
loadStalePortMentions,
|
|
32
|
+
OTHER_PORTS_MARKER,
|
|
31
33
|
loadSiblingPortCollisions,
|
|
32
34
|
loadOwnPortsOutsideBlock,
|
|
33
35
|
loadPlausibleSiteId,
|
|
@@ -38,6 +40,7 @@ import {
|
|
|
38
40
|
FIXED_COLORS_MARKER,
|
|
39
41
|
getHardcodedColors,
|
|
40
42
|
loadCoreLoader,
|
|
43
|
+
getLoaderClassVerdict,
|
|
41
44
|
loadStyledLoaderClasses,
|
|
42
45
|
} from '../repo-checks.js';
|
|
43
46
|
|
|
@@ -98,15 +101,40 @@ export function getSuiteFailures(suiteOutput) {
|
|
|
98
101
|
.map((match) => ({ checkId: match[1], reason: match[2].trim() }));
|
|
99
102
|
}
|
|
100
103
|
|
|
104
|
+
const GIT_BASH_RELATIVE_PATH = join('bin', 'bash.exe');
|
|
105
|
+
// PATH entries Git for Windows adds — each sits one or two levels under the install root.
|
|
106
|
+
const GIT_PATH_ENTRY_PATTERN = /^(.*[\\/]Git)[\\/](?:cmd|bin|usr[\\/]bin|mingw64[\\/]bin)[\\/]?$/i;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The bash that runs the rulebook scripts. On Windows a bare `bash` can resolve to WSL's
|
|
110
|
+
* (`System32\bash.exe`, `WindowsApps\bash.exe`), which can't read `C:/…` paths: every suite
|
|
111
|
+
* exits 127 and the rulebook never runs. So: KMWC_BASH when set, else Git for Windows'
|
|
112
|
+
* bash (found from PATH or the usual install roots), else `bash`. Exported for tests.
|
|
113
|
+
*/
|
|
114
|
+
export function getBashPath({ platform = process.platform, env = process.env, checkExists = existsSync } = {}) {
|
|
115
|
+
if (env.KMWC_BASH) return env.KMWC_BASH;
|
|
116
|
+
if (platform !== 'win32') return 'bash';
|
|
117
|
+
const pathEntries = (env.PATH || env.Path || '').split(';').filter(Boolean);
|
|
118
|
+
const gitRoots = [
|
|
119
|
+
...pathEntries.map((entry) => entry.match(GIT_PATH_ENTRY_PATTERN)?.[1]).filter(Boolean),
|
|
120
|
+
...[env.ProgramFiles, env.ProgramW6432, env.LOCALAPPDATA && join(env.LOCALAPPDATA, 'Programs')].filter(Boolean).map((directory) => join(directory, 'Git')),
|
|
121
|
+
];
|
|
122
|
+
return gitRoots.map((gitRoot) => join(gitRoot, GIT_BASH_RELATIVE_PATH)).find((bashPath) => checkExists(bashPath)) || 'bash';
|
|
123
|
+
}
|
|
124
|
+
|
|
101
125
|
function runRulebookSuite(config, suite) {
|
|
102
126
|
const scriptPath = join(config.rulebook.path, `${suite.name}.checks.sh`).replace(/\\/g, '/');
|
|
103
127
|
const scriptArgs = [scriptPath, config.root.replace(/\\/g, '/')];
|
|
104
128
|
if (suite.isPrefixed && config.prefix) scriptArgs.push(config.prefix);
|
|
105
|
-
return spawnSync(
|
|
129
|
+
return spawnSync(getBashPath(), scriptArgs, { encoding: 'utf8', env: { ...process.env, NO_COLOR: '1' } });
|
|
106
130
|
}
|
|
107
131
|
|
|
108
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Run every check; returns [{ level: 'pass'|'warning'|'error', checkName, message, fixHint? }].
|
|
134
|
+
* `doctorConfig.registry` replaces components.json (tests use a registry of fixture repos).
|
|
135
|
+
*/
|
|
109
136
|
export function checkComponent(config, doctorConfig = {}) {
|
|
137
|
+
const registry = doctorConfig.registry || getRegistry();
|
|
110
138
|
const results = [];
|
|
111
139
|
const addPass = (checkName, message) => results.push({ level: 'pass', checkName, message });
|
|
112
140
|
const addWarning = (checkName, message, fixHint) => results.push({ level: 'warning', checkName, message, fixHint });
|
|
@@ -240,7 +268,7 @@ export function checkComponent(config, doctorConfig = {}) {
|
|
|
240
268
|
}
|
|
241
269
|
|
|
242
270
|
// ── registry cross-check ─────────────────────────────────────────────────
|
|
243
|
-
const registryEntry =
|
|
271
|
+
const registryEntry = registry.components.find((component) => component.id === config.id);
|
|
244
272
|
if (!registryEntry) {
|
|
245
273
|
addWarning('registry', `"${config.id}" is not in the devkit components.json`, 'add an entry (README catalogue + port allocation)');
|
|
246
274
|
} else {
|
|
@@ -254,7 +282,7 @@ export function checkComponent(config, doctorConfig = {}) {
|
|
|
254
282
|
`set package.json#kmwc.ports to { "dev": ${registryPorts.dev}, "image": ${registryPorts.image} } (components.json port block ${registryEntry.port})`
|
|
255
283
|
);
|
|
256
284
|
} else addPass('registry', `ports match the registry (dev ${config.ports.dev}, image ${config.ports.image})`);
|
|
257
|
-
checkPorts(config, registryEntry.port, { addPass, addWarning, addError });
|
|
285
|
+
checkPorts(config, registryEntry.port, registry, { addPass, addWarning, addError });
|
|
258
286
|
}
|
|
259
287
|
if (registryEntry.prefix && config.prefix && registryEntry.prefix !== config.prefix) {
|
|
260
288
|
addError('registry', `kmwc.prefix "${config.prefix}" ≠ registry prefix "${registryEntry.prefix}"`, 'use the reserved prefix');
|
|
@@ -277,9 +305,12 @@ export function checkComponent(config, doctorConfig = {}) {
|
|
|
277
305
|
// ── core's default loader class ──────────────────────────────────────────
|
|
278
306
|
if (config.prefix && config.kind !== LIBRARY_KIND) checkLoaderClass(config, { addPass, addWarning, addError });
|
|
279
307
|
|
|
308
|
+
// ── variable manifest (what `kmwc check-vars` checks; it only runs in the build) ──
|
|
309
|
+
if (config.hasPackageConfig) checkVariableManifest(config, { addPass, addWarning, addError });
|
|
310
|
+
|
|
280
311
|
// ── analytics ────────────────────────────────────────────────────────────
|
|
281
312
|
const plausibleSiteId = loadPlausibleSiteId(config.root);
|
|
282
|
-
const sharingRepoIds = loadSharedPlausibleSiteIds(config,
|
|
313
|
+
const sharingRepoIds = loadSharedPlausibleSiteIds(config, registry, plausibleSiteId);
|
|
283
314
|
if (sharingRepoIds.length) {
|
|
284
315
|
addError(
|
|
285
316
|
'analytics',
|
|
@@ -298,7 +329,7 @@ export function checkComponent(config, doctorConfig = {}) {
|
|
|
298
329
|
}
|
|
299
330
|
|
|
300
331
|
/** Ports: script --port flags, own configs and docs outside the block, siblings inside it. */
|
|
301
|
-
function checkPorts(config, portBlock, { addPass, addWarning, addError }) {
|
|
332
|
+
function checkPorts(config, portBlock, registry, { addPass, addWarning, addError }) {
|
|
302
333
|
const [firstPort, lastPort] = getPortBlockRange(portBlock);
|
|
303
334
|
const blockLabel = `${firstPort}–${lastPort}`;
|
|
304
335
|
const scriptPortMismatches = getScriptPortMismatches(config.packageJson.scripts, config.ports);
|
|
@@ -307,7 +338,7 @@ function checkPorts(config, portBlock, { addPass, addWarning, addError }) {
|
|
|
307
338
|
}
|
|
308
339
|
const ownStrayPorts = loadOwnPortsOutsideBlock(config, portBlock).filter(({ file }) => !file.startsWith('package.json#scripts.'));
|
|
309
340
|
for (const { port, file } of ownStrayPorts) addError('ports', `${file} uses port ${port}, outside this repo's block ${blockLabel}`, 'take ports from kmwc.ports (the devkit presets do)');
|
|
310
|
-
const siblingCollisions = loadSiblingPortCollisions(config,
|
|
341
|
+
const siblingCollisions = loadSiblingPortCollisions(config, registry);
|
|
311
342
|
for (const { id, port, file } of siblingCollisions) {
|
|
312
343
|
addError('ports', `${id} uses port ${port} (${file}) — inside this repo's block ${blockLabel}`, `move ${id} to its own components.json block`);
|
|
313
344
|
}
|
|
@@ -316,7 +347,7 @@ function checkPorts(config, portBlock, { addPass, addWarning, addError }) {
|
|
|
316
347
|
addWarning(
|
|
317
348
|
'ports',
|
|
318
349
|
`docs mention ports outside ${blockLabel}: ${stalePortMentions.map(({ file, port }) => `${file} ${port}`).join(', ')}`,
|
|
319
|
-
`update them to ${getPortsForBlock(portBlock).dev} (dev) / ${getPortsForBlock(portBlock).image} (image)
|
|
350
|
+
`update them to ${getPortsForBlock(portBlock).dev} (dev) / ${getPortsForBlock(portBlock).image} (image); a doc that names other repos' ports on purpose gets <!-- ${OTHER_PORTS_MARKER}: why -->`
|
|
320
351
|
);
|
|
321
352
|
}
|
|
322
353
|
if (!scriptPortMismatches.length && !ownStrayPorts.length && !siblingCollisions.length && !stalePortMentions.length) {
|
|
@@ -330,13 +361,19 @@ function checkPorts(config, portBlock, { addPass, addWarning, addError }) {
|
|
|
330
361
|
* before (or after) the core bump leaves the default loader unstyled, and no rulebook
|
|
331
362
|
* check sees that (a VALIDATION-NOTES `## C-NC-8` even hides a leftover old class).
|
|
332
363
|
*/
|
|
333
|
-
function checkLoaderClass(config, { addPass, addError }) {
|
|
364
|
+
function checkLoaderClass(config, { addPass, addWarning, addError }) {
|
|
334
365
|
const coreLoader = loadCoreLoader(config.root);
|
|
335
366
|
if (!coreLoader?.separator) return;
|
|
336
367
|
const coreClassName = `${config.prefix}${coreLoader.separator}loader`;
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
|
|
368
|
+
const { isCoreClassStyled, renamedClasses, collidingClasses } = getLoaderClassVerdict(coreLoader.separator, loadStyledLoaderClasses(config.root, config.prefix));
|
|
369
|
+
for (const { file, className } of collidingClasses) {
|
|
370
|
+
addWarning(
|
|
371
|
+
'loader',
|
|
372
|
+
`${file} styles .${className} next to core's .${coreClassName} — from core 6d6b768 on, core draws its default loader as <span class="${className}"> and it picks up these rules`,
|
|
373
|
+
`rename the component's own .${className} element (e.g. .${config.prefix}__loading) in the same change as the core bump`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
for (const { file, className } of renamedClasses) {
|
|
340
377
|
addError(
|
|
341
378
|
'loader',
|
|
342
379
|
`${file} styles .${className}, but core ${coreLoader.version} draws the default loader as <span class="${coreClassName}"> — it renders unstyled`,
|
|
@@ -345,7 +382,30 @@ function checkLoaderClass(config, { addPass, addError }) {
|
|
|
345
382
|
: `keep .${coreClassName} until the component builds on a core that draws <prefix>__loader, then rename in the same change`
|
|
346
383
|
);
|
|
347
384
|
}
|
|
348
|
-
if (
|
|
385
|
+
if (isCoreClassStyled && !renamedClasses.length) addPass('loader', `CSS styles .${coreClassName}, the class core ${coreLoader.version} draws`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** The variable manifest matches the CSS — the same verdict as `kmwc check-vars` (undocumented = warning). */
|
|
389
|
+
function checkVariableManifest(config, { addPass, addWarning, addError }) {
|
|
390
|
+
let manifestVerdict;
|
|
391
|
+
try {
|
|
392
|
+
manifestVerdict = loadManifestVerdict(config);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
addError('vars', `kmwc check-vars can't run: ${error.message}`, 'fix kmwc.cssVariables (sources / manifest / prefix)');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const { verdict, prefix, skippedReason } = manifestVerdict;
|
|
398
|
+
if (skippedReason) return;
|
|
399
|
+
const fixHint = 'edit the manifest to match the CSS (npm run check:vars lists every name)';
|
|
400
|
+
const listNames = (names) => names.map((name) => `--${name}`).join(', ');
|
|
401
|
+
if (verdict.missingBaseVariables.length) addError('vars', `--base-* read in CSS but missing from the manifest: ${listNames(verdict.missingBaseVariables)}`, fixHint);
|
|
402
|
+
if (verdict.unusedBaseVariables.length) addError('vars', `--base-* in the manifest but not read in CSS: ${listNames(verdict.unusedBaseVariables)}`, fixHint);
|
|
403
|
+
if (verdict.unusedComponentVariables.length) addError('vars', `--${prefix}-* in the manifest but not in CSS: ${listNames(verdict.unusedComponentVariables)}`, fixHint);
|
|
404
|
+
if (verdict.undocumentedComponentVariables.length) {
|
|
405
|
+
addWarning('vars', `--${prefix}-* in CSS but missing from the manifest: ${listNames(verdict.undocumentedComponentVariables)}`, `${fixHint}; runtime internals go in kmwc.cssVariables.internalVariables`);
|
|
406
|
+
}
|
|
407
|
+
const isInSync = !['missingBaseVariables', 'unusedBaseVariables', 'unusedComponentVariables', 'undocumentedComponentVariables'].some((key) => verdict[key].length);
|
|
408
|
+
if (isInSync) addPass('vars', `variable manifest in sync — ${verdict.counts.base} base, ${verdict.counts.component} component vars`);
|
|
349
409
|
}
|
|
350
410
|
|
|
351
411
|
/** Packed paths that break PACKAGE_LEAK_RULES → [{ path, label }]. Pure — exported for tests. */
|
|
@@ -413,24 +473,29 @@ function checkRulebook(config, { addPass, addWarning, addError }) {
|
|
|
413
473
|
return;
|
|
414
474
|
}
|
|
415
475
|
const checkName = `rulebook:${suite.name}`;
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
476
|
+
const failures = getSuiteFailures(suiteResult.stdout);
|
|
477
|
+
// A suite passes with exit 0 and fails with exit 1 + FAIL lines. Anything else (2 = usage,
|
|
478
|
+
// 127 = the script wasn't found — WSL's bash on a C:/ path) means it never ran: say so,
|
|
479
|
+
// never count it as a pass.
|
|
480
|
+
const hasRun = suiteResult.status === 0 || (suiteResult.status === 1 && failures.length > 0);
|
|
481
|
+
if (!hasRun) {
|
|
482
|
+
const reason = mapToPlainText(suiteResult.stderr || suiteResult.stdout).trim().split(/\r?\n/)[0] || 'no output';
|
|
483
|
+
addWarning(checkName, `suite could not run (exit ${suiteResult.status}): ${reason}`, `bash used: ${getBashPath()} — set KMWC_BASH to Git for Windows' bash.exe if this is WSL's`);
|
|
419
484
|
continue;
|
|
420
485
|
}
|
|
421
|
-
const failures = getSuiteFailures(suiteResult.stdout);
|
|
422
486
|
for (const failure of failures) {
|
|
423
487
|
// An architectural decision recorded in VALIDATION-NOTES.md (the rulebook's accepted-deviation file).
|
|
424
488
|
if (acceptedCheckIds.has(failure.checkId)) addPass(checkName, `${failure.checkId} accepted deviation (${VALIDATION_NOTES_FILE}): ${failure.reason}`);
|
|
425
489
|
else addError(checkName, `${failure.checkId} ${failure.reason}`, `see ${suite.name}.checks.md → ${failure.checkId}`);
|
|
426
490
|
}
|
|
427
|
-
if (!failures.length) addPass(checkName,
|
|
491
|
+
if (!failures.length) addPass(checkName, 'all auto checks pass');
|
|
428
492
|
}
|
|
429
493
|
}
|
|
430
494
|
|
|
431
|
-
|
|
495
|
+
/** `registry` is for tests (fixture repos); the CLI always uses components.json. */
|
|
496
|
+
export async function run(args, { registry } = {}) {
|
|
432
497
|
const config = getKmwcConfig(args.root);
|
|
433
|
-
const results = checkComponent(config, { shouldRunRulebook: !args['no-rulebook'], shouldCheckPackage: !args['no-pack'] });
|
|
498
|
+
const results = checkComponent(config, { shouldRunRulebook: !args['no-rulebook'], shouldCheckPackage: !args['no-pack'], registry });
|
|
434
499
|
const errorCount = results.filter((result) => result.level === 'error').length;
|
|
435
500
|
const warningCount = results.filter((result) => result.level === 'warning').length;
|
|
436
501
|
|
package/src/cli/repo-checks.js
CHANGED
|
@@ -109,6 +109,12 @@ export function getMentionedPorts(text) {
|
|
|
109
109
|
return getUniqueSorted(patterns.flatMap((pattern) => [...text.matchAll(pattern)].map((match) => Number(match[1]))));
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* A doc that names other repos' ports on purpose (a dated plan, a cross-repo review) carries
|
|
114
|
+
* `<!-- kmwc-other-ports: why -->` and isn't scanned.
|
|
115
|
+
*/
|
|
116
|
+
export const OTHER_PORTS_MARKER = 'kmwc-other-ports';
|
|
117
|
+
|
|
112
118
|
/** Mentions in README.md / CLAUDE.md / docs/*.md of ports outside the repo's block → [{ file, port }]. */
|
|
113
119
|
export function loadStalePortMentions(root, portBlock) {
|
|
114
120
|
const blockRange = getPortBlockRange(portBlock);
|
|
@@ -116,7 +122,7 @@ export function loadStalePortMentions(root, portBlock) {
|
|
|
116
122
|
const docFiles = [...DOC_FILES, ...(existsSync(docsDir) ? readdirSync(docsDir).filter((name) => name.endsWith('.md')).map((name) => `${DOCS_DIR}/${name}`) : [])];
|
|
117
123
|
return docFiles.flatMap((file) => {
|
|
118
124
|
const text = readText(join(root, file));
|
|
119
|
-
return text ? getMentionedPorts(text).filter((port) => !checkIsInRange(port, blockRange)).map((port) => ({ file, port })) : [];
|
|
125
|
+
return text && !text.includes(OTHER_PORTS_MARKER) ? getMentionedPorts(text).filter((port) => !checkIsInRange(port, blockRange)).map((port) => ({ file, port })) : [];
|
|
120
126
|
});
|
|
121
127
|
}
|
|
122
128
|
|
|
@@ -252,7 +258,8 @@ export function getKitDuplicates(pageText, kitClassNames, { isStylesheet = false
|
|
|
252
258
|
|
|
253
259
|
/**
|
|
254
260
|
* Fixed colours on purpose (a theme gallery, a mockup, a test fixture): a page `<style>`
|
|
255
|
-
* block with the attribute, or one rule right
|
|
261
|
+
* block or an element with a `style="…"` that carries the attribute, or one rule right
|
|
262
|
+
* after a `/* kmwc-fixed-colors *\/` comment.
|
|
256
263
|
*/
|
|
257
264
|
export const FIXED_COLORS_ATTRIBUTE = 'data-kmwc-fixed-colors';
|
|
258
265
|
export const FIXED_COLORS_MARKER = 'kmwc-fixed-colors';
|
|
@@ -283,7 +290,7 @@ export function checkIsHardcodedColor(property, value) {
|
|
|
283
290
|
* Hard-coded colours in one page's `<style>` blocks and `style="…"` attributes (or in
|
|
284
291
|
* examples-local.css): they stay the same in dark mode while the kit around them flips.
|
|
285
292
|
* `var()` fallbacks and `light-dark()` pairs follow the theme; a `<style data-kmwc-fixed-colors>`
|
|
286
|
-
* block and a rule marked `/* kmwc-fixed-colors *\/` are skipped. Returns [{ found }]. Pure.
|
|
293
|
+
* block, an element carrying the attribute and a rule marked `/* kmwc-fixed-colors *\/` are skipped. Returns [{ found }]. Pure.
|
|
287
294
|
*/
|
|
288
295
|
export function getHardcodedColors(pageText, { isStylesheet = false } = {}) {
|
|
289
296
|
const styleText = isStylesheet
|
|
@@ -302,7 +309,12 @@ export function getHardcodedColors(pageText, { isStylesheet = false } = {}) {
|
|
|
302
309
|
}
|
|
303
310
|
}
|
|
304
311
|
if (isStylesheet) return hardcodedColors;
|
|
305
|
-
for (const
|
|
312
|
+
for (const match of pageText.matchAll(/\sstyle="([^"]*)"/g)) {
|
|
313
|
+
// The element's own attribute opts its style="…" out: markup a render callback draws
|
|
314
|
+
// inside a shadow root can't be reached by page CSS, so inline is the only way there.
|
|
315
|
+
const tagText = pageText.slice(pageText.lastIndexOf('<', match.index), pageText.indexOf('>', match.index + match[0].length) + 1);
|
|
316
|
+
if (tagText.includes(FIXED_COLORS_ATTRIBUTE)) continue;
|
|
317
|
+
const inlineStyle = match[1];
|
|
306
318
|
for (const declaration of inlineStyle.split(';')) {
|
|
307
319
|
const [property, ...valueParts] = declaration.split(':');
|
|
308
320
|
const value = valueParts.join(':').trim();
|
|
@@ -358,6 +370,24 @@ function getSourceStylesheets(directory, relativeDirectory = SOURCE_DIR) {
|
|
|
358
370
|
});
|
|
359
371
|
}
|
|
360
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Styled loader classes vs the separator core draws → { isCoreClassStyled, renamedClasses,
|
|
375
|
+
* collidingClasses }. A class with the other separator is a rename done too early / too late —
|
|
376
|
+
* unless core is still on `-` and its class is styled too: then the `__loader` is the
|
|
377
|
+
* component's own element (web-multiselect's loading-state row), which collides once core
|
|
378
|
+
* draws `<prefix>__loader`. Pure.
|
|
379
|
+
*/
|
|
380
|
+
export function getLoaderClassVerdict(coreSeparator, styledLoaderClasses) {
|
|
381
|
+
const isCoreClassStyled = styledLoaderClasses.some(({ separator }) => separator === coreSeparator);
|
|
382
|
+
const otherClasses = styledLoaderClasses.filter(({ separator }) => separator !== coreSeparator);
|
|
383
|
+
const isOwnLoaderElement = coreSeparator === '-' && isCoreClassStyled;
|
|
384
|
+
return {
|
|
385
|
+
isCoreClassStyled,
|
|
386
|
+
renamedClasses: isOwnLoaderElement ? [] : otherClasses,
|
|
387
|
+
collidingClasses: isOwnLoaderElement ? otherClasses : [],
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
361
391
|
/** Loader classes styled in the component's src CSS: [{ file, className, separator }]. */
|
|
362
392
|
export function loadStyledLoaderClasses(root, prefix) {
|
|
363
393
|
return getSourceStylesheets(join(root, SOURCE_DIR)).flatMap((file) =>
|
|
@@ -213,13 +213,13 @@
|
|
|
213
213
|
|
|
214
214
|
button.secondary,
|
|
215
215
|
button.btn-secondary {
|
|
216
|
-
background: var(--kmwc-neutral-color);
|
|
216
|
+
background: var(--kmwc-neutral-fill-color);
|
|
217
217
|
color: var(--kmwc-text-color-on-neutral);
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
button.secondary:hover,
|
|
221
221
|
button.btn-secondary:hover {
|
|
222
|
-
background:
|
|
222
|
+
background: var(--kmwc-neutral-fill-color-hover);
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
button.btn-outline {
|
|
@@ -120,9 +120,10 @@
|
|
|
120
120
|
--kmwc-console-err-color: #fca5a5;
|
|
121
121
|
/* Dark: the chip is a step up from the main surface (slate on a slate page, not a
|
|
122
122
|
neutral grey), and the text keeps the accent hue but at least 0.8 lightness, so a
|
|
123
|
-
mid-dark accent (treeview's #059669) stays readable on it.
|
|
123
|
+
mid-dark accent (treeview's #059669) stays readable on it. Light: the same hue at
|
|
124
|
+
most 0.5 lightness — the raw accent read at ~3.3:1 on the pale chip. */
|
|
124
125
|
--kmwc-inline-code-bg: light-dark(#edf2f7, oklch(from var(--kmwc-main-bg) calc(l + 0.07) c h));
|
|
125
|
-
--kmwc-inline-code-color: light-dark(var(--kmwc-accent-color), oklch(from var(--kmwc-accent-color) max(l, 0.8) c h));
|
|
126
|
+
--kmwc-inline-code-color: light-dark(oklch(from var(--kmwc-accent-color) min(l, 0.5) c h), oklch(from var(--kmwc-accent-color) max(l, 0.8) c h));
|
|
126
127
|
|
|
127
128
|
/* ======================================================================
|
|
128
129
|
COLORS — SEMANTIC
|
|
@@ -137,10 +138,15 @@
|
|
|
137
138
|
--kmwc-danger-fill-color-hover: var(--base-danger-color-hover, light-dark(color-mix(in srgb, black 15%, var(--kmwc-danger-fill-color)), color-mix(in srgb, white 15%, var(--kmwc-danger-fill-color))));
|
|
138
139
|
--kmwc-text-color-on-danger: var(--base-on-danger-text-color, color(from color(from var(--kmwc-danger-fill-color) srgb calc(0.21 * r * r * (0.4 + r * (0.82 - 0.22 * r)) + 0.72 * g * g * (0.4 + g * (0.82 - 0.22 * g)) + 0.07 * b * b * (0.4 + b * (0.82 - 0.22 * b))) 0 0) srgb clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1)));
|
|
139
140
|
--kmwc-neutral-color: light-dark(#6b7280, #9ca3af);
|
|
141
|
+
/* A grey button needs its own fill: the dark-mode neutral (#9ca3af) sits just under the
|
|
142
|
+
on-colour formula's 0.34 switch, so it got white text at 2.5:1. Dots and pills keep the
|
|
143
|
+
light grey; the button takes a dark one (white text ≥ 4.5:1 in both themes). */
|
|
144
|
+
--kmwc-neutral-fill-color: light-dark(#6b7280, #4b5563);
|
|
145
|
+
--kmwc-neutral-fill-color-hover: light-dark(color-mix(in srgb, black 15%, var(--kmwc-neutral-fill-color)), color-mix(in srgb, white 12%, var(--kmwc-neutral-fill-color)));
|
|
140
146
|
--kmwc-badge-new-bg: light-dark(#e53e3e, #f56565);
|
|
141
147
|
/* Text on the neutral / badge fills: the contract's on-colour formula on
|
|
142
148
|
their own background, not on the accent (a light primary would turn it black). */
|
|
143
|
-
--kmwc-text-color-on-neutral: color(from color(from var(--kmwc-neutral-color) srgb calc(0.21 * r * r * (0.4 + r * (0.82 - 0.22 * r)) + 0.72 * g * g * (0.4 + g * (0.82 - 0.22 * g)) + 0.07 * b * b * (0.4 + b * (0.82 - 0.22 * b))) 0 0) srgb clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1));
|
|
149
|
+
--kmwc-text-color-on-neutral: color(from color(from var(--kmwc-neutral-fill-color) srgb calc(0.21 * r * r * (0.4 + r * (0.82 - 0.22 * r)) + 0.72 * g * g * (0.4 + g * (0.82 - 0.22 * g)) + 0.07 * b * b * (0.4 + b * (0.82 - 0.22 * b))) 0 0) srgb clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1));
|
|
144
150
|
--kmwc-text-color-on-badge-new: color(from color(from var(--kmwc-badge-new-bg) srgb calc(0.21 * r * r * (0.4 + r * (0.82 - 0.22 * r)) + 0.72 * g * g * (0.4 + g * (0.82 - 0.22 * g)) + 0.07 * b * b * (0.4 + b * (0.82 - 0.22 * b))) 0 0) srgb clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1));
|
|
145
151
|
|
|
146
152
|
/* ======================================================================
|
|
@@ -39,7 +39,11 @@ const INCLUDE_PATTERN = /<kmwc-include\b([^>]*?)(?:\/>|>([\s\S]*?)<\/kmwc-includ
|
|
|
39
39
|
const DEMO_PATTERN = /<kmwc-demo\b([^>]*)>([\s\S]*?)<\/kmwc-demo>/g;
|
|
40
40
|
const SHOWN_SCRIPT_PATTERN = /<script\b([^>]*\bdata-kmwc-show\b[^>]*)>([\s\S]*?)<\/script>/g;
|
|
41
41
|
const PAGE_META_PATTERN = /<meta\s+name="kmwc:([a-z-]+)"\s+content="([^"]*)"\s*\/?>\s*/g;
|
|
42
|
-
|
|
42
|
+
// A <style> element, or ANY start tag (quoted attribute values may hold `>` and `<link>`):
|
|
43
|
+
// matching whole tags left to right means the text `<link>` inside a tooltip's title=""
|
|
44
|
+
// is consumed with its tag and never read as an element. Only <style> and <link> move.
|
|
45
|
+
const HEAD_ELEMENT_PATTERN = /<style(?=[\s>])(?:"[^"]*"|'[^']*'|[^>"'])*>[\s\S]*?<\/style>\s*|<([a-zA-Z][\w-]*)(?:"[^"]*"|'[^']*'|[^>"'])*>\s*/g;
|
|
46
|
+
const HEAD_ELEMENT_TAGS = ['style', 'link'];
|
|
43
47
|
const ATTRIBUTE_PATTERN = /([a-zA-Z][\w-]*)(?:\s*=\s*"([^"]*)")?/g;
|
|
44
48
|
|
|
45
49
|
const HTML_COMMENT_PATTERN = /<!--[\s\S]*?-->/g;
|
|
@@ -201,7 +205,8 @@ export function renderLayout(fragmentHtml, { config, pagePath }) {
|
|
|
201
205
|
const bodyContent = mapOutsideComments(
|
|
202
206
|
htmlWithoutMeta,
|
|
203
207
|
(text) =>
|
|
204
|
-
text.replace(HEAD_ELEMENT_PATTERN, (element) => {
|
|
208
|
+
text.replace(HEAD_ELEMENT_PATTERN, (element, tagName = 'style') => {
|
|
209
|
+
if (!HEAD_ELEMENT_TAGS.includes(tagName.toLowerCase())) return element;
|
|
205
210
|
headElements.push(` ${element.trim()}`);
|
|
206
211
|
return '';
|
|
207
212
|
}),
|
|
@@ -31,7 +31,8 @@ locally — every local fork is how they drifted apart.
|
|
|
31
31
|
too — no inline styles for them. Page colours come from the kit's `--kmwc-*`
|
|
32
32
|
tokens or `light-dark()` — a literal doesn't follow dark mode (doctor warns);
|
|
33
33
|
a demo that shows fixed colours on purpose goes in `<style data-kmwc-fixed-colors>`
|
|
34
|
-
(one rule: a `/* kmwc-fixed-colors: why */` comment before it
|
|
34
|
+
(one rule: a `/* kmwc-fixed-colors: why */` comment before it; an element's own
|
|
35
|
+
`style="…"`: the attribute on the element).
|
|
35
36
|
If a second component needs a pattern, it belongs in the devkit.
|
|
36
37
|
- **Example pages are fragments:** no `<html>`/`<head>`/`<header>` — start with
|
|
37
38
|
`<meta name="kmwc:title" content="…">`, the devkit layout adds the rest. Shared text
|
|
@@ -54,4 +55,6 @@ locally — every local fork is how they drifted apart.
|
|
|
54
55
|
`*.checks.sh` suites. A rulebook failure that is a real architectural decision
|
|
55
56
|
goes into `VALIDATION-NOTES.md` under a `## C-XX-N — …` heading (deferrals
|
|
56
57
|
don't count). After changing a page, `npx kmwc check-pages` loads every page at
|
|
57
|
-
desktop and phone widths and reports errors and
|
|
58
|
+
desktop and phone widths and reports errors, horizontal overflow and page CSS
|
|
59
|
+
that only matches inside a shadow root (markup a render callback draws there is
|
|
60
|
+
out of page CSS's reach — style it through the component's styles hook).
|