@oddkin/workbench 0.0.0-stage → 1.0.0-beta.1

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 (243) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -2
  3. package/dist/app/card-previews.d.ts +14 -0
  4. package/dist/app/card-previews.d.ts.map +1 -0
  5. package/dist/app/card-previews.js +140 -0
  6. package/dist/app/card-previews.js.map +1 -0
  7. package/dist/app/copy.d.ts +48 -0
  8. package/dist/app/copy.d.ts.map +1 -0
  9. package/dist/app/copy.js +52 -0
  10. package/dist/app/copy.js.map +1 -0
  11. package/dist/app/define.d.ts +3 -0
  12. package/dist/app/define.d.ts.map +1 -0
  13. package/dist/app/define.js +10 -0
  14. package/dist/app/define.js.map +1 -0
  15. package/dist/app/details.d.ts +22 -0
  16. package/dist/app/details.d.ts.map +1 -0
  17. package/dist/app/details.js +17 -0
  18. package/dist/app/details.js.map +1 -0
  19. package/dist/app/index.d.ts +12 -0
  20. package/dist/app/index.d.ts.map +1 -0
  21. package/dist/app/index.js +9 -0
  22. package/dist/app/index.js.map +1 -0
  23. package/dist/app/routes.d.ts +26 -0
  24. package/dist/app/routes.d.ts.map +1 -0
  25. package/dist/app/routes.js +96 -0
  26. package/dist/app/routes.js.map +1 -0
  27. package/dist/app/search.d.ts +17 -0
  28. package/dist/app/search.d.ts.map +1 -0
  29. package/dist/app/search.js +52 -0
  30. package/dist/app/search.js.map +1 -0
  31. package/dist/app/shell.d.ts +29 -0
  32. package/dist/app/shell.d.ts.map +1 -0
  33. package/dist/app/shell.js +478 -0
  34. package/dist/app/shell.js.map +1 -0
  35. package/dist/app/styles.d.ts +2 -0
  36. package/dist/app/styles.d.ts.map +1 -0
  37. package/dist/app/styles.js +356 -0
  38. package/dist/app/styles.js.map +1 -0
  39. package/dist/app/theme-studio.d.ts +12 -0
  40. package/dist/app/theme-studio.d.ts.map +1 -0
  41. package/dist/app/theme-studio.js +14 -0
  42. package/dist/app/theme-studio.js.map +1 -0
  43. package/dist/app/views.d.ts +38 -0
  44. package/dist/app/views.d.ts.map +1 -0
  45. package/dist/app/views.js +252 -0
  46. package/dist/app/views.js.map +1 -0
  47. package/dist/config/index.d.ts +170 -0
  48. package/dist/config/index.d.ts.map +1 -0
  49. package/dist/config/index.js +148 -0
  50. package/dist/config/index.js.map +1 -0
  51. package/dist/conformance/button.d.ts +2 -0
  52. package/dist/conformance/button.d.ts.map +1 -0
  53. package/dist/conformance/button.js +233 -0
  54. package/dist/conformance/button.js.map +1 -0
  55. package/dist/conformance/index.d.ts +107 -0
  56. package/dist/conformance/index.d.ts.map +1 -0
  57. package/dist/conformance/index.js +60 -0
  58. package/dist/conformance/index.js.map +1 -0
  59. package/dist/data/build.d.ts +36 -0
  60. package/dist/data/build.d.ts.map +1 -0
  61. package/dist/data/build.js +260 -0
  62. package/dist/data/build.js.map +1 -0
  63. package/dist/data/import-map.d.ts +59 -0
  64. package/dist/data/import-map.d.ts.map +1 -0
  65. package/dist/data/import-map.js +195 -0
  66. package/dist/data/import-map.js.map +1 -0
  67. package/dist/data/index.d.ts +14 -0
  68. package/dist/data/index.d.ts.map +1 -0
  69. package/dist/data/index.js +10 -0
  70. package/dist/data/index.js.map +1 -0
  71. package/dist/data/theme-studio.d.ts +21 -0
  72. package/dist/data/theme-studio.d.ts.map +1 -0
  73. package/dist/data/theme-studio.js +112 -0
  74. package/dist/data/theme-studio.js.map +1 -0
  75. package/dist/data/types.d.ts +187 -0
  76. package/dist/data/types.d.ts.map +1 -0
  77. package/dist/data/types.js +4 -0
  78. package/dist/data/types.js.map +1 -0
  79. package/dist/examples/index.d.ts +101 -0
  80. package/dist/examples/index.d.ts.map +1 -0
  81. package/dist/examples/index.js +106 -0
  82. package/dist/examples/index.js.map +1 -0
  83. package/dist/frame/index.d.ts +9 -0
  84. package/dist/frame/index.d.ts.map +1 -0
  85. package/dist/frame/index.js +482 -0
  86. package/dist/frame/index.js.map +1 -0
  87. package/dist/internal/diagnostics.d.ts +17 -0
  88. package/dist/internal/diagnostics.d.ts.map +1 -0
  89. package/dist/internal/diagnostics.js +19 -0
  90. package/dist/internal/diagnostics.js.map +1 -0
  91. package/dist/node/bin.d.ts +3 -0
  92. package/dist/node/bin.d.ts.map +1 -0
  93. package/dist/node/bin.js +12 -0
  94. package/dist/node/bin.js.map +1 -0
  95. package/dist/node/cli.d.ts +17 -0
  96. package/dist/node/cli.d.ts.map +1 -0
  97. package/dist/node/cli.js +198 -0
  98. package/dist/node/cli.js.map +1 -0
  99. package/dist/node/config.d.ts +28 -0
  100. package/dist/node/config.d.ts.map +1 -0
  101. package/dist/node/config.js +80 -0
  102. package/dist/node/config.js.map +1 -0
  103. package/dist/node/example-modules.d.ts +8 -0
  104. package/dist/node/example-modules.d.ts.map +1 -0
  105. package/dist/node/example-modules.js +28 -0
  106. package/dist/node/example-modules.js.map +1 -0
  107. package/dist/node/generated.d.ts +19 -0
  108. package/dist/node/generated.d.ts.map +1 -0
  109. package/dist/node/generated.js +109 -0
  110. package/dist/node/generated.js.map +1 -0
  111. package/dist/node/html.d.ts +35 -0
  112. package/dist/node/html.d.ts.map +1 -0
  113. package/dist/node/html.js +51 -0
  114. package/dist/node/html.js.map +1 -0
  115. package/dist/node/index.d.ts +24 -0
  116. package/dist/node/index.d.ts.map +1 -0
  117. package/dist/node/index.js +15 -0
  118. package/dist/node/index.js.map +1 -0
  119. package/dist/node/inputs.d.ts +26 -0
  120. package/dist/node/inputs.d.ts.map +1 -0
  121. package/dist/node/inputs.js +282 -0
  122. package/dist/node/inputs.js.map +1 -0
  123. package/dist/node/packages.d.ts +35 -0
  124. package/dist/node/packages.d.ts.map +1 -0
  125. package/dist/node/packages.js +87 -0
  126. package/dist/node/packages.js.map +1 -0
  127. package/dist/node/preload.d.ts +10 -0
  128. package/dist/node/preload.d.ts.map +1 -0
  129. package/dist/node/preload.js +61 -0
  130. package/dist/node/preload.js.map +1 -0
  131. package/dist/node/server.d.ts +42 -0
  132. package/dist/node/server.d.ts.map +1 -0
  133. package/dist/node/server.js +410 -0
  134. package/dist/node/server.js.map +1 -0
  135. package/dist/node/static.d.ts +43 -0
  136. package/dist/node/static.d.ts.map +1 -0
  137. package/dist/node/static.js +157 -0
  138. package/dist/node/static.js.map +1 -0
  139. package/dist/node/workbench.d.ts +43 -0
  140. package/dist/node/workbench.d.ts.map +1 -0
  141. package/dist/node/workbench.js +204 -0
  142. package/dist/node/workbench.js.map +1 -0
  143. package/dist/pages/build.d.ts +22 -0
  144. package/dist/pages/build.d.ts.map +1 -0
  145. package/dist/pages/build.js +363 -0
  146. package/dist/pages/build.js.map +1 -0
  147. package/dist/pages/defaults.d.ts +16 -0
  148. package/dist/pages/defaults.d.ts.map +1 -0
  149. package/dist/pages/defaults.js +88 -0
  150. package/dist/pages/defaults.js.map +1 -0
  151. package/dist/pages/index.d.ts +14 -0
  152. package/dist/pages/index.d.ts.map +1 -0
  153. package/dist/pages/index.js +10 -0
  154. package/dist/pages/index.js.map +1 -0
  155. package/dist/pages/navigation.d.ts +48 -0
  156. package/dist/pages/navigation.d.ts.map +1 -0
  157. package/dist/pages/navigation.js +83 -0
  158. package/dist/pages/navigation.js.map +1 -0
  159. package/dist/pages/types.d.ts +148 -0
  160. package/dist/pages/types.d.ts.map +1 -0
  161. package/dist/pages/types.js +3 -0
  162. package/dist/pages/types.js.map +1 -0
  163. package/dist/panels/catalog-define.d.ts +3 -0
  164. package/dist/panels/catalog-define.d.ts.map +1 -0
  165. package/dist/panels/catalog-define.js +7 -0
  166. package/dist/panels/catalog-define.js.map +1 -0
  167. package/dist/panels/catalog-layout-styles.d.ts +2 -0
  168. package/dist/panels/catalog-layout-styles.d.ts.map +1 -0
  169. package/dist/panels/catalog-layout-styles.js +54 -0
  170. package/dist/panels/catalog-layout-styles.js.map +1 -0
  171. package/dist/panels/catalog-styles.d.ts +2 -0
  172. package/dist/panels/catalog-styles.d.ts.map +1 -0
  173. package/dist/panels/catalog-styles.js +106 -0
  174. package/dist/panels/catalog-styles.js.map +1 -0
  175. package/dist/panels/catalog.d.ts +37 -0
  176. package/dist/panels/catalog.d.ts.map +1 -0
  177. package/dist/panels/catalog.js +74 -0
  178. package/dist/panels/catalog.js.map +1 -0
  179. package/dist/panels/component-define.d.ts +3 -0
  180. package/dist/panels/component-define.d.ts.map +1 -0
  181. package/dist/panels/component-define.js +6 -0
  182. package/dist/panels/component-define.js.map +1 -0
  183. package/dist/panels/component-detail.d.ts +49 -0
  184. package/dist/panels/component-detail.d.ts.map +1 -0
  185. package/dist/panels/component-detail.js +1352 -0
  186. package/dist/panels/component-detail.js.map +1 -0
  187. package/dist/panels/component.d.ts +17 -0
  188. package/dist/panels/component.d.ts.map +1 -0
  189. package/dist/panels/component.js +10 -0
  190. package/dist/panels/component.js.map +1 -0
  191. package/dist/panels/example-styles.d.ts +2 -0
  192. package/dist/panels/example-styles.d.ts.map +1 -0
  193. package/dist/panels/example-styles.js +343 -0
  194. package/dist/panels/example-styles.js.map +1 -0
  195. package/dist/panels/foundation-define.d.ts +3 -0
  196. package/dist/panels/foundation-define.d.ts.map +1 -0
  197. package/dist/panels/foundation-define.js +6 -0
  198. package/dist/panels/foundation-define.js.map +1 -0
  199. package/dist/panels/foundation-detail.d.ts +23 -0
  200. package/dist/panels/foundation-detail.d.ts.map +1 -0
  201. package/dist/panels/foundation-detail.js +216 -0
  202. package/dist/panels/foundation-detail.js.map +1 -0
  203. package/dist/panels/foundation.d.ts +15 -0
  204. package/dist/panels/foundation.d.ts.map +1 -0
  205. package/dist/panels/foundation.js +9 -0
  206. package/dist/panels/foundation.js.map +1 -0
  207. package/dist/panels/model.d.ts +184 -0
  208. package/dist/panels/model.d.ts.map +1 -0
  209. package/dist/panels/model.js +488 -0
  210. package/dist/panels/model.js.map +1 -0
  211. package/dist/panels/styles.d.ts +3 -0
  212. package/dist/panels/styles.d.ts.map +1 -0
  213. package/dist/panels/styles.js +265 -0
  214. package/dist/panels/styles.js.map +1 -0
  215. package/dist/panels/theme-model.d.ts +69 -0
  216. package/dist/panels/theme-model.d.ts.map +1 -0
  217. package/dist/panels/theme-model.js +166 -0
  218. package/dist/panels/theme-model.js.map +1 -0
  219. package/dist/panels/theme-studio.d.ts +32 -0
  220. package/dist/panels/theme-studio.d.ts.map +1 -0
  221. package/dist/panels/theme-studio.js +375 -0
  222. package/dist/panels/theme-studio.js.map +1 -0
  223. package/dist/preview/frame.d.ts +44 -0
  224. package/dist/preview/frame.d.ts.map +1 -0
  225. package/dist/preview/frame.js +260 -0
  226. package/dist/preview/frame.js.map +1 -0
  227. package/dist/preview/index.d.ts +26 -0
  228. package/dist/preview/index.d.ts.map +1 -0
  229. package/dist/preview/index.js +58 -0
  230. package/dist/preview/index.js.map +1 -0
  231. package/dist/preview/theme-generation.d.ts +33 -0
  232. package/dist/preview/theme-generation.d.ts.map +1 -0
  233. package/dist/preview/theme-generation.js +48 -0
  234. package/dist/preview/theme-generation.js.map +1 -0
  235. package/dist/protocol/index.d.ts +222 -0
  236. package/dist/protocol/index.d.ts.map +1 -0
  237. package/dist/protocol/index.js +272 -0
  238. package/dist/protocol/index.js.map +1 -0
  239. package/dist/testing/index.d.ts +47 -0
  240. package/dist/testing/index.d.ts.map +1 -0
  241. package/dist/testing/index.js +145 -0
  242. package/dist/testing/index.js.map +1 -0
  243. package/package.json +118 -4
