@adia-ai/web-components 0.5.4 → 0.5.6

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 (46) hide show
  1. package/components/accordion/accordion-item.a2ui.json +50 -0
  2. package/components/accordion/accordion-item.yaml +27 -0
  3. package/components/action-list/action-item.a2ui.json +63 -0
  4. package/components/action-list/action-item.yaml +37 -0
  5. package/components/agent-feedback-bar/class.js +9 -3
  6. package/components/avatar/avatar-group.a2ui.json +50 -0
  7. package/components/avatar/avatar-group.yaml +26 -0
  8. package/components/avatar/avatar.a2ui.json +4 -1
  9. package/components/avatar/avatar.yaml +7 -0
  10. package/components/button/class.js +39 -0
  11. package/components/chart/chart.a2ui.json +4 -2
  12. package/components/list/list-item.a2ui.json +53 -0
  13. package/components/list/list-item.yaml +29 -0
  14. package/components/segmented/segmented.d.ts +3 -3
  15. package/components/select/class.js +14 -0
  16. package/components/select/select.a2ui.json +5 -0
  17. package/components/select/select.css +10 -0
  18. package/components/select/select.d.ts +3 -3
  19. package/components/select/select.yaml +5 -0
  20. package/components/slider/class.js +58 -0
  21. package/components/slider/slider.a2ui.json +10 -0
  22. package/components/slider/slider.css +13 -0
  23. package/components/slider/slider.yaml +10 -0
  24. package/components/switch/class.js +18 -4
  25. package/components/switch/switch.css +10 -0
  26. package/components/switch/switch.d.ts +3 -3
  27. package/components/tabs/tab.a2ui.json +58 -0
  28. package/components/tabs/tab.yaml +33 -0
  29. package/components/timeline/timeline-item.a2ui.json +76 -0
  30. package/components/timeline/timeline-item.yaml +47 -0
  31. package/components/toast/toast.d.ts +35 -0
  32. package/components/tree/class.js +91 -0
  33. package/components/tree/tree-item.a2ui.json +65 -0
  34. package/components/tree/tree-item.yaml +41 -0
  35. package/components/tree/tree.a2ui.json +15 -0
  36. package/components/tree/tree.css +18 -0
  37. package/components/tree/tree.yaml +10 -0
  38. package/core/anchor.d.ts +71 -0
  39. package/core/controller.d.ts +171 -0
  40. package/core/markdown.d.ts +26 -0
  41. package/core/polyfills.d.ts +31 -0
  42. package/core/provider.d.ts +82 -0
  43. package/core/streams-bridge.d.ts +78 -0
  44. package/core/template.js +21 -3
  45. package/core/transport.d.ts +78 -0
  46. package/package.json +2 -2
@@ -42,13 +42,31 @@ export class UISlider extends UIFormElement {
42
42
  step: { type: Number, default: 1, reflect: true },
43
43
  label: { type: String, default: '', reflect: true },
44
44
  suffix: { type: String, default: '', reflect: true },
45
+ // §184 (v0.5.5, FEEDBACK-08 §4): declarative debounce for the
46
+ // `input` event when driving expensive computation (palette regen,
47
+ // shader compile, large list reflow). When > 0, value updates +
48
+ // visual feedback are immediate but `input` event emission is
49
+ // debounced — only the FINAL value in the throttle window dispatches.
50
+ // `change` fires unthrottled on pointerup / track click / keyboard;
51
+ // any pending `input` flushes BEFORE `change` so consumers always
52
+ // see input→input→…→input→change ordering. throttle="0" (default)
53
+ // preserves the pre-§184 every-pointer-move-fires-input behavior.
54
+ throttle: { type: Number, default: 0, reflect: true },
45
55
  };
46
56
 
47
57
  static template = () => null;
48
58
 
59
+ // §184: per-instance hint id counter for aria-describedby wiring.
60
+ static #hintSeq = 0;
61
+
49
62
  #trackEl = null;
50
63
  #thumbEl = null;
