@oddkin/workbench 0.0.0-stage → 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +36 -2
- package/dist/app/copy.d.ts +43 -0
- package/dist/app/copy.d.ts.map +1 -0
- package/dist/app/copy.js +47 -0
- package/dist/app/copy.js.map +1 -0
- package/dist/app/define.d.ts +3 -0
- package/dist/app/define.d.ts.map +1 -0
- package/dist/app/define.js +10 -0
- package/dist/app/define.js.map +1 -0
- package/dist/app/details.d.ts +22 -0
- package/dist/app/details.d.ts.map +1 -0
- package/dist/app/details.js +16 -0
- package/dist/app/details.js.map +1 -0
- package/dist/app/index.d.ts +12 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +9 -0
- package/dist/app/index.js.map +1 -0
- package/dist/app/routes.d.ts +26 -0
- package/dist/app/routes.d.ts.map +1 -0
- package/dist/app/routes.js +94 -0
- package/dist/app/routes.js.map +1 -0
- package/dist/app/search.d.ts +17 -0
- package/dist/app/search.d.ts.map +1 -0
- package/dist/app/search.js +52 -0
- package/dist/app/search.js.map +1 -0
- package/dist/app/shell.d.ts +29 -0
- package/dist/app/shell.d.ts.map +1 -0
- package/dist/app/shell.js +435 -0
- package/dist/app/shell.js.map +1 -0
- package/dist/app/styles.d.ts +2 -0
- package/dist/app/styles.d.ts.map +1 -0
- package/dist/app/styles.js +373 -0
- package/dist/app/styles.js.map +1 -0
- package/dist/app/theme-studio.d.ts +12 -0
- package/dist/app/theme-studio.d.ts.map +1 -0
- package/dist/app/theme-studio.js +14 -0
- package/dist/app/theme-studio.js.map +1 -0
- package/dist/app/views.d.ts +34 -0
- package/dist/app/views.d.ts.map +1 -0
- package/dist/app/views.js +230 -0
- package/dist/app/views.js.map +1 -0
- package/dist/config/index.d.ts +170 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +148 -0
- package/dist/config/index.js.map +1 -0
- package/dist/conformance/button.d.ts +2 -0
- package/dist/conformance/button.d.ts.map +1 -0
- package/dist/conformance/button.js +233 -0
- package/dist/conformance/button.js.map +1 -0
- package/dist/conformance/index.d.ts +107 -0
- package/dist/conformance/index.d.ts.map +1 -0
- package/dist/conformance/index.js +60 -0
- package/dist/conformance/index.js.map +1 -0
- package/dist/data/build.d.ts +36 -0
- package/dist/data/build.d.ts.map +1 -0
- package/dist/data/build.js +248 -0
- package/dist/data/build.js.map +1 -0
- package/dist/data/import-map.d.ts +53 -0
- package/dist/data/import-map.d.ts.map +1 -0
- package/dist/data/import-map.js +160 -0
- package/dist/data/import-map.js.map +1 -0
- package/dist/data/index.d.ts +14 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +10 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/theme-studio.d.ts +21 -0
- package/dist/data/theme-studio.d.ts.map +1 -0
- package/dist/data/theme-studio.js +116 -0
- package/dist/data/theme-studio.js.map +1 -0
- package/dist/data/types.d.ts +169 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/data/types.js +4 -0
- package/dist/data/types.js.map +1 -0
- package/dist/examples/index.d.ts +101 -0
- package/dist/examples/index.d.ts.map +1 -0
- package/dist/examples/index.js +106 -0
- package/dist/examples/index.js.map +1 -0
- package/dist/frame/index.d.ts +9 -0
- package/dist/frame/index.d.ts.map +1 -0
- package/dist/frame/index.js +468 -0
- package/dist/frame/index.js.map +1 -0
- package/dist/internal/diagnostics.d.ts +17 -0
- package/dist/internal/diagnostics.d.ts.map +1 -0
- package/dist/internal/diagnostics.js +19 -0
- package/dist/internal/diagnostics.js.map +1 -0
- package/dist/node/bin.d.ts +3 -0
- package/dist/node/bin.d.ts.map +1 -0
- package/dist/node/bin.js +12 -0
- package/dist/node/bin.js.map +1 -0
- package/dist/node/cli.d.ts +17 -0
- package/dist/node/cli.d.ts.map +1 -0
- package/dist/node/cli.js +198 -0
- package/dist/node/cli.js.map +1 -0
- package/dist/node/config.d.ts +28 -0
- package/dist/node/config.d.ts.map +1 -0
- package/dist/node/config.js +80 -0
- package/dist/node/config.js.map +1 -0
- package/dist/node/generated.d.ts +19 -0
- package/dist/node/generated.d.ts.map +1 -0
- package/dist/node/generated.js +109 -0
- package/dist/node/generated.js.map +1 -0
- package/dist/node/html.d.ts +35 -0
- package/dist/node/html.d.ts.map +1 -0
- package/dist/node/html.js +51 -0
- package/dist/node/html.js.map +1 -0
- package/dist/node/index.d.ts +24 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +15 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/inputs.d.ts +24 -0
- package/dist/node/inputs.d.ts.map +1 -0
- package/dist/node/inputs.js +259 -0
- package/dist/node/inputs.js.map +1 -0
- package/dist/node/packages.d.ts +35 -0
- package/dist/node/packages.d.ts.map +1 -0
- package/dist/node/packages.js +87 -0
- package/dist/node/packages.js.map +1 -0
- package/dist/node/preload.d.ts +10 -0
- package/dist/node/preload.d.ts.map +1 -0
- package/dist/node/preload.js +61 -0
- package/dist/node/preload.js.map +1 -0
- package/dist/node/server.d.ts +42 -0
- package/dist/node/server.d.ts.map +1 -0
- package/dist/node/server.js +369 -0
- package/dist/node/server.js.map +1 -0
- package/dist/node/static.d.ts +33 -0
- package/dist/node/static.d.ts.map +1 -0
- package/dist/node/static.js +141 -0
- package/dist/node/static.js.map +1 -0
- package/dist/node/workbench.d.ts +43 -0
- package/dist/node/workbench.d.ts.map +1 -0
- package/dist/node/workbench.js +183 -0
- package/dist/node/workbench.js.map +1 -0
- package/dist/pages/build.d.ts +22 -0
- package/dist/pages/build.d.ts.map +1 -0
- package/dist/pages/build.js +363 -0
- package/dist/pages/build.js.map +1 -0
- package/dist/pages/defaults.d.ts +16 -0
- package/dist/pages/defaults.d.ts.map +1 -0
- package/dist/pages/defaults.js +88 -0
- package/dist/pages/defaults.js.map +1 -0
- package/dist/pages/index.d.ts +14 -0
- package/dist/pages/index.d.ts.map +1 -0
- package/dist/pages/index.js +10 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/pages/navigation.d.ts +48 -0
- package/dist/pages/navigation.d.ts.map +1 -0
- package/dist/pages/navigation.js +83 -0
- package/dist/pages/navigation.js.map +1 -0
- package/dist/pages/types.d.ts +143 -0
- package/dist/pages/types.d.ts.map +1 -0
- package/dist/pages/types.js +3 -0
- package/dist/pages/types.js.map +1 -0
- package/dist/panels/catalog-define.d.ts +3 -0
- package/dist/panels/catalog-define.d.ts.map +1 -0
- package/dist/panels/catalog-define.js +7 -0
- package/dist/panels/catalog-define.js.map +1 -0
- package/dist/panels/catalog.d.ts +33 -0
- package/dist/panels/catalog.d.ts.map +1 -0
- package/dist/panels/catalog.js +54 -0
- package/dist/panels/catalog.js.map +1 -0
- package/dist/panels/component-define.d.ts +3 -0
- package/dist/panels/component-define.d.ts.map +1 -0
- package/dist/panels/component-define.js +6 -0
- package/dist/panels/component-define.js.map +1 -0
- package/dist/panels/component-detail.d.ts +46 -0
- package/dist/panels/component-detail.d.ts.map +1 -0
- package/dist/panels/component-detail.js +1120 -0
- package/dist/panels/component-detail.js.map +1 -0
- package/dist/panels/component.d.ts +17 -0
- package/dist/panels/component.d.ts.map +1 -0
- package/dist/panels/component.js +10 -0
- package/dist/panels/component.js.map +1 -0
- package/dist/panels/foundation-define.d.ts +3 -0
- package/dist/panels/foundation-define.d.ts.map +1 -0
- package/dist/panels/foundation-define.js +6 -0
- package/dist/panels/foundation-define.js.map +1 -0
- package/dist/panels/foundation-detail.d.ts +23 -0
- package/dist/panels/foundation-detail.d.ts.map +1 -0
- package/dist/panels/foundation-detail.js +216 -0
- package/dist/panels/foundation-detail.js.map +1 -0
- package/dist/panels/foundation.d.ts +15 -0
- package/dist/panels/foundation.d.ts.map +1 -0
- package/dist/panels/foundation.js +9 -0
- package/dist/panels/foundation.js.map +1 -0
- package/dist/panels/model.d.ts +173 -0
- package/dist/panels/model.d.ts.map +1 -0
- package/dist/panels/model.js +462 -0
- package/dist/panels/model.js.map +1 -0
- package/dist/panels/styles.d.ts +3 -0
- package/dist/panels/styles.d.ts.map +1 -0
- package/dist/panels/styles.js +316 -0
- package/dist/panels/styles.js.map +1 -0
- package/dist/panels/theme-model.d.ts +49 -0
- package/dist/panels/theme-model.d.ts.map +1 -0
- package/dist/panels/theme-model.js +146 -0
- package/dist/panels/theme-model.js.map +1 -0
- package/dist/panels/theme-studio.d.ts +32 -0
- package/dist/panels/theme-studio.d.ts.map +1 -0
- package/dist/panels/theme-studio.js +312 -0
- package/dist/panels/theme-studio.js.map +1 -0
- package/dist/preview/frame.d.ts +44 -0
- package/dist/preview/frame.d.ts.map +1 -0
- package/dist/preview/frame.js +250 -0
- package/dist/preview/frame.js.map +1 -0
- package/dist/preview/index.d.ts +26 -0
- package/dist/preview/index.d.ts.map +1 -0
- package/dist/preview/index.js +58 -0
- package/dist/preview/index.js.map +1 -0
- package/dist/preview/theme-generation.d.ts +33 -0
- package/dist/preview/theme-generation.d.ts.map +1 -0
- package/dist/preview/theme-generation.js +48 -0
- package/dist/preview/theme-generation.js.map +1 -0
- package/dist/protocol/index.d.ts +207 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +261 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/testing/index.d.ts +47 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +145 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +118 -4
|
@@ -0,0 +1,1120 @@
|
|
|
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 } from '../preview/frame.js';
|
|
12
|
+
import { authoredValues, componentFacts, decodeShare, defaultWriteMode, encodeShare, initialState, intendedUse, pageCoverage, presets, usagePairs, MAX_SHARE_WIDTH, MIN_SHARE_WIDTH, SHARE_PARAM, sharePayload, snippet, } from './model.js';
|
|
13
|
+
import { contentStyles, panelHostStyles } from './styles.js';
|
|
14
|
+
const LOG_LIMIT = 50;
|
|
15
|
+
const WIDTHS = {
|
|
16
|
+
narrow: 320,
|
|
17
|
+
medium: 640,
|
|
18
|
+
wide: 960,
|
|
19
|
+
};
|
|
20
|
+
const DEFAULT_SETTINGS = {
|
|
21
|
+
mode: 'site',
|
|
22
|
+
pair: false,
|
|
23
|
+
direction: 'ltr',
|
|
24
|
+
width: 'fill',
|
|
25
|
+
customWidth: 480,
|
|
26
|
+
};
|
|
27
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28
|
+
const message = (error) => (error instanceof Error ? error.message : String(error));
|
|
29
|
+
/** Text with Markdown code spans shown as code; everything stays text, never markup. */
|
|
30
|
+
function inline(text) {
|
|
31
|
+
const parts = text.split('`');
|
|
32
|
+
if (parts.length % 2 === 0)
|
|
33
|
+
parts[parts.length - 2] += `\`${parts.pop()}`;
|
|
34
|
+
return parts.map((part, i) => (i % 2 ? html `<code>${part}</code>` : part));
|
|
35
|
+
}
|
|
36
|
+
const notDocumented = (key, subject) => {
|
|
37
|
+
const lead = `${subject ? `${subject}: not documented` : 'Not documented'}. Supplied by `;
|
|
38
|
+
return html `<p class="wb-not-documented">${lead}<code>${key}</code>.</p>`;
|
|
39
|
+
};
|
|
40
|
+
const bullets = (items) => html `<ul class="wb-list">
|
|
41
|
+
${items.map((item) => html `<li>${inline(item)}</li>`)}
|
|
42
|
+
</ul>`;
|
|
43
|
+
function covered(coverage) {
|
|
44
|
+
const byId = new Map(coverage.map((item) => [item.id, item]));
|
|
45
|
+
return (id, content) => {
|
|
46
|
+
const item = byId.get(id);
|
|
47
|
+
if (!item)
|
|
48
|
+
return nothing;
|
|
49
|
+
return html `<div data-coverage=${id} data-state=${item.state}>
|
|
50
|
+
${item.state === 'documented'
|
|
51
|
+
? content()
|
|
52
|
+
: item.state === 'none'
|
|
53
|
+
? html `<p class="wb-muted">${item.empty}</p>`
|
|
54
|
+
: notDocumented(item.source ?? '', item.subject)}
|
|
55
|
+
</div>`;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export class ComponentDetailElement extends OddkinElement.with({
|
|
59
|
+
page: prop.object({ description: 'The component page data.' }),
|
|
60
|
+
index: prop.object({ description: 'The Workbench index.' }),
|
|
61
|
+
href: prop.custom({
|
|
62
|
+
default: () => (route) => route,
|
|
63
|
+
coerce: (value) => typeof value === 'function'
|
|
64
|
+
? { ok: true, value: value }
|
|
65
|
+
: { ok: false, reason: 'expected a function' },
|
|
66
|
+
description: 'Maps a Workbench route to a URL.',
|
|
67
|
+
}),
|
|
68
|
+
frameUrl: prop.string({
|
|
69
|
+
default: '',
|
|
70
|
+
attribute: false,
|
|
71
|
+
description: 'URL of the preview frame document.',
|
|
72
|
+
}),
|
|
73
|
+
}) {
|
|
74
|
+
static styles = [panelHostStyles, contentStyles];
|
|
75
|
+
#state = signal({
|
|
76
|
+
preset: 'default',
|
|
77
|
+
attributes: {},
|
|
78
|
+
properties: {},
|
|
79
|
+
slots: {},
|
|
80
|
+
});
|
|
81
|
+
#modes = signal({});
|
|
82
|
+
#errors = signal({});
|
|
83
|
+
#settings = signal(DEFAULT_SETTINGS);
|
|
84
|
+
#log = signal([]);
|
|
85
|
+
#status = signal({
|
|
86
|
+
text: '',
|
|
87
|
+
alert: false,
|
|
88
|
+
});
|
|
89
|
+
#link = signal('');
|
|
90
|
+
#hosts;
|
|
91
|
+
#previews = [];
|
|
92
|
+
#previewScope;
|
|
93
|
+
#key = '';
|
|
94
|
+
#logId = 0;
|
|
95
|
+
constructor() {
|
|
96
|
+
super();
|
|
97
|
+
this.#hosts = [document.createElement('div'), document.createElement('div')];
|
|
98
|
+
}
|
|
99
|
+
/** Current preview configuration (for tests and embedding); read-only snapshot. */
|
|
100
|
+
get previewState() {
|
|
101
|
+
return this.#state.peek();
|
|
102
|
+
}
|
|
103
|
+
/** Live preview frames, in order (light first when shown side by side). */
|
|
104
|
+
get previews() {
|
|
105
|
+
return this.#previews;
|
|
106
|
+
}
|
|
107
|
+
connected(scope) {
|
|
108
|
+
scope.effect(() => {
|
|
109
|
+
const page = this.page;
|
|
110
|
+
const frameUrl = this.frameUrl;
|
|
111
|
+
untracked(() => this.#sync(page, frameUrl));
|
|
112
|
+
});
|
|
113
|
+
scope.add(() => {
|
|
114
|
+
this.#key = '';
|
|
115
|
+
this.#stop();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/** Restart the preview only when the component, registration, or frame document changes. */
|
|
119
|
+
#sync(page, frameUrl) {
|
|
120
|
+
const registration = page?.enrichment?.['registration'];
|
|
121
|
+
const key = page ? `${page.tag}\n${String(registration)}\n${frameUrl}` : '';
|
|
122
|
+
if (key === this.#key)
|
|
123
|
+
return;
|
|
124
|
+
const sameTag = this.#key.split('\n')[0] === page?.tag;
|
|
125
|
+
this.#key = key;
|
|
126
|
+
if (!page) {
|
|
127
|
+
this.#stop();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!sameTag) {
|
|
131
|
+
this.#modes.value = {};
|
|
132
|
+
this.#errors.value = {};
|
|
133
|
+
this.#log.value = [];
|
|
134
|
+
this.#link.value = '';
|
|
135
|
+
this.#state.value = this.#fromLocation(page) ?? initialState(page);
|
|
136
|
+
}
|
|
137
|
+
this.#restart();
|
|
138
|
+
}
|
|
139
|
+
#fromLocation(page) {
|
|
140
|
+
const location = this.ownerDocument.defaultView?.location;
|
|
141
|
+
const hashQuery = location?.hash.includes('?')
|
|
142
|
+
? location.hash.slice(location.hash.indexOf('?'))
|
|
143
|
+
: '';
|
|
144
|
+
const encoded = new URLSearchParams(location?.search ?? '').get(SHARE_PARAM) ??
|
|
145
|
+
new URLSearchParams(hashQuery.split('#')[0]).get(SHARE_PARAM);
|
|
146
|
+
if (!encoded)
|
|
147
|
+
return undefined;
|
|
148
|
+
const { state, preview, problems } = decodeShare(page, encoded);
|
|
149
|
+
if (problems.length)
|
|
150
|
+
this.#status.value = { text: problems.join(' '), alert: !state, keep: true };
|
|
151
|
+
if (!state)
|
|
152
|
+
return undefined;
|
|
153
|
+
if (preview) {
|
|
154
|
+
const width = preview.width;
|
|
155
|
+
const named = typeof width === 'number'
|
|
156
|
+
? Object.keys(WIDTHS).find((key) => WIDTHS[key] === width)
|
|
157
|
+
: undefined;
|
|
158
|
+
this.#settings.value = {
|
|
159
|
+
...DEFAULT_SETTINGS,
|
|
160
|
+
mode: preview.mode ?? 'site',
|
|
161
|
+
direction: preview.direction ?? 'ltr',
|
|
162
|
+
...(typeof width !== 'number'
|
|
163
|
+
? {}
|
|
164
|
+
: named
|
|
165
|
+
? { width: named }
|
|
166
|
+
: { width: 'custom', customWidth: width }),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return state;
|
|
170
|
+
}
|
|
171
|
+
#stop() {
|
|
172
|
+
this.#previewScope?.dispose();
|
|
173
|
+
this.#previewScope = undefined;
|
|
174
|
+
this.#previews = [];
|
|
175
|
+
for (const host of this.#hosts)
|
|
176
|
+
host.replaceChildren();
|
|
177
|
+
}
|
|
178
|
+
#frameSettings(index) {
|
|
179
|
+
const settings = this.#settings.peek();
|
|
180
|
+
const mode = settings.pair ? (index === 0 ? 'light' : 'dark') : settings.mode;
|
|
181
|
+
const width = settings.width === 'fill'
|
|
182
|
+
? undefined
|
|
183
|
+
: settings.width === 'custom'
|
|
184
|
+
? settings.customWidth
|
|
185
|
+
: WIDTHS[settings.width];
|
|
186
|
+
return {
|
|
187
|
+
...(mode === 'site' ? {} : { mode }),
|
|
188
|
+
direction: settings.direction,
|
|
189
|
+
...(width ? { container: { width } } : {}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
#mountPayload(page, registration, index) {
|
|
193
|
+
const state = this.#state.peek();
|
|
194
|
+
const facts = componentFacts(page);
|
|
195
|
+
return {
|
|
196
|
+
tag: page.tag,
|
|
197
|
+
registration,
|
|
198
|
+
example: `example:${page.tag}/${state.preset}`,
|
|
199
|
+
properties: state.properties,
|
|
200
|
+
attributes: state.attributes,
|
|
201
|
+
slots: state.slots,
|
|
202
|
+
events: facts.events.map((event) => event.name),
|
|
203
|
+
sync: facts.syncPairs,
|
|
204
|
+
settings: this.#frameSettings(index),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/** Create the preview frame(s) and mount the current state; replaces earlier frames. */
|
|
208
|
+
#restart() {
|
|
209
|
+
this.#stop();
|
|
210
|
+
const page = this.page;
|
|
211
|
+
if (!page || !this.isConnected)
|
|
212
|
+
return;
|
|
213
|
+
const registration = page.enrichment?.['registration'];
|
|
214
|
+
// Without a registration entry the preview item itself says what is missing.
|
|
215
|
+
if (typeof registration !== 'string') {
|
|
216
|
+
if (!this.#status.peek().keep)
|
|
217
|
+
this.#status.value = { text: '', alert: false };
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (!this.frameUrl) {
|
|
221
|
+
this.#status.value = {
|
|
222
|
+
text: 'Preview unavailable: no preview frame document is configured.',
|
|
223
|
+
alert: false,
|
|
224
|
+
};
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const scope = createScope({ name: `workbench-detail:${page.tag}` });
|
|
228
|
+
this.#previewScope = scope;
|
|
229
|
+
const count = this.#settings.peek().pair ? 2 : 1;
|
|
230
|
+
try {
|
|
231
|
+
for (let i = 0; i < count; i++) {
|
|
232
|
+
const preview = createIsolatedPreview({
|
|
233
|
+
document: this.ownerDocument,
|
|
234
|
+
container: this.#hosts[i],
|
|
235
|
+
frameUrl: this.frameUrl,
|
|
236
|
+
title: `${page.tag} preview${count === 2 ? (i === 0 ? ' (light)' : ' (dark)') : ''}`,
|
|
237
|
+
onMessage: (notification) => this.#onMessage(notification, i),
|
|
238
|
+
});
|
|
239
|
+
preview.iframe.style.blockSize = '14rem';
|
|
240
|
+
scope.add(() => preview.dispose(), 'isolated preview');
|
|
241
|
+
this.#previews.push(preview);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
this.#fail(error);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
this.#mountAll(scope, page, registration);
|
|
249
|
+
}
|
|
250
|
+
#mountAll(scope, page, registration) {
|
|
251
|
+
const previews = [...this.#previews];
|
|
252
|
+
void Promise.all(previews.map((preview, i) => preview.ready.then(() => preview.request('mount', this.#mountPayload(page, registration, i))))).then(() => {
|
|
253
|
+
if (!scope.disposed && !this.#status.peek().alert && !this.#status.peek().keep)
|
|
254
|
+
this.#status.value = { text: '', alert: false };
|
|
255
|
+
}, (error) => {
|
|
256
|
+
if (!scope.disposed)
|
|
257
|
+
this.#fail(error);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
#fail(error) {
|
|
261
|
+
this.#status.value = { text: `Preview unavailable: ${message(error)}`, alert: true };
|
|
262
|
+
}
|
|
263
|
+
#onMessage(notification, index) {
|
|
264
|
+
const payload = notification.payload;
|
|
265
|
+
if (notification.kind === 'event' && isRecord(payload) && index === 0) {
|
|
266
|
+
const entry = {
|
|
267
|
+
id: ++this.#logId,
|
|
268
|
+
name: String(payload['name']),
|
|
269
|
+
time: typeof payload['time'] === 'number' ? payload['time'] : 0,
|
|
270
|
+
bubbles: payload['bubbles'] === true,
|
|
271
|
+
composed: payload['composed'] === true,
|
|
272
|
+
target: String(payload['target'] ?? ''),
|
|
273
|
+
...(payload['detail'] !== undefined ? { detail: JSON.stringify(payload['detail']) } : {}),
|
|
274
|
+
};
|
|
275
|
+
this.#log.value = [entry, ...this.#log.peek()].slice(0, LOG_LIMIT);
|
|
276
|
+
}
|
|
277
|
+
else if (notification.kind === 'error' && isRecord(payload)) {
|
|
278
|
+
this.#status.value = {
|
|
279
|
+
text: `Preview ${String(payload['phase'])} failed: ${String(payload['message'])}. Use Reset to start a fresh preview.`,
|
|
280
|
+
alert: true,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
else if (notification.kind === 'state' &&
|
|
284
|
+
isRecord(payload) &&
|
|
285
|
+
typeof payload['property'] === 'string' &&
|
|
286
|
+
index === 0) {
|
|
287
|
+
// A declared sync pair: mirror the value without writing it back (no reflection loop).
|
|
288
|
+
const property = payload['property'];
|
|
289
|
+
const value = payload['value'];
|
|
290
|
+
const state = this.#state.peek();
|
|
291
|
+
this.#state.value = { ...state, properties: { ...state.properties, [property]: value } };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/** Send one request to every live preview. */
|
|
295
|
+
#send(kind, payload) {
|
|
296
|
+
for (const preview of this.#previews)
|
|
297
|
+
void preview.request(kind, payload).catch((error) => this.#fail(error));
|
|
298
|
+
}
|
|
299
|
+
#setError(id, text) {
|
|
300
|
+
const errors = { ...this.#errors.peek() };
|
|
301
|
+
if (text)
|
|
302
|
+
errors[id] = text;
|
|
303
|
+
else
|
|
304
|
+
delete errors[id];
|
|
305
|
+
this.#errors.value = errors;
|
|
306
|
+
}
|
|
307
|
+
#writeAttribute(row, value) {
|
|
308
|
+
const name = row.attribute;
|
|
309
|
+
const state = this.#state.peek();
|
|
310
|
+
const attributes = { ...state.attributes };
|
|
311
|
+
if (value === null)
|
|
312
|
+
delete attributes[name];
|
|
313
|
+
else
|
|
314
|
+
attributes[name] = value;
|
|
315
|
+
this.#state.value = { ...state, attributes };
|
|
316
|
+
this.#setError(row.id, undefined);
|
|
317
|
+
this.#send('set', { attribute: name, attributeValue: value });
|
|
318
|
+
}
|
|
319
|
+
#writeProperty(row, value) {
|
|
320
|
+
const name = row.property;
|
|
321
|
+
const state = this.#state.peek();
|
|
322
|
+
this.#state.value = { ...state, properties: { ...state.properties, [name]: value } };
|
|
323
|
+
this.#setError(row.id, undefined);
|
|
324
|
+
this.#send('set', { property: name, value });
|
|
325
|
+
}
|
|
326
|
+
/** Unset a property assignment: the frame cannot un-assign, so a clean instance is mounted. */
|
|
327
|
+
#unsetProperty(row) {
|
|
328
|
+
const name = row.property;
|
|
329
|
+
const state = this.#state.peek();
|
|
330
|
+
if (!(name in state.properties))
|
|
331
|
+
return;
|
|
332
|
+
const properties = { ...state.properties };
|
|
333
|
+
delete properties[name];
|
|
334
|
+
this.#state.value = { ...state, properties };
|
|
335
|
+
this.#setError(row.id, undefined);
|
|
336
|
+
this.#remount('Unset recreated the instance without this assignment.');
|
|
337
|
+
}
|
|
338
|
+
#writeSlot(name, text) {
|
|
339
|
+
const state = this.#state.peek();
|
|
340
|
+
this.#state.value = { ...state, slots: { ...state.slots, [name]: text } };
|
|
341
|
+
this.#send('slot', { name, text });
|
|
342
|
+
}
|
|
343
|
+
/** Mount the current state again in the existing frames (a clean instance, same realm). */
|
|
344
|
+
#remount(note) {
|
|
345
|
+
const page = this.page;
|
|
346
|
+
const registration = page?.enrichment?.['registration'];
|
|
347
|
+
const scope = this.#previewScope;
|
|
348
|
+
if (!page || typeof registration !== 'string' || !scope)
|
|
349
|
+
return;
|
|
350
|
+
this.#status.value = { text: note ?? '', alert: false, ...(note ? { keep: true } : {}) };
|
|
351
|
+
this.#mountAll(scope, page, registration);
|
|
352
|
+
}
|
|
353
|
+
#applyPreset(entry) {
|
|
354
|
+
if (!entry.state)
|
|
355
|
+
return;
|
|
356
|
+
this.#state.value = entry.state;
|
|
357
|
+
this.#errors.value = {};
|
|
358
|
+
this.#remount();
|
|
359
|
+
}
|
|
360
|
+
/** Reset: fresh frame documents (new generation) with the selected preset's authored values. */
|
|
361
|
+
#reset() {
|
|
362
|
+
const page = this.page;
|
|
363
|
+
if (!page)
|
|
364
|
+
return;
|
|
365
|
+
const preset = presets(page).find((p) => p.name === this.#state.peek().preset);
|
|
366
|
+
this.#state.value = preset?.state ?? initialState(page);
|
|
367
|
+
this.#errors.value = {};
|
|
368
|
+
this.#log.value = [];
|
|
369
|
+
this.#status.value = { text: '', alert: false };
|
|
370
|
+
this.#replace();
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Replace the preview realms with fresh frame documents and mount the current configuration
|
|
374
|
+
* again. The shell calls this when component code changes (a registration cannot be redefined
|
|
375
|
+
* in the same realm).
|
|
376
|
+
*/
|
|
377
|
+
replacePreview() {
|
|
378
|
+
return this.#replace();
|
|
379
|
+
}
|
|
380
|
+
#replace() {
|
|
381
|
+
const page = this.page;
|
|
382
|
+
const registration = page?.enrichment?.['registration'];
|
|
383
|
+
const scope = this.#previewScope;
|
|
384
|
+
if (!page || typeof registration !== 'string' || !scope) {
|
|
385
|
+
this.#restart();
|
|
386
|
+
return Promise.resolve();
|
|
387
|
+
}
|
|
388
|
+
return Promise.all(this.#previews.map((preview) => preview.reload())).then(() => this.#mountAll(scope, page, registration), (error) => {
|
|
389
|
+
if (!scope.disposed)
|
|
390
|
+
this.#fail(error);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
#applySettings(next) {
|
|
394
|
+
const pairChanged = next.pair !== this.#settings.peek().pair;
|
|
395
|
+
this.#settings.value = next;
|
|
396
|
+
if (pairChanged) {
|
|
397
|
+
this.#restart();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
this.#previews.forEach((preview, i) => {
|
|
401
|
+
void preview
|
|
402
|
+
.request('settings', this.#frameSettings(i))
|
|
403
|
+
.catch((error) => this.#fail(error));
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
#shareLink(page) {
|
|
407
|
+
const settings = this.#settings.peek();
|
|
408
|
+
const width = settings.width === 'fill'
|
|
409
|
+
? 'fill'
|
|
410
|
+
: settings.width === 'custom'
|
|
411
|
+
? settings.customWidth
|
|
412
|
+
: WIDTHS[settings.width];
|
|
413
|
+
// Light and dark side by side is a view of the page, not a preview setting to share.
|
|
414
|
+
const preview = {
|
|
415
|
+
...(settings.mode === 'site' || settings.pair
|
|
416
|
+
? { sameAsSite: true }
|
|
417
|
+
: { mode: settings.mode }),
|
|
418
|
+
direction: settings.direction,
|
|
419
|
+
width,
|
|
420
|
+
};
|
|
421
|
+
const encoded = encodeShare(sharePayload(page.tag, this.#state.peek(), preview));
|
|
422
|
+
const route = this.index?.registry.pages.find((p) => p.id === page.id)?.route ?? `/${page.id}`;
|
|
423
|
+
const url = new URL(this.href(route), this.ownerDocument.baseURI);
|
|
424
|
+
// Hash routing keeps the query inside the fragment, where the router reads it.
|
|
425
|
+
if (url.hash.startsWith('#/')) {
|
|
426
|
+
const [path = '', query = ''] = url.hash.slice(1).split('?');
|
|
427
|
+
const params = new URLSearchParams(query);
|
|
428
|
+
params.set(SHARE_PARAM, encoded);
|
|
429
|
+
url.hash = `${path}?${params}`;
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
url.searchParams.set(SHARE_PARAM, encoded);
|
|
433
|
+
url.hash = 'interactive-example';
|
|
434
|
+
}
|
|
435
|
+
return url.href;
|
|
436
|
+
}
|
|
437
|
+
#copyLink(page) {
|
|
438
|
+
const link = this.#shareLink(page);
|
|
439
|
+
this.#link.value = link;
|
|
440
|
+
const clipboard = this.ownerDocument.defaultView?.navigator.clipboard;
|
|
441
|
+
if (!clipboard)
|
|
442
|
+
return;
|
|
443
|
+
clipboard.writeText(link).then(() => (this.#status.value = { text: 'Example link copied.', alert: false, keep: true }), () => undefined);
|
|
444
|
+
}
|
|
445
|
+
// ---- Rendering ----
|
|
446
|
+
render() {
|
|
447
|
+
const page = this.page;
|
|
448
|
+
const index = this.index;
|
|
449
|
+
if (!page || !index)
|
|
450
|
+
return html `<div class="wb-content"></div>`;
|
|
451
|
+
const entry = index.registry.pages.find((p) => p.id === page.id);
|
|
452
|
+
const sections = (entry?.sections ?? []).filter((s) => s.visible);
|
|
453
|
+
const facts = componentFacts(page);
|
|
454
|
+
const at = covered(pageCoverage(page));
|
|
455
|
+
// Sections are built eagerly (not with repeat()), so the signals they read are tracked.
|
|
456
|
+
return html `<div class="wb-content">
|
|
457
|
+
${sections.map((section) => html `<section id=${section.id} aria-labelledby=${`${section.id}-title`}>
|
|
458
|
+
<h2 id=${`${section.id}-title`}>${section.title}</h2>
|
|
459
|
+
${this.#section(section.id, page, facts, at)}
|
|
460
|
+
</section>`)}
|
|
461
|
+
</div>`;
|
|
462
|
+
}
|
|
463
|
+
#section(id, page, facts, at) {
|
|
464
|
+
switch (id) {
|
|
465
|
+
case 'overview':
|
|
466
|
+
return this.#overview(page, at);
|
|
467
|
+
case 'interactive-example':
|
|
468
|
+
return this.#interactive(page, facts, at);
|
|
469
|
+
case 'api':
|
|
470
|
+
return this.#api(facts, at);
|
|
471
|
+
case 'examples':
|
|
472
|
+
return at('examples/examples', () => this.#examples(page));
|
|
473
|
+
case 'tokens':
|
|
474
|
+
return this.#tokens(page, at);
|
|
475
|
+
case 'behavior-and-accessibility':
|
|
476
|
+
return this.#behavior(page, facts, at);
|
|
477
|
+
case 'guidance':
|
|
478
|
+
return this.#guidance(page, at);
|
|
479
|
+
case 'source':
|
|
480
|
+
return at('source/module', () => this.#source(page));
|
|
481
|
+
case 'known-limitations':
|
|
482
|
+
return at('known-limitations/limitations', () => bullets(authoredValues(page.enrichment?.['limitations'])));
|
|
483
|
+
default:
|
|
484
|
+
return notDocumented(`pages["${page.id}"].sections["${id}"] in oddkin.docs.ts`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
#overview(page, at) {
|
|
488
|
+
const description = typeof page.declaration['description'] === 'string' ? page.declaration['description'] : '';
|
|
489
|
+
const registration = page.enrichment?.['registration'];
|
|
490
|
+
const related = Array.isArray(page.enrichment?.['related'])
|
|
491
|
+
? page.enrichment['related'].filter((r) => typeof r === 'string')
|
|
492
|
+
: [];
|
|
493
|
+
return html `${at('overview/description', () => description
|
|
494
|
+
.split(/\n\s*\n/)
|
|
495
|
+
.map((paragraph) => html `<p>${inline(paragraph.replace(/\s*\n\s*/g, ' '))}</p>`))}
|
|
496
|
+
<h3>Imports</h3>
|
|
497
|
+
${at('overview/imports', () => html `<p>Register the element with its define entry:</p>
|
|
498
|
+
<pre class="wb-code" tabindex="0"><code>import '${String(registration)}';</code></pre>`)}
|
|
499
|
+
${related.length
|
|
500
|
+
? html `<h3>Related components</h3>
|
|
501
|
+
<ul class="wb-list">
|
|
502
|
+
${related.map((tag) => {
|
|
503
|
+
const name = tag.replace(/^component:/, '');
|
|
504
|
+
return html `<li><a href=${this.href(`/components/${name}`)}>${name}</a></li>`;
|
|
505
|
+
})}
|
|
506
|
+
</ul>`
|
|
507
|
+
: nothing}`;
|
|
508
|
+
}
|
|
509
|
+
/** Intended use and explained mistakes, each paired with the example to use instead. */
|
|
510
|
+
#guidance(page, at) {
|
|
511
|
+
const { useFor, notFor } = intendedUse(page);
|
|
512
|
+
const authored = page.authored?.guidance ?? '';
|
|
513
|
+
const show = (entry, label) => entry.state
|
|
514
|
+
? html `<button type="button" @click=${() => this.#applyPreset(entry)}>${label}</button>`
|
|
515
|
+
: nothing;
|
|
516
|
+
return html `${at('guidance/authored', () => authored.split(/\n\s*\n/).map((paragraph) => html `<p>${inline(paragraph)}</p>`))}
|
|
517
|
+
<h3>Use it for</h3>
|
|
518
|
+
${at('guidance/use-for', () => bullets(useFor))}
|
|
519
|
+
<h3>Not for</h3>
|
|
520
|
+
${at('guidance/not-for', () => bullets(notFor))}
|
|
521
|
+
<h3>Correct and incorrect usage</h3>
|
|
522
|
+
${at('guidance/usage-pairs', () => html `<ul class="wb-list wb-pairs">
|
|
523
|
+
${usagePairs(page).map(({ incorrect, correct }) => html `<li data-pair=${incorrect.id}>
|
|
524
|
+
<p><strong>Incorrect:</strong> ${incorrect.label} <code>${incorrect.id}</code></p>
|
|
525
|
+
${incorrect.incorrect?.explanation
|
|
526
|
+
? html `<p>${inline(incorrect.incorrect.explanation)}</p>`
|
|
527
|
+
: notDocumented('incorrect.explanation in defineExamples', 'Explanation')}
|
|
528
|
+
<p>
|
|
529
|
+
<strong>Correct:</strong>
|
|
530
|
+
${correct
|
|
531
|
+
? html `${correct.label} <code>${correct.id}</code>`
|
|
532
|
+
: html `<span class="wb-not-documented">Not documented</span>`}
|
|
533
|
+
</p>
|
|
534
|
+
${show(incorrect, 'Show the incorrect usage in the preview')}
|
|
535
|
+
${correct ? show(correct, 'Show the correct usage in the preview') : nothing}
|
|
536
|
+
</li>`)}
|
|
537
|
+
</ul>`)}`;
|
|
538
|
+
}
|
|
539
|
+
#interactive(page, facts, at) {
|
|
540
|
+
const state = this.#state.value;
|
|
541
|
+
const settings = this.#settings.value;
|
|
542
|
+
const status = this.#status.value;
|
|
543
|
+
const entries = presets(page).filter((p) => p.state);
|
|
544
|
+
const link = this.#link.value;
|
|
545
|
+
const frameSettings = settings.pair ? {} : this.#frameSettings(0);
|
|
546
|
+
const update = (patch) => this.#applySettings({ ...settings, ...patch });
|
|
547
|
+
return html `<div class="wb-toolbar">
|
|
548
|
+
${at('interactive-example/presets', () => html `<label
|
|
549
|
+
>Preset
|
|
550
|
+
<select
|
|
551
|
+
.value=${state.preset}
|
|
552
|
+
@change=${(event) => {
|
|
553
|
+
const name = event.target.value;
|
|
554
|
+
const entry = entries.find((p) => p.name === name);
|
|
555
|
+
if (entry)
|
|
556
|
+
this.#applyPreset(entry);
|
|
557
|
+
}}
|
|
558
|
+
>
|
|
559
|
+
${entries.map((p) => html `<option value=${p.name} ?selected=${p.name === state.preset}>
|
|
560
|
+
${p.label}${p.incorrect ? ' (incorrect usage)' : ''}
|
|
561
|
+
</option>`)}
|
|
562
|
+
</select></label
|
|
563
|
+
>`)}
|
|
564
|
+
<label
|
|
565
|
+
>Mode
|
|
566
|
+
<select
|
|
567
|
+
.value=${settings.mode}
|
|
568
|
+
?disabled=${settings.pair}
|
|
569
|
+
@change=${(event) => update({ mode: event.target.value })}
|
|
570
|
+
>
|
|
571
|
+
<option value="site" ?selected=${settings.mode === 'site'}>Same as site</option>
|
|
572
|
+
<option value="light" ?selected=${settings.mode === 'light'}>Light</option>
|
|
573
|
+
<option value="dark" ?selected=${settings.mode === 'dark'}>Dark</option>
|
|
574
|
+
</select></label
|
|
575
|
+
>
|
|
576
|
+
<label
|
|
577
|
+
><span>Light and dark side by side</span>
|
|
578
|
+
<input
|
|
579
|
+
type="checkbox"
|
|
580
|
+
.checked=${settings.pair}
|
|
581
|
+
@change=${(event) => update({ pair: event.target.checked })}
|
|
582
|
+
/></label>
|
|
583
|
+
<label
|
|
584
|
+
>Direction
|
|
585
|
+
<select
|
|
586
|
+
.value=${settings.direction}
|
|
587
|
+
@change=${(event) => update({
|
|
588
|
+
direction: event.target.value,
|
|
589
|
+
})}
|
|
590
|
+
>
|
|
591
|
+
<option value="ltr" ?selected=${settings.direction === 'ltr'}>Left to right</option>
|
|
592
|
+
<option value="rtl" ?selected=${settings.direction === 'rtl'}>Right to left</option>
|
|
593
|
+
</select></label
|
|
594
|
+
>
|
|
595
|
+
<label
|
|
596
|
+
>Width
|
|
597
|
+
<select
|
|
598
|
+
.value=${settings.width}
|
|
599
|
+
@change=${(event) => update({ width: event.target.value })}
|
|
600
|
+
>
|
|
601
|
+
<option value="fill" ?selected=${settings.width === 'fill'}>Fill</option>
|
|
602
|
+
<option value="narrow" ?selected=${settings.width === 'narrow'}>Narrow (320px)</option>
|
|
603
|
+
<option value="medium" ?selected=${settings.width === 'medium'}>Medium (640px)</option>
|
|
604
|
+
<option value="wide" ?selected=${settings.width === 'wide'}>Wide (960px)</option>
|
|
605
|
+
<option value="custom" ?selected=${settings.width === 'custom'}>Custom</option>
|
|
606
|
+
</select></label
|
|
607
|
+
>
|
|
608
|
+
${settings.width === 'custom'
|
|
609
|
+
? html `<label
|
|
610
|
+
>Width in px
|
|
611
|
+
<input
|
|
612
|
+
type="number"
|
|
613
|
+
min=${String(MIN_SHARE_WIDTH)}
|
|
614
|
+
max=${String(MAX_SHARE_WIDTH)}
|
|
615
|
+
step="1"
|
|
616
|
+
.value=${String(settings.customWidth)}
|
|
617
|
+
@change=${(event) => {
|
|
618
|
+
const value = Number(event.target.value);
|
|
619
|
+
if (Number.isInteger(value) &&
|
|
620
|
+
value >= MIN_SHARE_WIDTH &&
|
|
621
|
+
value <= MAX_SHARE_WIDTH)
|
|
622
|
+
update({ customWidth: value });
|
|
623
|
+
}}
|
|
624
|
+
/></label>`
|
|
625
|
+
: nothing}
|
|
626
|
+
<button type="button" @click=${() => this.#reset()}>Reset</button>
|
|
627
|
+
<button type="button" @click=${() => this.#copyLink(page)}>Copy example link</button>
|
|
628
|
+
</div>
|
|
629
|
+
<p class="wb-muted wb-settings-summary">
|
|
630
|
+
Preview only:
|
|
631
|
+
${settings.pair ? 'light and dark' : settings.mode === 'site' ? 'same mode as the site' : settings.mode}
|
|
632
|
+
· ${settings.direction === 'rtl' ? 'right to left' : 'left to right'} ·
|
|
633
|
+
${settings.width === 'fill'
|
|
634
|
+
? 'fills the column'
|
|
635
|
+
: `${settings.width === 'custom' ? settings.customWidth : WIDTHS[settings.width]}px container`}
|
|
636
|
+
</p>
|
|
637
|
+
${link
|
|
638
|
+
? html `<label class="wb-share"
|
|
639
|
+
>Example link <input type="url" readonly .value=${link} size="40"
|
|
640
|
+
/></label>`
|
|
641
|
+
: nothing}
|
|
642
|
+
${at('interactive-example/preview', () => html `<div class=${settings.pair ? 'wb-previews wb-pair' : 'wb-previews'}>
|
|
643
|
+
<div class="wb-preview">
|
|
644
|
+
${settings.pair ? html `<p>Light</p>` : nothing}${this.#hosts[0]}
|
|
645
|
+
</div>
|
|
646
|
+
${settings.pair
|
|
647
|
+
? html `<div class="wb-preview">
|
|
648
|
+
<p>Dark</p>
|
|
649
|
+
${this.#hosts[1]}
|
|
650
|
+
</div>`
|
|
651
|
+
: nothing}
|
|
652
|
+
</div>`)}
|
|
653
|
+
<p class="wb-status" role=${status.alert ? 'alert' : 'status'}>${status.text}</p>
|
|
654
|
+
<h3>Event log</h3>
|
|
655
|
+
${this.#eventLog(facts)}
|
|
656
|
+
<h3>Code</h3>
|
|
657
|
+
<p class="wb-muted">Plain HTML and JavaScript for the current configuration.</p>
|
|
658
|
+
<pre
|
|
659
|
+
class="wb-code wb-snippet"
|
|
660
|
+
tabindex="0"
|
|
661
|
+
><code>${snippet(page, state, { settings: frameSettings })}</code></pre>`;
|
|
662
|
+
}
|
|
663
|
+
#eventLog(facts) {
|
|
664
|
+
if (!facts.events.length)
|
|
665
|
+
return html `<p class="wb-muted wb-event-log">No events are declared, so none are logged.</p>`;
|
|
666
|
+
const log = this.#log.value;
|
|
667
|
+
return html `<div class="wb-event-log">
|
|
668
|
+
<p class="wb-muted">
|
|
669
|
+
Logging ${facts.events.map((e, i) => html `${i ? ', ' : ''}<code>${e.name}</code>`)} (latest
|
|
670
|
+
first, at most ${LOG_LIMIT}).
|
|
671
|
+
<button type="button" ?disabled=${!log.length} @click=${() => (this.#log.value = [])}>
|
|
672
|
+
Clear
|
|
673
|
+
</button>
|
|
674
|
+
</p>
|
|
675
|
+
${log.length
|
|
676
|
+
? html `<ol class="wb-log" aria-label="Events">
|
|
677
|
+
${repeat(log, (entry) => entry.id, (entry) => html `<li>
|
|
678
|
+
<code>${entry.name}</code> at ${entry.time.toFixed(0)} ms on
|
|
679
|
+
<code>${entry.target}</code> · ${entry.bubbles ? 'bubbles' : 'does not bubble'}
|
|
680
|
+
· ${entry.composed ? 'composed' : 'not composed'}
|
|
681
|
+
${entry.detail !== undefined ? html `<br /><code>detail: ${entry.detail}</code>` : nothing}
|
|
682
|
+
</li>`)}
|
|
683
|
+
</ol>`
|
|
684
|
+
: html `<p class="wb-muted">No events yet.</p>`}
|
|
685
|
+
</div>`;
|
|
686
|
+
}
|
|
687
|
+
#api(facts, at) {
|
|
688
|
+
return html `${at('api/properties', () => this.#apiTable(facts))}
|
|
689
|
+
<h3>Slots</h3>
|
|
690
|
+
${at('api/slots', () => this.#slots(facts))}
|
|
691
|
+
<h3>Events</h3>
|
|
692
|
+
${at('api/events', () => this.#namedTable(facts.events, 'events', 'Event', true))}
|
|
693
|
+
<h3>Methods</h3>
|
|
694
|
+
${at('api/methods', () => html `<p class="wb-muted">
|
|
695
|
+
Methods are documented here and never invoked by the Workbench.
|
|
696
|
+
</p>
|
|
697
|
+
${this.#namedTable(facts.methods, 'methods', 'Method', false)}`)}
|
|
698
|
+
<h3>CSS parts</h3>
|
|
699
|
+
${at('api/parts', () => this.#namedTable(facts.parts, 'parts', 'Part', false))}
|
|
700
|
+
<h3>CSS custom properties</h3>
|
|
701
|
+
${at('api/css-properties', () => this.#namedTable(facts.cssProperties, 'css-properties', 'Property', false))}
|
|
702
|
+
<h3>Custom states</h3>
|
|
703
|
+
${at('api/css-states', () => this.#namedTable(facts.cssStates, 'states', 'State', false))}`;
|
|
704
|
+
}
|
|
705
|
+
#namedTable(entries, id, label, withType) {
|
|
706
|
+
return html `<div class="wb-table-wrap">
|
|
707
|
+
<table class="wb-stack" data-table=${id}>
|
|
708
|
+
<thead>
|
|
709
|
+
<tr>
|
|
710
|
+
<th scope="col">${label}</th>
|
|
711
|
+
${withType ? html `<th scope="col">Type</th>` : nothing}
|
|
712
|
+
<th scope="col">Description</th>
|
|
713
|
+
</tr>
|
|
714
|
+
</thead>
|
|
715
|
+
<tbody>
|
|
716
|
+
${entries.map((entry) => html `<tr>
|
|
717
|
+
<td data-label=${label}><code>${entry.name || '(default)'}</code></td>
|
|
718
|
+
${withType ? html `<td data-label="Type"><code>${entry.type ?? 'Unknown'}</code></td>` : nothing}
|
|
719
|
+
<td data-label="Description">
|
|
720
|
+
${entry.description ? inline(entry.description) : html `<span class="wb-not-documented">Not documented</span>`}
|
|
721
|
+
</td>
|
|
722
|
+
</tr>`)}
|
|
723
|
+
</tbody>
|
|
724
|
+
</table>
|
|
725
|
+
</div>`;
|
|
726
|
+
}
|
|
727
|
+
#apiTable(facts) {
|
|
728
|
+
return html `<div class="wb-table-wrap">
|
|
729
|
+
<table class="wb-stack wb-api" data-table="api">
|
|
730
|
+
<caption>
|
|
731
|
+
Editing a value updates the preview: one write per change, as a property or as an
|
|
732
|
+
attribute. Reflected attributes are not written back.
|
|
733
|
+
</caption>
|
|
734
|
+
<thead>
|
|
735
|
+
<tr>
|
|
736
|
+
<th scope="col">Name</th>
|
|
737
|
+
<th scope="col">Type</th>
|
|
738
|
+
<th scope="col">Default</th>
|
|
739
|
+
<th scope="col">Current value</th>
|
|
740
|
+
<th scope="col">Description</th>
|
|
741
|
+
</tr>
|
|
742
|
+
</thead>
|
|
743
|
+
<tbody>
|
|
744
|
+
${facts.rows.map((row) => this.#apiRow(row))}
|
|
745
|
+
</tbody>
|
|
746
|
+
</table>
|
|
747
|
+
</div>`;
|
|
748
|
+
}
|
|
749
|
+
#apiRow(row) {
|
|
750
|
+
const name = row.property ?? row.attribute ?? '';
|
|
751
|
+
const notes = [];
|
|
752
|
+
if (row.property && row.attribute)
|
|
753
|
+
notes.push(`attr ${row.attribute}${row.reflects ? ', reflects' : ''}`);
|
|
754
|
+
else if (!row.property)
|
|
755
|
+
notes.push('attribute only');
|
|
756
|
+
else
|
|
757
|
+
notes.push(row.propertyOnly ? 'property only' : 'no attribute');
|
|
758
|
+
return html `<tr data-row=${row.id}>
|
|
759
|
+
<th scope="row" data-label="Name"><code>${name}</code><small>${notes.join(' · ')}</small></th>
|
|
760
|
+
<td data-label="Type">
|
|
761
|
+
<code>${row.values ? row.values.map((v) => JSON.stringify(v)).join(' | ') : row.type}</code>
|
|
762
|
+
${row.nullable ? html `<small>nullable</small>` : nothing}
|
|
763
|
+
</td>
|
|
764
|
+
<td data-label="Default">${row.default ? html `<code>${row.default}</code>` : 'Unknown'}</td>
|
|
765
|
+
<td data-label="Current value">${this.#control(row)}</td>
|
|
766
|
+
<td data-label="Description">
|
|
767
|
+
${row.description ? inline(row.description) : html `<span class="wb-not-documented">Not documented</span>`}
|
|
768
|
+
</td>
|
|
769
|
+
</tr>`;
|
|
770
|
+
}
|
|
771
|
+
#control(row) {
|
|
772
|
+
const name = row.property ?? row.attribute ?? '';
|
|
773
|
+
if (row.control === 'readonly')
|
|
774
|
+
return html `<span class="wb-muted">Read-only; inspected, never edited.</span>`;
|
|
775
|
+
if (row.control === 'fixture')
|
|
776
|
+
return html `<span class="wb-muted"
|
|
777
|
+
>Requires fixture: <code>${row.type}</code> is not editable generically.</span
|
|
778
|
+
>`;
|
|
779
|
+
const state = this.#state.value;
|
|
780
|
+
const modes = this.#modes.value;
|
|
781
|
+
const mode = modes[row.id] ?? defaultWriteMode(row);
|
|
782
|
+
const error = this.#errors.value[row.id];
|
|
783
|
+
const label = `${name} (${mode})`;
|
|
784
|
+
const attr = row.attribute;
|
|
785
|
+
const prop = row.property;
|
|
786
|
+
const attrValue = attr ? state.attributes[attr] : undefined;
|
|
787
|
+
const propSet = prop !== undefined && prop in state.properties;
|
|
788
|
+
const propValue = prop !== undefined ? state.properties[prop] : undefined;
|
|
789
|
+
const modeSwitch = row.property && row.attribute && !row.propertyOnly && row.control !== 'json'
|
|
790
|
+
? html `<select
|
|
791
|
+
aria-label=${`Write ${name} as`}
|
|
792
|
+
.value=${mode}
|
|
793
|
+
@change=${(event) => {
|
|
794
|
+
this.#modes.value = {
|
|
795
|
+
...this.#modes.peek(),
|
|
796
|
+
[row.id]: event.target.value,
|
|
797
|
+
};
|
|
798
|
+
}}
|
|
799
|
+
>
|
|
800
|
+
<option value="attribute" ?selected=${mode === 'attribute'}>attribute</option>
|
|
801
|
+
<option value="property" ?selected=${mode === 'property'}>property</option>
|
|
802
|
+
</select>`
|
|
803
|
+
: nothing;
|
|
804
|
+
let control;
|
|
805
|
+
if (row.control === 'boolean') {
|
|
806
|
+
const checked = mode === 'attribute' ? attrValue !== undefined : propValue === true;
|
|
807
|
+
control = html `<input
|
|
808
|
+
type="checkbox"
|
|
809
|
+
aria-label=${label}
|
|
810
|
+
.checked=${checked}
|
|
811
|
+
@change=${(event) => {
|
|
812
|
+
const on = event.target.checked;
|
|
813
|
+
if (mode === 'attribute')
|
|
814
|
+
this.#writeAttribute(row, on ? '' : null);
|
|
815
|
+
else
|
|
816
|
+
this.#writeProperty(row, on);
|
|
817
|
+
}}
|
|
818
|
+
/>
|
|
819
|
+
<span class="wb-muted"
|
|
820
|
+
>${mode === 'attribute'
|
|
821
|
+
? attrValue !== undefined
|
|
822
|
+
? 'present'
|
|
823
|
+
: 'absent'
|
|
824
|
+
: propSet
|
|
825
|
+
? String(propValue)
|
|
826
|
+
: 'unset'}</span
|
|
827
|
+
>`;
|
|
828
|
+
}
|
|
829
|
+
else if (row.control === 'enum') {
|
|
830
|
+
const current = mode === 'attribute'
|
|
831
|
+
? (attrValue ?? '')
|
|
832
|
+
: propSet
|
|
833
|
+
? propValue === null
|
|
834
|
+
? '\u0000null'
|
|
835
|
+
: String(propValue)
|
|
836
|
+
: '';
|
|
837
|
+
control = html `<select
|
|
838
|
+
aria-label=${label}
|
|
839
|
+
.value=${current}
|
|
840
|
+
@change=${(event) => {
|
|
841
|
+
const value = event.target.value;
|
|
842
|
+
if (mode === 'attribute')
|
|
843
|
+
this.#writeAttribute(row, value === '' ? null : value);
|
|
844
|
+
else if (value === '')
|
|
845
|
+
this.#unsetProperty(row);
|
|
846
|
+
else
|
|
847
|
+
this.#writeProperty(row, value === '\u0000null' ? null : value);
|
|
848
|
+
}}
|
|
849
|
+
>
|
|
850
|
+
<option value="" ?selected=${current === ''}>(unset)</option>
|
|
851
|
+
${(row.values ?? []).map((value) => html `<option value=${value} ?selected=${current === value}>${value}</option>`)}
|
|
852
|
+
${mode === 'property' && row.nullable
|
|
853
|
+
? html `<option value=${'\u0000null'} ?selected=${current === '\u0000null'}>
|
|
854
|
+
null
|
|
855
|
+
</option>`
|
|
856
|
+
: nothing}
|
|
857
|
+
</select>`;
|
|
858
|
+
}
|
|
859
|
+
else if (row.control === 'json') {
|
|
860
|
+
control = html `<textarea
|
|
861
|
+
aria-label=${label}
|
|
862
|
+
rows="3"
|
|
863
|
+
.value=${propSet ? JSON.stringify(propValue) : ''}
|
|
864
|
+
placeholder="unset"
|
|
865
|
+
@change=${(event) => {
|
|
866
|
+
const text = event.target.value.trim();
|
|
867
|
+
if (!text) {
|
|
868
|
+
this.#unsetProperty(row);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
try {
|
|
872
|
+
const value = JSON.parse(text);
|
|
873
|
+
if (!Array.isArray(value) && !(value === null && row.nullable))
|
|
874
|
+
throw new Error('expected a JSON array');
|
|
875
|
+
this.#writeProperty(row, value);
|
|
876
|
+
}
|
|
877
|
+
catch (cause) {
|
|
878
|
+
this.#setError(row.id, `Not written: ${message(cause)}.`);
|
|
879
|
+
}
|
|
880
|
+
}}
|
|
881
|
+
></textarea>`;
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
const number = row.control === 'number';
|
|
885
|
+
const isSet = mode === 'attribute' ? attrValue !== undefined : propSet;
|
|
886
|
+
const shown = mode === 'attribute'
|
|
887
|
+
? (attrValue ?? '')
|
|
888
|
+
: propSet && propValue !== null
|
|
889
|
+
? String(propValue)
|
|
890
|
+
: '';
|
|
891
|
+
control = html `<input
|
|
892
|
+
type=${number ? 'number' : 'text'}
|
|
893
|
+
aria-label=${label}
|
|
894
|
+
placeholder=${isSet ? '' : 'unset'}
|
|
895
|
+
.value=${shown}
|
|
896
|
+
@change=${(event) => {
|
|
897
|
+
const text = event.target.value;
|
|
898
|
+
if (mode === 'attribute') {
|
|
899
|
+
if (number && text !== '' && !Number.isFinite(Number(text))) {
|
|
900
|
+
this.#setError(row.id, 'Not written: not a number.');
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
this.#writeAttribute(row, text);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (number) {
|
|
907
|
+
if (text.trim() === '' || !Number.isFinite(Number(text))) {
|
|
908
|
+
this.#setError(row.id, 'Not written: enter a number, or use Unset.');
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
this.#writeProperty(row, Number(text));
|
|
912
|
+
}
|
|
913
|
+
else
|
|
914
|
+
this.#writeProperty(row, text);
|
|
915
|
+
}}
|
|
916
|
+
/>
|
|
917
|
+
<button
|
|
918
|
+
type="button"
|
|
919
|
+
?disabled=${!isSet}
|
|
920
|
+
@click=${() => {
|
|
921
|
+
if (mode === 'attribute')
|
|
922
|
+
this.#writeAttribute(row, null);
|
|
923
|
+
else
|
|
924
|
+
this.#unsetProperty(row);
|
|
925
|
+
}}
|
|
926
|
+
>
|
|
927
|
+
Unset
|
|
928
|
+
</button>
|
|
929
|
+
${mode === 'property' && row.nullable
|
|
930
|
+
? html `<button
|
|
931
|
+
type="button"
|
|
932
|
+
?disabled=${propSet && propValue === null}
|
|
933
|
+
@click=${() => this.#writeProperty(row, null)}
|
|
934
|
+
>
|
|
935
|
+
Set null
|
|
936
|
+
</button>`
|
|
937
|
+
: nothing}
|
|
938
|
+
<span class="wb-muted"
|
|
939
|
+
>${!isSet
|
|
940
|
+
? 'unset'
|
|
941
|
+
: mode === 'property' && propValue === null
|
|
942
|
+
? 'null'
|
|
943
|
+
: shown === ''
|
|
944
|
+
? 'empty string'
|
|
945
|
+
: ''}</span
|
|
946
|
+
>`;
|
|
947
|
+
}
|
|
948
|
+
return html `<div class="wb-cell-controls">${control} ${modeSwitch}</div>
|
|
949
|
+
${error ? html `<p class="wb-error" role="alert">${error}</p>` : nothing}`;
|
|
950
|
+
}
|
|
951
|
+
#slots(facts) {
|
|
952
|
+
const state = this.#state.value;
|
|
953
|
+
return html `<div class="wb-table-wrap">
|
|
954
|
+
<table class="wb-stack" data-table="slots">
|
|
955
|
+
<thead>
|
|
956
|
+
<tr>
|
|
957
|
+
<th scope="col">Slot</th>
|
|
958
|
+
<th scope="col">Description</th>
|
|
959
|
+
<th scope="col">Text content</th>
|
|
960
|
+
</tr>
|
|
961
|
+
</thead>
|
|
962
|
+
<tbody>
|
|
963
|
+
${facts.slots.map((slot) => {
|
|
964
|
+
const label = slot.name || '(default)';
|
|
965
|
+
return html `<tr data-slot=${slot.name}>
|
|
966
|
+
<th scope="row" data-label="Slot"><code>${label}</code></th>
|
|
967
|
+
<td data-label="Description">
|
|
968
|
+
${slot.description ? inline(slot.description) : html `<span class="wb-not-documented">Not documented</span>`}
|
|
969
|
+
</td>
|
|
970
|
+
<td data-label="Text content">
|
|
971
|
+
<input
|
|
972
|
+
type="text"
|
|
973
|
+
aria-label=${`${label} slot text`}
|
|
974
|
+
.value=${state.slots[slot.name] ?? ''}
|
|
975
|
+
@change=${(event) => this.#writeSlot(slot.name, event.target.value)}
|
|
976
|
+
/>
|
|
977
|
+
<small>Text only; richer compositions need a fixture from defineExamples.</small>
|
|
978
|
+
</td>
|
|
979
|
+
</tr>`;
|
|
980
|
+
})}
|
|
981
|
+
</tbody>
|
|
982
|
+
</table>
|
|
983
|
+
</div>`;
|
|
984
|
+
}
|
|
985
|
+
#examples(page) {
|
|
986
|
+
const entries = presets(page);
|
|
987
|
+
return html `<ul class="wb-list wb-examples">
|
|
988
|
+
${entries.map((entry) => html `<li data-example=${entry.id}>
|
|
989
|
+
<strong>${entry.label}</strong> <code>${entry.id}</code>
|
|
990
|
+
${entry.incorrect ? html `<small>Incorrect usage</small>` : nothing}
|
|
991
|
+
${entry.guidance ? html `<p>${inline(entry.guidance)}</p>` : nothing}
|
|
992
|
+
${entry.incorrect?.explanation ? html `<p>${inline(entry.incorrect.explanation)}</p>` : nothing}
|
|
993
|
+
${entry.requiresFixture
|
|
994
|
+
? html `<p class="wb-muted">
|
|
995
|
+
Requires fixture: rendered by its example module only.
|
|
996
|
+
</p>`
|
|
997
|
+
: entry.state
|
|
998
|
+
? html `<button type="button" @click=${() => this.#applyPreset(entry)}>
|
|
999
|
+
Show in preview
|
|
1000
|
+
</button>`
|
|
1001
|
+
: html `<p class="wb-muted">
|
|
1002
|
+
Values not published (the example module was not loaded).
|
|
1003
|
+
</p>`}
|
|
1004
|
+
</li>`)}
|
|
1005
|
+
</ul>`;
|
|
1006
|
+
}
|
|
1007
|
+
#tokens(page, at) {
|
|
1008
|
+
const mappings = Array.isArray(page.enrichment?.['styleMappings'])
|
|
1009
|
+
? page.enrichment['styleMappings'].filter(isRecord)
|
|
1010
|
+
: [];
|
|
1011
|
+
const reads = Array.isArray(page.enrichment?.['tokenReads'])
|
|
1012
|
+
? page.enrichment['tokenReads'].filter(isRecord)
|
|
1013
|
+
: [];
|
|
1014
|
+
const resolved = new Map((page.tokens ?? []).map((t) => [t.name, t]));
|
|
1015
|
+
const theme = this.index?.libraries.length ? 'the generated baseline theme' : 'the baseline';
|
|
1016
|
+
return html `<h3>From property to token</h3>
|
|
1017
|
+
${at('tokens/style-mappings', () => html `<div class="wb-table-wrap">
|
|
1018
|
+
<table class="wb-stack" data-table="style-mappings">
|
|
1019
|
+
<caption>
|
|
1020
|
+
Declared style mappings extracted from the component's stylesheets.
|
|
1021
|
+
</caption>
|
|
1022
|
+
<thead>
|
|
1023
|
+
<tr>
|
|
1024
|
+
<th scope="col">When</th>
|
|
1025
|
+
<th scope="col">Part</th>
|
|
1026
|
+
<th scope="col">CSS property</th>
|
|
1027
|
+
<th scope="col">Token</th>
|
|
1028
|
+
</tr>
|
|
1029
|
+
</thead>
|
|
1030
|
+
<tbody>
|
|
1031
|
+
${mappings.map((m) => html `<tr>
|
|
1032
|
+
<td data-label="When"><code>${String(m['when'] ?? 'default')}</code></td>
|
|
1033
|
+
<td data-label="Part">
|
|
1034
|
+
${m['part'] ? html `<code>${String(m['part'])}</code>` : 'host'}
|
|
1035
|
+
</td>
|
|
1036
|
+
<td data-label="CSS property">
|
|
1037
|
+
<code>${String(m['cssProperty'] ?? '')}</code>
|
|
1038
|
+
</td>
|
|
1039
|
+
<td data-label="Token"><code>${String(m['token'] ?? '')}</code></td>
|
|
1040
|
+
</tr>`)}
|
|
1041
|
+
</tbody>
|
|
1042
|
+
</table>
|
|
1043
|
+
</div>`)}
|
|
1044
|
+
<h3>Every token read</h3>
|
|
1045
|
+
${at('tokens/token-reads', () => html `<div class="wb-table-wrap">
|
|
1046
|
+
<table class="wb-stack" data-table="token-reads">
|
|
1047
|
+
<caption>
|
|
1048
|
+
${resolved.size} tokens read · values for ${theme} (not a live preview reading)
|
|
1049
|
+
</caption>
|
|
1050
|
+
<thead>
|
|
1051
|
+
<tr>
|
|
1052
|
+
<th scope="col">Token</th>
|
|
1053
|
+
<th scope="col">Reference</th>
|
|
1054
|
+
<th scope="col">Read through</th>
|
|
1055
|
+
<th scope="col">Value</th>
|
|
1056
|
+
</tr>
|
|
1057
|
+
</thead>
|
|
1058
|
+
<tbody>
|
|
1059
|
+
${[...new Map(reads.map((r) => [String(r['token']), r])).values()].map((read) => {
|
|
1060
|
+
const name = String(read['token']);
|
|
1061
|
+
const token = resolved.get(name);
|
|
1062
|
+
const definition = token &&
|
|
1063
|
+
typeof token.definition === 'string' &&
|
|
1064
|
+
token.definition.startsWith('{')
|
|
1065
|
+
? token.definition
|
|
1066
|
+
: undefined;
|
|
1067
|
+
return html `<tr>
|
|
1068
|
+
<td data-label="Token">
|
|
1069
|
+
<code>${name}</code>
|
|
1070
|
+
${token?.cssVariable ? html `<small><code>${token.cssVariable}</code></small>` : nothing}
|
|
1071
|
+
</td>
|
|
1072
|
+
<td data-label="Reference">${String(read['kind'] ?? 'declared')}</td>
|
|
1073
|
+
<td data-label="Read through">${String(read['readThrough'] ?? 'Unknown')}</td>
|
|
1074
|
+
<td data-label="Value">
|
|
1075
|
+
${token && !('missing' in token) && token.value !== undefined
|
|
1076
|
+
? html `<code>${String(token.value)}</code
|
|
1077
|
+
>${definition ? html ` <small><code>${definition}</code></small>` : nothing}`
|
|
1078
|
+
: 'Unknown'}
|
|
1079
|
+
</td>
|
|
1080
|
+
</tr>`;
|
|
1081
|
+
})}
|
|
1082
|
+
</tbody>
|
|
1083
|
+
</table>
|
|
1084
|
+
</div>`)}`;
|
|
1085
|
+
}
|
|
1086
|
+
#behavior(page, facts, at) {
|
|
1087
|
+
return html `<h3>Accessibility</h3>
|
|
1088
|
+
${at('behavior-and-accessibility/a11y', () => bullets(authoredValues(page.enrichment?.['a11y'])))}
|
|
1089
|
+
<h3>Keyboard</h3>
|
|
1090
|
+
${at('behavior-and-accessibility/keyboard', () => bullets(authoredValues(page.enrichment?.['keyboard'])))}
|
|
1091
|
+
${facts.cssStates.length
|
|
1092
|
+
? html `<h3>States</h3>
|
|
1093
|
+
<ul class="wb-list">
|
|
1094
|
+
${facts.cssStates.map((s) => html `<li>
|
|
1095
|
+
<code>:state(${s.name})</code
|
|
1096
|
+
>${s.description ? html ` — ${inline(s.description)}` : nothing}
|
|
1097
|
+
</li>`)}
|
|
1098
|
+
</ul>`
|
|
1099
|
+
: nothing}`;
|
|
1100
|
+
}
|
|
1101
|
+
#source(page) {
|
|
1102
|
+
const definition = isRecord(page.enrichment?.['definition'])
|
|
1103
|
+
? page.enrichment['definition']
|
|
1104
|
+
: undefined;
|
|
1105
|
+
const ref = isRecord(definition?.['ref']) ? definition['ref'] : undefined;
|
|
1106
|
+
return html `<dl>
|
|
1107
|
+
<dt>Package</dt>
|
|
1108
|
+
<dd><code>${page.package}</code></dd>
|
|
1109
|
+
<dt>Module</dt>
|
|
1110
|
+
<dd><code>${page.module}</code></dd>
|
|
1111
|
+
<dt>Class</dt>
|
|
1112
|
+
<dd><code>${String(page.declaration['name'] ?? 'Unknown')}</code></dd>
|
|
1113
|
+
${ref
|
|
1114
|
+
? html `<dt>Definition</dt>
|
|
1115
|
+
<dd><code>${String(ref['module'])}</code> (<code>${String(ref['name'])}</code>)</dd>`
|
|
1116
|
+
: nothing}
|
|
1117
|
+
</dl>`;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
//# sourceMappingURL=component-detail.js.map
|