@adia-ai/web-components 0.7.5 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/components/action-list/action-list.a2ui.json +2 -2
  3. package/components/action-list/action-list.yaml +2 -2
  4. package/components/agent-artifact/agent-artifact.class.js +28 -12
  5. package/components/agent-artifact/agent-artifact.test.js +61 -0
  6. package/components/badge/badge.yaml +1 -0
  7. package/components/block/block.class.js +20 -4
  8. package/components/block/block.css +18 -13
  9. package/components/button/button.a2ui.json +1 -2
  10. package/components/button/button.css +17 -6
  11. package/components/button/button.yaml +1 -0
  12. package/components/card/card.yaml +1 -0
  13. package/components/chart/chart.a2ui.json +1 -2
  14. package/components/chart/chart.d.ts +1 -1
  15. package/components/chart/chart.yaml +1 -0
  16. package/components/check/check.a2ui.json +0 -3
  17. package/components/check/check.yaml +0 -2
  18. package/components/combobox/combobox.a2ui.json +1 -2
  19. package/components/combobox/combobox.yaml +1 -0
  20. package/components/command/command.a2ui.json +0 -3
  21. package/components/command/command.class.js +19 -1
  22. package/components/command/command.yaml +0 -2
  23. package/components/context-menu/context-menu.class.js +1 -0
  24. package/components/description-list/description-list.a2ui.json +1 -2
  25. package/components/description-list/description-list.d.ts +1 -1
  26. package/components/description-list/description-list.yaml +1 -0
  27. package/components/drawer/drawer.css +1 -1
  28. package/components/field/field.class.js +1 -0
  29. package/components/fields/fields.class.js +1 -0
  30. package/components/grid/grid.yaml +2 -0
  31. package/components/input/input.a2ui.json +2 -14
  32. package/components/input/input.yaml +2 -0
  33. package/components/integration-card/integration-card.class.js +1 -0
  34. package/components/list/list.class.js +1 -0
  35. package/components/list/list.yaml +1 -0
  36. package/components/menu/menu.class.js +1 -0
  37. package/components/menu/menu.css +14 -2
  38. package/components/pipeline-status/pipeline-status.yaml +1 -0
  39. package/components/radio/radio.a2ui.json +0 -3
  40. package/components/radio/radio.yaml +0 -2
  41. package/components/rating/rating.yaml +1 -0
  42. package/components/search/search.a2ui.json +1 -8
  43. package/components/search/search.yaml +1 -5
  44. package/components/select/select.a2ui.json +1 -2
  45. package/components/select/select.class.js +46 -1
  46. package/components/select/select.test.js +33 -0
  47. package/components/select/select.yaml +2 -0
  48. package/components/slider/slider.a2ui.json +0 -3
  49. package/components/slider/slider.yaml +0 -2
  50. package/components/swatch/swatch.class.js +1 -0
  51. package/components/table/table.a2ui.json +2 -4
  52. package/components/table/table.d.ts +2 -2
  53. package/components/table/table.yaml +2 -0
  54. package/components/tabs/tabs.yaml +1 -0
  55. package/components/tag/tag.yaml +1 -0
  56. package/components/toggle-group/toggle-group.yaml +1 -0
  57. package/components/toolbar/toolbar.class.js +1 -0
  58. package/components/tree/tree.a2ui.json +2 -2
  59. package/components/tree/tree.yaml +2 -2
  60. package/dist/host.min.css +1 -1
  61. package/dist/web-components.min.css +1 -1
  62. package/dist/web-components.min.js +29 -29
  63. package/index.css +11 -17
  64. package/package.json +1 -1
  65. package/styles/api/sizing.css +18 -6
  66. package/styles/foundation/space.css +33 -0
  67. package/styles/host.css +20 -22
  68. package/styles/index.css +14 -17
  69. package/styles/verse.css +24 -0
@@ -80,12 +80,14 @@ props:
80
80
  - email
81
81
  - url
82
82
  - none
83
+ dynamic: true # native attr — routed via setAttribute to the host, deliberately NOT in static properties (would clobber native reflection)
83
84
  autocomplete:
84
85
  description: 'Browser autofill behavior per HTML autocomplete spec. Routed via setAttribute to the host element. Common
