@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,24 @@
|
|
|
1
|
+
# Accessibility — `<{{TAG}}>`
|
|
2
|
+
|
|
3
|
+
## Keyboard
|
|
4
|
+
|
|
5
|
+
| Key | Action |
|
|
6
|
+
|---|---|
|
|
7
|
+
| `Tab` / `Shift+Tab` | Move focus to / from the toggle button. |
|
|
8
|
+
| `Enter` / `Space` | Toggle the active state (native `<button>` behaviour). |
|
|
9
|
+
|
|
10
|
+
## ARIA
|
|
11
|
+
|
|
12
|
+
The toggle is a native `<button>` with `aria-pressed` reflecting the active
|
|
13
|
+
state, so screen readers announce it as a toggle button. The label text is the
|
|
14
|
+
accessible name.
|
|
15
|
+
|
|
16
|
+
## Focus
|
|
17
|
+
|
|
18
|
+
A visible `:focus-visible` outline uses `--{{PREFIX}}-accent-color`.
|
|
19
|
+
|
|
20
|
+
## Contrast
|
|
21
|
+
|
|
22
|
+
Default colours meet WCAG AA in both light and dark schemes. When overriding
|
|
23
|
+
`--{{PREFIX}}-accent-color`, keep `--{{PREFIX}}-text-color-on-accent` legible
|
|
24
|
+
on it (4.5:1).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Examples — `<{{TAG}}>`
|
|
2
|
+
|
|
3
|
+
Live examples: run `make dev` and open http://localhost:{{DEV_PORT}}.
|
|
4
|
+
|
|
5
|
+
## Common patterns
|
|
6
|
+
|
|
7
|
+
### React to the active state
|
|
8
|
+
|
|
9
|
+
Shows the event API and the `active` attribute reflecting the state.
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<{{TAG}} id="toggle" label="Notifications"></{{TAG}}>
|
|
13
|
+
<script type="module">
|
|
14
|
+
import '{{PACKAGE_NAME}}';
|
|
15
|
+
const toggle = document.getElementById('toggle');
|
|
16
|
+
toggle.addEventListener('change', (event) => {
|
|
17
|
+
console.log('notifications', event.detail.isActive ? 'on' : 'off');
|
|
18
|
+
// the element also reflects the state: toggle.hasAttribute('active')
|
|
19
|
+
});
|
|
20
|
+
</script>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### A themed instance
|
|
24
|
+
|
|
25
|
+
Combines a `--base-*` override (all components) with a `--{{PREFIX}}-*` one
|
|
26
|
+
(this instance only), forced dark.
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<style>
|
|
30
|
+
:root { --base-accent-color: #10b981; }
|
|
31
|
+
</style>
|
|
32
|
+
<{{TAG}} label="Dark" data-theme="dark" style="--{{PREFIX}}-root-border-radius: 999px"></{{TAG}}>
|
|
33
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Theming — `<{{TAG}}>`
|
|
2
|
+
|
|
3
|
+
## Container contract
|
|
4
|
+
|
|
5
|
+
The theme container is `:host` — the `<{{TAG}}>` element itself. It paints
|
|
6
|
+
its own surface (`--{{PREFIX}}-bg`), so it is visible standalone. Attach
|
|
7
|
+
`data-theme="dark|light"` to the element (per instance) or to any ancestor; the
|
|
8
|
+
framework conventions `[data-bs-theme]` (Bootstrap) and `.dark` / `.light`
|
|
9
|
+
(Tailwind and friends) are honoured too.
|
|
10
|
+
|
|
11
|
+
## Variable contract
|
|
12
|
+
|
|
13
|
+
Two layers, never three: `--base-*` is the cross-component hook (set by
|
|
14
|
+
`@keenmate/theme-designer` or by you on `:root`), `--{{PREFIX}}-*` is the
|
|
15
|
+
component-local override. The full list, with categories, is in
|
|
16
|
+
[`component-variables.manifest.json`](../component-variables.manifest.json)
|
|
17
|
+
(exported as `{{PACKAGE_NAME}}/manifest`).
|
|
18
|
+
|
|
19
|
+
All sizes multiply through `--{{PREFIX}}-rem` (default `10px`; set `1rem` to
|
|
20
|
+
follow the page font size).
|
|
21
|
+
|
|
22
|
+
```css
|
|
23
|
+
/* every Keenmate component on the page */
|
|
24
|
+
:root { --base-accent-color: #10b981; }
|
|
25
|
+
|
|
26
|
+
/* this component only */
|
|
27
|
+
{{TAG}} { --{{PREFIX}}-accent-color: #8b5cf6; }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Color-scheme strategy
|
|
31
|
+
|
|
32
|
+
Strategy B (color-scheme.md): every colour falls back through
|
|
33
|
+
`light-dark(<light>, <dark>)`, and the component sets `color-scheme` on the
|
|
34
|
+
host only when you signal a theme (`data-theme`, `data-bs-theme`, `.dark`).
|
|
35
|
+
Without a signal it inherits the page's `color-scheme` — declare
|
|
36
|
+
`color-scheme: light dark` on your page to follow the OS.
|
|
37
|
+
|
|
38
|
+
## Cascade-layer contract
|
|
39
|
+
|
|
40
|
+
```css
|
|
41
|
+
@layer variables, component, overrides;
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Any unlayered rule of yours beats every rule of the component; any `--base-*`
|
|
45
|
+
on `:root` beats the `variables` layer. If your page ships a universal reset
|
|
46
|
+
(`* { margin: 0; padding: 0 }`), wrap it in a layer — an unlayered reset beats
|
|
47
|
+
the component's layered defaults.
|
|
48
|
+
|
|
49
|
+
## BEM hooks
|
|
50
|
+
|
|
51
|
+
| Part | Class | `::part()` |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| The toggle button | `.{{PREFIX}}__root` (`.{{PREFIX}}__root--active` when active) | `::part(root)` |
|
|
54
|
+
| The label | `.{{PREFIX}}__label` | `::part(label)` |
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Usage — `<{{TAG}}>`
|
|
2
|
+
|
|
3
|
+
## Attributes / properties
|
|
4
|
+
|
|
5
|
+
Every input works as an HTML attribute **and** a JS property (core input
|
|
6
|
+
table, `ATTRIBUTE_TABLE` in `src/web-component.ts`).
|
|
7
|
+
|
|
8
|
+
| Attribute | Property | Type | Default | Description |
|
|
9
|
+
|---|---|---|---|---|
|
|
10
|
+
| `label` | `label` | `string` | `"Hello from {{TAG}}"` | Text shown inside the component. |
|
|
11
|
+
| `active` | `isActive` | `boolean` (`bool-default-false`: present = true, `"false"` = false) | `false` | Whether the component is in its active (pressed) state. |
|
|
12
|
+
|
|
13
|
+
## Events
|
|
14
|
+
|
|
15
|
+
| Event | `detail` (`{{EVENT_DETAIL_TYPE}}`) | Property twin | When |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| `change` | `{ isActive: boolean }` | `onChange` | The active state was toggled by the user. Bubbles, composed, not cancelable. |
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
const el = document.querySelector('{{TAG}}');
|
|
21
|
+
el.addEventListener('change', (event) => console.log(event.detail.isActive));
|
|
22
|
+
el.onChange = (event) => console.log(event.detail.isActive); // same event, property form
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Slots / children
|
|
26
|
+
|
|
27
|
+
None.
|
|
28
|
+
|
|
29
|
+
## Methods
|
|
30
|
+
|
|
31
|
+
None beyond the core `BlissElement` API (`setAttributes()`, `batch()`, `whenSettled()`).
|
|
32
|
+
|
|
33
|
+
## Lifecycle notes
|
|
34
|
+
|
|
35
|
+
The element builds its shadow DOM on first connect; attribute and property
|
|
36
|
+
changes are batched into one update per microtask (`await el.whenSettled()`
|
|
37
|
+
before reading rendered state in tests).
|
|
38
|
+
|
|
39
|
+
## Editor integration
|
|
40
|
+
|
|
41
|
+
The package ships a Custom Elements Manifest plus VS Code custom data. Add to
|
|
42
|
+
`.vscode/settings.json` for attribute autocomplete in HTML:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"html.customData": ["./node_modules/{{PACKAGE_NAME}}/vscode.html-custom-data.json"],
|
|
47
|
+
"css.customData": ["./node_modules/{{PACKAGE_NAME}}/vscode.css-custom-data.json"]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
JetBrains IDEs pick up `web-types.json` automatically.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { test, expect, type Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
// Dark-mode signals (rulebook color-scheme.md): page color-scheme, framework
|
|
4
|
+
// class on an ancestor, and the per-instance data-theme attribute.
|
|
5
|
+
const getHostBackground = (page: Page, selector: string) =>
|
|
6
|
+
page.locator(selector).evaluate((element) => getComputedStyle(element).backgroundColor);
|
|
7
|
+
|
|
8
|
+
test.describe('<{{TAG}}> dark mode', () => {
|
|
9
|
+
test('defaults to light, per-instance data-theme="dark" goes dark', async ({ page }) => {
|
|
10
|
+
await page.goto('/test/dark-mode.html');
|
|
11
|
+
expect(await getHostBackground(page, '#light-default')).toBe('rgb(255, 255, 255)');
|
|
12
|
+
expect(await getHostBackground(page, '#dark-instance')).toBe('rgb(26, 26, 26)');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('an ancestor data-theme="dark" reaches the component', async ({ page }) => {
|
|
16
|
+
await page.goto('/test/dark-mode.html');
|
|
17
|
+
expect(await getHostBackground(page, '#dark-ancestor')).toBe('rgb(26, 26, 26)');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('a page color-scheme of dark is inherited', async ({ page }) => {
|
|
21
|
+
await page.goto('/test/dark-mode.html');
|
|
22
|
+
await page.evaluate(() => (document.documentElement.style.colorScheme = 'dark'));
|
|
23
|
+
expect(await getHostBackground(page, '#light-default')).toBe('rgb(26, 26, 26)');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('--base-* themes the component', async ({ page }) => {
|
|
27
|
+
await page.goto('/test/dark-mode.html');
|
|
28
|
+
expect(await getHostBackground(page, '#base-themed')).toBe('rgb(255, 0, 0)');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
test.describe('<{{TAG}}>', () => {
|
|
4
|
+
test('renders the label and reacts to attribute changes', async ({ page }) => {
|
|
5
|
+
await page.goto('/test/fixture.html');
|
|
6
|
+
const label = page.locator('#subject .{{PREFIX}}__label');
|
|
7
|
+
await expect(label).toHaveText('fixture');
|
|
8
|
+
|
|
9
|
+
await page.evaluate(() => document.getElementById('subject')!.setAttribute('label', 'changed'));
|
|
10
|
+
await expect(label).toHaveText('changed');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('click toggles the active state, reflects it and fires change', async ({ page }) => {
|
|
14
|
+
await page.goto('/test/fixture.html');
|
|
15
|
+
const changeDetails = page.evaluate(
|
|
16
|
+
() =>
|
|
17
|
+
new Promise<unknown>((resolve) =>
|
|
18
|
+
document.getElementById('subject')!.addEventListener('change', (event) => resolve((event as CustomEvent).detail), { once: true })
|
|
19
|
+
)
|
|
20
|
+
);
|
|
21
|
+
await page.locator('#subject .{{PREFIX}}__root').click();
|
|
22
|
+
expect(await changeDetails).toEqual({ isActive: true });
|
|
23
|
+
await expect(page.locator('#subject')).toHaveAttribute('active', '');
|
|
24
|
+
await expect(page.locator('#subject .{{PREFIX}}__root')).toHaveClass(/{{PREFIX}}__root--active/);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<meta name="kmwc:title" content="Basic Usage">
|
|
2
|
+
<meta name="kmwc:subtitle" content="The essentials of <{{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-accent-color</code> themes every Keenmate component in a subtree; <code>--{{PREFIX}}-accent-color</code> one instance.</p>
|
|
22
|
+
<kmwc-demo>
|
|
23
|
+
<div style="--base-accent-color: #10b981">
|
|
24
|
+
<{{TAG}} id="th-base" label="--base-accent-color" active></{{TAG}}>
|
|
25
|
+
<{{TAG}} id="th-local" label="--{{PREFIX}}-accent-color" active style="--{{PREFIX}}-accent-color: #8b5cf6"></{{TAG}}>
|
|
26
|
+
</div>
|
|
27
|
+
</kmwc-demo>
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<meta name="kmwc:layout" content="bare">
|
|
2
|
+
<meta name="kmwc:entry" content="none">
|
|
3
|
+
|
|
4
|
+
<!-- Header, cards, footer, version badge and the cross-page nav are all
|
|
5
|
+
rendered by the devkit from this registry + package.json#kmwc. -->
|
|
6
|
+
<script type="module">
|
|
7
|
+
import { renderExamplesIndex } from '@keenmate/web-components-devkit/examples';
|
|
8
|
+
|
|
9
|
+
renderExamplesIndex({
|
|
10
|
+
pages: [
|
|
11
|
+
{ href: 'examples-basic.html', icon: '🧩', title: 'Basic Usage', description: 'The essentials, as one live component driven by control switches.' },
|
|
12
|
+
{ href: 'examples-theming.html', icon: '🎨', title: 'Theming', description: 'Dark-mode signals, --base-* and --{{PREFIX}}-* overrides.' },
|
|
13
|
+
],
|
|
14
|
+
links: [{ label: 'API Docs', href: 'https://github.com/keenmate/{{ID}}/blob/main/docs/usage.md' }],
|
|
15
|
+
});
|
|
16
|
+
</script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logic class: framework-agnostic state + rendering for {{PACKAGE_NAME}}.
|
|
3
|
+
* Knows nothing about custom elements — it renders into the container it is
|
|
4
|
+
* given and reports back through the `on*` events in its config. The Element
|
|
5
|
+
* layer (web-component.ts) owns attribute parsing and DOM events.
|
|
6
|
+
*/
|
|
7
|
+
import type { {{CONFIG_TYPE}} } from './types.js';
|
|
8
|
+
|
|
9
|
+
const CSS_CLASSES = {
|
|
10
|
+
root: '{{PREFIX}}__root',
|
|
11
|
+
rootActive: '{{PREFIX}}__root--active',
|
|
12
|
+
label: '{{PREFIX}}__label',
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export class {{LOGIC_CLASS}} {
|
|
16
|
+
#container: HTMLElement;
|
|
17
|
+
#config: {{CONFIG_TYPE}};
|
|
18
|
+
#rootElement: HTMLButtonElement | null = null;
|
|
19
|
+
#labelElement: HTMLSpanElement | null = null;
|
|
20
|
+
|
|
21
|
+
constructor(container: HTMLElement, config: {{CONFIG_TYPE}}) {
|
|
22
|
+
this.#container = container;
|
|
23
|
+
this.#config = { ...config };
|
|
24
|
+
this.render();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Build the markup once. */
|
|
28
|
+
render(): void {
|
|
29
|
+
this.#container.innerHTML = `<button type="button" class="${CSS_CLASSES.root}" part="root"><span class="${CSS_CLASSES.label}" part="label"></span></button>`;
|
|
30
|
+
this.#rootElement = this.#container.querySelector(`.${CSS_CLASSES.root}`);
|
|
31
|
+
this.#labelElement = this.#container.querySelector(`.${CSS_CLASSES.label}`);
|
|
32
|
+
this.#rootElement?.addEventListener('click', this.handleRootClick);
|
|
33
|
+
this.update(this.#config);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Patch the rendered DOM to a (partial) new config. */
|
|
37
|
+
update(partialConfig: Partial<{{CONFIG_TYPE}}>): void {
|
|
38
|
+
this.#config = { ...this.#config, ...partialConfig };
|
|
39
|
+
if (this.#labelElement) this.#labelElement.textContent = this.#config.label;
|
|
40
|
+
this.#rootElement?.classList.toggle(CSS_CLASSES.rootActive, this.#config.isActive);
|
|
41
|
+
this.#rootElement?.setAttribute('aria-pressed', String(this.#config.isActive));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
toggle(): void {
|
|
45
|
+
this.update({ isActive: !this.#config.isActive });
|
|
46
|
+
this.#config.onChange?.({ isActive: this.#config.isActive });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
destroy(): void {
|
|
50
|
+
this.#rootElement?.removeEventListener('click', this.handleRootClick);
|
|
51
|
+
this.#container.innerHTML = '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private handleRootClick = (): void => this.toggle();
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* animations.css — no keyframe animations in this component. */
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* ==============================================================================
|
|
2
|
+
BASE STYLES
|
|
3
|
+
==============================================================================
|
|
4
|
+
FOUC prevention and the theme container (:host). */
|
|
5
|
+
|
|
6
|
+
/* ==============================================================================
|
|
7
|
+
FOUC PREVENTION
|
|
8
|
+
==============================================================================
|
|
9
|
+
Light-DOM rule (reaches the page via index.ts's side-effect CSS import):
|
|
10
|
+
reserve the post-upgrade footprint and hide child text until the element is
|
|
11
|
+
defined. The tag MUST equal the customElements.define() tag. */
|
|
12
|
+
{{TAG}}:not(:defined) {
|
|
13
|
+
display: inline-block;
|
|
14
|
+
min-height: calc(3.5 * 10px);
|
|
15
|
+
color: transparent !important;
|
|
16
|
+
background: transparent;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* ==============================================================================
|
|
20
|
+
THEME CONTAINER
|
|
21
|
+
==============================================================================
|
|
22
|
+
:host is the container: layout sanity, the default surface, text colour and
|
|
23
|
+
font. Self-painted host (D-TC-3 case 1). */
|
|
24
|
+
:host {
|
|
25
|
+
display: inline-block;
|
|
26
|
+
position: relative;
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
background: var(--{{PREFIX}}-bg);
|
|
29
|
+
color: var(--{{PREFIX}}-text-color-1);
|
|
30
|
+
font-family: var(--{{PREFIX}}-font-family);
|
|
31
|
+
font-size: var(--{{PREFIX}}-font-size-base);
|
|
32
|
+
border-radius: var(--{{PREFIX}}-root-border-radius);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
:host([hidden]) {
|
|
36
|
+
display: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
*,
|
|
40
|
+
*::before,
|
|
41
|
+
*::after {
|
|
42
|
+
box-sizing: inherit;
|
|
43
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* ==============================================================================
|
|
2
|
+
CONTROLS
|
|
3
|
+
==============================================================================
|
|
4
|
+
The root toggle button. */
|
|
5
|
+
.{{PREFIX}}__root {
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
min-height: var(--{{PREFIX}}-root-min-height);
|
|
9
|
+
padding: var(--{{PREFIX}}-root-padding-block) var(--{{PREFIX}}-root-padding-inline);
|
|
10
|
+
border: 1px solid var(--{{PREFIX}}-border-color);
|
|
11
|
+
border-radius: var(--{{PREFIX}}-root-border-radius);
|
|
12
|
+
background: transparent;
|
|
13
|
+
color: inherit;
|
|
14
|
+
font: inherit;
|
|
15
|
+
cursor: pointer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.{{PREFIX}}__label {
|
|
19
|
+
white-space: nowrap;
|
|
20
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/* ==============================================================================
|
|
2
|
+
DARK MODE — framework class + per-instance signal handling (Strategy B)
|
|
3
|
+
==============================================================================
|
|
4
|
+
Every --{{PREFIX}}-* colour in variables.css resolves through light-dark(),
|
|
5
|
+
so flipping `color-scheme` on the host picks the right branch without
|
|
6
|
+
touching any --base-* value — the consumer's themed --base-* stays the
|
|
7
|
+
source of truth. Declared ONLY when the consumer signals intent, never bare
|
|
8
|
+
on :host (that would shadow the page's inherited color-scheme).
|
|
9
|
+
|
|
10
|
+
Signals handled (color-scheme.md):
|
|
11
|
+
#1-#3 OS / forced / page color-scheme — inherited, light-dark() resolves.
|
|
12
|
+
#4 Framework class on an ancestor — [data-theme], [data-bs-theme], .dark / .light
|
|
13
|
+
#5 Per-instance attribute — <{{TAG}} data-theme="dark|light">, wins over #4. */
|
|
14
|
+
|
|
15
|
+
/* DARK — framework class on an ancestor (signal #4) */
|
|
16
|
+
:host-context([data-theme="dark"]),
|
|
17
|
+
:host-context([data-bs-theme="dark"]),
|
|
18
|
+
:host-context(.dark) {
|
|
19
|
+
color-scheme: dark;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* LIGHT — framework class on an ancestor (signal #4) */
|
|
23
|
+
:host-context([data-theme="light"]),
|
|
24
|
+
:host-context([data-bs-theme="light"]),
|
|
25
|
+
:host-context(.light) {
|
|
26
|
+
color-scheme: light;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Per-instance attribute on the host (signal #5) */
|
|
30
|
+
:host([data-theme="dark"]) {
|
|
31
|
+
color-scheme: dark;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
:host([data-theme="light"]) {
|
|
35
|
+
color-scheme: light;
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* floating.css — no floating elements in this component. */
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* ==============================================================================
|
|
2
|
+
{{TITLE_UPPER}} — MAIN ENTRY POINT
|
|
3
|
+
==============================================================================
|
|
4
|
+
Cascade layer order (later = higher priority):
|
|
5
|
+
variables — :host { --{{PREFIX}}-* } declarations
|
|
6
|
+
component — base + feature rules
|
|
7
|
+
overrides — dark mode, framework themes, per-instance overrides
|
|
8
|
+
|
|
9
|
+
Consumer override contract:
|
|
10
|
+
- Any unlayered consumer rule beats every rule in these layers.
|
|
11
|
+
- Any :root --base-* declaration beats the variables layer trivially.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
@layer variables, component, overrides;
|
|
15
|
+
|
|
16
|
+
/* === SKELETON + CANONICAL CONCERNS (Tier 1 + Tier 2) === */
|
|
17
|
+
@import url('./variables.css') layer(variables);
|
|
18
|
+
@import url('./base.css') layer(component);
|
|
19
|
+
@import url('./controls.css') layer(component);
|
|
20
|
+
@import url('./floating.css') layer(component);
|
|
21
|
+
@import url('./states.css') layer(component);
|
|
22
|
+
@import url('./animations.css') layer(component);
|
|
23
|
+
|
|
24
|
+
/* === COMPONENT-SPECIFIC FEATURES (Tier 3 — alphabetized) === */
|
|
25
|
+
|
|
26
|
+
/* === OVERRIDES === */
|
|
27
|
+
@import url('./dark-mode.css') layer(overrides);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* ==============================================================================
|
|
2
|
+
STATES
|
|
3
|
+
==============================================================================
|
|
4
|
+
Hover, focus and the active (pressed) modifier. */
|
|
5
|
+
.{{PREFIX}}__root:hover {
|
|
6
|
+
background: var(--{{PREFIX}}-hover-bg);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.{{PREFIX}}__root:focus-visible {
|
|
10
|
+
outline: 2px solid var(--{{PREFIX}}-accent-color);
|
|
11
|
+
outline-offset: 2px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.{{PREFIX}}__root--active {
|
|
15
|
+
background: var(--{{PREFIX}}-accent-color);
|
|
16
|
+
border-color: var(--{{PREFIX}}-accent-color);
|
|
17
|
+
color: var(--{{PREFIX}}-text-color-on-accent);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.{{PREFIX}}__root--active:hover {
|
|
21
|
+
background: var(--{{PREFIX}}-accent-color);
|
|
22
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* ==============================================================================
|
|
2
|
+
CSS CUSTOM PROPERTIES (:host level)
|
|
3
|
+
==============================================================================
|
|
4
|
+
Every --{{PREFIX}}-* variable reads through the cross-component --base-* hook
|
|
5
|
+
with a standalone fallback; colours fall back to light-dark(), so the
|
|
6
|
+
component follows the page's color-scheme. `color-scheme` is deliberately
|
|
7
|
+
NOT declared here (it would shadow the page's) — see dark-mode.css.
|
|
8
|
+
|
|
9
|
+
Override from outside the shadow DOM:
|
|
10
|
+
{{TAG}} { --{{PREFIX}}-accent-color: #10b981; }
|
|
11
|
+
============================================================================== */
|
|
12
|
+
|
|
13
|
+
:host {
|
|
14
|
+
/* ==========================================================================
|
|
15
|
+
SCALE
|
|
16
|
+
==========================================================================
|
|
17
|
+
Unitless --base-* sizes multiply through --{{PREFIX}}-rem. Set it to 1rem
|
|
18
|
+
to follow the host page's font size (Pure Admin integration). */
|
|
19
|
+
--{{PREFIX}}-rem: 10px;
|
|
20
|
+
|
|
21
|
+
/* ==========================================================================
|
|
22
|
+
COLORS
|
|
23
|
+
========================================================================== */
|
|
24
|
+
--{{PREFIX}}-bg: var(--base-main-bg, light-dark(#ffffff, #1a1a1a));
|
|
25
|
+
--{{PREFIX}}-text-color-1: var(--base-text-color-1, light-dark(#242424, #f5f5f5));
|
|
26
|
+
--{{PREFIX}}-border-color: var(--base-border-color, light-dark(#e0e0e0, #3d3d3d));
|
|
27
|
+
--{{PREFIX}}-accent-color: var(--base-accent-color, light-dark(#0078d4, #3a96dd));
|
|
28
|
+
--{{PREFIX}}-text-color-on-accent: var(--base-text-color-on-accent, light-dark(#ffffff, #ffffff));
|
|
29
|
+
--{{PREFIX}}-hover-bg: var(--base-hover-bg, color-mix(in srgb, var(--{{PREFIX}}-text-color-1) 8%, var(--{{PREFIX}}-bg)));
|
|
30
|
+
|
|
31
|
+
/* ==========================================================================
|
|
32
|
+
TYPOGRAPHY & SHAPE
|
|
33
|
+
========================================================================== */
|
|
34
|
+
--{{PREFIX}}-font-family: var(--base-font-family, inherit);
|
|
35
|
+
--{{PREFIX}}-font-size-base: calc(var(--base-font-size-base, 1.6) * var(--{{PREFIX}}-rem));
|
|
36
|
+
--{{PREFIX}}-root-border-radius: calc(var(--base-border-radius-md, 0.6) * var(--{{PREFIX}}-rem));
|
|
37
|
+
--{{PREFIX}}-root-min-height: calc(var(--base-input-size-md-height, 3.5) * var(--{{PREFIX}}-rem));
|
|
38
|
+
--{{PREFIX}}-root-padding-block: calc(0.6 * var(--{{PREFIX}}-rem));
|
|
39
|
+
--{{PREFIX}}-root-padding-inline: calc(1.2 * var(--{{PREFIX}}-rem));
|
|
40
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Build-time constants defined by the devkit Vite preset (from package.json).
|
|
2
|
+
declare const __VERSION__: string;
|
|
3
|
+
declare const __PACKAGE_NAME__: string;
|
|
4
|
+
declare const __AUTHOR__: string;
|
|
5
|
+
declare const __LICENSE__: string;
|
|
6
|
+
declare const __REPOSITORY__: string;
|
|
7
|
+
declare const __HOMEPAGE__: string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Light-DOM stylesheet (dist/style.css) — carries the :not(:defined) FOUC rule
|
|
2
|
+
// from base.css. The shadow root gets the same CSS via ?inline in web-component.ts.
|
|
3
|
+
import './css/main.css';
|
|
4
|
+
|
|
5
|
+
import { registerComponent } from '@keenmate/web-components-core';
|
|
6
|
+
import { {{ELEMENT_CLASS}} } from './web-component.js';
|
|
7
|
+
import { logging } from './logger.js';
|
|
8
|
+
|
|
9
|
+
export { {{ELEMENT_CLASS}} };
|
|
10
|
+
export { {{LOGIC_CLASS}} } from './{{FEATURE}}.js';
|
|
11
|
+
export type { {{CONFIG_TYPE}}, {{EVENT_DETAIL_TYPE}} } from './types.js';
|
|
12
|
+
export { logging };
|
|
13
|
+
|
|
14
|
+
registerComponent('{{TAG}}', {{ELEMENT_CLASS}} as unknown as CustomElementConstructor, {
|
|
15
|
+
config: { name: __PACKAGE_NAME__, version: __VERSION__, author: __AUTHOR__ },
|
|
16
|
+
logging,
|
|
17
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side layer: categorized loggers for {{PACKAGE_NAME}}, built on the core
|
|
3
|
+
* logging module. Enable from the console:
|
|
4
|
+
*
|
|
5
|
+
* window.components['{{TAG}}'].logging.enableLogging();
|
|
6
|
+
*/
|
|
7
|
+
import { createLoggers } from '@keenmate/web-components-core';
|
|
8
|
+
|
|
9
|
+
export const LOGGING_CATEGORIES = ['INIT', 'UI', 'INTERACTION'] as const;
|
|
10
|
+
|
|
11
|
+
export const logging = createLoggers('{{LOG_NAME}}', LOGGING_CATEGORIES);
|