@agent-ui-kit/components 0.0.3
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/dist/base-styles.css +8 -0
- package/dist/component-styles.css +147 -0
- package/dist/controls/_base/index.d.ts +3 -0
- package/dist/controls/_base/index.js +9 -0
- package/dist/controls/_base/indicator-element.d.ts +132 -0
- package/dist/controls/_base/indicator-element.js +158 -0
- package/dist/controls/_base/listbox-element.d.ts +70 -0
- package/dist/controls/_base/listbox-element.js +74 -0
- package/dist/controls/_base/range-element.d.ts +120 -0
- package/dist/controls/_base/range-element.js +158 -0
- package/dist/controls/_surface/container-box.css +301 -0
- package/dist/controls/_surface/container.css +156 -0
- package/dist/controls/_token-surface/token-surface.d.ts +35 -0
- package/dist/controls/_token-surface/token-surface.js +97 -0
- package/dist/controls/attachment/attachment-meta.d.ts +8 -0
- package/dist/controls/attachment/attachment-meta.js +84 -0
- package/dist/controls/attachment/attachment.css +91 -0
- package/dist/controls/attachment/attachment.d.ts +40 -0
- package/dist/controls/attachment/attachment.js +70 -0
- package/dist/controls/avatar/avatar-initials.d.ts +4 -0
- package/dist/controls/avatar/avatar-initials.js +30 -0
- package/dist/controls/avatar/avatar.css +87 -0
- package/dist/controls/avatar/avatar.d.ts +40 -0
- package/dist/controls/avatar/avatar.js +77 -0
- package/dist/controls/badge/badge.css +175 -0
- package/dist/controls/badge/badge.d.ts +36 -0
- package/dist/controls/badge/badge.js +71 -0
- package/dist/controls/bar-chart/bar-chart.css +110 -0
- package/dist/controls/bar-chart/bar-chart.d.ts +26 -0
- package/dist/controls/bar-chart/bar-chart.js +80 -0
- package/dist/controls/bar-chart/bar-math.d.ts +31 -0
- package/dist/controls/bar-chart/bar-math.js +106 -0
- package/dist/controls/button/button.css +263 -0
- package/dist/controls/button/button.d.ts +61 -0
- package/dist/controls/button/button.js +115 -0
- package/dist/controls/calendar/calendar.css +495 -0
- package/dist/controls/calendar/calendar.d.ts +161 -0
- package/dist/controls/calendar/calendar.js +1004 -0
- package/dist/controls/card/card-content.d.ts +24 -0
- package/dist/controls/card/card-content.js +133 -0
- package/dist/controls/card/card-footer.d.ts +3 -0
- package/dist/controls/card/card-footer.js +16 -0
- package/dist/controls/card/card-header.d.ts +3 -0
- package/dist/controls/card/card-header.js +18 -0
- package/dist/controls/card/card.css +445 -0
- package/dist/controls/card/card.d.ts +51 -0
- package/dist/controls/card/card.js +61 -0
- package/dist/controls/checkbox/checkbox.css +204 -0
- package/dist/controls/checkbox/checkbox.d.ts +114 -0
- package/dist/controls/checkbox/checkbox.js +130 -0
- package/dist/controls/code/code.css +72 -0
- package/dist/controls/code/code.d.ts +23 -0
- package/dist/controls/code/code.js +40 -0
- package/dist/controls/color-picker/color-picker.css +209 -0
- package/dist/controls/color-picker/color-picker.d.ts +84 -0
- package/dist/controls/color-picker/color-picker.js +545 -0
- package/dist/controls/color-picker/color.d.ts +40 -0
- package/dist/controls/color-picker/color.js +159 -0
- package/dist/controls/column/column.css +151 -0
- package/dist/controls/column/column.d.ts +107 -0
- package/dist/controls/column/column.js +57 -0
- package/dist/controls/combo-box/combo-box.css +397 -0
- package/dist/controls/combo-box/combo-box.d.ts +133 -0
- package/dist/controls/combo-box/combo-box.js +680 -0
- package/dist/controls/command-modal/command-modal.css +226 -0
- package/dist/controls/command-modal/command-modal.d.ts +42 -0
- package/dist/controls/command-modal/command-modal.js +315 -0
- package/dist/controls/disclosure/disclosure.css +162 -0
- package/dist/controls/disclosure/disclosure.d.ts +37 -0
- package/dist/controls/disclosure/disclosure.js +207 -0
- package/dist/controls/field/field.css +140 -0
- package/dist/controls/field/field.d.ts +27 -0
- package/dist/controls/field/field.js +228 -0
- package/dist/controls/form-provider/form-provider.css +40 -0
- package/dist/controls/form-provider/form-provider.d.ts +42 -0
- package/dist/controls/form-provider/form-provider.js +105 -0
- package/dist/controls/grid/grid.css +72 -0
- package/dist/controls/grid/grid.d.ts +60 -0
- package/dist/controls/grid/grid.js +56 -0
- package/dist/controls/icon/icon.css +37 -0
- package/dist/controls/icon/icon.d.ts +25 -0
- package/dist/controls/icon/icon.js +55 -0
- package/dist/controls/index.d.ts +57 -0
- package/dist/controls/index.js +136 -0
- package/dist/controls/ladder/ladder.css +89 -0
- package/dist/controls/ladder/ladder.d.ts +27 -0
- package/dist/controls/ladder/ladder.js +78 -0
- package/dist/controls/list/list.css +118 -0
- package/dist/controls/list/list.d.ts +84 -0
- package/dist/controls/list/list.js +48 -0
- package/dist/controls/menu/menu.css +195 -0
- package/dist/controls/menu/menu.d.ts +44 -0
- package/dist/controls/menu/menu.js +248 -0
- package/dist/controls/modal/modal.css +99 -0
- package/dist/controls/modal/modal.d.ts +61 -0
- package/dist/controls/modal/modal.js +155 -0
- package/dist/controls/popover/popover.css +83 -0
- package/dist/controls/popover/popover.d.ts +44 -0
- package/dist/controls/popover/popover.js +164 -0
- package/dist/controls/progress/progress.css +106 -0
- package/dist/controls/progress/progress.d.ts +28 -0
- package/dist/controls/progress/progress.js +80 -0
- package/dist/controls/radio/radio-group.css +84 -0
- package/dist/controls/radio/radio-group.d.ts +152 -0
- package/dist/controls/radio/radio-group.js +350 -0
- package/dist/controls/radio/radio.css +219 -0
- package/dist/controls/radio/radio.d.ts +19 -0
- package/dist/controls/radio/radio.js +75 -0
- package/dist/controls/ramp/ramp.css +75 -0
- package/dist/controls/ramp/ramp.d.ts +29 -0
- package/dist/controls/ramp/ramp.js +72 -0
- package/dist/controls/row/row.css +152 -0
- package/dist/controls/row/row.d.ts +95 -0
- package/dist/controls/row/row.js +45 -0
- package/dist/controls/segment/segment.css +66 -0
- package/dist/controls/segment/segment.d.ts +3 -0
- package/dist/controls/segment/segment.js +26 -0
- package/dist/controls/segmented-control/segmented-control.css +189 -0
- package/dist/controls/segmented-control/segmented-control.d.ts +20 -0
- package/dist/controls/segmented-control/segmented-control.js +64 -0
- package/dist/controls/select/select.css +467 -0
- package/dist/controls/select/select.d.ts +147 -0
- package/dist/controls/select/select.js +538 -0
- package/dist/controls/slider/slider.css +231 -0
- package/dist/controls/slider/slider.d.ts +111 -0
- package/dist/controls/slider/slider.js +60 -0
- package/dist/controls/slider-multi/slider-multi.css +197 -0
- package/dist/controls/slider-multi/slider-multi.d.ts +149 -0
- package/dist/controls/slider-multi/slider-multi.js +380 -0
- package/dist/controls/sparkline/sparkline-math.d.ts +23 -0
- package/dist/controls/sparkline/sparkline-math.js +79 -0
- package/dist/controls/sparkline/sparkline.css +61 -0
- package/dist/controls/sparkline/sparkline.d.ts +27 -0
- package/dist/controls/sparkline/sparkline.js +74 -0
- package/dist/controls/split/constrain.d.ts +44 -0
- package/dist/controls/split/constrain.js +126 -0
- package/dist/controls/split/split-pane.css +63 -0
- package/dist/controls/split/split-pane.d.ts +49 -0
- package/dist/controls/split/split-pane.js +42 -0
- package/dist/controls/split/split.css +127 -0
- package/dist/controls/split/split.d.ts +42 -0
- package/dist/controls/split/split.js +364 -0
- package/dist/controls/stat/stat-model.d.ts +22 -0
- package/dist/controls/stat/stat-model.js +69 -0
- package/dist/controls/stat/stat.css +101 -0
- package/dist/controls/stat/stat.d.ts +30 -0
- package/dist/controls/stat/stat.js +73 -0
- package/dist/controls/status-stream/status-stream.css +151 -0
- package/dist/controls/status-stream/status-stream.d.ts +108 -0
- package/dist/controls/status-stream/status-stream.js +417 -0
- package/dist/controls/swatch/swatch.css +66 -0
- package/dist/controls/swatch/swatch.d.ts +28 -0
- package/dist/controls/swatch/swatch.js +74 -0
- package/dist/controls/swiper/swiper-item.css +17 -0
- package/dist/controls/swiper/swiper-item.d.ts +31 -0
- package/dist/controls/swiper/swiper-item.js +47 -0
- package/dist/controls/swiper/swiper-label.css +10 -0
- package/dist/controls/swiper/swiper-label.d.ts +4 -0
- package/dist/controls/swiper/swiper-label.js +20 -0
- package/dist/controls/swiper/swiper-paddles.css +26 -0
- package/dist/controls/swiper/swiper-paddles.d.ts +10 -0
- package/dist/controls/swiper/swiper-paddles.js +62 -0
- package/dist/controls/swiper/swiper-pagination.css +60 -0
- package/dist/controls/swiper/swiper-pagination.d.ts +31 -0
- package/dist/controls/swiper/swiper-pagination.js +78 -0
- package/dist/controls/swiper/swiper.css +153 -0
- package/dist/controls/swiper/swiper.d.ts +158 -0
- package/dist/controls/swiper/swiper.js +659 -0
- package/dist/controls/switch/switch.css +194 -0
- package/dist/controls/switch/switch.d.ts +5 -0
- package/dist/controls/switch/switch.js +18 -0
- package/dist/controls/table/table-model.d.ts +43 -0
- package/dist/controls/table/table-model.js +124 -0
- package/dist/controls/table/table.css +90 -0
- package/dist/controls/table/table.d.ts +29 -0
- package/dist/controls/table/table.js +178 -0
- package/dist/controls/tabs/tab-panel.d.ts +10 -0
- package/dist/controls/tabs/tab-panel.js +89 -0
- package/dist/controls/tabs/tab.d.ts +26 -0
- package/dist/controls/tabs/tab.js +67 -0
- package/dist/controls/tabs/tabs.css +181 -0
- package/dist/controls/tabs/tabs.d.ts +61 -0
- package/dist/controls/tabs/tabs.js +176 -0
- package/dist/controls/text/href.d.ts +26 -0
- package/dist/controls/text/href.js +48 -0
- package/dist/controls/text/text.css +328 -0
- package/dist/controls/text/text.d.ts +85 -0
- package/dist/controls/text/text.js +273 -0
- package/dist/controls/text-field/text-field.css +498 -0
- package/dist/controls/text-field/text-field.d.ts +226 -0
- package/dist/controls/text-field/text-field.js +905 -0
- package/dist/controls/textarea/textarea.css +251 -0
- package/dist/controls/textarea/textarea.d.ts +136 -0
- package/dist/controls/textarea/textarea.js +312 -0
- package/dist/controls/theme-provider/theme-provider.css +48 -0
- package/dist/controls/theme-provider/theme-provider.d.ts +59 -0
- package/dist/controls/theme-provider/theme-provider.js +40 -0
- package/dist/controls/timeline/timeline.css +33 -0
- package/dist/controls/timeline/timeline.d.ts +39 -0
- package/dist/controls/timeline/timeline.js +52 -0
- package/dist/controls/timeline-item/timeline-item.css +495 -0
- package/dist/controls/timeline-item/timeline-item.d.ts +79 -0
- package/dist/controls/timeline-item/timeline-item.js +360 -0
- package/dist/controls/toast/toast-region.css +56 -0
- package/dist/controls/toast/toast-region.d.ts +20 -0
- package/dist/controls/toast/toast-region.js +113 -0
- package/dist/controls/toast/toast.css +96 -0
- package/dist/controls/toast/toast.d.ts +28 -0
- package/dist/controls/toast/toast.js +191 -0
- package/dist/controls/toolbar/toolbar.css +125 -0
- package/dist/controls/toolbar/toolbar.d.ts +108 -0
- package/dist/controls/toolbar/toolbar.js +68 -0
- package/dist/controls/tooltip/tooltip.css +80 -0
- package/dist/controls/tooltip/tooltip.d.ts +57 -0
- package/dist/controls/tooltip/tooltip.js +205 -0
- package/dist/descriptor/component-descriptor.d.ts +112 -0
- package/dist/descriptor/component-descriptor.js +492 -0
- package/dist/descriptor/index.d.ts +1 -0
- package/dist/descriptor/index.js +5 -0
- package/dist/dom/container.d.ts +45 -0
- package/dist/dom/container.js +60 -0
- package/dist/dom/element.d.ts +98 -0
- package/dist/dom/element.js +221 -0
- package/dist/dom/form.d.ts +240 -0
- package/dist/dom/form.js +399 -0
- package/dist/dom/index.d.ts +10 -0
- package/dist/dom/index.js +31 -0
- package/dist/dom/props.d.ts +56 -0
- package/dist/dom/props.js +231 -0
- package/dist/dom/repeat.d.ts +10 -0
- package/dist/dom/repeat.js +202 -0
- package/dist/dom/template.d.ts +161 -0
- package/dist/dom/template.js +634 -0
- package/dist/dom/watch.d.ts +11 -0
- package/dist/dom/watch.js +82 -0
- package/dist/foundation-styles.css +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/reactive/graph.d.ts +65 -0
- package/dist/reactive/graph.js +310 -0
- package/dist/reactive/index.d.ts +3 -0
- package/dist/reactive/index.js +4 -0
- package/dist/reactive/scheduler.d.ts +13 -0
- package/dist/reactive/scheduler.js +49 -0
- package/dist/traits/area-drag.d.ts +12 -0
- package/dist/traits/area-drag.js +70 -0
- package/dist/traits/form-registry.d.ts +36 -0
- package/dist/traits/form-registry.js +108 -0
- package/dist/traits/index.d.ts +24 -0
- package/dist/traits/index.js +19 -0
- package/dist/traits/overlay.d.ts +50 -0
- package/dist/traits/overlay.js +277 -0
- package/dist/traits/pane-resize.d.ts +26 -0
- package/dist/traits/pane-resize.js +103 -0
- package/dist/traits/press-activation.d.ts +11 -0
- package/dist/traits/press-activation.js +55 -0
- package/dist/traits/roving-focus.d.ts +52 -0
- package/dist/traits/roving-focus.js +214 -0
- package/dist/traits/scroll-fade.d.ts +28 -0
- package/dist/traits/scroll-fade.js +120 -0
- package/dist/traits/selection-commit.d.ts +30 -0
- package/dist/traits/selection-commit.js +146 -0
- package/dist/traits/tabbable.d.ts +12 -0
- package/dist/traits/tabbable.js +59 -0
- package/dist/traits/track-user-invalid.d.ts +36 -0
- package/dist/traits/track-user-invalid.js +52 -0
- package/dist/traits/value-codec.d.ts +120 -0
- package/dist/traits/value-codec.js +497 -0
- package/dist/traits/value-drag.d.ts +18 -0
- package/dist/traits/value-drag.js +84 -0
- package/package.json +263 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
// component-descriptor.ts — the reusable {name}.md frontmatter reader + the descriptor schema (ADR-0004).
|
|
2
|
+
//
|
|
3
|
+
// A component's public surface is recorded as `{name}.md` YAML frontmatter (ADR-0004 — the descriptor
|
|
4
|
+
// replaces the never-built `{name}.api.json`). Two artifacts live here, both consumed by the governance
|
|
5
|
+
// slices that target that fence:
|
|
6
|
+
//
|
|
7
|
+
// 1. parseDescriptor(fence) — the fence-scoped READER (factored out of the s8 button probe so
|
|
8
|
+
// the s10 contract↔props trip-wire + every future component reuse
|
|
9
|
+
// ONE parser, never a divergent copy — the drift the process kills).
|
|
10
|
+
// 2. validateComponentDescriptor(desc) — the descriptor SCHEMA: a hand-rolled, TOTAL structural validator
|
|
11
|
+
// that IS the referential standard the frontmatter is checked
|
|
12
|
+
// against (process.md §4's "api-contract schema", now a frontmatter
|
|
13
|
+
// schema per ADR-0004). It is the STRUCTURAL standard only — it does
|
|
14
|
+
// NOT compare the frontmatter against the live class.
|
|
15
|
+
// 3. compareDescriptorToProps(a, p) — the contract↔props TRIP-WIRE (process.md §1): asserts the
|
|
16
|
+
// descriptor's `attributes[]` and the live `static props` table
|
|
17
|
+
// (what `finalize(Class)` installs from) are a faithful BIJECTION,
|
|
18
|
+
// so the descriptor cannot drift from the real component. Fleet-wide
|
|
19
|
+
// (any component runs it); kept import-free by reading the live props
|
|
20
|
+
// STRUCTURALLY (LivePropConfig) instead of pulling the dom layer.
|
|
21
|
+
//
|
|
22
|
+
// Zero-dep by ruling: no YAML parser is installed and the descriptor is a CONTROLLED format, so the reader is
|
|
23
|
+
// a small indentation-scoped parser over the subset the descriptor uses (top-level scalars · `- ` sequences ·
|
|
24
|
+
// nested maps · inline `[a, b]` arrays · trailing `#` comments). It never executes the file; it reads text.
|
|
25
|
+
// ── reader ──────────────────────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
/** Split the leading `---`…`---` frontmatter fence from the prose body. Throws if there is no fence. */
|
|
27
|
+
export function splitFrontmatter(src) {
|
|
28
|
+
const m = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(src);
|
|
29
|
+
if (!m)
|
|
30
|
+
throw new Error('source has no leading --- frontmatter fence');
|
|
31
|
+
return { fence: m[1], body: m[2] };
|
|
32
|
+
}
|
|
33
|
+
const COMMENT_OR_BLANK = /^\s*#|^\s*$/;
|
|
34
|
+
/** Strip a whitespace-led inline `#` comment. (Controlled format: values carry no literal `#`.) */
|
|
35
|
+
const stripComment = (raw) => raw.replace(/\s+#.*$/, '').trim();
|
|
36
|
+
/** Drop a single pair of surrounding quotes (e.g. `'null'` → `null`). */
|
|
37
|
+
const unquote = (s) => /^(['"])([\s\S]*)\1$/.exec(s)?.[2] ?? s;
|
|
38
|
+
/** A scalar value: comment-stripped, unquoted, trimmed. */
|
|
39
|
+
const scalarValue = (raw) => unquote(stripComment(raw));
|
|
40
|
+
/** Sentinel key a BARE-SCALAR sequence item (`- ready`, no `key:` of its own) stores its value under. `#` can
|
|
41
|
+
* never be a real field name (fields match `[A-Za-z][\w]*`), so it cannot collide — and `scalarSeq` reads it back. */
|
|
42
|
+
const BARE_SCALAR_KEY = '#scalar';
|
|
43
|
+
/** Group the fence into top-level (column-0) `key:` blocks with their indented child lines. */
|
|
44
|
+
function toBlocks(fence) {
|
|
45
|
+
const blocks = [];
|
|
46
|
+
let current = null;
|
|
47
|
+
for (const line of fence.split('\n')) {
|
|
48
|
+
if (COMMENT_OR_BLANK.test(line))
|
|
49
|
+
continue;
|
|
50
|
+
const top = /^([A-Za-z][\w]*):(.*)$/.exec(line); // `^[A-Za-z]` ⇒ column 0, no leading whitespace
|
|
51
|
+
if (top) {
|
|
52
|
+
current = { key: top[1], inline: top[2], childLines: [] };
|
|
53
|
+
blocks.push(current);
|
|
54
|
+
}
|
|
55
|
+
else if (current) {
|
|
56
|
+
current.childLines.push(line);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return blocks;
|
|
60
|
+
}
|
|
61
|
+
/** Parse a `key: value` field line into a sequence item (inline `[a, b]` → string[]); a bare scalar with no
|
|
62
|
+
* `key:` of its own (e.g. customStates `- ready`) is kept under BARE_SCALAR_KEY (was DROPPED before this fix).
|
|
63
|
+
* An inline array element ELEMENT is `.filter`ed for blank/trailing-comma ARTIFACTS (an empty split segment,
|
|
64
|
+
* e.g. `[a, b, ]`'s trailing slot) BEFORE it is `unquote()`d — in that order, so a genuinely-quoted empty
|
|
65
|
+
* string (`''`/`""`, 2 characters pre-unquote, needed for ADR-0083's `landmark` enum whose first member IS
|
|
66
|
+
* the empty string) survives the artifact filter and only THEN unquotes to a real `''`, rather than being
|
|
67
|
+
* indistinguishable from a blank artifact and dropped. Every existing bare (unquoted) token is unaffected —
|
|
68
|
+
* `unquote` is a no-op unless the WHOLE element is wrapped in one matching quote pair. */
|
|
69
|
+
function addField(item, text) {
|
|
70
|
+
const m = /^\s*([A-Za-z][\w]*):\s*([\s\S]*)$/.exec(text);
|
|
71
|
+
if (!m) {
|
|
72
|
+
const bare = scalarValue(text);
|
|
73
|
+
if (bare !== '')
|
|
74
|
+
item.set(BARE_SCALAR_KEY, bare);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const value = scalarValue(m[2]);
|
|
78
|
+
// Quoted-default marker (TKT-0070, closing TKT-0069's String(default) gate hole): `unquote` erases the
|
|
79
|
+
// authored quoting, but a QUOTED scalar default on a boolean/number/integer attribute is exactly the
|
|
80
|
+
// YAML mis-typing the contract↔props trip-wire cannot see (it compares String(config.default), so
|
|
81
|
+
// 'false' == false). Record quotedness under a sentinel key (the BARE_SCALAR_KEY trick — `#` can never
|
|
82
|
+
// be a real field name) for the validator's rule 5b below. Only `default` needs it.
|
|
83
|
+
if (m[1] === 'default' && /^(['"])[\s\S]*\1$/.test(stripComment(m[2])))
|
|
84
|
+
item.set('#quoted:default', 'true');
|
|
85
|
+
if (/^\[.*\]$/.test(value)) {
|
|
86
|
+
item.set(m[1], value.replace(/^\[|\]$/g, '').split(',').map((s) => s.trim()).filter((s) => s !== '').map((s) => unquote(s)));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
item.set(m[1], value);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Parse an indented `- ` sequence block into a list of field mappings. */
|
|
93
|
+
function parseSequence(lines) {
|
|
94
|
+
const items = [];
|
|
95
|
+
let current = null;
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const dash = /^\s*-\s*(.*)$/.exec(line);
|
|
98
|
+
if (dash) {
|
|
99
|
+
current = new Map();
|
|
100
|
+
items.push(current);
|
|
101
|
+
addField(current, dash[1]);
|
|
102
|
+
}
|
|
103
|
+
else if (current) {
|
|
104
|
+
addField(current, line);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return items;
|
|
108
|
+
}
|
|
109
|
+
/** Parse an indented nested-mapping block (e.g. `face`/`aria`/`geometry`) into a key→scalar map. */
|
|
110
|
+
function parseMap(lines) {
|
|
111
|
+
const map = new Map();
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
const m = /^\s*([A-Za-z][\w]*):\s*([\s\S]*)$/.exec(line);
|
|
114
|
+
if (m)
|
|
115
|
+
map.set(m[1], scalarValue(m[2]));
|
|
116
|
+
}
|
|
117
|
+
return map;
|
|
118
|
+
}
|
|
119
|
+
/** Shape one raw `attributes[]` item into a typed ParsedAttribute. */
|
|
120
|
+
function toAttribute(item) {
|
|
121
|
+
const name = item.get('name');
|
|
122
|
+
const type = item.get('type');
|
|
123
|
+
const values = item.get('values');
|
|
124
|
+
const def = item.get('default');
|
|
125
|
+
const reflect = item.get('reflect');
|
|
126
|
+
return {
|
|
127
|
+
name: typeof name === 'string' ? name : undefined,
|
|
128
|
+
type: typeof type === 'string' ? type : undefined,
|
|
129
|
+
values: Array.isArray(values) ? values : typeof values === 'string' ? [values] : undefined,
|
|
130
|
+
default: typeof def === 'string' ? def : undefined,
|
|
131
|
+
reflect: reflect === 'true' ? true : reflect === 'false' ? false : undefined,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Read a frontmatter fence into the structured descriptor (top-level keys · scalars · sequences · maps). */
|
|
135
|
+
export function parseDescriptor(fence) {
|
|
136
|
+
const scalars = new Map();
|
|
137
|
+
const sequences = new Map();
|
|
138
|
+
const maps = new Map();
|
|
139
|
+
for (const block of toBlocks(fence)) {
|
|
140
|
+
const inline = scalarValue(block.inline);
|
|
141
|
+
if (inline === '[]')
|
|
142
|
+
sequences.set(block.key, []);
|
|
143
|
+
else if (inline === '{}')
|
|
144
|
+
maps.set(block.key, new Map());
|
|
145
|
+
else if (inline !== '')
|
|
146
|
+
scalars.set(block.key, inline);
|
|
147
|
+
else if (block.childLines.length > 0 && /^\s*-/.test(block.childLines[0]))
|
|
148
|
+
sequences.set(block.key, parseSequence(block.childLines));
|
|
149
|
+
else
|
|
150
|
+
maps.set(block.key, parseMap(block.childLines));
|
|
151
|
+
}
|
|
152
|
+
const topLevelKeys = new Set([...scalars.keys(), ...sequences.keys(), ...maps.keys()]);
|
|
153
|
+
const attributes = (sequences.get('attributes') ?? []).map(toAttribute);
|
|
154
|
+
return { topLevelKeys, scalars, sequences, maps, attributes };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Read a BARE-SCALAR sequence field as the list of values its items carry (e.g. customStates `- ready` →
|
|
158
|
+
* `['ready']`). A bare-scalar item parses to a single-entry map keyed by BARE_SCALAR_KEY (the addField fix);
|
|
159
|
+
* this reads those back. Items that are full `key: value` mappings (e.g. attributes/slots) contribute nothing.
|
|
160
|
+
*/
|
|
161
|
+
export function scalarSeq(desc, field) {
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const item of desc.sequences.get(field) ?? []) {
|
|
164
|
+
const v = item.get(BARE_SCALAR_KEY);
|
|
165
|
+
if (typeof v === 'string')
|
|
166
|
+
out.push(v);
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
// ── schema ──────────────────────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
/** The structural defects validateComponentDescriptor reports. */
|
|
172
|
+
export const DESCRIPTOR_CODES = ['MISSING_FIELD', 'BAD_SHAPE', 'BAD_TAG', 'BAD_TIER', 'BAD_EXTENDS', 'BAD_ATTRIBUTE', 'BAD_FACE'];
|
|
173
|
+
// The descriptor field set (ADR-0004 / plan §10) and the shape each field MUST parse into.
|
|
174
|
+
const FIELD_SHAPE = {
|
|
175
|
+
tag: 'scalar',
|
|
176
|
+
tier: 'scalar',
|
|
177
|
+
extends: 'scalar',
|
|
178
|
+
attributes: 'sequence',
|
|
179
|
+
properties: 'sequence',
|
|
180
|
+
events: 'sequence',
|
|
181
|
+
slots: 'sequence',
|
|
182
|
+
parts: 'sequence',
|
|
183
|
+
customStates: 'sequence',
|
|
184
|
+
face: 'map',
|
|
185
|
+
aria: 'map',
|
|
186
|
+
keyboard: 'sequence',
|
|
187
|
+
geometry: 'map',
|
|
188
|
+
forcedColors: 'scalar',
|
|
189
|
+
};
|
|
190
|
+
const SIZE_CLASSES = ['control', 'indicator', 'range', 'pattern', 'container', 'layout', 'display']; // geometry.md size-class set (range added for Wave-2 Range family)
|
|
191
|
+
const ATTR_TYPES = ['enum', 'boolean', 'number', 'string', 'json']; // the attribute codec set
|
|
192
|
+
const BASE_CLASSES = [
|
|
193
|
+
'UIElement',
|
|
194
|
+
'UIFormElement',
|
|
195
|
+
'UIContainerElement',
|
|
196
|
+
// Wave 0/1 control-suite bases (controls/_base/); each is a direct-extends target for leaf controls.
|
|
197
|
+
'UIIndicatorElement', // checkbox / switch / radio (ADR-0042)
|
|
198
|
+
'UIRangeElement', // slider family
|
|
199
|
+
'UIListboxElement', // listbox / select options (listbox-roving LLD-C3)
|
|
200
|
+
];
|
|
201
|
+
const BOOLEANS = ['true', 'false'];
|
|
202
|
+
const has = (set, v) => set.includes(v);
|
|
203
|
+
/**
|
|
204
|
+
* Validate a parsed frontmatter against the component-descriptor schema (ADR-0004). TOTAL — never throws;
|
|
205
|
+
* every defect is a structured DescriptorFailure. This is the structural referential standard the contract
|
|
206
|
+
* trip-wire (s10) builds on; it does NOT compare against the live class.
|
|
207
|
+
*/
|
|
208
|
+
export function validateComponentDescriptor(d) {
|
|
209
|
+
const failures = [];
|
|
210
|
+
const add = (code, path, message) => void failures.push({ code, path, message });
|
|
211
|
+
// 1 — every required field is present and parsed into its declared shape.
|
|
212
|
+
for (const field of Object.keys(FIELD_SHAPE)) {
|
|
213
|
+
if (!d.topLevelKeys.has(field)) {
|
|
214
|
+
add('MISSING_FIELD', field, `required field "${field}" is absent`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const shape = FIELD_SHAPE[field];
|
|
218
|
+
const present = shape === 'scalar' ? d.scalars.has(field) : shape === 'sequence' ? d.sequences.has(field) : d.maps.has(field);
|
|
219
|
+
if (!present)
|
|
220
|
+
add('BAD_SHAPE', field, `field "${field}" must be a ${shape}`);
|
|
221
|
+
}
|
|
222
|
+
// 2 — tag is a ui-{name} custom-element tag.
|
|
223
|
+
const tag = d.scalars.get('tag');
|
|
224
|
+
if (tag !== undefined && !/^ui-[a-z][a-z0-9-]*$/.test(tag))
|
|
225
|
+
add('BAD_TAG', 'tag', `tag "${tag}" must match ui-{name}`);
|
|
226
|
+
// 3 — tier is one of the geometry size-classes.
|
|
227
|
+
const tier = d.scalars.get('tier');
|
|
228
|
+
if (tier !== undefined && !has(SIZE_CLASSES, tier))
|
|
229
|
+
add('BAD_TIER', 'tier', `tier "${tier}" is not a size-class`);
|
|
230
|
+
// 4 — extends is a known base element.
|
|
231
|
+
const ext = d.scalars.get('extends');
|
|
232
|
+
if (ext !== undefined && !has(BASE_CLASSES, ext))
|
|
233
|
+
add('BAD_EXTENDS', 'extends', `extends "${ext}" is not a known base element`);
|
|
234
|
+
// 5 — each attributes[] entry is a well-formed attributes-as-API row.
|
|
235
|
+
const seen = new Set();
|
|
236
|
+
for (const [i, item] of (d.sequences.get('attributes') ?? []).entries()) {
|
|
237
|
+
const at = (sub) => `attributes[${i}].${sub}`;
|
|
238
|
+
const name = item.get('name');
|
|
239
|
+
if (typeof name !== 'string' || name === '') {
|
|
240
|
+
add('BAD_ATTRIBUTE', at('name'), `attribute #${i} is missing a name`);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
if (seen.has(name))
|
|
244
|
+
add('BAD_ATTRIBUTE', at('name'), `duplicate attribute name "${name}"`);
|
|
245
|
+
seen.add(name);
|
|
246
|
+
}
|
|
247
|
+
const type = item.get('type');
|
|
248
|
+
if (typeof type !== 'string' || !has(ATTR_TYPES, type)) {
|
|
249
|
+
add('BAD_ATTRIBUTE', at('type'), `attribute "${name ?? i}" has an invalid type "${typeof type === 'string' ? type : ''}"`);
|
|
250
|
+
}
|
|
251
|
+
else if (type === 'enum' && !(Array.isArray(item.get('values')) && item.get('values').length > 0)) {
|
|
252
|
+
add('BAD_ATTRIBUTE', at('values'), `enum attribute "${name ?? i}" needs a non-empty values list`);
|
|
253
|
+
}
|
|
254
|
+
if (!item.has('default'))
|
|
255
|
+
add('BAD_ATTRIBUTE', at('default'), `attribute "${name ?? i}" is missing a default`);
|
|
256
|
+
// 5b (TKT-0070): a QUOTED scalar default on a typed attribute is a YAML mis-typing the downstream
|
|
257
|
+
// String(config.default) comparison is structurally blind to — reject at the schema (`default: false`,
|
|
258
|
+
// never `default: 'false'`; a STRING attribute's quoted default — `''` — stays legal).
|
|
259
|
+
if (item.has('#quoted:default') && typeof type === 'string' && (type === 'boolean' || type === 'number')) {
|
|
260
|
+
add('BAD_ATTRIBUTE', at('default'), `attribute "${name ?? i}" has a QUOTED default for type ${type} — unquote it (the trip-wire compares String(default) and cannot catch this)`);
|
|
261
|
+
}
|
|
262
|
+
const reflect = item.get('reflect');
|
|
263
|
+
if (typeof reflect !== 'string' || !has(BOOLEANS, reflect))
|
|
264
|
+
add('BAD_ATTRIBUTE', at('reflect'), `attribute "${name ?? i}" reflect must be true|false`);
|
|
265
|
+
}
|
|
266
|
+
// 6 — face records a boolean formAssociated (FACE form-control participation).
|
|
267
|
+
const face = d.maps.get('face');
|
|
268
|
+
if (face !== undefined) {
|
|
269
|
+
const fa = face.get('formAssociated');
|
|
270
|
+
if (fa === undefined || !has(BOOLEANS, fa))
|
|
271
|
+
add('BAD_FACE', 'face.formAssociated', 'face.formAssociated must be true|false');
|
|
272
|
+
}
|
|
273
|
+
return failures;
|
|
274
|
+
}
|
|
275
|
+
/** The drift defects compareDescriptorToProps reports (the descriptor disagrees with the live class). */
|
|
276
|
+
export const DRIFT_CODES = ['DRIFT_MISSING', 'DRIFT_EXTRA', 'DRIFT_TYPE', 'DRIFT_DEFAULT', 'DRIFT_REFLECT', 'DRIFT_VALUES'];
|
|
277
|
+
/** A sentinel attribute string that is never a valid enum member nor valid JSON (probes the codec fallback). */
|
|
278
|
+
const NON_MEMBER = '\u0000__agent-ui-non-member__';
|
|
279
|
+
/** Run a codec probe, swallowing throws (an opaque codec may throw on input it cannot parse, e.g. JSON). */
|
|
280
|
+
function probe(fn) {
|
|
281
|
+
try {
|
|
282
|
+
return { ok: true, value: fn() };
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return { ok: false };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Recover a live codec's KIND by behaviour (the codecs carry no kind tag — props.ts). Each branch keys off
|
|
290
|
+
* `config.type.from` on a few probe inputs: boolean maps absence→false / presence→true; number and json both
|
|
291
|
+
* map absence→null but split on `from('5')` vs `from('"x"')` (json parses, number does not); a STRING/ENUM
|
|
292
|
+
* codec both map absence→a string, so the two are told apart by a SECOND probe: `from(NON_MEMBER)` (a garbage
|
|
293
|
+
* sentinel no real enum contains) — a plain string codec is a passthrough (`attr ?? ''`), so it returns
|
|
294
|
+
* NON_MEMBER UNCHANGED; an enum SNAPS a non-member back to its fixed `values[0]`, so it returns something
|
|
295
|
+
* OTHER than NON_MEMBER (even when that fixed member is itself `''` — ADR-0083's `landmark`, whose enum
|
|
296
|
+
* `values[0]` is the empty string, would otherwise be indistinguishable from a plain string prop: both read
|
|
297
|
+
* `from(null) === ''`. Only the NON_MEMBER probe's OUTCOME, never its literal value, tells them apart — a bare
|
|
298
|
+
* `nul.value === ''` shortcut checked BEFORE this probe was the pre-ADR-0083 bug). Returns 'unknown' when no
|
|
299
|
+
* branch matches (the natural DRIFT_TYPE signal).
|
|
300
|
+
*/
|
|
301
|
+
function kindOf(config) {
|
|
302
|
+
const from = (a) => probe(() => config.type.from(a));
|
|
303
|
+
const nul = from(null);
|
|
304
|
+
if (!nul.ok)
|
|
305
|
+
return 'unknown';
|
|
306
|
+
// A HARDENED array codec (SPEC-R3/R7, the chart family's safe values/data codecs) maps a removed/absent
|
|
307
|
+
// attribute to `[]`, never `null` — the generic `jsonType<T>()` shape below never produces this. An array
|
|
308
|
+
// result is unambiguous (never `false`/string/`null`), so it is classified "json" directly, ahead of the
|
|
309
|
+
// null-keyed branches that would otherwise miss it and fall through to 'unknown'.
|
|
310
|
+
if (Array.isArray(nul.value))
|
|
311
|
+
return 'json';
|
|
312
|
+
if (nul.value === false) {
|
|
313
|
+
const empty = from('');
|
|
314
|
+
return empty.ok && empty.value === true ? 'boolean' : 'unknown';
|
|
315
|
+
}
|
|
316
|
+
if (typeof nul.value === 'string') {
|
|
317
|
+
const fallback = from(NON_MEMBER);
|
|
318
|
+
if (fallback.ok && fallback.value !== NON_MEMBER)
|
|
319
|
+
return 'enum';
|
|
320
|
+
if (nul.value === '')
|
|
321
|
+
return 'string';
|
|
322
|
+
}
|
|
323
|
+
if (nul.value === null) {
|
|
324
|
+
const empty = from('');
|
|
325
|
+
const five = from('5');
|
|
326
|
+
if (empty.ok && empty.value === null && five.ok && five.value === 5)
|
|
327
|
+
return 'number';
|
|
328
|
+
const quoted = from('"x"');
|
|
329
|
+
return quoted.ok && quoted.value === 'x' ? 'json' : 'unknown';
|
|
330
|
+
}
|
|
331
|
+
return 'unknown';
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Probe whether a live enum codec accepts EXACTLY the descriptor's `values` (order-significant): every
|
|
335
|
+
* declared member must round-trip (be a real live member) and the fallback for a non-member must equal
|
|
336
|
+
* `values[0]` (so the first member — the codec's snap target — agrees). NOTE the one asymmetry probing an
|
|
337
|
+
* opaque closure cannot close: a live member the descriptor OMITS while preserving the prefix is invisible
|
|
338
|
+
* (you cannot enumerate the closure), so values[] must be the declared contract a human keeps complete.
|
|
339
|
+
*/
|
|
340
|
+
function enumMembersMatch(config, values) {
|
|
341
|
+
if (values.length === 0)
|
|
342
|
+
return false;
|
|
343
|
+
const from = (a) => probe(() => config.type.from(a));
|
|
344
|
+
const everyMember = values.every((v) => {
|
|
345
|
+
const r = from(v);
|
|
346
|
+
return r.ok && r.value === v;
|
|
347
|
+
});
|
|
348
|
+
const fallback = from(NON_MEMBER);
|
|
349
|
+
return everyMember && fallback.ok && fallback.value === values[0];
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* The contract↔props trip-wire: assert the descriptor `attributes[]` and the live `static props` are a
|
|
353
|
+
* faithful BIJECTION. TOTAL — never throws; every disagreement is a structured DriftFailure. Checks: (NAME)
|
|
354
|
+
* every live prop has a descriptor attribute and vice-versa (DRIFT_MISSING / DRIFT_EXTRA); then per matched
|
|
355
|
+
* name the (TYPE) live codec behaves as the declared kind (DRIFT_TYPE), the (DEFAULT) `String(config.default)`
|
|
356
|
+
* equals the descriptor token (DRIFT_DEFAULT), the (REFLECT) `config.reflect ?? false` equals the flag
|
|
357
|
+
* (DRIFT_REFLECT), and (VALUES) an enum's declared members are the live member set (DRIFT_VALUES). The
|
|
358
|
+
* descriptor is assumed schema-valid first (validateComponentDescriptor); nameless attributes are that
|
|
359
|
+
* validator's concern and are skipped here.
|
|
360
|
+
*/
|
|
361
|
+
export function compareDescriptorToProps(attributes, props) {
|
|
362
|
+
const failures = [];
|
|
363
|
+
const add = (code, path, message) => void failures.push({ code, path, message });
|
|
364
|
+
const byName = new Map();
|
|
365
|
+
for (const a of attributes)
|
|
366
|
+
if (a.name !== undefined && a.name !== '')
|
|
367
|
+
byName.set(a.name, a);
|
|
368
|
+
const liveNames = Object.keys(props);
|
|
369
|
+
const liveSet = new Set(liveNames);
|
|
370
|
+
// 1 — NAME bijection: a live prop with no descriptor row is MISSING; a descriptor row with no live prop is EXTRA.
|
|
371
|
+
for (const name of liveNames) {
|
|
372
|
+
if (!byName.has(name))
|
|
373
|
+
add('DRIFT_MISSING', `attributes.${name}`, `live prop "${name}" has no descriptor attribute`);
|
|
374
|
+
}
|
|
375
|
+
for (const name of byName.keys()) {
|
|
376
|
+
if (!liveSet.has(name))
|
|
377
|
+
add('DRIFT_EXTRA', `attributes.${name}`, `descriptor attribute "${name}" has no live prop`);
|
|
378
|
+
}
|
|
379
|
+
// 2 — per matched name: type kind · default · reflect · enum members consistent with the live PropConfig.
|
|
380
|
+
for (const name of liveNames) {
|
|
381
|
+
const attr = byName.get(name);
|
|
382
|
+
if (attr === undefined)
|
|
383
|
+
continue;
|
|
384
|
+
const config = props[name];
|
|
385
|
+
const at = (sub) => `attributes.${name}.${sub}`;
|
|
386
|
+
const liveKind = kindOf(config);
|
|
387
|
+
if (attr.type !== liveKind) {
|
|
388
|
+
add('DRIFT_TYPE', at('type'), `descriptor type "${attr.type ?? ''}" != live codec kind "${liveKind}"`);
|
|
389
|
+
}
|
|
390
|
+
else if (liveKind === 'enum' && !enumMembersMatch(config, attr.values ?? [])) {
|
|
391
|
+
add('DRIFT_VALUES', at('values'), `enum "${name}" values [${(attr.values ?? []).join(', ')}] are not the live member set`);
|
|
392
|
+
}
|
|
393
|
+
const liveDefault = String(config.default);
|
|
394
|
+
if (attr.default !== liveDefault) {
|
|
395
|
+
add('DRIFT_DEFAULT', at('default'), `descriptor default "${attr.default ?? ''}" != live default "${liveDefault}"`);
|
|
396
|
+
}
|
|
397
|
+
const descReflect = attr.reflect ?? false;
|
|
398
|
+
const liveReflect = config.reflect ?? false;
|
|
399
|
+
if (descReflect !== liveReflect) {
|
|
400
|
+
add('DRIFT_REFLECT', at('reflect'), `descriptor reflect ${descReflect} != live reflect ${liveReflect}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return failures;
|
|
404
|
+
}
|
|
405
|
+
// ── contract↔source trip-wire ───────────────────────────────────────────────────────────────────────────
|
|
406
|
+
//
|
|
407
|
+
// compareDescriptorToProps closes the props gap, but two facts the descriptor records have NO `static props`
|
|
408
|
+
// row to compare against — they are not class fields at all:
|
|
409
|
+
//
|
|
410
|
+
// • customStates — a control's custom states are added IMPERATIVELY and conditionally (button.ts:
|
|
411
|
+
// `requestAnimationFrame(() => this.internals.states?.add('ready'))`, optional-chained, behind rAF), and a
|
|
412
|
+
// state may also be referenced ONLY by the stylesheet (`:state(ready)`). Neither is a statically-inspectable
|
|
413
|
+
// field, so the truth lives in the .ts/.css source text.
|
|
414
|
+
// • slots — light-DOM, host-as-grid components place slotted children purely in CSS (`[slot='leading']`);
|
|
415
|
+
// there is no slot manifest in the class. The truth lives in the .css selectors.
|
|
416
|
+
//
|
|
417
|
+
// compareDescriptorToSource cross-checks the descriptor against that SOURCE USAGE (the caller passes the .ts and
|
|
418
|
+
// .css TEXT — this module stays import-free; it never reads the filesystem). It is a NAME-BIJECTION trip-wire,
|
|
419
|
+
// like compareDescriptorToProps, but states/slots carry no codec, so there is no TYPE/DEFAULT/REFLECT — only the
|
|
420
|
+
// name sets are compared. STATES are bidirectional (used ≡ documented); SLOTS are one-directional — every
|
|
421
|
+
// CSS-styled slot must be documented, but a documented slot need NOT be CSS-selected (`label`, button.md's
|
|
422
|
+
// default centre cell, is a real slot the grid never selects by name — the one asymmetry, mirroring the live
|
|
423
|
+
// member the enum probe cannot enumerate in enumMembersMatch).
|
|
424
|
+
/**
|
|
425
|
+
* The custom states a component USES — the union of the imperative `internals.states` mutations in the .ts
|
|
426
|
+
* (`.states?.add('X')` / `.delete` / `.toggle` / `.replace` / `.has`, optional-chain tolerant) AND the
|
|
427
|
+
* `:state(X)` selectors in the .css. A state is "used" if EITHER source references it.
|
|
428
|
+
*/
|
|
429
|
+
export function collectUsedStates(ts, css) {
|
|
430
|
+
const names = new Set();
|
|
431
|
+
for (const m of ts.matchAll(/\.states\??\.(?:add|delete|toggle|replace|has)\(\s*['"]([^'"]+)['"]/g))
|
|
432
|
+
names.add(m[1]);
|
|
433
|
+
for (const m of css.matchAll(/:state\(\s*([A-Za-z][\w-]*)\s*\)/g))
|
|
434
|
+
names.add(m[1]);
|
|
435
|
+
return names;
|
|
436
|
+
}
|
|
437
|
+
/** The slot POSITIONS the .css styles — every `[slot='X']` attribute selector (quoting optional, `:has()`-nested or not). */
|
|
438
|
+
export function collectStyledSlots(css) {
|
|
439
|
+
const names = new Set();
|
|
440
|
+
for (const m of css.matchAll(/\[slot\s*=\s*['"]?([A-Za-z][\w-]*)['"]?\s*\]/g))
|
|
441
|
+
names.add(m[1]);
|
|
442
|
+
return names;
|
|
443
|
+
}
|
|
444
|
+
/** The content ROLES the .css styles — every `[data-role='X']` attribute selector (quoting optional). Reused by the /site dead-name guard (s12). */
|
|
445
|
+
export function collectStyledRoles(css) {
|
|
446
|
+
const names = new Set();
|
|
447
|
+
for (const m of css.matchAll(/\[data-role\s*=\s*['"]?([A-Za-z][\w-]*)['"]?\s*\]/g))
|
|
448
|
+
names.add(m[1]);
|
|
449
|
+
return names;
|
|
450
|
+
}
|
|
451
|
+
/** The drift defects compareDescriptorToSource reports (the descriptor disagrees with the component .ts/.css). */
|
|
452
|
+
export const SOURCE_DRIFT_CODES = ['STATE_UNDOCUMENTED', 'STATE_UNUSED', 'SLOT_UNDOCUMENTED'];
|
|
453
|
+
/** The slot names a descriptor declares (the `slots[]` sequence rows' `name:` values). */
|
|
454
|
+
function declaredSlotNames(desc) {
|
|
455
|
+
const names = new Set();
|
|
456
|
+
for (const item of desc.sequences.get('slots') ?? []) {
|
|
457
|
+
const name = item.get('name');
|
|
458
|
+
if (typeof name === 'string' && name !== '')
|
|
459
|
+
names.add(name);
|
|
460
|
+
}
|
|
461
|
+
return names;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* The contract↔source trip-wire: assert the descriptor's customStates/slots tell the truth about where the fact
|
|
465
|
+
* ACTUALLY lives (the component .ts/.css). TOTAL — never throws; every disagreement is a structured
|
|
466
|
+
* SourceDriftFailure. STATES (bidirectional): used-states (collectUsedStates) ≡ descriptor.customStates —
|
|
467
|
+
* STATE_UNDOCUMENTED for a used state the descriptor omits, STATE_UNUSED for a documented state no source uses.
|
|
468
|
+
* SLOTS (one-directional): every CSS-styled slot (collectStyledSlots) must be IN descriptor.slots —
|
|
469
|
+
* SLOT_UNDOCUMENTED otherwise; a documented-but-unstyled slot (`label`, the default cell) is NOT a defect.
|
|
470
|
+
*/
|
|
471
|
+
export function compareDescriptorToSource(desc, source) {
|
|
472
|
+
const failures = [];
|
|
473
|
+
const add = (code, path, message) => void failures.push({ code, path, message });
|
|
474
|
+
// STATES — bijection between source usage (.ts add + .css :state) and the documented customStates.
|
|
475
|
+
const usedStates = collectUsedStates(source.ts, source.css);
|
|
476
|
+
const declaredStates = new Set(scalarSeq(desc, 'customStates'));
|
|
477
|
+
for (const s of usedStates) {
|
|
478
|
+
if (!declaredStates.has(s))
|
|
479
|
+
add('STATE_UNDOCUMENTED', `customStates.${s}`, `state "${s}" is used in source but absent from customStates`);
|
|
480
|
+
}
|
|
481
|
+
for (const s of declaredStates) {
|
|
482
|
+
if (!usedStates.has(s))
|
|
483
|
+
add('STATE_UNUSED', `customStates.${s}`, `customStates declares "${s}" but no source (.ts/.css) uses it`);
|
|
484
|
+
}
|
|
485
|
+
// SLOTS — one-directional: every CSS-styled slot must be documented (an undocumented styled slot is the drift).
|
|
486
|
+
const declaredSlots = declaredSlotNames(desc);
|
|
487
|
+
for (const s of collectStyledSlots(source.css)) {
|
|
488
|
+
if (!declaredSlots.has(s))
|
|
489
|
+
add('SLOT_UNDOCUMENTED', `slots.${s}`, `slot "${s}" is styled in css but absent from slots`);
|
|
490
|
+
}
|
|
491
|
+
return failures;
|
|
492
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './component-descriptor.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// descriptor — the component-descriptor public surface (ADR-0004). This barrel exposes the CANONICAL
|
|
2
|
+
// `{name}.md` frontmatter reader + schema + contract↔props trip-wire as the package subpath
|
|
3
|
+
// `@agent-ui-kit/components/descriptor`, so a second consumer (the /site doc page, A4) reuses the SAME parser
|
|
4
|
+
// the contract trip-wire is built on — one parser, two consumers, never a forked frontmatter dialect.
|
|
5
|
+
export * from "./component-descriptor.js";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { UIElement } from './element.js';
|
|
2
|
+
export declare class UIContainerElement extends UIElement {
|
|
3
|
+
/** The spreadable surface axes — `elevation`/`brightness` (ADR-0015). Subclasses fold into `static props`. */
|
|
4
|
+
static surfaceProps: {
|
|
5
|
+
elevation: {
|
|
6
|
+
reflect: true;
|
|
7
|
+
type: import("./props.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
|
|
8
|
+
default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
|
|
9
|
+
attribute?: string | false;
|
|
10
|
+
};
|
|
11
|
+
brightness: {
|
|
12
|
+
reflect: true;
|
|
13
|
+
type: import("./props.js").PropType<"0" | "1" | "2" | "3" | "-3" | "-2" | "-1">;
|
|
14
|
+
default: "0" | "1" | "2" | "3" | "-3" | "-2" | "-1";
|
|
15
|
+
attribute?: string | false;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
/** The spreadable layout grammar — `align`/`justify`/`gap`/`wrap` (ADR-0016). Subclasses fold into `static props`. */
|
|
19
|
+
static flexProps: {
|
|
20
|
+
align: {
|
|
21
|
+
reflect: true;
|
|
22
|
+
type: import("./props.js").PropType<"end" | "start" | "center" | "stretch" | "baseline">;
|
|
23
|
+
default: "end" | "start" | "center" | "stretch" | "baseline";
|
|
24
|
+
attribute?: string | false;
|
|
25
|
+
};
|
|
26
|
+
justify: {
|
|
27
|
+
reflect: true;
|
|
28
|
+
type: import("./props.js").PropType<"end" | "start" | "center" | "between" | "around" | "evenly">;
|
|
29
|
+
default: "end" | "start" | "center" | "between" | "around" | "evenly";
|
|
30
|
+
attribute?: string | false;
|
|
31
|
+
};
|
|
32
|
+
gap: {
|
|
33
|
+
reflect: true;
|
|
34
|
+
type: import("./props.js").PropType<"none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl">;
|
|
35
|
+
default: "none" | "sm" | "md" | "lg" | "xs" | "xl" | "2xl";
|
|
36
|
+
attribute?: string | false;
|
|
37
|
+
};
|
|
38
|
+
wrap: {
|
|
39
|
+
reflect: true;
|
|
40
|
+
type: import("./props.js").PropType<boolean>;
|
|
41
|
+
default: boolean;
|
|
42
|
+
attribute?: string | false;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// container.ts — `UIContainerElement`, the FACE container surface base (ADR-0015 / ADR-0016; decomp
|
|
2
|
+
// g9-containers slice s2).
|
|
3
|
+
//
|
|
4
|
+
// The base every container/layout `ui-*` element extends — the FIRST non-form family. It is a plain
|
|
5
|
+
// `UIElement` (NOT form-associated: no `static formAssociated`, no `setFormValue`/validity — a container
|
|
6
|
+
// contributes nothing to a form), and it reuses the inherited single `ElementInternals` handle (acquired
|
|
7
|
+
// ONCE in the `UIElement` constructor) so the stateful subclasses (`ui-tabs`, `ui-modal`) set ARIA roles +
|
|
8
|
+
// custom states THROUGH `this.internals`, never host attributes.
|
|
9
|
+
//
|
|
10
|
+
// The base owns NO layout/surface CSS opinion in TypeScript — every visual decision lives in the CSS layer.
|
|
11
|
+
// What it owns is the two SPREADABLE prop sets the container family shares (the ADR-0013 `formProps`
|
|
12
|
+
// precedent — props.ts has no static-props prototype merge, so a base cannot hand props down by inheritance;
|
|
13
|
+
// instead it exposes a schema each subclass folds into its OWN `static props`):
|
|
14
|
+
// • `surfaceProps` — the two signed surface axes `elevation`/`brightness` (ADR-0015): each a reflected
|
|
15
|
+
// literal-union `'-3'…'3'`, default `'0'` (the neutral base). The CSS `[elevation=n]`/`[brightness=m]`
|
|
16
|
+
// selectors (controls/_surface/container.css) repoint the one role-pure `--ui-container-bg`/`-tint`
|
|
17
|
+
// consumption seam; the component holds zero colour opinion.
|
|
18
|
+
// • `flexProps` — the shared A2UI-faithful layout grammar `align`/`justify`/`gap`/`wrap` (ADR-0016), each a
|
|
19
|
+
// reflected literal-union (or boolean) the layout primitives (`ui-row`/`ui-column`/`ui-list`/`ui-grid`)
|
|
20
|
+
// map 1:1 onto a CSS flex property in their OWN `@scope` block. One grammar, four consumers — no drift.
|
|
21
|
+
// A subclass spreads them: `static props = { ...UIContainerElement.surfaceProps, ...flexProps, … }`.
|
|
22
|
+
//
|
|
23
|
+
// Imports only same-layer `./element.ts` / `./props.ts` (the dom layer; the layering law holds —
|
|
24
|
+
// `reactive ← dom`). The kernel is reached transitively through `UIElement` / `prop`; nothing here touches
|
|
25
|
+
// `../reactive` directly. `ElementInternals` is an ambient DOM global, inherited via `UIElement`.
|
|
26
|
+
import { UIElement } from "./element.js";
|
|
27
|
+
import { prop } from "./props.js";
|
|
28
|
+
// The two surface axes (ADR-0015 clause 1). Both are SIGNED literal unions `'-3'…'3'` (typed strings, not
|
|
29
|
+
// `number` — matching `size: 'sm'|'md'|'lg'`, so a `@ts-expect-error` proves a bare number is rejected) and
|
|
30
|
+
// both REFLECT, so the `[elevation=n]`/`[brightness=m]` attribute selectors in container.css apply to
|
|
31
|
+
// JS-set values too. `'0'` LEADS the value array so an out-of-range attribute snaps back to the neutral base
|
|
32
|
+
// (`enumType.from` falls to `values[0]`), not a surprise elevation — the literal-union TYPE is order-independent.
|
|
33
|
+
const SURFACE_STEPS = ['0', '1', '2', '3', '-1', '-2', '-3'];
|
|
34
|
+
const surfaceProps = {
|
|
35
|
+
// elevation → the scheme-INVERTING plane (`--md-sys-color-neutral-surface-{lowest…highest}`); 0 = the neutral surface.
|
|
36
|
+
elevation: { ...prop.enum(SURFACE_STEPS, '0'), reflect: true },
|
|
37
|
+
// brightness → the scheme-CONSISTENT tonal wash; 0 = no wash (the neutral surface).
|
|
38
|
+
brightness: { ...prop.enum(SURFACE_STEPS, '0'), reflect: true },
|
|
39
|
+
};
|
|
40
|
+
// The shared layout grammar (ADR-0016 clause 1). Each maps 1:1 onto a CSS flex property; the literal-union →
|
|
41
|
+
// CSS-keyword mapping lives in each primitive's `@scope` block (a role-pure repoint, never an inline style),
|
|
42
|
+
// so this set is the TYPED vocabulary only — the base owns no flex CSS. All REFLECT so the `[align]`/etc.
|
|
43
|
+
// selectors apply to JS-set values. `values[0]` is each prop's default (consistent snap + default).
|
|
44
|
+
const flexProps = {
|
|
45
|
+
// align (cross-axis) → `align-items`
|
|
46
|
+
align: { ...prop.enum(['start', 'center', 'end', 'stretch', 'baseline'], 'start'), reflect: true },
|
|
47
|
+
// justify (main-axis distribution) → `justify-content` (`between`/`around`/`evenly` → `space-*` in the CSS)
|
|
48
|
+
justify: { ...prop.enum(['start', 'center', 'end', 'between', 'around', 'evenly'], 'start'), reflect: true },
|
|
49
|
+
// gap → `gap: var(--md-sys-space-{step})` — the density-responsive layout-spacing ladder (ADR-0015 cl.4), NEVER a
|
|
50
|
+
// control dimension. The step vocabulary mirrors s1's `--md-sys-space-{none,xs,sm,md,lg,xl,2xl}` ramp 1:1.
|
|
51
|
+
gap: { ...prop.enum(['none', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'], 'none'), reflect: true },
|
|
52
|
+
// wrap → `flex-wrap` (boolean presence: present ⇒ wrap)
|
|
53
|
+
wrap: { ...prop.boolean(false), reflect: true },
|
|
54
|
+
};
|
|
55
|
+
export class UIContainerElement extends UIElement {
|
|
56
|
+
/** The spreadable surface axes — `elevation`/`brightness` (ADR-0015). Subclasses fold into `static props`. */
|
|
57
|
+
static surfaceProps = surfaceProps;
|
|
58
|
+
/** The spreadable layout grammar — `align`/`justify`/`gap`/`wrap` (ADR-0016). Subclasses fold into `static props`. */
|
|
59
|
+
static flexProps = flexProps;
|
|
60
|
+
}
|