@keenmate/web-components-devkit 1.0.0-rc01 → 1.0.0-rc03

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 (41) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +70 -12
  3. package/components.json +206 -189
  4. package/package.json +4 -1
  5. package/src/cli/commands/doctor.js +1 -0
  6. package/src/cli/commands/new.js +29 -8
  7. package/src/cli/kmwc.js +1 -0
  8. package/src/config.d.ts +5 -0
  9. package/src/config.js +26 -3
  10. package/src/examples/chapter-links.js +35 -0
  11. package/src/examples/chapter-search.js +84 -0
  12. package/src/examples/constants.js +51 -0
  13. package/src/examples/css/content.css +4 -34
  14. package/src/examples/css/controls.css +30 -25
  15. package/src/examples/css/copy-link.css +1 -0
  16. package/src/examples/css/demo.css +2 -0
  17. package/src/examples/css/form-controls.css +285 -0
  18. package/src/examples/css/index-page.css +1 -1
  19. package/src/examples/css/spotlight.css +417 -0
  20. package/src/examples/css/style.css +3 -0
  21. package/src/examples/css/tables.css +141 -0
  22. package/src/examples/css/tokens.css +69 -27
  23. package/src/examples/index.d.ts +38 -0
  24. package/src/examples/index.js +6 -3
  25. package/src/examples/kit.js +11 -4
  26. package/src/examples/page-config.js +1 -0
  27. package/src/examples/sections.js +38 -1
  28. package/src/examples/spotlight.js +473 -0
  29. package/src/vite/chapter-index.js +107 -0
  30. package/src/vite/index.js +19 -1
  31. package/src/vite/page-composer.js +17 -10
  32. package/templates/claude-block.md +9 -2
  33. package/templates/new/component-variables.manifest.json +3 -3
  34. package/templates/new/docs/examples.md +1 -1
  35. package/templates/new/docs/theming.md +1 -1
  36. package/templates/new/e2e/dark-mode.spec.ts +3 -3
  37. package/templates/new/examples-theming.html +3 -3
  38. package/templates/new/src/css/base.css +5 -3
  39. package/templates/new/src/css/variables.css +10 -8
  40. package/templates/new-minimal/src/css/base.css +5 -3
  41. package/templates/new-minimal/src/css/variables.css +7 -5
@@ -9,6 +9,11 @@
9
9
  component together — demo pages demonstrate cross-component theming;
10
10
  • with no --base-* set, the page still looks right, in light and dark.
11
11
 
12
+ Fallbacks are the contract's own (@keenmate/base-css-variables): an input
13
+ falls back to its default, a derived token to the contract formula on the
14
+ --kmwc-* tokens, so a page looks the same with and without the contract.
15
+ `npm run check:contract` (base-contract-check) enforces it.
16
+
12
17
  Unlike component variables (theme-container.md: never on :root), page
13
18
  tokens DO live on :root — the page is the scope here. Feature files read
14
19
  only --kmwc-* tokens; colour literals live in this file alone.
@@ -38,42 +43,65 @@
38
43
  /* ======================================================================
39
44
  COLORS — ACCENT
40
45
  ====================================================================== */
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));
46
+ --kmwc-accent-color: var(--base-primary-color, #3b82f6);
47
+ --kmwc-accent-end-color: oklch(from var(--kmwc-accent-color) calc(l - 0.12) c calc(h + 25)); /* header gradient end */
48
+ --kmwc-accent-color-hover: var(--base-primary-color-hover, light-dark(color-mix(in srgb, black 15%, var(--kmwc-accent-color)), color-mix(in srgb, white 15%, var(--kmwc-accent-color))));
49
+ --kmwc-accent-color-active: var(--base-primary-color-active, light-dark(color-mix(in srgb, black 30%, var(--kmwc-accent-color)), color-mix(in srgb, white 30%, var(--kmwc-accent-color))));
50
+ --kmwc-accent-light-bg: var(--base-primary-bg-subtle, light-dark(color-mix(in srgb, var(--kmwc-accent-color) 10%, transparent), color-mix(in srgb, var(--kmwc-accent-color) 20%, transparent)));
51
+ --kmwc-accent-light-bg-hover: var(--base-primary-bg-subtle-hover, light-dark(color-mix(in srgb, var(--kmwc-accent-color) 15%, transparent), color-mix(in srgb, var(--kmwc-accent-color) 30%, transparent)));
47
52
  --kmwc-accent-border-color: color-mix(in srgb, var(--kmwc-accent-color) 40%, transparent);
48
53
  --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);
