@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.
- package/CHANGELOG.md +76 -0
- package/LICENSE +21 -0
- package/README.md +417 -2
- package/components.json +189 -0
- package/package.json +75 -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 +302 -0
- package/src/cli/commands/readme.js +111 -0
- package/src/cli/commands/sync.js +48 -0
- package/src/cli/kmwc.js +72 -0
- package/src/cli/sync-plan.js +163 -0
- package/src/config.d.ts +147 -0
- package/src/config.js +257 -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 +110 -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 +209 -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 +40 -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 +26 -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,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}}.
|
|
@@ -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 <{{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 · Default</h2>
|
|
11
|
+
<p>Light-DOM children render through the default slot.</p>
|
|
12
|
+
<kmwc-demo>
|
|
13
|
+
<{{TAG}} id="ex-basic">Hello from <{{TAG}}></{{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,26 @@
|
|
|
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}}-bg: #f0fdf4; }
|
|
11
|
+
============================================================================== */
|
|
12
|
+
|
|
13
|
+
:host {
|
|
14
|
+
/* Unitless --base-* sizes multiply through --{{PREFIX}}-rem. */
|
|
15
|
+
--{{PREFIX}}-rem: 10px;
|
|
16
|
+
|
|
17
|
+
--{{PREFIX}}-bg: var(--base-main-bg, light-dark(#ffffff, #1a1a1a));
|
|
18
|
+
--{{PREFIX}}-text-color-1: var(--base-text-color-1, light-dark(#242424, #f5f5f5));
|
|
19
|
+
--{{PREFIX}}-border-color: var(--base-border-color, light-dark(#e0e0e0, #3d3d3d));
|
|
20
|
+
|
|
21
|
+
--{{PREFIX}}-font-family: var(--base-font-family, inherit);
|
|
22
|
+
--{{PREFIX}}-font-size-base: calc(var(--base-font-size-base, 1.6) * var(--{{PREFIX}}-rem));
|
|
23
|
+
--{{PREFIX}}-root-border-radius: calc(var(--base-border-radius-md, 0.6) * var(--{{PREFIX}}-rem));
|
|
24
|
+
--{{PREFIX}}-root-padding-block: calc(0.8 * var(--{{PREFIX}}-rem));
|
|
25
|
+
--{{PREFIX}}-root-padding-inline: calc(1.2 * var(--{{PREFIX}}-rem));
|
|
26
|
+
}
|
|
@@ -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}} {}
|