@@ -0,0 +1,1352 @@
1
+ /**
2
+ * Component detail content (workbench-pages.md §7): every section of a component page, with the
3
+ * editable API table as the only control surface over one isolated preview (or a light/dark
4
+ * pair). Rendered by the public `<oddkin-component-panel>` (open shadow root) and by the shell
5
+ * (light DOM, so "On this page" fragments resolve).
6
+ */
7
+ import { OddkinElement, prop } from '@oddkin/core/element';
8
+ import { signal, untracked } from '@oddkin/core/reactive';
9
+ import { createScope } from '@oddkin/core/scope';
10
+ import { html, nothing, repeat } from '@oddkin/core/template';
11
+ import { createIsolatedPreview, PreviewFrameError, } from '../preview/frame.js';
12
+ import { isCustomPropertyName, } from '../protocol/index.js';
13
+ import { authoredValues, componentFacts, decodeShare, defaultWriteMode, encodeShare, initialState, intendedUse, pageCoverage, presets, registrationOf, usagePairs, MAX_SHARE_WIDTH, MIN_SHARE_WIDTH, SHARE_PARAM, sharePayload, snippet, } from './model.js';
14
+ import { contentStyles, panelHostStyles } from './styles.js';
15
+ import { exampleStyles } from './example-styles.js';
16
+ const LOG_LIMIT = 50;
17
+ const WIDTHS = {
18
+ narrow: 320,
19
+ medium: 640,
20
+ wide: 960,
21
+ };
22
+ const DEFAULT_SETTINGS = {
23
+ mode: 'site',
24
+ pair: false,
25
+ direction: 'ltr',
26
+ width: 'fill',
27
+ customWidth: 480,
28
+ };
29
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
30
+ const message = (error) => (error instanceof Error ? error.message : String(error));
31
+ /** Text with Markdown code spans shown as code; everything stays text, never markup. */
32
+ function inline(text) {
33
+ const parts = text.split('`');
34
+ if (parts.length % 2 === 0)
35
+ parts[parts.length - 2] += `\`${parts.pop()}`;
36
+ return parts.map((part, i) => (i % 2 ? html `<code>${part}</code>` : part));
37
+ }
38
+ const notDocumented = (key, subject) => {
39
+ const lead = `${subject ? `${subject}: not documented` : 'Not documented'}. Supplied by `;
40
+ return html `<p class="wb-not-documented">${lead}<code>${key}</code>.</p>`;
41
+ };
42
+ const bullets = (items) => html `<ul class="wb-list">
43
+ ${items.map((item) => html `<li>${inline(item)}</li>`)}
44
+ </ul>`;
45
+ /** The reference layout's links to its sections (Figma 19:874), by section id. */
46
+ const PAGE_LINKS = [
47
+ ['overview', 'Overview'],
48
+ ['interactive-example', 'Controls'],
49
+ ['examples', 'Examples'],
50
+ ['behavior-and-accessibility', 'Accessibility'],
51
+ ];
52
+ /** `'secondary'` reads as `secondary`; an empty literal stays as written. */
53
+ const unquote = (value) => (/^(['"`]).+\1$/.test(value) ? value.slice(1, -1) : value);
54
+ /** A row's name cell: the name, and its description or "Not documented". */
55
+ const named = (name, description) => html `<th scope="row" data-label="Name">
56
+ <code>${name}</code>
57
+ <small
58
+ >${description ? inline(description) : html `<span class="wb-not-documented">Not documented</span>`}</small
59
+ >
60
+ </th>`;
61
+ function covered(coverage, quiet) {
62
+ const byId = new Map(coverage.map((item) => [item.id, item]));
63
+ return (id, content) => {
64
+ const item = byId.get(id);
65
+ if (!item || (quiet && item.state !== 'documented'))
66
+ return nothing;
67
+ return html `<div data-coverage=${id} data-state=${item.state}>
68
+ ${item.state === 'documented'
69
+ ? content()
70
+ : item.state === 'none'
71
+ ? html `<p class="wb-muted">${item.empty}</p>`
72
+ : notDocumented(item.source ?? '', item.subject)}
73
+ </div>`;
74
+ };
75
+ }
76
+ export class ComponentDetailElement extends OddkinElement.with({
77
+ page: prop.object({ description: 'The component page data.' }),
78
+ index: prop.object({ description: 'The Workbench index.' }),
79
+ href: prop.custom({
80
+ default: () => (route) => route,
81
+ coerce: (value) => typeof value === 'function'
82
+ ? { ok: true, value: value }
83
+ : { ok: false, reason: 'expected a function' },
84
+ description: 'Maps a Workbench route to a URL.',
85
+ }),
86
+ frameUrl: prop.string({
87
+ default: '',
88
+ attribute: false,
89
+ description: 'URL of the preview frame document.',
90
+ }),
91
+ layout: prop.enum(['workbench', 'reference'], {
92
+ default: 'workbench',
93
+ description: "'reference' is the documentation page (Figma 20:2190): links to the page's sections, no preview tools (presets, settings, example link, event log), and absent items left out instead of labelled.",
94
+ }),
95
+ examplePreview: prop.custom({
96
+ default: () => undefined,
97
+ coerce: (value) => value === undefined || typeof value === 'function'
98
+ ? {
99
+ ok: true,
100
+ value: value,
101
+ }
102
+ : { ok: false, reason: 'expected a function' },
103
+ description: 'Returns a live element for an example card (the example id, `example:<tag>/<name>`). Only a host that loads the library supplies one; the same example should get the same node.',
104
+ }),
105
+ sections: prop.custom({
106
+ default: () => null,
107
+ coerce: (value) => value === null || value === undefined
108
+ ? { ok: true, value: null }
109
+ : Array.isArray(value) && value.every((id) => typeof id === 'string')
110
+ ? { ok: true, value: Object.freeze([...value]) }
111
+ : { ok: false, reason: 'expected an array of section ids, or null' },
112
+ description: "The section ids to show, for a host's tabs (overview, interactive-example, api, examples, tokens, behavior-and-accessibility, guidance, source, known-limitations); null shows every section. The others stay rendered but hidden, so the example's preview and controls survive a switch, and the panel leaves out its own section links.",
113
+ }),
114
+ }) {
115
+ static styles = [panelHostStyles, contentStyles, exampleStyles];
116
+ #state = signal({
117
+ preset: 'default',
118
+ attributes: {},
119
+ properties: {},
120
+ slots: {},
121
+ });
122
+ #modes = signal({});
123
+ #errors = signal({});
124
+ #settings = signal(DEFAULT_SETTINGS);
125
+ #log = signal([]);
126
+ #status = signal({
127
+ text: '',
128
+ alert: false,
129
+ });
130
+ #link = signal('');
131
+ /** The generated code is shown under the example. */
132
+ #code = signal(false);
133
+ /** The section the reference layout's links mark as current. */
134
+ #current = signal('');
135
+ /** The reference layout shows every example card, not the first four. */
136
+ #allExamples = signal(false);
137
+ #hosts;
138
+ #previews = [];
139
+ #previewScope;
140
+ #key = '';
141
+ #logId = 0;
142
+ constructor() {
143
+ super();
144
+ this.#hosts = [document.createElement('div'), document.createElement('div')];
145
+ }
146
+ /** Current preview configuration (for tests and embedding); read-only snapshot. */
147
+ get previewState() {
148
+ return this.#state.peek();
149
+ }
150
+ /** Live preview frames, in order (light first when shown side by side). */
151
+ get previews() {
152
+ return this.#previews;
153
+ }
154
+ connected(scope) {
155
+ scope.effect(() => {
156
+ const page = this.page;
157
+ const frameUrl = this.frameUrl;
158
+ untracked(() => this.#sync(page, frameUrl));
159
+ });
160
+ scope.add(() => {
161
+ this.#key = '';
162
+ this.#stop();
163
+ });
164
+ let frame = 0;
165
+ scope.listen(this.ownerDocument, 'scroll', () => {
166
+ if (this.layout === 'reference' && !frame)
167
+ frame = requestAnimationFrame(() => {
168
+ frame = 0;
169
+ this.#spy();
170
+ });
171
+ }, { capture: true, passive: true });
172
+ scope.add(() => cancelAnimationFrame(frame));
173
+ }
174
+ /** The current section is the last linked one whose top has passed a third of the view. */
175
+ #spy() {
176
+ const view = this.ownerDocument.defaultView;
177
+ const page = this.ownerDocument.scrollingElement;
178
+ if (!view || !page)
179
+ return;
180
+ const linked = PAGE_LINKS.flatMap(([id]) => this.shadowRoot?.getElementById(id) ?? []);
181
+ const atEnd = page.scrollTop + view.innerHeight >= page.scrollHeight - 2;
182
+ let current = linked[0]?.id ?? '';
183
+ for (const section of linked)
184
+ if (atEnd || section.getBoundingClientRect().top <= view.innerHeight / 3)
185
+ current = section.id;
186
+ this.#current.value = current;
187
+ }
188
+ /** Scroll a section into view and move focus to its heading. */
189
+ #jump(event, id) {
190
+ event.preventDefault();
191
+ const section = this.shadowRoot?.getElementById(id);
192
+ section?.scrollIntoView({ block: 'start' });
193
+ section?.querySelector('h2')?.focus({ preventScroll: true });
194
+ this.#current.value = id;
195
+ }
196
+ /** Restart the preview only when the component, registration, or frame document changes. */
197
+ #sync(page, frameUrl) {
198
+ const registration = page && registrationOf(page);
199
+ const key = page ? `${page.tag}\n${String(registration)}\n${frameUrl}` : '';
200
+ if (key === this.#key)
201
+ return;
202
+ const sameTag = this.#key.split('\n')[0] === page?.tag;
203
+ this.#key = key;
204
+ if (!page) {
205
+ this.#stop();
206
+ return;
207
+ }
208
+ if (!sameTag) {
209
+ this.#modes.value = {};
210
+ this.#errors.value = {};
211
+ this.#log.value = [];
212
+ this.#link.value = '';
213
+ this.#state.value = this.#fromLocation(page) ?? initialState(page);
214
+ }
215
+ this.#restart();
216
+ }
217
+ #fromLocation(page) {
218
+ const location = this.ownerDocument.defaultView?.location;
219
+ const hashQuery = location?.hash.includes('?')
220
+ ? location.hash.slice(location.hash.indexOf('?'))
221
+ : '';
222
+ const encoded = new URLSearchParams(location?.search ?? '').get(SHARE_PARAM) ??
223
+ new URLSearchParams(hashQuery.split('#')[0]).get(SHARE_PARAM);
224
+ if (!encoded)
225
+ return undefined;
226
+ const { state, preview, problems } = decodeShare(page, encoded);
227
+ if (problems.length)
228
+ this.#status.value = { text: problems.join(' '), alert: !state, keep: true };
229
+ if (!state)
230
+ return undefined;
231
+ if (preview) {
232
+ const width = preview.width;
233
+ const named = typeof width === 'number'
234
+ ? Object.keys(WIDTHS).find((key) => WIDTHS[key] === width)
235
+ : undefined;
236
+ this.#settings.value = {
237
+ ...DEFAULT_SETTINGS,
238
+ mode: preview.mode ?? 'site',
239
+ direction: preview.direction ?? 'ltr',
240
+ ...(typeof width !== 'number'
241
+ ? {}
242
+ : named
243
+ ? { width: named }
244
+ : { width: 'custom', customWidth: width }),
245
+ };
246
+ }
247
+ return state;
248
+ }
249
+ #stop() {
250
+ this.#previewScope?.dispose();
251
+ this.#previewScope = undefined;
252
+ this.#previews = [];
253
+ for (const host of this.#hosts)
254
+ host.replaceChildren();
255
+ }
256
+ #frameSettings(index) {
257
+ const settings = this.#settings.peek();
258
+ const mode = settings.pair ? (index === 0 ? 'light' : 'dark') : settings.mode;
259
+ const width = settings.width === 'fill'
260
+ ? undefined
261
+ : settings.width === 'custom'
262
+ ? settings.customWidth
263
+ : WIDTHS[settings.width];
264
+ return {
265
+ ...(mode === 'site' ? {} : { mode }),
266
+ direction: settings.direction,
267
+ ...(width ? { container: { width } } : {}),
268
+ };
269
+ }
270
+ #mountPayload(page, registration, index) {
271
+ const state = this.#state.peek();
272
+ const facts = componentFacts(page);
273
+ const fixture = presets(page).some((p) => p.name === state.preset && p.fixture);
274
+ return {
275
+ tag: page.tag,
276
+ registration,
277
+ example: `example:${page.tag}/${state.preset}`,
278
+ properties: state.properties,
279
+ attributes: state.attributes,
280
+ slots: state.slots,
281
+ styles: state.styles ?? {},
282
+ ...(fixture && page.exampleModule ? { fixture: page.exampleModule } : {}),
283
+ events: facts.events.map((event) => event.name),
284
+ sync: facts.syncPairs,
285
+ settings: this.#frameSettings(index),
286
+ };
287
+ }
288
+ /** Create the preview frame(s) and mount the current state; replaces earlier frames. */
289
+ #restart() {
290
+ this.#stop();
291
+ const page = this.page;
292
+ if (!page || !this.isConnected)
293
+ return;
294
+ const registration = registrationOf(page);
295
+ // Without a registration entry the preview item itself says what is missing.
296
+ if (registration === undefined) {
297
+ if (!this.#status.peek().keep)
298
+ this.#status.value = { text: '', alert: false };
299
+ return;
300
+ }
301
+ if (!this.frameUrl) {
302
+ this.#status.value = {
303
+ text: 'Preview unavailable: no preview frame document is configured.',
304
+ alert: false,
305
+ };
306
+ return;
307
+ }
308
+ const scope = createScope({ name: `workbench-detail:${page.tag}` });
309
+ this.#previewScope = scope;
310
+ const count = this.#settings.peek().pair ? 2 : 1;
311
+ try {
312
+ for (let i = 0; i < count; i++) {
313
+ const preview = createIsolatedPreview({
314
+ document: this.ownerDocument,
315
+ container: this.#hosts[i],
316
+ frameUrl: this.frameUrl,
317
+ title: `${page.tag} preview${count === 2 ? (i === 0 ? ' (light)' : ' (dark)') : ''}`,
318
+ onMessage: (notification) => this.#onMessage(notification, i),
319
+ });
320
+ preview.iframe.style.blockSize = '14rem';
321
+ scope.add(() => preview.dispose(), 'isolated preview');
322
+ this.#previews.push(preview);
323
+ }
324
+ }
325
+ catch (error) {
326
+ this.#fail(error);
327
+ return;
328
+ }
329
+ this.#mountAll(scope, page, registration);
330
+ }
331
+ #mountAll(scope, page, registration) {
332
+ // Each preview handles requests in order, so changes made from now on follow this mount.
333
+ void Promise.all(this.#previews.map((preview, i) => preview.request('mount', this.#mountPayload(page, registration, i)))).then(() => {
334
+ if (!scope.disposed && !this.#status.peek().alert && !this.#status.peek().keep)
335
+ this.#status.value = { text: '', alert: false };
336
+ }, (error) => {
337
+ if (!scope.disposed)
338
+ this.#fail(error);
339
+ });
340
+ }
341
+ #fail(error) {
342
+ // A request for a frame that was replaced (Reset) or removed has nothing left to report.
343
+ if (error instanceof PreviewFrameError &&
344
+ (error.code === 'frame.generation.superseded' || error.code === 'frame.disposed'))
345
+ return;
346
+ this.#status.value = { text: `Preview unavailable: ${message(error)}`, alert: true };
347
+ }
348
+ #onMessage(notification, index) {
349
+ const payload = notification.payload;
350
+ if (notification.kind === 'event' && isRecord(payload) && index === 0) {
351
+ const entry = {
352
+ id: ++this.#logId,
353
+ name: String(payload['name']),
354
+ time: typeof payload['time'] === 'number' ? payload['time'] : 0,
355
+ bubbles: payload['bubbles'] === true,
356
+ composed: payload['composed'] === true,
357
+ target: String(payload['target'] ?? ''),
358
+ ...(payload['detail'] !== undefined ? { detail: JSON.stringify(payload['detail']) } : {}),
359
+ };
360
+ this.#log.value = [entry, ...this.#log.peek()].slice(0, LOG_LIMIT);
361
+ }
362
+ else if (notification.kind === 'error' &&
363
+ isRecord(payload) &&
364
+ payload['phase'] === 'fixture') {
365
+ this.#status.value = {
366
+ text: `The example's setup did not run (${String(payload['message'])}), so the preview shows the element without it.`,
367
+ alert: false,
368
+ keep: true,
369
+ };
370
+ }
371
+ else if (notification.kind === 'error' && isRecord(payload)) {
372
+ this.#status.value = {
373
+ text: `Preview ${String(payload['phase'])} failed: ${String(payload['message'])}. Use Reset to start a fresh preview.`,
374
+ alert: true,
375
+ };
376
+ }
377
+ else if (notification.kind === 'state' &&
378
+ isRecord(payload) &&
379
+ typeof payload['property'] === 'string' &&
380
+ index === 0) {
381
+ // A declared sync pair: mirror the value without writing it back (no reflection loop).
382
+ const property = payload['property'];
383
+ const value = payload['value'];
384
+ const state = this.#state.peek();
385
+ this.#state.value = { ...state, properties: { ...state.properties, [property]: value } };
386
+ }
387
+ }
388
+ /** Send one request to every live preview. */
389
+ #send(kind, payload) {
390
+ for (const preview of this.#previews)
391
+ void preview.request(kind, payload).catch((error) => this.#fail(error));
392
+ }
393
+ #setError(id, text) {
394
+ const errors = { ...this.#errors.peek() };
395
+ if (text)
396
+ errors[id] = text;
397
+ else
398
+ delete errors[id];
399
+ this.#errors.value = errors;
400
+ }
401
+ /** Why a typed value is not written (a number field's text that is not a number yet). */
402
+ #textProblem(number, mode, text) {
403
+ if (!number)
404
+ return undefined;
405
+ if (mode === 'attribute')
406
+ return text !== '' && !Number.isFinite(Number(text))
407
+ ? 'Not written: not a number.'
408
+ : undefined;
409
+ return text.trim() === '' || !Number.isFinite(Number(text))
410
+ ? 'Not written: enter a number, or use Unset.'
411
+ : undefined;
412
+ }
413
+ #writeAttribute(row, value) {
414
+ const name = row.attribute;
415
+ const state = this.#state.peek();
416
+ const attributes = { ...state.attributes };
417
+ if (value === null)
418
+ delete attributes[name];
419
+ else
420
+ attributes[name] = value;
421
+ this.#state.value = { ...state, attributes };
422
+ this.#setError(row.id, undefined);
423
+ this.#send('set', { attribute: name, attributeValue: value });
424
+ }
425
+ #writeProperty(row, value) {
426
+ const name = row.property;
427
+ const state = this.#state.peek();
428
+ this.#state.value = { ...state, properties: { ...state.properties, [name]: value } };
429
+ this.#setError(row.id, undefined);
430
+ this.#send('set', { property: name, value });
431
+ }
432
+ /** Unset a property assignment: the frame cannot un-assign, so a clean instance is mounted. */
433
+ #unsetProperty(row) {
434
+ const name = row.property;
435
+ const state = this.#state.peek();
436
+ if (!(name in state.properties))
437
+ return;
438
+ const properties = { ...state.properties };
439
+ delete properties[name];
440
+ this.#state.value = { ...state, properties };
441
+ this.#setError(row.id, undefined);
442
+ this.#remount('Unset recreated the instance without this assignment.');
443
+ }
444
+ #writeSlot(name, text) {
445
+ const state = this.#state.peek();
446
+ this.#state.value = { ...state, slots: { ...state.slots, [name]: text } };
447
+ this.#send('slot', { name, text });
448
+ }
449
+ /** A CSS custom property on the element's style; `null` removes it. */
450
+ #writeStyle(name, value) {
451
+ const state = this.#state.peek();
452
+ const styles = { ...state.styles };
453
+ if (value === null)
454
+ delete styles[name];
455
+ else
456
+ styles[name] = value;
457
+ this.#state.value = { ...state, styles };
458
+ this.#send('set', { cssProperty: name, cssValue: value });
459
+ }
460
+ /** Mount the current state again in the existing frames (a clean instance, same realm). */
461
+ #remount(note) {
462
+ const page = this.page;
463
+ const registration = page && registrationOf(page);
464
+ const scope = this.#previewScope;
465
+ if (!page || registration === undefined || !scope)
466
+ return;
467
+ this.#status.value = { text: note ?? '', alert: false, ...(note ? { keep: true } : {}) };
468
+ this.#mountAll(scope, page, registration);
469
+ }
470
+ #applyPreset(entry) {
471
+ if (!entry.state)
472
+ return;
473
+ this.#state.value = entry.state;
474
+ this.#errors.value = {};
475
+ this.#remount();
476
+ }
477
+ /** Reset: fresh frame documents (new generation) with the selected preset's authored values. */
478
+ #reset() {
479
+ const page = this.page;
480
+ if (!page)
481
+ return;
482
+ const preset = presets(page).find((p) => p.name === this.#state.peek().preset);
483
+ this.#state.value = preset?.state ?? initialState(page);
484
+ this.#errors.value = {};
485
+ this.#log.value = [];
486
+ this.#status.value = { text: '', alert: false };
487
+ this.#replace();
488
+ }
489
+ /**
490
+ * Replace the preview realms with fresh frame documents and mount the current configuration
491
+ * again. The shell calls this when component code changes (a registration cannot be redefined
492
+ * in the same realm).
493
+ */
494
+ replacePreview() {
495
+ return this.#replace();
496
+ }
497
+ #replace() {
498
+ const page = this.page;
499
+ const registration = page && registrationOf(page);
500
+ const scope = this.#previewScope;
501
+ if (!page || registration === undefined || !scope) {
502
+ this.#restart();
503
+ return Promise.resolve();
504
+ }
505
+ return Promise.all(this.#previews.map((preview) => preview.reload())).then(() => this.#mountAll(scope, page, registration), (error) => {
506
+ if (!scope.disposed)
507
+ this.#fail(error);
508
+ });
509
+ }
510
+ #applySettings(next) {
511
+ const pairChanged = next.pair !== this.#settings.peek().pair;
512
+ this.#settings.value = next;
513
+ if (pairChanged) {
514
+ this.#restart();
515
+ return;
516
+ }
517
+ this.#previews.forEach((preview, i) => {
518
+ void preview
519
+ .request('settings', this.#frameSettings(i))
520
+ .catch((error) => this.#fail(error));
521
+ });
522
+ }
523
+ #shareLink(page) {
524
+ const settings = this.#settings.peek();
525
+ const width = settings.width === 'fill'
526
+ ? 'fill'
527
+ : settings.width === 'custom'
528
+ ? settings.customWidth
529
+ : WIDTHS[settings.width];
530
+ // Light and dark side by side is a view of the page, not a preview setting to share.
531
+ const preview = {
532
+ ...(settings.mode === 'site' || settings.pair
533
+ ? { sameAsSite: true }
534
+ : { mode: settings.mode }),
535
+ direction: settings.direction,
536
+ width,
537
+ };
538
+ const encoded = encodeShare(sharePayload(page.tag, this.#state.peek(), preview));
539
+ const route = this.index?.registry.pages.find((p) => p.id === page.id)?.route ?? `/${page.id}`;
540
+ const url = new URL(this.href(route), this.ownerDocument.baseURI);
541
+ // Hash routing keeps the query inside the fragment, where the router reads it.
542
+ if (url.hash.startsWith('#/')) {
543
+ const [path = '', query = ''] = url.hash.slice(1).split('?');
544
+ const params = new URLSearchParams(query);
545
+ params.set(SHARE_PARAM, encoded);
546
+ url.hash = `${path}?${params}`;
547
+ }
548
+ else {
549
+ url.searchParams.set(SHARE_PARAM, encoded);
550
+ url.hash = 'interactive-example';
551
+ }
552
+ return url.href;
553
+ }
554
+ #copyLink(page) {
555
+ const link = this.#shareLink(page);
556
+ this.#link.value = link;
557
+ const clipboard = this.ownerDocument.defaultView?.navigator.clipboard;
558
+ if (!clipboard)
559
+ return;
560
+ clipboard.writeText(link).then(() => (this.#status.value = { text: 'Example link copied.', alert: false, keep: true }), () => undefined);
561
+ }
562
+ #copyCode() {
563
+ const code = this.shadowRoot?.querySelector('.wb-snippet')?.textContent ?? '';
564
+ this.ownerDocument.defaultView?.navigator.clipboard?.writeText(code).then(() => (this.#status.value = { text: 'Code copied.', alert: false, keep: true }), () => undefined);
565
+ }
566
+ // ---- Rendering ----
567
+ render() {
568
+ const page = this.page;
569
+ const index = this.index;
570
+ if (!page || !index)
571
+ return html `<div class="wb-content"></div>`;
572
+ const entry = index.registry.pages.find((p) => p.id === page.id);
573
+ const reference = this.layout === 'reference';
574
+ const facts = componentFacts(page);
575
+ const coverage = pageCoverage(page);
576
+ const at = covered(coverage, reference);
577
+ // The reference layout leaves out a section with nothing to show; Workbench labels it.
578
+ const sections = (entry?.sections ?? []).filter((s) => s.visible &&
579
+ (!reference ||
580
+ coverage.some((item) => item.section === s.id && item.state === 'documented') ||
581
+ (s.id === 'api' && facts.cssProperties.length > 0)));
582
+ const links = reference && !this.sections
583
+ ? PAGE_LINKS.filter(([id]) => sections.some((s) => s.id === id))
584
+ : [];
585
+ const current = this.#current.value || links[0]?.[0];
586
+ const shown = this.sections;
587
+ // Sections are built eagerly (not with repeat()), so the signals they read are tracked.
588
+ return html `<div class="wb-content">
589
+ ${links.length
590
+ ? html `<nav class="wb-page-nav" aria-label="Page sections">
591
+ ${links.map(([id, label]) => html `<a
592
+ href=${`#${id}`}
593
+ aria-current=${current === id ? 'true' : null}
594
+ @click=${(event) => this.#jump(event, id)}
595
+ >${label}</a
596
+ >`)}
597
+ </nav>`
598
+ : nothing}
599
+ ${sections.map((section) => html `<section
600
+ id=${section.id}
601
+ aria-labelledby=${`${section.id}-title`}
602
+ ?hidden=${!!shown && !shown.includes(section.id)}
603
+ >
604
+ <h2 id=${`${section.id}-title`} tabindex="-1">${section.title}</h2>
605
+ ${this.#section(section.id, page, facts, at, sections)}
606
+ </section>`)}
607
+ </div>`;
608
+ }
609
+ #section(id, page, facts, at, sections) {
610
+ switch (id) {
611
+ case 'overview':
612
+ return this.#overview(page, at);
613
+ case 'interactive-example':
614
+ return this.#interactive(page, facts, at);
615
+ case 'api':
616
+ return this.#reference(facts, at, sections.some((s) => s.id === 'interactive-example'));
617
+ case 'examples':
618
+ return at('examples/examples', () => this.#examples(page));
619
+ case 'tokens':
620
+ return this.#tokens(page, at);
621
+ case 'behavior-and-accessibility':
622
+ return this.#behavior(page, at);
623
+ case 'guidance':
624
+ return this.#guidance(page, at);
625
+ case 'source':
626
+ return this.#source(page, at);
627
+ case 'known-limitations':
628
+ return at('known-limitations/limitations', () => html `<div class="wb-callout">
629
+ ${bullets(authoredValues(page.enrichment?.['limitations']))}
630
+ </div>`);
631
+ default:
632
+ return notDocumented(`pages["${page.id}"].sections["${id}"] in oddkin.docs.ts`);
633
+ }
634
+ }
635
+ #overview(page, at) {
636
+ const description = typeof page.declaration['description'] === 'string' ? page.declaration['description'] : '';
637
+ return at('overview/description', () => description
638
+ .split(/\n\s*\n/)
639
+ .map((paragraph) => html `<p>${inline(paragraph.replace(/\s*\n\s*/g, ' '))}</p>`));
640
+ }
641
+ /** Intended use and explained mistakes, each paired with the example to use instead. */
642
+ #guidance(page, at) {
643
+ const { useFor, notFor } = intendedUse(page);
644
+ const authored = page.authored?.guidance ?? '';
645
+ const show = (entry, label) => entry.state
646
+ ? html `<button type="button" @click=${() => this.#applyPreset(entry)}>${label}</button>`
647
+ : nothing;
648
+ return html `${at('guidance/authored', () => authored.split(/\n\s*\n/).map((paragraph) => html `<p>${inline(paragraph)}</p>`))}
649
+ <h3>Use it for</h3>
650
+ ${at('guidance/use-for', () => bullets(useFor))}
651
+ <h3>Not for</h3>
652
+ ${at('guidance/not-for', () => bullets(notFor))}
653
+ <h3>Correct and incorrect usage</h3>
654
+ ${at('guidance/usage-pairs', () => html `<ul class="wb-list wb-pairs">
655
+ ${usagePairs(page).map(({ incorrect, correct }) => html `<li data-pair=${incorrect.id}>
656
+ <p><strong>Incorrect:</strong> ${incorrect.label} <code>${incorrect.id}</code></p>
657
+ ${incorrect.incorrect?.explanation
658
+ ? html `<p>${inline(incorrect.incorrect.explanation)}</p>`
659
+ : notDocumented('incorrect.explanation in defineExamples', 'Explanation')}
660
+ <p>
661
+ <strong>Correct:</strong>
662
+ ${correct
663
+ ? html `${correct.label} <code>${correct.id}</code>`
664
+ : html `<span class="wb-not-documented">Not documented</span>`}
665
+ </p>
666
+ ${show(incorrect, 'Show the incorrect usage in the preview')}
667
+ ${correct ? show(correct, 'Show the correct usage in the preview') : nothing}
668
+ </li>`)}
669
+ </ul>`)}`;
670
+ }
671
+ #interactive(page, facts, at) {
672
+ const state = this.#state.value;
673
+ const settings = this.#settings.value;
674
+ const status = this.#status.value;
675
+ const entries = presets(page).filter((p) => p.state);
676
+ const link = this.#link.value;
677
+ const frameSettings = settings.pair ? {} : this.#frameSettings(0);
678
+ const update = (patch) => this.#applySettings({ ...settings, ...patch });
679
+ const code = this.#code.value;
680
+ const tools = this.layout === 'workbench';
681
+ return html `<div class="wb-example">
682
+ <div class="wb-example-bar">
683
+ <p class="wb-example-title">Interactive example · ${page.tag}</p>
684
+ ${tools
685
+ ? at('interactive-example/presets', () => html `<label
686
+ >Preset
687
+ <select
688
+ .value=${state.preset}
689
+ @change=${(event) => {
690
+ const name = event.target.value;
691
+ const entry = entries.find((p) => p.name === name);
692
+ if (entry)
693
+ this.#applyPreset(entry);
694
+ }}
695
+ >
696
+ ${entries.map((p) => html `<option value=${p.name} ?selected=${p.name === state.preset}>
697
+ ${p.label}${p.incorrect ? ' (incorrect usage)' : ''}
698
+ </option>`)}
699
+ </select></label
700
+ >`)
701
+ : nothing}
702
+ <button
703
+ type="button"
704
+ aria-expanded=${code ? 'true' : 'false'}
705
+ aria-controls="wb-example-code"
706
+ @click=${() => (this.#code.value = !code)}
707
+ >
708
+ ${code ? 'Hide code' : 'See code'}
709
+ </button>
710
+ ${tools
711
+ ? html `<button type="button" @click=${() => this.#copyLink(page)}>
712
+ Copy example link
713
+ </button>`
714
+ : nothing}
715
+ <button type="button" class="wb-quiet" @click=${() => this.#reset()}>Reset</button>
716
+ </div>
717
+ ${link
718
+ ? html `<label class="wb-share"
719
+ >Example link <input type="url" readonly .value=${link} size="40"
720
+ /></label>`
721
+ : nothing}
722
+ <div class="wb-example-stage">
723
+ ${at('interactive-example/preview', () => html `<div class=${settings.pair ? 'wb-previews wb-pair' : 'wb-previews'}>
724
+ <div class="wb-preview">
725
+ ${settings.pair ? html `<p>Light</p>` : nothing}${this.#hosts[0]}
726
+ </div>
727
+ ${settings.pair
728
+ ? html `<div class="wb-preview">
729
+ <p>Dark</p>
730
+ ${this.#hosts[1]}
731
+ </div>`
732
+ : nothing}
733
+ </div>
734
+ <p class="wb-muted wb-example-caption">Changes appear here as you edit.</p>`)}
735
+ <p class="wb-status" role=${status.alert ? 'alert' : 'status'}>${status.text}</p>
736
+ </div>
737
+ ${tools ? this.#settingsBar(settings, update) : nothing}
738
+ <div class="wb-example-controls">
739
+ <div class="wb-controls-head">
740
+ <h3>Controls and API</h3>
741
+ <p class="wb-muted">Scroll the table; the preview stays in view.</p>
742
+ </div>
743
+ <div class="wb-controls-scroll" role="region" aria-label="Controls and API" tabindex="0">
744
+ ${at('api/properties', () => this.#apiTable(facts))}
745
+ ${at('api/slots', () => this.#slots(facts))}
746
+ ${at('api/css-properties', () => this.#cssTable(facts))}
747
+ </div>
748
+ </div>
749
+ <div class="wb-example-code" id="wb-example-code" ?hidden=${!code}>
750
+ <div class="wb-code-head">
751
+ <p class="wb-example-title">Generated HTML · follows the current controls</p>
752
+ <button type="button" @click=${() => this.#copyCode()}>Copy code</button>
753
+ </div>
754
+ <pre
755
+ class="wb-code wb-snippet"
756
+ tabindex="0"
757
+ ><code>${snippet(page, state, { settings: frameSettings })}</code></pre>
758
+ </div>
759
+ </div>
760
+ ${tools
761
+ ? html `<h3>Event log</h3>
762
+ ${this.#eventLog(facts)}`
763
+ : nothing}`;
764
+ }
765
+ /** Workbench's preview settings: mode, light and dark together, direction, and width. */
766
+ #settingsBar(settings, update) {
767
+ return html `<div class="wb-example-settings">
768
+ <div class="wb-toolbar">
769
+ <label
770
+ >Mode
771
+ <select
772
+ .value=${settings.mode}
773
+ ?disabled=${settings.pair}
774
+ @change=${(event) => update({ mode: event.target.value })}
775
+ >
776
+ <option value="site" ?selected=${settings.mode === 'site'}>Same as site</option>
777
+ <option value="light" ?selected=${settings.mode === 'light'}>Light</option>
778
+ <option value="dark" ?selected=${settings.mode === 'dark'}>Dark</option>
779
+ </select></label
780
+ >
781
+ <label
782
+ ><span>Light and dark side by side</span>
783
+ <input
784
+ type="checkbox"
785
+ .checked=${settings.pair}
786
+ @change=${(event) => update({ pair: event.target.checked })}
787
+ /></label>
788
+ <label
789
+ >Direction
790
+ <select
791
+ .value=${settings.direction}
792
+ @change=${(event) => update({
793
+ direction: event.target.value,
794
+ })}
795
+ >
796
+ <option value="ltr" ?selected=${settings.direction === 'ltr'}>Left to right</option>
797
+ <option value="rtl" ?selected=${settings.direction === 'rtl'}>Right to left</option>
798
+ </select></label
799
+ >
800
+ <label
801
+ >Width
802
+ <select
803
+ .value=${settings.width}
804
+ @change=${(event) => update({ width: event.target.value })}
805
+ >
806
+ <option value="fill" ?selected=${settings.width === 'fill'}>Fill</option>
807
+ <option value="narrow" ?selected=${settings.width === 'narrow'}>Narrow (320px)</option>
808
+ <option value="medium" ?selected=${settings.width === 'medium'}>Medium (640px)</option>
809
+ <option value="wide" ?selected=${settings.width === 'wide'}>Wide (960px)</option>
810
+ <option value="custom" ?selected=${settings.width === 'custom'}>Custom</option>
811
+ </select></label
812
+ >
813
+ ${settings.width === 'custom'
814
+ ? html `<label
815
+ >Width in px
816
+ <input
817
+ type="number"
818
+ min=${String(MIN_SHARE_WIDTH)}
819
+ max=${String(MAX_SHARE_WIDTH)}
820
+ step="1"
821
+ .value=${String(settings.customWidth)}
822
+ @change=${(event) => {
823
+ const value = Number(event.target.value);
824
+ if (Number.isInteger(value) &&
825
+ value >= MIN_SHARE_WIDTH &&
826
+ value <= MAX_SHARE_WIDTH)
827
+ update({ customWidth: value });
828
+ }}
829
+ /></label>`
830
+ : nothing}
831
+ </div>
832
+ <p class="wb-muted wb-settings-summary">
833
+ Preview only:
834
+ ${settings.pair ? 'light and dark' : settings.mode === 'site' ? 'same mode as the site' : settings.mode}
835
+ · ${settings.direction === 'rtl' ? 'right to left' : 'left to right'} ·
836
+ ${settings.width === 'fill'
837
+ ? 'fills the column'
838
+ : `${settings.width === 'custom' ? settings.customWidth : WIDTHS[settings.width]}px container`}
839
+ </p>
840
+ </div>`;
841
+ }
842
+ #eventLog(facts) {
843
+ if (!facts.events.length)
844
+ return html `<p class="wb-muted wb-event-log">No events are declared, so none are logged.</p>`;
845
+ const log = this.#log.value;
846
+ return html `<div class="wb-event-log">
847
+ <p class="wb-muted">
848
+ Logging ${facts.events.map((e, i) => html `${i ? ', ' : ''}<code>${e.name}</code>`)} (latest
849
+ first, at most ${LOG_LIMIT}).
850
+ <button type="button" ?disabled=${!log.length} @click=${() => (this.#log.value = [])}>
851
+ Clear
852
+ </button>
853
+ </p>
854
+ ${log.length
855
+ ? html `<ol class="wb-log" aria-label="Events">
856
+ ${repeat(log, (entry) => entry.id, (entry) => html `<li>
857
+ <code>${entry.name}</code> at ${entry.time.toFixed(0)} ms on
858
+ <code>${entry.target}</code> · ${entry.bubbles ? 'bubbles' : 'does not bubble'}
859
+ · ${entry.composed ? 'composed' : 'not composed'}
860
+ ${entry.detail !== undefined ? html `<br /><code>detail: ${entry.detail}</code>` : nothing}
861
+ </li>`)}
862
+ </ol>`
863
+ : html `<p class="wb-muted">No events yet.</p>`}
864
+ </div>`;
865
+ }
866
+ /** Contracts beside the controls (Figma 232:9895): events, parts, methods, states, and styling hooks. */
867
+ #reference(facts, at, inExample) {
868
+ const reference = this.layout === 'reference';
869
+ const card = (id, title, entries, name = (n) => n) => reference && !entries.length
870
+ ? nothing
871
+ : html `<div class="wb-box" data-card=${id}>
872
+ <h3>${title}</h3>
873
+ ${at(`api/${id}`, () => html `<ul class="wb-plain">
874
+ ${entries.map((entry) => html `<li>
875
+ <code>${name(entry.name)}</code
876
+ >${entry.description ? html ` — ${inline(entry.description)}` : nothing}
877
+ </li>`)}
878
+ </ul>`)}
879
+ </div>`;
880
+ return html `${inExample
881
+ ? html `<p class="wb-lede">
882
+ Contracts that complement the properties and slots table above.
883
+ </p>`
884
+ : html `${at('api/properties', () => this.#apiTable(facts))}
885
+ ${at('api/slots', () => this.#slots(facts))}
886
+ ${at('api/css-properties', () => this.#cssTable(facts))}`}
887
+ <div class="wb-boxes">
888
+ ${card('events', 'Events', facts.events)} ${card('parts', 'CSS parts', facts.parts)}
889
+ ${card('methods', 'Methods', facts.methods)}
890
+ ${card('css-states', 'Custom states', facts.cssStates, (n) => `:state(${n})`)}
891
+ </div>
892
+ ${facts.cssProperties.length
893
+ ? html `<div class="wb-box wb-sunken" data-card="css-properties">
894
+ <h3>CSS custom properties</h3>
895
+ <ul class="wb-plain wb-names">
896
+ ${facts.cssProperties.map((entry) => html `<li><code>${entry.name}</code></li>`)}
897
+ </ul>
898
+ </div>`
899
+ : nothing}`;
900
+ }
901
+ #apiTable(facts) {
902
+ return this.#group('api', 'Editing a value updates the preview: one write per change, as a property or as an attribute. Reflected attributes are not written back.', ['Properties', 'Types', 'Default', 'Attribute', 'Reflected', 'Control / current value'], facts.rows.map((row) => this.#apiRow(row)));
903
+ }
904
+ #apiRow(row) {
905
+ return html `<tr data-row=${row.id}>
906
+ ${named(row.property ?? row.attribute ?? '', row.description)}
907
+ <td data-label="Types">
908
+ ${row.values ? row.values.join(', ') : row.type}${row.values && row.nullable ? ' · nullable' : ''}
909
+ </td>
910
+ <td data-label="Default">${row.default ? unquote(row.default) : 'Unknown'}</td>
911
+ <td data-label="Attribute">
912
+ ${row.attribute ? (row.property ? row.attribute : `${row.attribute} (attribute only)`) : row.propertyOnly ? 'Property only' : 'None'}
913
+ </td>
914
+ <td data-label="Reflected">
915
+ ${row.property && row.attribute ? (row.reflects ? 'Yes' : 'No') : '—'}
916
+ </td>
917
+ <td data-label="Control">${this.#control(row)}</td>
918
+ </tr>`;
919
+ }
920
+ #control(row) {
921
+ const name = row.property ?? row.attribute ?? '';
922
+ if (row.control === 'readonly')
923
+ return html `<span class="wb-muted">Read-only; inspected, never edited.</span>`;
924
+ if (row.control === 'fixture')
925
+ return html `<span class="wb-muted"
926
+ >Requires fixture: <code>${row.type}</code> is not editable generically.</span
927
+ >`;
928
+ const state = this.#state.value;
929
+ const modes = this.#modes.value;
930
+ const mode = modes[row.id] ?? defaultWriteMode(row);
931
+ const error = this.#errors.value[row.id];
932
+ const label = `${name} (${mode})`;
933
+ const attr = row.attribute;
934
+ const prop = row.property;
935
+ const attrValue = attr ? state.attributes[attr] : undefined;
936
+ const propSet = prop !== undefined && prop in state.properties;
937
+ const propValue = prop !== undefined ? state.properties[prop] : undefined;
938
+ const modeSwitch = this.layout === 'workbench' &&
939
+ row.property &&
940
+ row.attribute &&
941
+ !row.propertyOnly &&
942
+ row.control !== 'json'
943
+ ? html `<select
944
+ aria-label=${`Write ${name} as`}
945
+ .value=${mode}
946
+ @change=${(event) => {
947
+ this.#modes.value = {
948
+ ...this.#modes.peek(),
949
+ [row.id]: event.target.value,
950
+ };
951
+ }}
952
+ >
953
+ <option value="attribute" ?selected=${mode === 'attribute'}>attribute</option>
954
+ <option value="property" ?selected=${mode === 'property'}>property</option>
955
+ </select>`
956
+ : nothing;
957
+ let control;
958
+ if (row.control === 'boolean') {
959
+ const checked = mode === 'attribute' ? attrValue !== undefined : propValue === true;
960
+ control = html `<input
961
+ type="checkbox"
962
+ role="switch"
963
+ class="wb-switch"
964
+ aria-label=${label}
965
+ .checked=${checked}
966
+ @change=${(event) => {
967
+ const on = event.target.checked;
968
+ if (mode === 'attribute')
969
+ this.#writeAttribute(row, on ? '' : null);
970
+ else
971
+ this.#writeProperty(row, on);
972
+ }}
973
+ />
974
+ <span class="wb-muted"
975
+ >${mode === 'attribute'
976
+ ? attrValue !== undefined
977
+ ? 'present'
978
+ : 'absent'
979
+ : propSet
980
+ ? String(propValue)
981
+ : 'unset'}</span
982
+ >`;
983
+ }
984
+ else if (row.control === 'enum') {
985
+ const current = mode === 'attribute'
986
+ ? (attrValue ?? '')
987
+ : propSet
988
+ ? propValue === null
989
+ ? '\u0000null'
990
+ : String(propValue)
991
+ : '';
992
+ const write = (value) => {
993
+ if (mode === 'attribute')
994
+ this.#writeAttribute(row, value === '' ? null : value);
995
+ else if (value === '')
996
+ this.#unsetProperty(row);
997
+ else
998
+ this.#writeProperty(row, value === '\u0000null' ? null : value);
999
+ };
1000
+ const values = row.values ?? [];
1001
+ control = html `<select
1002
+ aria-label=${label}
1003
+ .value=${current}
1004
+ @change=${(event) => write(event.target.value)}
1005
+ >
1006
+ <option value="" ?selected=${current === ''}>(unset)</option>
1007
+ ${values.map((value) => html `<option value=${value} ?selected=${current === value}>${value}</option>`)}
1008
+ ${mode === 'property' && row.nullable
1009
+ ? html `<option value=${'\u0000null'} ?selected=${current === '\u0000null'}>
1010
+ null
1011
+ </option>`
1012
+ : nothing}
1013
+ </select>`;
1014
+ }
1015
+ else if (row.control === 'json') {
1016
+ control = html `<textarea
1017
+ aria-label=${label}
1018
+ rows="3"
1019
+ .value=${propSet ? JSON.stringify(propValue) : ''}
1020
+ placeholder="unset"
1021
+ @change=${(event) => {
1022
+ const text = event.target.value.trim();
1023
+ if (!text) {
1024
+ this.#unsetProperty(row);
1025
+ return;
1026
+ }
1027
+ try {
1028
+ const value = JSON.parse(text);
1029
+ if (!Array.isArray(value) && !(value === null && row.nullable))
1030
+ throw new Error('expected a JSON array');
1031
+ this.#writeProperty(row, value);
1032
+ }
1033
+ catch (cause) {
1034
+ this.#setError(row.id, `Not written: ${message(cause)}.`);
1035
+ }
1036
+ }}
1037
+ ></textarea>`;
1038
+ }
1039
+ else {
1040
+ const number = row.control === 'number';
1041
+ const isSet = mode === 'attribute' ? attrValue !== undefined : propSet;
1042
+ const shown = mode === 'attribute'
1043
+ ? (attrValue ?? '')
1044
+ : propSet && propValue !== null
1045
+ ? String(propValue)
1046
+ : '';
1047
+ control = html `<input
1048
+ type=${number ? 'number' : 'text'}
1049
+ aria-label=${label}
1050
+ placeholder=${isSet ? '' : 'unset'}
1051
+ .value=${shown}
1052
+ @input=${(event) => {
1053
+ // The preview follows the field as the reader types; an unfinished number waits.
1054
+ const text = event.target.value;
1055
+ if (this.#textProblem(number, mode, text))
1056
+ return;
1057
+ if (mode === 'attribute')
1058
+ this.#writeAttribute(row, text);
1059
+ else
1060
+ this.#writeProperty(row, number ? Number(text) : text);
1061
+ }}
1062
+ @change=${(event) => {
1063
+ const problem = this.#textProblem(number, mode, event.target.value);
1064
+ if (problem)
1065
+ this.#setError(row.id, problem);
1066
+ }}
1067
+ />
1068
+ <button
1069
+ type="button"
1070
+ ?disabled=${!isSet}
1071
+ @click=${() => {
1072
+ if (mode === 'attribute')
1073
+ this.#writeAttribute(row, null);
1074
+ else
1075
+ this.#unsetProperty(row);
1076
+ }}
1077
+ >
1078
+ Unset
1079
+ </button>
1080
+ ${mode === 'property' && row.nullable
1081
+ ? html `<button
1082
+ type="button"
1083
+ ?disabled=${propSet && propValue === null}
1084
+ @click=${() => this.#writeProperty(row, null)}
1085
+ >
1086
+ Set null
1087
+ </button>`
1088
+ : nothing}
1089
+ <span class="wb-muted"
1090
+ >${!isSet
1091
+ ? 'unset'
1092
+ : mode === 'property' && propValue === null
1093
+ ? 'null'
1094
+ : shown === ''
1095
+ ? 'empty string'
1096
+ : ''}</span
1097
+ >`;
1098
+ }
1099
+ return html `<div class="wb-cell-controls">${control} ${modeSwitch}</div>
1100
+ ${error ? html `<p class="wb-error" role="alert">${error}</p>` : nothing}`;
1101
+ }
1102
+ /** One group of the controls: its header row names the group and its columns, and stays in view. */
1103
+ #group(id, caption, heads, rows) {
1104
+ return html `<div class="wb-table-wrap">
1105
+ <table class="wb-stack wb-api" data-table=${id}>
1106
+ <caption class="wb-visually-hidden">
1107
+ ${caption}
1108
+ </caption>
1109
+ <colgroup>
1110
+ ${[0, 1, 2, 3, 4, 5].map(() => html `<col />`)}
1111
+ </colgroup>
1112
+ <thead>
1113
+ <tr>
1114
+ ${heads.map((head, i) => html `<th scope="col" colspan=${heads.length === 5 && i === 3 ? '2' : null}>
1115
+ ${head}
1116
+ </th>`)}
1117
+ </tr>
1118
+ </thead>
1119
+ <tbody>
1120
+ ${rows}
1121
+ </tbody>
1122
+ </table>
1123
+ </div>`;
1124
+ }
1125
+ #slots(facts) {
1126
+ const state = this.#state.value;
1127
+ return this.#group('slots', 'Slots: text content written into the preview.', ['Slots', 'Content type', 'Default', 'Name', 'Position', 'Control / current content'], facts.slots.map((slot) => {
1128
+ const label = slot.name || 'default';
1129
+ return html `<tr data-slot=${slot.name}>
1130
+ ${named(label, undefined)}
1131
+ <td data-label="Content type">Text</td>
1132
+ <td data-label="Default">Empty</td>
1133
+ <td data-label="Name">${slot.name || '(none)'}</td>
1134
+ <td data-label="Position">
1135
+ ${slot.description ? inline(slot.description) : html `<span class="wb-not-documented">Not documented</span>`}
1136
+ </td>
1137
+ <td data-label="Control">
1138
+ <input
1139
+ type="text"
1140
+ aria-label=${`${label} slot text`}
1141
+ .value=${state.slots[slot.name] ?? ''}
1142
+ @input=${(event) => this.#writeSlot(slot.name, event.target.value)}
1143
+ />
1144
+ </td>
1145
+ </tr>`;
1146
+ }));
1147
+ }
1148
+ #cssTable(facts) {
1149
+ const styles = this.#state.value.styles ?? {};
1150
+ return this.#group('css-properties', "CSS custom properties: values set on the preview element's style.", ['CSS custom properties', 'Syntax', 'Default', 'Description', 'Control / current value'], facts.cssProperties.map((entry) => {
1151
+ const value = styles[entry.name];
1152
+ return html `<tr data-css=${entry.name}>
1153
+ ${named(entry.name, undefined)}
1154
+ <td data-label="Syntax">${entry.type ?? 'Unknown'}</td>
1155
+ <td data-label="Default">${entry.default ?? 'Unknown'}</td>
1156
+ <td data-label="Description" colspan="2">
1157
+ ${entry.description ? inline(entry.description) : html `<span class="wb-not-documented">Not documented</span>`}
1158
+ </td>
1159
+ <td data-label="Control">
1160
+ ${isCustomPropertyName(entry.name)
1161
+ ? html `<div class="wb-cell-controls">
1162
+ <input
1163
+ type="text"
1164
+ aria-label=${`${entry.name} value`}
1165
+ placeholder="unset"
1166
+ .value=${value ?? ''}
1167
+ @input=${(event) => {
1168
+ const text = event.target.value;
1169
+ this.#writeStyle(entry.name, text === '' ? null : text);
1170
+ }}
1171
+ />
1172
+ <button
1173
+ type="button"
1174
+ ?disabled=${value === undefined}
1175
+ @click=${() => this.#writeStyle(entry.name, null)}
1176
+ >
1177
+ Unset
1178
+ </button>
1179
+ </div>`
1180
+ : html `<span class="wb-muted">Not editable: not a custom property name.</span>`}
1181
+ </td>
1182
+ </tr>`;
1183
+ }));
1184
+ }
1185
+ /** The published examples as cards (Figma 20:2266), each with a live specimen when the host gives one. */
1186
+ #examples(page) {
1187
+ const preview = this.examplePreview;
1188
+ const tools = this.layout === 'workbench';
1189
+ const shown = presets(page).filter((e) => !e.incorrect && (tools || !e.requiresFixture));
1190
+ // Presets lead; the reference layout shows four and keeps the rest one action away.
1191
+ const ordered = [...shown.filter((e) => e.preset), ...shown.filter((e) => !e.preset)];
1192
+ const all = tools || this.#allExamples.value;
1193
+ return html `<p class="wb-lede">Compare the published examples without leaving the page.</p>
1194
+ <ul class="wb-boxes wb-examples" role="list">
1195
+ ${(all ? ordered : ordered.slice(0, 4)).map((entry) => {
1196
+ const node = preview?.(page, entry.id);
1197
+ return html `<li class="wb-box" data-example=${entry.id}>
1198
+ ${node ? html `<div class="wb-specimen">${node}</div>` : nothing}
1199
+ <div class="wb-box-body">
1200
+ <h3>${entry.label}</h3>
1201
+ ${entry.guidance ? html `<p>${inline(entry.guidance)}</p>` : nothing}
1202
+ ${entry.requiresFixture
1203
+ ? html `<p class="wb-muted">
1204
+ Requires fixture: rendered by its example module only.
1205
+ </p>`
1206
+ : !tools
1207
+ ? nothing
1208
+ : entry.state
1209
+ ? html `<button type="button" @click=${() => this.#applyPreset(entry)}>
1210
+ Show in preview
1211
+ </button>`
1212
+ : html `<p class="wb-muted">
1213
+ Values not published (the example module was not loaded).
1214
+ </p>`}
1215
+ </div>
1216
+ </li>`;
1217
+ })}
1218
+ </ul>
1219
+ ${!tools && ordered.length > 4
1220
+ ? html `<button
1221
+ type="button"
1222
+ class="wb-more"
1223
+ aria-expanded=${all ? 'true' : 'false'}
1224
+ @click=${() => (this.#allExamples.value = !all)}
1225
+ >
1226
+ ${all ? 'Show fewer examples' : `Show all ${ordered.length} examples`}
1227
+ </button>`
1228
+ : nothing}`;
1229
+ }
1230
+ #tokens(page, at) {
1231
+ const mappings = Array.isArray(page.enrichment?.['styleMappings'])
1232
+ ? page.enrichment['styleMappings'].filter(isRecord)
1233
+ : [];
1234
+ const reads = Array.isArray(page.enrichment?.['tokenReads'])
1235
+ ? page.enrichment['tokenReads'].filter(isRecord)
1236
+ : [];
1237
+ const resolved = new Map((page.tokens ?? []).map((t) => [t.name, t]));
1238
+ const theme = this.index?.libraries.length ? 'the generated baseline theme' : 'the baseline';
1239
+ return html `<h3>From property to token</h3>
1240
+ ${at('tokens/style-mappings', () => html `<div class="wb-table-wrap">
1241
+ <table class="wb-stack" data-table="style-mappings">
1242
+ <caption>
1243
+ Declared style mappings extracted from the component's stylesheets.
1244
+ </caption>
1245
+ <thead>
1246
+ <tr>
1247
+ <th scope="col">When</th>
1248
+ <th scope="col">Part</th>
1249
+ <th scope="col">CSS property</th>
1250
+ <th scope="col">Token</th>
1251
+ </tr>
1252
+ </thead>
1253
+ <tbody>
1254
+ ${mappings.map((m) => html `<tr>
1255
+ <td data-label="When"><code>${String(m['when'] ?? 'default')}</code></td>
1256
+ <td data-label="Part">
1257
+ ${m['part'] ? html `<code>${String(m['part'])}</code>` : 'host'}
1258
+ </td>
1259
+ <td data-label="CSS property">
1260
+ <code>${String(m['cssProperty'] ?? '')}</code>
1261
+ </td>
1262
+ <td data-label="Token"><code>${String(m['token'] ?? '')}</code></td>
1263
+ </tr>`)}
1264
+ </tbody>
1265
+ </table>
1266
+ </div>`)}
1267
+ <h3>Every token read</h3>
1268
+ ${at('tokens/token-reads', () => html `<div class="wb-table-wrap">
1269
+ <table class="wb-stack" data-table="token-reads">
1270
+ <caption>
1271
+ ${resolved.size} tokens read · values for ${theme} (not a live preview reading)
1272
+ </caption>
1273
+ <thead>
1274
+ <tr>
1275
+ <th scope="col">Token</th>
1276
+ <th scope="col">Reference</th>
1277
+ <th scope="col">Read through</th>
1278
+ <th scope="col">Value</th>
1279
+ </tr>
1280
+ </thead>
1281
+ <tbody>
1282
+ ${[...new Map(reads.map((r) => [String(r['token']), r])).values()].map((read) => {
1283
+ const name = String(read['token']);
1284
+ const token = resolved.get(name);
1285
+ const definition = token &&
1286
+ typeof token.definition === 'string' &&
1287
+ token.definition.startsWith('{')
1288
+ ? token.definition
1289
+ : undefined;
1290
+ return html `<tr>
1291
+ <td data-label="Token">
1292
+ <code>${name}</code>
1293
+ ${token?.cssVariable ? html `<small><code>${token.cssVariable}</code></small>` : nothing}
1294
+ </td>
1295
+ <td data-label="Reference">${String(read['kind'] ?? 'declared')}</td>
1296
+ <td data-label="Read through">${String(read['readThrough'] ?? 'Unknown')}</td>
1297
+ <td data-label="Value">
1298
+ ${token && !('missing' in token) && token.value !== undefined
1299
+ ? html `<code>${String(token.value)}</code
1300
+ >${definition ? html ` <small><code>${definition}</code></small>` : nothing}`
1301
+ : 'Unknown'}
1302
+ </td>
1303
+ </tr>`;
1304
+ })}
1305
+ </tbody>
1306
+ </table>
1307
+ </div>`)}`;
1308
+ }
1309
+ /** Accessibility as two cards (Figma 20:2292): behavior, then keyboard and guidance. */
1310
+ #behavior(page, at) {
1311
+ const card = (title, key) => {
1312
+ const items = authoredValues(page.enrichment?.[key]);
1313
+ return this.layout === 'reference' && !items.length
1314
+ ? nothing
1315
+ : html `<div class="wb-box">
1316
+ <h3>${title}</h3>
1317
+ ${at(`behavior-and-accessibility/${key}`, () => bullets(items))}
1318
+ </div>`;
1319
+ };
1320
+ return html `<div class="wb-boxes">
1321
+ ${card('Behavior', 'a11y')} ${card('Keyboard and guidance', 'keyboard')}
1322
+ </div>`;
1323
+ }
1324
+ /** Where the element lives and how to register it (Figma 232:9845). */
1325
+ #source(page, at) {
1326
+ const definition = isRecord(page.enrichment?.['definition'])
1327
+ ? page.enrichment['definition']
1328
+ : undefined;
1329
+ const ref = isRecord(definition?.['ref']) ? definition['ref'] : undefined;
1330
+ const field = (label, ...values) => html `<div class="wb-fact">
1331
+ <span class="wb-fact-label">${label}</span>
1332
+ ${values.map((value) => html `<code
1333
+ >${String(value)
1334
+ .split('/')
1335
+ .map((part, i) => (i ? html `/<wbr />${part}` : part))}</code
1336
+ >`)}
1337
+ </div>`;
1338
+ return html `<p class="wb-lede">
1339
+ Where the element is published, so it is easy to locate, import, and verify.
1340
+ </p>
1341
+ ${at('source/module', () => html `<div class="wb-facts">
1342
+ ${field('Package', page.package)} ${field('Module', page.module)}
1343
+ ${field('Class', page.declaration['name'] ?? 'Unknown')}
1344
+ ${ref ? field('Definition', ref['name'], ref['module']) : nothing}
1345
+ </div>`)}
1346
+ ${at('source/register', () => html `<div class="wb-fact wb-register">
1347
+ <span class="wb-fact-label">Register</span>
1348
+ <code>import '${String(registrationOf(page))}';</code>
1349
+ </div>`)}`;
1350
+ }
1351
+ }
1352
+ //# sourceMappingURL=component-detail.js.map