@adia-ai/web-components 0.8.30 → 0.8.32

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 (120) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/components/accordion/accordion.d.ts +2 -2
  3. package/components/action-list/action-list.class.js +6 -2
  4. package/components/action-list/action-list.d.ts +1 -1
  5. package/components/agent-artifact/agent-artifact.d.ts +1 -1
  6. package/components/agent-feedback-bar/agent-feedback-bar.d.ts +2 -2
  7. package/components/agent-questions/agent-questions.d.ts +1 -1
  8. package/components/agent-reasoning/agent-reasoning.d.ts +3 -3
  9. package/components/agent-suggestions/agent-suggestions.d.ts +1 -1
  10. package/components/agent-trace/agent-trace.d.ts +1 -1
  11. package/components/alert/alert.d.ts +2 -2
  12. package/components/anchor-bar/anchor-bar.d.ts +2 -2
  13. package/components/avatar/avatar.class.js +0 -1
  14. package/components/avatar/avatar.css +16 -2
  15. package/components/badge/badge.css +30 -2
  16. package/components/breadcrumb/breadcrumb.css +15 -3
  17. package/components/button/button.d.ts +1 -1
  18. package/components/calendar-grid/calendar-grid.d.ts +2 -2
  19. package/components/card/card.css +21 -0
  20. package/components/card/card.yaml +9 -0
  21. package/components/chart/chart.d.ts +4 -4
  22. package/components/chart-legend/chart-legend.d.ts +1 -1
  23. package/components/chat-thread/chat-input.css +1 -1
  24. package/components/chat-thread/chat-input.js +9 -1
  25. package/components/chat-thread/chat-thread.class.js +6 -0
  26. package/components/chat-thread/chat-thread.d.ts +2 -2
  27. package/components/check/check.css +3 -3
  28. package/components/command/command.d.ts +2 -2
  29. package/components/context-menu/context-menu.d.ts +3 -3
  30. package/components/datetime-picker/datetime-picker.d.ts +5 -5
  31. package/components/demo-toggle/demo-toggle.d.ts +1 -1
  32. package/components/drawer/drawer.d.ts +2 -2
  33. package/components/heatmap/heatmap.d.ts +5 -5
  34. package/components/index.js +1 -0
  35. package/components/inline-edit/inline-edit.d.ts +4 -4
  36. package/components/input/input.a2ui.json +4 -1
  37. package/components/input/input.css +18 -0
  38. package/components/input/input.yaml +11 -1
  39. package/components/integration-card/integration-card.d.ts +4 -4
  40. package/components/link/link.d.ts +1 -1
  41. package/components/list/list-item.a2ui.json +6 -0
  42. package/components/list/list-item.yaml +13 -0
  43. package/components/list/list.class.js +28 -7
  44. package/components/list/list.d.ts +1 -1
  45. package/components/list-window/list-window.d.ts +5 -5
  46. package/components/menu/menu.css +2 -2
  47. package/components/menu/menu.d.ts +1 -1
  48. package/components/modal/modal.d.ts +1 -1
  49. package/components/nav/nav.css +8 -0
  50. package/components/nav/nav.d.ts +1 -1
  51. package/components/nav-group/nav-group.css +58 -9
  52. package/components/nav-group/nav-group.d.ts +1 -1
  53. package/components/nav-item/nav-item.css +7 -4
  54. package/components/nav-item/nav-item.d.ts +1 -1
  55. package/components/noodles/noodles.d.ts +3 -3
  56. package/components/option-card/option-card.css +1 -1
  57. package/components/pagination/pagination.d.ts +1 -1
  58. package/components/pane/pane.d.ts +1 -1
  59. package/components/password-strength/password-strength.d.ts +1 -1
  60. package/components/pipeline-status/pipeline-status.a2ui.json +14 -8
  61. package/components/pipeline-status/pipeline-status.d.ts +5 -4
  62. package/components/pipeline-status/pipeline-status.yaml +17 -11
  63. package/components/radio/radio.a2ui.json +4 -3
  64. package/components/radio/radio.css +2 -2
  65. package/components/radio/radio.yaml +7 -14
  66. package/components/radio-group/radio-group.a2ui.json +118 -0
  67. package/components/radio-group/radio-group.class.js +75 -0
  68. package/components/radio-group/radio-group.css +31 -0
  69. package/components/radio-group/radio-group.d.ts +20 -0
  70. package/components/radio-group/radio-group.examples.md +37 -0
  71. package/components/radio-group/radio-group.js +17 -0
  72. package/components/radio-group/radio-group.yaml +114 -0
  73. package/components/segmented/segmented.a2ui.json +5 -0
  74. package/components/segmented/segmented.class.js +20 -2
  75. package/components/segmented/segmented.yaml +5 -0
  76. package/components/stat/stat.css +12 -8
  77. package/components/stat/stat.js +66 -1
  78. package/components/stat/stat.yaml +3 -0
  79. package/components/stepper/stepper-item.a2ui.json +5 -0
  80. package/components/stepper/stepper-item.yaml +11 -0
  81. package/components/stepper/stepper.a2ui.json +38 -3
  82. package/components/stepper/stepper.class.js +199 -2
  83. package/components/stepper/stepper.css +30 -0
  84. package/components/stepper/stepper.d.ts +28 -1
  85. package/components/stepper/stepper.yaml +76 -2
  86. package/components/stream/stream.d.ts +3 -3
  87. package/components/swatch/swatch.class.js +1 -1
  88. package/components/swatch/swatch.d.ts +1 -1
  89. package/components/swiper/swiper.d.ts +3 -3
  90. package/components/table/table.a2ui.json +2 -2
  91. package/components/table/table.d.ts +7 -7
  92. package/components/table/table.yaml +9 -1
  93. package/components/table-toolbar/table-toolbar.d.ts +4 -4
  94. package/components/tabs/tabs.css +11 -3
  95. package/components/tabs/tabs.d.ts +1 -1
  96. package/components/tag/tag.a2ui.json +1 -1
  97. package/components/tag/tag.css +78 -13
  98. package/components/tag/tag.d.ts +12 -8
  99. package/components/tag/tag.examples.md +4 -4
  100. package/components/tag/tag.yaml +18 -7
  101. package/components/tags-input/tags-input.d.ts +4 -4
  102. package/components/timeline/timeline.d.ts +1 -1
  103. package/components/toc/toc.d.ts +1 -1
  104. package/components/toggle-group/toggle-group.d.ts +1 -1
  105. package/components/toggle-scheme/toggle-scheme.d.ts +1 -1
  106. package/components/tour/tour.d.ts +4 -4
  107. package/components/tree/tree.d.ts +1 -1
  108. package/core/icons-phosphor.js +129 -71
  109. package/core/provider.js +84 -23
  110. package/dist/host.min.css +1 -1
  111. package/dist/host.sheet.js +1 -1
  112. package/dist/theme-provider.min.js +1 -1
  113. package/dist/web-components.min.css +1 -1
  114. package/dist/web-components.min.js +89 -87
  115. package/dist/web-components.sheet.js +1 -1
  116. package/package.json +1 -1
  117. package/styles/api/sizing-fallback.css +32 -0
  118. package/styles/api/sizing.css +7 -3
  119. package/styles/components.css +1 -0
  120. package/styles/tokens.css +1 -0