85
86
  values: off, on, cc-number, cc-exp, cc-csc, cc-name, email, username, current-password, new-password, one-time-code,
86
87
  given-name, family-name, street-address, postal-code.'
87
88
  type: string
88
89
  default: ''
90
+ dynamic: true # native attr — routed via setAttribute to the host, deliberately NOT in static properties (would clobber native reflection)
89
91
  min:
90
92
  description: Minimum numeric value. Applies when `type="number"`. Clamps + drives aria-valuemin + the [-] button's disabled
91
93
  state.
@@ -226,6 +226,7 @@ export class UIIntegrationCard extends UIElement {
226
226
  #hasConsumerDescription() {
227
227
  // Author description = direct child element that is neither one of our
228
228
  // stamped data- markers nor an [slot="actions"] / [slot="action"] node.
229
+ // wrapper-trap-ok: detects presence of consumer-provided description; wrapper spans from interpolated content also qualify as "consumer description present" (correct — the content IS there)
229
230
  for (const child of this.children) {
230
231
  if (child === this.#bodyEl) continue;
231
232
  if (child === this.#descEl) continue;
@@ -174,6 +174,7 @@ export class UIListItem extends UIElement {
174
174
  // otherwise we'd accidentally remove e.g. a <span slot="icon"> inside a
175
175
  // child <card-ui> when list-item-ui has no `icon` prop.
176
176
  #ownChild(selector) {
177
+ // wrapper-trap-ok: finds component-stamped children (identified by [data-list-stamped]) — consumer list-item-ui children are never queried here
177
178
  for (const ch of this.children) {
178
179
  if (ch.matches(selector)) return ch;
179
180
  }
@@ -34,6 +34,7 @@ props:
34
34
  so content sits at the list-ui edges (canonical edge-to-edge style).
35
35
  type: boolean
36
36
  default: false
37
+ reflect: false # declarative presentational attribute — CSS/author-time read, non-reactive by design
37
38
  selectable:
38
39
  description: Enable selection on child listitems (roving tabindex + aria-selected).
39
40
  type: boolean
@@ -290,6 +290,7 @@ export class UIMenuItem extends UIElement {
290
290
  #wasStamped(el) { return el?.dataset?.menuItemStamped === '1'; }
291
291
 
292
292
  #ownChild(selector) {
293
+ // wrapper-trap-ok: finds component-stamped children (data-menuItemStamped); consumer menu-item-ui children are adopted by #show() using logical children
293
294
  for (const ch of this.children) {
294
295
  if (ch.matches(selector)) return ch;
295
296
  }
@@ -21,9 +21,21 @@
21
21
 
22
22
  /* Items/dividers in Light DOM are hidden unless they've been adopted
23
23
  into the popover on open. Popover API also hides the popover itself
24
- when closed. */
24
+ when closed.
25
+
26
+ FB-97: interpolated items (`.map()` / `repeat()`) arrive nested in the
27
+ template engine's `display:contents` / `role="presentation"` wrapper
28
+ span (core/template.js), so the direct-child combinator alone misses
29
+ them on the initial (never-opened) render — a CLOSED declarative menu
30
+ would lay them out in flow and balloon to its widest item's width,
31
+ displacing siblings. The wrapper-piercing descendant selectors hide
32
+ those too. Safe vs. the open state: #show() appendChild's the real
33
+ items (not the wrappers) into [data-menu-popover], so open-state items
34
+ are neither `:scope >` children nor under a `:scope > [role]` wrapper. */
25
35
  :scope > menu-item-ui,
26
- :scope > menu-divider-ui {
36
+ :scope > menu-divider-ui,
37
+ :scope > [role="presentation"] menu-item-ui,
38
+ :scope > [role="presentation"] menu-divider-ui {
27
39
  display: none;
28
40
  }
29
41
  }
@@ -15,6 +15,7 @@ props:
15
15
  description: 'Component property: complete.'
16
16
  type: boolean
17
17
  default: false
18
+ reflect: false # declarative presentational attribute — CSS/author-time read, non-reactive by design
18
19
  message:
19
20
  description: 'Component property: message.'
20
21
  type: string
@@ -108,9 +108,6 @@
108
108
  "dot": {
109
109
  "description": "Radio dot indicator, sized via --dot-size"
110
110
  },
111
- "hint": {
112
- "description": "Hint text container, rendered via CSS attr(hint) on ::after"
113
- },
114
111
  "label": {
115
112
  "description": "Label text container, rendered via CSS attr(label) on ::after"
116
113
  }
@@ -59,8 +59,6 @@ events:
59
59
  slots:
60
60
  dot:
61
61
  description: Radio dot indicator, sized via --dot-size
62
- hint:
63
- description: Hint text container, rendered via CSS attr(hint) on ::after
64
62
  label:
65
63
  description: Label text container, rendered via CSS attr(label) on ::after
66
64
  states:
@@ -53,6 +53,7 @@ props:
53
53
  - ""
54
54
  - accent
55
55
  - warning
56
+ reflect: false # declarative presentational attribute — CSS/author-time read, non-reactive by design
56
57
  events:
57
58
  "[object Object]":
58
59
  description: "Fired on [object Object]."
@@ -99,14 +99,7 @@
99
99
  "Command",
100
100
  "Select"
101
101
  ],
102
- "slots": {
103
- "clear": {
104
- "description": "Clear button (shown when value is non-empty)"
105
- },
106
- "input": {
107
- "description": "Native search input element"
108
- }
109
- },
102
+ "slots": {},
110
103
  "states": [
111
104
  {
112
105
  "description": "Default, ready for interaction.",
@@ -48,11 +48,7 @@ events:
48
48
  description: Current search-input value.
49
49
  search:
50
50
  description: Debounced CustomEvent with detail.query containing the search string
51
- slots:
52
- clear:
53
- description: Clear button (shown when value is non-empty)
54
- input:
55
- description: Native search input element
51
+ slots: {}
56
52
  states:
57
53
  - name: idle
58
54
  description: Default, ready for interaction.
@@ -108,8 +108,7 @@
108
108
  },
109
109
  "options": {
110
110
  "description": "Option list. Array of {value, label, disabled?, icon?, avatar?} or grouped {label, options: [...]}. Alternative to declarative <option> / <optgroup> children. Per-option icon/avatar render in the list AND reflect in the trigger's selected state.",
111
- "type": "array",
112
- "default": []
111
+ "$ref": "common_types.json#/$defs/DynamicStringList"
113
112
  },
114
113
  "pattern": {
115
114
  "description": "Regex pattern for validation",
@@ -77,6 +77,9 @@ export class UISelect extends UIFormElement {
77
77
  #query = '';
78
78
  #searchInput = null;
79
79
  #rafId = null;
80
+ // FB-98: re-parse interpolated <option> children (.map()/repeat()) that arrive
81
+ // wrapper-nested and/or after connected(); torn down in disconnected().
82
+ #optionObserver = null;
80
83
 
81
84
  #onSearchFocus = () => { if (!this.disabled) this.open = true; };
82
85
  #onSearchClick = (e) => { e.stopPropagation(); if (!this.disabled) this.open = true; };
@@ -371,6 +374,27 @@ export class UISelect extends UIFormElement {
371
374
  if (this.#options.length === 0) {
372
375
  this.#parseOptions();
373
376
  }
377
+ // FB-98: interpolated <option>/<optgroup> (.map()/repeat()) are nested in the
378
+ // engine's display:contents wrapper spans and often applied in the template's
379
+ // update() pass AFTER connected() — so the parse above can see nothing ("No
380
+ // options"). Observe for them and re-parse once they appear. Guards: skip if
381
+ // options are already parsed/set, and skip unless there's a real
382
+ // <option>/<optgroup> to parse — so the component's own listbox/empty-state
383
+ // renders (and #parseOptions's own option removals) don't re-enter or loop.
384
+ if (!this.#optionObserver) {
385
+ this.#optionObserver = new MutationObserver(() => {
386
+ if (this.#options.length) return;
387
+ const kids = this.#logicalOptionChildren();
388
+ if (!kids.some((c) => c.tagName === 'OPTION' || c.tagName === 'OPTGROUP')) return;
389
+ this.#parseOptions();
390
+ if (this.#options.length) {
391
+ this.#renderOptions();
392
+ const display = this.querySelector('[slot="display"]');
393
+ if (display) display.textContent = this.#displayText();
394
+ }
395
+ });
396
+ }
397
+ this.#optionObserver.observe(this, { childList: true, subtree: true });
374
398
  }
375
399
 
376
400
  render() {
@@ -544,6 +568,25 @@ export class UISelect extends UIFormElement {
544
568
  }
545
569
 
546
570
  /** Parse <option> and <optgroup> children into internal model. */
571
+ // FB-98: collect declarative <option>/<optgroup> children — direct OR nested
572
+ // inside the template engine's display:contents wrapper spans (which carry
573
+ // role="presentation" since 0.7.2) — so .map()/repeat()-rendered options are
574
+ // seen, not just static literals. Skips component-stamped [slot] children
575
+ // (display/listbox/trigger). Mirrors tree-ui's #logicalSlotChildren (FB-96).
576
+ #logicalOptionChildren() {
577
+ const out = [];
578
+ const walk = (parent) => {
579
+ for (const ch of parent.children) {
580
+ if (ch.hasAttribute('slot')) continue; // component-stamped — skip, don't descend
581
+ if (ch.tagName === 'OPTION' || ch.tagName === 'OPTGROUP') { out.push(ch); continue; }
582
+ if (ch.matches('[role="presentation"]') || ch.style?.display === 'contents') { walk(ch); continue; }
583
+ out.push(ch); // unknown authored child — caller warns
584
+ }
585
+ };
586
+ walk(this);
587
+ return out;
588
+ }
589
+
547
590
  #parseOptions() {
548
591
  this.#options = [];
549
592
  // §225 (v0.5.9): track unknown-child-tag names so we can warn once per element.
@@ -552,7 +595,8 @@ export class UISelect extends UIFormElement {
552
595
  // declarative initial selection is honoured (native <select> parity).
553
596
  // §FB-46: collect ALL selected options for [multiple] mode (not just first).
554
597
  const preSelectedArr = [];
555
- for (const child of this.children) {
598
+ // FB-98: iterate wrapper-pierced logical children so interpolated options work.
599
+ for (const child of this.#logicalOptionChildren()) {
556
600
  if (child.tagName === 'OPTGROUP') {
557
601
  const group = { label: child.label || child.getAttribute('label') || '', options: [] };
558
602
  for (const opt of child.querySelectorAll('option')) {
@@ -974,6 +1018,7 @@ export class UISelect extends UIFormElement {
974
1018
  this.removeEventListener('click', this.#onClick);
975
1019
  this.removeEventListener('keydown', this.#onKey);
976
1020
  this.removeEventListener('remove', this.#onChipRemove);
1021
+ this.#optionObserver?.disconnect(); // FB-98
977
1022
  if (this.#rafId != null) {
978
1023
  cancelAnimationFrame(this.#rafId);
979
1024
  this.#rafId = null;
@@ -27,6 +27,39 @@ describe('select-ui', () => {
27
27
  expect(s.value).toBe('b');
28
28
  });
29
29
 
30
+ // §FB-98 — <option> children nested in the template engine's display:contents
31
+ // wrapper spans (.map()/repeat()) must be parsed, not just static literals.
32
+ it('FB-98: parses options nested in display:contents/role=presentation wrappers (present at connect)', async () => {
33
+ const s = mount(`
34
+ <select-ui value="a">
35
+ <span style="display:contents" role="presentation">
36
+ <span style="display:contents" role="presentation"><option value="a">Alpha</option></span>
37
+ <span style="display:contents" role="presentation"><option value="b">Beta</option></span>
38
+ <span style="display:contents" role="presentation"><option value="c">Gamma</option></span>
39
+ </span>
40
+ </select-ui>
41
+ `);
42
+ await tick();
43
+ expect(s.querySelectorAll('[role="option"]').length).toBe(3);
44
+ expect(s.options.map((o) => o.value)).toEqual(['a', 'b', 'c']);
45
+ expect(s.value).toBe('a');
46
+ });
47
+
48
+ // §FB-98 — interpolated options that ARRIVE after connect (the template's
49
+ // update() pass) are picked up by the re-parse MutationObserver.
50
+ it('FB-98: re-parses wrapped options that arrive after connect (observer)', async () => {
51
+ const s = mount(`<select-ui></select-ui>`);
52
+ await tick();
53
+ expect(s.querySelectorAll('[role="option"]').length).toBe(0);
54
+ const wrap = document.createElement('span');
55
+ wrap.style.display = 'contents';
56
+ wrap.setAttribute('role', 'presentation');
57
+ wrap.innerHTML = `<option value="x">X</option><option value="y">Y</option>`;
58
+ s.appendChild(wrap);
59
+ await tick(); await tick(); // mutation → observer microtask → re-parse + render
60
+ expect(s.querySelectorAll('[role="option"]').length).toBe(2);
61
+ });
62
+
30
63
  // §FB-46 — <option selected> must capture ALL values in [multiple] mode
31
64
  it('captures all <option selected> values as comma-separated for [multiple]', async () => {
32
65
  const s = mount(`
@@ -35,6 +35,7 @@ props:
35
35
  type: string
36
36
  default: default
37
37
  enum: [default, ghost]
38
+ reflect: false # declarative presentational attribute — CSS/author-time read, non-reactive by design
38
39
  size:
39
40
  description: >-
40
41
  Sizing scale via universal `[size]` attribute system
@@ -154,6 +155,7 @@ props:
154
155
  description: "Option list. Array of {value, label, disabled?, icon?, avatar?} or grouped {label, options: [...]}. Alternative to declarative <option> / <optgroup> children. Per-option icon/avatar render in the list AND reflect in the trigger's selected state."
155
156
  type: array
156
157
  default: []
158
+ dynamic: true # JS-set collection prop with a custom setter — deliberately not in static properties
157
159
  pattern:
158
160
  description: Regex pattern for validation
159
161
  type: string
@@ -139,9 +139,6 @@
139
139
  "hint": {
140
140
  "description": "Override slot for hint markup richer than the plain [hint] attribute string (inline links, code spans). Renders beneath the slider at body-subtle typography. Mutually exclusive with [hint]."
141
141
  },
142
- "input": {
143
- "description": "Native range input element"
144
- },
145
142
  "label": {
146
143
  "description": "Label element above the slider"
147
144
  },
@@ -90,8 +90,6 @@ events:
90
90
  input:
91
91
  description: Fired on each slider movement
92
92
  slots:
93
- input:
94
- description: Native range input element
95
93
  label:
96
94
  description: Label element above the slider
97
95
  value:
@@ -409,6 +409,7 @@ export class UISwatch extends UIElement {
409
409
  #absorbChromeSlot() {
410
410
  if (!this.#tileEl || !this.#badgeEl) return;
411
411
  const chromeChildren = [];
412
+ // wrapper-trap-ok: swatch chrome-slot children are always a single literal element (e.g. a selected-state checkmark); interpolated chrome collections are not a documented pattern
412
413
  for (const child of this.children) {
413
414
  if (child === this.#tileEl) continue;
414
415
  if (child === this.#badgeEl) break; // hit the badge — done scanning the head region
@@ -15,16 +15,14 @@
15
15
  "properties": {
16
16
  "columns": {
17
17
  "description": "Column definitions. Array of {key, label, type?, width?, minWidth?, maxWidth?, flex?, sortable?, resizable?, filterable?, pinned?, hidden?, wrap?, accessor?, format?, render?, sortFn?, filterType?, meta?}. Alternative to declarative <col-def> children.",
18
- "type": "array",
19
- "default": []
18
+ "$ref": "common_types.json#/$defs/DynamicStringList"
20
19
  },
21
20
  "component": {
22
21
  "const": "Table"
23
22
  },
24
23
  "data": {
25
24
  "description": "Row records. Array of plain objects keyed to columns[].key.",
26
- "type": "array",
27
- "default": []
25
+ "$ref": "common_types.json#/$defs/DynamicStringList"
28
26
  },
29
27
  "density": {
30
28
  "description": "Controls cell padding and row height. 'compact' for dense data, 'standard' for default spacing, 'comfortable' for spacious rows.",
@@ -87,9 +87,9 @@ export type TableSortEvent = CustomEvent<TableSortEventDetail>;
87
87
 
88
88
  export class UITable extends UIElement {
89
89
  /** Column definitions. Array of {key, label, type?, width?, minWidth?, maxWidth?, flex?, sortable?, resizable?, filterable?, pinned?, hidden?, wrap?, accessor?, format?, render?, sortFn?, filterType?, meta?}. Alternative to declarative <col-def> children. */
90
- columns: unknown[];
90
+ columns: string;
91
91
  /** Row records. Array of plain objects keyed to columns[].key. */
92
- data: unknown[];
92
+ data: string;
93
93
  /** Controls cell padding and row height. 'compact' for dense data, 'standard' for default spacing, 'comfortable' for spacious rows. */
94
94
  density: 'compact' | 'standard' | 'comfortable';
95
95
  /** Enable row expansion */
@@ -23,10 +23,12 @@ props:
23
23
  description: Column definitions. Array of {key, label, type?, width?, minWidth?, maxWidth?, flex?, sortable?, resizable?, filterable?, pinned?, hidden?, wrap?, accessor?, format?, render?, sortFn?, filterType?, meta?}. Alternative to declarative <col-def> children.
24
24
  type: array
25
25
  default: []
26
+ dynamic: true # JS-set collection prop with a custom setter — deliberately not in static properties
26
27
  data:
27
28
  description: Row records. Array of plain objects keyed to columns[].key.
28
29
  type: array
29
30
  default: []
31
+ dynamic: true # JS-set collection prop with a custom setter — deliberately not in static properties
30
32
  density:
31
33
  description: Controls cell padding and row height. 'compact' for dense data, 'standard' for default
32
34
  spacing, 'comfortable' for spacious rows.
@@ -26,6 +26,7 @@ props:
26
26
  type: string
27
27
  default: default
28
28
  enum: [default, bordered]
29
+ reflect: false # declarative presentational attribute — CSS/author-time read, non-reactive by design
29
30
  value:
30
31
  description: Value of the currently active tab. Set programmatically or auto-assigned to the first
31
32
  non-disabled tab on connect.
@@ -71,6 +71,7 @@ props:
71
71
  - solid
72
72
  - muted
73
73
  - outline
74
+ reflect: false # declarative presentational attribute — CSS/author-time read, non-reactive by design
74
75
  events:
75
76
  remove:
76
77
  description: Fired when the dismiss button is activated.
@@ -18,6 +18,7 @@ props:
18
18
  description: When true, restrict to one active option (single-select).
19
19
  type: boolean
20
20
  default: false
21
+ reflect: false # declarative presentational attribute — CSS/author-time read, non-reactive by design
21
22
  value:
22
23
  description: Comma-separated selected values
23
24
  type: string
@@ -297,6 +297,7 @@ export class UIToolbar extends UIElement {
297
297
  // (Items moved to the popover and back must restore their original slot —
298
298
  // otherwise dividers end up clumped next to dividers and groups split.)
299
299
  let idx = 0;
300
+ // wrapper-trap-ok: stamps original DOM order on toolbar items for overflow tracking; toolbar children are always static literal buttons/groups (dynamic toolbars not documented)
300
301
  for (const el of this.children) {
301
302
  if (el === trigger || el === popover) continue;
302
303
  if (el.dataset.toolbarOrder == null) el.dataset.toolbarOrder = String(idx);
@@ -77,8 +77,8 @@
77
77
  "Accordion"
78
78
  ],
79
79
  "slots": {
80
- "default (tree-item-ui children)": {
81
- "description": "Child content region for the `default (tree-item-ui children)` slot."
80
+ "default": {
81
+ "description": "Holds the tree's top-level `<tree-item-ui>` children."
82
82
  },
83
83
  "icon": {
84
84
  "description": "Override slot for the per-node icon glyph. Tree-item's `[icon]` attr stamps a default Phosphor icon; the slot lets consumers fill custom icons (folder open/closed, file-type glyphs, branded markers)."
@@ -41,8 +41,8 @@ events:
41
41
  type: boolean
42
42
  description: Shift key held during activation (false for programmatic select()).
43
43
  slots:
44
- default (tree-item-ui children):
45
- description: "Child content region for the `default (tree-item-ui children)` slot."
44
+ default:
45
+ description: "Holds the tree's top-level `<tree-item-ui>` children."
46
46
  icon:
47
47
  description: >-
48
48
  Override slot for the per-node icon glyph. Tree-item's `[icon]` attr