@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
@@ -0,0 +1,114 @@
1
+ # Edit this file; run `npm run build:components` to regenerate a2ui.json.
2
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
3
+ name: UIRadioGroup
4
+ tag: radio-group-ui
5
+ status: stable
6
+ component: RadioGroup
7
+ category: input
8
+ version: 1
9
+ description: >-
10
+ Groups a set of bare <radio-ui label="…"> children under one accessible
11
+ name — role="radiogroup" on the host plus aria-labelledby pointing at
12
+ its own [label]. Radio-ui already supports ancestor fieldset/[role=radiogroup]
13
+ grouping for exclusivity + arrow-key navigation (unchanged by this
14
+ wrapper); a plain layout container (Column) can carry neither the role
15
+ nor the label association, which left the canonical questionnaire shape
16
+ (radio.yaml's own example) with no programmatic question↔group link
17
+ (gh#729). Use this wherever a question/prompt introduces a set of
18
+ mutually-exclusive radio-ui options — it is the ONLY primitive that
19
+ carries the group's accessible name; radio-ui itself stays self-labeling
20
+ per-option.
21
+ props:
22
+ label:
23
+ description: >-
24
+ The question or prompt text — rendered above the options and wired
25
+ to the host via aria-labelledby. This is the group's accessible
26
+ name; without it the group has none.
27
+ required: true
28
+ type: string
29
+ default: ""
30
+ reflect: true
31
+ required:
32
+ description: Marks the group as requiring a selection for form validation (visual + aria-required only — the constraint itself lives on each radio-ui's own [required]).
33
+ type: boolean
34
+ default: false
35
+ reflect: true
36
+ events: {}
37
+ slots:
38
+ default:
39
+ description: "radio-ui children sharing one [name=] — exclusivity and arrow-key navigation are unchanged; they still resolve via closest('fieldset, [role=radiogroup]'), which now matches this host."
40
+ label:
41
+ description: Auto-minted label element (mounted by radio-group-ui from the [label] prop) — do not author this slot directly.
42
+ states:
43
+ - name: idle
44
+ description: Default, ready for interaction.
45
+ traits: []
46
+ tokens:
47
+ --radio-group-gap:
48
+ description: Gap between the label and the option stack, and between each option.
49
+ --radio-group-label-color:
50
+ description: Override the question/prompt label color.
51
+ --radio-group-label-font-size:
52
+ description: Override the question/prompt label font size.
53
+ --radio-group-label-font-weight:
54
+ description: Override the question/prompt label font weight.
55
+ a2ui:
56
+ rules:
57
+ - "The canonical radio-group shape is <radio-group-ui label=\"…\"> wrapping a set of bare <radio-ui label=\"…\" name=\"…\"> children — the wrapper supplies the group's accessible name (role=radiogroup + aria-labelledby); Column/Section cannot carry a role, so a bare Column of radios has no programmatic question↔group association."
58
+ - rule: "Do not put the question text as a sibling Text node before a Column of radios — put it in radio-group-ui's own [label] prop instead."
59
+ reason: "A sibling Text has no aria relationship to the radios; radio-group-ui's [label] is wired via aria-labelledby, which a sibling Text is not."
60
+ - rule: "radio-ui children keep their own [label] per-option; radio-group-ui's [label] is the shared question, not a per-option label."
61
+ reason: "Two different accessible names at two different levels — the group's name and each option's name — both need to reach an AT user."
62
+ anti_patterns:
63
+ - description: Wrapping radio-ui options in a bare Column/Section with the question as a sibling Text node instead of radio-group-ui's [label].
64
+ wrong: |
65
+ <column>
66
+ <text>How often do you exercise?</text>
67
+ <radio-ui label="Never" name="exercise"></radio-ui>
68
+ <radio-ui label="Sometimes" name="exercise"></radio-ui>
69
+ </column>
70
+ right: |
71
+ <radio-group-ui label="How often do you exercise?">
72
+ <radio-ui label="Never" name="exercise"></radio-ui>
73
+ <radio-ui label="Sometimes" name="exercise"></radio-ui>
74
+ </radio-group-ui>
75
+ rule: The question that introduces a radio-ui set belongs in radio-group-ui's [label], not a sibling Text node.
76
+ examples:
77
+ - name: questionnaire-item
78
+ description: "A single scored-questionnaire item: the question as the group's accessible label, three bare radio-ui options sharing one [name=]."
79
+ a2ui: >-
80
+ [
81
+ {
82
+ "id": "root",
83
+ "component": "RadioGroup",
84
+ "label": "Little interest or pleasure in doing things?",
85
+ "children": ["opt-0", "opt-1", "opt-2"]
86
+ },
87
+ { "id": "opt-0", "component": "Radio", "label": "Not at all", "name": "phq2-1", "value": "0" },
88
+ { "id": "opt-1", "component": "Radio", "label": "Several days", "name": "phq2-1", "value": "1" },
89
+ { "id": "opt-2", "component": "Radio", "label": "More than half the days", "name": "phq2-1", "value": "2" }
90
+ ]
91
+ keywords:
92
+ - radio
93
+ - radiogroup
94
+ - fieldset
95
+ - legend
96
+ - questionnaire
97
+ - survey
98
+ - poll
99
+ - accessibility
100
+ - aria-labelledby
101
+ synonyms:
102
+ questionnaire:
103
+ - radio-group
104
+ - form
105
+ - survey
106
+ survey:
107
+ - radio-group
108
+ - questionnaire
109
+ poll:
110
+ - radio-group
111
+ - questionnaire
112
+ related:
113
+ - radio
114
+ - field
@@ -13,6 +13,11 @@
13
13
  }
14
14
  ],
15
15
  "properties": {
16
+ "allowEmpty": {
17
+ "description": "Suppresses the connect-time self-select of the first enabled segment when value is falsy. Without it, an empty value silently selects the first segment; with it, a falsy value renders every segment unselected until a real value arrives — for path-bound consumers whose bound value can be a genuine empty state.",
18
+ "type": "boolean",
19
+ "default": false
20
+ },
16
21
  "component": {
17
22
  "const": "Segmented"
18
23
  },
@@ -29,6 +29,14 @@ export class UISegmented extends UIFormElement {
29
29
  static properties = {
30
30
  ...UIFormElement.properties,
31
31
  value: { type: String, default: '', reflect: true },
32
+ // gh#693: absent (default false) preserves the original self-select
33
+ // behavior below. A path-bound consumer whose bound value can be a
34
+ // real empty state (e.g. "no disposition recorded yet") sets this to
35
+ // suppress the connect-time self-select, so a falsy `value` renders
36
+ // every segment unselected instead of silently picking one — a
37
+ // display-truth defect where the UI claimed a state the data never
38
+ // recorded.
39
+ allowEmpty: { type: Boolean, default: false, reflect: true, attribute: 'allow-empty' },
32
40
  };
33
41
 
34
42
  static template = () => null;
@@ -55,8 +63,10 @@ export class UISegmented extends UIFormElement {
55
63
  this.addEventListener('keydown', this.#handleKeydown);
56
64
  }
57
65
 
58
- // Default to first segment if no value
59
- if (!this.value) {
66
+ // Default to first segment if no value — unless allow-empty opts out
67
+ // (a falsy path-bound value must render as truly empty, not silently
68
+ // pick one, gh#693).
69
+ if (!this.value && !this.allowEmpty) {
60
70
  const first = this.querySelector('segment-ui:not([disabled])');
61
71
  if (first) this.value = first.value || first.getAttribute('value') || '';
62
72
  }
@@ -128,16 +138,24 @@ export class UISegmented extends UIFormElement {
128
138
  if (!segs.length) return;
129
139
 
130
140
  // Sync selection state
141
+ let anyActive = false;
131
142
  for (const seg of segs) {
132
143
  const segVal = seg.value || seg.getAttribute('value') || '';
133
144
  const isActive = segVal === this.value;
134
145
  if (isActive) {
135
146
  seg.setAttribute('selected', '');
147
+ anyActive = true;
136
148
  } else {
137
149
  seg.removeAttribute('selected');
138
150
  }
139
151
  seg.setAttribute('tabindex', isActive ? '0' : '-1');
140
152
  }
153
+ // allow-empty's "nothing selected" state must still be keyboard
154
+ // reachable — mirrors list-ui's roving-tabindex fallback (list.class.js).
155
+ if (!anyActive) {
156
+ const firstEnabled = segs.find((s) => !s.disabled);
157
+ firstEnabled?.setAttribute('tabindex', '0');
158
+ }
141
159
 
142
160
  if (this.label) this.setAttribute('aria-label', this.label);
143
161
 
@@ -11,6 +11,11 @@ props:
11
11
  description: Value of the currently selected segment.
12
12
  type: string
13
13
  default: ''
14
+ allowEmpty:
15
+ description: Suppresses the connect-time self-select of the first enabled segment when value is falsy. Without it, an empty value silently selects the first segment; with it, a falsy value renders every segment unselected until a real value arrives — for path-bound consumers whose bound value can be a genuine empty state.
16
+ type: boolean
17
+ default: false
18
+ attribute: allow-empty
14
19
  events:
15
20
  change:
16
21
  description: Fired when the selected segment changes. detail contains { value }.
@@ -202,19 +202,23 @@
202
202
  white-space: nowrap;
203
203
  }
204
204
 
205
- /* Trend arrows via ::before */
206
- :scope[trend="up"] [slot="change"]::before {
207
- content: "\25B2";
208
- color: var(--stat-up-fg);
205
+ /* Trend arrow — Phosphor icon-ui, colored via currentColor from the
206
+ [trend] color rules below (issue 805; was a CSS ::before unicode
207
+ triangle). Sized relative to the change text so it scales with
208
+ --stat-change-size instead of a fixed px value. No slot="icon"
209
+ attribute — that value is reserved for stat's own [slot="icon"]
210
+ region; an ancestor's `[slot="icon"]` descendant rule would otherwise
211
+ leak margin/positioning onto this nested icon-ui (issue 809, PR 810
212
+ shape). Spacing from the text comes from [slot="change"]'s own
213
+ `gap`, not margin, for the same reason. */
214
+ [slot="change"] icon-ui[data-trend-icon] {
215
+ --a-icon-size: 1em;
209
216
  }
217
+
210
218
  :scope[trend="up"] [slot="change"] {
211
219
  color: var(--stat-up-fg);
212
220
  }
213
221
 
214
- :scope[trend="down"] [slot="change"]::before {
215
- content: "\25BC";
216
- color: var(--stat-down-fg);
217
- }
218
222
  :scope[trend="down"] [slot="change"] {
219
223
  color: var(--stat-down-fg);
220
224
  }
@@ -32,9 +32,15 @@ class UIStat extends UIElement {
32
32
 
33
33
  static template = () => null;
34
34
 
35
+ // Trend arrows the class stamps directly (issue 805) — consumers calling
36
+ // installIconLoaders() manually need these two names present.
37
+ static requiredIcons = ['arrow-up', 'arrow-down'];
38
+
35
39
  #valueEl = null;
36
40
  #labelEl = null;
37
41
  #changeEl = null;
42
+ #changeIconEl = null;
43
+ #changeTextEl = null;
38
44
  #iconEl = null;
39
45
 
40
46
  connected() {
@@ -59,6 +65,28 @@ class UIStat extends UIElement {
59
65
  this.appendChild(this.#changeEl);
60
66
  }
61
67
 
68
+ // Trend arrow (issue 805): a real icon-ui child instead of a CSS ::before
69
+ // unicode triangle, so the delta indicator rides the Phosphor icon
70
+ // system (contrast, weight, RTL) like every other icon on the page.
71
+ // No `slot="icon"` attribute on this child — that value is reserved for
72
+ // stat's own outer `[slot="icon"]` region, and an ancestor's `[slot="icon"]`
73
+ // descendant selector would otherwise leak margin/positioning onto it
74
+ // (issue 809, PR 810 shape). `[data-trend-icon]` is scoped to stat's own CSS.
75
+ this.#changeIconEl = this.#changeEl.querySelector(':scope > icon-ui[data-trend-icon]');
76
+ if (!this.#changeIconEl) {
77
+ this.#changeIconEl = document.createElement('icon-ui');
78
+ this.#changeIconEl.setAttribute('data-trend-icon', '');
79
+ this.#changeIconEl.hidden = true;
80
+ this.#changeEl.appendChild(this.#changeIconEl);
81
+ }
82
+
83
+ this.#changeTextEl = this.#changeEl.querySelector(':scope > [data-trend-text]');
84
+ if (!this.#changeTextEl) {
85
+ this.#changeTextEl = document.createElement('span');
86
+ this.#changeTextEl.setAttribute('data-trend-text', '');
87
+ this.#changeEl.appendChild(this.#changeTextEl);
88
+ }
89
+
62
90
  this.#iconEl = this.querySelector(':scope > [slot="icon"]');
63
91
  if (!this.#iconEl) {
64
92
  this.#iconEl = document.createElement('icon-ui');
@@ -83,6 +111,9 @@ class UIStat extends UIElement {
83
111
  // value+change visual mass without being so wide as to look like text.
84
112
  this.#valueEl.textContent = '';
85
113
  this.#valueEl.innerHTML = '<skeleton-ui width="60%" height="2em" radius="sm"></skeleton-ui>';
114
+ // Skeleton replaces the trend icon + text children wholesale; the
115
+ // non-loading branch below detects the detached refs (parentNode
116
+ // check) and re-inserts them once real data arrives.
86
117
  this.#changeEl.textContent = '';
87
118
  this.#changeEl.innerHTML = '<skeleton-ui width="40%" height="1em" radius="sm"></skeleton-ui>';
88
119
  this.#changeEl.hidden = false;
@@ -103,7 +134,39 @@ class UIStat extends UIElement {
103
134
 
104
135
  // Change badge
105
136
  if (this.change) {
106
- this.#changeEl.textContent = this.change;
137
+ // Contract: the generated icon+text pair owns [slot="change"]'s
138
+ // content — same "consumer content is overwritten" rule #valueEl /
139
+ // #labelEl already apply (their textContent is replaced every render
140
+ // regardless of what markup a consumer authored there). #changeEl
141
+ // additionally needs a specific two-node SHAPE (icon then text), not
142
+ // just overwritten text, so any deviation from that exact shape is
143
+ // normalized before writing: a consumer-authored `<stat-ui
144
+ // change="…"><span slot="change">stale</span></stat-ui>` gets its
145
+ // stale content cleared and the generated pair installed; loading's
146
+ // skeleton wipe (above) detaches the pair the same way and is
147
+ // recovered by the same branch; a disconnect/reconnect cycle that
148
+ // leaves an orphaned skeleton behind is also caught here (fresh
149
+ // #changeIconEl/#changeTextEl instances from the new connected()
150
+ // never match old stray children).
151
+ const hasOnlyTrendChildren =
152
+ this.#changeEl.childNodes.length === 2 &&
153
+ this.#changeEl.firstChild === this.#changeIconEl &&
154
+ this.#changeEl.lastChild === this.#changeTextEl;
155
+ if (!hasOnlyTrendChildren) {
156
+ this.#changeEl.textContent = '';
157
+ this.#changeEl.appendChild(this.#changeIconEl);
158
+ this.#changeEl.appendChild(this.#changeTextEl);
159
+ }
160
+ this.#changeTextEl.textContent = this.change;
161
+ if (this.trend === 'up') {
162
+ this.#changeIconEl.setAttribute('name', 'arrow-up');
163
+ this.#changeIconEl.hidden = false;
164
+ } else if (this.trend === 'down') {
165
+ this.#changeIconEl.setAttribute('name', 'arrow-down');
166
+ this.#changeIconEl.hidden = false;
167
+ } else {
168
+ this.#changeIconEl.hidden = true;
169
+ }
107
170
  this.#changeEl.hidden = false;
108
171
  } else {
109
172
  this.#changeEl.hidden = true;
@@ -122,6 +185,8 @@ class UIStat extends UIElement {
122
185
  this.#valueEl = null;
123
186
  this.#labelEl = null;
124
187
  this.#changeEl = null;
188
+ this.#changeIconEl = null;
189
+ this.#changeTextEl = null;
125
190
  this.#iconEl = null;
126
191
  }
127
192
  }
@@ -112,6 +112,9 @@ tokens:
112
112
  description: Font size for the primary value
113
113
  --stat-value-weight:
114
114
  description: Font weight for the primary value
115
+ requiredIcons:
116
+ - arrow-up
117
+ - arrow-down
115
118
  a2ui:
116
119
  rules:
117
120
  - rule: 'Use for prominent metric/KPI displays inside dashboard cards. Value + label + optional delta indicator.'
@@ -21,6 +21,11 @@
21
21
  "component": {
22
22
  "const": "StepperItem"
23
23
  },
24
+ "disabled": {
25
+ "description": "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).",
26
+ "type": "boolean",
27
+ "default": false
28
+ },
24
29
  "icon": {
25
30
  "description": "Optional leading Phosphor icon name; overrides the auto-stamped numeric badge.",
26
31
  "type": "string",
@@ -39,6 +39,15 @@ props:
39
39
  - active
40
40
  - completed
41
41
  - error
42
+ disabled:
43
+ description: >-
44
+ S2's phase-gating mechanism (gh#711 S1 review). Skipped in the
45
+ parent's [interactive] roving tabindex/arrow order, unclickable,
46
+ carries aria-disabled. Independent of [status] — a completed or
47
+ upcoming step can still be disabled for a different reason (e.g.
48
+ the visit phase hasn't unlocked it yet).
49
+ type: boolean
50
+ default: false
42
51
  slots:
43
52
  label:
44
53
  description: Custom label content; falls through to `[text]` prop if not slotted.
@@ -73,3 +82,5 @@ a2ui:
73
82
  reason: 'Parent owns step progression.'
74
83
  - rule: 'For chronological events (no completion semantics) use <timeline-item-ui> instead.'
75
84
  reason: 'Stepper is process; timeline is history.'
85
+ - rule: 'Set [disabled] to phase-gate a step out of click/keyboard reach without changing [status].'
86
+ reason: 'gh#711 S1 review — S2 uses this to lock steps the current visit phase hasn''t unlocked.'
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://adiaui.dev/a2ui/v0_9/components/Stepper.json",
4
4
  "title": "Stepper",
5
- "description": "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.",
5
+ "description": "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.",
6
6
  "type": "object",
7
7
  "allOf": [
8
8
  {
@@ -16,6 +16,11 @@
16
16
  "component": {
17
17
  "const": "Stepper"
18
18
  },
19
+ "interactive": {
20
+ "description": "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).",
21
+ "type": "boolean",
22
+ "default": false
23
+ },
19
24
  "orientation": {
20
25
  "description": "horizontal | vertical",
21
26
  "type": "string",
@@ -37,12 +42,40 @@
37
42
  "composes": [
38
43
  "icon-ui"
39
44
  ],
40
- "events": {},
45
+ "events": {
46
+ "change": {
47
+ "description": "Fired after a click or keyboard navigation changes [step] (i.e. only when the preceding [step-request] wasn't vetoed). Only fires when [interactive] is set.",
48
+ "detail": {
49
+ "step": {
50
+ "description": "New current step index.",
51
+ "type": "number"
52
+ }
53
+ }
54
+ },
55
+ "step-request": {
56
+ "description": "Cancelable — fires BEFORE [step] mutates, on a click or keyboard navigation. call preventDefault() to veto: the step does not change, [change] does not fire, and focus/roving-tabindex stay exactly where they were. A host that never listens for this event keeps the pre-veto uncontrolled behavior unchanged (gh#711 S1 review).",
57
+ "detail": {
58
+ "previous": {
59
+ "description": "The current step index before this navigation.",
60
+ "type": "number"
61
+ },
62
+ "step": {
63
+ "description": "The step index the navigation would land on if not vetoed.",
64
+ "type": "number"
65
+ }
66
+ }
67
+ }
68
+ },
41
69
  "examples": [
42
70
  {
43
71
  "description": "Three-step wizard",
44
72
  "a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"Stepper\",\n \"step\": 1,\n \"children\": [\n \"s1\",\n \"s2\",\n \"s3\"\n ]\n },\n {\n \"id\": \"s1\",\n \"component\": \"StepperItem\",\n \"text\": \"Account\"\n },\n {\n \"id\": \"s2\",\n \"component\": \"StepperItem\",\n \"text\": \"Details\"\n },\n {\n \"id\": \"s3\",\n \"component\": \"StepperItem\",\n \"text\": \"Review\"\n }\n]",
45
73
  "name": "basic"
74
+ },
75
+ {
76
+ "description": "Click/keyboard-navigable stepper — host allows jumping between steps.",
77
+ "a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"Stepper\",\n \"step\": 1,\n \"interactive\": true,\n \"children\": [\n \"s1\",\n \"s2\",\n \"s3\"\n ]\n },\n {\n \"id\": \"s1\",\n \"component\": \"StepperItem\",\n \"text\": \"Account\"\n },\n {\n \"id\": \"s2\",\n \"component\": \"StepperItem\",\n \"text\": \"Details\"\n },\n {\n \"id\": \"s3\",\n \"component\": \"StepperItem\",\n \"text\": \"Review\"\n }\n]",
78
+ "name": "interactive"
46
79
  }
47
80
  ],
48
81
  "keywords": [
@@ -51,7 +84,9 @@
51
84
  "steps",
52
85
  "process",
53
86
  "onboarding",
54
- "multi-step"
87
+ "multi-step",
88
+ "interactive",
89
+ "click-to-navigate"
55
90
  ],
56
91
  "name": "UIStepper",
57
92
  "related": [