@@ -28,21 +28,104 @@
28
28
  *
29
29
  * el.next(); el.prev(); el.goTo(2);
30
30
  *
31
+ * <!-- Host-controlled click-to-navigate + keyboard (gh#711 S1) -->
32
+ * <stepper-ui step="1" interactive>
33
+ * <stepper-item-ui text="Account"></stepper-item-ui>
34
+ * <stepper-item-ui text="Details" disabled></stepper-item-ui>
35
+ * ...
36
+ * </stepper-ui>
37
+ *
38
+ * <!-- Veto a navigation before it lands (gh#711 S1 review) -->
39
+ * el.addEventListener('step-request', (e) => {
40
+ * if (!phaseAllows(e.detail.step)) e.preventDefault();
41
+ * });
42
+ *
31
43
  * Attributes:
32
44
  * step — current step index (0-based)
33
45
  * orientation — horizontal (default) | vertical
46
+ * interactive — opt-in click-to-navigate + roving-tabindex keyboard
47
+ * nav (arrow keys / Home / End). Absent by default —
48
+ * a bare stepper is a read-only progress display.
49
+ *
50
+ * Events (interactive only):
51
+ * step-request — cancelable, fires BEFORE `step` mutates.
52
+ * { detail: { step, previous } }. `preventDefault()`
53
+ * vetoes the navigation: no mutation, no `change`,
54
+ * focus/roving state unaffected. A host that never
55
+ * listens keeps today's uncontrolled behavior —
56
+ * this is additive, not a breaking change.
57
+ * change — { detail: { step } } — fires AFTER a click or
58
+ * keyboard navigation changes `step` (i.e. only if
59
+ * `step-request` wasn't vetoed). The host reacts;
60
+ * the stepper does not itself drive any other UI.
34
61
  */
35
62
 
36
63
  import { UIElement } from '../../core/element.js';
37
64
 
65
+ // gh#736 — during static innerHTML parsing, a stepper-item-ui's attribute
66
+ // list is on the DOM node the instant the parser inserts it, but its
67
+ // `disabled` PROPERTY mirror only lands once its own attributeChangedCallback
68
+ // replay has run — which some environments (this repo's happy-dom test env,
69
+ // certain upgrade orderings) don't guarantee has happened yet by the time the
70
+ // PARENT's connectedCallback (and its first render()) fires, since children
71
+ // connect after their parent in tree order. Reading the attribute directly
72
+ // is synchronous with parsing and never lags, so it's the race-proof source
73
+ // of truth; the property read stays primary for the (overwhelmingly common)
74
+ // already-upgraded case.
75
+ function isDisabled(item) {
76
+ return item.disabled || item.hasAttribute('disabled');
77
+ }
78
+
38
79
  export class UIStepper extends UIElement {
39
80
  static properties = {
40
- step: { type: Number, default: 0, reflect: true },
41
- orientation: { type: String, default: 'horizontal', reflect: true },
81
+ step: { type: Number, default: 0, reflect: true },
82
+ orientation: { type: String, default: 'horizontal', reflect: true },
83
+ interactive: { type: Boolean, default: false, reflect: true },
42
84
  };
43
85
 
44
86
  static template = () => null;
45
87
 
88
+ // gh#736 — two related child-reactivity gaps, one observer:
89
+ // 1. Upgrade-order race: during static innerHTML parsing this element's
90
+ // connectedCallback (and its first synchronous render()) can fire
91
+ // BEFORE its stepper-item-ui children are even appended as DOM
92
+ // children (confirmed: this element's own render() sees zero items
93
+ // on that first pass) — the isDisabled() hasAttribute fallback in
94
+ // render() alone can't help when there's nothing to iterate yet.
95
+ // Watching `childList` catches children landing a tick later and
96
+ // re-renders once they're actually there.
97
+ // 2. A child's [disabled] flip after mount must re-render this parent's
98
+ // roving-tabindex/aria-disabled computation without the host
99
+ // manually poking render() (nav-group-ui's gh#501 idiom: a scoped
100
+ // MutationObserver, not a signal, since `disabled` lives on
101
+ // descendant DOM, not this element's own reactive properties).
102
+ // attributeFilter is narrow (['disabled']) and render() never writes
103
+ // childList or that attribute itself (it only reads `disabled`), so the
104
+ // observer can't feed back into its own render.
105
+ #onChildMutation = () => this.render();
106
+ #childObserver = null;
107
+
108
+ connected() {
109
+ this.addEventListener('click', this.#handleClick);
110
+ this.addEventListener('keydown', this.#handleKeydown);
111
+ if (!this.#childObserver) {
112
+ this.#childObserver = new MutationObserver(this.#onChildMutation);
113
+ this.#childObserver.observe(this, {
114
+ childList: true,
115
+ subtree: true,
116
+ attributes: true,
117
+ attributeFilter: ['disabled'],
118
+ });
119
+ }
120
+ }
121
+
122
+ disconnected() {
123
+ this.removeEventListener('click', this.#handleClick);
124
+ this.removeEventListener('keydown', this.#handleKeydown);
125
+ this.#childObserver?.disconnect();
126
+ this.#childObserver = null;
127
+ }
128
+
46
129
  render() {
47
130
  const items = [...this.querySelectorAll('stepper-item-ui')];
48
131
 
@@ -64,7 +147,40 @@ export class UIStepper extends UIElement {
64
147
  } else {
65
148
  item.setAttribute('status', 'idle');
66
149
  }
150
+
151
+ // Roving tabindex + button semantics only when the host opts
152
+ // in via [interactive] — a bare stepper stays a non-focusable
153
+ // progress display (Rule: absent attribute is the default).
154
+ if (this.interactive) {
155
+ item.setAttribute('role', 'button');
156
+ if (i === this.step) item.setAttribute('aria-current', 'step');
157
+ else item.removeAttribute('aria-current');
158
+ } else {
159
+ item.removeAttribute('role');
160
+ item.removeAttribute('tabindex');
161
+ item.removeAttribute('aria-current');
162
+ item.removeAttribute('aria-disabled');
163
+ }
67
164
  });
165
+
166
+ // Disabled items (S2's phase-gating mechanism) never take the
167
+ // roving tabindex stop and are never clickable/keyboard-reachable
168
+ // — an author-set [disabled] on a stepper-item-ui always wins,
169
+ // whatever [status] the parent computed for it above.
170
+ if (this.interactive) {
171
+ const enabled = items.filter((item) => !isDisabled(item));
172
+ const current = items[this.step];
173
+ const focusTarget = current && !isDisabled(current) ? current : enabled[0];
174
+ for (const item of items) {
175
+ if (isDisabled(item)) {
176
+ item.setAttribute('aria-disabled', 'true');
177
+ item.removeAttribute('tabindex');
178
+ } else {
179
+ item.removeAttribute('aria-disabled');
180
+ item.setAttribute('tabindex', item === focusTarget ? '0' : '-1');
181
+ }
182
+ }
183
+ }
68
184
  }
69
185
 
70
186
  next() {
@@ -80,6 +196,82 @@ export class UIStepper extends UIElement {
80
196
  const max = this.querySelectorAll('stepper-item-ui').length - 1;
81
197
  this.step = Math.max(0, Math.min(index, max));
82
198
  }
199
+
200
+ get #items() {
201
+ return [...this.querySelectorAll('stepper-item-ui')];
202
+ }
203
+
204
+ get #enabledItems() {
205
+ return this.#items.filter((item) => !isDisabled(item));
206
+ }
207
+
208
+ /** @returns {boolean} true if the navigation landed, false if vetoed or a no-op. */
209
+ #select(index) {
210
+ const items = this.#items;
211
+ const max = items.length - 1;
212
+ const next = Math.max(0, Math.min(index, max));
213
+ if (next === this.step || (items[next] && isDisabled(items[next]))) return false;
214
+
215
+ // Pre-mutation cancelable event (gh#711 S1 review) — the host
216
+ // gets a veto BEFORE `step` changes. `preventDefault()` stops
217
+ // the mutation and the `change` event; focus/roving are
218
+ // untouched either way. A host that never listens sees no
219
+ // difference from before this event existed.
220
+ const previous = this.step;
221
+ const proceed = this.dispatchEvent(new CustomEvent('step-request', {
222
+ bubbles: true,
223
+ cancelable: true,
224
+ detail: { step: next, previous },
225
+ }));
226
+ if (!proceed) return false; // vetoed
227
+
228
+ this.step = next;
229
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { step: this.step } }));
230
+ return true;
231
+ }
232
+
233
+ #handleClick = (e) => {
234
+ if (!this.interactive) return;
235
+ const item = e.target.closest('stepper-item-ui');
236
+ if (!item || !this.contains(item) || isDisabled(item)) return;
237
+ const index = this.#items.indexOf(item);
238
+ if (index < 0) return;
239
+ this.#select(index);
240
+ };
241
+
242
+ #handleKeydown = (e) => {
243
+ if (!this.interactive) return;
244
+ const item = e.target.closest('stepper-item-ui');
245
+ if (!item || !this.contains(item)) return;
246
+
247
+ // Disabled items are unreachable (no tabindex) and skipped in the
248
+ // arrow/Home/End order — the roving set is the enabled subset.
249
+ const items = this.#enabledItems;
250
+ const idx = items.indexOf(item);
251
+ if (idx < 0) return;
252
+
253
+ const isVertical = this.orientation === 'vertical';
254
+ const nextKey = isVertical ? 'ArrowDown' : 'ArrowRight';
255
+ const prevKey = isVertical ? 'ArrowUp' : 'ArrowLeft';
256
+
257
+ let next;
258
+ switch (e.key) {
259
+ case nextKey: next = idx < items.length - 1 ? idx + 1 : 0; break; // wraps to first
260
+ case prevKey: next = idx > 0 ? idx - 1 : items.length - 1; break; // wraps to last
261
+ case 'Home': next = 0; break;
262
+ case 'End': next = items.length - 1; break;
263
+ case 'Enter':
264
+ case ' ': e.preventDefault(); this.#select(this.#items.indexOf(item)); return;
265
+ default: return;
266
+ }
267
+
268
+ e.preventDefault();
269
+ const target = items[next];
270
+ // Only move roving focus if the navigation actually landed — a
271
+ // veto leaves focus (and the tabindex roving stop) exactly where
272
+ // it was, per the review's "focus/roving unaffected" contract.
273
+ if (this.#select(this.#items.indexOf(target))) target.focus();
274
+ };
83
275
  }