51
64
  #dragging = false;
65
+ // §184 (v0.5.5, FEEDBACK-08 §4): debounce timer for the `input`
66
+ // event. When `throttle > 0`, #setValue stores a pending dispatch
67
+ // here + restarts the timer on every value change. Flushed before
68
+ // any `change` event so input always precedes change.
69
+ #inputTimer = null;
52
70
 
53
71
  get #pct() {
54
72
  const range = this.max - this.min;
@@ -69,6 +87,9 @@ export class UISlider extends UIFormElement {
69
87
  if (this.label) this.setAttribute('aria-label', this.label);
70
88
 
71
89
  if (!this.querySelector('[slot="track"]')) {
90
+ // §184 (v0.5.5, FEEDBACK-08 §7): hint slot stamped underneath
91
+ // the track when [hint] is set. Wired to aria-describedby below.
92
+ const hintId = this.hint ? `slider-hint-${++UISlider.#hintSeq}` : '';
72
93
  this.innerHTML = `
73
94
  <div slot="header">
74
95
  ${this.label ? `<span slot="label">${this.label}</span>` : ''}
@@ -81,7 +102,9 @@ export class UISlider extends UIFormElement {
81
102
  <div slot="fill"></div>
82
103
  <div slot="thumb" tabindex="0"></div>
83
104
  </div>
105
+ ${this.hint ? `<span slot="hint" id="${hintId}">${this.hint}</span>` : ''}
84
106
  `;
107
+ if (this.hint) this.setAttribute('aria-describedby', hintId);
85
108
  }
86
109
 
87
110
  this.#trackEl = this.querySelector('[slot="track"]');
@@ -148,9 +171,36 @@ export class UISlider extends UIFormElement {
148
171
  #setValue(v) {
149
172
  if (v === this.value) return;
150
173
  this.value = v;
174
+ // §184: when throttle > 0, debounce the `input` dispatch.
175
+ // The value update + UI is still immediate; only event emission is
176
+ // accumulated. Same value can dispatch input multiple times across
177
+ // pointer moves, but the throttle collapses them to one trailing
178
+ // emission at quiet+throttle ms.
179
+ const t = Number(this.throttle) || 0;
180
+ if (t > 0) {
181
+ if (this.#inputTimer != null) clearTimeout(this.#inputTimer);
182
+ this.#inputTimer = setTimeout(() => {
183
+ this.#inputTimer = null;
184
+ this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
185
+ }, t);
186
+ return;
187
+ }
151
188
  this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
152
189
  }
153
190
 
191
+ /**
192
+ * §184: flush any pending throttled `input` dispatch synchronously.
193
+ * Called before `change` so consumers see the trailing input event
194
+ * BEFORE the change commit. No-op when no timer is pending.
195
+ */
196
+ #flushInput() {
197
+ if (this.#inputTimer != null) {
198
+ clearTimeout(this.#inputTimer);
199
+ this.#inputTimer = null;
200
+ this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
201
+ }
202
+ }
203
+
154
204
  #onPointerDown = (e) => {
155
205
  if (this.disabled) return;
156
206
  e.preventDefault();
@@ -170,12 +220,14 @@ export class UISlider extends UIFormElement {
170
220
  this.#thumbEl.releasePointerCapture(e.pointerId);
171
221
  this.#thumbEl.removeEventListener('pointermove', this.#onPointerMove);
172
222
  this.#thumbEl.removeEventListener('pointerup', this.#onPointerUp);
223
+ this.#flushInput(); // §184: pending throttled input fires before change
173
224
  this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
174
225
  };
175
226
 
176
227
  #onTrackClick = (e) => {
177
228
  if (this.disabled || e.target === this.#thumbEl) return;
178
229
  this.#setValue(this.#valueFromX(e.clientX));
230
+ this.#flushInput(); // §184: ensure trailing input precedes change
179
231
  this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
180
232
  };
181
233
 