54
+ --kmwc-text-color-on-accent: var(--base-on-primary-text-color, color(from color(from var(--kmwc-accent-color) srgb calc(0.21 * r * r * (0.4 + r * (0.82 - 0.22 * r)) + 0.72 * g * g * (0.4 + g * (0.82 - 0.22 * g)) + 0.07 * b * b * (0.4 + b * (0.82 - 0.22 * b))) 0 0) srgb clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1)));
50
55
  --kmwc-header-bg: linear-gradient(135deg, var(--kmwc-accent-color) 0%, var(--kmwc-accent-end-color) 100%);
51
56
 
52
57
  /* ======================================================================
53
58
  COLORS — TEXT
54
59
  ====================================================================== */
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));
60
+ --kmwc-text-color-1: var(--base-text-color-1, light-dark(#334155, #f1f5f9));
61
+ --kmwc-text-color-2: var(--base-text-color-2, color-mix(in srgb, var(--kmwc-text-color-1) 75%, var(--kmwc-main-bg)));
62
+ --kmwc-text-color-3: var(--base-text-color-3, color-mix(in srgb, var(--kmwc-text-color-1) 55%, var(--kmwc-main-bg)));
63
+ --kmwc-text-color-4: var(--base-text-color-4, color-mix(in srgb, var(--kmwc-text-color-1) 40%, var(--kmwc-main-bg)));
59
64
 
60
65
  /* ======================================================================
61
66
  COLORS — SURFACE
62
67
  ====================================================================== */
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));
68
+ --kmwc-page-bg: var(--base-page-bg, light-dark(#f8f9fa, #0f172a));
69
+ --kmwc-main-bg: var(--base-main-bg, light-dark(#ffffff, #1e293b));
70
+ --kmwc-elevated-bg: var(--base-elevated-bg, light-dark(oklch(from var(--kmwc-main-bg) calc(l - 0.03) c h), oklch(from var(--kmwc-main-bg) calc(l + 0.06) c h)));
71
+ --kmwc-hover-bg: var(--base-hover-bg, light-dark(color-mix(in oklch, oklch(from var(--kmwc-main-bg) calc(l - 0.45) c h) 14%, transparent), color-mix(in oklch, oklch(from var(--kmwc-main-bg) calc(l + 0.45) c h) 28%, transparent)));
72
+ --kmwc-border-color: var(--base-border-color, light-dark(oklch(from var(--kmwc-main-bg) calc(l - 0.096) c h), oklch(from var(--kmwc-main-bg) calc(l + 0.192) c h)));
73
+ --kmwc-border-color-strong: var(--base-input-border-color, light-dark(color-mix(in srgb, var(--kmwc-text-color-1) 45%, var(--kmwc-main-bg)), color-mix(in srgb, var(--kmwc-text-color-1) 35%, var(--kmwc-main-bg)))); /* control outline */
74
+ --kmwc-shadow-color: var(--base-shadow-color, light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.4)));
75
+ --kmwc-table-stripe-bg: color-mix(in srgb, var(--kmwc-elevated-bg) 55%, transparent); /* zebra rows */
76
+ --kmwc-overlay-bg: var(--base-overlay-bg, rgba(30, 41, 59, 0.5)); /* behind a modal (spotlight) */
77
+
78
+ /* ======================================================================
79
+ FOCUSED ITEM — the keyboard cursor in a list (spotlight rows); the
80
+ shared state every Keenmate list uses (wcp --cp-item-active-*)
81
+ ====================================================================== */
82
+ --kmwc-outline-width: var(--base-outline-width, 1px);
83
+ --kmwc-focused-bg: var(--base-focused-bg, transparent);
84
+ --kmwc-focused-text-color: var(--base-focused-text-color, currentColor);
85
+ --kmwc-focused-outline: var(--base-focused-outline, var(--kmwc-outline-width) solid var(--kmwc-accent-color));
70
86
 
71
87
  /* ======================================================================
72
88
  COLORS — INPUTS & CODE
73
89
  ====================================================================== */
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));
90
+ --kmwc-input-bg: var(--base-input-bg, var(--kmwc-main-bg));
91
+ --kmwc-input-color: var(--base-input-text-color, var(--kmwc-text-color-1));
92
+ --kmwc-input-border: var(--base-input-border, 1px solid var(--kmwc-border-color-strong));
93
+ --kmwc-input-border-hover: var(--base-input-border-hover, 1px solid var(--kmwc-accent-color));
94
+ --kmwc-input-border-focus: var(--base-input-border-focus, 1px solid var(--kmwc-accent-color));
95
+ --kmwc-input-shadow-color-focus: var(--base-input-shadow-color-focus, color-mix(in srgb, var(--kmwc-accent-color) 5%, transparent));
96
+ --kmwc-input-box-shadow-focus: var(--base-input-box-shadow-focus, 0 0 0 3px var(--kmwc-input-shadow-color-focus));
97
+ --kmwc-input-placeholder-color: var(--base-input-placeholder-color, var(--kmwc-text-color-3));
98
+ --kmwc-input-icon-color: var(--base-input-icon-color, var(--kmwc-text-color-3)); /* select caret */
99
+ --kmwc-input-disabled-bg: var(--base-input-bg-disabled, var(--base-disabled-bg, light-dark(oklch(from var(--kmwc-main-bg) calc(l - 0.024) c h), oklch(from var(--kmwc-main-bg) calc(l + 0.048) c h))));
100
+ --kmwc-disabled-opacity: var(--base-disabled-opacity, 0.65);
101
+ --kmwc-checkbox-border-color: var(--base-checkbox-border-color, var(--kmwc-border-color-strong));
102
+ --kmwc-icon-check: var(--base-icon-check, url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M20 6 9 17l-5-5%22/%3E%3C/svg%3E"));
103
+ --kmwc-icon-indeterminate: var(--base-icon-indeterminate, url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M5 12h14%22/%3E%3C/svg%3E"));
104
+ --kmwc-icon-check-size: var(--base-icon-check-size, 68%);
77
105
  --kmwc-code-bg: light-dark(#2d3748, #0d1117);
78
106
  --kmwc-code-text-color: #e2e8f0;
79
107
  --kmwc-code-comment-color: #8b949e;
@@ -90,21 +118,35 @@
90
118
  /* ======================================================================
91
119
  COLORS — SEMANTIC
92
120
  ====================================================================== */
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);
121
+ /* -color = the role as text on a surface (notes, feedback); the danger
122
+ button fills with --kmwc-danger-fill-color. */
123
+ --kmwc-success-color: var(--base-success-text-color, light-dark(#155724, #34d399));
124
+ --kmwc-warning-color: var(--base-warning-text-color, light-dark(#856404, #fbbf24));
125
+ --kmwc-danger-color: var(--base-danger-text-color, light-dark(#721c24, #f87171));
126
+ --kmwc-info-color: var(--base-info-text-color, light-dark(#0c5460, #22d3ee));
127
+ --kmwc-danger-fill-color: var(--base-danger-color, #dc2626);
128
+ --kmwc-danger-fill-color-hover: var(--base-danger-color-hover, light-dark(color-mix(in srgb, black 15%, var(--kmwc-danger-fill-color)), color-mix(in srgb, white 15%, var(--kmwc-danger-fill-color))));
129
+ --kmwc-text-color-on-danger: var(--base-on-danger-text-color, color(from color(from var(--kmwc-danger-fill-color) srgb calc(0.21 * r * r * (0.4 + r * (0.82 - 0.22 * r)) + 0.72 * g * g * (0.4 + g * (0.82 - 0.22 * g)) + 0.07 * b * b * (0.4 + b * (0.82 - 0.22 * b))) 0 0) srgb clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1)));
97
130
  --kmwc-neutral-color: light-dark(#6b7280, #9ca3af);
98
131
  --kmwc-badge-new-bg: light-dark(#e53e3e, #f56565);
132
+ /* Text on the neutral / badge fills: the contract's on-colour formula on
133
+ their own background, not on the accent (a light primary would turn it black). */
134
+ --kmwc-text-color-on-neutral: color(from color(from var(--kmwc-neutral-color) srgb calc(0.21 * r * r * (0.4 + r * (0.82 - 0.22 * r)) + 0.72 * g * g * (0.4 + g * (0.82 - 0.22 * g)) + 0.07 * b * b * (0.4 + b * (0.82 - 0.22 * b))) 0 0) srgb clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1));
135
+ --kmwc-text-color-on-badge-new: color(from color(from var(--kmwc-badge-new-bg) srgb calc(0.21 * r * r * (0.4 + r * (0.82 - 0.22 * r)) + 0.72 * g * g * (0.4 + g * (0.82 - 0.22 * g)) + 0.07 * b * b * (0.4 + b * (0.82 - 0.22 * b))) 0 0) srgb clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1));
99
136
 
100
137
  /* ======================================================================
101
138
  TYPOGRAPHY & SHAPE
102
139
  ====================================================================== */
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;
140
+ --kmwc-font-family: var(--base-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif);
141
+ --kmwc-mono-font-family: var(--base-font-family-mono, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
105
142
  --kmwc-radius-lg: 12px;
106
143
  --kmwc-radius-md: 8px;
107
144
  --kmwc-radius-sm: 6px;
145
+ /* One height for inputs, selects and buttons, so a control row lines up.
146
+ A kit size, not --base-input-size-md-height: the contract's unitless
147
+ heights assume a 10px rem, the example pages keep the browser's 16px. */
148
+ --kmwc-control-height: 2.25rem;
149
+ --kmwc-checkbox-size: calc(1rem * var(--base-checkbox-scale, 1));
108
150
  --kmwc-max-width: 1400px;
109
151
  }
110
152
  }
@@ -8,6 +8,7 @@ export interface KmwcPageConfig {
8
8
  mode: 'dev' | 'build';
9
9
  canonicalOrigin: string;
10
10
  storagePrefix: string;
11
+ isSpotlightEnabled: boolean;
11
12
  repository: string;
12
13
  homepage: string;
13
14
  /** The @keenmate/web-components-devkit version that built the page (shown in the index footer). */
@@ -40,6 +41,20 @@ export interface ChapterNavConfig {
40
41
  indexHref?: string;
41
42
  }
42
43
 
44
+ /** One entry of the spotlight's chapter index (virtual:kmwc-chapters). */
45
+ export interface Chapter {
46
+ /** Section code (`TH07`), null for an uncoded heading (`Summary`). */
47
+ code: string | null;
48
+ /** Heading text without the code. */
49
+ title: string;
50
+ /** Heading text as shown (`TH07 · Title`). */
51
+ label: string;
52
+ pageHref: string;
53
+ pageTitle: string;
54
+ /** `examples-theming.html#th07` — by code, else by the heading slug. */
55
+ href: string;
56
+ }
57
+
43
58
  export interface Section {
44
59
  heading: HTMLElement;
45
60
  section: HTMLElement;
@@ -58,6 +73,18 @@ export function renderExamplesIndex(indexConfig: ExamplesIndexConfig): HTMLEleme
58
73
  export function mountExamplesKit(): void;
59
74
  export function mountChapterNav(navConfig?: ChapterNavConfig): { unmount(): void } | null;
60
75
 
76
+ /** Ctrl/Cmd+K chapter search across every example page (mirrors web-commandpalette); null when `isSpotlightEnabled` is false. */
77
+ export function mountSpotlight(): { open(): void; close(): void; toggle(): void; unmount(): void } | null;
78
+ /** Resolve the URL hash (`#th07`, an anchor id or a heading slug) now and on every hash change. */
79
+ export function attachChapterLinks(): void;
80
+ export function scrollToChapterHash(hash?: string): boolean;
81
+ /** Chapters whose code or title contains the query; exact code (`th7` = TH07) first, then code prefix, then the rest. */
82
+ export function searchChapters<T extends Pick<Chapter, 'code' | 'title'>>(chapters: T[], query: string): T[];
83
+ /** Substring match of `needle` against any field (web-commandpalette matchesAny). */
84
+ export function checkMatchesAny(fields: readonly unknown[], needle: string): boolean;
85
+ /** Escaped `text` with the occurrences of `needle` in <mark> (web-commandpalette highlight). */
86
+ export function renderHighlightedText(text: string, needle: string): string;
87
+
61
88
  export function attachCopyLinks(): void;
62
89
  export function copyText(text: string): Promise<boolean>;
63
90
  export function getSectionUrl(sectionId: string, canonicalOrigin?: string): string;
@@ -73,11 +100,22 @@ export function getSavedTheme(): Theme;
73
100
  export function ensureSectionAnchors(root?: Document): Section[];
74
101
  export function generateSlug(text: string): string;
75
102
  export function getHeadingLabel(heading: Element, fallbackLabel: string): string;
103
+ export function getChapterCode(label: string): string | null;
104
+ export function mapToCodeKey(code: string): string;
105
+ export function findSectionByHash(hash: string, sections: Section[]): Section | null;
76
106
 
77
107
  export function getPageConfig(): KmwcPageConfig;
78
108
  export function attachDomReadyHandler(handler: () => void): void;
79
109
 
80
110
  export const CHAPTER_NAV_CLASSES: Record<string, string>;
111
+ export const SPOTLIGHT_CLASSES: Record<string, string>;
112
+ export const SPOTLIGHT_RESULTS_ID: string;
113
+ export const SPOTLIGHT_ITEM_ID_PREFIX: string;
114
+ export const SPOTLIGHT_OPEN_KEY: string;
115
+ export const SPOTLIGHT_PAGE_STEP: number;
116
+ export const SPOTLIGHT_FULLSCREEN_MEDIA: string;
117
+ /** `TH07 · Title` → group 1 is the section code. */
118
+ export const CHAPTER_CODE_PATTERN: RegExp;
81
119
  export const COPY_LINK_CLASSES: Record<string, string>;
82
120
  export const INDEX_PAGE_CLASSES: Record<string, string>;
83
121
  export const THEME_SWITCH_CLASSES: Record<string, string>;
@@ -1,7 +1,7 @@
1
1
  /**
2
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
3
+ * The page-level helpers (styles, chapter nav, spotlight, copy-links, version
4
+ * badge, theme switch) are mounted automatically by the Vite plugin; import from here
5
5
  * only for the opt-in bits (controls persistence, index page) or to drive a
6
6
  * helper manually.
7
7
  */
@@ -9,10 +9,13 @@ export { attachControlsPersistence, getControlsStorageKey } from './controls-per
9
9
  export { renderExamplesIndex } from './index-page.js';
10
10
  export { mountExamplesKit } from './kit.js';
11
11
  export { mountChapterNav } from './chapter-nav.js';
12
+ export { mountSpotlight } from './spotlight.js';
13
+ export { attachChapterLinks, scrollToChapterHash } from './chapter-links.js';
14
+ export { searchChapters, checkMatchesAny, renderHighlightedText } from './chapter-search.js';
12
15
  export { attachCopyLinks, copyText, getSectionUrl } from './copy-link.js';
13
16
  export { renderVersionBadges, getVersionLabel } from './version-badge.js';
14
17
  export { attachDemoCopyButtons } from './demo-code.js';
15
18
  export { mountThemeSwitch, applyTheme, getSavedTheme } from './theme-switch.js';
16
- export { ensureSectionAnchors, generateSlug, getHeadingLabel } from './sections.js';
19
+ export { ensureSectionAnchors, generateSlug, getHeadingLabel, getChapterCode, mapToCodeKey, findSectionByHash } from './sections.js';
17
20
  export { getPageConfig, attachDomReadyHandler } from './page-config.js';
18
21
  export * from './constants.js';
@@ -1,4 +1,6 @@
1
1
  import { mountChapterNav } from './chapter-nav.js';
2
+ import { attachChapterLinks } from './chapter-links.js';
3
+ import { mountSpotlight } from './spotlight.js';
2
4
  import { attachCopyLinks } from './copy-link.js';
3
5
  import { renderVersionBadges } from './version-badge.js';
4
6
  import { mountThemeSwitch } from './theme-switch.js';
@@ -6,16 +8,21 @@ import { attachDemoCopyButtons } from './demo-code.js';
6
8
  import { INDEX_PAGE_CLASSES } from './constants.js';
7
9
 
8
10
  /**
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.
11
+ * (Re)mount the page-level helpers on the current DOM (copy-links, chapter
12
+ * nav, short chapter links, spotlight, version badges, demo copy buttons,
13
+ * theme switch). Runs automatically once per page; call it again after
14
+ * rendering sections asynchronously. Idempotent: copy-link / demo copy buttons
15
+ * skip elements that already have one, the nav, spotlight and theme switch are
16
+ * remounted.
13
17
  */
14
18
  export function mountExamplesKit() {
15
19
  // Copy-links before the nav: the nav strips the buttons back out when
16
20
  // reading labels, and this order avoids a relayout after the nav is built.
17
21
  if (!document.body.classList.contains(INDEX_PAGE_CLASSES.body)) attachCopyLinks();
18
22
  mountChapterNav();
23
+ // After the anchors exist: an arriving #th07 can only resolve now.
24
+ attachChapterLinks();
25
+ mountSpotlight();
19
26
  renderVersionBadges();
20
27
  attachDemoCopyButtons();
21
28
  mountThemeSwitch();
@@ -16,6 +16,7 @@ export function getPageConfig() {
16
16
  mode: injectedConfig.mode || 'dev',
17
17
  canonicalOrigin: injectedConfig.canonicalOrigin || '',
18
18
  storagePrefix: injectedConfig.storagePrefix || injectedConfig.id || 'component',
19
+ isSpotlightEnabled: injectedConfig.isSpotlightEnabled ?? true,
19
20
  repository: injectedConfig.repository || '',
20
21
  homepage: injectedConfig.homepage || '',
21
22
  devkitVersion: injectedConfig.devkitVersion || '',
@@ -8,7 +8,7 @@
8
8
  * reusing an existing section/h2 id before slugging the heading text.
9
9
  * Idempotent: a second call returns the same ids.
10
10
  */
11
- import { HEADING_CHIP_SELECTOR, SECTION_CONTAINER_SELECTOR, SECTION_SELECTOR } from './constants.js';
11
+ import { CHAPTER_CODE_PATTERN, HEADING_CHIP_SELECTOR, SECTION_CONTAINER_SELECTOR, SECTION_SELECTOR } from './constants.js';
12
12
 
13
13
  export function generateSlug(text) {
14
14
  return String(text)
@@ -51,3 +51,40 @@ export function ensureSectionAnchors(root = document) {
51
51
  return { heading, section, id, label };
52
52
  });
53
53
  }
54
+
55
+ /** `TH07 · Title` → 'TH07'; an uncoded label (`Summary`) → null. */
56
+ export function getChapterCode(label) {
57
+ return String(label).match(CHAPTER_CODE_PATTERN)?.[1] ?? null;
58
+ }
59
+
60
+ /** Comparison key for a code: case and leading zeros don't matter (`TH07`, `th7` → 'th7'). */
61
+ export function mapToCodeKey(code) {
62
+ return String(code).toLowerCase().replace(/^([a-z]+)0+(?=\d)/, '$1');
63
+ }
64
+
65
+ /**
66
+ * The section a URL hash points at: its anchor id, else its section code
67
+ * (`#th07`, `#th7` — the short link the codes exist for), else the slug of its
68
+ * heading text (an uncoded chapter whose section carries its own id).
69
+ * @param {string} hash `#…` or the bare fragment
70
+ * @param {ReturnType<typeof ensureSectionAnchors>} sections
71
+ */
72
+ export function findSectionByHash(hash, sections) {
73
+ let fragment = String(hash).replace(/^#/, '');
74
+ try {
75
+ fragment = decodeURIComponent(fragment);
76
+ } catch {
77
+ // a malformed escape — match it as written
78
+ }
79
+ if (!fragment) return null;
80
+ const fragmentKey = mapToCodeKey(fragment);
81
+ return (
82
+ sections.find((section) => section.id === fragment) ??
83
+ sections.find((section) => {
84
+ const code = getChapterCode(section.label);
85
+ return code !== null && mapToCodeKey(code) === fragmentKey;
86
+ }) ??
87
+ sections.find((section) => generateSlug(section.label) === fragment.toLowerCase()) ??
88
+ null
89
+ );
90
+ }