84
276
 
85
277
  export class UIStepperItem extends UIElement {
@@ -92,6 +284,11 @@ export class UIStepperItem extends UIElement {
92
284
  // values prevent the `active && completed` footgun the prior multi-
93
285
  // Boolean shape allowed.
94
286
  status: { type: String, default: 'idle', reflect: true },
287
+ // S2's phase-gating mechanism (gh#711 S1 review) — a disabled step
288
+ // is skipped in [interactive] roving tabindex/arrow order, is
289
+ // unclickable, and carries aria-disabled. Independent of `status`:
290
+ // a completed or upcoming step can be disabled for a different reason.
291
+ disabled: { type: Boolean, default: false, reflect: true },
95
292
  };
96
293
 
97
294
  static template = () => null;
@@ -253,6 +253,36 @@
253
253
  :scope [slot]:empty {
254
254
  display: none;
255
255
  }
256
+
257
+ /* Interactive (click-to-navigate) — host opts in via [interactive]
258
+ on the parent; absent by default (Rule: bare stepper is a
259
+ read-only progress display, not a nav control). */
260
+ stepper-ui[interactive] > :scope {
261
+ cursor: pointer;
262
+ outline: none;
263
+ }
264
+
265
+ stepper-ui[interactive] > :scope:focus-visible::after {
266
+ outline: 2px solid var(--md-sys-color-primary);
267
+ outline-offset: 2px;
268
+ }
269
+
270
+ /* Disabled (S2's phase-gating mechanism) — never clickable, never
271
+ the roving-tabindex stop; visually recedes regardless of status. */
272
+ stepper-ui[interactive] > :scope[disabled] {
273
+ cursor: not-allowed;
274
+ }
275
+
276
+ :scope[disabled]::after,
277
+ :scope[disabled] [slot="icon"] {
278
+ opacity: 0.5;
279
+ }
280
+
281
+ :scope[disabled] [slot="label"],
282
+ :scope[disabled] [slot="description"] {
283
+ color: var(--stepper-item-pending-fg);
284
+ opacity: 0.5;
285
+ }
256
286
  }