@@ -193,6 +245,7 @@ export class UISlider extends UIFormElement {
193
245
  }
194
246
  e.preventDefault();
195
247
  this.#setValue(this.#snap(v));
248
+ this.#flushInput(); // §184: trailing input fires before change
196
249
  this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
197
250
  };
198
251
 
@@ -203,6 +256,11 @@ export class UISlider extends UIFormElement {
203
256
  this.#thumbEl?.removeEventListener('pointermove', this.#onPointerMove);
204
257
  this.#thumbEl?.removeEventListener('pointerup', this.#onPointerUp);
205
258
  this.removeEventListener('keydown', this.#onKey);
259
+ // §184: drop any pending throttle timer (no flush — element is gone)
260
+ if (this.#inputTimer != null) {
261
+ clearTimeout(this.#inputTimer);
262
+ this.#inputTimer = null;
263
+ }
206
264
  this.#trackEl = null;
207
265
  this.#thumbEl = null;
208
266
  }
@@ -31,6 +31,11 @@
31
31
  "type": "string",
32
32
  "default": ""
33
33
  },
34
+ "hint": {
35
+ "description": "§184 (v0.5.5, FEEDBACK-08 §7): small caption rendered beneath the slider track. Sets `aria-describedby` on the host so screen readers announce it as a description (distinct from `aria-label`, which comes from `label`). Does not conflict with the in-component `label`. Use for semantic clarifications a `<field-ui>` wrapper would be overkill for.",
36
+ "type": "string",
37
+ "default": ""
38
+ },
34
39
  "label": {
35
40
  "description": "Label text above the slider",
36
41
  "type": "string",
@@ -61,6 +66,11 @@
61
66
  "type": "string",
62
67
  "default": ""
63
68
  },
69
+ "throttle": {
70
+ "description": "§184 (v0.5.5, FEEDBACK-08 §4): when > 0, debounce the `input` event by this many milliseconds. Value updates + visual feedback remain immediate; only event dispatch accumulates. Pending input flushes BEFORE `change` so consumers always see input→…→input→change ordering. throttle=\"0\" (default) preserves the pre-§184 every-pointer-move-fires-input behavior. Common values: 50-100ms for palette regen / shader compile / large list reflow.",
71
+ "type": "number",
72
+ "default": 0
73
+ },
64
74
  "value": {
65
75
  "description": "Current slider value",
66
76
  "type": "number",
@@ -134,4 +134,17 @@
134
134
  :scope[disabled] [slot="fill"] { background: var(--slider-fill-bg-disabled); }
135
135
  :scope[disabled] [slot="thumb"] { cursor: not-allowed; background: var(--slider-thumb-bg-disabled); }
136
136
  :scope[disabled] [slot="thumb"]:hover { box-shadow: none; }
137
+
138
+ /* ── Hint (§184, v0.5.5, FEEDBACK-08 §7) ──
139
+ Small caption rendered beneath the track. aria-describedby is
140
+ wired on the host in class.js so screen readers announce this
141
+ as a description (distinct from aria-label, which comes from
142
+ [label]). Uses the same muted typography as field-ui's hint. */
143
+ [slot="hint"] {
144
+ display: block;
145
+ margin-top: var(--slider-hint-mt, var(--a-space-1));
146
+ font-size: var(--slider-hint-size, var(--a-fine-size));
147
+ color: var(--slider-hint-fg, var(--a-fg-muted));
148
+ line-height: var(--slider-hint-lh, 1.4);
149
+ }
137
150
  }
@@ -51,6 +51,16 @@ props:
51
51
  description: Current slider value
52
52
  type: number
53
53
  default: 50
54
+ throttle:
55
+ description: |-
56
+ §184 (v0.5.5, FEEDBACK-08 §4): when > 0, debounce the `input` event by this many milliseconds. Value updates + visual feedback remain immediate; only event dispatch accumulates. Pending input flushes BEFORE `change` so consumers always see input→…→input→change ordering. throttle="0" (default) preserves the pre-§184 every-pointer-move-fires-input behavior. Common values: 50-100ms for palette regen / shader compile / large list reflow.
57
+ type: number
58
+ default: 0
59
+ hint:
60
+ description: |-
61
+ §184 (v0.5.5, FEEDBACK-08 §7): small caption rendered beneath the slider track. Sets `aria-describedby` on the host so screen readers announce it as a description (distinct from `aria-label`, which comes from `label`). Does not conflict with the in-component `label`. Use for semantic clarifications a `<field-ui>` wrapper would be overkill for.
62
+ type: string
63
+ default: ""
54
64
  events:
55
65
  change:
56
66
  description: "Fired when the value changes (on blur for inputs, on selection for pickers)."
@@ -26,12 +26,26 @@ export class UISwitch extends UIFormElement {
26
26
  checked: { type: Boolean, default: false, reflect: true },
27
27
  label: { type: String, default: '', reflect: true },
28
28
  size: { type: String, default: '', reflect: true },
29
+ // §184 (v0.5.5, FEEDBACK-08 §7): caption beneath the toggle.
30
+ // switch.yaml has declared `hint` since §170 but the template
31
+ // never rendered it — this arc closes the spec-vs-impl gap +
32
+ // wires aria-describedby on the host for screen readers.
33
+ hint: { type: String, default: '', reflect: true },
29
34
  };
30
35
 
31
- static template = (el) => html`
32
- <span slot="track"><span slot="thumb"></span></span>
33
- ${el.label ? html`<span slot="label">${el.label}</span>` : null}
34
- `;
36
+ // §184: per-instance hint id counter for aria-describedby wiring.
37
+ static #hintSeq = 0;
38
+
39
+ static template = (el) => {
40
+ const hintId = el.hint ? `switch-hint-${++UISwitch.#hintSeq}` : '';
41
+ if (hintId) el.setAttribute('aria-describedby', hintId);
42
+ else el.removeAttribute('aria-describedby');
43
+ return html`
44
+ <span slot="track"><span slot="thumb"></span></span>
45
+ ${el.label ? html`<span slot="label">${el.label}</span>` : null}
46
+ ${el.hint ? html`<span slot="hint" id=${hintId}>${el.hint}</span>` : null}
47
+ `;
48
+ };
35
49
 
36
50
  connected() {
37
51
  super.connected();
@@ -105,6 +105,16 @@ switch-ui[checked] [slot="thumb"] {
105
105
 
106
106
  [slot="label"] { font-size: var(--switch-font-size); }
107
107
 
108
+ /* ── Hint (§184, v0.5.5, FEEDBACK-08 §7) ──
109
+ Caption beneath the toggle row; wired to aria-describedby on host. */
110
+ [slot="hint"] {
111
+ display: block;
112
+ margin-top: var(--switch-hint-mt, var(--a-space-1));
113
+ font-size: var(--switch-hint-size, var(--a-fine-size));
114
+ color: var(--switch-hint-fg, var(--a-fg-muted));
115
+ line-height: var(--switch-hint-lh, 1.4);
116
+ }
117
+
108
118
  :scope:focus-visible { outline: none; }
109
119
  :scope:focus-visible [slot="track"] { box-shadow: var(--switch-focus-ring); }
110
120
 
@@ -6,13 +6,13 @@
6
6
 
7
7
  import { UIFormElement } from '../../core/form.js';
8
8
 
9
- export interface SwitchChangeEventDetail {
9
+ export interface SwitchChangeEventDetail<V extends string = string> {
10
10
  /** Submitted value (defaults to `"on"` when checked). */
11
- value: string;
11
+ value: V;
12
12
  /** Current checked state. */
13
13
  checked: boolean;
14
14
  }
15
- export type SwitchChangeEvent = CustomEvent<SwitchChangeEventDetail>;
15
+ export type SwitchChangeEvent<V extends string = string> = CustomEvent<SwitchChangeEventDetail<V>>;
16
16
 
17
17
  export class UISwitch extends UIFormElement {
18
18
  /** Checked state — reflected, toggles on click. */
@@ -0,0 +1,58 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://adiaui.dev/a2ui/v0_9/components/Tab.json",
4
+ "title": "Tab",
5
+ "description": "Child of <tabs-ui>. One tab panel — the tab BUTTON is rendered by the parent from this child's text/icon. Wraps panel content as light DOM.",
6
+ "type": "object",
7
+ "allOf": [
8
+ {
9
+ "$ref": "common_types.json#/$defs/ComponentCommon"
10
+ },
11
+ {
12
+ "$ref": "common_types.json#/$defs/CatalogComponentCommon"
13
+ }
14
+ ],
15
+ "properties": {
16
+ "component": {
17
+ "const": "Tab"
18
+ },
19
+ "disabled": {
20
+ "description": "Whether the tab is selectable.",
21
+ "type": "boolean",
22
+ "default": false
23
+ },
24
+ "icon": {
25
+ "description": "Optional leading icon name (Phosphor) for the tab button.",
26
+ "type": "string"
27
+ },
28
+ "text": {
29
+ "description": "Tab button label (rendered by parent <tabs-ui>).",
30
+ "type": "string"
31
+ },
32
+ "value": {
33
+ "description": "Stable id for the tab. Parent uses this to coordinate active state.",
34
+ "type": "string"
35
+ }
36
+ },
37
+ "required": [
38
+ "component"
39
+ ],
40
+ "unevaluatedProperties": false,
41
+ "x-adiaui": {
42
+ "anti_patterns": [],
43
+ "category": "navigation",
44
+ "composes": [],
45
+ "events": {},
46
+ "examples": [],
47
+ "keywords": [],
48
+ "name": "UITab",
49
+ "related": [],
50
+ "slots": {},
51
+ "states": [],
52
+ "synonyms": {},
53
+ "tag": "tab-ui",
54
+ "tokens": {},
55
+ "traits": [],
56
+ "version": 1
57
+ }
58
+ }
@@ -0,0 +1,33 @@
1
+ # Edit this file; run `npm run build:components` to regenerate a2ui.json.
2
+ #
3
+ # §176 (v0.5.5): authored to close the §175 baseline-orphan class. The
4
+ # component already existed as a sibling class in the parent's class.js
5
+ # + was registered alongside the parent (e.g. UIList + UIListItem both
6
+ # from list/class.js). The catalog just lacked its own entry. With the
7
+ # §172 sibling-yaml scanner, this file gets picked up next to the parent
8
+ # yaml.
9
+
10
+ # Child component of <tabs-ui>. Surface only inside that parent.
11
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
12
+ name: UITab
13
+ tag: tab-ui
14
+ component: Tab
15
+ category: navigation
16
+ version: 1
17
+ description: |-
18
+ Child of <tabs-ui>. One tab panel — the tab BUTTON is rendered by the parent from this child's text/icon. Wraps panel content as light DOM.
19
+
20
+ props:
21
+ text:
22
+ description: Tab button label (rendered by parent <tabs-ui>).
23
+ type: string
24
+ value:
25
+ description: Stable id for the tab. Parent uses this to coordinate active state.
26
+ type: string
27
+ icon:
28
+ description: Optional leading icon name (Phosphor) for the tab button.
29
+ type: string
30
+ disabled:
31
+ description: Whether the tab is selectable.
32
+ type: boolean
33
+ default: false
@@ -0,0 +1,76 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://adiaui.dev/a2ui/v0_9/components/TimelineItem.json",
4
+ "title": "TimelineItem",
5
+ "description": "Child of <timeline-ui>. One step in a sequenced reasoning/process timeline. Used heavily by <agent-reasoning-ui> + <agent-trace-ui>.",
6
+ "type": "object",
7
+ "allOf": [
8
+ {
9
+ "$ref": "common_types.json#/$defs/ComponentCommon"
10
+ },
11
+ {
12
+ "$ref": "common_types.json#/$defs/CatalogComponentCommon"
13
+ }
14
+ ],
15
+ "properties": {
16
+ "description": {
17
+ "description": "Subtitle below the label.",
18
+ "type": "string"
19
+ },
20
+ "component": {
21
+ "const": "TimelineItem"
22
+ },
23
+ "duration": {
24
+ "description": "Elapsed time (e.g. \"1.2s\", \"340ms\").",
25
+ "type": "string"
26
+ },
27
+ "icon": {
28
+ "description": "Optional leading icon name (Phosphor).",
29
+ "type": "string"
30
+ },
31
+ "spinner": {
32
+ "description": "Show a spinner instead of the icon while status=pending.",
33
+ "type": "boolean",
34
+ "default": false
35
+ },
36
+ "status": {
37
+ "description": "Lifecycle state (idle | pending | done | failed).",
38
+ "type": "string",
39
+ "default": "idle"
40
+ },
41
+ "text": {
42
+ "description": "Primary step label.",
43
+ "type": "string"
44
+ },
45
+ "time": {
46
+ "description": "Absolute timestamp (HH:MM or ISO).",
47
+ "type": "string"
48
+ },
49
+ "variant": {
50
+ "description": "Visual variant (default | accent | success | warning | danger).",
51
+ "type": "string",
52
+ "default": "default"
53
+ }
54
+ },
55
+ "required": [
56
+ "component"
57
+ ],
58
+ "unevaluatedProperties": false,
59
+ "x-adiaui": {
60
+ "anti_patterns": [],
61
+ "category": "feedback",
62
+ "composes": [],
63
+ "events": {},
64
+ "examples": [],
65
+ "keywords": [],
66
+ "name": "UITimelineItem",
67
+ "related": [],
68
+ "slots": {},
69
+ "states": [],
70
+ "synonyms": {},
71
+ "tag": "timeline-item-ui",
72
+ "tokens": {},
73
+ "traits": [],
74
+ "version": 1
75
+ }
76
+ }
@@ -0,0 +1,47 @@
1
+ # Edit this file; run `npm run build:components` to regenerate a2ui.json.
2
+ #
3
+ # §176 (v0.5.5): authored to close the §175 baseline-orphan class. The
4
+ # component already existed as a sibling class in the parent's class.js
5
+ # + was registered alongside the parent (e.g. UIList + UIListItem both
6
+ # from list/class.js). The catalog just lacked its own entry. With the
7
+ # §172 sibling-yaml scanner, this file gets picked up next to the parent
8
+ # yaml.
9
+
10
+ # Child component of <timeline-ui>. Surface only inside that parent.
11
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
12
+ name: UITimelineItem
13
+ tag: timeline-item-ui
14
+ component: TimelineItem
15
+ category: feedback
16
+ version: 1
17
+ description: |-
18
+ Child of <timeline-ui>. One step in a sequenced reasoning/process timeline. Used heavily by <agent-reasoning-ui> + <agent-trace-ui>.
19
+
20
+ props:
21
+ text:
22
+ description: Primary step label.
23
+ type: string
24
+ description:
25
+ description: Subtitle below the label.
26
+ type: string
27
+ time:
28
+ description: Absolute timestamp (HH:MM or ISO).
29
+ type: string
30
+ duration:
31
+ description: Elapsed time (e.g. "1.2s", "340ms").
32
+ type: string
33
+ icon:
34
+ description: Optional leading icon name (Phosphor).
35
+ type: string
36
+ variant:
37
+ description: Visual variant (default | accent | success | warning | danger).
38
+ type: string
39
+ default: 'default'
40
+ status:
41
+ description: Lifecycle state (idle | pending | done | failed).
42
+ type: string
43
+ default: 'idle'
44
+ spinner:
45
+ description: Show a spinner instead of the icon while status=pending.
46
+ type: boolean
47
+ default: false
@@ -12,6 +12,28 @@
12
12
 
13
13
  import { UIElement } from '../../core/element.js';
14
14
 
15
+ /** Options accepted by `UIToast.show(opts)` — the imperative one-shot path. */
16
+ export interface UIToastShowOptions {
17
+ /** Toast message text. */
18
+ text?: string;
19
+ /** Semantic variant. Legacy alias `"error"` is auto-mapped to `"danger"`. */
20
+ variant?: 'default' | 'info' | 'success' | 'warning' | 'danger' | 'primary' | 'muted' | 'neutral' | 'error';
21
+ /** Auto-dismiss time in milliseconds. 0 disables auto-dismiss. Default `4000`. */
22
+ duration?: number;
23
+ /** Screen position. Default `'bottom-right'`. */
24
+ position?: 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
25
+ }
26
+
27
+ /** Returned by `UIToast.show()` — imperative handle for dismiss / update. */
28
+ export interface UIToastFeedHandle {
29
+ /** Stable id assigned by `<feed-ui>`. `null` if the toast couldn't be posted. */
30
+ id: string | null;
31
+ /** Dismiss the toast programmatically. */
32
+ dismiss(): void;
33
+ /** Mutate the toast's content in-place (e.g. promote from "loading" to "done"). */
34
+ update(patch: Partial<UIToastShowOptions>): void;
35
+ }
36
+
15
37
  export class UIToast extends UIElement {
16
38
  /** Auto-dismiss time in milliseconds. 0 disables auto-dismiss. */
17
39
  duration: number;
@@ -21,4 +43,17 @@ export class UIToast extends UIElement {
21
43
  text: string;
22
44
  /** Semantic variant — `default | info | success | warning | danger`. `primary` and `muted` are style hints; canonical "neutral but interesting" tone is `info`. */
23
45
  variant: 'default' | 'info' | 'success' | 'warning' | 'danger' | 'primary' | 'muted' | 'neutral';
46
+
47
+ /**
48
+ * Post a one-shot toast through the shared `<feed-ui>` host. Imperative
49
+ * alternative to declarative `<toast-ui>`. Returns a `UIToastFeedHandle`
50
+ * for programmatic dismiss / update.
51
+ *
52
+ * Legacy alias `variant: 'error'` is auto-mapped to `variant: 'danger'`.
53
+ *
54
+ * @example
55
+ * const t = UIToast.show({ text: 'Saved!', variant: 'success' });
56
+ * setTimeout(() => t.dismiss(), 1000);
57
+ */
58
+ static show(opts?: UIToastShowOptions): UIToastFeedHandle;
24
59
  }
@@ -63,6 +63,82 @@ export class UITree extends UIElement {
63
63
  }));
64
64
  }
65
65
 
66
+ // ──────────────────────────────────────────────────────────────────
67
+ // §184 (v0.5.5, FEEDBACK-08 §2): programmatic expand/collapse API.
68
+ // Pre-§184 consumers had to maintain local `_expanded[]` state +
69
+ // manually sync `tree-item.open` on every `tree-select` event. These
70
+ // five methods + the `auto-expand-selected` reflection let consumers
71
+ // declare the open state directly. All methods accept either a
72
+ // tree-item-ui element OR a value string (matched against [value]
73
+ // first, then [text] for ergonomics).
74
+ // ──────────────────────────────────────────────────────────────────
75
+
76
+ /**
77
+ * Resolve an `item` argument to a tree-item-ui element, or `null` if
78
+ * no match. Accepts: HTMLElement (used as-is when it matches `tree-item-ui`)
79
+ * OR a string (matched against [value] first, then [text]).
80
+ */
81
+ #resolveItem(arg) {
82
+ if (!arg) return null;
83
+ if (arg instanceof Element) {
84
+ return arg.matches?.('tree-item-ui') ? arg : null;
85
+ }
86
+ if (typeof arg === 'string') {
87
+ const escaped = arg.replace(/"/g, '\\"');
88
+ return (
89
+ this.querySelector(`tree-item-ui[value="${escaped}"]`) ||
90
+ this.querySelector(`tree-item-ui[text="${escaped}"]`) ||
91
+ null
92
+ );
93
+ }
94
+ return null;
95
+ }
96
+
97
+ /** Expand the given item (no-op when already open or it has no children). */
98
+ expand(itemOrValue) {
99
+ const item = this.#resolveItem(itemOrValue);
100
+ if (item && item.hasChildren && !item.open) item.open = true;
101
+ }
102
+
103
+ /** Collapse the given item (no-op when already collapsed). */
104
+ collapse(itemOrValue) {
105
+ const item = this.#resolveItem(itemOrValue);
106
+ if (item && item.open) item.open = false;
107
+ }
108
+
109
+ /** Expand every item with children. */
110
+ expandAll() {
111
+ for (const it of this.querySelectorAll('tree-item-ui')) {
112
+ if (it.hasChildren && !it.open) it.open = true;
113
+ }
114
+ }
115
+
116
+ /** Collapse every item. */
117
+ collapseAll() {
118
+ for (const it of this.querySelectorAll('tree-item-ui')) {
119
+ if (it.open) it.open = false;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Expand every ancestor of the given item so it becomes visible, then
125
+ * scroll it into view. Useful for revealing a programmatically-selected
126
+ * item nested inside collapsed parents.
127
+ */
128
+ expandTo(itemOrValue) {
129
+ const item = this.#resolveItem(itemOrValue);
130
+ if (!item) return;
131
+ let parent = item.parentElement?.closest('tree-item-ui');
132
+ while (parent) {
133
+ if (parent.hasChildren && !parent.open) parent.open = true;
134
+ parent = parent.parentElement?.closest('tree-item-ui');
135
+ }
136
+ item.querySelector(':scope > [slot="row"]')?.scrollIntoView?.({
137
+ block: 'nearest',
138
+ inline: 'nearest',
139
+ });
140
+ }
141
+
66
142
  #onClick = (e) => {
67
143
  const item = e.target.closest('tree-item-ui');
68
144
  if (!item || !this.contains(item)) return;
@@ -171,6 +247,10 @@ export class UITreeItem extends UIElement {
171
247
  value: { type: String, default: '', reflect: true },
172
248
  open: { type: Boolean, default: false, reflect: true },
173
249
  selected: { type: Boolean, default: false, reflect: true },
250
+ // §184 (v0.5.5, FEEDBACK-08 §1): optional trailing badge for counts
251
+ // / labels (e.g. "Colors (7)" → text="Colors" badge="7"). Mirrors
252
+ // nav-item-ui badge API; styled muted-and-small via tree.css.
253
+ badge: { type: String, default: '', reflect: true },
174
254
  };
175
255
 
176
256
  static template = () => null;
@@ -222,6 +302,13 @@ export class UITreeItem extends UIElement {
222
302
  textEl.textContent = this.text;
223
303
  row.appendChild(textEl);
224
304
 
305
+ // §184: trailing badge (when [badge] is set). Stamped even when
306
+ // empty so render() can populate without re-stamping the DOM.
307
+ const badgeEl = document.createElement('span');
308
+ badgeEl.setAttribute('slot', 'badge');
309
+ if (this.badge) badgeEl.textContent = this.badge;
310
+ row.appendChild(badgeEl);
311
+
225
312
  // Actions slot placeholder
226
313
  const actions = document.createElement('span');
227
314
  actions.setAttribute('slot', 'actions');
@@ -247,5 +334,9 @@ export class UITreeItem extends UIElement {
247
334
  // Update text
248
335
  const textEl = row.querySelector('[slot="text"]');
249
336
  if (textEl && this.text) textEl.textContent = this.text;
337
+
338
+ // §184: keep badge slot synced with the [badge] attribute.
339
+ const badgeEl = row.querySelector('[slot="badge"]');
340
+ if (badgeEl) badgeEl.textContent = this.badge || '';
250
341
  }
251
342
  }