@keenmate/web-components-devkit 0.0.0-stage → 1.0.0-rc01

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 (109) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/LICENSE +21 -0
  3. package/README.md +417 -2
  4. package/components.json +189 -0
  5. package/package.json +75 -4
  6. package/src/cli/args.js +16 -0
  7. package/src/cli/commands/check-release.js +66 -0
  8. package/src/cli/commands/check-vars.js +116 -0
  9. package/src/cli/commands/doctor.js +289 -0
  10. package/src/cli/commands/new.js +302 -0
  11. package/src/cli/commands/readme.js +111 -0
  12. package/src/cli/commands/sync.js +48 -0
  13. package/src/cli/kmwc.js +72 -0
  14. package/src/cli/sync-plan.js +163 -0
  15. package/src/config.d.ts +147 -0
  16. package/src/config.js +257 -0
  17. package/src/examples/auto.js +20 -0
  18. package/src/examples/chapter-nav.js +246 -0
  19. package/src/examples/constants.js +80 -0
  20. package/src/examples/controls-persistence.js +97 -0
  21. package/src/examples/copy-link.js +89 -0
  22. package/src/examples/css/base.css +215 -0
  23. package/src/examples/css/chapter-nav.css +179 -0
  24. package/src/examples/css/content.css +251 -0
  25. package/src/examples/css/controls.css +205 -0
  26. package/src/examples/css/copy-link.css +82 -0
  27. package/src/examples/css/demo.css +50 -0
  28. package/src/examples/css/index-page.css +118 -0
  29. package/src/examples/css/style.css +30 -0
  30. package/src/examples/css/theme-switch.css +38 -0
  31. package/src/examples/css/tokens.css +110 -0
  32. package/src/examples/css/widgets.css +133 -0
  33. package/src/examples/demo-code.js +30 -0
  34. package/src/examples/index-page.js +78 -0
  35. package/src/examples/index.d.ts +107 -0
  36. package/src/examples/index.js +18 -0
  37. package/src/examples/kit.js +22 -0
  38. package/src/examples/page-config.js +30 -0
  39. package/src/examples/sections.js +53 -0
  40. package/src/examples/theme-switch.js +72 -0
  41. package/src/examples/version-badge.js +23 -0
  42. package/src/playwright/index.d.ts +15 -0
  43. package/src/playwright/index.js +66 -0
  44. package/src/template.js +11 -0
  45. package/src/vite/index.d.ts +28 -0
  46. package/src/vite/index.js +243 -0
  47. package/src/vite/page-composer.js +209 -0
  48. package/templates/Dockerfile.tmpl +39 -0
  49. package/templates/Makefile.tmpl +136 -0
  50. package/templates/claude-block.md +40 -0
  51. package/templates/dockerignore.tmpl +10 -0
  52. package/templates/examples-layout.html +13 -0
  53. package/templates/new/.claude/commands/publish.md +226 -0
  54. package/templates/new/CHANGELOG.md +13 -0
  55. package/templates/new/LICENSE +21 -0
  56. package/templates/new/README.md +71 -0
  57. package/templates/new/_gitignore +7 -0
  58. package/templates/new/component-variables.manifest.json +32 -0
  59. package/templates/new/custom-elements-manifest.config.mjs +23 -0
  60. package/templates/new/docs/accessibility.md +24 -0
  61. package/templates/new/docs/examples.md +33 -0
  62. package/templates/new/docs/theming.md +54 -0
  63. package/templates/new/docs/usage.md +51 -0
  64. package/templates/new/e2e/dark-mode.spec.ts +30 -0
  65. package/templates/new/e2e/smoke.spec.ts +26 -0
  66. package/templates/new/examples-basic.html +57 -0
  67. package/templates/new/examples-theming.html +28 -0
  68. package/templates/new/index.html +16 -0
  69. package/templates/new/partials/note.html +5 -0
  70. package/templates/new/playwright.config.ts +4 -0
  71. package/templates/new/src/__FEATURE__.ts +55 -0
  72. package/templates/new/src/css/animations.css +1 -0
  73. package/templates/new/src/css/base.css +43 -0
  74. package/templates/new/src/css/controls.css +20 -0
  75. package/templates/new/src/css/dark-mode.css +36 -0
  76. package/templates/new/src/css/floating.css +1 -0
  77. package/templates/new/src/css/main.css +27 -0
  78. package/templates/new/src/css/states.css +22 -0
  79. package/templates/new/src/css/variables.css +40 -0
  80. package/templates/new/src/env.d.ts +7 -0
  81. package/templates/new/src/index.ts +17 -0
  82. package/templates/new/src/logger.ts +11 -0
  83. package/templates/new/src/types.ts +25 -0
  84. package/templates/new/src/web-component.ts +73 -0
  85. package/templates/new/test/dark-mode.html +18 -0
  86. package/templates/new/test/fixture.html +11 -0
  87. package/templates/new/tsconfig.json +15 -0
  88. package/templates/new/vite.config.ts +5 -0
  89. package/templates/new-minimal/CHANGELOG.md +13 -0
  90. package/templates/new-minimal/README.md +71 -0
  91. package/templates/new-minimal/component-variables.manifest.json +24 -0
  92. package/templates/new-minimal/docs/accessibility.md +20 -0
  93. package/templates/new-minimal/docs/examples.md +17 -0
  94. package/templates/new-minimal/docs/theming.md +53 -0
  95. package/templates/new-minimal/docs/usage.md +43 -0
  96. package/templates/new-minimal/e2e/smoke.spec.ts +10 -0
  97. package/templates/new-minimal/examples-basic.html +23 -0
  98. package/templates/new-minimal/examples-theming.html +28 -0
  99. package/templates/new-minimal/src/__FEATURE__.ts +37 -0
  100. package/templates/new-minimal/src/css/base.css +48 -0
  101. package/templates/new-minimal/src/css/controls.css +1 -0
  102. package/templates/new-minimal/src/css/states.css +1 -0
  103. package/templates/new-minimal/src/css/variables.css +26 -0
  104. package/templates/new-minimal/src/index.ts +17 -0
  105. package/templates/new-minimal/src/types.ts +10 -0
  106. package/templates/new-minimal/src/web-component.ts +48 -0
  107. package/templates/new-minimal/test/dark-mode.html +18 -0
  108. package/templates/new-minimal/test/fixture.html +11 -0
  109. package/templates/nginx.conf.tmpl +43 -0
