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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/LICENSE +21 -0
  3. package/README.md +434 -2
  4. package/components.json +206 -0
  5. package/package.json +77 -4
  6. package/src/cli/args.js +16 -0
  7. package/src/cli/commands/check-release.js +66 -0
  8. package/src/cli/commands/check-vars.js +116 -0
  9. package/src/cli/commands/doctor.js +289 -0
  10. package/src/cli/commands/new.js +323 -0
  11. package/src/cli/commands/readme.js +111 -0
  12. package/src/cli/commands/sync.js +48 -0
  13. package/src/cli/kmwc.js +73 -0
  14. package/src/cli/sync-plan.js +163 -0
  15. package/src/config.d.ts +149 -0
  16. package/src/config.js +279 -0
  17. package/src/examples/auto.js +20 -0
  18. package/src/examples/chapter-nav.js +246 -0
  19. package/src/examples/constants.js +80 -0
  20. package/src/examples/controls-persistence.js +97 -0
  21. package/src/examples/copy-link.js +89 -0
  22. package/src/examples/css/base.css +215 -0
  23. package/src/examples/css/chapter-nav.css +179 -0
  24. package/src/examples/css/content.css +251 -0
  25. package/src/examples/css/controls.css +205 -0
  26. package/src/examples/css/copy-link.css +82 -0
  27. package/src/examples/css/demo.css +50 -0
  28. package/src/examples/css/index-page.css +118 -0
  29. package/src/examples/css/style.css +30 -0
  30. package/src/examples/css/theme-switch.css +38 -0
  31. package/src/examples/css/tokens.css +124 -0
  32. package/src/examples/css/widgets.css +133 -0
  33. package/src/examples/demo-code.js +30 -0
  34. package/src/examples/index-page.js +78 -0
  35. package/src/examples/index.d.ts +107 -0
  36. package/src/examples/index.js +18 -0
  37. package/src/examples/kit.js +22 -0
  38. package/src/examples/page-config.js +30 -0
  39. package/src/examples/sections.js +53 -0
  40. package/src/examples/theme-switch.js +72 -0
  41. package/src/examples/version-badge.js +23 -0
  42. package/src/playwright/index.d.ts +15 -0
  43. package/src/playwright/index.js +66 -0
  44. package/src/template.js +11 -0
  45. package/src/vite/index.d.ts +28 -0
  46. package/src/vite/index.js +243 -0
  47. package/src/vite/page-composer.js +216 -0
  48. package/templates/Dockerfile.tmpl +39 -0
  49. package/templates/Makefile.tmpl +136 -0
  50. package/templates/claude-block.md +40 -0
  51. package/templates/dockerignore.tmpl +10 -0
  52. package/templates/examples-layout.html +13 -0
  53. package/templates/new/.claude/commands/publish.md +226 -0
  54. package/templates/new/CHANGELOG.md +13 -0
  55. package/templates/new/LICENSE +21 -0
  56. package/templates/new/README.md +71 -0
  57. package/templates/new/_gitignore +7 -0
  58. package/templates/new/component-variables.manifest.json +32 -0
  59. package/templates/new/custom-elements-manifest.config.mjs +23 -0
  60. package/templates/new/docs/accessibility.md +24 -0
  61. package/templates/new/docs/examples.md +33 -0
  62. package/templates/new/docs/theming.md +54 -0
  63. package/templates/new/docs/usage.md +51 -0
  64. package/templates/new/e2e/dark-mode.spec.ts +30 -0
  65. package/templates/new/e2e/smoke.spec.ts +26 -0
  66. package/templates/new/examples-basic.html +57 -0
  67. package/templates/new/examples-theming.html +28 -0
  68. package/templates/new/index.html +16 -0
  69. package/templates/new/partials/note.html +5 -0
  70. package/templates/new/playwright.config.ts +4 -0
  71. package/templates/new/src/__FEATURE__.ts +55 -0
  72. package/templates/new/src/css/animations.css +1 -0
  73. package/templates/new/src/css/base.css +43 -0
  74. package/templates/new/src/css/controls.css +20 -0
  75. package/templates/new/src/css/dark-mode.css +36 -0
  76. package/templates/new/src/css/floating.css +1 -0
  77. package/templates/new/src/css/main.css +27 -0
  78. package/templates/new/src/css/states.css +22 -0
  79. package/templates/new/src/css/variables.css +42 -0
  80. package/templates/new/src/env.d.ts +7 -0
  81. package/templates/new/src/index.ts +17 -0
  82. package/templates/new/src/logger.ts +11 -0
  83. package/templates/new/src/types.ts +25 -0
  84. package/templates/new/src/web-component.ts +73 -0
  85. package/templates/new/test/dark-mode.html +18 -0
  86. package/templates/new/test/fixture.html +11 -0
  87. package/templates/new/tsconfig.json +15 -0
  88. package/templates/new/vite.config.ts +5 -0
  89. package/templates/new-minimal/CHANGELOG.md +13 -0
  90. package/templates/new-minimal/README.md +71 -0
  91. package/templates/new-minimal/component-variables.manifest.json +24 -0
  92. package/templates/new-minimal/docs/accessibility.md +20 -0
  93. package/templates/new-minimal/docs/examples.md +17 -0
  94. package/templates/new-minimal/docs/theming.md +53 -0
  95. package/templates/new-minimal/docs/usage.md +43 -0
  96. package/templates/new-minimal/e2e/smoke.spec.ts +10 -0
  97. package/templates/new-minimal/examples-basic.html +23 -0
  98. package/templates/new-minimal/examples-theming.html +28 -0
  99. package/templates/new-minimal/src/__FEATURE__.ts +37 -0
  100. package/templates/new-minimal/src/css/base.css +48 -0
  101. package/templates/new-minimal/src/css/controls.css +1 -0
  102. package/templates/new-minimal/src/css/states.css +1 -0
  103. package/templates/new-minimal/src/css/variables.css +28 -0
  104. package/templates/new-minimal/src/index.ts +17 -0
  105. package/templates/new-minimal/src/types.ts +10 -0
  106. package/templates/new-minimal/src/web-component.ts +48 -0
  107. package/templates/new-minimal/test/dark-mode.html +18 -0
  108. package/templates/new-minimal/test/fixture.html +11 -0
  109. package/templates/nginx.conf.tmpl +43 -0
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Page composition for example pages — the "one copy of every repeated line"
3
+ * half of the examples kit. Runs inside the kmwc Vite plugin (dev and build),
4
+ * before the kit is injected:
5
+ *
6
+ * 1. <kmwc-include src="partials/x.html" any-param="…">inner</kmwc-include>
7
+ * → the partial, with {{anyParam}} / {{content}} substituted. Nested
8
+ * includes are fine; cycles are an error.
9
+ * 2. <kmwc-demo>…markup…</kmwc-demo>
10
+ * → the live markup AND a code block of that exact markup, so a snippet
11
+ * can never drift from its demo. <script data-kmwc-show> inside a demo is
12
+ * executed and shown too.
13
+ * 3. A page without <html> is a FRAGMENT: it gets the devkit layout (head,
14
+ * header with back link / title / subtitle, container, component entry
15
+ * script), driven by <meta name="kmwc:…"> tags:
16
+ * kmwc:title page title (h1 + <title>) — required
17
+ * kmwc:subtitle header subtitle
18
+ * kmwc:layout page (default) | bare (no header / container — e.g. the index)
19
+ * kmwc:entry component entry script; default /<library.entry>, "none" to skip
20
+ * kmwc:body-class classes for <body> (e.g. dark-output)
21
+ * <link> and <style> elements anywhere in a fragment move into <head>
22
+ * (not those inside a <script> — there they are strings, e.g. shadow-DOM styles).
23
+ * Full documents (<html …>) are left as they are — pages migrate one at a time.
24
+ *
25
+ * Pure string work on purpose: unit-testable, no HTML parser dependency. The
26
+ * tag shapes are deliberately strict (no nesting of a tag inside itself).
27
+ */
28
+ import { readFileSync, existsSync } from 'node:fs';
29
+ import { join, resolve } from 'node:path';
30
+ import { DEVKIT_ROOT } from '../config.js';
31
+ import { renderTemplate } from '../template.js';
32
+
33
+ const LAYOUT_TEMPLATE_PATH = join(DEVKIT_ROOT, 'templates', 'examples-layout.html');
34
+ const MAX_INCLUDE_DEPTH = 10;
35
+ const DEFAULT_LAYOUT = 'page';
36
+ const LAYOUTS = ['page', 'bare'];
37
+
38
+ const INCLUDE_PATTERN = /<kmwc-include\b([^>]*?)(?:\/>|>([\s\S]*?)<\/kmwc-include>)/g;
39
+ const DEMO_PATTERN = /<kmwc-demo\b([^>]*)>([\s\S]*?)<\/kmwc-demo>/g;
40
+ const SHOWN_SCRIPT_PATTERN = /<script\b([^>]*\bdata-kmwc-show\b[^>]*)>([\s\S]*?)<\/script>/g;
41
+ const PAGE_META_PATTERN = /<meta\s+name="kmwc:([a-z-]+)"\s+content="([^"]*)"\s*\/?>\s*/g;
42
+ const HEAD_ELEMENT_PATTERN = /<link\b[^>]*>\s*|<style\b[^>]*>[\s\S]*?<\/style>\s*/g;
43
+ const ATTRIBUTE_PATTERN = /([a-zA-Z][\w-]*)(?:\s*=\s*"([^"]*)")?/g;
44
+
45
+ const HTML_COMMENT_PATTERN = /<!--[\s\S]*?-->/g;
46
+ // Comments plus whole <script> elements: a <style> / <link> inside a script is a string
47
+ // (shadow-DOM styles, an SVG's own <style>), never a page element to move into <head>.
48
+ const COMMENT_OR_SCRIPT_PATTERN = /<!--[\s\S]*?-->|<script\b[^>]*>[\s\S]*?<\/script>/gi;
49
+ // A full document STARTS with a doctype or <html> (after a BOM, whitespace, comments). Matching
50
+ // `<html>` anywhere would misread a fragment whose script or code sample mentions it.
51
+ const DOCUMENT_START_PATTERN = /^?(?:\s|<!--[\s\S]*?-->)*<(?:!doctype\b|html[\s>])/i;
52
+
53
+ /**
54
+ * Apply `mapper` to the text OUTSIDE HTML comments only, so a usage example in
55
+ * a comment (`<!-- <kmwc-include src="…"> -->`) is never expanded.
56
+ */
57
+ export function mapOutsideComments(html, mapper, skippedPattern = HTML_COMMENT_PATTERN) {
58
+ let mappedHtml = '';
59
+ let lastIndex = 0;
60
+ for (const skipped of html.matchAll(skippedPattern)) {
61
+ mappedHtml += mapper(html.slice(lastIndex, skipped.index)) + skipped[0];
62
+ lastIndex = skipped.index + skipped[0].length;
63
+ }
64
+ return mappedHtml + mapper(html.slice(lastIndex));
65
+ }
66
+
67
+ const mapToCamelCase = (kebabName) => kebabName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
68
+
69
+ const HTML_ESCAPES = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' };
70
+ const mapToEscapedHtml = (text) => String(text).replace(/[&<>"]/g, (character) => HTML_ESCAPES[character]);
71
+
72
+ /** `preview-class="x" code` → { previewClass: 'x', code: '' } */
73
+ export function getAttributes(attributeText) {
74
+ const attributes = {};
75
+ for (const match of attributeText.matchAll(ATTRIBUTE_PATTERN)) {
76
+ attributes[mapToCamelCase(match[1])] = match[2] ?? '';
77
+ }
78
+ return attributes;
79
+ }
80
+
81
+ /** Strip the common leading indentation and surrounding blank lines. */
82
+ export function mapToDedentedText(text) {
83
+ const lines = text.replace(/\t/g, ' ').split(/\r?\n/);
84
+ while (lines.length && !lines[0].trim()) lines.shift();
85
+ while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
86
+ const indentation = Math.min(...lines.filter((line) => line.trim()).map((line) => line.match(/^ */)[0].length));
87
+ return lines.map((line) => line.slice(Number.isFinite(indentation) ? indentation : 0)).join('\n');
88
+ }
89
+
90
+ /**
91
+ * Replace every <kmwc-include> (recursively). Parameters are the tag's
92
+ * attributes (kebab → camel: `note-title` → {{noteTitle}}) plus {{content}}
93
+ * (the tag's inner HTML). Unknown {{placeholders}} are left untouched — a
94
+ * partial may legitimately show template syntax.
95
+ */
96
+ export function resolveIncludes(html, { root, includeChain = [], readFile = defaultReadFile }) {
97
+ if (includeChain.length > MAX_INCLUDE_DEPTH) throw new Error(`kmwc: includes nested deeper than ${MAX_INCLUDE_DEPTH}: ${includeChain.join(' → ')}`);
98
+ return mapOutsideComments(html, (text) =>
99
+ text.replace(INCLUDE_PATTERN, (includeTag, attributeText, innerHtml = '') => {
100
+ const { src: partialPath, ...parameters } = getAttributes(attributeText);
101
+ if (!partialPath) throw new Error(`kmwc: <kmwc-include> without src: ${includeTag.slice(0, 80)}`);
102
+ if (includeChain.includes(partialPath)) throw new Error(`kmwc: include cycle: ${[...includeChain, partialPath].join(' → ')}`);
103
+ const partialHtml = readFile(resolve(root, partialPath), partialPath);
104
+ const variables = { ...parameters, content: innerHtml.trim() };
105
+ const filledHtml = partialHtml.replace(/\{\{\s*([a-zA-Z][\w]*)\s*\}\}/g, (placeholder, name) => (name in variables ? variables[name] : placeholder));
106
+ return resolveIncludes(filledHtml, { root, includeChain: [...includeChain, partialPath], readFile });
107
+ })
108
+ );
109
+ }
110
+
111
+ function defaultReadFile(absolutePath, displayPath) {
112
+ if (!existsSync(absolutePath)) throw new Error(`kmwc: <kmwc-include src="${displayPath}"> — file not found (${absolutePath})`);
113
+ return readFileSync(absolutePath, 'utf8');
114
+ }
115
+
116
+ function renderCodeBlock(codeText, language) {
117
+ return `<div class="code-block kmwc-demo__code" data-language="${language}"><pre><code>${mapToEscapedHtml(codeText)}</code></pre></div>`;
118
+ }
119
+
120
+ /**
121
+ * Turn every <kmwc-demo> into live markup + the code that produced it.
122
+ * Attributes: `preview-class` (default "demo-area"), `markup-hidden` (show
123
+ * only the scripts' code), `code-hidden` (live only — rarely what you want).
124
+ */
125
+ export function renderDemos(html) {
126
+ return mapOutsideComments(html, (text) => text.replace(DEMO_PATTERN, renderDemo));
127
+ }
128
+
129
+ function renderDemo(demoTag, attributeText, innerHtml) {
130
+ const attributes = getAttributes(attributeText);
131
+ const shownScripts = [];
132
+ const markupHtml = innerHtml.replace(SHOWN_SCRIPT_PATTERN, (scriptTag, scriptAttributeText, scriptBody) => {
133
+ shownScripts.push({ attributeText: scriptAttributeText.replace(/\s*\bdata-kmwc-show\b(="[^"]*")?/, ''), body: scriptBody });
134
+ return '';
135
+ });
136
+ const codeBlocks = [];
137
+ if (!('markupHidden' in attributes) && markupHtml.trim()) codeBlocks.push(renderCodeBlock(mapToDedentedText(markupHtml), 'html'));
138
+ for (const script of shownScripts) codeBlocks.push(renderCodeBlock(mapToDedentedText(script.body), 'js'));
139
+
140
+ const previewClass = attributes.previewClass ?? 'demo-area';
141
+ const scriptsHtml = shownScripts.map((script) => `<script${script.attributeText}>${script.body}</script>`).join('\n');
142
+ return (
143
+ `<div class="kmwc-demo">\n` +
144
+ `<div class="kmwc-demo__preview ${previewClass}">${markupHtml}</div>\n` +
145
+ ('codeHidden' in attributes ? '' : codeBlocks.join('\n')) +
146
+ `\n</div>\n${scriptsHtml}`
147
+ );
148
+ }
149
+
150
+ /** Read and remove the page's <meta name="kmwc:…"> tags (`kmwc:body-class` → `bodyClass`). */
151
+ export function getPageMeta(html) {
152
+ const pageMeta = {};
153
+ const htmlWithoutMeta = mapOutsideComments(html, (text) =>
154
+ text.replace(PAGE_META_PATTERN, (metaTag, name, content) => {
155
+ pageMeta[mapToCamelCase(name)] = content;
156
+ return '';
157
+ })
158
+ );
159
+ return { pageMeta, htmlWithoutMeta };
160
+ }
161
+
162
+ export const checkIsFragment = (html) => !DOCUMENT_START_PATTERN.test(html);
163
+
164
+ /** Wrap a fragment page in the devkit layout. */
165
+ export function renderLayout(fragmentHtml, { config, pagePath }) {
166
+ const { pageMeta, htmlWithoutMeta } = getPageMeta(fragmentHtml);
167
+ const layout = pageMeta.layout || DEFAULT_LAYOUT;
168
+ if (!LAYOUTS.includes(layout)) throw new Error(`kmwc: ${pagePath}: unknown kmwc:layout "${layout}" (${LAYOUTS.join(' | ')})`);
169
+ if (layout === 'page' && !pageMeta.title) throw new Error(`kmwc: ${pagePath}: a fragment page needs <meta name="kmwc:title" content="…">`);
170
+
171
+ const headElements = [];
172
+ const bodyContent = mapOutsideComments(
173
+ htmlWithoutMeta,
174
+ (text) =>
175
+ text.replace(HEAD_ELEMENT_PATTERN, (element) => {
176
+ headElements.push(` ${element.trim()}`);
177
+ return '';
178
+ }),
179
+ COMMENT_OR_SCRIPT_PATTERN
180
+ );
181
+
182
+ const entry = pageMeta.entry ?? `/${config.library.entry.replace(/^\.?\//, '')}`;
183
+ const pageTitle = pageMeta.title || config.title;
184
+ const bodyHtml =
185
+ layout === 'bare'
186
+ ? bodyContent.trim()
187
+ : [
188
+ '<div class="container">',
189
+ ' <header>',
190
+ ' <a href="index.html" class="back-link">← Back to Examples</a>',
191
+ ` <h1>${pageTitle}</h1>`,
192
+ pageMeta.subtitle ? ` <p class="subtitle">${pageMeta.subtitle}</p>` : '',
193
+ ' </header>',
194
+ bodyContent.trim(),
195
+ '</div>',
196
+ ]
197
+ .filter(Boolean)
198
+ .join('\n');
199
+
200
+ return renderTemplate(readFileSync(LAYOUT_TEMPLATE_PATH, 'utf8'), {
201
+ DOCUMENT_TITLE: layout === 'bare' ? pageTitle : `${pageTitle} — ${mapToEscapedHtml(config.title)}`,
202
+ HEAD_HTML: headElements.join('\n'),
203
+ BODY_ATTRIBUTES: pageMeta.bodyClass ? ` class="${pageMeta.bodyClass}"` : '',
204
+ BODY_HTML: bodyHtml,
205
+ // In <head>: module scripts run in document order, so the element is
206
+ // defined before the page's own scripts run.
207
+ ENTRY_SCRIPT_HTML: entry === 'none' ? '' : ` <script type="module" src="${entry}"></script>`,
208
+ });
209
+ }
210
+
211
+ /** The whole pipeline: includes → demos → layout (fragments only). */
212
+ export function composePage(html, { config, pagePath, readFile }) {
213
+ let composedHtml = resolveIncludes(html, { root: config.root, readFile });
214
+ composedHtml = renderDemos(composedHtml);
215
+ return checkIsFragment(composedHtml) ? renderLayout(composedHtml, { config, pagePath }) : composedHtml;
216
+ }
@@ -0,0 +1,39 @@
1
+ # syntax=docker/dockerfile:1
2
+ {{HEADER}}
3
+ #
4
+ # Examples site for {{PACKAGE_NAME}}: Vite multi-page build → static nginx.
5
+
6
+ # ─────────────────────────────────────────────────────────────────────────────
7
+ # Stage 1 — build: `npm run build:examples` (= vite build --mode examples)
8
+ # ─────────────────────────────────────────────────────────────────────────────
9
+ FROM node:{{NODE_IMAGE}} AS build
10
+ WORKDIR /app
11
+
12
+ # Latest npm first — the bundled one can lag by weeks; newer npm ships registry
13
+ # hardening that reduces supply-chain exposure. Own layer, cached per base image.
14
+ RUN npm install -g npm@latest
15
+
16
+ # NOT `npm ci`: local lockfiles may resolve sibling packages (core, devkit) to
17
+ # file: links that don't exist in this build context. A plain install
18
+ # re-resolves them from the registry. Manifests first so this layer stays
19
+ # cached until dependencies change.
20
+ {{COPY_PACKAGE_JSONS}}
21
+ RUN {{INSTALL_COMMAND}}
22
+
23
+ # Source + build. The kmwc Vite plugin bundles each example page, injects the
24
+ # shared kit, stamps the version badge (BUILD_TIME, default: now) and adds the
25
+ # analytics snippet — no post-build sed.
26
+ COPY . .
27
+ ARG BUILD_TIME
28
+ ENV BUILD_TIME=${BUILD_TIME}
29
+ RUN npm run build:examples
30
+
31
+ # ─────────────────────────────────────────────────────────────────────────────
32
+ # Stage 2 — serve
33
+ # ─────────────────────────────────────────────────────────────────────────────
34
+ FROM nginx:alpine AS serve
35
+ COPY nginx.conf /etc/nginx/conf.d/default.conf
36
+ COPY --from=build /app/{{OUT_DIR}} /usr/share/nginx/html
37
+
38
+ EXPOSE 80
39
+ CMD ["nginx", "-g", "daemon off;"]
@@ -0,0 +1,136 @@
1
+ {{HEADER}}
2
+ #
3
+ # Repo-specific targets go in Makefile.local (included at the end; a target
4
+ # redefined there wins, with a make "overriding recipe" warning). Per-developer
5
+ # overrides (container runner, image name, port) go in .makefile.env.
6
+
7
+ .PHONY: help setup dev kill-port build build-examples preview-examples package publish-dry publish publish-rc clean check-version check-vars doctor sync test test-e2e test-e2e-ui test-e2e-headed test-e2e-install image-build image-run image-stop image-clean
8
+
9
+ -include .makefile.env
10
+ DOCKER_RUNNER ?= {{RUNNER}}
11
+ IMAGE_NAME ?= {{IMAGE_NAME}}
12
+ CONTAINER_NAME ?= {{CONTAINER_NAME}}
13
+ IMAGE_PORT ?= {{IMAGE_PORT}}
14
+ DEV_PORT := {{DEV_PORT}}
15
+ PREVIEW_PORT := {{PREVIEW_PORT}}
16
+
17
+ help: ## Show this help message
18
+ @echo "Available targets:"
19
+ @grep -hE '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-18s %s\n", $$1, $$2}'
20
+
21
+ setup: ## Install dependencies
22
+ npm install
23
+
24
+ dev: ## Start the dev server (examples on http://localhost:{{DEV_PORT}})
25
+ @rm -rf node_modules/.vite
26
+ npm run dev
27
+
28
+ # Free the dev + preview ports. Vite runs with strictPort, so a stale server is
29
+ # the only thing that can hold them. On Windows a PowerShell one-liner covers
30
+ # IPv4 + IPv6 listeners; `$$_` escapes make's `$` and the single quotes stop sh
31
+ # from expanding `$_` before PowerShell sees it.
32
+ kill-port: ## Free the dev/preview ports ({{DEV_PORT}}, {{PREVIEW_PORT}})
33
+ ifeq ($(OS),Windows_NT)
34
+ -@powershell -NoProfile -Command '$(DEV_PORT),$(PREVIEW_PORT) | ForEach-Object { Get-NetTCPConnection -LocalPort $$_ -State Listen -ErrorAction SilentlyContinue } | Select-Object -ExpandProperty OwningProcess -Unique | ForEach-Object { Stop-Process -Id $$_ -Force -ErrorAction SilentlyContinue }'
35
+ else
36
+ -@for p in $(DEV_PORT) $(PREVIEW_PORT); do lsof -ti tcp:$$p | xargs -r kill -9; done
37
+ endif
38
+ @echo "Ports $(DEV_PORT), $(PREVIEW_PORT) are free"
39
+
40
+ build: ## Build the library (dist/)
41
+ npm run build
42
+
43
+ build-examples: ## Build the static examples site ({{OUT_DIR}}/)
44
+ npm run build:examples
45
+
46
+ preview-examples: build-examples ## Serve the built examples site (http://localhost:{{PREVIEW_PORT}})
47
+ npm run preview:examples
48
+
49
+ package: build ## Create an npm tarball
50
+ npm pack
51
+
52
+ publish-dry: ## Publish dry run (clean build first)
53
+ npm run clean:dist
54
+ npm run build
55
+ npm publish --dry-run
56
+
57
+ # Release gates (publish-command.md): the version must fit the dist-tag
58
+ # (latest = no pre-release suffix, rc = -rcN), must not be on npm yet, and the
59
+ # e2e suite must pass. `kmwc check-release` does the first two.
60
+ publish: ## Publish to npm as 'latest' (release versions only)
61
+ npx kmwc check-release --tag latest
62
+ @echo "WARNING: publishing to npm under the 'latest' dist-tag. Use 'make publish-rc' for pre-releases."
63
+ @$(CONFIRM)
64
+ npm run clean:dist
65
+ npm run build
66
+ ifneq ($(wildcard playwright.config.*),)
67
+ npm run test:e2e
68
+ endif
69
+ npm publish
70
+
71
+ publish-rc: ## Publish to npm under the 'rc' dist-tag (does NOT touch 'latest')
72
+ npx kmwc check-release --tag rc
73
+ @echo "WARNING: publishing to npm under the 'rc' dist-tag."
74
+ @$(CONFIRM)
75
+ npm run clean:dist
76
+ npm run build
77
+ ifneq ($(wildcard playwright.config.*),)
78
+ npm run test:e2e
79
+ endif
80
+ npm publish --tag rc
81
+
82
+ clean: ## Clean build artifacts
83
+ npm run clean
84
+
85
+ check-version: ## Show the current package version
86
+ @node -p "require('./package.json').version"
87
+
88
+ check-vars: ## Validate component-variables.manifest.json against the CSS
89
+ npx kmwc check-vars
90
+
91
+ doctor: ## Check this repo against the Keenmate component standard
92
+ npx kmwc doctor
93
+
94
+ sync: ## Regenerate devkit-owned files (Dockerfile, nginx.conf, Makefile, …)
95
+ npx kmwc sync
96
+
97
+ test: ## Run unit tests
98
+ npm test
99
+
100
+ test-e2e: ## Run Playwright e2e tests (headless)
101
+ npm run test:e2e
102
+
103
+ test-e2e-ui: ## Run Playwright e2e tests in UI mode
104
+ npm run test:e2e:ui
105
+
106
+ test-e2e-headed: ## Run Playwright e2e tests headed
107
+ npm run test:e2e:headed
108
+
109
+ test-e2e-install: ## Install the chromium browser binary (one-time)
110
+ npm run test:e2e:install
111
+
112
+ # ── Container image (examples site) ──────────────────────────────────────────
113
+
114
+ image-build: ## Build the examples container image
115
+ $(DOCKER_RUNNER) build --build-arg BUILD_TIME=$$(date -u +%Y-%m-%dT%H:%MZ) -t $(IMAGE_NAME) .
116
+
117
+ image-run: ## Run the examples image on IMAGE_PORT (default {{IMAGE_PORT}})
118
+ -@$(DOCKER_RUNNER) rm -f $(CONTAINER_NAME) >/dev/null 2>&1
119
+ $(DOCKER_RUNNER) run -d --name $(CONTAINER_NAME) -p $(IMAGE_PORT):80 $(IMAGE_NAME)
120
+ @echo "Serving examples at http://localhost:$(IMAGE_PORT)"
121
+
122
+ image-stop: ## Stop and remove the examples container
123
+ -@$(DOCKER_RUNNER) rm -f $(CONTAINER_NAME) >/dev/null 2>&1
124
+
125
+ image-clean: image-stop ## Remove the examples container and image
126
+ -@$(DOCKER_RUNNER) rmi $(IMAGE_NAME) >/dev/null 2>&1
127
+
128
+ ifeq ($(OS),Windows_NT)
129
+ CONFIRM = powershell -NoProfile -Command "Write-Host 'Press Enter to continue, Ctrl+C to cancel...'; Read-Host | Out-Null"
130
+ else
131
+ CONFIRM = read -p "Press Enter to continue, Ctrl+C to cancel... " _
132
+ endif
133
+
134
+ -include Makefile.local
135
+
136
+ .DEFAULT_GOAL := help
@@ -0,0 +1,40 @@
1
+ ## Shared infrastructure — `@keenmate/web-components-devkit` (read before touching examples/build files)
2
+
3
+ This repo follows the Keenmate component standard: the BlissFramework rulebook
4
+ (`C:\Git\BlissFramework\guidelines\web-components\` — structure, naming, CSS,
5
+ theming, README, CEM, publish) plus the devkit (`../web-components`, published
6
+ as `@keenmate/web-components-devkit`), which owns the example-site kit, the
7
+ Vite/Playwright configs and the deploy files. Do not re-create or patch those
8
+ locally — every local fork is how they drifted apart.
9
+
10
+ - **Never add** `examples-chapter-nav.js`, `examples-copy-link.js`,
11
+ `examples-controls-store.js`, `examples-shared.css`, `plausible-snippet.html`
12
+ or `scripts/check-variable-manifest.mjs`. The chapter nav, copy-link buttons,
13
+ shared styles, theme switch, version badge and analytics are injected into
14
+ every root `*.html` page by the Vite plugin automatically.
15
+ - **Generated — do not edit:** `Dockerfile`, `nginx.conf`, `Makefile`,
16
+ `.dockerignore` (they start with a `GENERATED by kmwc sync` header). Change
17
+ `package.json#kmwc` and run `npx kmwc sync`, or fix the template in the devkit.
18
+ Repo-specific make targets go in `Makefile.local`.
19
+ - **Per-component settings** (title, tagline, CSS prefix, ports, accent colours,
20
+ analytics id, image name, canonical origin) live **only** in `package.json#kmwc`.
21
+ - **Example page styles:** shared rules are in `@layer kmwc` and read
22
+ `--base-*` tokens (pages support light / dark / auto via the theme switch), so
23
+ any unlayered rule wins. Put component-only rules in `examples-local.css`.
24
+ Re-theme with `kmwc.theme.accentColor`, never by copying CSS. If a second
25
+ component needs a pattern, it belongs in the devkit.
26
+ - **Example pages are fragments:** no `<html>`/`<head>`/`<header>` — start with
27
+ `<meta name="kmwc:title" content="…">`, the devkit layout adds the rest. Shared text
28
+ goes in `partials/` via `<kmwc-include>`; show code with `<kmwc-demo>` (the live
29
+ markup IS the snippet) — never hand-copy a demo into a `.code-block`.
30
+ - **Controls persistence:**
31
+ `import { attachControlsPersistence } from '@keenmate/web-components-devkit/examples'`.
32
+ **Landing page:** `renderExamplesIndex({ pages })` from the same module.
33
+ - **Found a bug in a shared helper?** Fix it in `../web-components` (so every
34
+ component gets it), not here. Say so in your summary if you can't.
35
+ - **Never ship AI-assistant files:** `package.json` `files` must keep
36
+ `"!**/CLAUDE.md"` and `"!**/.claude"` (a `CLAUDE.md` you add under `src/` is
37
+ otherwise published with it).
38
+ - Before finishing: `npx kmwc doctor` must pass — it runs the devkit checks,
39
+ checks the `npm pack` file list for dev-only files, and runs the rulebook's
40
+ `*.checks.sh` suites.
@@ -0,0 +1,10 @@
1
+ {{HEADER}}
2
+ node_modules
3
+ **/node_modules
4
+ dist
5
+ {{OUT_DIR}}
6
+ test-results
7
+ playwright-report
8
+ .git
9
+ *.tgz
10
+ .makefile.env
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{DOCUMENT_TITLE}}</title>
7
+ {{ENTRY_SCRIPT_HTML}}
8
+ {{HEAD_HTML}}
9
+ </head>
10
+ <body{{BODY_ATTRIBUTES}}>
11
+ {{BODY_HTML}}
12
+ </body>
13
+ </html>