@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.
- package/CHANGELOG.md +126 -0
- package/LICENSE +21 -0
- package/README.md +434 -2
- package/components.json +206 -0
- package/package.json +77 -4
- package/src/cli/args.js +16 -0
- package/src/cli/commands/check-release.js +66 -0
- package/src/cli/commands/check-vars.js +116 -0
- package/src/cli/commands/doctor.js +289 -0
- package/src/cli/commands/new.js +323 -0
- package/src/cli/commands/readme.js +111 -0
- package/src/cli/commands/sync.js +48 -0
- package/src/cli/kmwc.js +73 -0
- package/src/cli/sync-plan.js +163 -0
- package/src/config.d.ts +149 -0
- package/src/config.js +279 -0
- package/src/examples/auto.js +20 -0
- package/src/examples/chapter-nav.js +246 -0
- package/src/examples/constants.js +80 -0
- package/src/examples/controls-persistence.js +97 -0
- package/src/examples/copy-link.js +89 -0
- package/src/examples/css/base.css +215 -0
- package/src/examples/css/chapter-nav.css +179 -0
- package/src/examples/css/content.css +251 -0
- package/src/examples/css/controls.css +205 -0
- package/src/examples/css/copy-link.css +82 -0
- package/src/examples/css/demo.css +50 -0
- package/src/examples/css/index-page.css +118 -0
- package/src/examples/css/style.css +30 -0
- package/src/examples/css/theme-switch.css +38 -0
- package/src/examples/css/tokens.css +124 -0
- package/src/examples/css/widgets.css +133 -0
- package/src/examples/demo-code.js +30 -0
- package/src/examples/index-page.js +78 -0
- package/src/examples/index.d.ts +107 -0
- package/src/examples/index.js +18 -0
- package/src/examples/kit.js +22 -0
- package/src/examples/page-config.js +30 -0
- package/src/examples/sections.js +53 -0
- package/src/examples/theme-switch.js +72 -0
- package/src/examples/version-badge.js +23 -0
- package/src/playwright/index.d.ts +15 -0
- package/src/playwright/index.js +66 -0
- package/src/template.js +11 -0
- package/src/vite/index.d.ts +28 -0
- package/src/vite/index.js +243 -0
- package/src/vite/page-composer.js +216 -0
- package/templates/Dockerfile.tmpl +39 -0
- package/templates/Makefile.tmpl +136 -0
- package/templates/claude-block.md +40 -0
- package/templates/dockerignore.tmpl +10 -0
- package/templates/examples-layout.html +13 -0
- package/templates/new/.claude/commands/publish.md +226 -0
- package/templates/new/CHANGELOG.md +13 -0
- package/templates/new/LICENSE +21 -0
- package/templates/new/README.md +71 -0
- package/templates/new/_gitignore +7 -0
- package/templates/new/component-variables.manifest.json +32 -0
- package/templates/new/custom-elements-manifest.config.mjs +23 -0
- package/templates/new/docs/accessibility.md +24 -0
- package/templates/new/docs/examples.md +33 -0
- package/templates/new/docs/theming.md +54 -0
- package/templates/new/docs/usage.md +51 -0
- package/templates/new/e2e/dark-mode.spec.ts +30 -0
- package/templates/new/e2e/smoke.spec.ts +26 -0
- package/templates/new/examples-basic.html +57 -0
- package/templates/new/examples-theming.html +28 -0
- package/templates/new/index.html +16 -0
- package/templates/new/partials/note.html +5 -0
- package/templates/new/playwright.config.ts +4 -0
- package/templates/new/src/__FEATURE__.ts +55 -0
- package/templates/new/src/css/animations.css +1 -0
- package/templates/new/src/css/base.css +43 -0
- package/templates/new/src/css/controls.css +20 -0
- package/templates/new/src/css/dark-mode.css +36 -0
- package/templates/new/src/css/floating.css +1 -0
- package/templates/new/src/css/main.css +27 -0
- package/templates/new/src/css/states.css +22 -0
- package/templates/new/src/css/variables.css +42 -0
- package/templates/new/src/env.d.ts +7 -0
- package/templates/new/src/index.ts +17 -0
- package/templates/new/src/logger.ts +11 -0
- package/templates/new/src/types.ts +25 -0
- package/templates/new/src/web-component.ts +73 -0
- package/templates/new/test/dark-mode.html +18 -0
- package/templates/new/test/fixture.html +11 -0
- package/templates/new/tsconfig.json +15 -0
- package/templates/new/vite.config.ts +5 -0
- package/templates/new-minimal/CHANGELOG.md +13 -0
- package/templates/new-minimal/README.md +71 -0
- package/templates/new-minimal/component-variables.manifest.json +24 -0
- package/templates/new-minimal/docs/accessibility.md +20 -0
- package/templates/new-minimal/docs/examples.md +17 -0
- package/templates/new-minimal/docs/theming.md +53 -0
- package/templates/new-minimal/docs/usage.md +43 -0
- package/templates/new-minimal/e2e/smoke.spec.ts +10 -0
- package/templates/new-minimal/examples-basic.html +23 -0
- package/templates/new-minimal/examples-theming.html +28 -0
- package/templates/new-minimal/src/__FEATURE__.ts +37 -0
- package/templates/new-minimal/src/css/base.css +48 -0
- package/templates/new-minimal/src/css/controls.css +1 -0
- package/templates/new-minimal/src/css/states.css +1 -0
- package/templates/new-minimal/src/css/variables.css +28 -0
- package/templates/new-minimal/src/index.ts +17 -0
- package/templates/new-minimal/src/types.ts +10 -0
- package/templates/new-minimal/src/web-component.ts +48 -0
- package/templates/new-minimal/test/dark-mode.html +18 -0
- package/templates/new-minimal/test/fixture.html +11 -0
- package/templates/nginx.conf.tmpl +43 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<meta name="kmwc:title" content="Basic Usage">
|
|
2
|
+
<meta name="kmwc:subtitle" content="The essentials of <{{TAG}}>.">
|
|
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 & 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-primary-color</code> themes every Keenmate component in a subtree; <code>--{{PREFIX}}-accent-color</code> one instance.</p>
|
|
22
|
+
<kmwc-demo>
|
|
23
|
+
<div style="--base-primary-color: #10b981">
|
|
24
|
+
<{{TAG}} id="th-base" label="--base-primary-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,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,42 @@
|
|
|
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}}-accent-color: #10b981; }
|
|
13
|
+
============================================================================== */
|
|
14
|
+
|
|
15
|
+
:host {
|
|
16
|
+
/* ==========================================================================
|
|
17
|
+
SCALE
|
|
18
|
+
==========================================================================
|
|
19
|
+
Unitless --base-* sizes multiply through --{{PREFIX}}-rem. Set it to 1rem
|
|
20
|
+
to follow the host page's font size (Pure Admin integration). */
|
|
21
|
+
--{{PREFIX}}-rem: 10px;
|
|
22
|
+
|
|
23
|
+
/* ==========================================================================
|
|
24
|
+
COLORS
|
|
25
|
+
========================================================================== */
|
|
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)));
|
|
32
|
+
|
|
33
|
+
/* ==========================================================================
|
|
34
|
+
TYPOGRAPHY & SHAPE
|
|
35
|
+
========================================================================== */
|
|
36
|
+
--{{PREFIX}}-font-family: var(--base-font-family, inherit);
|
|
37
|
+
--{{PREFIX}}-font-size-base: calc(var(--base-font-size-base, 1.6) * var(--{{PREFIX}}-rem));
|
|
38
|
+
--{{PREFIX}}-root-border-radius: calc(var(--base-border-radius-md, 0.6) * var(--{{PREFIX}}-rem));
|
|
39
|
+
--{{PREFIX}}-root-min-height: calc(var(--base-input-size-md-height, 3.5) * var(--{{PREFIX}}-rem));
|
|
40
|
+
--{{PREFIX}}-root-padding-block: calc(0.6 * var(--{{PREFIX}}-rem));
|
|
41
|
+
--{{PREFIX}}-root-padding-inline: calc(1.2 * var(--{{PREFIX}}-rem));
|
|
42
|
+
}
|
|
@@ -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);
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
/** Configuration of the framework-agnostic logic class ({{LOGIC_CLASS}}). */
|
|
7
|
+
export interface {{CONFIG_TYPE}} {
|
|
8
|
+
/** Text shown inside the component. */
|
|
9
|
+
label: string;
|
|
10
|
+
/** Whether the component is in its active (pressed) state. */
|
|
11
|
+
isActive: boolean;
|
|
12
|
+
/** Event: the active state changed. Fire-and-forget — the return value is ignored. */
|
|
13
|
+
onChange?: (detail: {{EVENT_DETAIL_TYPE}}) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** `detail` payload of the `change` CustomEvent. */
|
|
17
|
+
export interface {{EVENT_DETAIL_TYPE}} {
|
|
18
|
+
isActive: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Event map for the element (`static events`): event name → detail type. */
|
|
22
|
+
export interface {{EVENTS_TYPE}} {
|
|
23
|
+
change: {{EVENT_DETAIL_TYPE}};
|
|
24
|
+
[eventName: string]: unknown;
|
|
25
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element layer: the `<{{TAG}}>` custom element. Parses inputs (attribute +
|
|
3
|
+
* property, through the core input table), hosts the lifecycle, owns the
|
|
4
|
+
* shadow root and turns the logic class's `on*` events into DOM CustomEvents.
|
|
5
|
+
* Talks to {{LOGIC_CLASS}} only — never to service classes directly.
|
|
6
|
+
*/
|
|
7
|
+
import { BlissElement, adoptStyles, toBool, toText, type InputDef } from '@keenmate/web-components-core';
|
|
8
|
+
import { {{LOGIC_CLASS}} } from './{{FEATURE}}.js';
|
|
9
|
+
import type { {{EVENT_DETAIL_TYPE}}, {{EVENTS_TYPE}} } from './types.js';
|
|
10
|
+
import styles from './css/main.css?inline';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Every public input is one row: HTML attribute ⇔ config key, converter
|
|
14
|
+
* (parsing + validation), and what a change triggers. Attribute names read like
|
|
15
|
+
* HTML (`active`); config keys read like JavaScript (`isActive`).
|
|
16
|
+
*/
|
|
17
|
+
export const ATTRIBUTE_TABLE: readonly InputDef[] = [
|
|
18
|
+
{
|
|
19
|
+
configKey: 'label',
|
|
20
|
+
attribute: 'label',
|
|
21
|
+
converter: toText({ default: 'Hello from {{TAG}}' }),
|
|
22
|
+
on: 'update',
|
|
23
|
+
description: 'Text shown inside the component.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
configKey: 'isActive',
|
|
27
|
+
attribute: 'active',
|
|
28
|
+
converter: toBool('default-false'),
|
|
29
|
+
on: 'update',
|
|
30
|
+
description: 'Whether the component is in its active (pressed) state. Toggled by clicking; fires `change`.',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export class {{ELEMENT_CLASS}} extends BlissElement<{{EVENTS_TYPE}}> {
|
|
35
|
+
protected static override inputs = ATTRIBUTE_TABLE;
|
|
36
|
+
protected static override events = ['change'];
|
|
37
|
+
|
|
38
|
+
#shadow: ShadowRoot | null = null;
|
|
39
|
+
/** The logic class renders here, so it never touches the adopted/fallback <style>. */
|
|
40
|
+
#logicContainer: HTMLElement | null = null;
|
|
41
|
+
#logic: {{LOGIC_CLASS}} | null = null;
|
|
42
|
+
|
|
43
|
+
/** First connect + any `on: 'reinit'` change: (re)build from the full config. */
|
|
44
|
+
protected override reinit(): void {
|
|
45
|
+
this.#shadow ??= this.attachShadow({ mode: 'open' });
|
|
46
|
+
adoptStyles(this.#shadow, styles);
|
|
47
|
+
this.#logicContainer ??= this.#shadow.appendChild(document.createElement('div'));
|
|
48
|
+
this.#logic?.destroy();
|
|
49
|
+
this.#logic = new {{LOGIC_CLASS}}(this.#logicContainer, {
|
|
50
|
+
label: String(this.config.label ?? ''),
|
|
51
|
+
isActive: Boolean(this.config.isActive),
|
|
52
|
+
onChange: (detail) => this.handleLogicChange(detail),
|
|
53
|
+
});
|
|
54
|
+
this.log.INIT?.debug('built');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `on: 'update'` changes: patch in place. */
|
|
58
|
+
protected override update(partial: Record<string, unknown>): void {
|
|
59
|
+
this.#logic?.update(partial);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The logic class's `onChange` event → reflect + dispatch the `change` CustomEvent. */
|
|
63
|
+
private handleLogicChange(detail: {{EVENT_DETAIL_TYPE}}): void {
|
|
64
|
+
this.toggleAttribute('active', detail.isActive);
|
|
65
|
+
this.emit('change', detail);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Importing this module registers the element. index.ts additionally publishes
|
|
70
|
+
// the global API via registerComponent(); both defines are idempotent.
|
|
71
|
+
if (typeof customElements !== 'undefined' && !customElements.get('{{TAG}}')) {
|
|
72
|
+
customElements.define('{{TAG}}', {{ELEMENT_CLASS}});
|
|
73
|
+
}
|
|
@@ -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" label="light default"></{{TAG}}>
|
|
9
|
+
<{{TAG}} id="dark-instance" label="dark instance" data-theme="dark"></{{TAG}}>
|
|
10
|
+
<div data-theme="dark">
|
|
11
|
+
<{{TAG}} id="dark-ancestor" label="dark ancestor"></{{TAG}}>
|
|
12
|
+
</div>
|
|
13
|
+
<div style="--base-main-bg: rgb(255, 0, 0)">
|
|
14
|
+
<{{TAG}} id="base-themed" label="--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" label="fixture"></{{TAG}}>
|
|
9
|
+
<script type="module" src="/src/index.ts"></script>
|
|
10
|
+
</body>
|
|
11
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"verbatimModuleSyntax": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"types": ["vite/client"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [{{VERSION}}] - {{TODAY}}
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Initial skeleton (`kmwc new --minimal`, `@keenmate/web-components-devkit`): `<{{TAG}}>` element (`{{ELEMENT_CLASS}}`) over the `{{LOGIC_CLASS}}` logic class, empty `ATTRIBUTE_TABLE`, default slot.
|
|
8
|
+
- Canonical CSS set (`src/css/`, prefix `{{PREFIX}}`) with `@layer variables, component, overrides`, `--base-*` fallback chains and Strategy-B dark mode.
|
|
9
|
+
- Custom Elements Manifest (`custom-elements.json`, `web-types.json`, VS Code custom data) and `component-variables.manifest.json`.
|
|
10
|
+
|
|
11
|
+
### Internal
|
|
12
|
+
|
|
13
|
+
- Intake: technology web-component · package `{{PACKAGE_NAME}}` · tag `<{{TAG}}>` · CSS prefix `{{PREFIX}}` (reserve it in BlissFramework `guidelines/web-components/base-variables.md`) · dev port {{DEV_PORT}}.
|