257
287
 
258
288
  /* ── Vertical orientation rules — must reach across stepper-ui →
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `<stepper-ui>` — Wizard / process stepper — parent step index drives numbered children's complete/current/upcoming states automatically. Renders horizontally as a labeled step bar with connectors. Use for multi-step forms, onboarding, or pipelines; for read-only event history use <timeline-ui> instead.
2
+ * `<stepper-ui>` — Wizard / process stepper — parent step index drives numbered children's complete/current/upcoming states automatically. Renders horizontally as a labeled step bar with connectors. Use for multi-step forms, onboarding, or pipelines; for read-only event history use <timeline-ui> instead. Add [interactive] to opt into click-to-navigate + roving- tabindex keyboard (arrow keys / Home / End) — the host still owns whether navigation is permitted: listen for the cancelable [step-request] event and call preventDefault() to veto a step before it lands, or set [disabled] on a stepper-item-ui to phase-gate it out of reach entirely.
3
3
  *
4
4
  * @see https://ui-kit.exe.xyz/site/components/stepper
5
5
  *
@@ -12,16 +12,43 @@
12
12
 
13
13
  import { UIElement } from '../../core/element.js';
14
14
 
15
+ export interface StepperChangeEventDetail {
16
+ /** New current step index. */
17
+ step: number;
18
+ }
19
+
20
+ export type StepperChangeEvent = CustomEvent<StepperChangeEventDetail>;
21
+ export interface StepperStepRequestEventDetail {
22
+ /** The current step index before this navigation. */
23
+ previous: number;
24
+ /** The step index the navigation would land on if not vetoed. */
25
+ step: number;
26
+ }
27
+
28
+ export type StepperStepRequestEvent = CustomEvent<StepperStepRequestEventDetail>;
29
+
15
30
  export class UIStepper extends UIElement {
31
+ /** Opt into click-to-navigate + roving-tabindex keyboard nav (arrow keys / Home / End / Enter / Space). Absent by default — a bare stepper is a read-only progress display, not a nav control (gh#711 S1). */
32
+ interactive: boolean;
16
33
  /** horizontal | vertical */
17
34
  orientation: string;
18
35
  /** Current step index (0-based). */
19
36
  step: number;
37
+
38
+ addEventListener(type: 'change', listener: (ev: StepperChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
39
+ addEventListener(type: 'step-request', listener: (ev: StepperStepRequestEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
40
+ addEventListener<K extends keyof HTMLElementEventMap>(
41
+ type: K,
42
+ listener: (this: UIStepper, ev: HTMLElementEventMap[K]) => unknown,
43
+ options?: boolean | AddEventListenerOptions,
44
+ ): void;
20
45
  }
21
46
 
22
47
  export class UIStepperItem extends UIElement {
23
48
  /** Secondary description line under the label. */
24
49
  description: string;
50
+ /** S2's phase-gating mechanism (gh#711 S1 review). Skipped in the parent's [interactive] roving tabindex/arrow order, unclickable, carries aria-disabled. Independent of [status] — a completed or upcoming step can still be disabled for a different reason (e.g. the visit phase hasn't unlocked it yet). */
51
+ disabled: boolean;
25
52
  /** Optional leading Phosphor icon name; overrides the auto-stamped numeric badge. */
26
53
  icon: string;
27
54
  /** Canonical stage-progress state. Mutually exclusive (prevents the
@@ -12,7 +12,12 @@ description: >-
12
12
  complete/current/upcoming states automatically. Renders horizontally as
13
13
  a labeled step bar with connectors. Use for multi-step forms,
14
14
  onboarding, or pipelines; for read-only event history use <timeline-ui>
15
- instead.
15
+ instead. Add [interactive] to opt into click-to-navigate + roving-
16
+ tabindex keyboard (arrow keys / Home / End) — the host still owns
17
+ whether navigation is permitted: listen for the cancelable
18
+ [step-request] event and call preventDefault() to veto a step before
19
+ it lands, or set [disabled] on a stepper-item-ui to phase-gate it
20
+ out of reach entirely.
16
21
  # Per ADR-0027 — primitives that programmatically create other primitives
17
22
  # do NOT auto-import them. Consumer (or demo shell) must explicitly import.
18
23
  composes:
@@ -26,7 +31,39 @@ props:
26
31
  description: Current step index (0-based).
27
32
  type: number
28
33
  default: 0
29
- events: {}
34
+ interactive:
35
+ description: >-
36
+ Opt into click-to-navigate + roving-tabindex keyboard nav
37
+ (arrow keys / Home / End / Enter / Space). Absent by default —
38
+ a bare stepper is a read-only progress display, not a nav
39
+ control (gh#711 S1).
40
+ type: boolean
41
+ default: false
42
+ events:
43
+ step-request:
44
+ description: >-
45
+ Cancelable — fires BEFORE [step] mutates, on a click or keyboard
46
+ navigation. call preventDefault() to veto: the step does not
47
+ change, [change] does not fire, and focus/roving-tabindex stay
48
+ exactly where they were. A host that never listens for this
49
+ event keeps the pre-veto uncontrolled behavior unchanged
50
+ (gh#711 S1 review).
51
+ detail:
52
+ step:
53
+ type: number
54
+ description: The step index the navigation would land on if not vetoed.
55
+ previous:
56
+ type: number
57
+ description: The current step index before this navigation.
58
+ change:
59
+ description: >-
60
+ Fired after a click or keyboard navigation changes [step] (i.e.
61
+ only when the preceding [step-request] wasn't vetoed).
62
+ Only fires when [interactive] is set.
63
+ detail:
64
+ step:
65
+ type: number
66
+ description: New current step index.
30
67
  slots:
31
68
  default:
32
69
  description: "Default slot — primary child content."
@@ -43,6 +80,10 @@ a2ui:
43
80
  reason: 'Surface examples.'
44
81
  - rule: 'For read-only event history use <timeline-ui>; stepper requires forward progress semantics.'
45
82
  reason: 'Decision rule vs timeline.'
83
+ - rule: 'Add [interactive] only when the host permits jumping to a step out of order; leave absent for a linear wizard the host advances programmatically.'
84
+ reason: 'Click-to-navigate is host-controlled, not automatic (gh#711 S1).'
85
+ - rule: 'Listen for [step-request] and call preventDefault() to veto a navigation conditionally (e.g. block a phase the app has not unlocked); set [disabled] on a stepper-item-ui to phase-gate it out of reach unconditionally.'
86
+ reason: 'Two host-controlled gates at different granularities (gh#711 S1 review).'
46
87
  anti_patterns: []
47
88
  examples:
48
89
  - name: basic
@@ -75,6 +116,37 @@ examples:
75
116
  "text": "Review"
76
117
  }
77
118
  ]
119
+ - name: interactive
120
+ description: Click/keyboard-navigable stepper — host allows jumping between steps.
121
+ a2ui: >-
122
+ [
123
+ {
124
+ "id": "root",
125
+ "component": "Stepper",
126
+ "step": 1,
127
+ "interactive": true,
128
+ "children": [
129
+ "s1",
130
+ "s2",
131
+ "s3"
132
+ ]
133
+ },
134
+ {
135
+ "id": "s1",
136
+ "component": "StepperItem",
137
+ "text": "Account"
138
+ },
139
+ {
140
+ "id": "s2",
141
+ "component": "StepperItem",
142
+ "text": "Details"
143
+ },
144
+ {
145
+ "id": "s3",
146
+ "component": "StepperItem",
147
+ "text": "Review"
148
+ }
149
+ ]
78
150
  keywords:
79
151
  - stepper
80
152
  - wizard
@@ -82,6 +154,8 @@ keywords:
82
154
  - process
83
155
  - onboarding
84
156
  - multi-step
157
+ - interactive
158
+ - click-to-navigate
85
159
  synonyms:
86
160
  timeline-ui mode=steps: stepper-ui
87
161
  related:
@@ -27,12 +27,12 @@ export class UIStream extends UIElement {
27
27
  /** Read-only reflected state, true while stream is active */
28
28
  streaming: boolean;
29
29
 
30
+ addEventListener(type: 'stream-end', listener: (ev: StreamEndEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
31
+ addEventListener(type: 'stream-error', listener: (ev: StreamErrorEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
32
+ addEventListener(type: 'stream-start', listener: (ev: StreamStartEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
30
33
  addEventListener<K extends keyof HTMLElementEventMap>(
31
34
  type: K,
32
35
  listener: (this: UIStream, ev: HTMLElementEventMap[K]) => unknown,
33
36
  options?: boolean | AddEventListenerOptions,
34
37
  ): void;
35
- addEventListener(type: 'stream-end', listener: (ev: StreamEndEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
36
- addEventListener(type: 'stream-error', listener: (ev: StreamErrorEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
37
- addEventListener(type: 'stream-start', listener: (ev: StreamStartEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
38
38
  }
@@ -166,7 +166,7 @@ export class UISwatch extends UIElement {
166
166
  copyValue: { type: String, default: '' }, // not reflected — long color strings as attrs read poorly
167
167
  selectable: { type: Boolean, default: false, reflect: true },
168
168
  selected: { type: Boolean, default: false, reflect: true },
169
- autoContrast: { type: Boolean, default: false, reflect: true },
169
+ autoContrast: { type: Boolean, default: false, reflect: true, attribute: 'auto-contrast' },
170
170
  // §253 (v0.5.12, FB-23 §1): label position relative to the tile.
171
171
  // `below` (default) = current shape="block" stacked layout.
172
172
  // `overlay` = label sits ON the tile via absolute positioning;
@@ -73,10 +73,10 @@ pattern; blocked 3 cycles on this design call). */
73
73
  /** Size preset — sm / md / lg. Drives the swatch's intrinsic dimensions; lg is reserved for the token-scale demo (40 px tall). */
74
74
  size: 'sm' | 'md' | 'lg';
75
75
 
76
+ addEventListener(type: 'select', listener: (ev: SwatchSelectEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
76
77
  addEventListener<K extends keyof HTMLElementEventMap>(
77
78
  type: K,
78
79
  listener: (this: UISwatch, ev: HTMLElementEventMap[K]) => unknown,
79
80
  options?: boolean | AddEventListenerOptions,
80
81
  ): void;
81
- addEventListener(type: 'select', listener: (ev: SwatchSelectEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
82
82
  }
@@ -52,12 +52,12 @@ export class UISwiper extends UIElement {
52
52
  /** Scroll-snap alignment — start, center, end, or none. */
53
53
  snap: 'start' | 'center' | 'end' | 'none';
54
54
 
55
+ addEventListener(type: 'autoplay-pause', listener: (ev: SwiperAutoplayPauseEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
56
+ addEventListener(type: 'autoplay-resume', listener: (ev: SwiperAutoplayResumeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
57
+ addEventListener(type: 'change', listener: (ev: SwiperChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
55
58
  addEventListener<K extends keyof HTMLElementEventMap>(
56
59
  type: K,
57
60
  listener: (this: UISwiper, ev: HTMLElementEventMap[K]) => unknown,
58
61
  options?: boolean | AddEventListenerOptions,
59
62
  ): void;
60
- addEventListener(type: 'autoplay-pause', listener: (ev: SwiperAutoplayPauseEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
61
- addEventListener(type: 'autoplay-resume', listener: (ev: SwiperAutoplayResumeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
62
- addEventListener(type: 'change', listener: (ev: SwiperChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
63
63
  }
@@ -40,7 +40,7 @@
40
40
  "default": false
41
41
  },
42
42
  "frameless": {
43
- "description": "Drop the table's outer 1px perimeter and nothing else (gh#511) — row dividers are untouched (the perimeter is split onto its own --table-perimeter token, defaulting to --table-border). For card-composed tables: the card owns the frame, so the table's square perimeter otherwise renders a doubled edge (flush bleed) or a stray inner box (with content above). Standalone tables keep their chrome by default. Theming path without the attribute: set --table-perimeter to transparent per instance.",
43
+ "description": "Drop the table's outer 1px perimeter and nothing else (gh#511) — row dividers are untouched (the perimeter is split onto its own --table-perimeter token, defaulting to --table-border). For card-composed tables: the card owns the frame, so the table's square perimeter otherwise renders a doubled edge (flush bleed) or a stray inner box (with content above). Standalone tables keep their chrome by default. Theming path without the attribute: set --table-perimeter to transparent per instance. Note: `<card-ui><section bleed>` already drops a direct-child table-ui's chrome automatically (card.css, gh#796) — `frameless`/`raw` on the instance still applies and is honored, but is no longer required just to avoid the doubled-edge look in that composition.",
44
44
  "type": "boolean",
45
45
  "default": false
46
46
  },
@@ -55,7 +55,7 @@
55
55
  "default": 0
56
56
  },
57
57
  "raw": {
58
- "description": "Visual-only passthrough — applies `<table-ui>`'s chrome reset (background / border / border-radius all transparent) AND short-circuits the data lifecycle entirely. The consumer owns the body shape: no header injection, no row reconciliation from `.data`, no empty-state / loading overlays, no aggregation or pagination footers. Use raw when embedding `<table-ui>` inside surfaces that supply their own chrome (e.g. `<card-ui><section bleed>`), or when wrapping a consumer-authored native `<table>` for design-token styling without the framework's data semantics. Pre-v0.6.33 (FB-53 §2) `raw` was visual-only and the data lifecycle still ran — wrapping a `.data`-unset native table produced a phantom \"No data\" overlay. v0.6.33+ matches the documented contract.",
58
+ "description": "Visual-only passthrough — applies `<table-ui>`'s chrome reset (background / border / border-radius all transparent) AND short-circuits the data lifecycle entirely. The consumer owns the body shape: no header injection, no row reconciliation from `.data`, no empty-state / loading overlays, no aggregation or pagination footers. Use raw when embedding `<table-ui>` inside surfaces that supply their own chrome (e.g. `<card-ui><section bleed>`), or when wrapping a consumer-authored native `<table>` for design-token styling without the framework's data semantics. Note: a `<table-ui>` placed directly inside `<card-ui><section bleed>` drops the table's visual chrome automatically (card.css, gh#796) even without `raw` — `raw` is still required to also short-circuit the data lifecycle. Pre-v0.6.33 (FB-53 §2) `raw` was visual-only and the data lifecycle still ran — wrapping a `.data`-unset native table produced a phantom \"No data\" overlay. v0.6.33+ matches the documented contract.",
59
59
  "type": "boolean",
60
60
  "default": false
61
61
  },
@@ -94,13 +94,13 @@ export class UITable extends UIElement {
94
94
  density: 'compact' | 'standard' | 'comfortable';
95
95
  /** Enable row expansion */
96
96
  expandable: boolean;
97
- /** Drop the table's outer 1px perimeter and nothing else (gh#511) — row dividers are untouched (the perimeter is split onto its own --table-perimeter token, defaulting to --table-border). For card-composed tables: the card owns the frame, so the table's square perimeter otherwise renders a doubled edge (flush bleed) or a stray inner box (with content above). Standalone tables keep their chrome by default. Theming path without the attribute: set --table-perimeter to transparent per instance. */
97
+ /** Drop the table's outer 1px perimeter and nothing else (gh#511) — row dividers are untouched (the perimeter is split onto its own --table-perimeter token, defaulting to --table-border). For card-composed tables: the card owns the frame, so the table's square perimeter otherwise renders a doubled edge (flush bleed) or a stray inner box (with content above). Standalone tables keep their chrome by default. Theming path without the attribute: set --table-perimeter to transparent per instance. Note: `<card-ui><section bleed>` already drops a direct-child table-ui's chrome automatically (card.css, gh#796) — `frameless`/`raw` on the instance still applies and is honored, but is no longer required just to avoid the doubled-edge look in that composition. */
98
98
  frameless: boolean;
99
99
  /** Renders N ghost skeleton rows in place of the body data (count derived from `paginate` if set, else 5). Header + columns stay intact so the table layout is preserved while data fetches. Sets aria-busy="true" on the host. Data updates are deferred until loading is set back to false. */
100
100
  loading: boolean;
101
101
  /** Rows per page. 0 = show all rows without pagination. When > 0, renders a pagination bar below the table. */
102
102
  paginate: number;
103
- /** Visual-only passthrough — applies `<table-ui>`'s chrome reset (background / border / border-radius all transparent) AND short-circuits the data lifecycle entirely. The consumer owns the body shape: no header injection, no row reconciliation from `.data`, no empty-state / loading overlays, no aggregation or pagination footers. Use raw when embedding `<table-ui>` inside surfaces that supply their own chrome (e.g. `<card-ui><section bleed>`), or when wrapping a consumer-authored native `<table>` for design-token styling without the framework's data semantics. Pre-v0.6.33 (FB-53 §2) `raw` was visual-only and the data lifecycle still ran — wrapping a `.data`-unset native table produced a phantom "No data" overlay. v0.6.33+ matches the documented contract. */
103
+ /** Visual-only passthrough — applies `<table-ui>`'s chrome reset (background / border / border-radius all transparent) AND short-circuits the data lifecycle entirely. The consumer owns the body shape: no header injection, no row reconciliation from `.data`, no empty-state / loading overlays, no aggregation or pagination footers. Use raw when embedding `<table-ui>` inside surfaces that supply their own chrome (e.g. `<card-ui><section bleed>`), or when wrapping a consumer-authored native `<table>` for design-token styling without the framework's data semantics. Note: a `<table-ui>` placed directly inside `<card-ui><section bleed>` drops the table's visual chrome automatically (card.css, gh#796) even without `raw` — `raw` is still required to also short-circuit the data lifecycle. Pre-v0.6.33 (FB-53 §2) `raw` was visual-only and the data lifecycle still ran — wrapping a `.data`-unset native table produced a phantom "No data" overlay. v0.6.33+ matches the documented contract. */
104
104
  raw: boolean;
105
105
  /** Global search/filter string. Filters visible rows across all columns using case-insensitive substring matching. Resets to page 1 on change. */
106
106
  search: string;
@@ -115,11 +115,6 @@ export class UITable extends UIElement {
115
115
  /** Allow body-cell content to wrap onto multiple lines. Default is single-line with ellipsis truncation (matches the row convention shared by <select-ui> + <nav-item-ui>); long unbreakable strings clip gracefully rather than rewrapping the row. With [wrap], row height auto-grows to fit wrapped content. For surgical opt-in on a single column, set [data-wrap] on the cell / col-def instead. */
116
116
  wrap: boolean;
117
117
 
118
- addEventListener<K extends keyof HTMLElementEventMap>(
119
- type: K,
120
- listener: (this: UITable, ev: HTMLElementEventMap[K]) => unknown,
121
- options?: boolean | AddEventListenerOptions,
122
- ): void;
123
118
  addEventListener(type: 'cell-click', listener: (ev: TableCellClickEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
124
119
  addEventListener(type: 'filter-change', listener: (ev: TableFilterChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
125
120
  addEventListener(type: 'page', listener: (ev: TablePageEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
@@ -129,4 +124,9 @@ export class UITable extends UIElement {
129
124
  addEventListener(type: 'row-expand', listener: (ev: TableRowExpandEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
130
125
  addEventListener(type: 'select', listener: (ev: TableSelectEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
131
126
  addEventListener(type: 'sort', listener: (ev: TableSortEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
127
+ addEventListener<K extends keyof HTMLElementEventMap>(
128
+ type: K,
129
+ listener: (this: UITable, ev: HTMLElementEventMap[K]) => unknown,
130
+ options?: boolean | AddEventListenerOptions,
131
+ ): void;
132
132
  }
@@ -74,6 +74,10 @@ props:
74
74
  surfaces that supply their own chrome (e.g. `<card-ui><section
75
75
  bleed>`), or when wrapping a consumer-authored native `<table>`
76
76
  for design-token styling without the framework's data semantics.
77
+ Note: a `<table-ui>` placed directly inside `<card-ui><section bleed>`
78
+ drops the table's visual chrome automatically (card.css, gh#796) even
79
+ without `raw` — `raw` is still required to also short-circuit the
80
+ data lifecycle.
77
81
  Pre-v0.6.33 (FB-53 §2) `raw` was visual-only and the data lifecycle
78
82
  still ran — wrapping a `.data`-unset native table produced a
79
83
  phantom "No data" overlay. v0.6.33+ matches the documented contract.
@@ -107,7 +111,11 @@ props:
107
111
  square perimeter otherwise renders a doubled edge (flush bleed) or
108
112
  a stray inner box (with content above). Standalone tables keep
109
113
  their chrome by default. Theming path without the attribute:
110
- set --table-perimeter to transparent per instance.
114
+ set --table-perimeter to transparent per instance. Note:
115
+ `<card-ui><section bleed>` already drops a direct-child table-ui's
116
+ chrome automatically (card.css, gh#796) — `frameless`/`raw` on the
117
+ instance still applies and is honored, but is no longer required
118
+ just to avoid the doubled-edge look in that composition.
111
119
  type: boolean
112
120
  default: false
113
121
  reflect: true
@@ -37,13 +37,13 @@ export class UITableToolbar extends UIElement {
37
37
  /** Toolbar visual variant. `default` renders bare on parent surface; `card` adds the same chrome as a card-ui header. */
38
38
  variant: 'default' | 'card';
39
39
 
40
+ addEventListener(type: 'columns-change', listener: (ev: TableToolbarColumnsChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
41
+ addEventListener(type: 'filter-change', listener: (ev: TableToolbarFilterChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
42
+ addEventListener(type: 'search', listener: (ev: TableToolbarSearchEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
43
+ addEventListener(type: 'sort-change', listener: (ev: TableToolbarSortChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
40
44
  addEventListener<K extends keyof HTMLElementEventMap>(
41
45
  type: K,
42
46
  listener: (this: UITableToolbar, ev: HTMLElementEventMap[K]) => unknown,
43
47
  options?: boolean | AddEventListenerOptions,
44
48
  ): void;
45
- addEventListener(type: 'columns-change', listener: (ev: TableToolbarColumnsChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
46
- addEventListener(type: 'filter-change', listener: (ev: TableToolbarFilterChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
47
- addEventListener(type: 'search', listener: (ev: TableToolbarSearchEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
48
- addEventListener(type: 'sort-change', listener: (ev: TableToolbarSortChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
49
49
  }
@@ -5,10 +5,12 @@
5
5
  --tabs-bg: transparent;
6
6
  --tabs-radius: var(--a-radius-md);
7
7
  --tabs-button-height: var(--a-size);
8
- --tabs-button-px: var(--a-space-2);
9
8
  --tabs-font-size: var(--a-ui-md);
10
9
  --tabs-font-family: var(--a-font-family-ui);
11
- --tabs-gap: var(--a-space-1);
10
+ /* Strip owns the button-to-button spacing — 4px base gap + one
11
+ padding unit (8px) folded in, since tab-button carries no
12
+ horizontal padding of its own (gh#705). */
13
+ --tabs-gap: var(--a-space-3);
12
14
  --tabs-indicator-color: var(--md-sys-color-primary);
13
15
  --tabs-indicator-height: 2px;
14
16
  --tabs-fg: var(--md-sys-color-neutral-on-surface-variant);
@@ -101,6 +103,13 @@
101
103
  height var(--tabs-duration) var(--tabs-easing-out);
102
104
  }
103
105
 
106
+ /* Vertical: the indicator lives on the inline-start edge (not the bottom),
107
+ so contents need clearance from the line — the horizontal strip-gap
108
+ spacing (gh#705) can't provide it on this axis. */
109
+ :scope[orientation="vertical"] [slot="tab-button"] {
110
+ padding-inline-start: var(--a-space-3);
111
+ }
112
+
104
113
  /* Vertical panel area takes remaining space */
105
114
  :scope[orientation="vertical"] > tab-ui {
106
115
  flex: 1;
@@ -114,7 +123,6 @@
114
123
  align-items: center;
115
124
  gap: var(--tabs-button-gap);
116
125
  height: var(--tabs-button-height);
117
- padding-inline: var(--tabs-button-px);
118
126
  border: none;
119
127
  background: none;
120
128
  color: var(--tabs-fg);