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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/LICENSE +21 -0
  3. package/README.md +434 -2
  4. package/components.json +206 -0
  5. package/package.json +77 -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 +323 -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 +73 -0
  14. package/src/cli/sync-plan.js +163 -0
  15. package/src/config.d.ts +149 -0
  16. package/src/config.js +279 -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 +124 -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 +216 -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 +42 -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 +28 -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,71 @@
1
+ # {{PACKAGE_NAME}}
2
+
3
+ > {{TAGLINE}}
4
+
5
+ ## What is it
6
+
7
+ `{{PACKAGE_NAME}}` is a custom element (`<{{TAG}}>`) built on
8
+ [`@keenmate/web-components-core`](https://www.npmjs.com/package/@keenmate/web-components-core).
9
+ It works in any HTML page or framework that can render a custom element —
10
+ React, Vue, Svelte, Blazor, plain HTML.
11
+
12
+ It reads `--base-*` variables from the page when `@keenmate/theme-designer`
13
+ is present, falls back to sensible light/dark defaults otherwise, and supports
14
+ per-instance dark mode via `data-theme`.
15
+
16
+ ## What's New in v{{VERSION}}
17
+
18
+ - **`<{{TAG}}>` — initial skeleton** — First version, scaffolded with `kmwc new --minimal` from `@keenmate/web-components-devkit`. Ships the Element / Logic / Side-layer split with an empty `ATTRIBUTE_TABLE`, the canonical eight-file CSS set with `@layer variables, component, overrides`, light/dark defaults through `light-dark()` fallbacks and a Custom Elements Manifest for editor tooling. See [CHANGELOG.md](./CHANGELOG.md).
19
+
20
+ ## Demos & docs
21
+
22
+ - 🚀 [Live demo](https://demos.keenmate.com/{{ID}})
23
+ - 📘 [Usage / API reference](./docs/usage.md)
24
+ - 🎨 [Theming contract](./docs/theming.md)
25
+ - 📚 [Examples / cookbook](./docs/examples.md)
26
+ - ♿ [Accessibility](./docs/accessibility.md)
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ npm install {{PACKAGE_NAME}}
32
+ ```
33
+
34
+ ## Quick start
35
+
36
+ ```html
37
+ <script type="module">
38
+ import '{{PACKAGE_NAME}}';
39
+ </script>
40
+
41
+ <{{TAG}}>Hello</{{TAG}}>
42
+ ```
43
+
44
+ The element renders its content inside a themed container. See
45
+ [usage.md](./docs/usage.md) for the full API.
46
+
47
+ ## Browser support
48
+
49
+ Modern evergreen browsers (Chrome, Edge, Firefox, Safari) — anything with
50
+ native `customElements`, CSS `@layer` and `light-dark()` support. No polyfills
51
+ shipped.
52
+
53
+ ## About
54
+
55
+ Authored and maintained by [Keenmate s.r.o.](https://keenmate.com/).
56
+ The component ships standalone with sensible light/dark defaults;
57
+ when mounted inside [Pure Admin](https://pureadmin.io/) — or any
58
+ host that publishes the `--base-*` taxonomy via
59
+ [`@keenmate/theme-designer`](https://www.npmjs.com/package/@keenmate/theme-designer) — it adopts the host's colors,
60
+ typography, and sizing automatically. There is no runtime
61
+ dependency on Pure Admin; the integration is opt-in via CSS
62
+ variables.
63
+
64
+ ## Built with BlissFramework
65
+
66
+ Follows the [BlissFramework component guidelines](https://blissframework.dev/)
67
+ for structure, theming, color-scheme, and accessibility.
68
+
69
+ ## License
70
+
71
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/keenmate/schemas/main/component-variables.schema.json",
3
+ "component": "{{PACKAGE_NAME}}",
4
+ "prefix": "{{PREFIX}}",
5
+ "baseVariables": [
6
+ { "name": "base-main-bg", "required": false, "usage": "Component surface" },
7
+ { "name": "base-text-color-1", "required": false, "usage": "Text" },
8
+ { "name": "base-border-color", "required": false, "usage": "Root border" },
9
+ { "name": "base-font-family", "required": false, "usage": "Font family" },
10
+ { "name": "base-font-size-base", "required": false, "usage": "Font size (multiplier × --{{PREFIX}}-rem)" },
11
+ { "name": "base-border-radius-md", "required": false, "usage": "Corner radius (multiplier × --{{PREFIX}}-rem)" }
12
+ ],
13
+ "componentVariables": [
14
+ { "name": "{{PREFIX}}-rem", "category": "spacing", "usage": "Base unit for every size (default 10px; 1rem follows the page font size)" },
15
+ { "name": "{{PREFIX}}-bg", "category": "surface", "usage": "Component surface" },
16
+ { "name": "{{PREFIX}}-text-color-1", "category": "text", "usage": "Text" },
17
+ { "name": "{{PREFIX}}-border-color", "category": "border", "usage": "Root border" },
18
+ { "name": "{{PREFIX}}-font-family", "category": "typography", "usage": "Font family" },
19
+ { "name": "{{PREFIX}}-font-size-base", "category": "typography", "usage": "Font size" },
20
+ { "name": "{{PREFIX}}-root-border-radius", "category": "radius", "usage": "Corner radius" },
21
+ { "name": "{{PREFIX}}-root-padding-block", "category": "spacing", "usage": "Vertical padding" },
22
+ { "name": "{{PREFIX}}-root-padding-inline", "category": "spacing", "usage": "Horizontal padding" }
23
+ ]
24
+ }
@@ -0,0 +1,20 @@
1
+ # Accessibility — `<{{TAG}}>`
2
+
3
+ ## Keyboard
4
+
5
+ The component has no interactive parts yet; slotted content keeps its own
6
+ keyboard behaviour. Document every key binding here as you add controls.
7
+
8
+ ## ARIA
9
+
10
+ No roles yet — the root is a plain container. Add the role / state attributes
11
+ your component's pattern needs (see the WAI-ARIA Authoring Practices).
12
+
13
+ ## Focus
14
+
15
+ Not focusable. Interactive parts must get a visible `:focus-visible` outline.
16
+
17
+ ## Contrast
18
+
19
+ Default colours meet WCAG AA in both light and dark schemes; keep 4.5:1 for
20
+ text when overriding `--{{PREFIX}}-text-color-1` / `--{{PREFIX}}-bg`.
@@ -0,0 +1,17 @@
1
+ # Examples — `<{{TAG}}>`
2
+
3
+ Live examples: run `make dev` and open http://localhost:{{DEV_PORT}}.
4
+
5
+ ## Common patterns
6
+
7
+ ### A themed instance
8
+
9
+ Combines a `--base-*` override (all components) with a `--{{PREFIX}}-*` one
10
+ (this instance only), forced dark.
11
+
12
+ ```html
13
+ <style>
14
+ :root { --base-border-color: #10b981; }
15
+ </style>
16
+ <{{TAG}} data-theme="dark" style="--{{PREFIX}}-root-border-radius: 999px">Dark</{{TAG}}>
17
+ ```
@@ -0,0 +1,53 @@
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-main-bg: #f8fafc; }
25
+
26
+ /* this component only */
27
+ {{TAG}} { --{{PREFIX}}-bg: #f5f3ff; }
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 root wrapper | `.{{PREFIX}}__root` | `::part(root)` |
@@ -0,0 +1,43 @@
1
+ # Usage — `<{{TAG}}>`
2
+
3
+ ## Attributes / properties
4
+
5
+ None yet. Every input is one row in `ATTRIBUTE_TABLE` (`src/web-component.ts`)
6
+ and works as an HTML attribute **and** a JS property — document each row here
7
+ (attribute, property, type, default, description).
8
+
9
+ ## Events
10
+
11
+ None yet. Declare events in `static events` and dispatch them with
12
+ `this.emit(name, detail)`; core installs the `on<Name>` property twin.
13
+ Document name, `detail` shape, when it fires and whether it's cancelable.
14
+
15
+ ## Slots / children
16
+
17
+ | Slot | Content |
18
+ |---|---|
19
+ | (default) | Any light-DOM content; rendered inside `.{{PREFIX}}__root`. |
20
+
21
+ ## Methods
22
+
23
+ None beyond the core `BlissElement` API (`setAttributes()`, `batch()`, `whenSettled()`).
24
+
25
+ ## Lifecycle notes
26
+
27
+ The element builds its shadow DOM on first connect; attribute and property
28
+ changes are batched into one update per microtask (`await el.whenSettled()`
29
+ before reading rendered state in tests).
30
+
31
+ ## Editor integration
32
+
33
+ The package ships a Custom Elements Manifest plus VS Code custom data. Add to
34
+ `.vscode/settings.json` for attribute autocomplete in HTML:
35
+
36
+ ```json
37
+ {
38
+ "html.customData": ["./node_modules/{{PACKAGE_NAME}}/vscode.html-custom-data.json"],
39
+ "css.customData": ["./node_modules/{{PACKAGE_NAME}}/vscode.css-custom-data.json"]
40
+ }
41
+ ```
42
+
43
+ JetBrains IDEs pick up `web-types.json` automatically.
@@ -0,0 +1,10 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ test.describe('<{{TAG}}>', () => {
4
+ test('upgrades, renders its root and shows slotted content', async ({ page }) => {
5
+ await page.goto('/test/fixture.html');
6
+ await expect(page.locator('#subject .{{PREFIX}}__root')).toBeVisible();
7
+ await expect(page.locator('#subject')).toHaveText('fixture');
8
+ expect(await page.evaluate(() => !!customElements.get('{{TAG}}'))).toBe(true);
9
+ });
10
+ });
@@ -0,0 +1,23 @@
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 · Default</h2>
11
+ <p>Light-DOM children render through the default slot.</p>
12
+ <kmwc-demo>
13
+ <{{TAG}} id="ex-basic">Hello from &lt;{{TAG}}&gt;</{{TAG}}>
14
+ </kmwc-demo>
15
+ </div>
16
+
17
+ <div class="card">
18
+ <h2>BU02 · Dark instance</h2>
19
+ <p><code>data-theme="dark"</code> forces one instance dark.</p>
20
+ <kmwc-demo>
21
+ <{{TAG}} data-theme="dark">Always dark</{{TAG}}>
22
+ </kmwc-demo>
23
+ </div>
@@ -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">Follows the page</{{TAG}}>
14
+ <{{TAG}} id="th-dark" data-theme="dark">data-theme=dark</{{TAG}}>
15
+ <{{TAG}} id="th-light" data-theme="light">data-theme=light</{{TAG}}>
16
+ </kmwc-demo>
17
+ </div>
18
+
19
+ <div class="card">
20
+ <h2>TH02 · Variable overrides</h2>
21
+ <p><code>--base-main-bg</code> themes every Keenmate component in a subtree; <code>--{{PREFIX}}-bg</code> one instance.</p>
22
+ <kmwc-demo>
23
+ <div style="--base-main-bg: #ecfdf5">
24
+ <{{TAG}} id="th-base">--base-main-bg</{{TAG}}>
25
+ <{{TAG}} id="th-local" style="--{{PREFIX}}-bg: #f5f3ff">--{{PREFIX}}-bg</{{TAG}}>
26
+ </div>
27
+ </kmwc-demo>
28
+ </div>
@@ -0,0 +1,37 @@
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. The Element layer (web-component.ts) owns attribute parsing and DOM
5
+ * events.
6
+ */
7
+ import type { {{CONFIG_TYPE}} } from './types.js';
8
+
9
+ const CSS_CLASSES = {
10
+ root: '{{PREFIX}}__root',
11
+ } as const;
12
+
13
+ export class {{LOGIC_CLASS}} {
14
+ #container: HTMLElement;
15
+ #config: {{CONFIG_TYPE}};
16
+
17
+ constructor(container: HTMLElement, config: {{CONFIG_TYPE}}) {
18
+ this.#container = container;
19
+ this.#config = { ...config };
20
+ this.render();
21
+ }
22
+
23
+ /** Build the markup once. Light-DOM children show through the slot. */
24
+ render(): void {
25
+ this.#container.innerHTML = `<div class="${CSS_CLASSES.root}" part="root"><slot></slot></div>`;
26
+ this.update(this.#config);
27
+ }
28
+
29
+ /** Patch the rendered DOM to a (partial) new config. */
30
+ update(partialConfig: Partial<{{CONFIG_TYPE}}>): void {
31
+ this.#config = { ...this.#config, ...partialConfig };
32
+ }
33
+
34
+ destroy(): void {
35
+ this.#container.innerHTML = '';
36
+ }
37
+ }
@@ -0,0 +1,48 @@
1
+ /* ==============================================================================
2
+ BASE STYLES
3
+ ==============================================================================
4
+ FOUC prevention, the theme container (:host) and the root wrapper. */
5
+
6
+ /* ==============================================================================
7
+ FOUC PREVENTION
8
+ ==============================================================================
9
+ Light-DOM rule (reaches the page via index.ts's side-effect CSS import):
10
+ hide child text until the element is defined. The tag MUST equal the
11
+ customElements.define() tag. */
12
+ {{TAG}}:not(:defined) {
13
+ display: block;
14
+ color: transparent !important;
15
+ background: transparent;
16
+ }
17
+
18
+ /* ==============================================================================
19
+ THEME CONTAINER
20
+ ==============================================================================
21
+ :host is the container: layout sanity, the default surface, text colour and
22
+ font. Self-painted host (D-TC-3 case 1). */
23
+ :host {
24
+ display: block;
25
+ position: relative;
26
+ box-sizing: border-box;
27
+ background: var(--{{PREFIX}}-bg);
28
+ color: var(--{{PREFIX}}-text-color-1);
29
+ font-family: var(--{{PREFIX}}-font-family);
30
+ font-size: var(--{{PREFIX}}-font-size-base);
31
+ border-radius: var(--{{PREFIX}}-root-border-radius);
32
+ }
33
+
34
+ :host([hidden]) {
35
+ display: none;
36
+ }
37
+
38
+ *,
39
+ *::before,
40
+ *::after {
41
+ box-sizing: inherit;
42
+ }
43
+
44
+ .{{PREFIX}}__root {
45
+ padding: var(--{{PREFIX}}-root-padding-block) var(--{{PREFIX}}-root-padding-inline);
46
+ border: 1px solid var(--{{PREFIX}}-border-color);
47
+ border-radius: var(--{{PREFIX}}-root-border-radius);
48
+ }
@@ -0,0 +1 @@
1
+ /* controls.css — no interactive controls in this component yet. */
@@ -0,0 +1 @@
1
+ /* states.css — no state variants in this component yet. */
@@ -0,0 +1,28 @@
1
+ /* ==============================================================================
2
+ CSS CUSTOM PROPERTIES (:host level)
3
+ ==============================================================================
4
+ Every --{{PREFIX}}-* variable reads through the cross-component --base-* hook
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
9
+ NOT declared here (it would shadow the page's) — see dark-mode.css.
10
+
11
+ Override from outside the shadow DOM:
12
+ {{TAG}} { --{{PREFIX}}-bg: #f0fdf4; }
13
+ ============================================================================== */
14
+
15
+ :host {
16
+ /* Unitless --base-* sizes multiply through --{{PREFIX}}-rem. */
17
+ --{{PREFIX}}-rem: 10px;
18
+
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)));
22
+
23
+ --{{PREFIX}}-font-family: var(--base-font-family, inherit);
24
+ --{{PREFIX}}-font-size-base: calc(var(--base-font-size-base, 1.6) * var(--{{PREFIX}}-rem));
25
+ --{{PREFIX}}-root-border-radius: calc(var(--base-border-radius-md, 0.6) * var(--{{PREFIX}}-rem));
26
+ --{{PREFIX}}-root-padding-block: calc(0.8 * var(--{{PREFIX}}-rem));
27
+ --{{PREFIX}}-root-padding-inline: calc(1.2 * var(--{{PREFIX}}-rem));
28
+ }
@@ -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}} } 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,10 @@
1
+ /**
2
+ * Side layer: the public types of {{PACKAGE_NAME}}. No imports from the other
3
+ * layers — this file could be lifted into its own package tomorrow.
4
+ */
5
+
6
+ /**
7
+ * Configuration of the framework-agnostic logic class ({{LOGIC_CLASS}}).
8
+ * Add a field here for every row you add to ATTRIBUTE_TABLE.
9
+ */
10
+ export interface {{CONFIG_TYPE}} {}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Element layer: the `<{{TAG}}>` custom element. Parses inputs (attribute +
3
+ * property, through the core input table), hosts the lifecycle and owns the
4
+ * shadow root. Talks to {{LOGIC_CLASS}} only — never to service classes directly.
5
+ */
6
+ import { BlissElement, adoptStyles, type InputDef } from '@keenmate/web-components-core';
7
+ import { {{LOGIC_CLASS}} } from './{{FEATURE}}.js';
8
+ import styles from './css/main.css?inline';
9
+
10
+ /**
11
+ * Every public input is one row: HTML attribute ⇔ config key, converter
12
+ * (parsing + validation), and what a change triggers, e.g.
13
+ *
14
+ * { configKey: 'isOpen', attribute: 'open', converter: toBool('default-false'), on: 'update', description: '…' },
15
+ *
16
+ * Attribute names read like HTML; config keys read like JavaScript.
17
+ */
18
+ export const ATTRIBUTE_TABLE: readonly InputDef[] = [];
19
+
20
+ export class {{ELEMENT_CLASS}} extends BlissElement {
21
+ protected static override inputs = ATTRIBUTE_TABLE;
22
+
23
+ #shadow: ShadowRoot | null = null;
24
+ /** The logic class renders here, so it never touches the adopted/fallback <style>. */
25
+ #logicContainer: HTMLElement | null = null;
26
+ #logic: {{LOGIC_CLASS}} | null = null;
27
+
28
+ /** First connect + any `on: 'reinit'` change: (re)build from the full config. */
29
+ protected override reinit(): void {
30
+ this.#shadow ??= this.attachShadow({ mode: 'open' });
31
+ adoptStyles(this.#shadow, styles);
32
+ this.#logicContainer ??= this.#shadow.appendChild(document.createElement('div'));
33
+ this.#logic?.destroy();
34
+ this.#logic = new {{LOGIC_CLASS}}(this.#logicContainer, {});
35
+ this.log.INIT?.debug('built');
36
+ }
37
+
38
+ /** `on: 'update'` changes: patch in place. */
39
+ protected override update(partial: Record<string, unknown>): void {
40
+ this.#logic?.update(partial);
41
+ }
42
+ }
43
+
44
+ // Importing this module registers the element. index.ts additionally publishes
45
+ // the global API via registerComponent(); both defines are idempotent.
46
+ if (typeof customElements !== 'undefined' && !customElements.get('{{TAG}}')) {
47
+ customElements.define('{{TAG}}', {{ELEMENT_CLASS}});
48
+ }
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-kmwc="off">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>{{TAG}} dark-mode fixture</title>
6
+ </head>
7
+ <body>
8
+ <{{TAG}} id="light-default">light default</{{TAG}}>
9
+ <{{TAG}} id="dark-instance" data-theme="dark">dark instance</{{TAG}}>
10
+ <div data-theme="dark">
11
+ <{{TAG}} id="dark-ancestor">dark ancestor</{{TAG}}>
12
+ </div>
13
+ <div style="--base-main-bg: rgb(255, 0, 0)">
14
+ <{{TAG}} id="base-themed">--base-main-bg</{{TAG}}>
15
+ </div>
16
+ <script type="module" src="/src/index.ts"></script>
17
+ </body>
18
+ </html>
@@ -0,0 +1,11 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-kmwc="off">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>{{TAG}} fixture</title>
6
+ </head>
7
+ <body>
8
+ <{{TAG}} id="subject">fixture</{{TAG}}>
9
+ <script type="module" src="/src/index.ts"></script>
10
+ </body>
11
+ </html>
@@ -0,0 +1,43 @@
1
+ {{HEADER}}
2
+ #
3
+ # Static server for the {{ID}} examples site. 100% static (HTML + bundled
4
+ # assets + demo data), no backend — so the vulnerability scanners that probe the
5
+ # public host (leakix/l9scan et al. hitting /api/gql, info.php, .git, .env, …)
6
+ # have nothing to exploit. Drop that traffic with 444 (close, no response).
7
+
8
+ server {
9
+ listen 80;
10
+ server_name _;
11
+ root /usr/share/nginx/html;
12
+ index index.html;
13
+
14
+ # 1. Known vulnerability scanners by User-Agent → close immediately.
15
+ if ($http_user_agent ~* "(l9scan|leakix|nikto|sqlmap|nmap|masscan|zgrab|censys|nuclei|httpx|semrush|petalbot)") {
16
+ return 444;
17
+ }
18
+
19
+ # 2. Exploit-probe file extensions this static site never serves.
20
+ location ~* \.(php|phtml|asp|aspx|jsp|cgi|pl|env|ini|sh|bak|old|sql)$ {
21
+ access_log off;
22
+ return 444;
23
+ }
24
+
25
+ # 3. Exploit-probe paths (app frameworks, VCS/CI metadata, dotfiles).
26
+ location ~* /(\.git|\.svn|\.hg|\.env|wp-admin|wp-login|wp-content|wp-includes|xmlrpc|telescope|actuator|vendor|META-INF|WEB-INF|phpmyadmin|pma|adminer|owa)(/|$) {
27
+ access_log off;
28
+ return 444;
29
+ }
30
+
31
+ # 4. Vite-bundled assets carry a content hash in their name — cache forever.
32
+ location /assets/ {
33
+ try_files $uri =404;
34
+ add_header Cache-Control "public, max-age=31536000, immutable" always;
35
+ }
36
+
37
+ # 5. Pages and un-hashed files (demo data, docs): always revalidate (cheap
38
+ # 304 via ETag), so a redeploy is visible immediately.
39
+ location / {
40
+ try_files $uri $uri/ =404;
41
+ add_header Cache-Control "no-cache" always;
42
+ }
43
+ }