@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,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,28 @@
|
|
|
1
|
+
/* ==============================================================================
|
|
2
|
+
CSS CUSTOM PROPERTIES (:host level)
|
|
3
|
+
==============================================================================
|
|
4
|
+
Every --{{PREFIX}}-* variable reads through the cross-component --base-* hook
|
|
5
|
+
with a standalone fallback: an input falls back to the contract default, a
|
|
6
|
+
derived token to the contract formula on this component's own tokens
|
|
7
|
+
(base-contract-check). Colours use light-dark(), so the component follows
|
|
8
|
+
the page's color-scheme. `color-scheme` is deliberately
|
|
9
|
+
NOT declared here (it would shadow the page's) — see dark-mode.css.
|
|
10
|
+
|
|
11
|
+
Override from outside the shadow DOM:
|
|
12
|
+
{{TAG}} { --{{PREFIX}}-bg: #f0fdf4; }
|
|
13
|
+
============================================================================== */
|
|
14
|
+
|
|
15
|
+
:host {
|
|
16
|
+
/* Unitless --base-* sizes multiply through --{{PREFIX}}-rem. */
|
|
17
|
+
--{{PREFIX}}-rem: 10px;
|
|
18
|
+
|
|
19
|
+
--{{PREFIX}}-bg: var(--base-main-bg, light-dark(#ffffff, #1e293b));
|
|
20
|
+
--{{PREFIX}}-text-color-1: var(--base-text-color-1, light-dark(#334155, #f1f5f9));
|
|
21
|
+
--{{PREFIX}}-border-color: var(--base-border-color, light-dark(oklch(from var(--{{PREFIX}}-bg) calc(l - 0.096) c h), oklch(from var(--{{PREFIX}}-bg) calc(l + 0.192) c h)));
|
|
22
|
+
|
|
23
|
+
--{{PREFIX}}-font-family: var(--base-font-family, inherit);
|
|
24
|
+
--{{PREFIX}}-font-size-base: calc(var(--base-font-size-base, 1.6) * var(--{{PREFIX}}-rem));
|
|
25
|
+
--{{PREFIX}}-root-border-radius: calc(var(--base-border-radius-md, 0.6) * var(--{{PREFIX}}-rem));
|
|
26
|
+
--{{PREFIX}}-root-padding-block: calc(0.8 * var(--{{PREFIX}}-rem));
|
|
27
|
+
--{{PREFIX}}-root-padding-inline: calc(1.2 * var(--{{PREFIX}}-rem));
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Light-DOM stylesheet (dist/style.css) — carries the :not(:defined) FOUC rule
|
|
2
|
+
// from base.css. The shadow root gets the same CSS via ?inline in web-component.ts.
|
|
3
|
+
import './css/main.css';
|
|
4
|
+
|
|
5
|
+
import { registerComponent } from '@keenmate/web-components-core';
|
|
6
|
+
import { {{ELEMENT_CLASS}} } from './web-component.js';
|
|
7
|
+
import { logging } from './logger.js';
|
|
8
|
+
|
|
9
|
+
export { {{ELEMENT_CLASS}} };
|
|
10
|
+
export { {{LOGIC_CLASS}} } from './{{FEATURE}}.js';
|
|
11
|
+
export type { {{CONFIG_TYPE}} } from './types.js';
|
|
12
|
+
export { logging };
|
|
13
|
+
|
|
14
|
+
registerComponent('{{TAG}}', {{ELEMENT_CLASS}} as unknown as CustomElementConstructor, {
|
|
15
|
+
config: { name: __PACKAGE_NAME__, version: __VERSION__, author: __AUTHOR__ },
|
|
16
|
+
logging,
|
|
17
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side layer: the public types of {{PACKAGE_NAME}}. No imports from the other
|
|
3
|
+
* layers — this file could be lifted into its own package tomorrow.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configuration of the framework-agnostic logic class ({{LOGIC_CLASS}}).
|
|
8
|
+
* Add a field here for every row you add to ATTRIBUTE_TABLE.
|
|
9
|
+
*/
|
|
10
|
+
export interface {{CONFIG_TYPE}} {}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element layer: the `<{{TAG}}>` custom element. Parses inputs (attribute +
|
|
3
|
+
* property, through the core input table), hosts the lifecycle and owns the
|
|
4
|
+
* shadow root. Talks to {{LOGIC_CLASS}} only — never to service classes directly.
|
|
5
|
+
*/
|
|
6
|
+
import { BlissElement, adoptStyles, type InputDef } from '@keenmate/web-components-core';
|
|
7
|
+
import { {{LOGIC_CLASS}} } from './{{FEATURE}}.js';
|
|
8
|
+
import styles from './css/main.css?inline';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Every public input is one row: HTML attribute ⇔ config key, converter
|
|
12
|
+
* (parsing + validation), and what a change triggers, e.g.
|
|
13
|
+
*
|
|
14
|
+
* { configKey: 'isOpen', attribute: 'open', converter: toBool('default-false'), on: 'update', description: '…' },
|
|
15
|
+
*
|
|
16
|
+
* Attribute names read like HTML; config keys read like JavaScript.
|
|
17
|
+
*/
|
|
18
|
+
export const ATTRIBUTE_TABLE: readonly InputDef[] = [];
|
|
19
|
+
|
|
20
|
+
export class {{ELEMENT_CLASS}} extends BlissElement {
|
|
21
|
+
protected static override inputs = ATTRIBUTE_TABLE;
|
|
22
|
+
|
|
23
|
+
#shadow: ShadowRoot | null = null;
|
|
24
|
+
/** The logic class renders here, so it never touches the adopted/fallback <style>. */
|
|
25
|
+
#logicContainer: HTMLElement | null = null;
|
|
26
|
+
#logic: {{LOGIC_CLASS}} | null = null;
|
|
27
|
+
|
|
28
|
+
/** First connect + any `on: 'reinit'` change: (re)build from the full config. */
|
|
29
|
+
protected override reinit(): void {
|
|
30
|
+
this.#shadow ??= this.attachShadow({ mode: 'open' });
|
|
31
|
+
adoptStyles(this.#shadow, styles);
|
|
32
|
+
this.#logicContainer ??= this.#shadow.appendChild(document.createElement('div'));
|
|
33
|
+
this.#logic?.destroy();
|
|
34
|
+
this.#logic = new {{LOGIC_CLASS}}(this.#logicContainer, {});
|
|
35
|
+
this.log.INIT?.debug('built');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `on: 'update'` changes: patch in place. */
|
|
39
|
+
protected override update(partial: Record<string, unknown>): void {
|
|
40
|
+
this.#logic?.update(partial);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Importing this module registers the element. index.ts additionally publishes
|
|
45
|
+
// the global API via registerComponent(); both defines are idempotent.
|
|
46
|
+
if (typeof customElements !== 'undefined' && !customElements.get('{{TAG}}')) {
|
|
47
|
+
customElements.define('{{TAG}}', {{ELEMENT_CLASS}});
|
|
48
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-kmwc="off">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>{{TAG}} dark-mode fixture</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<{{TAG}} id="light-default">light default</{{TAG}}>
|
|
9
|
+
<{{TAG}} id="dark-instance" data-theme="dark">dark instance</{{TAG}}>
|
|
10
|
+
<div data-theme="dark">
|
|
11
|
+
<{{TAG}} id="dark-ancestor">dark ancestor</{{TAG}}>
|
|
12
|
+
</div>
|
|
13
|
+
<div style="--base-main-bg: rgb(255, 0, 0)">
|
|
14
|
+
<{{TAG}} id="base-themed">--base-main-bg</{{TAG}}>
|
|
15
|
+
</div>
|
|
16
|
+
<script type="module" src="/src/index.ts"></script>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{{HEADER}}
|
|
2
|
+
#
|
|
3
|
+
# Static server for the {{ID}} examples site. 100% static (HTML + bundled
|
|
4
|
+
# assets + demo data), no backend — so the vulnerability scanners that probe the
|
|
5
|
+
# public host (leakix/l9scan et al. hitting /api/gql, info.php, .git, .env, …)
|
|
6
|
+
# have nothing to exploit. Drop that traffic with 444 (close, no response).
|
|
7
|
+
|
|
8
|
+
server {
|
|
9
|
+
listen 80;
|
|
10
|
+
server_name _;
|
|
11
|
+
root /usr/share/nginx/html;
|
|
12
|
+
index index.html;
|
|
13
|
+
|
|
14
|
+
# 1. Known vulnerability scanners by User-Agent → close immediately.
|
|
15
|
+
if ($http_user_agent ~* "(l9scan|leakix|nikto|sqlmap|nmap|masscan|zgrab|censys|nuclei|httpx|semrush|petalbot)") {
|
|
16
|
+
return 444;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# 2. Exploit-probe file extensions this static site never serves.
|
|
20
|
+
location ~* \.(php|phtml|asp|aspx|jsp|cgi|pl|env|ini|sh|bak|old|sql)$ {
|
|
21
|
+
access_log off;
|
|
22
|
+
return 444;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# 3. Exploit-probe paths (app frameworks, VCS/CI metadata, dotfiles).
|
|
26
|
+
location ~* /(\.git|\.svn|\.hg|\.env|wp-admin|wp-login|wp-content|wp-includes|xmlrpc|telescope|actuator|vendor|META-INF|WEB-INF|phpmyadmin|pma|adminer|owa)(/|$) {
|
|
27
|
+
access_log off;
|
|
28
|
+
return 444;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# 4. Vite-bundled assets carry a content hash in their name — cache forever.
|
|
32
|
+
location /assets/ {
|
|
33
|
+
try_files $uri =404;
|
|
34
|
+
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# 5. Pages and un-hashed files (demo data, docs): always revalidate (cheap
|
|
38
|
+
# 304 via ETag), so a redeploy is visible immediately.
|
|
39
|
+
location / {
|
|
40
|
+
try_files $uri $uri/ =404;
|
|
41
|
+
add_header Cache-Control "no-cache" always;
|
|
42
|
+
}
|
|
43
|
+
}
|