@keenmate/web-components-devkit 1.0.0-rc01 → 1.0.0-rc02

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,55 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.0-rc02] - 2026-10-31 [PUBLISHED]
4
+
5
+ ### Added
6
+
7
+ - **`kmwc new --adopt`** for migrating a component that is already in the registry:
8
+ the same scaffold, with the entry's port block and prefix (or `--prefix` when the
9
+ entry has none), no npm-name check (the package may exist) and no registry edit.
10
+ Scaffold into a scratch `--dir`, then move the component's code into it — how
11
+ web-editor's one-package monorepo was flattened.
12
+
13
+ - **`npm run check:contract`** — `base-contract-check` (from `@keenmate/base-css-variables`,
14
+ a devDependency) over the kit, the scaffold templates and the playground: every `--base-*`
15
+ read or set exists in the contract, input fallbacks equal its defaults, derived ones are its
16
+ formulas. Deliberate differences go in `contract-check.json` (the scaffolds' `inherit` font).
17
+
18
+ ### Fixed
19
+
20
+ - **Kit and scaffolds read the 2.0 `--base-*` names.** `--base-accent-color*`,
21
+ `--base-text-color-on-accent` and `--base-input-color` were removed in base-css-variables
22
+ 2.0.0, so a theme's primary never reached the example pages or a new component. Now
23
+ `--base-primary-color` / `-hover` / `-active`, `--base-primary-bg-subtle(-hover)`,
24
+ `--base-on-primary-text-color` and `--base-input-text-color`; the theming pages and docs set
25
+ `--base-primary-color`. Fallbacks are the contract's: example pages default to the blue-500
26
+ primary (was purple `#667eea`; the header gradient end now follows the accent), slate surfaces
27
+ (dark main `#1e293b`, page `#f8f9fa` / `#0f172a`) and the contract text, border, hover, shadow
28
+ and font values. The kit's status colours read `--base-<role>-text-color`; the danger button
29
+ fills with `--base-danger-color` and its text is `--base-on-danger-text-color`. The secondary
30
+ button and the index "new" badge compute their text from their own fill, so a light primary
31
+ no longer turns them black.
32
+ - **`kmwc doctor` skipped every rulebook suite once the devkit came from npm.** The
33
+ rulebook was looked up at a fixed path from the devkit (`<devkit>/../../BlissFramework`),
34
+ which lands inside `node_modules` for an installed package. It is now the nearest
35
+ `BlissFramework/guidelines/web-components` above the component (then above the
36
+ devkit); `kmwc.rulebook.path` resolves against the component root.
37
+ - **Fragment pages: a `<style>` or `<link>` inside a `<script>` stays put.** Shadow-DOM
38
+ styles and an SVG's own `<style>` written into a script were moved into `<head>`; they
39
+ are strings there, not page elements.
40
+
41
+ ### Migrated
42
+
43
+ - **web-editor** — flattened from a one-package npm-workspaces monorepo; registry entry
44
+ now carries its prefix (`km`) and no longer says monorepo.
45
+ - **pure-css** (library, 15200) and **base-css-variables** (library, 15300) added to the
46
+ registry.
47
+
48
+ ### Internal
49
+
50
+ - **Root `Makefile`** for the devkit's own tasks: test, readme / contract checks, and
51
+ `publish` / `publish-rc` gated on `kmwc check-release`.
52
+
3
53
  ## [1.0.0-rc01] - 2026-10-12 [PUBLISHED]
4
54
 
5
55
  First release of `@keenmate/web-components-devkit`, plus the component catalogue.
package/README.md CHANGED
@@ -19,6 +19,13 @@ 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-rc02
23
+
24
+ - **Theming — kit and scaffolds follow base-css-variables 2.0** — base-css-variables 2.0 dropped `--base-accent-color*`, `--base-text-color-on-accent` and `--base-input-color`, so a theme's primary colour silently stopped reaching the example pages and every newly scaffolded component. The `--kmwc-*` tokens in `src/examples/css/tokens.css` and the `templates/new*` `variables.css` now read `--base-primary-color` (with `-hover` / `-active` / `-bg-subtle`), `--base-on-primary-text-color` and `--base-input-text-color`, and the theming pages, docs and manifests set the new names. Standalone fallbacks are now the contract's own values, so a page looks the same with or without the contract loaded: a blue-500 primary instead of the old purple, slate surfaces, and on-colours computed from their fill. Re-run `kmwc new` output or diff your `variables.css` if you scaffolded on rc01.
25
+ - **`npm run check:contract` — fallbacks can't drift from the contract** — `base-contract-check` from `@keenmate/base-css-variables` now runs over the kit, the scaffold templates and the playground. It fails when a `--base-*` read doesn't exist in the contract, when an input's fallback differs from the contract default, or when a derived token's fallback isn't the contract formula. Deliberate differences, such as the scaffolds inheriting the page font, are listed with a reason in `contract-check.json`. It's part of the gate before every kit or template change.
26
+ - **`kmwc new --adopt` — migrate a component that's already registered** — Brings an existing repo onto the devkit with the same rulebook-shaped scaffold, using the registry entry's port block and CSS prefix, skipping the npm-name check (the package already exists) and leaving `components.json` untouched. Scaffold into a scratch `--dir` and move the component's code in. web-editor's one-package monorepo was flattened this way.
27
+ - **`kmwc doctor` — rulebook suites run from an npm install** — With the devkit installed from npm instead of linked, doctor looked for the BlissFramework rulebook at a path relative to the devkit, which ended up inside `node_modules`, and silently skipped every suite. It now uses the nearest `BlissFramework/guidelines/web-components` above the component (then above the devkit), and `kmwc.rulebook.path` resolves against the component root.
28
+
22
29
  ## What's New in v1.0.0-rc01
23
30
 
24
31
  - **Devkit — one shared toolchain for every Keenmate web component** — Each `web-*` repo used to carry its own copy of the example helpers, the shared stylesheet, the Vite/Playwright configs and the deploy files, and every session "fixed" its own copy until they drifted apart. `@keenmate/web-components-devkit` owns all of it now: a component describes itself once in `package.json#kmwc` (title, prefix, ports, accent colours, analytics, image) and `defineComponentConfig()`, `definePlaywrightConfig()` and the `kmwc` CLI read it from there. A shared concern is fixed once in the devkit and every component picks it up on the next install.
@@ -42,7 +49,7 @@ them in the same shape: `@keenmate/web-components-devkit`.
42
49
  | **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,… |
43
50
  | **web-dropzone** | `@keenmate/web-dropzone` | `dz` | 12500 | [repo](https://github.com/keenmate/web-dropzone) | — | Lightweight file dropzone web component with drag-drop, previews, validation, and multiple display modes. |
44
51
  | **web-grid** | `@keenmate/web-grid` | `wg` | 12600 | [repo](https://github.com/keenmate/web-grid) · [docs](https://web-grid.keenmate.com) · [showcase](https://web-grid.keenmate.com) | — | Framework-agnostic data grid web component with sorting, filtering, inline editing, and keyboard navigation |
45
- | **web-editor** | `@keenmate/web-editor` | — | 12700 | [examples](https://examples.web-editor.keenmate.dev) | — | KeenMate Web Editor monorepo — a framework-agnostic WYSIWYG / Markdown editor built on ProseMirror. |
52
+ | **web-editor** | `@keenmate/web-editor` | `km` | 12700 | [examples](https://examples.web-editor.keenmate.dev) | ✅ | Keenmate Web Editor — a framework-agnostic WYSIWYG / Markdown editor built on ProseMirror. |
46
53
  | **web-switch** | `@keenmate/web-switch` | — | 12800 | [repo](https://github.com/keenmate/web-switch) | — | Framework-agnostic web component wrapper for @keenmate/svelte-switch. Works with React, Vue, Angular, or… |
47
54
  | **web-timepicker** | — | — | 12900 | — | — | Vanilla-JS prototypes (spiral / slider time pickers); no package yet. |
48
55
  | **web-wysiwyg** | — | — | 13000 | — | — | ProseMirror exploration; superseded by web-editor. |
@@ -53,6 +60,8 @@ them in the same shape: `@keenmate/web-components-devkit`.
53
60
  |---|---|---:|---|
54
61
  | **web-components-core** | `@keenmate/web-components-core` | 15000 | Runtime foundation: BlissElement, reactive input table, converters, logging, positioning, CEM preset. |
55
62
  | **web-components** | `@keenmate/web-components-devkit` | 15100 | This repo — dev/demo/build tooling (Vite preset, examples kit, kmwc CLI). |
63
+ | **pure-css** | `@keenmate/pure-css` | 15200 | CSS framework mirroring the base-css-variables token contract (demo site + e2e on the dev port). |
64
+ | **base-css-variables** | `@keenmate/base-css-variables` | 15300 | The shared --base-* token contract (base-variables.css) + base-contract-check; examples site + e2e on the dev port. |
56
65
 
57
66
  ### Showcases, docs & test harnesses
58
67
 
@@ -166,7 +175,8 @@ Every field is optional. Defaults are derived from the package.
166
175
  "checkMode": "declared" // or "consumed" (var() reads)
167
176
  },
168
177
  "rulebook": {
169
- "path": "../../BlissFramework/guidelines/web-components", // default; env KMWC_RULEBOOK_PATH wins
178
+ "path": "../BlissFramework/guidelines/web-components", // relative to the component; default: the nearest
179
+ // BlissFramework checkout above it. env KMWC_RULEBOOK_PATH wins
170
180
  "skippedChecks": [] // e.g. ["responsive-overlay"]
171
181
  },
172
182
  "sync": { "skippedFiles": [] } // e.g. ["Makefile"] to opt a file out
@@ -302,6 +312,7 @@ kmwc doctor [--json] devkit checks + package contents + the rulebook's
302
312
  kmwc check-vars [--strict] component-variables.manifest.json ⇔ CSS
303
313
  kmwc check-release --tag t latest ⇔ release version, rc ⇔ -rcN, version not yet on npm
304
314
  kmwc new web-<name> scaffold a component (--prefix, --title, --tag, --dir, --port, --minimal, --register)
315
+ --adopt: scaffold an ALREADY registered component (its port/prefix) to migrate into
305
316
  kmwc readme [--check] regenerate this README's catalogue (devkit repo only)
306
317
  ```
307
318
 
@@ -366,6 +377,12 @@ command prints the reminder.
366
377
 
367
378
  `kmwc doctor` lists everything that's left, so run it after each step.
368
379
 
380
+ **Repo in a different shape** (a one-package monorepo, a non-standard layout)?
381
+ Don't teach the devkit the old shape: back the repo up, run `kmwc new web-<name>
382
+ --adopt --dir <scratch>` (same files, with the registered port and prefix, no
383
+ registry edits), and move the component's own code into that layout with
384
+ `git mv`. web-editor was flattened this way.
385
+
369
386
  1. `npm i -D @keenmate/web-components-devkit vite@^7` (plus `@playwright/test`
370
387
  if the repo has e2e tests).
371
388
  2. Add `package.json#kmwc`. Take the port block (`ports.dev` = block,
package/components.json CHANGED
@@ -1,189 +1,206 @@
1
- {
2
- "$comment": "Registry of Keenmate web components — source for the README catalogue (`npm run readme`). `port` is the repo's block: dev server = port, preview = port + 1, examples image = port + 10. Each kind has its own range with headroom (components 12100–14900, libraries 15000–15900, showcases/docs 16000–16900, tests 17000–17900), numbered by 100 in registry order; `kmwc doctor` checks package.json#kmwc.ports against it and `kmwc new` takes the next block. `prefix` mirrors the reserved-prefix table in BlissFramework guidelines/web-components/base-variables.md (the authority).",
3
- "components": [
4
- {
5
- "id": "web-multiselect",
6
- "kind": "component",
7
- "prefix": "ms",
8
- "port": 12100,
9
- "path": "../web-multiselect",
10
- "npm": "@keenmate/web-multiselect",
11
- "repo": "https://github.com/keenmate/web-multiselect",
12
- "examples": "https://examples.web-multiselect.keenmate.dev",
13
- "docs": "https://web-multiselect.keenmate.dev",
14
- "showcase": "web-multiselect-showcase"
15
- },
16
- {
17
- "id": "web-daterangepicker",
18
- "kind": "component",
19
- "prefix": "drp",
20
- "port": 12200,
21
- "path": "../web-daterangepicker",
22
- "npm": "@keenmate/web-daterangepicker",
23
- "repo": "https://github.com/keenmate/web-daterangepicker",
24
- "docs": "https://web-daterangepicker.keenmate.dev",
25
- "showcase": "web-daterangepicker-showcase"
26
- },
27
- {
28
- "id": "web-treeview",
29
- "kind": "component",
30
- "prefix": "wtv",
31
- "port": 12300,
32
- "path": "../web-treeview",
33
- "npm": "@keenmate/web-treeview",
34
- "repo": "https://github.com/keenmate/web-treeview",
35
- "showcase": "web-treeview-showcase"
36
- },
37
- {
38
- "id": "web-commandpalette",
39
- "kind": "component",
40
- "prefix": "cp",
41
- "port": 12400,
42
- "path": "../web-commandpalette",
43
- "npm": "@keenmate/web-commandpalette",
44
- "repo": "https://github.com/keenmate/web-commandpalette",
45
- "examples": "https://examples.web-commandpalette.keenmate.dev"
46
- },
47
- {
48
- "id": "web-dropzone",
49
- "kind": "component",
50
- "prefix": "dz",
51
- "port": 12500,
52
- "path": "../web-dropzone",
53
- "npm": "@keenmate/web-dropzone",
54
- "repo": "https://github.com/keenmate/web-dropzone",
55
- "monorepo": true,
56
- "note": "Monorepo: renderer + headless core."
57
- },
58
- {
59
- "id": "web-grid",
60
- "kind": "component",
61
- "prefix": "wg",
62
- "port": 12600,
63
- "path": "../web-grid",
64
- "npm": "@keenmate/web-grid",
65
- "repo": "https://github.com/keenmate/web-grid",
66
- "docs": "https://web-grid.keenmate.com",
67
- "showcase": "web-grid-showcase",
68
- "monorepo": true
69
- },
70
- {
71
- "id": "web-editor",
72
- "kind": "component",
73
- "port": 12700,
74
- "path": "../web-editor",
75
- "npm": "@keenmate/web-editor",
76
- "examples": "https://examples.web-editor.keenmate.dev",
77
- "monorepo": true,
78
- "note": "Monorepo: ProseMirror-based WYSIWYG / Markdown editor."
79
- },
80
- {
81
- "id": "web-switch",
82
- "kind": "component",
83
- "port": 12800,
84
- "path": "../web-switch",
85
- "npm": "@keenmate/web-switch",
86
- "repo": "https://github.com/keenmate/web-switch",
87
- "note": "Svelte-based wrapper around @keenmate/svelte-switch."
88
- },
89
- {
90
- "id": "web-timepicker",
91
- "kind": "component",
92
- "port": 12900,
93
- "path": "../web-timepicker",
94
- "note": "Vanilla-JS prototypes (spiral / slider time pickers); no package yet."
95
- },
96
- {
97
- "id": "web-wysiwyg",
98
- "kind": "component",
99
- "port": 13000,
100
- "path": "../web-wysiwyg",
101
- "note": "ProseMirror exploration; superseded by web-editor."
102
- },
103
- {
104
- "id": "web-components-core",
105
- "kind": "library",
106
- "port": 15000,
107
- "path": "../web-components-core",
108
- "npm": "@keenmate/web-components-core",
109
- "note": "Runtime foundation: BlissElement, reactive input table, converters, logging, positioning, CEM preset."
110
- },
111
- {
112
- "id": "web-components",
113
- "kind": "library",
114
- "port": 15100,
115
- "path": ".",
116
- "npm": "@keenmate/web-components-devkit",
117
- "repo": "https://github.com/keenmate/web-components",
118
- "note": "This repo — dev/demo/build tooling (Vite preset, examples kit, kmwc CLI)."
119
- },
120
- {
121
- "id": "web-multiselect-showcase",
122
- "kind": "showcase",
123
- "port": 16000,
124
- "path": "../web-multiselect-showcase",
125
- "for": "web-multiselect"
126
- },
127
- {
128
- "id": "web-daterangepicker-showcase",
129
- "kind": "showcase",
130
- "port": 16100,
131
- "path": "../web-daterangepicker-showcase",
132
- "for": "web-daterangepicker"
133
- },
134
- {
135
- "id": "web-treeview-showcase",
136
- "kind": "showcase",
137
- "port": 16200,
138
- "path": "../web-treeview-showcase",
139
- "for": "web-treeview"
140
- },
141
- {
142
- "id": "web-grid-showcase",
143
- "kind": "showcase",
144
- "port": 16300,
145
- "path": "../web-grid-showcase",
146
- "for": "web-grid",
147
- "url": "https://web-grid.keenmate.com"
148
- },
149
- {
150
- "id": "web-multiselect-docs",
151
- "kind": "docs",
152
- "port": 16400,
153
- "path": "../web-multiselect-docs",
154
- "for": "web-multiselect",
155
- "note": "Content-only docs (keendocs)."
156
- },
157
- {
158
- "id": "web-component-framework-test",
159
- "kind": "test",
160
- "port": 17000,
161
- "path": "../web-component-framework-test",
162
- "note": "Smoke + contrast tests inside CSS host frameworks (plain / Bootstrap / …)."
163
- },
164
- {
165
- "id": "web-multiselect-test",
166
- "kind": "test",
167
- "port": 17100,
168
- "path": "../web-multiselect-test",
169
- "for": "web-multiselect",
170
- "note": "npm-install vs direct-file consumption checks before publishing."
171
- },
172
- {
173
- "id": "web-multiselect-positioning-demo",
174
- "kind": "test",
175
- "port": 17200,
176
- "path": "../web-multiselect-positioning-demo",
177
- "for": "web-multiselect",
178
- "note": "Single-page positioning repro."
179
- },
180
- {
181
- "id": "web-playground",
182
- "kind": "test",
183
- "port": 17300,
184
- "path": "playground",
185
- "for": "web-components",
186
- "note": "Devkit e2e test bed — a component scaffolded with `kmwc new`, linked to this repo."
187
- }
188
- ]
189
- }
1
+ {
2
+ "$comment": "Registry of Keenmate web components — source for the README catalogue (`npm run readme`). `port` is the repo's block: dev server = port, preview = port + 1, examples image = port + 10. Each kind has its own range with headroom (components 12100–14900, libraries 15000–15900, showcases/docs 16000–16900, tests 17000–17900), numbered by 100 in registry order; `kmwc doctor` checks package.json#kmwc.ports against it and `kmwc new` takes the next block. `prefix` mirrors the reserved-prefix table in BlissFramework guidelines/web-components/base-variables.md (the authority).",
3
+ "components": [
4
+ {
5
+ "id": "web-multiselect",
6
+ "kind": "component",
7
+ "prefix": "ms",
8
+ "port": 12100,
9
+ "path": "../web-multiselect",
10
+ "npm": "@keenmate/web-multiselect",
11
+ "repo": "https://github.com/keenmate/web-multiselect",
12
+ "examples": "https://examples.web-multiselect.keenmate.dev",
13
+ "docs": "https://web-multiselect.keenmate.dev",
14
+ "showcase": "web-multiselect-showcase"
15
+ },
16
+ {
17
+ "id": "web-daterangepicker",
18
+ "kind": "component",
19
+ "prefix": "drp",
20
+ "port": 12200,
21
+ "path": "../web-daterangepicker",
22
+ "npm": "@keenmate/web-daterangepicker",
23
+ "repo": "https://github.com/keenmate/web-daterangepicker",
24
+ "docs": "https://web-daterangepicker.keenmate.dev",
25
+ "showcase": "web-daterangepicker-showcase"
26
+ },
27
+ {
28
+ "id": "web-treeview",
29
+ "kind": "component",
30
+ "prefix": "wtv",
31
+ "port": 12300,
32
+ "path": "../web-treeview",
33
+ "npm": "@keenmate/web-treeview",
34
+ "repo": "https://github.com/keenmate/web-treeview",
35
+ "showcase": "web-treeview-showcase"
36
+ },
37
+ {
38
+ "id": "web-commandpalette",
39
+ "kind": "component",
40
+ "prefix": "cp",
41
+ "port": 12400,
42
+ "path": "../web-commandpalette",
43
+ "npm": "@keenmate/web-commandpalette",
44
+ "repo": "https://github.com/keenmate/web-commandpalette",
45
+ "examples": "https://examples.web-commandpalette.keenmate.dev"
46
+ },
47
+ {
48
+ "id": "web-dropzone",
49
+ "kind": "component",
50
+ "prefix": "dz",
51
+ "port": 12500,
52
+ "path": "../web-dropzone",
53
+ "npm": "@keenmate/web-dropzone",
54
+ "repo": "https://github.com/keenmate/web-dropzone",
55
+ "monorepo": true,
56
+ "note": "Monorepo: renderer + headless core."
57
+ },
58
+ {
59
+ "id": "web-grid",
60
+ "kind": "component",
61
+ "prefix": "wg",
62
+ "port": 12600,
63
+ "path": "../web-grid",
64
+ "npm": "@keenmate/web-grid",
65
+ "repo": "https://github.com/keenmate/web-grid",
66
+ "docs": "https://web-grid.keenmate.com",
67
+ "showcase": "web-grid-showcase",
68
+ "monorepo": true
69
+ },
70
+ {
71
+ "id": "web-editor",
72
+ "kind": "component",
73
+ "prefix": "km",
74
+ "port": 12700,
75
+ "path": "../web-editor",
76
+ "npm": "@keenmate/web-editor",
77
+ "examples": "https://examples.web-editor.keenmate.dev",
78
+ "note": "ProseMirror-based WYSIWYG / Markdown editor."
79
+ },
80
+ {
81
+ "id": "web-switch",
82
+ "kind": "component",
83
+ "port": 12800,
84
+ "path": "../web-switch",
85
+ "npm": "@keenmate/web-switch",
86
+ "repo": "https://github.com/keenmate/web-switch",
87
+ "note": "Svelte-based wrapper around @keenmate/svelte-switch."
88
+ },
89
+ {
90
+ "id": "web-timepicker",
91
+ "kind": "component",
92
+ "port": 12900,
93
+ "path": "../web-timepicker",
94
+ "note": "Vanilla-JS prototypes (spiral / slider time pickers); no package yet."
95
+ },
96
+ {
97
+ "id": "web-wysiwyg",
98
+ "kind": "component",
99
+ "port": 13000,
100
+ "path": "../web-wysiwyg",
101
+ "note": "ProseMirror exploration; superseded by web-editor."
102
+ },
103
+ {
104
+ "id": "web-components-core",
105
+ "kind": "library",
106
+ "port": 15000,
107
+ "path": "../web-components-core",
108
+ "npm": "@keenmate/web-components-core",
109
+ "note": "Runtime foundation: BlissElement, reactive input table, converters, logging, positioning, CEM preset."
110
+ },
111
+ {
112
+ "id": "web-components",
113
+ "kind": "library",
114
+ "port": 15100,
115
+ "path": ".",
116
+ "npm": "@keenmate/web-components-devkit",
117
+ "repo": "https://github.com/keenmate/web-components",
118
+ "note": "This repo — dev/demo/build tooling (Vite preset, examples kit, kmwc CLI)."
119
+ },
120
+ {
121
+ "id": "pure-css",
122
+ "kind": "library",
123
+ "port": 15200,
124
+ "path": "../pure-css",
125
+ "npm": "@keenmate/pure-css",
126
+ "note": "CSS framework mirroring the base-css-variables token contract (demo site + e2e on the dev port)."
127
+ },
128
+ {
129
+ "id": "base-css-variables",
130
+ "kind": "library",
131
+ "port": 15300,
132
+ "path": "../base-css-variables",
133
+ "npm": "@keenmate/base-css-variables",
134
+ "repo": "https://github.com/keenmate/base-css-variables",
135
+ "note": "The shared --base-* token contract (base-variables.css) + base-contract-check; examples site + e2e on the dev port."
136
+ },
137
+ {
138
+ "id": "web-multiselect-showcase",
139
+ "kind": "showcase",
140
+ "port": 16000,
141
+ "path": "../web-multiselect-showcase",
142
+ "for": "web-multiselect"
143
+ },
144
+ {
145
+ "id": "web-daterangepicker-showcase",
146
+ "kind": "showcase",
147
+ "port": 16100,
148
+ "path": "../web-daterangepicker-showcase",
149
+ "for": "web-daterangepicker"
150
+ },
151
+ {
152
+ "id": "web-treeview-showcase",
153
+ "kind": "showcase",
154
+ "port": 16200,
155
+ "path": "../web-treeview-showcase",
156
+ "for": "web-treeview"
157
+ },
158
+ {
159
+ "id": "web-grid-showcase",
160
+ "kind": "showcase",
161
+ "port": 16300,
162
+ "path": "../web-grid-showcase",
163
+ "for": "web-grid",
164
+ "url": "https://web-grid.keenmate.com"
165
+ },
166
+ {
167
+ "id": "web-multiselect-docs",
168
+ "kind": "docs",
169
+ "port": 16400,
170
+ "path": "../web-multiselect-docs",
171
+ "for": "web-multiselect",
172
+ "note": "Content-only docs (keendocs)."
173
+ },
174
+ {
175
+ "id": "web-component-framework-test",
176
+ "kind": "test",
177
+ "port": 17000,
178
+ "path": "../web-component-framework-test",
179
+ "note": "Smoke + contrast tests inside CSS host frameworks (plain / Bootstrap / …)."
180
+ },
181
+ {
182
+ "id": "web-multiselect-test",
183
+ "kind": "test",
184
+ "port": 17100,
185
+ "path": "../web-multiselect-test",
186
+ "for": "web-multiselect",
187
+ "note": "npm-install vs direct-file consumption checks before publishing."
188
+ },
189
+ {
190
+ "id": "web-multiselect-positioning-demo",
191
+ "kind": "test",
192
+ "port": 17200,
193
+ "path": "../web-multiselect-positioning-demo",
194
+ "for": "web-multiselect",
195
+ "note": "Single-page positioning repro."
196
+ },
197
+ {
198
+ "id": "web-playground",
199
+ "kind": "test",
200
+ "port": 17300,
201
+ "path": "playground",
202
+ "for": "web-components",
203
+ "note": "Devkit e2e test bed — a component scaffolded with `kmwc new`, linked to this repo."
204
+ }
205
+ ]
206
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keenmate/web-components-devkit",
3
- "version": "1.0.0-rc01",
3
+ "version": "1.0.0-rc02",
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
+ "check:contract": "base-contract-check src templates playground",
50
51
  "playground": "cd playground && vite",
51
52
  "playground:build": "cd playground && vite build --mode examples",
52
53
  "playground:lib": "cd playground && vite build"
@@ -65,6 +66,7 @@
65
66
  }
66
67
  },
67
68
  "devDependencies": {
69
+ "@keenmate/base-css-variables": "^2.0.0-rc07",
68
70
  "typescript": "^5.9.3",
69
71
  "vite": "^7.3.7"
70
72
  },
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * kmwc new web-colorpicker --prefix cpk [--title "Web Color Picker"] [--tag web-colorpicker]
5
5
  * [--dir ../web-colorpicker] [--port 13200] [--minimal] [--register] [--offline]
6
+ * kmwc new web-editor --adopt --dir /tmp/web-editor [--prefix …]
6
7
  *
7
8
  * Default: a small working sample (a toggle with `label` / `active` inputs and
8
9
  * a `change` event) to rename or replace. `--minimal`: an empty skeleton — no
@@ -21,6 +22,12 @@
21
22
  * `.claude/commands/publish.md`, examples, fixtures + e2e specs, then the
22
23
  * `kmwc sync` files. The port block (dev = block, image = +10) is the next free
23
24
  * one in the component range (see PORT_RANGES in config.js; `--port` overrides).
25
+ *
26
+ * `--adopt` is for MIGRATING a component that is already registered: the id must
27
+ * be in components.json, the port block (and prefix, unless the entry has none —
28
+ * then `--prefix`) come from its entry, the npm-name check is skipped (the package
29
+ * may exist), and the registry is left alone. Scaffold into a scratch `--dir`,
30
+ * then move the component's own code into the generated shape.
24
31
  */
25
32
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
26
33
  import { dirname, join, relative, resolve } from 'node:path';
@@ -184,7 +191,7 @@ export function generatePackageJson({ componentId, packageName, tagline, tag, pr
184
191
  export async function run(args) {
185
192
  const requestedName = args._[0];
186
193
  if (!requestedName) {
187
- throw new Error('usage: kmwc new <name> --prefix <2-4 letters> [--title "…"] [--tag …] [--dir …] [--port …] [--register] [--offline]');
194
+ throw new Error('usage: kmwc new <name> --prefix <2-4 letters> [--title "…"] [--tag …] [--dir …] [--port …] [--register] [--offline] [--adopt]');
188
195
  }
189
196
 
190
197
  const unscopedName = getUnscopedName(requestedName);
@@ -201,24 +208,35 @@ export async function run(args) {
201
208
 
202
209
  // ── intake ───────────────────────────────────────────────────────────────
203
210
  const registry = getRegistry();
204
- if (registry.components.some((entry) => entry.id === componentId)) throw new Error(`"${componentId}" is already in components.json`);
211
+ const isAdopting = !!args.adopt;
212
+ const adoptedEntry = registry.components.find((entry) => entry.id === componentId);
213
+ if (isAdopting && !adoptedEntry) throw new Error(`--adopt: "${componentId}" is not in components.json — register it, or drop --adopt for a new component`);
214
+ if (!isAdopting && adoptedEntry) throw new Error(`"${componentId}" is already in components.json (migrating it? add --adopt)`);
215
+ if (isAdopting && args.register) throw new Error('--adopt never edits the registry — drop --register');
205
216
  if (existsSync(targetPath) && readdirSync(targetPath).length) throw new Error(`${targetPath} exists and is not empty`);
206
217
 
207
- const prefix = args.prefix || generatePrefix(componentId);
218
+ if (isAdopting && adoptedEntry.prefix && args.prefix && args.prefix !== adoptedEntry.prefix) {
219
+ throw new Error(`--prefix ${args.prefix} ≠ the registered prefix "${adoptedEntry.prefix}"`);
220
+ }
221
+ const prefix = (isAdopting && adoptedEntry.prefix) || args.prefix || generatePrefix(componentId);
208
222
  if (!prefix) {
209
223
  throw new Error(
210
224
  `cannot derive a CSS prefix for "${componentId}" — is "${featureName}" one English word or several? ` +
211
225
  `Pass --prefix (initials of each word, e.g. daterangepicker → drp; component-intake.md "C — CSS prefix derivation").`
212
226
  );
213
227
  }
214
- const prefixError = checkPrefix(prefix, getReservedPrefixes(registry));
228
+ // An adopted component's own registered prefix isn't a clash with itself.
229
+ const reservedPrefixes = getReservedPrefixes(registry);
230
+ if (isAdopting && adoptedEntry.prefix) reservedPrefixes.delete(adoptedEntry.prefix);
231
+ const prefixError = checkPrefix(prefix, reservedPrefixes);
215
232
  if (prefixError) throw new Error(prefixError);
216
233
 
217
- if (!args.offline && checkIsPackageNameTaken(packageName)) throw new Error(`${packageName} already exists on npm (component-intake.md "B")`);
234
+ if (!isAdopting && !args.offline && checkIsPackageNameTaken(packageName)) throw new Error(`${packageName} already exists on npm (component-intake.md "B")`);
218
235
 
219
- const devPort = Number(args.port) || getNextPortBlock(registry);
236
+ if (isAdopting && args.port && Number(args.port) !== adoptedEntry.port) throw new Error(`--port ${args.port} ≠ the registered port block ${adoptedEntry.port}`);
237
+ const devPort = isAdopting ? adoptedEntry.port : Number(args.port) || getNextPortBlock(registry);
220
238
  if (devPort % PORT_BLOCK_SIZE !== 0) throw new Error(`--port ${devPort} is not a port block (a multiple of ${PORT_BLOCK_SIZE})`);
221
- const portOwner = registry.components.find((entry) => entry.port === devPort);
239
+ const portOwner = registry.components.find((entry) => entry.port === devPort && entry.id !== componentId);
222
240
  if (portOwner) throw new Error(`port block ${devPort} belongs to ${portOwner.id} — omit --port to take the next free block`);
223
241
  const corePackageJsonPath = join(DEVKIT_ROOT, '..', 'web-components-core', 'package.json');
224
242
  const coreVersion = existsSync(corePackageJsonPath) ? getJsonFile(corePackageJsonPath).version : '1.0.0-rc12';
@@ -279,7 +297,10 @@ export async function run(args) {
279
297
  repo: `https://github.com/keenmate/${componentId}`,
280
298
  port: devPort,
281
299
  };
282
- if (args.register) {
300
+ if (isAdopting) {
301
+ if (!adoptedEntry.prefix) console.log(`
302
+ The registry entry has no prefix — add "prefix": "${prefix}" to its components.json entry.`);
303
+ } else if (args.register) {
283
304
  const registryPath = join(DEVKIT_ROOT, 'components.json');
284
305
  const registryJson = getJsonFile(registryPath);
285
306
  const lastComponentIndex = registryJson.components.map((entry) => entry.kind).lastIndexOf('component');
package/src/cli/kmwc.js CHANGED
@@ -41,6 +41,7 @@ Usage: kmwc <command> [options]
41
41
  new <name> Scaffold a new component (e.g. \`kmwc new web-colorpicker --prefix cpk\`).
42
42
  --title "…" --tag … --prefix … --dir … --port … --register
43
43
  --minimal: empty skeleton (no inputs/events) instead of the sample
44
+ --adopt: an already registered component (migration scratch scaffold)
44
45
  readme [--check] Regenerate the README catalogue from components.json (devkit repo).
45
46
 
46
47
  Global: --root <dir> Act on <dir> instead of the current directory.`);
package/src/config.d.ts CHANGED
@@ -123,6 +123,8 @@ export interface KmwcConfig {
123
123
 
124
124
  export const DEVKIT_ROOT: string;
125
125
  export const DEFAULT_RULEBOOK_PATH: string;
126
+ /** The nearest `<ancestor>/BlissFramework/guidelines/web-components` above `startPath`, or null. */
127
+ export function getRulebookPath(startPath: string): string | null;
126
128
  export const LEGACY_FILES: string[];
127
129
  export const PACKAGE_LEAK_RULES: Array<{ pattern: RegExp; label: string }>;
128
130
  export const PACKAGE_FILES_EXCLUSIONS: string[];
package/src/config.js CHANGED
@@ -19,8 +19,28 @@ import { createRequire } from 'node:module';
19
19
  /** Absolute path of the devkit package root (this repo). */
20
20
  export const DEVKIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
21
21
 
22
- /** Default rulebook location: a BlissFramework checkout next to the KM folder. */
23
- export const DEFAULT_RULEBOOK_PATH = resolve(DEVKIT_ROOT, '..', '..', 'BlissFramework', 'guidelines', 'web-components');
22
+ const RULEBOOK_RELATIVE_PATH = join('BlissFramework', 'guidelines', 'web-components');
23
+
24
+ /**
25
+ * The nearest BlissFramework rulebook checkout: `<ancestor>/BlissFramework/
26
+ * guidelines/web-components` for the first ancestor of `startPath` that has one
27
+ * (e.g. C:\Git\KM\web-editor → C:\Git\BlissFramework\…), or null. Searching
28
+ * from the component (not a fixed path from the devkit) is what keeps it
29
+ * working when the devkit is installed into node_modules from npm.
30
+ */
31
+ export function getRulebookPath(startPath) {
32
+ let directory = resolve(startPath);
33
+ for (;;) {
34
+ const candidatePath = join(directory, RULEBOOK_RELATIVE_PATH);
35
+ if (existsSync(candidatePath)) return candidatePath;
36
+ const parentDirectory = dirname(directory);
37
+ if (parentDirectory === directory) return null;
38
+ directory = parentDirectory;
39
+ }
40
+ }
41
+
42
+ /** Fallback rulebook location: the nearest checkout above the devkit, else next to the KM folder. */
43
+ export const DEFAULT_RULEBOOK_PATH = getRulebookPath(DEVKIT_ROOT) ?? resolve(DEVKIT_ROOT, '..', '..', RULEBOOK_RELATIVE_PATH);
24
44
 
25
45
  /** Files the devkit now owns — finding one of these in a component is a doctor error. */
26
46
  export const LEGACY_FILES = [
@@ -235,7 +255,9 @@ export function mapKmwcConfig(packageJson, root = process.cwd()) {
235
255
  checkMode: cssVariables.checkMode || 'declared',
236
256
  },
237
257
  rulebook: {
238
- path: process.env.KMWC_RULEBOOK_PATH || rulebook.path || DEFAULT_RULEBOOK_PATH,
258
+ path:
259
+ process.env.KMWC_RULEBOOK_PATH ||
260
+ (rulebook.path ? resolve(root, rulebook.path) : getRulebookPath(root) ?? DEFAULT_RULEBOOK_PATH),
239
261
  skippedChecks: rulebook.skippedChecks || [],
240
262
  },
241
263
  sync: {
@@ -176,7 +176,7 @@
176
176
  button.secondary,
177
177
  button.btn-secondary {
178
178
  background: var(--kmwc-neutral-color);
179
- color: var(--kmwc-text-color-on-accent);
179
+ color: var(--kmwc-text-color-on-neutral);
180
180
  }
181
181
 
182
182
  button.secondary:hover,
@@ -195,11 +195,11 @@
195
195
  }
196
196
 
197
197
  button.btn-danger {
198
- background: var(--kmwc-danger-color);
199
- color: var(--kmwc-text-color-on-accent);
198
+ background: var(--kmwc-danger-fill-color);
199
+ color: var(--kmwc-text-color-on-danger);
200
200
  }
201
201
 
202
202
  button.btn-danger:hover {
203
- background: color-mix(in srgb, var(--kmwc-danger-color) 85%, black);
203
+ background: var(--kmwc-danger-fill-color-hover);
204
204
  }
205
205
  }
@@ -93,7 +93,7 @@
93
93
  .kmwc-index__badge {
94
94
  display: inline-block;
95
95
  background: var(--kmwc-badge-new-bg);
96
- color: var(--kmwc-text-color-on-accent);
96
+ color: var(--kmwc-text-color-on-badge-new);
97
97
  padding: 0.15rem 0.5rem;
98
98
  border-radius: var(--kmwc-radius-sm);
99
99
  font-size: 0.65rem;
@@ -9,6 +9,11 @@
9
9
  component together — demo pages demonstrate cross-component theming;
10
10
  • with no --base-* set, the page still looks right, in light and dark.
11
11
 
12
+ Fallbacks are the contract's own (@keenmate/base-css-variables): an input
13
+ falls back to its default, a derived token to the contract formula on the
14
+ --kmwc-* tokens, so a page looks the same with and without the contract.
15
+ `npm run check:contract` (base-contract-check) enforces it.
16
+
12
17
  Unlike component variables (theme-container.md: never on :root), page
13
18
  tokens DO live on :root — the page is the scope here. Feature files read
14
19
  only --kmwc-* tokens; colour literals live in this file alone.
@@ -38,42 +43,42 @@
38
43
  /* ======================================================================
39
44
  COLORS — ACCENT
40
45
  ====================================================================== */
41
- --kmwc-accent-color: var(--base-accent-color, #667eea);
42
- --kmwc-accent-end-color: #764ba2;
43
- --kmwc-accent-color-hover: var(--base-accent-color-hover, color-mix(in srgb, var(--kmwc-accent-color) 88%, black));
44
- --kmwc-accent-color-active: var(--base-accent-color-active, color-mix(in srgb, var(--kmwc-accent-color) 72%, black));
45
- --kmwc-accent-light-bg: var(--base-accent-color-light, color-mix(in srgb, var(--kmwc-accent-color) 12%, transparent));
46
- --kmwc-accent-light-bg-hover: var(--base-accent-color-light-hover, color-mix(in srgb, var(--kmwc-accent-color) 20%, transparent));
46
+ --kmwc-accent-color: var(--base-primary-color, #3b82f6);
47
+ --kmwc-accent-end-color: oklch(from var(--kmwc-accent-color) calc(l - 0.12) c calc(h + 25)); /* header gradient end */
48
+ --kmwc-accent-color-hover: var(--base-primary-color-hover, light-dark(color-mix(in srgb, black 15%, var(--kmwc-accent-color)), color-mix(in srgb, white 15%, var(--kmwc-accent-color))));
49
+ --kmwc-accent-color-active: var(--base-primary-color-active, light-dark(color-mix(in srgb, black 30%, var(--kmwc-accent-color)), color-mix(in srgb, white 30%, var(--kmwc-accent-color))));
50
+ --kmwc-accent-light-bg: var(--base-primary-bg-subtle, light-dark(color-mix(in srgb, var(--kmwc-accent-color) 10%, transparent), color-mix(in srgb, var(--kmwc-accent-color) 20%, transparent)));
51
+ --kmwc-accent-light-bg-hover: var(--base-primary-bg-subtle-hover, light-dark(color-mix(in srgb, var(--kmwc-accent-color) 15%, transparent), color-mix(in srgb, var(--kmwc-accent-color) 30%, transparent)));
47
52
  --kmwc-accent-border-color: color-mix(in srgb, var(--kmwc-accent-color) 40%, transparent);
48
53
  --kmwc-accent-shadow-color: color-mix(in srgb, var(--kmwc-accent-color) 35%, transparent);
49
- --kmwc-text-color-on-accent: var(--base-text-color-on-accent, #ffffff);
54
+ --kmwc-text-color-on-accent: var(--base-on-primary-text-color, color(from color(from var(--kmwc-accent-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)));
50
55
  --kmwc-header-bg: linear-gradient(135deg, var(--kmwc-accent-color) 0%, var(--kmwc-accent-end-color) 100%);
51
56
 
52
57
  /* ======================================================================
53
58
  COLORS — TEXT
54
59
  ====================================================================== */
55
- --kmwc-text-color-1: var(--base-text-color-1, light-dark(#2d3748, #f5f5f5));
56
- --kmwc-text-color-2: var(--base-text-color-2, light-dark(#4a5568, #d4d4d4));
57
- --kmwc-text-color-3: var(--base-text-color-3, light-dark(#718096, #a3a3a3));
58
- --kmwc-text-color-4: var(--base-text-color-4, light-dark(#9ca3af, #737373));
60
+ --kmwc-text-color-1: var(--base-text-color-1, light-dark(#334155, #f1f5f9));
61
+ --kmwc-text-color-2: var(--base-text-color-2, color-mix(in srgb, var(--kmwc-text-color-1) 75%, var(--kmwc-main-bg)));
62
+ --kmwc-text-color-3: var(--base-text-color-3, color-mix(in srgb, var(--kmwc-text-color-1) 55%, var(--kmwc-main-bg)));
63
+ --kmwc-text-color-4: var(--base-text-color-4, color-mix(in srgb, var(--kmwc-text-color-1) 40%, var(--kmwc-main-bg)));
59
64
 
60
65
  /* ======================================================================
61
66
  COLORS — SURFACE
62
67
  ====================================================================== */
63
- --kmwc-page-bg: light-dark(#f7fafc, #111317);
64
- --kmwc-main-bg: var(--base-main-bg, light-dark(#ffffff, #1a1a1a));
65
- --kmwc-elevated-bg: var(--base-elevated-bg, light-dark(#edf2f7, #2b2b2b));
66
- --kmwc-hover-bg: var(--base-hover-bg, color-mix(in srgb, var(--kmwc-text-color-1) 6%, var(--kmwc-main-bg)));
67
- --kmwc-border-color: var(--base-border-color, light-dark(#e2e8f0, #3d3d3d));
68
- --kmwc-border-color-strong: light-dark(#cbd5e0, #525252);
69
- --kmwc-shadow-color: light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.45));
68
+ --kmwc-page-bg: var(--base-page-bg, light-dark(#f8f9fa, #0f172a));
69
+ --kmwc-main-bg: var(--base-main-bg, light-dark(#ffffff, #1e293b));
70
+ --kmwc-elevated-bg: var(--base-elevated-bg, light-dark(oklch(from var(--kmwc-main-bg) calc(l - 0.03) c h), oklch(from var(--kmwc-main-bg) calc(l + 0.06) c h)));
71
+ --kmwc-hover-bg: var(--base-hover-bg, light-dark(color-mix(in oklch, oklch(from var(--kmwc-main-bg) calc(l - 0.45) c h) 14%, transparent), color-mix(in oklch, oklch(from var(--kmwc-main-bg) calc(l + 0.45) c h) 28%, transparent)));
72
+ --kmwc-border-color: var(--base-border-color, light-dark(oklch(from var(--kmwc-main-bg) calc(l - 0.096) c h), oklch(from var(--kmwc-main-bg) calc(l + 0.192) c h)));
73
+ --kmwc-border-color-strong: var(--base-input-border-color, light-dark(color-mix(in srgb, var(--kmwc-text-color-1) 45%, var(--kmwc-main-bg)), color-mix(in srgb, var(--kmwc-text-color-1) 35%, var(--kmwc-main-bg)))); /* control outline */
74
+ --kmwc-shadow-color: var(--base-shadow-color, light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.4)));
70
75
 
71
76
  /* ======================================================================
72
77
  COLORS — INPUTS & CODE
73
78
  ====================================================================== */
74
- --kmwc-input-bg: var(--base-input-bg, light-dark(#ffffff, #1f1f1f));
75
- --kmwc-input-color: var(--base-input-color, var(--kmwc-text-color-1));
76
- --kmwc-input-border: var(--base-input-border, 1px solid light-dark(#d1d5db, #5a5a5a));
79
+ --kmwc-input-bg: var(--base-input-bg, var(--kmwc-main-bg));
80
+ --kmwc-input-color: var(--base-input-text-color, var(--kmwc-text-color-1));
81
+ --kmwc-input-border: var(--base-input-border, 1px solid var(--kmwc-border-color-strong));
77
82
  --kmwc-code-bg: light-dark(#2d3748, #0d1117);
78
83
  --kmwc-code-text-color: #e2e8f0;
79
84
  --kmwc-code-comment-color: #8b949e;
@@ -90,18 +95,27 @@
90
95
  /* ======================================================================
91
96
  COLORS — SEMANTIC
92
97
  ====================================================================== */
93
- --kmwc-success-color: light-dark(#2f855a, #68d391);
94
- --kmwc-warning-color: light-dark(#b45309, #fbbf24);
95
- --kmwc-danger-color: var(--base-danger-color, light-dark(#dc2626, #f87c86));
96
- --kmwc-info-color: light-dark(#2563eb, #60a5fa);
98
+ /* -color = the role as text on a surface (notes, feedback); the danger
99
+ button fills with --kmwc-danger-fill-color. */
100
+ --kmwc-success-color: var(--base-success-text-color, light-dark(#155724, #34d399));
101
+ --kmwc-warning-color: var(--base-warning-text-color, light-dark(#856404, #fbbf24));
102
+ --kmwc-danger-color: var(--base-danger-text-color, light-dark(#721c24, #f87171));
103
+ --kmwc-info-color: var(--base-info-text-color, light-dark(#0c5460, #22d3ee));
104
+ --kmwc-danger-fill-color: var(--base-danger-color, #dc2626);
105
+ --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))));
106
+ --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)));
97
107
  --kmwc-neutral-color: light-dark(#6b7280, #9ca3af);
98
108
  --kmwc-badge-new-bg: light-dark(#e53e3e, #f56565);
109
+ /* Text on the neutral / badge fills: the contract's on-colour formula on
110
+ their own background, not on the accent (a light primary would turn it black). */
111
+ --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));
112
+ --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));
99
113
 
100
114
  /* ======================================================================
101
115
  TYPOGRAPHY & SHAPE
102
116
  ====================================================================== */
103
- --kmwc-font-family: var(--base-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif);
104
- --kmwc-mono-font-family: ui-monospace, SFMono-Regular, Menlo, 'Courier New', monospace;
117
+ --kmwc-font-family: var(--base-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif);
118
+ --kmwc-mono-font-family: var(--base-font-family-mono, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
105
119
  --kmwc-radius-lg: 12px;
106
120
  --kmwc-radius-md: 8px;
107
121
  --kmwc-radius-sm: 6px;
@@ -18,7 +18,8 @@
18
18
  * kmwc:layout page (default) | bare (no header / container — e.g. the index)
19
19
  * kmwc:entry component entry script; default /<library.entry>, "none" to skip
20
20
  * kmwc:body-class classes for <body> (e.g. dark-output)
21
- * <link> and <style> elements anywhere in a fragment move into <head>.
21
+ * <link> and <style> elements anywhere in a fragment move into <head>
22
+ * (not those inside a <script> — there they are strings, e.g. shadow-DOM styles).
22
23
  * Full documents (<html …>) are left as they are — pages migrate one at a time.
23
24
  *
24
25
  * Pure string work on purpose: unit-testable, no HTML parser dependency. The
@@ -42,6 +43,9 @@ const HEAD_ELEMENT_PATTERN = /<link\b[^>]*>\s*|<style\b[^>]*>[\s\S]*?<\/style>\s
42
43
  const ATTRIBUTE_PATTERN = /([a-zA-Z][\w-]*)(?:\s*=\s*"([^"]*)")?/g;
43
44
 
44
45
  const HTML_COMMENT_PATTERN = /<!--[\s\S]*?-->/g;
46
+ // Comments plus whole <script> elements: a <style> / <link> inside a script is a string
47
+ // (shadow-DOM styles, an SVG's own <style>), never a page element to move into <head>.
48
+ const COMMENT_OR_SCRIPT_PATTERN = /<!--[\s\S]*?-->|<script\b[^>]*>[\s\S]*?<\/script>/gi;
45
49
  // A full document STARTS with a doctype or <html> (after a BOM, whitespace, comments). Matching
46
50
  // `<html>` anywhere would misread a fragment whose script or code sample mentions it.
47
51
  const DOCUMENT_START_PATTERN = /^?(?:\s|<!--[\s\S]*?-->)*<(?:!doctype\b|html[\s>])/i;
@@ -50,12 +54,12 @@ const DOCUMENT_START_PATTERN = /^?(?:\s|<!--[\s\S]*?-->)*<(?:!doctype\b|html[
50
54
  * Apply `mapper` to the text OUTSIDE HTML comments only, so a usage example in
51
55
  * a comment (`<!-- <kmwc-include src="…"> -->`) is never expanded.
52
56
  */
53
- export function mapOutsideComments(html, mapper) {
57
+ export function mapOutsideComments(html, mapper, skippedPattern = HTML_COMMENT_PATTERN) {
54
58
  let mappedHtml = '';
55
59
  let lastIndex = 0;
56
- for (const comment of html.matchAll(HTML_COMMENT_PATTERN)) {
57
- mappedHtml += mapper(html.slice(lastIndex, comment.index)) + comment[0];
58
- lastIndex = comment.index + comment[0].length;
60
+ for (const skipped of html.matchAll(skippedPattern)) {
61
+ mappedHtml += mapper(html.slice(lastIndex, skipped.index)) + skipped[0];
62
+ lastIndex = skipped.index + skipped[0].length;
59
63
  }
60
64
  return mappedHtml + mapper(html.slice(lastIndex));
61
65
  }
@@ -165,11 +169,14 @@ export function renderLayout(fragmentHtml, { config, pagePath }) {
165
169
  if (layout === 'page' && !pageMeta.title) throw new Error(`kmwc: ${pagePath}: a fragment page needs <meta name="kmwc:title" content="…">`);
166
170
 
167
171
  const headElements = [];
168
- const bodyContent = mapOutsideComments(htmlWithoutMeta, (text) =>
169
- text.replace(HEAD_ELEMENT_PATTERN, (element) => {
170
- headElements.push(` ${element.trim()}`);
171
- return '';
172
- })
172
+ const bodyContent = mapOutsideComments(
173
+ htmlWithoutMeta,
174
+ (text) =>
175
+ text.replace(HEAD_ELEMENT_PATTERN, (element) => {
176
+ headElements.push(` ${element.trim()}`);
177
+ return '';
178
+ }),
179
+ COMMENT_OR_SCRIPT_PATTERN
173
180
  );
174
181
 
175
182
  const entry = pageMeta.entry ?? `/${config.library.entry.replace(/^\.?\//, '')}`;
@@ -6,9 +6,9 @@
6
6
  { "name": "base-main-bg", "required": false, "usage": "Component surface" },
7
7
  { "name": "base-text-color-1", "required": false, "usage": "Label text" },
8
8
  { "name": "base-border-color", "required": false, "usage": "Button border" },
9
- { "name": "base-accent-color", "required": false, "usage": "Active state fill, focus outline" },
10
- { "name": "base-text-color-on-accent", "required": false, "usage": "Label text in the active state" },
11
- { "name": "base-hover-bg", "required": false, "usage": "Hover background (falls back to a text/surface color-mix)" },
9
+ { "name": "base-primary-color", "required": false, "usage": "Active state fill, focus outline" },
10
+ { "name": "base-on-primary-text-color", "required": false, "usage": "Label text in the active state" },
11
+ { "name": "base-hover-bg", "required": false, "usage": "Hover background (falls back to the contract formula on --{{PREFIX}}-bg)" },
12
12
  { "name": "base-font-family", "required": false, "usage": "Font family" },
13
13
  { "name": "base-font-size-base", "required": false, "usage": "Font size (multiplier × --{{PREFIX}}-rem)" },
14
14
  { "name": "base-border-radius-md", "required": false, "usage": "Corner radius (multiplier × --{{PREFIX}}-rem)" },
@@ -27,7 +27,7 @@ Combines a `--base-*` override (all components) with a `--{{PREFIX}}-*` one
27
27
 
28
28
  ```html
29
29
  <style>
30
- :root { --base-accent-color: #10b981; }
30
+ :root { --base-primary-color: #10b981; }
31
31
  </style>
32
32
  <{{TAG}} label="Dark" data-theme="dark" style="--{{PREFIX}}-root-border-radius: 999px"></{{TAG}}>
33
33
  ```
@@ -21,7 +21,7 @@ follow the page font size).
21
21
 
22
22
  ```css
23
23
  /* every Keenmate component on the page */
24
- :root { --base-accent-color: #10b981; }
24
+ :root { --base-primary-color: #10b981; }
25
25
 
26
26
  /* this component only */
27
27
  {{TAG}} { --{{PREFIX}}-accent-color: #8b5cf6; }
@@ -9,18 +9,18 @@ test.describe('<{{TAG}}> dark mode', () => {
9
9
  test('defaults to light, per-instance data-theme="dark" goes dark', async ({ page }) => {
10
10
  await page.goto('/test/dark-mode.html');
11
11
  expect(await getHostBackground(page, '#light-default')).toBe('rgb(255, 255, 255)');
12
- expect(await getHostBackground(page, '#dark-instance')).toBe('rgb(26, 26, 26)');
12
+ expect(await getHostBackground(page, '#dark-instance')).toBe('rgb(30, 41, 59)');
13
13
  });
14
14
 
15
15
  test('an ancestor data-theme="dark" reaches the component', async ({ page }) => {
16
16
  await page.goto('/test/dark-mode.html');
17
- expect(await getHostBackground(page, '#dark-ancestor')).toBe('rgb(26, 26, 26)');
17
+ expect(await getHostBackground(page, '#dark-ancestor')).toBe('rgb(30, 41, 59)');
18
18
  });
19
19
 
20
20
  test('a page color-scheme of dark is inherited', async ({ page }) => {
21
21
  await page.goto('/test/dark-mode.html');
22
22
  await page.evaluate(() => (document.documentElement.style.colorScheme = 'dark'));
23
- expect(await getHostBackground(page, '#light-default')).toBe('rgb(26, 26, 26)');
23
+ expect(await getHostBackground(page, '#light-default')).toBe('rgb(30, 41, 59)');
24
24
  });
25
25
 
26
26
  test('--base-* themes the component', async ({ page }) => {
@@ -18,10 +18,10 @@
18
18
 
19
19
  <div class="card">
20
20
  <h2>TH02 · Variable overrides</h2>
21
- <p><code>--base-accent-color</code> themes every Keenmate component in a subtree; <code>--{{PREFIX}}-accent-color</code> one instance.</p>
21
+ <p><code>--base-primary-color</code> themes every Keenmate component in a subtree; <code>--{{PREFIX}}-accent-color</code> one instance.</p>
22
22
  <kmwc-demo>
23
- <div style="--base-accent-color: #10b981">
24
- <{{TAG}} id="th-base" label="--base-accent-color" active></{{TAG}}>
23
+ <div style="--base-primary-color: #10b981">
24
+ <{{TAG}} id="th-base" label="--base-primary-color" active></{{TAG}}>
25
25
  <{{TAG}} id="th-local" label="--{{PREFIX}}-accent-color" active style="--{{PREFIX}}-accent-color: #8b5cf6"></{{TAG}}>
26
26
  </div>
27
27
  </kmwc-demo>
@@ -2,8 +2,10 @@
2
2
  CSS CUSTOM PROPERTIES (:host level)
3
3
  ==============================================================================
4
4
  Every --{{PREFIX}}-* variable reads through the cross-component --base-* hook
5
- with a standalone fallback; colours fall back to light-dark(), so the
6
- component follows the page's color-scheme. `color-scheme` is deliberately
5
+ with a standalone fallback: an input falls back to the contract default, a
6
+ derived token to the contract formula on this component's own tokens
7
+ (base-contract-check). Colours use light-dark(), so the component follows
8
+ the page's color-scheme. `color-scheme` is deliberately
7
9
  NOT declared here (it would shadow the page's) — see dark-mode.css.
8
10
 
9
11
  Override from outside the shadow DOM:
@@ -21,12 +23,12 @@
21
23
  /* ==========================================================================
22
24
  COLORS
23
25
  ========================================================================== */
24
- --{{PREFIX}}-bg: var(--base-main-bg, light-dark(#ffffff, #1a1a1a));
25
- --{{PREFIX}}-text-color-1: var(--base-text-color-1, light-dark(#242424, #f5f5f5));
26
- --{{PREFIX}}-border-color: var(--base-border-color, light-dark(#e0e0e0, #3d3d3d));
27
- --{{PREFIX}}-accent-color: var(--base-accent-color, light-dark(#0078d4, #3a96dd));
28
- --{{PREFIX}}-text-color-on-accent: var(--base-text-color-on-accent, light-dark(#ffffff, #ffffff));
29
- --{{PREFIX}}-hover-bg: var(--base-hover-bg, color-mix(in srgb, var(--{{PREFIX}}-text-color-1) 8%, var(--{{PREFIX}}-bg)));
26
+ --{{PREFIX}}-bg: var(--base-main-bg, light-dark(#ffffff, #1e293b));
27
+ --{{PREFIX}}-text-color-1: var(--base-text-color-1, light-dark(#334155, #f1f5f9));
28
+ --{{PREFIX}}-border-color: var(--base-border-color, light-dark(oklch(from var(--{{PREFIX}}-bg) calc(l - 0.096) c h), oklch(from var(--{{PREFIX}}-bg) calc(l + 0.192) c h)));
29
+ --{{PREFIX}}-accent-color: var(--base-primary-color, #3b82f6);
30
+ --{{PREFIX}}-text-color-on-accent: var(--base-on-primary-text-color, color(from color(from var(--{{PREFIX}}-accent-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)));
31
+ --{{PREFIX}}-hover-bg: var(--base-hover-bg, light-dark(color-mix(in oklch, oklch(from var(--{{PREFIX}}-bg) calc(l - 0.45) c h) 14%, transparent), color-mix(in oklch, oklch(from var(--{{PREFIX}}-bg) calc(l + 0.45) c h) 28%, transparent)));
30
32
 
31
33
  /* ==========================================================================
32
34
  TYPOGRAPHY & SHAPE
@@ -2,8 +2,10 @@
2
2
  CSS CUSTOM PROPERTIES (:host level)
3
3
  ==============================================================================
4
4
  Every --{{PREFIX}}-* variable reads through the cross-component --base-* hook
5
- with a standalone fallback; colours fall back to light-dark(), so the
6
- component follows the page's color-scheme. `color-scheme` is deliberately
5
+ with a standalone fallback: an input falls back to the contract default, a
6
+ derived token to the contract formula on this component's own tokens
7
+ (base-contract-check). Colours use light-dark(), so the component follows
8
+ the page's color-scheme. `color-scheme` is deliberately
7
9
  NOT declared here (it would shadow the page's) — see dark-mode.css.
8
10
 
9
11
  Override from outside the shadow DOM:
@@ -14,9 +16,9 @@
14
16
  /* Unitless --base-* sizes multiply through --{{PREFIX}}-rem. */
15
17
  --{{PREFIX}}-rem: 10px;
16
18
 
17
- --{{PREFIX}}-bg: var(--base-main-bg, light-dark(#ffffff, #1a1a1a));
18
- --{{PREFIX}}-text-color-1: var(--base-text-color-1, light-dark(#242424, #f5f5f5));
19
- --{{PREFIX}}-border-color: var(--base-border-color, light-dark(#e0e0e0, #3d3d3d));
19
+ --{{PREFIX}}-bg: var(--base-main-bg, light-dark(#ffffff, #1e293b));
20
+ --{{PREFIX}}-text-color-1: var(--base-text-color-1, light-dark(#334155, #f1f5f9));
21
+ --{{PREFIX}}-border-color: var(--base-border-color, light-dark(oklch(from var(--{{PREFIX}}-bg) calc(l - 0.096) c h), oklch(from var(--{{PREFIX}}-bg) calc(l + 0.192) c h)));
20
22
 
21
23
  --{{PREFIX}}-font-family: var(--base-font-family, inherit);
22
24
  --{{PREFIX}}-font-size-base: calc(var(--base-font-size-base, 1.6) * var(--{{PREFIX}}-rem));