@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.
Files changed (109) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/LICENSE +21 -0
  3. package/README.md +417 -2
  4. package/components.json +189 -0
  5. package/package.json +75 -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 +302 -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 +72 -0
  14. package/src/cli/sync-plan.js +163 -0
  15. package/src/config.d.ts +147 -0
  16. package/src/config.js +257 -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 +110 -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 +209 -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 +40 -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 +26 -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,30 @@
1
+ /* ==============================================================================
2
+ KMWC EXAMPLES KIT — MAIN ENTRY POINT
3
+ ==============================================================================
4
+ Loaded automatically on every example page by the kmwc Vite plugin.
5
+
6
+ Everything lives in `@layer kmwc`, so ANY unlayered rule — a page's own
7
+ <style>, a repo's `examples-local.css`, a light-DOM component's styles —
8
+ wins regardless of load order or specificity. The universal reset is
9
+ layered too, so it can never clobber a component (css-structure.md "The
10
+ unlayered-reset footgun").
11
+
12
+ • colours → tokens (tokens.css) read through --base-*; per-site accent via
13
+ package.json#kmwc.theme
14
+ • a rule → write the selector unlayered in examples-local.css
15
+ • missing → if two components need it, add it HERE, not locally
16
+
17
+ Page vocabulary (.card, .controls, .note, …) is authored by pages and stays
18
+ unprefixed; DOM the kit generates is prefixed BEM (kmwc-chapter-nav__item…). */
19
+ @layer kmwc;
20
+
21
+ @import './tokens.css';
22
+ @import './base.css';
23
+ @import './controls.css';
24
+ @import './content.css';
25
+ @import './widgets.css';
26
+ @import './demo.css';
27
+ @import './copy-link.css';
28
+ @import './chapter-nav.css';
29
+ @import './theme-switch.css';
30
+ @import './index-page.css';
@@ -0,0 +1,38 @@
1
+ /* ==============================================================================
2
+ EXAMPLES KIT — THEME SWITCH
3
+ ==============================================================================
4
+ Fixed light / dark / auto button (theme-switch.js), top-right corner. */
5
+ @layer kmwc {
6
+ .kmwc-theme-switch {
7
+ position: fixed;
8
+ top: 0.75rem;
9
+ right: 0.75rem;
10
+ z-index: 9000;
11
+ display: inline-flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ width: 36px;
15
+ height: 36px;
16
+ padding: 0;
17
+ border-radius: 50%;
18
+ border: 1px solid var(--kmwc-border-color);
19
+ background: var(--kmwc-main-bg);
20
+ color: var(--kmwc-text-color-1);
21
+ box-shadow: 0 2px 6px var(--kmwc-shadow-color);
22
+ cursor: pointer;
23
+ }
24
+
25
+ .kmwc-theme-switch:hover {
26
+ background: var(--kmwc-hover-bg);
27
+ }
28
+
29
+ .kmwc-theme-switch:focus-visible {
30
+ outline: 2px solid var(--kmwc-accent-color);
31
+ outline-offset: 2px;
32
+ }
33
+
34
+ .kmwc-theme-switch__icon {
35
+ font-size: 1.1rem;
36
+ line-height: 1;
37
+ }
38
+ }
@@ -0,0 +1,110 @@
1
+ /* ==============================================================================
2
+ EXAMPLES KIT — TOKENS
3
+ ==============================================================================
4
+ The page-chrome counterpart of a component's variables.css. Same two-layer
5
+ model as the rulebook (base-variables.md): every --kmwc-* token reads through
6
+ the cross-component --base-* hook with a light-dark() fallback, so
7
+
8
+ • a theme-designer theme (:root --base-*) restyles the demo page AND the
9
+ component together — demo pages demonstrate cross-component theming;
10
+ • with no --base-* set, the page still looks right, in light and dark.
11
+
12
+ Unlike component variables (theme-container.md: never on :root), page
13
+ tokens DO live on :root — the page is the scope here. Feature files read
14
+ only --kmwc-* tokens; colour literals live in this file alone.
15
+
16
+ Re-theme a component's example site via package.json#kmwc.theme (injected
17
+ as --kmwc-accent-color / --kmwc-accent-end-color) — never by copying CSS. */
18
+ @layer kmwc {
19
+ /* ==========================================================================
20
+ COLOR SCHEME — driven by <html data-theme> (theme switch)
21
+ ==========================================================================
22
+ light (default) / dark force the scheme; auto follows the OS. Components
23
+ on the page inherit color-scheme and see the same data-theme signal via
24
+ :host-context([data-theme="…"]). */
25
+ :root {
26
+ color-scheme: light;
27
+ }
28
+
29
+ :root[data-theme="dark"] {
30
+ color-scheme: dark;
31
+ }
32
+
33
+ :root[data-theme="auto"] {
34
+ color-scheme: light dark;
35
+ }
36
+
37
+ :root {
38
+ /* ======================================================================
39
+ COLORS — ACCENT
40
+ ====================================================================== */
41
+ --kmwc-accent-color: var(--base-accent-color, #667eea);
42
+ --kmwc-accent-end-color: #764ba2;
43
+ --kmwc-accent-color-hover: var(--base-accent-color-hover, color-mix(in srgb, var(--kmwc-accent-color) 88%, black));
44
+ --kmwc-accent-color-active: var(--base-accent-color-active, color-mix(in srgb, var(--kmwc-accent-color) 72%, black));
45
+ --kmwc-accent-light-bg: var(--base-accent-color-light, color-mix(in srgb, var(--kmwc-accent-color) 12%, transparent));
46
+ --kmwc-accent-light-bg-hover: var(--base-accent-color-light-hover, color-mix(in srgb, var(--kmwc-accent-color) 20%, transparent));
47
+ --kmwc-accent-border-color: color-mix(in srgb, var(--kmwc-accent-color) 40%, transparent);
48
+ --kmwc-accent-shadow-color: color-mix(in srgb, var(--kmwc-accent-color) 35%, transparent);
49
+ --kmwc-text-color-on-accent: var(--base-text-color-on-accent, #ffffff);
50
+ --kmwc-header-bg: linear-gradient(135deg, var(--kmwc-accent-color) 0%, var(--kmwc-accent-end-color) 100%);
51
+
52
+ /* ======================================================================
53
+ COLORS — TEXT
54
+ ====================================================================== */
55
+ --kmwc-text-color-1: var(--base-text-color-1, light-dark(#2d3748, #f5f5f5));
56
+ --kmwc-text-color-2: var(--base-text-color-2, light-dark(#4a5568, #d4d4d4));
57
+ --kmwc-text-color-3: var(--base-text-color-3, light-dark(#718096, #a3a3a3));
58
+ --kmwc-text-color-4: var(--base-text-color-4, light-dark(#9ca3af, #737373));
59
+
60
+ /* ======================================================================
61
+ COLORS — SURFACE
62
+ ====================================================================== */
63
+ --kmwc-page-bg: light-dark(#f7fafc, #111317);
64
+ --kmwc-main-bg: var(--base-main-bg, light-dark(#ffffff, #1a1a1a));
65
+ --kmwc-elevated-bg: var(--base-elevated-bg, light-dark(#edf2f7, #2b2b2b));
66
+ --kmwc-hover-bg: var(--base-hover-bg, color-mix(in srgb, var(--kmwc-text-color-1) 6%, var(--kmwc-main-bg)));
67
+ --kmwc-border-color: var(--base-border-color, light-dark(#e2e8f0, #3d3d3d));
68
+ --kmwc-border-color-strong: light-dark(#cbd5e0, #525252);
69
+ --kmwc-shadow-color: light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.45));
70
+
71
+ /* ======================================================================
72
+ COLORS — INPUTS & CODE
73
+ ====================================================================== */
74
+ --kmwc-input-bg: var(--base-input-bg, light-dark(#ffffff, #1f1f1f));
75
+ --kmwc-input-color: var(--base-input-color, var(--kmwc-text-color-1));
76
+ --kmwc-input-border: var(--base-input-border, 1px solid light-dark(#d1d5db, #5a5a5a));
77
+ --kmwc-code-bg: light-dark(#2d3748, #0d1117);
78
+ --kmwc-code-text-color: #e2e8f0;
79
+ --kmwc-code-comment-color: #8b949e;
80
+ --kmwc-code-keyword-color: #c678dd;
81
+ --kmwc-code-string-color: #98c379;
82
+ --kmwc-code-function-color: #61afef;
83
+ --kmwc-code-property-color: #d19a66;
84
+ --kmwc-console-bg: light-dark(#0f172a, #05070d);
85
+ --kmwc-console-text-color: #e2e8f0;
86
+ --kmwc-console-muted-color: #94a3b8;
87
+ --kmwc-console-highlight-color: #68d391;
88
+ --kmwc-inline-code-bg: light-dark(#edf2f7, #2b2b2b);
89
+
90
+ /* ======================================================================
91
+ COLORS — SEMANTIC
92
+ ====================================================================== */
93
+ --kmwc-success-color: light-dark(#2f855a, #68d391);
94
+ --kmwc-warning-color: light-dark(#b45309, #fbbf24);
95
+ --kmwc-danger-color: var(--base-danger-color, light-dark(#dc2626, #f87c86));
96
+ --kmwc-info-color: light-dark(#2563eb, #60a5fa);
97
+ --kmwc-neutral-color: light-dark(#6b7280, #9ca3af);
98
+ --kmwc-badge-new-bg: light-dark(#e53e3e, #f56565);
99
+
100
+ /* ======================================================================
101
+ TYPOGRAPHY & SHAPE
102
+ ====================================================================== */
103
+ --kmwc-font-family: var(--base-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif);
104
+ --kmwc-mono-font-family: ui-monospace, SFMono-Regular, Menlo, 'Courier New', monospace;
105
+ --kmwc-radius-lg: 12px;
106
+ --kmwc-radius-md: 8px;
107
+ --kmwc-radius-sm: 6px;
108
+ --kmwc-max-width: 1400px;
109
+ }
110
+ }
@@ -0,0 +1,133 @@
1
+ /* ==============================================================================
2
+ EXAMPLES KIT — WIDGETS
3
+ ==============================================================================
4
+ Opt-in helpers more than one component's examples use. Add here (not to a
5
+ repo-local stylesheet) once a second component needs a pattern. */
6
+ @layer kmwc {
7
+ /* ==========================================================================
8
+ LOG PANELS
9
+ ==========================================================================
10
+ `.log-panel` — monospace, scrollable box for streaming output; light by
11
+ default, `.log-panel--dark` for the console look; height cap via
12
+ `--log-max-height`. `.event-log` is the legacy dark console with
13
+ `.event-*` entries — prefer `.log-panel` for new pages. */
14
+ .log-panel {
15
+ font-family: var(--kmwc-mono-font-family);
16
+ font-size: 0.85rem;
17
+ line-height: 1.5;
18
+ padding: 1rem 1.25rem;
19
+ border-radius: var(--kmwc-radius-md);
20
+ max-height: var(--log-max-height, 220px);
21
+ overflow: auto;
22
+ background: var(--kmwc-page-bg);
23
+ color: var(--kmwc-text-color-1);
24
+ border: 1px solid var(--kmwc-border-color);
25
+ }
26
+
27
+ .log-panel--dark {
28
+ background: var(--kmwc-console-bg);
29
+ color: var(--kmwc-console-text-color);
30
+ border-color: transparent;
31
+ }
32
+
33
+ .event-log {
34
+ background: var(--kmwc-console-bg);
35
+ color: var(--kmwc-console-muted-color);
36
+ padding: 1rem;
37
+ border-radius: var(--kmwc-radius-md);
38
+ font-family: var(--kmwc-mono-font-family);
39
+ font-size: 0.8rem;
40
+ max-height: 200px;
41
+ overflow-y: auto;
42
+ line-height: 1.5;
43
+ }
44
+
45
+ .event-log .event-entry {
46
+ padding: 0.15rem 0;
47
+ border-bottom: 1px solid color-mix(in srgb, var(--kmwc-console-text-color) 6%, transparent);
48
+ }
49
+
50
+ .event-log .event-time { color: var(--kmwc-console-muted-color); }
51
+ .event-log .event-name { color: var(--kmwc-console-highlight-color); font-weight: 600; }
52
+
53
+ /* ==========================================================================
54
+ CSS-VARIABLE DEMO HELPERS
55
+ ==========================================================================
56
+ `.display-sliders` — (name, range, value) rows for live CSS-var demos;
57
+ `.css-readout` — chips showing measured values (`.is-gap` accents a
58
+ derived figure). */
59
+ .display-sliders {
60
+ display: grid;
61
+ grid-template-columns: auto 1fr auto;
62
+ align-items: center;
63
+ gap: 0.4rem 0.75rem;
64
+ margin: 0.25rem 0 0.75rem;
65
+ max-width: 460px;
66
+ }
67
+
68
+ .display-sliders label {
69
+ font-family: var(--kmwc-mono-font-family);
70
+ font-size: 0.78rem;
71
+ color: var(--kmwc-text-color-2);
72
+ }
73
+
74
+ .display-sliders input[type="range"] {
75
+ width: 100%;
76
+ accent-color: var(--kmwc-accent-color);
77
+ }
78
+
79
+ .display-sliders output {
80
+ font-family: var(--kmwc-mono-font-family);
81
+ font-size: 0.78rem;
82
+ font-weight: 700;
83
+ color: var(--kmwc-info-color);
84
+ min-width: 3.2em;
85
+ text-align: right;
86
+ }
87
+
88
+ .css-readout {
89
+ display: flex;
90
+ flex-wrap: wrap;
91
+ gap: 0.4rem;
92
+ margin: 0.25rem 0 1rem;
93
+ }
94
+
95
+ .css-readout code { font-size: 0.78rem; }
96
+ .css-readout code b { color: var(--kmwc-info-color); font-weight: 700; }
97
+ .css-readout code.is-gap b { color: var(--kmwc-warning-color); }
98
+
99
+ /* ==========================================================================
100
+ STATUS DOTS & PILLS
101
+ ========================================================================== */
102
+ .status-dot {
103
+ --kmwc-status-color: var(--kmwc-neutral-color);
104
+ display: inline-block;
105
+ width: 8px;
106
+ height: 8px;
107
+ border-radius: 50%;
108
+ background: var(--kmwc-status-color);
109
+ margin-right: 0.4rem;
110
+ vertical-align: middle;
111
+ }
112
+
113
+ .status-dot--ok { --kmwc-status-color: var(--kmwc-success-color); }
114
+ .status-dot--warn { --kmwc-status-color: var(--kmwc-warning-color); }
115
+ .status-dot--err { --kmwc-status-color: var(--kmwc-danger-color); }
116
+
117
+ .pill {
118
+ --kmwc-status-color: var(--kmwc-neutral-color);
119
+ display: inline-flex;
120
+ align-items: center;
121
+ padding: 0.15rem 0.55rem;
122
+ border-radius: 999px;
123
+ font-size: 0.75rem;
124
+ font-weight: 600;
125
+ font-family: var(--kmwc-mono-font-family);
126
+ background: color-mix(in srgb, var(--kmwc-status-color) 16%, transparent);
127
+ color: var(--kmwc-status-color);
128
+ }
129
+
130
+ .pill--ok { --kmwc-status-color: var(--kmwc-success-color); }
131
+ .pill--warn { --kmwc-status-color: var(--kmwc-warning-color); }
132
+ .pill--err { --kmwc-status-color: var(--kmwc-danger-color); }
133
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Copy buttons for the code blocks <kmwc-demo> generates. The code itself is
3
+ * rendered at build time (page-composer.js) from the demo's live markup, so it
4
+ * can't drift; this only adds the "Copy" affordance. Styles: css/demo.css.
5
+ */
6
+ import { copyText } from './copy-link.js';
7
+ import { COPY_FEEDBACK_DURATION_MS, DEMO_CLASSES } from './constants.js';
8
+
9
+ export function attachDemoCopyButtons(root = document) {
10
+ for (const codeBlock of root.querySelectorAll(`.${DEMO_CLASSES.code}`)) {
11
+ if (codeBlock.querySelector(`.${DEMO_CLASSES.copyButton}`)) continue;
12
+ const button = document.createElement('button');
13
+ button.type = 'button';
14
+ button.className = DEMO_CLASSES.copyButton;
15
+ button.textContent = 'Copy';
16
+ let revertTimer = null;
17
+ button.addEventListener('click', () => {
18
+ copyText(codeBlock.querySelector('code')?.textContent ?? '').then((isCopied) => {
19
+ button.textContent = isCopied ? 'Copied!' : 'Ctrl/Cmd+C';
20
+ button.classList.toggle(DEMO_CLASSES.copyButtonCopied, isCopied);
21
+ clearTimeout(revertTimer);
22
+ revertTimer = setTimeout(() => {
23
+ button.textContent = 'Copy';
24
+ button.classList.remove(DEMO_CLASSES.copyButtonCopied);
25
+ }, COPY_FEEDBACK_DURATION_MS);
26
+ });
27
+ });
28
+ codeBlock.appendChild(button);
29
+ }
30
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The examples landing page, rendered from a page registry. A component's
3
+ * index.html shrinks to:
4
+ *
5
+ * <body>
6
+ * <script type="module">
7
+ * import { renderExamplesIndex } from '@keenmate/web-components-devkit/examples';
8
+ * renderExamplesIndex({
9
+ * pages: [
10
+ * { href: 'examples-basic.html', icon: '🧩', title: 'Basic Usage', description: '…' },
11
+ * ],
12
+ * });
13
+ * </script>
14
+ * </body>
15
+ *
16
+ * Title, tagline, version and the GitHub link come from package.json#kmwc via
17
+ * the injected page config. The registry also feeds the cross-page chapter nav.
18
+ */
19
+ import { getPageConfig } from './page-config.js';
20
+ import { getVersionLabel } from './version-badge.js';
21
+ import { INDEX_PAGE_CLASSES } from './constants.js';
22
+
23
+ const HTML_ESCAPES = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
24
+ const mapToEscapedHtml = (text) => String(text ?? '').replace(/[&<>"']/g, (character) => HTML_ESCAPES[character]);
25
+
26
+ /**
27
+ * @param {import('./index.js').ExamplesIndexConfig} indexConfig
28
+ * @returns {HTMLElement} the rendered page wrapper
29
+ */
30
+ export function renderExamplesIndex(indexConfig) {
31
+ const pageConfig = getPageConfig();
32
+ const pages = indexConfig.pages || [];
33
+ const title = indexConfig.title ?? pageConfig.title;
34
+ const tagline = indexConfig.tagline ?? pageConfig.tagline;
35
+ const footerLinks = [
36
+ ...(pageConfig.repository ? [{ label: 'GitHub', href: pageConfig.repository }] : []),
37
+ ...(indexConfig.links || []),
38
+ ];
39
+ const classes = INDEX_PAGE_CLASSES;
40
+
41
+ document.body.classList.add(classes.body);
42
+ if (!document.title) document.title = `${title} — Examples`;
43
+
44
+ const containerEl = indexConfig.containerEl || document.body;
45
+ const pageElement = document.createElement('div');
46
+ pageElement.className = classes.page;
47
+ pageElement.innerHTML = `
48
+ <header class="${classes.header}">
49
+ <h1>${mapToEscapedHtml(title)}</h1>
50
+ ${tagline ? `<p class="${classes.tagline}">${mapToEscapedHtml(tagline)}</p>` : ''}
51
+ <span class="${classes.version}" data-kmwc-version>${mapToEscapedHtml(getVersionLabel(pageConfig))}</span>
52
+ </header>
53
+ <div class="${classes.cards}">
54
+ ${pages
55
+ .map(
56
+ (page) => `
57
+ <a href="${mapToEscapedHtml(page.href)}" class="${classes.card}">
58
+ ${page.icon ? `<div class="${classes.icon}">${mapToEscapedHtml(page.icon)}</div>` : ''}
59
+ <h2 class="${classes.cardTitle}">${mapToEscapedHtml(page.title)}${page.badge ? ` <span class="${classes.badge}">${mapToEscapedHtml(page.badge)}</span>` : ''}</h2>
60
+ ${page.description ? `<p class="${classes.cardDescription}">${mapToEscapedHtml(page.description)}</p>` : ''}
61
+ </a>`
62
+ )
63
+ .join('')}
64
+ </div>
65
+ <footer class="${classes.footer}">
66
+ <p>Created by <a href="https://github.com/keenmate" target="_blank" rel="noopener">Keenmate s.r.o.</a>${footerLinks
67
+ .map((link) => ` | <a href="${mapToEscapedHtml(link.href)}" target="_blank" rel="noopener">${mapToEscapedHtml(link.label)}</a>`)
68
+ .join('')}${
69
+ pageConfig.devkitVersion
70
+ ? ` | <a class="${classes.devkit}" href="${mapToEscapedHtml(pageConfig.devkitHomepage)}" target="_blank" rel="noopener">web-components-devkit v${mapToEscapedHtml(pageConfig.devkitVersion)}</a>`
71
+ : ''
72
+ }</p>
73
+ </footer>`;
74
+ containerEl.prepend(pageElement);
75
+
76
+ window.CHAPTER_NAV_ITEMS = pages.map((page) => ({ label: page.title, href: page.href }));
77
+ return pageElement;
78
+ }
@@ -0,0 +1,107 @@
1
+ export interface KmwcPageConfig {
2
+ id: string;
3
+ title: string;
4
+ tagline: string;
5
+ version: string;
6
+ packageName: string;
7
+ buildTimestamp: string | null;
8
+ mode: 'dev' | 'build';
9
+ canonicalOrigin: string;
10
+ storagePrefix: string;
11
+ repository: string;
12
+ homepage: string;
13
+ /** The @keenmate/web-components-devkit version that built the page (shown in the index footer). */
14
+ devkitVersion: string;
15
+ devkitHomepage: string;
16
+ }
17
+
18
+ /** One card on the examples index (plain data model). */
19
+ export interface ExamplePage {
20
+ href: string;
21
+ title: string;
22
+ description?: string;
23
+ icon?: string;
24
+ badge?: string;
25
+ }
26
+
27
+ export interface ExamplesIndexConfig {
28
+ pages: ExamplePage[];
29
+ title?: string;
30
+ tagline?: string;
31
+ links?: Array<{ label: string; href: string }>;
32
+ /** Where the page is rendered; defaults to <body>. */
33
+ containerEl?: Element;
34
+ }
35
+
36
+ export interface ChapterNavConfig {
37
+ /** Cross-page jump list (the index passes its registry). */
38
+ items?: Array<{ label: string; href: string }>;
39
+ /** "Back to Examples" target; '' hides it. */
40
+ indexHref?: string;
41
+ }
42
+
43
+ export interface Section {
44
+ heading: HTMLElement;
45
+ section: HTMLElement;
46
+ id: string;
47
+ label: string;
48
+ }
49
+
50
+ export type Theme = 'light' | 'dark' | 'auto';
51
+
52
+ export function attachControlsPersistence(pageId: string, root?: ParentNode): { clear(): void };
53
+ export function getControlsStorageKey(pageId: string, storagePrefix?: string): string;
54
+
55
+ export function renderExamplesIndex(indexConfig: ExamplesIndexConfig): HTMLElement;
56
+
57
+ /** Re-mount copy-links + chapter nav + version badges + theme switch after rendering sections late. */
58
+ export function mountExamplesKit(): void;
59
+ export function mountChapterNav(navConfig?: ChapterNavConfig): { unmount(): void } | null;
60
+
61
+ export function attachCopyLinks(): void;
62
+ export function copyText(text: string): Promise<boolean>;
63
+ export function getSectionUrl(sectionId: string, canonicalOrigin?: string): string;
64
+
65
+ export function renderVersionBadges(root?: ParentNode): void;
66
+ export function attachDemoCopyButtons(root?: ParentNode): void;
67
+ export function getVersionLabel(pageConfig?: KmwcPageConfig): string;
68
+
69
+ export function mountThemeSwitch(): { unmount(): void };
70
+ export function applyTheme(theme: Theme): void;
71
+ export function getSavedTheme(): Theme;
72
+
73
+ export function ensureSectionAnchors(root?: Document): Section[];
74
+ export function generateSlug(text: string): string;
75
+ export function getHeadingLabel(heading: Element, fallbackLabel: string): string;
76
+
77
+ export function getPageConfig(): KmwcPageConfig;
78
+ export function attachDomReadyHandler(handler: () => void): void;
79
+
80
+ export const CHAPTER_NAV_CLASSES: Record<string, string>;
81
+ export const COPY_LINK_CLASSES: Record<string, string>;
82
+ export const INDEX_PAGE_CLASSES: Record<string, string>;
83
+ export const THEME_SWITCH_CLASSES: Record<string, string>;
84
+ export const DEMO_CLASSES: Record<string, string>;
85
+ export const THEMES: Theme[];
86
+ export const DEFAULT_THEME: Theme;
87
+ /** id of the chapter-nav panel (the toggle's `aria-controls`). */
88
+ export const CHAPTER_NAV_PANEL_ID: string;
89
+ /** Where sections live, and what counts as one (one `<h2>` each = one chapter). */
90
+ export const SECTION_CONTAINER_SELECTOR: string;
91
+ export const SECTION_SELECTOR: string;
92
+ /** Heading children left out of a chapter label (badges, the copy-link button). */
93
+ export const HEADING_CHIP_SELECTOR: string;
94
+ export const VERSION_BADGE_SELECTOR: string;
95
+ export const DEFAULT_INDEX_HREF: string;
96
+ /** `data-<THEME_ATTRIBUTE>` on `<html>`, and the localStorage key of the saved choice. */
97
+ export const THEME_ATTRIBUTE: string;
98
+ export const THEME_STORAGE_KEY: string;
99
+ export const ACTIVE_CHAPTER_OFFSET_PX: number;
100
+ export const COPY_FEEDBACK_DURATION_MS: number;
101
+
102
+ declare global {
103
+ interface Window {
104
+ __KMWC__?: Partial<KmwcPageConfig>;
105
+ CHAPTER_NAV_ITEMS?: Array<{ label: string; href: string }>;
106
+ }
107
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `@keenmate/web-components-devkit/examples` — named exports for example pages.
3
+ * The page-level helpers (styles, chapter nav, copy-links, version badge,
4
+ * theme switch) are mounted automatically by the Vite plugin; import from here
5
+ * only for the opt-in bits (controls persistence, index page) or to drive a
6
+ * helper manually.
7
+ */
8
+ export { attachControlsPersistence, getControlsStorageKey } from './controls-persistence.js';
9
+ export { renderExamplesIndex } from './index-page.js';
10
+ export { mountExamplesKit } from './kit.js';
11
+ export { mountChapterNav } from './chapter-nav.js';
12
+ export { attachCopyLinks, copyText, getSectionUrl } from './copy-link.js';
13
+ export { renderVersionBadges, getVersionLabel } from './version-badge.js';
14
+ export { attachDemoCopyButtons } from './demo-code.js';
15
+ export { mountThemeSwitch, applyTheme, getSavedTheme } from './theme-switch.js';
16
+ export { ensureSectionAnchors, generateSlug, getHeadingLabel } from './sections.js';
17
+ export { getPageConfig, attachDomReadyHandler } from './page-config.js';
18
+ export * from './constants.js';
@@ -0,0 +1,22 @@
1
+ import { mountChapterNav } from './chapter-nav.js';
2
+ import { attachCopyLinks } from './copy-link.js';
3
+ import { renderVersionBadges } from './version-badge.js';
4
+ import { mountThemeSwitch } from './theme-switch.js';
5
+ import { attachDemoCopyButtons } from './demo-code.js';
6
+ import { INDEX_PAGE_CLASSES } from './constants.js';
7
+
8
+ /**
9
+ * (Re)mount the page-level helpers on the current DOM. Runs automatically once
10
+ * per page; call it again after rendering sections asynchronously. Idempotent:
11
+ * copy-link / demo copy buttons skip elements that already have one, the nav
12
+ * and the theme switch are remounted.
13
+ */
14
+ export function mountExamplesKit() {
15
+ // Copy-links before the nav: the nav strips the buttons back out when
16
+ // reading labels, and this order avoids a relayout after the nav is built.
17
+ if (!document.body.classList.contains(INDEX_PAGE_CLASSES.body)) attachCopyLinks();
18
+ mountChapterNav();
19
+ renderVersionBadges();
20
+ attachDemoCopyButtons();
21
+ mountThemeSwitch();
22
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Page-level config. The Vite plugin injects `window.__KMWC__` into every
3
+ * example page's <head> (id, title, version, build timestamp, canonical
4
+ * origin, …). Reading it through here gives sane defaults when a page is
5
+ * opened outside the plugin (e.g. a fixture page or a plain static copy).
6
+ */
7
+ export function getPageConfig() {
8
+ const injectedConfig = (typeof window !== 'undefined' && window.__KMWC__) || {};
9
+ return {
10
+ id: injectedConfig.id || 'component',
11
+ title: injectedConfig.title || document.title || '',
12
+ tagline: injectedConfig.tagline || '',
13
+ version: injectedConfig.version || '',
14
+ packageName: injectedConfig.packageName || '',
15
+ buildTimestamp: injectedConfig.buildTimestamp || null,
16
+ mode: injectedConfig.mode || 'dev',
17
+ canonicalOrigin: injectedConfig.canonicalOrigin || '',
18
+ storagePrefix: injectedConfig.storagePrefix || injectedConfig.id || 'component',
19
+ repository: injectedConfig.repository || '',
20
+ homepage: injectedConfig.homepage || '',
21
+ devkitVersion: injectedConfig.devkitVersion || '',
22
+ devkitHomepage: injectedConfig.devkitHomepage || '',
23
+ };
24
+ }
25
+
26
+ /** Run `handler` once the DOM is parsed (immediately if it already is). */
27
+ export function attachDomReadyHandler(handler) {
28
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', handler, { once: true });
29
+ else handler();
30
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Section discovery shared by the chapter nav and the copy-link buttons — ONE
3
+ * slug algorithm, so the "on this page" jump list and the copied deep links
4
+ * always agree on every hash.
5
+ *
6
+ * Sections = every <h2> inside `.container` (falling back to <body>). The
7
+ * anchor id lives on the section wrapper (`.card` / `.example-section`),
8
+ * reusing an existing section/h2 id before slugging the heading text.
9
+ * Idempotent: a second call returns the same ids.
10
+ */
11
+ import { HEADING_CHIP_SELECTOR, SECTION_CONTAINER_SELECTOR, SECTION_SELECTOR } from './constants.js';
12
+
13
+ export function generateSlug(text) {
14
+ return String(text)
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9]+/g, '-')
17
+ .replace(/^-+|-+$/g, '');
18
+ }
19
+
20
+ export function getHeadingLabel(heading, fallbackLabel) {
21
+ const headingClone = heading.cloneNode(true);
22
+ headingClone.querySelectorAll(HEADING_CHIP_SELECTOR).forEach((chip) => chip.remove());
23
+ return (headingClone.textContent || '').replace(/\s+/g, ' ').trim() || fallbackLabel;
24
+ }
25
+
26
+ /**
27
+ * Make sure every section has an anchor id, and return the sections in order.
28
+ * @param {Document} [root]
29
+ * @returns {Array<{ heading: HTMLElement, section: HTMLElement, id: string, label: string }>}
30
+ */
31
+ export function ensureSectionAnchors(root = document) {
32
+ const container = root.querySelector(SECTION_CONTAINER_SELECTOR) || root.body || root;
33
+ const headings = [...container.querySelectorAll('h2')];
34
+ const usedIds = new Set();
35
+
36
+ return headings.map((heading, headingIndex) => {
37
+ const section = heading.closest(SECTION_SELECTOR) || heading.parentElement || heading;
38
+ const label = getHeadingLabel(heading, `Section ${headingIndex + 1}`);
39
+
40
+ let id = section.id || heading.id;
41
+ if (!id) {
42
+ id = generateSlug(label) || `section-${headingIndex + 1}`;
43
+ if (usedIds.has(id)) {
44
+ let suffix = 2;
45
+ while (usedIds.has(`${id}-${suffix}`)) suffix++;
46
+ id = `${id}-${suffix}`;
47
+ }
48
+ section.id = id;
49
+ }
50
+ usedIds.add(id);
51
+ return { heading, section, id, label };
52
+ });
53
+ }