@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.
- package/CHANGELOG.md +114 -0
- package/README.md +70 -12
- package/components.json +206 -189
- package/package.json +4 -1
- package/src/cli/commands/doctor.js +1 -0
- package/src/cli/commands/new.js +29 -8
- package/src/cli/kmwc.js +1 -0
- package/src/config.d.ts +5 -0
- package/src/config.js +26 -3
- package/src/examples/chapter-links.js +35 -0
- package/src/examples/chapter-search.js +84 -0
- package/src/examples/constants.js +51 -0
- package/src/examples/css/content.css +4 -34
- package/src/examples/css/controls.css +30 -25
- package/src/examples/css/copy-link.css +1 -0
- package/src/examples/css/demo.css +2 -0
- package/src/examples/css/form-controls.css +285 -0
- package/src/examples/css/index-page.css +1 -1
- package/src/examples/css/spotlight.css +417 -0
- package/src/examples/css/style.css +3 -0
- package/src/examples/css/tables.css +141 -0
- package/src/examples/css/tokens.css +69 -27
- package/src/examples/index.d.ts +38 -0
- package/src/examples/index.js +6 -3
- package/src/examples/kit.js +11 -4
- package/src/examples/page-config.js +1 -0
- package/src/examples/sections.js +38 -1
- package/src/examples/spotlight.js +473 -0
- package/src/vite/chapter-index.js +107 -0
- package/src/vite/index.js +19 -1
- package/src/vite/page-composer.js +17 -10
- package/templates/claude-block.md +9 -2
- package/templates/new/component-variables.manifest.json +3 -3
- package/templates/new/docs/examples.md +1 -1
- package/templates/new/docs/theming.md +1 -1
- package/templates/new/e2e/dark-mode.spec.ts +3 -3
- package/templates/new/examples-theming.html +3 -3
- package/templates/new/src/css/base.css +5 -3
- package/templates/new/src/css/variables.css +10 -8
- package/templates/new-minimal/src/css/base.css +5 -3
- 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
|
|
57
|
-
mappedHtml += mapper(html.slice(lastIndex,
|
|
58
|
-
lastIndex =
|
|
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(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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.
|
|
25
|
-
|
|
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-
|
|
10
|
-
{ "name": "base-text-color
|
|
11
|
-
{ "name": "base-hover-bg", "required": false, "usage": "Hover background (falls back to
|
|
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-
|
|
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
|
```
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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-
|
|
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-
|
|
24
|
-
<{{TAG}} id="th-base" label="--base-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
6
|
-
|
|
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, #
|
|
25
|
-
--{{PREFIX}}-text-color-1: var(--base-text-color-1, light-dark(#
|
|
26
|
-
--{{PREFIX}}-border-color: var(--base-border-color, light-dark(
|
|
27
|
-
--{{PREFIX}}-accent-color: var(--base-
|
|
28
|
-
--{{PREFIX}}-text-color-on-accent: var(--base-text-color-
|
|
29
|
-
--{{PREFIX}}-hover-bg: var(--base-hover-bg, color-mix(in
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
6
|
-
|
|
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, #
|
|
18
|
-
--{{PREFIX}}-text-color-1: var(--base-text-color-1, light-dark(#
|
|
19
|
-
--{{PREFIX}}-border-color: var(--base-border-color, light-dark(
|
|
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));
|