@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
@@ -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(/^\.?\//, '')}`;
@@ -21,12 +21,19 @@ locally — every local fork is how they drifted apart.
21
21
  - **Example page styles:** shared rules are in `@layer kmwc` and read
22
22
  `--base-*` tokens (pages support light / dark / auto via the theme switch), so
23
23
  any unlayered rule wins. Put component-only rules in `examples-local.css`.
24
- Re-theme with `kmwc.theme.accentColor`, never by copying CSS. If a second
25
- component needs a pattern, it belongs in the devkit.
24
+ Re-theme with `kmwc.theme.accentColor`, never by copying CSS. Native form
25
+ controls (inputs, selects, checkboxes, radios, `role="switch"`, buttons) and
26
+ tables are already styled by the kit (`.reference-table`, `.table-striped`,
27
+ `.table-compact`, …) — don't restyle them per page; `kmwc-native` opts an
28
+ element out. If a second component needs a pattern, it belongs in the devkit.
26
29
  - **Example pages are fragments:** no `<html>`/`<head>`/`<header>` — start with
27
30
  `<meta name="kmwc:title" content="…">`, the devkit layout adds the rest. Shared text
28
31
  goes in `partials/` via `<kmwc-include>`; show code with `<kmwc-demo>` (the live
29
32
  markup IS the snippet) — never hand-copy a demo into a `.code-block`.
33
+ - **Every example has a permanent code:** each `<h2>` starts `TH07 · Title`
34
+ (page prefix + number; a plain `Summary` is exempt). New examples take the
35
+ next free number; never renumber or reuse one — the rules are the rulebook's
36
+ `example-pages.md`.
30
37
  - **Controls persistence:**
31
38
  `import { attachControlsPersistence } from '@keenmate/web-components-devkit/examples'`.
32
39
  **Landing page:** `renderExamplesIndex({ pages })` from the same module.
@@ -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>
@@ -36,8 +36,10 @@
36
36
  display: none;
37
37
  }
38
38
 
39
- *,
40
- *::before,
41
- *::after {
39
+ /* Scoped to :host — the whole file also reaches the page through index.ts's
40
+ CSS import, and a bare `*` would reset box-sizing on the host page too. */
41
+ :host *,
42
+ :host *::before,
43
+ :host *::after {
42
44
  box-sizing: inherit;
43
45
  }
@@ -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
@@ -35,9 +35,11 @@
35
35
  display: none;
36
36
  }
37
37
 
38
- *,
39
- *::before,
40
- *::after {
38
+ /* Scoped to :host — the whole file also reaches the page through index.ts's
39
+ CSS import, and a bare `*` would reset box-sizing on the host page too. */
40
+ :host *,
41
+ :host *::before,
42
+ :host *::after {
41
43
  box-sizing: inherit;
42
44
  }
43
45
 
@@ -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));