@oddkin/workbench 1.0.0-beta.0 → 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.
- package/README.md +33 -3
- package/dist/app/card-previews.d.ts +14 -0
- package/dist/app/card-previews.d.ts.map +1 -0
- package/dist/app/card-previews.js +140 -0
- package/dist/app/card-previews.js.map +1 -0
- package/dist/app/copy.d.ts +6 -1
- package/dist/app/copy.d.ts.map +1 -1
- package/dist/app/copy.js +6 -1
- package/dist/app/copy.js.map +1 -1
- package/dist/app/details.d.ts.map +1 -1
- package/dist/app/details.js +2 -1
- package/dist/app/details.js.map +1 -1
- package/dist/app/routes.d.ts +1 -1
- package/dist/app/routes.d.ts.map +1 -1
- package/dist/app/routes.js +5 -3
- package/dist/app/routes.js.map +1 -1
- package/dist/app/shell.d.ts +1 -1
- package/dist/app/shell.d.ts.map +1 -1
- package/dist/app/shell.js +46 -3
- package/dist/app/shell.js.map +1 -1
- package/dist/app/styles.d.ts.map +1 -1
- package/dist/app/styles.js +9 -26
- package/dist/app/styles.js.map +1 -1
- package/dist/app/views.d.ts +4 -0
- package/dist/app/views.d.ts.map +1 -1
- package/dist/app/views.js +47 -25
- package/dist/app/views.js.map +1 -1
- package/dist/data/build.d.ts.map +1 -1
- package/dist/data/build.js +13 -1
- package/dist/data/build.js.map +1 -1
- package/dist/data/import-map.d.ts +6 -0
- package/dist/data/import-map.d.ts.map +1 -1
- package/dist/data/import-map.js +35 -0
- package/dist/data/import-map.js.map +1 -1
- package/dist/data/theme-studio.d.ts.map +1 -1
- package/dist/data/theme-studio.js +2 -6
- package/dist/data/theme-studio.js.map +1 -1
- package/dist/data/types.d.ts +18 -0
- package/dist/data/types.d.ts.map +1 -1
- package/dist/frame/index.d.ts.map +1 -1
- package/dist/frame/index.js +78 -64
- package/dist/frame/index.js.map +1 -1
- package/dist/node/example-modules.d.ts +8 -0
- package/dist/node/example-modules.d.ts.map +1 -0
- package/dist/node/example-modules.js +28 -0
- package/dist/node/example-modules.js.map +1 -0
- package/dist/node/html.js +1 -1
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -1
- package/dist/node/index.js.map +1 -1
- package/dist/node/inputs.d.ts +2 -0
- package/dist/node/inputs.d.ts.map +1 -1
- package/dist/node/inputs.js +26 -3
- package/dist/node/inputs.js.map +1 -1
- package/dist/node/server.d.ts.map +1 -1
- package/dist/node/server.js +56 -15
- package/dist/node/server.js.map +1 -1
- package/dist/node/static.d.ts +10 -0
- package/dist/node/static.d.ts.map +1 -1
- package/dist/node/static.js +35 -19
- package/dist/node/static.js.map +1 -1
- package/dist/node/workbench.d.ts.map +1 -1
- package/dist/node/workbench.js +25 -4
- package/dist/node/workbench.js.map +1 -1
- package/dist/pages/build.js +2 -2
- package/dist/pages/build.js.map +1 -1
- package/dist/pages/defaults.js +3 -3
- package/dist/pages/defaults.js.map +1 -1
- package/dist/pages/types.d.ts +5 -0
- package/dist/pages/types.d.ts.map +1 -1
- package/dist/panels/catalog-layout-styles.d.ts +2 -0
- package/dist/panels/catalog-layout-styles.d.ts.map +1 -0
- package/dist/panels/catalog-layout-styles.js +54 -0
- package/dist/panels/catalog-layout-styles.js.map +1 -0
- package/dist/panels/catalog-styles.d.ts +2 -0
- package/dist/panels/catalog-styles.d.ts.map +1 -0
- package/dist/panels/catalog-styles.js +106 -0
- package/dist/panels/catalog-styles.js.map +1 -0
- package/dist/panels/catalog.d.ts +7 -3
- package/dist/panels/catalog.d.ts.map +1 -1
- package/dist/panels/catalog.js +24 -4
- package/dist/panels/catalog.js.map +1 -1
- package/dist/panels/component-detail.d.ts +3 -0
- package/dist/panels/component-detail.d.ts.map +1 -1
- package/dist/panels/component-detail.js +488 -256
- package/dist/panels/component-detail.js.map +1 -1
- package/dist/panels/example-styles.d.ts +2 -0
- package/dist/panels/example-styles.d.ts.map +1 -0
- package/dist/panels/example-styles.js +343 -0
- package/dist/panels/example-styles.js.map +1 -0
- package/dist/panels/model.d.ts +11 -0
- package/dist/panels/model.d.ts.map +1 -1
- package/dist/panels/model.js +44 -18
- package/dist/panels/model.js.map +1 -1
- package/dist/panels/styles.d.ts.map +1 -1
- package/dist/panels/styles.js +2 -53
- package/dist/panels/styles.js.map +1 -1
- package/dist/panels/theme-model.d.ts +20 -0
- package/dist/panels/theme-model.d.ts.map +1 -1
- package/dist/panels/theme-model.js +20 -0
- package/dist/panels/theme-model.js.map +1 -1
- package/dist/panels/theme-studio.d.ts.map +1 -1
- package/dist/panels/theme-studio.js +64 -1
- package/dist/panels/theme-studio.js.map +1 -1
- package/dist/preview/frame.d.ts.map +1 -1
- package/dist/preview/frame.js +46 -36
- package/dist/preview/frame.js.map +1 -1
- package/dist/protocol/index.d.ts +15 -0
- package/dist/protocol/index.d.ts.map +1 -1
- package/dist/protocol/index.js +12 -1
- package/dist/protocol/index.js.map +1 -1
- package/package.json +7 -7
|
@@ -8,9 +8,11 @@ import { OddkinElement, prop } from '@oddkin/core/element';
|
|
|
8
8
|
import { signal, untracked } from '@oddkin/core/reactive';
|
|
9
9
|
import { createScope } from '@oddkin/core/scope';
|
|
10
10
|
import { html, nothing, repeat } from '@oddkin/core/template';
|
|
11
|
-
import { createIsolatedPreview } from '../preview/frame.js';
|
|
12
|
-
import {
|
|
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';
|
|
13
14
|
import { contentStyles, panelHostStyles } from './styles.js';
|
|
15
|
+
import { exampleStyles } from './example-styles.js';
|
|
14
16
|
const LOG_LIMIT = 50;
|
|
15
17
|
const WIDTHS = {
|
|
16
18
|
narrow: 320,
|
|
@@ -40,11 +42,27 @@ const notDocumented = (key, subject) => {
|
|
|
40
42
|
const bullets = (items) => html `<ul class="wb-list">
|
|
41
43
|
${items.map((item) => html `<li>${inline(item)}</li>`)}
|
|
42
44
|
</ul>`;
|
|
43
|
-
|
|
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) {
|
|
44
62
|
const byId = new Map(coverage.map((item) => [item.id, item]));
|
|
45
63
|
return (id, content) => {
|
|
46
64
|
const item = byId.get(id);
|
|
47
|
-
if (!item)
|
|
65
|
+
if (!item || (quiet && item.state !== 'documented'))
|
|
48
66
|
return nothing;
|
|
49
67
|
return html `<div data-coverage=${id} data-state=${item.state}>
|
|
50
68
|
${item.state === 'documented'
|
|
@@ -70,8 +88,31 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
70
88
|
attribute: false,
|
|
71
89
|
description: 'URL of the preview frame document.',
|
|
72
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
|
+
}),
|
|
73
114
|
}) {
|
|
74
|
-
static styles = [panelHostStyles, contentStyles];
|
|
115
|
+
static styles = [panelHostStyles, contentStyles, exampleStyles];
|
|
75
116
|
#state = signal({
|
|
76
117
|
preset: 'default',
|
|
77
118
|
attributes: {},
|
|
@@ -87,6 +128,12 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
87
128
|
alert: false,
|
|
88
129
|
});
|
|
89
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);
|
|
90
137
|
#hosts;
|
|
91
138
|
#previews = [];
|
|
92
139
|
#previewScope;
|
|
@@ -114,10 +161,41 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
114
161
|
this.#key = '';
|
|
115
162
|
this.#stop();
|
|
116
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;
|
|
117
195
|
}
|
|
118
196
|
/** Restart the preview only when the component, registration, or frame document changes. */
|
|
119
197
|
#sync(page, frameUrl) {
|
|
120
|
-
const registration = page
|
|
198
|
+
const registration = page && registrationOf(page);
|
|
121
199
|
const key = page ? `${page.tag}\n${String(registration)}\n${frameUrl}` : '';
|
|
122
200
|
if (key === this.#key)
|
|
123
201
|
return;
|
|
@@ -192,6 +270,7 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
192
270
|
#mountPayload(page, registration, index) {
|
|
193
271
|
const state = this.#state.peek();
|
|
194
272
|
const facts = componentFacts(page);
|
|
273
|
+
const fixture = presets(page).some((p) => p.name === state.preset && p.fixture);
|
|
195
274
|
return {
|
|
196
275
|
tag: page.tag,
|
|
197
276
|
registration,
|
|
@@ -199,6 +278,8 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
199
278
|
properties: state.properties,
|
|
200
279
|
attributes: state.attributes,
|
|
201
280
|
slots: state.slots,
|
|
281
|
+
styles: state.styles ?? {},
|
|
282
|
+
...(fixture && page.exampleModule ? { fixture: page.exampleModule } : {}),
|
|
202
283
|
events: facts.events.map((event) => event.name),
|
|
203
284
|
sync: facts.syncPairs,
|
|
204
285
|
settings: this.#frameSettings(index),
|
|
@@ -210,9 +291,9 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
210
291
|
const page = this.page;
|
|
211
292
|
if (!page || !this.isConnected)
|
|
212
293
|
return;
|
|
213
|
-
const registration = page
|
|
294
|
+
const registration = registrationOf(page);
|
|
214
295
|
// Without a registration entry the preview item itself says what is missing.
|
|
215
|
-
if (
|
|
296
|
+
if (registration === undefined) {
|
|
216
297
|
if (!this.#status.peek().keep)
|
|
217
298
|
this.#status.value = { text: '', alert: false };
|
|
218
299
|
return;
|
|
@@ -248,8 +329,8 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
248
329
|
this.#mountAll(scope, page, registration);
|
|
249
330
|
}
|
|
250
331
|
#mountAll(scope, page, registration) {
|
|
251
|
-
|
|
252
|
-
void Promise.all(previews.map((preview, i) => preview.
|
|
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(() => {
|
|
253
334
|
if (!scope.disposed && !this.#status.peek().alert && !this.#status.peek().keep)
|
|
254
335
|
this.#status.value = { text: '', alert: false };
|
|
255
336
|
}, (error) => {
|
|
@@ -258,6 +339,10 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
258
339
|
});
|
|
259
340
|
}
|
|
260
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;
|
|
261
346
|
this.#status.value = { text: `Preview unavailable: ${message(error)}`, alert: true };
|
|
262
347
|
}
|
|
263
348
|
#onMessage(notification, index) {
|
|
@@ -274,6 +359,15 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
274
359
|
};
|
|
275
360
|
this.#log.value = [entry, ...this.#log.peek()].slice(0, LOG_LIMIT);
|
|
276
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
|
+
}
|
|
277
371
|
else if (notification.kind === 'error' && isRecord(payload)) {
|
|
278
372
|
this.#status.value = {
|
|
279
373
|
text: `Preview ${String(payload['phase'])} failed: ${String(payload['message'])}. Use Reset to start a fresh preview.`,
|
|
@@ -304,6 +398,18 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
304
398
|
delete errors[id];
|
|
305
399
|
this.#errors.value = errors;
|
|
306
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
|
+
}
|
|
307
413
|
#writeAttribute(row, value) {
|
|
308
414
|
const name = row.attribute;
|
|
309
415
|
const state = this.#state.peek();
|
|
@@ -340,12 +446,23 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
340
446
|
this.#state.value = { ...state, slots: { ...state.slots, [name]: text } };
|
|
341
447
|
this.#send('slot', { name, text });
|
|
342
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
|
+
}
|
|
343
460
|
/** Mount the current state again in the existing frames (a clean instance, same realm). */
|
|
344
461
|
#remount(note) {
|
|
345
462
|
const page = this.page;
|
|
346
|
-
const registration = page
|
|
463
|
+
const registration = page && registrationOf(page);
|
|
347
464
|
const scope = this.#previewScope;
|
|
348
|
-
if (!page ||
|
|
465
|
+
if (!page || registration === undefined || !scope)
|
|
349
466
|
return;
|
|
350
467
|
this.#status.value = { text: note ?? '', alert: false, ...(note ? { keep: true } : {}) };
|
|
351
468
|
this.#mountAll(scope, page, registration);
|
|
@@ -379,9 +496,9 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
379
496
|
}
|
|
380
497
|
#replace() {
|
|
381
498
|
const page = this.page;
|
|
382
|
-
const registration = page
|
|
499
|
+
const registration = page && registrationOf(page);
|
|
383
500
|
const scope = this.#previewScope;
|
|
384
|
-
if (!page ||
|
|
501
|
+
if (!page || registration === undefined || !scope) {
|
|
385
502
|
this.#restart();
|
|
386
503
|
return Promise.resolve();
|
|
387
504
|
}
|
|
@@ -442,6 +559,10 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
442
559
|
return;
|
|
443
560
|
clipboard.writeText(link).then(() => (this.#status.value = { text: 'Example link copied.', alert: false, keep: true }), () => undefined);
|
|
444
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
|
+
}
|
|
445
566
|
// ---- Rendering ----
|
|
446
567
|
render() {
|
|
447
568
|
const page = this.page;
|
|
@@ -449,62 +570,73 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
449
570
|
if (!page || !index)
|
|
450
571
|
return html `<div class="wb-content"></div>`;
|
|
451
572
|
const entry = index.registry.pages.find((p) => p.id === page.id);
|
|
452
|
-
const
|
|
573
|
+
const reference = this.layout === 'reference';
|
|
453
574
|
const facts = componentFacts(page);
|
|
454
|
-
const
|
|
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;
|
|
455
587
|
// Sections are built eagerly (not with repeat()), so the signals they read are tracked.
|
|
456
588
|
return html `<div class="wb-content">
|
|
457
|
-
${
|
|
458
|
-
|
|
459
|
-
|
|
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)}
|
|
460
606
|
</section>`)}
|
|
461
607
|
</div>`;
|
|
462
608
|
}
|
|
463
|
-
#section(id, page, facts, at) {
|
|
609
|
+
#section(id, page, facts, at, sections) {
|
|
464
610
|
switch (id) {
|
|
465
611
|
case 'overview':
|
|
466
612
|
return this.#overview(page, at);
|
|
467
613
|
case 'interactive-example':
|
|
468
614
|
return this.#interactive(page, facts, at);
|
|
469
615
|
case 'api':
|
|
470
|
-
return this.#
|
|
616
|
+
return this.#reference(facts, at, sections.some((s) => s.id === 'interactive-example'));
|
|
471
617
|
case 'examples':
|
|
472
618
|
return at('examples/examples', () => this.#examples(page));
|
|
473
619
|
case 'tokens':
|
|
474
620
|
return this.#tokens(page, at);
|
|
475
621
|
case 'behavior-and-accessibility':
|
|
476
|
-
return this.#behavior(page,
|
|
622
|
+
return this.#behavior(page, at);
|
|
477
623
|
case 'guidance':
|
|
478
624
|
return this.#guidance(page, at);
|
|
479
625
|
case 'source':
|
|
480
|
-
return
|
|
626
|
+
return this.#source(page, at);
|
|
481
627
|
case 'known-limitations':
|
|
482
|
-
return at('known-limitations/limitations', () =>
|
|
628
|
+
return at('known-limitations/limitations', () => html `<div class="wb-callout">
|
|
629
|
+
${bullets(authoredValues(page.enrichment?.['limitations']))}
|
|
630
|
+
</div>`);
|
|
483
631
|
default:
|
|
484
632
|
return notDocumented(`pages["${page.id}"].sections["${id}"] in oddkin.docs.ts`);
|
|
485
633
|
}
|
|
486
634
|
}
|
|
487
635
|
#overview(page, at) {
|
|
488
636
|
const description = typeof page.declaration['description'] === 'string' ? page.declaration['description'] : '';
|
|
489
|
-
|
|
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
|
|
637
|
+
return at('overview/description', () => description
|
|
494
638
|
.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}`;
|
|
639
|
+
.map((paragraph) => html `<p>${inline(paragraph.replace(/\s*\n\s*/g, ' '))}</p>`));
|
|
508
640
|
}
|
|
509
641
|
/** Intended use and explained mistakes, each paired with the example to use instead. */
|
|
510
642
|
#guidance(page, at) {
|
|
@@ -544,23 +676,96 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
544
676
|
const link = this.#link.value;
|
|
545
677
|
const frameSettings = settings.pair ? {} : this.#frameSettings(0);
|
|
546
678
|
const update = (patch) => this.#applySettings({ ...settings, ...patch });
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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">
|
|
564
769
|
<label
|
|
565
770
|
>Mode
|
|
566
771
|
<select
|
|
@@ -623,8 +828,6 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
623
828
|
}}
|
|
624
829
|
/></label>`
|
|
625
830
|
: nothing}
|
|
626
|
-
<button type="button" @click=${() => this.#reset()}>Reset</button>
|
|
627
|
-
<button type="button" @click=${() => this.#copyLink(page)}>Copy example link</button>
|
|
628
831
|
</div>
|
|
629
832
|
<p class="wb-muted wb-settings-summary">
|
|
630
833
|
Preview only:
|
|
@@ -634,31 +837,7 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
634
837
|
? 'fills the column'
|
|
635
838
|
: `${settings.width === 'custom' ? settings.customWidth : WIDTHS[settings.width]}px container`}
|
|
636
839
|
</p>
|
|
637
|
-
|
|
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>`;
|
|
840
|
+
</div>`;
|
|
662
841
|
}
|
|
663
842
|
#eventLog(facts) {
|
|
664
843
|
if (!facts.events.length)
|
|
@@ -684,88 +863,58 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
684
863
|
: html `<p class="wb-muted">No events yet.</p>`}
|
|
685
864
|
</div>`;
|
|
686
865
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
</td>
|
|
722
|
-
</tr>`)}
|
|
723
|
-
</tbody>
|
|
724
|
-
</table>
|
|
725
|
-
</div>`;
|
|
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}`;
|
|
726
900
|
}
|
|
727
901
|
#apiTable(facts) {
|
|
728
|
-
return
|
|
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>`;
|
|
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)));
|
|
748
903
|
}
|
|
749
904
|
#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
905
|
return html `<tr data-row=${row.id}>
|
|
759
|
-
|
|
760
|
-
<td data-label="
|
|
761
|
-
|
|
762
|
-
${row.nullable ? html `<small>nullable</small>` : nothing}
|
|
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' : ''}
|
|
763
909
|
</td>
|
|
764
|
-
<td data-label="Default">${row.default ?
|
|
765
|
-
<td data-label="
|
|
766
|
-
|
|
767
|
-
${row.description ? inline(row.description) : html `<span class="wb-not-documented">Not documented</span>`}
|
|
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'}
|
|
768
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>
|
|
769
918
|
</tr>`;
|
|
770
919
|
}
|
|
771
920
|
#control(row) {
|
|
@@ -786,7 +935,11 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
786
935
|
const attrValue = attr ? state.attributes[attr] : undefined;
|
|
787
936
|
const propSet = prop !== undefined && prop in state.properties;
|
|
788
937
|
const propValue = prop !== undefined ? state.properties[prop] : undefined;
|
|
789
|
-
const modeSwitch =
|
|
938
|
+
const modeSwitch = this.layout === 'workbench' &&
|
|
939
|
+
row.property &&
|
|
940
|
+
row.attribute &&
|
|
941
|
+
!row.propertyOnly &&
|
|
942
|
+
row.control !== 'json'
|
|
790
943
|
? html `<select
|
|
791
944
|
aria-label=${`Write ${name} as`}
|
|
792
945
|
.value=${mode}
|
|
@@ -806,6 +959,8 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
806
959
|
const checked = mode === 'attribute' ? attrValue !== undefined : propValue === true;
|
|
807
960
|
control = html `<input
|
|
808
961
|
type="checkbox"
|
|
962
|
+
role="switch"
|
|
963
|
+
class="wb-switch"
|
|
809
964
|
aria-label=${label}
|
|
810
965
|
.checked=${checked}
|
|
811
966
|
@change=${(event) => {
|
|
@@ -834,21 +989,22 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
834
989
|
? '\u0000null'
|
|
835
990
|
: String(propValue)
|
|
836
991
|
: '';
|
|
837
|
-
|
|
838
|
-
aria-label=${label}
|
|
839
|
-
.value=${current}
|
|
840
|
-
@change=${(event) => {
|
|
841
|
-
const value = event.target.value;
|
|
992
|
+
const write = (value) => {
|
|
842
993
|
if (mode === 'attribute')
|
|
843
994
|
this.#writeAttribute(row, value === '' ? null : value);
|
|
844
995
|
else if (value === '')
|
|
845
996
|
this.#unsetProperty(row);
|
|
846
997
|
else
|
|
847
998
|
this.#writeProperty(row, value === '\u0000null' ? null : value);
|
|
848
|
-
}
|
|
999
|
+
};
|
|
1000
|
+
const values = row.values ?? [];
|
|
1001
|
+
control = html `<select
|
|
1002
|
+
aria-label=${label}
|
|
1003
|
+
.value=${current}
|
|
1004
|
+
@change=${(event) => write(event.target.value)}
|
|
849
1005
|
>
|
|
850
1006
|
<option value="" ?selected=${current === ''}>(unset)</option>
|
|
851
|
-
${
|
|
1007
|
+
${values.map((value) => html `<option value=${value} ?selected=${current === value}>${value}</option>`)}
|
|
852
1008
|
${mode === 'property' && row.nullable
|
|
853
1009
|
? html `<option value=${'\u0000null'} ?selected=${current === '\u0000null'}>
|
|
854
1010
|
null
|
|
@@ -893,25 +1049,20 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
893
1049
|
aria-label=${label}
|
|
894
1050
|
placeholder=${isSet ? '' : 'unset'}
|
|
895
1051
|
.value=${shown}
|
|
896
|
-
@
|
|
1052
|
+
@input=${(event) => {
|
|
1053
|
+
// The preview follows the field as the reader types; an unfinished number waits.
|
|
897
1054
|
const text = event.target.value;
|
|
898
|
-
if (mode
|
|
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);
|
|
1055
|
+
if (this.#textProblem(number, mode, text))
|
|
904
1056
|
return;
|
|
905
|
-
|
|
906
|
-
|
|
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
|
-
}
|
|
1057
|
+
if (mode === 'attribute')
|
|
1058
|
+
this.#writeAttribute(row, text);
|
|
913
1059
|
else
|
|
914
|
-
this.#writeProperty(row, text);
|
|
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);
|
|
915
1066
|
}}
|
|
916
1067
|
/>
|
|
917
1068
|
<button
|
|
@@ -948,61 +1099,133 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
948
1099
|
return html `<div class="wb-cell-controls">${control} ${modeSwitch}</div>
|
|
949
1100
|
${error ? html `<p class="wb-error" role="alert">${error}</p>` : nothing}`;
|
|
950
1101
|
}
|
|
951
|
-
|
|
952
|
-
|
|
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) {
|
|
953
1104
|
return html `<div class="wb-table-wrap">
|
|
954
|
-
<table class="wb-stack" data-table
|
|
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>
|
|
955
1112
|
<thead>
|
|
956
1113
|
<tr>
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1114
|
+
${heads.map((head, i) => html `<th scope="col" colspan=${heads.length === 5 && i === 3 ? '2' : null}>
|
|
1115
|
+
${head}
|
|
1116
|
+
</th>`)}
|
|
960
1117
|
</tr>
|
|
961
1118
|
</thead>
|
|
962
1119
|
<tbody>
|
|
963
|
-
${
|
|
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
|
-
})}
|
|
1120
|
+
${rows}
|
|
981
1121
|
</tbody>
|
|
982
1122
|
</table>
|
|
983
1123
|
</div>`;
|
|
984
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. */
|
|
985
1186
|
#examples(page) {
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
:
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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}`;
|
|
1006
1229
|
}
|
|
1007
1230
|
#tokens(page, at) {
|
|
1008
1231
|
const mappings = Array.isArray(page.enrichment?.['styleMappings'])
|
|
@@ -1083,38 +1306,47 @@ export class ComponentDetailElement extends OddkinElement.with({
|
|
|
1083
1306
|
</table>
|
|
1084
1307
|
</div>`)}`;
|
|
1085
1308
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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>`;
|
|
1100
1323
|
}
|
|
1101
|
-
|
|
1324
|
+
/** Where the element lives and how to register it (Figma 232:9845). */
|
|
1325
|
+
#source(page, at) {
|
|
1102
1326
|
const definition = isRecord(page.enrichment?.['definition'])
|
|
1103
1327
|
? page.enrichment['definition']
|
|
1104
1328
|
: undefined;
|
|
1105
1329
|
const ref = isRecord(definition?.['ref']) ? definition['ref'] : undefined;
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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>`)}`;
|
|
1118
1350
|
}
|
|
1119
1351
|
}
|
|
1120
1352
|
//# sourceMappingURL=component-detail.js.map
|