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

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +70 -12
  3. package/components.json +206 -189
  4. package/package.json +4 -1
  5. package/src/cli/commands/doctor.js +1 -0
  6. package/src/cli/commands/new.js +29 -8
  7. package/src/cli/kmwc.js +1 -0
  8. package/src/config.d.ts +5 -0
  9. package/src/config.js +26 -3
  10. package/src/examples/chapter-links.js +35 -0
  11. package/src/examples/chapter-search.js +84 -0
  12. package/src/examples/constants.js +51 -0
  13. package/src/examples/css/content.css +4 -34
  14. package/src/examples/css/controls.css +30 -25
  15. package/src/examples/css/copy-link.css +1 -0
  16. package/src/examples/css/demo.css +2 -0
  17. package/src/examples/css/form-controls.css +285 -0
  18. package/src/examples/css/index-page.css +1 -1
  19. package/src/examples/css/spotlight.css +417 -0
  20. package/src/examples/css/style.css +3 -0
  21. package/src/examples/css/tables.css +141 -0
  22. package/src/examples/css/tokens.css +69 -27
  23. package/src/examples/index.d.ts +38 -0
  24. package/src/examples/index.js +6 -3
  25. package/src/examples/kit.js +11 -4
  26. package/src/examples/page-config.js +1 -0
  27. package/src/examples/sections.js +38 -1
  28. package/src/examples/spotlight.js +473 -0
  29. package/src/vite/chapter-index.js +107 -0
  30. package/src/vite/index.js +19 -1
  31. package/src/vite/page-composer.js +17 -10
  32. package/templates/claude-block.md +9 -2
  33. package/templates/new/component-variables.manifest.json +3 -3
  34. package/templates/new/docs/examples.md +1 -1
  35. package/templates/new/docs/theming.md +1 -1
  36. package/templates/new/e2e/dark-mode.spec.ts +3 -3
  37. package/templates/new/examples-theming.html +3 -3
  38. package/templates/new/src/css/base.css +5 -3
  39. package/templates/new/src/css/variables.css +10 -8
  40. package/templates/new-minimal/src/css/base.css +5 -3
  41. package/templates/new-minimal/src/css/variables.css +7 -5
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-rc03",
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,8 @@
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",
51
+ "check:spotlight-mirror": "node scripts/check-spotlight-mirror.js",
50
52
  "playground": "cd playground && vite",
51
53
  "playground:build": "cd playground && vite build --mode examples",
52
54
  "playground:lib": "cd playground && vite build"
@@ -65,6 +67,7 @@
65
67
  }
66
68
  },
67
69
  "devDependencies": {
70
+ "@keenmate/base-css-variables": "^2.0.0-rc07",
68
71
  "typescript": "^5.9.3",
69
72
  "vite": "^7.3.7"
70
73
  },
@@ -37,6 +37,7 @@ export const RULEBOOK_SUITES = [
37
37
  { name: 'custom-elements-manifest', isPrefixed: false },
38
38
  { name: 'responsive-overlay', isPrefixed: true },
39
39
  { name: 'publish-command', isPrefixed: false },
40
+ { name: 'example-pages', isPrefixed: false },
40
41
  ];
41
42
 
42
43
  function getMajorVersion(versionRange) {
@@ -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
@@ -38,6 +38,8 @@ export interface KmwcPackageConfig {
38
38
  excludedPages?: string[];
39
39
  /** localStorage namespace for the controls persistence; defaults to `id`. */
40
40
  storagePrefix?: string;
41
+ /** The Ctrl/Cmd+K chapter spotlight (search every page's chapters); default true. */
42
+ isSpotlightEnabled?: boolean;
41
43
  };
42
44
  analytics?: { plausibleSiteId?: string; plausibleHostUrl?: string };
43
45
  image?: {
@@ -101,6 +103,7 @@ export interface KmwcConfig {
101
103
  staticPaths: string[];
102
104
  excludedPages: string[];
103
105
  storagePrefix: string;
106
+ isSpotlightEnabled: boolean;
104
107
  };
105
108
  analytics: { plausibleSiteId: string; plausibleHostUrl: string };
106
109
  image: {
@@ -123,6 +126,8 @@ export interface KmwcConfig {
123
126
 
124
127
  export const DEVKIT_ROOT: string;
125
128
  export const DEFAULT_RULEBOOK_PATH: string;
129
+ /** The nearest `<ancestor>/BlissFramework/guidelines/web-components` above `startPath`, or null. */
130
+ export function getRulebookPath(startPath: string): string | null;
126
131
  export const LEGACY_FILES: string[];
127
132
  export const PACKAGE_LEAK_RULES: Array<{ pattern: RegExp; label: string }>;
128
133
  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 = [
@@ -215,6 +235,7 @@ export function mapKmwcConfig(packageJson, root = process.cwd()) {
215
235
  staticPaths: examples.staticPaths || [],
216
236
  excludedPages: examples.excludedPages || [],
217
237
  storagePrefix: examples.storagePrefix || id,
238
+ isSpotlightEnabled: examples.isSpotlightEnabled ?? true,
218
239
  },
219
240
  analytics: {
220
241
  plausibleSiteId: analytics.plausibleSiteId || '',
@@ -235,7 +256,9 @@ export function mapKmwcConfig(packageJson, root = process.cwd()) {
235
256
  checkMode: cssVariables.checkMode || 'declared',
236
257
  },
237
258
  rulebook: {
238
- path: process.env.KMWC_RULEBOOK_PATH || rulebook.path || DEFAULT_RULEBOOK_PATH,
259
+ path:
260
+ process.env.KMWC_RULEBOOK_PATH ||
261
+ (rulebook.path ? resolve(root, rulebook.path) : getRulebookPath(root) ?? DEFAULT_RULEBOOK_PATH),
239
262
  skippedChecks: rulebook.skippedChecks || [],
240
263
  },
241
264
  sync: {
@@ -0,0 +1,35 @@
1
+ /**
2
+ * CHAPTER LINKS — make every chapter reachable by a short link.
3
+ *
4
+ * Anchor ids are assigned by the kit after the page loads (sections.js), so the
5
+ * browser can't scroll to them on arrival by itself. This resolves the hash
6
+ * once the anchors exist, and on every hash change, accepting:
7
+ *
8
+ * #bu02-declarative-markup the anchor id (chapter nav, copy-link)
9
+ * #bu02 / #bu2 the section code — the link the codes exist for
10
+ * #summary the slug of an uncoded heading
11
+ *
12
+ * A hash that names some other element on the page is left to the browser.
13
+ */
14
+ import { ensureSectionAnchors, findSectionByHash } from './sections.js';
15
+
16
+ let isHashChangeAttached = false;
17
+
18
+ /**
19
+ * Scroll to the chapter `hash` points at.
20
+ * @returns {boolean} whether a chapter was found
21
+ */
22
+ export function scrollToChapterHash(hash = location.hash) {
23
+ const section = findSectionByHash(hash, ensureSectionAnchors());
24
+ if (!section) return false;
25
+ section.section.scrollIntoView({ block: 'start' });
26
+ return true;
27
+ }
28
+
29
+ /** Resolve the current hash now, and every later hash change. Idempotent. */
30
+ export function attachChapterLinks() {
31
+ if (location.hash) scrollToChapterHash();
32
+ if (isHashChangeAttached) return;
33
+ isHashChangeAttached = true;
34
+ window.addEventListener('hashchange', () => scrollToChapterHash());
35
+ }