@@ -0,0 +1,24 @@
1
+ # Accessibility — `<{{TAG}}>`
2
+
3
+ ## Keyboard
4
+
5
+ | Key | Action |
6
+ |---|---|
7
+ | `Tab` / `Shift+Tab` | Move focus to / from the toggle button. |
8
+ | `Enter` / `Space` | Toggle the active state (native `<button>` behaviour). |
9
+
10
+ ## ARIA
11
+
12
+ The toggle is a native `<button>` with `aria-pressed` reflecting the active
13
+ state, so screen readers announce it as a toggle button. The label text is the
14
+ accessible name.
15
+
16
+ ## Focus
17
+
18
+ A visible `:focus-visible` outline uses `--{{PREFIX}}-accent-color`.
19
+
20
+ ## Contrast
21
+
22
+ Default colours meet WCAG AA in both light and dark schemes. When overriding
23
+ `--{{PREFIX}}-accent-color`, keep `--{{PREFIX}}-text-color-on-accent` legible
24
+ on it (4.5:1).
@@ -0,0 +1,33 @@
1
+ # Examples — `<{{TAG}}>`
2
+
3
+ Live examples: run `make dev` and open http://localhost:{{DEV_PORT}}.
4
+
5
+ ## Common patterns
6
+
7
+ ### React to the active state
8
+
9
+ Shows the event API and the `active` attribute reflecting the state.
10
+
11
+ ```html
12
+ <{{TAG}} id="toggle" label="Notifications"></{{TAG}}>
13
+ <script type="module">
14
+ import '{{PACKAGE_NAME}}';
15
+ const toggle = document.getElementById('toggle');
16
+ toggle.addEventListener('change', (event) => {
17
+ console.log('notifications', event.detail.isActive ? 'on' : 'off');
18
+ // the element also reflects the state: toggle.hasAttribute('active')
19
+ });
20
+ </script>
21
+ ```
22
+
23
+ ### A themed instance
24
+
25
+ Combines a `--base-*` override (all components) with a `--{{PREFIX}}-*` one
26
+ (this instance only), forced dark.
27
+
28
+ ```html
29
+ <style>
30
+ :root { --base-accent-color: #10b981; }
31
+ </style>
32
+ <{{TAG}} label="Dark" data-theme="dark" style="--{{PREFIX}}-root-border-radius: 999px"></{{TAG}}>
33
+ ```
@@ -0,0 +1,54 @@
1
+ # Theming — `<{{TAG}}>`
2
+
3
+ ## Container contract
4
+
5
+ The theme container is `:host` — the `<{{TAG}}>` element itself. It paints
6
+ its own surface (`--{{PREFIX}}-bg`), so it is visible standalone. Attach
7
+ `data-theme="dark|light"` to the element (per instance) or to any ancestor; the
8
+ framework conventions `[data-bs-theme]` (Bootstrap) and `.dark` / `.light`
9
+ (Tailwind and friends) are honoured too.
10
+
11
+ ## Variable contract
12
+
13
+ Two layers, never three: `--base-*` is the cross-component hook (set by
14
+ `@keenmate/theme-designer` or by you on `:root`), `--{{PREFIX}}-*` is the
15
+ component-local override. The full list, with categories, is in
16
+ [`component-variables.manifest.json`](../component-variables.manifest.json)
17
+ (exported as `{{PACKAGE_NAME}}/manifest`).
18
+
19
+ All sizes multiply through `--{{PREFIX}}-rem` (default `10px`; set `1rem` to
20
+ follow the page font size).
21
+
22
+ ```css
23
+ /* every Keenmate component on the page */
24
+ :root { --base-accent-color: #10b981; }
25
+
26
+ /* this component only */
27
+ {{TAG}} { --{{PREFIX}}-accent-color: #8b5cf6; }
28
+ ```
29
+
30
+ ## Color-scheme strategy
31
+
32
+ Strategy B (color-scheme.md): every colour falls back through
33
+ `light-dark(<light>, <dark>)`, and the component sets `color-scheme` on the
34
+ host only when you signal a theme (`data-theme`, `data-bs-theme`, `.dark`).
35
+ Without a signal it inherits the page's `color-scheme` — declare
36
+ `color-scheme: light dark` on your page to follow the OS.
37
+
38
+ ## Cascade-layer contract
39
+
40
+ ```css
41
+ @layer variables, component, overrides;
42
+ ```
43
+
44
+ Any unlayered rule of yours beats every rule of the component; any `--base-*`
45
+ on `:root` beats the `variables` layer. If your page ships a universal reset
46
+ (`* { margin: 0; padding: 0 }`), wrap it in a layer — an unlayered reset beats
47
+ the component's layered defaults.
48
+
49
+ ## BEM hooks
50
+
51
+ | Part | Class | `::part()` |
52
+ |---|---|---|
53
+ | The toggle button | `.{{PREFIX}}__root` (`.{{PREFIX}}__root--active` when active) | `::part(root)` |
54
+ | The label | `.{{PREFIX}}__label` | `::part(label)` |
@@ -0,0 +1,51 @@
1
+ # Usage — `<{{TAG}}>`
2
+
3
+ ## Attributes / properties
4
+
5
+ Every input works as an HTML attribute **and** a JS property (core input
6
+ table, `ATTRIBUTE_TABLE` in `src/web-component.ts`).
7
+
8
+ | Attribute | Property | Type | Default | Description |
9
+ |---|---|---|---|---|
10
+ | `label` | `label` | `string` | `"Hello from {{TAG}}"` | Text shown inside the component. |
11
+ | `active` | `isActive` | `boolean` (`bool-default-false`: present = true, `"false"` = false) | `false` | Whether the component is in its active (pressed) state. |
12
+
13
+ ## Events
14
+
15
+ | Event | `detail` (`{{EVENT_DETAIL_TYPE}}`) | Property twin | When |
16
+ |---|---|---|---|
17
+ | `change` | `{ isActive: boolean }` | `onChange` | The active state was toggled by the user. Bubbles, composed, not cancelable. |
18
+
19
+ ```js
20
+ const el = document.querySelector('{{TAG}}');
21
+ el.addEventListener('change', (event) => console.log(event.detail.isActive));
22
+ el.onChange = (event) => console.log(event.detail.isActive); // same event, property form
23
+ ```
24
+
25
+ ## Slots / children
26
+
27
+ None.
28
+
29
+ ## Methods
30
+
31
+ None beyond the core `BlissElement` API (`setAttributes()`, `batch()`, `whenSettled()`).
32
+
33
+ ## Lifecycle notes
34
+
35
+ The element builds its shadow DOM on first connect; attribute and property
36
+ changes are batched into one update per microtask (`await el.whenSettled()`
37
+ before reading rendered state in tests).
38
+
39
+ ## Editor integration
40
+
41
+ The package ships a Custom Elements Manifest plus VS Code custom data. Add to
42
+ `.vscode/settings.json` for attribute autocomplete in HTML:
43
+
44
+ ```json
45
+ {
46
+ "html.customData": ["./node_modules/{{PACKAGE_NAME}}/vscode.html-custom-data.json"],
47
+ "css.customData": ["./node_modules/{{PACKAGE_NAME}}/vscode.css-custom-data.json"]
48
+ }
49
+ ```
50
+
51
+ JetBrains IDEs pick up `web-types.json` automatically.
@@ -0,0 +1,30 @@
1
+ import { test, expect, type Page } from '@playwright/test';
2
+
3
+ // Dark-mode signals (rulebook color-scheme.md): page color-scheme, framework
4
+ // class on an ancestor, and the per-instance data-theme attribute.
5
+ const getHostBackground = (page: Page, selector: string) =>
6
+ page.locator(selector).evaluate((element) => getComputedStyle(element).backgroundColor);
7
+
8
+ test.describe('<{{TAG}}> dark mode', () => {
9
+ test('defaults to light, per-instance data-theme="dark" goes dark', async ({ page }) => {
10
+ await page.goto('/test/dark-mode.html');
11
+ expect(await getHostBackground(page, '#light-default')).toBe('rgb(255, 255, 255)');
12
+ expect(await getHostBackground(page, '#dark-instance')).toBe('rgb(26, 26, 26)');
13
+ });
14
+
15
+ test('an ancestor data-theme="dark" reaches the component', async ({ page }) => {
16
+ await page.goto('/test/dark-mode.html');
17
+ expect(await getHostBackground(page, '#dark-ancestor')).toBe('rgb(26, 26, 26)');
18
+ });
19
+
20
+ test('a page color-scheme of dark is inherited', async ({ page }) => {
21
+ await page.goto('/test/dark-mode.html');
22
+ await page.evaluate(() => (document.documentElement.style.colorScheme = 'dark'));
23
+ expect(await getHostBackground(page, '#light-default')).toBe('rgb(26, 26, 26)');
24
+ });
25
+
26
+ test('--base-* themes the component', async ({ page }) => {
27
+ await page.goto('/test/dark-mode.html');
28
+ expect(await getHostBackground(page, '#base-themed')).toBe('rgb(255, 0, 0)');
29
+ });
30
+ });
@@ -0,0 +1,26 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ test.describe('<{{TAG}}>', () => {
4
+ test('renders the label and reacts to attribute changes', async ({ page }) => {
5
+ await page.goto('/test/fixture.html');
6
+ const label = page.locator('#subject .{{PREFIX}}__label');
7
+ await expect(label).toHaveText('fixture');
8
+
9
+ await page.evaluate(() => document.getElementById('subject')!.setAttribute('label', 'changed'));
10
+ await expect(label).toHaveText('changed');
11
+ });
12
+
13
+ test('click toggles the active state, reflects it and fires change', async ({ page }) => {
14
+ await page.goto('/test/fixture.html');
15
+ const changeDetails = page.evaluate(
16
+ () =>
17
+ new Promise<unknown>((resolve) =>
18
+ document.getElementById('subject')!.addEventListener('change', (event) => resolve((event as CustomEvent).detail), { once: true })
19
+ )
20
+ );
21
+ await page.locator('#subject .{{PREFIX}}__root').click();
22
+ expect(await changeDetails).toEqual({ isActive: true });
23
+ await expect(page.locator('#subject')).toHaveAttribute('active', '');
24
+ await expect(page.locator('#subject .{{PREFIX}}__root')).toHaveClass(/{{PREFIX}}__root--active/);
25
+ });
26
+ });
@@ -0,0 +1,57 @@
1
+ <meta name="kmwc:title" content="Basic Usage">
2
+ <meta name="kmwc:subtitle" content="The essentials of &lt;{{TAG}}&gt;.">
3
+
4
+ <!-- A fragment page: the devkit layout adds <head>, the header (back link,
5
+ title, subtitle), the container and the component entry script; the kit
6
+ adds the shared styles, chapter nav, copy-links and theme switch.
7
+ Each <kmwc-demo> renders its markup live AND as a code block. -->
8
+
9
+ <div class="card">
10
+ <h2>BU01 · Label &amp; active state</h2>
11
+ <p>Every input is reactive through attribute and property alike. Click the component to toggle it — it fires <code>change</code>.</p>
12
+ <div class="controls">
13
+ <label>label <input type="text" id="bu-label" value="Hello from {{TAG}}"></label>
14
+ <label><input type="checkbox" id="bu-active"> <code>active</code></label>
15
+ </div>
16
+ <kmwc-demo>
17
+ <{{TAG}} id="ex-basic"></{{TAG}}>
18
+ </kmwc-demo>
19
+ <div class="log-panel" id="bu-log">Click the component…</div>
20
+ </div>
21
+
22
+ <div class="card">
23
+ <h2>BU02 · Declarative markup</h2>
24
+ <p>No JavaScript needed — attributes only.</p>
25
+ <kmwc-demo>
26
+ <{{TAG}} label="Pre-activated" active></{{TAG}}>
27
+ </kmwc-demo>
28
+ </div>
29
+
30
+ <div class="card">
31
+ <h2>BU03 · Listening for changes</h2>
32
+ <p>The <code>change</code> event carries <code>detail.isActive</code>; the script below runs on this page and is shown verbatim.</p>
33
+ <kmwc-demo>
34
+ <{{TAG}} id="ex-events" label="Off"></{{TAG}}>
35
+ <script type="module" data-kmwc-show>
36
+ const toggle = document.getElementById('ex-events');
37
+ toggle.addEventListener('change', (event) => {
38
+ toggle.setAttribute('label', event.detail.isActive ? 'On' : 'Off');
39
+ });
40
+ </script>
41
+ </kmwc-demo>
42
+ </div>
43
+
44
+ <script type="module">
45
+ import { attachControlsPersistence } from '@keenmate/web-components-devkit/examples';
46
+
47
+ const example = document.getElementById('ex-basic');
48
+ const log = document.getElementById('bu-log');
49
+ document.getElementById('bu-label').addEventListener('input', (event) => example.setAttribute('label', event.target.value));
50
+ document.getElementById('bu-active').addEventListener('change', (event) => example.toggleAttribute('active', event.target.checked));
51
+ example.addEventListener('change', (event) => {
52
+ log.textContent = `change → isActive: ${event.detail.isActive}`;
53
+ document.getElementById('bu-active').checked = event.detail.isActive;
54
+ });
55
+
56
+ attachControlsPersistence('basic');
57
+ </script>
@@ -0,0 +1,28 @@
1
+ <meta name="kmwc:title" content="Theming">
2
+ <meta name="kmwc:subtitle" content="--base-* / --{{PREFIX}}-* overrides and the dark-mode signals.">
3
+
4
+ <kmwc-include src="partials/note.html" title="Theme signals">
5
+ The ☀/☾ switch (top right) sets <code>data-theme</code> on the page — the
6
+ same signal a host app sends. <code>data-theme</code> on one instance wins.
7
+ </kmwc-include>
8
+
9
+ <div class="card">
10
+ <h2>TH01 · Dark-mode signals</h2>
11
+ <p>Page theme reaches every instance; <code>data-theme</code> on one instance overrides it.</p>
12
+ <kmwc-demo>
13
+ <{{TAG}} id="th-follow" label="Follows the page" active></{{TAG}}>
14
+ <{{TAG}} id="th-dark" label="data-theme=dark" data-theme="dark" active></{{TAG}}>
15
+ <{{TAG}} id="th-light" label="data-theme=light" data-theme="light" active></{{TAG}}>
16
+ </kmwc-demo>
17
+ </div>
18
+
19
+ <div class="card">
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>
22
+ <kmwc-demo>
23
+ <div style="--base-accent-color: #10b981">
24
+ <{{TAG}} id="th-base" label="--base-accent-color" active></{{TAG}}>
25
+ <{{TAG}} id="th-local" label="--{{PREFIX}}-accent-color" active style="--{{PREFIX}}-accent-color: #8b5cf6"></{{TAG}}>
26
+ </div>
27
+ </kmwc-demo>
28
+ </div>
@@ -0,0 +1,16 @@
1
+ <meta name="kmwc:layout" content="bare">
2
+ <meta name="kmwc:entry" content="none">
3
+
4
+ <!-- Header, cards, footer, version badge and the cross-page nav are all
5
+ rendered by the devkit from this registry + package.json#kmwc. -->
6
+ <script type="module">
7
+ import { renderExamplesIndex } from '@keenmate/web-components-devkit/examples';
8
+
9
+ renderExamplesIndex({
10
+ pages: [
11
+ { href: 'examples-basic.html', icon: '🧩', title: 'Basic Usage', description: 'The essentials, as one live component driven by control switches.' },
12
+ { href: 'examples-theming.html', icon: '🎨', title: 'Theming', description: 'Dark-mode signals, --base-* and --{{PREFIX}}-* overrides.' },
13
+ ],
14
+ links: [{ label: 'API Docs', href: 'https://github.com/keenmate/{{ID}}/blob/main/docs/usage.md' }],
15
+ });
16
+ </script>
@@ -0,0 +1,5 @@
1
+ <!-- Shared callout: <kmwc-include src="partials/note.html" title="…">body</kmwc-include> -->
2
+ <div class="note info">
3
+ <div class="note-title">{{title}}</div>
4
+ {{content}}
5
+ </div>
@@ -0,0 +1,4 @@
1
+ import { definePlaywrightConfig } from '@keenmate/web-components-devkit/playwright';
2
+
3
+ // Specs in e2e/ target fixture pages under test/*.html on kmwc.ports.dev.
4
+ export default definePlaywrightConfig();
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Logic class: framework-agnostic state + rendering for {{PACKAGE_NAME}}.
3
+ * Knows nothing about custom elements — it renders into the container it is
4
+ * given and reports back through the `on*` events in its config. The Element
5
+ * layer (web-component.ts) owns attribute parsing and DOM events.
6
+ */
7
+ import type { {{CONFIG_TYPE}} } from './types.js';
8
+
9
+ const CSS_CLASSES = {
10
+ root: '{{PREFIX}}__root',
11
+ rootActive: '{{PREFIX}}__root--active',
12
+ label: '{{PREFIX}}__label',
13
+ } as const;
14
+
15
+ export class {{LOGIC_CLASS}} {
16
+ #container: HTMLElement;
17
+ #config: {{CONFIG_TYPE}};
18
+ #rootElement: HTMLButtonElement | null = null;
19
+ #labelElement: HTMLSpanElement | null = null;
20
+
21
+ constructor(container: HTMLElement, config: {{CONFIG_TYPE}}) {
22
+ this.#container = container;
23
+ this.#config = { ...config };
24
+ this.render();
25
+ }
26
+
27
+ /** Build the markup once. */
28
+ render(): void {
29
+ this.#container.innerHTML = `<button type="button" class="${CSS_CLASSES.root}" part="root"><span class="${CSS_CLASSES.label}" part="label"></span></button>`;
30
+ this.#rootElement = this.#container.querySelector(`.${CSS_CLASSES.root}`);
31
+ this.#labelElement = this.#container.querySelector(`.${CSS_CLASSES.label}`);
32
+ this.#rootElement?.addEventListener('click', this.handleRootClick);
33
+ this.update(this.#config);
34
+ }
35
+
36
+ /** Patch the rendered DOM to a (partial) new config. */
37
+ update(partialConfig: Partial<{{CONFIG_TYPE}}>): void {
38
+ this.#config = { ...this.#config, ...partialConfig };
39
+ if (this.#labelElement) this.#labelElement.textContent = this.#config.label;
40
+ this.#rootElement?.classList.toggle(CSS_CLASSES.rootActive, this.#config.isActive);
41
+ this.#rootElement?.setAttribute('aria-pressed', String(this.#config.isActive));
42
+ }
43
+
44
+ toggle(): void {
45
+ this.update({ isActive: !this.#config.isActive });
46
+ this.#config.onChange?.({ isActive: this.#config.isActive });
47
+ }
48
+
49
+ destroy(): void {
50
+ this.#rootElement?.removeEventListener('click', this.handleRootClick);
51
+ this.#container.innerHTML = '';
52
+ }
53
+
54
+ private handleRootClick = (): void => this.toggle();
55
+ }
@@ -0,0 +1 @@
1
+ /* animations.css — no keyframe animations in this component. */
@@ -0,0 +1,43 @@
1
+ /* ==============================================================================
2
+ BASE STYLES
3
+ ==============================================================================
4
+ FOUC prevention and the theme container (:host). */
5
+
6
+ /* ==============================================================================
7
+ FOUC PREVENTION
8
+ ==============================================================================
9
+ Light-DOM rule (reaches the page via index.ts's side-effect CSS import):
10
+ reserve the post-upgrade footprint and hide child text until the element is
11
+ defined. The tag MUST equal the customElements.define() tag. */
12
+ {{TAG}}:not(:defined) {
13
+ display: inline-block;
14
+ min-height: calc(3.5 * 10px);
15
+ color: transparent !important;
16
+ background: transparent;
17
+ }
18
+
19
+ /* ==============================================================================
20
+ THEME CONTAINER
21
+ ==============================================================================
22
+ :host is the container: layout sanity, the default surface, text colour and
23
+ font. Self-painted host (D-TC-3 case 1). */
24
+ :host {
25
+ display: inline-block;
26
+ position: relative;
27
+ box-sizing: border-box;
28
+ background: var(--{{PREFIX}}-bg);
29
+ color: var(--{{PREFIX}}-text-color-1);
30
+ font-family: var(--{{PREFIX}}-font-family);
31
+ font-size: var(--{{PREFIX}}-font-size-base);
32
+ border-radius: var(--{{PREFIX}}-root-border-radius);
33
+ }
34
+
35
+ :host([hidden]) {
36
+ display: none;
37
+ }
38
+
39
+ *,
40
+ *::before,
41
+ *::after {
42
+ box-sizing: inherit;
43
+ }
@@ -0,0 +1,20 @@
1
+ /* ==============================================================================
2
+ CONTROLS
3
+ ==============================================================================
4
+ The root toggle button. */
5
+ .{{PREFIX}}__root {
6
+ display: inline-flex;
7
+ align-items: center;
8
+ min-height: var(--{{PREFIX}}-root-min-height);
9
+ padding: var(--{{PREFIX}}-root-padding-block) var(--{{PREFIX}}-root-padding-inline);
10
+ border: 1px solid var(--{{PREFIX}}-border-color);
11
+ border-radius: var(--{{PREFIX}}-root-border-radius);
12
+ background: transparent;
13
+ color: inherit;
14
+ font: inherit;
15
+ cursor: pointer;
16
+ }
17
+
18
+ .{{PREFIX}}__label {
19
+ white-space: nowrap;
20
+ }
@@ -0,0 +1,36 @@
1
+ /* ==============================================================================
2
+ DARK MODE — framework class + per-instance signal handling (Strategy B)
3
+ ==============================================================================
4
+ Every --{{PREFIX}}-* colour in variables.css resolves through light-dark(),
5
+ so flipping `color-scheme` on the host picks the right branch without
6
+ touching any --base-* value — the consumer's themed --base-* stays the
7
+ source of truth. Declared ONLY when the consumer signals intent, never bare
8
+ on :host (that would shadow the page's inherited color-scheme).
9
+
10
+ Signals handled (color-scheme.md):
11
+ #1-#3 OS / forced / page color-scheme — inherited, light-dark() resolves.
12
+ #4 Framework class on an ancestor — [data-theme], [data-bs-theme], .dark / .light
13
+ #5 Per-instance attribute — <{{TAG}} data-theme="dark|light">, wins over #4. */
14
+
15
+ /* DARK — framework class on an ancestor (signal #4) */
16
+ :host-context([data-theme="dark"]),
17
+ :host-context([data-bs-theme="dark"]),
18
+ :host-context(.dark) {
19
+ color-scheme: dark;
20
+ }
21
+
22
+ /* LIGHT — framework class on an ancestor (signal #4) */
23
+ :host-context([data-theme="light"]),
24
+ :host-context([data-bs-theme="light"]),
25
+ :host-context(.light) {
26
+ color-scheme: light;
27
+ }
28
+
29
+ /* Per-instance attribute on the host (signal #5) */
30
+ :host([data-theme="dark"]) {
31
+ color-scheme: dark;
32
+ }
33
+
34
+ :host([data-theme="light"]) {
35
+ color-scheme: light;
36
+ }
@@ -0,0 +1 @@
1
+ /* floating.css — no floating elements in this component. */
@@ -0,0 +1,27 @@
1
+ /* ==============================================================================
2
+ {{TITLE_UPPER}} — MAIN ENTRY POINT
3
+ ==============================================================================
4
+ Cascade layer order (later = higher priority):
5
+ variables — :host { --{{PREFIX}}-* } declarations
6
+ component — base + feature rules
7
+ overrides — dark mode, framework themes, per-instance overrides
8
+
9
+ Consumer override contract:
10
+ - Any unlayered consumer rule beats every rule in these layers.
11
+ - Any :root --base-* declaration beats the variables layer trivially.
12
+ */
13
+
14
+ @layer variables, component, overrides;
15
+
16
+ /* === SKELETON + CANONICAL CONCERNS (Tier 1 + Tier 2) === */
17
+ @import url('./variables.css') layer(variables);
18
+ @import url('./base.css') layer(component);
19
+ @import url('./controls.css') layer(component);
20
+ @import url('./floating.css') layer(component);
21
+ @import url('./states.css') layer(component);
22
+ @import url('./animations.css') layer(component);
23
+
24
+ /* === COMPONENT-SPECIFIC FEATURES (Tier 3 — alphabetized) === */
25
+
26
+ /* === OVERRIDES === */
27
+ @import url('./dark-mode.css') layer(overrides);
@@ -0,0 +1,22 @@
1
+ /* ==============================================================================
2
+ STATES
3
+ ==============================================================================
4
+ Hover, focus and the active (pressed) modifier. */
5
+ .{{PREFIX}}__root:hover {
6
+ background: var(--{{PREFIX}}-hover-bg);
7
+ }
8
+
9
+ .{{PREFIX}}__root:focus-visible {
10
+ outline: 2px solid var(--{{PREFIX}}-accent-color);
11
+ outline-offset: 2px;
12
+ }
13
+
14
+ .{{PREFIX}}__root--active {
15
+ background: var(--{{PREFIX}}-accent-color);
16
+ border-color: var(--{{PREFIX}}-accent-color);
17
+ color: var(--{{PREFIX}}-text-color-on-accent);
18
+ }
19
+
20
+ .{{PREFIX}}__root--active:hover {
21
+ background: var(--{{PREFIX}}-accent-color);
22
+ }
@@ -0,0 +1,40 @@
1
+ /* ==============================================================================
2
+ CSS CUSTOM PROPERTIES (:host level)
3
+ ==============================================================================
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
7
+ NOT declared here (it would shadow the page's) — see dark-mode.css.
8
+
9
+ Override from outside the shadow DOM:
10
+ {{TAG}} { --{{PREFIX}}-accent-color: #10b981; }
11
+ ============================================================================== */
12
+
13
+ :host {
14
+ /* ==========================================================================
15
+ SCALE
16
+ ==========================================================================
17
+ Unitless --base-* sizes multiply through --{{PREFIX}}-rem. Set it to 1rem
18
+ to follow the host page's font size (Pure Admin integration). */
19
+ --{{PREFIX}}-rem: 10px;
20
+
21
+ /* ==========================================================================
22
+ COLORS
23
+ ========================================================================== */
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)));
30
+
31
+ /* ==========================================================================
32
+ TYPOGRAPHY & SHAPE
33
+ ========================================================================== */
34
+ --{{PREFIX}}-font-family: var(--base-font-family, inherit);
35
+ --{{PREFIX}}-font-size-base: calc(var(--base-font-size-base, 1.6) * var(--{{PREFIX}}-rem));
36
+ --{{PREFIX}}-root-border-radius: calc(var(--base-border-radius-md, 0.6) * var(--{{PREFIX}}-rem));
37
+ --{{PREFIX}}-root-min-height: calc(var(--base-input-size-md-height, 3.5) * var(--{{PREFIX}}-rem));
38
+ --{{PREFIX}}-root-padding-block: calc(0.6 * var(--{{PREFIX}}-rem));
39
+ --{{PREFIX}}-root-padding-inline: calc(1.2 * var(--{{PREFIX}}-rem));
40
+ }
@@ -0,0 +1,7 @@
1
+ // Build-time constants defined by the devkit Vite preset (from package.json).
2
+ declare const __VERSION__: string;
3
+ declare const __PACKAGE_NAME__: string;
4
+ declare const __AUTHOR__: string;
5
+ declare const __LICENSE__: string;
6
+ declare const __REPOSITORY__: string;
7
+ declare const __HOMEPAGE__: string;
@@ -0,0 +1,17 @@
1
+ // Light-DOM stylesheet (dist/style.css) — carries the :not(:defined) FOUC rule
2
+ // from base.css. The shadow root gets the same CSS via ?inline in web-component.ts.
3
+ import './css/main.css';
4
+
5
+ import { registerComponent } from '@keenmate/web-components-core';
6
+ import { {{ELEMENT_CLASS}} } from './web-component.js';
7
+ import { logging } from './logger.js';
8
+
9
+ export { {{ELEMENT_CLASS}} };
10
+ export { {{LOGIC_CLASS}} } from './{{FEATURE}}.js';
11
+ export type { {{CONFIG_TYPE}}, {{EVENT_DETAIL_TYPE}} } from './types.js';
12
+ export { logging };
13
+
14
+ registerComponent('{{TAG}}', {{ELEMENT_CLASS}} as unknown as CustomElementConstructor, {
15
+ config: { name: __PACKAGE_NAME__, version: __VERSION__, author: __AUTHOR__ },
16
+ logging,
17
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Side layer: categorized loggers for {{PACKAGE_NAME}}, built on the core
3
+ * logging module. Enable from the console:
4
+ *
5
+ * window.components['{{TAG}}'].logging.enableLogging();
6
+ */
7
+ import { createLoggers } from '@keenmate/web-components-core';
8
+
9
+ export const LOGGING_CATEGORIES = ['INIT', 'UI', 'INTERACTION'] as const;
10
+
11
+ export const logging = createLoggers('{{LOG_NAME}}', LOGGING_CATEGORIES);