@adia-ai/web-components 0.8.4 → 0.8.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 (64) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/USAGE.md +60 -25
  3. package/components/adia-mark/adia-mark.a2ui.json +123 -0
  4. package/components/adia-mark/adia-mark.class.js +64 -0
  5. package/components/adia-mark/adia-mark.css +48 -0
  6. package/components/adia-mark/adia-mark.d.ts +20 -0
  7. package/components/adia-mark/adia-mark.examples.md +26 -0
  8. package/components/adia-mark/adia-mark.js +17 -0
  9. package/components/adia-mark/adia-mark.test.js +69 -0
  10. package/components/adia-mark/adia-mark.yaml +121 -0
  11. package/components/agent-questions/agent-questions.a2ui.json +2 -1
  12. package/components/agent-questions/agent-questions.class.js +96 -53
  13. package/components/agent-questions/agent-questions.css +27 -106
  14. package/components/agent-questions/agent-questions.yaml +1 -0
  15. package/components/button/button.class.js +5 -1
  16. package/components/calendar-grid/calendar-grid.a2ui.json +3 -1
  17. package/components/calendar-grid/calendar-grid.class.js +11 -6
  18. package/components/calendar-grid/calendar-grid.css +42 -20
  19. package/components/calendar-grid/calendar-grid.yaml +5 -0
  20. package/components/calendar-picker/calendar-picker.a2ui.json +3 -1
  21. package/components/calendar-picker/calendar-picker.class.js +11 -6
  22. package/components/calendar-picker/calendar-picker.css +39 -19
  23. package/components/calendar-picker/calendar-picker.yaml +5 -0
  24. package/components/chart/chart.class.js +17 -14
  25. package/components/color-input/color-input.class.js +8 -5
  26. package/components/color-picker/color-picker.class.js +6 -3
  27. package/components/field/field.class.js +10 -7
  28. package/components/field/field.css +21 -0
  29. package/components/fields/fields.class.js +15 -12
  30. package/components/icon/icon.class.js +9 -5
  31. package/components/index.js +1 -0
  32. package/components/nav/nav.class.js +2 -1
  33. package/components/noodles/noodles.class.js +14 -10
  34. package/components/page/page.class.js +3 -2
  35. package/components/select/select.class.js +3 -2
  36. package/components/swatch/swatch.class.js +27 -24
  37. package/components/swiper/swiper.class.js +4 -1
  38. package/components/swiper/swiper.css +13 -4
  39. package/components/table/table.a2ui.json +1 -1
  40. package/components/table/table.class.js +17 -0
  41. package/components/table/table.css +15 -2
  42. package/components/table/table.d.ts +1 -1
  43. package/components/table/table.test.js +51 -0
  44. package/components/table/table.yaml +6 -1
  45. package/components/toolbar/toolbar.class.js +11 -6
  46. package/components/tree/tree.class.js +5 -2
  47. package/core/data-stream.js +21 -15
  48. package/core/element.js +60 -1
  49. package/core/element.test.js +138 -0
  50. package/core/icons-phosphor.js +86 -22
  51. package/core/icons.js +22 -8
  52. package/dist/host.min.css +1 -1
  53. package/dist/host.sheet.js +1 -1
  54. package/dist/theme-provider.min.js +2 -2
  55. package/dist/web-components.min.css +1 -1
  56. package/dist/web-components.min.js +107 -102
  57. package/dist/web-components.sheet.js +1 -1
  58. package/package.json +1 -5
  59. package/styles/colors/semantics/core.css +1 -1
  60. package/styles/colors/semantics/features.css +1 -1
  61. package/styles/components.css +1 -0
  62. package/traits/error-shake/error-shake.js +23 -17
  63. package/traits/fade-presence/fade-presence.js +19 -15
  64. package/traits/success-checkmark/success-checkmark.js +21 -16
@@ -0,0 +1,121 @@
1
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
2
+ name: UIAdiaMark
3
+ tag: adia-mark-ui
4
+ status: stable
5
+ component: AdiaMark
6
+ category: display
7
+ version: 1
8
+ description: >-
9
+ The Adia brand mark — a rounded square with the "Adia" wordmark cut out
10
+ in a contrasting color. Token-driven: the background always reads as
11
+ the current scheme's inverse surface, and the wordmark always reads as
12
+ the current scheme's inverse foreground, so a single asset replaces
13
+ separate light/dark/transparent static exports (gh#283). Not an
14
+ `icon-ui` registry entry — icon-ui is the Phosphor-glyph registry only;
15
+ this is a fixed, one-off brand image, a separate delivery shape.
16
+ props:
17
+ size:
18
+ description: Diameter of the square mark. Matches avatar-ui's size ladder for visual consistency when the two sit side by side (e.g. an app switcher row).
19
+ type: string
20
+ default: md
21
+ enum:
22
+ - xs
23
+ - sm
24
+ - md
25
+ - lg
26
+ - xl
27
+ reflect: true
28
+ inline:
29
+ description: Renders as `display:inline-block` instead of the block-level default (ADR-0037) — set this when the mark sits beside a text run (a topbar brand slot) rather than standing alone.
30
+ type: boolean
31
+ default: false
32
+ reflect: true
33
+ events: {}
34
+ slots: {}
35
+ states:
36
+ - name: default
37
+ description: The only state — a static, non-interactive brand mark.
38
+ traits: []
39
+ tokens:
40
+ --adia-mark-size:
41
+ description: Width and height of the square mark.
42
+ default: 2.5rem
43
+ --adia-mark-radius:
44
+ description: Corner radius of the background square.
45
+ default: var(--a-radius-lg)
46
+ --adia-mark-bg:
47
+ description: Background fill — always the inverse surface token, regardless of variant or scheme.
48
+ default: var(--a-canvas-surface-inverse)
49
+ --adia-mark-fg:
50
+ description: Wordmark fill — always the inverse foreground token, regardless of variant or scheme.
51
+ default: var(--a-fg-inverse)
52
+ a2ui:
53
+ rules:
54
+ - Use <AdiaMark> for the Adia brand/app-identity mark — a favicon-style app-switcher tile, a splash/loading screen, an about-panel header. Do not use it as a generic icon; it has no [name] prop and always renders the same wordmark.
55
+ - Do not wrap <AdiaMark> in <Icon> or pass it a color/tone override — the background/foreground pairing is fixed by design (always inverse-on-inverse for contrast), not a themeable variant.
56
+ - Prefer <AdiaMark size="sm"> in a nav/topbar brand slot (a Row alongside the product name); reserve larger sizes (lg, xl) for splash screens or empty-state headers. Set [inline] only when the mark sits directly inside a run of prose text, not inside a Row/flex layout (block-level children compose fine as flex items already).
57
+ anti_patterns:
58
+ - wrong: |
59
+ <icon-ui name="adia"></icon-ui>
60
+ why: |
61
+ icon-ui only resolves Phosphor glyph names from the registry; there
62
+ is no "adia" Phosphor icon, and registering one would misuse a
63
+ third-party glyph set for a proprietary brand mark.
64
+ fix: |
65
+ <adia-mark-ui size="sm"></adia-mark-ui>
66
+ - wrong: |
67
+ <adia-mark-ui style="--adia-mark-bg: #171818; --adia-mark-fg: #E2E3E4;"></adia-mark-ui>
68
+ why: |
69
+ Hardcoding the fill colors defeats the entire point of gh#283 —
70
+ the mark is supposed to track the inverse-surface/inverse-foreground
71
+ tokens automatically across scheme changes, not be pinned to one
72
+ scheme's static values.
73
+ fix: |
74
+ <adia-mark-ui size="sm"></adia-mark-ui>
75
+ examples:
76
+ - name: topbar-brand-mark
77
+ description: Small brand mark in an admin-shell topbar, beside the product name.
78
+ a2ui: >-
79
+ [
80
+ {
81
+ "id": "brand-row",
82
+ "component": "Row",
83
+ "gap": "2",
84
+ "align": "center",
85
+ "children": ["mark", "name"]
86
+ },
87
+ {
88
+ "id": "mark",
89
+ "component": "AdiaMark",
90
+ "size": "sm"
91
+ },
92
+ {
93
+ "id": "name",
94
+ "component": "Text",
95
+ "variant": "heading",
96
+ "text": "Adia"
97
+ }
98
+ ]
99
+ keywords:
100
+ - adia
101
+ - brand
102
+ - logo
103
+ - wordmark
104
+ - mark
105
+ - identity
106
+ - favicon
107
+ synonyms:
108
+ logo:
109
+ - adia
110
+ - brand
111
+ - mark
112
+ - wordmark
113
+ brand:
114
+ - adia
115
+ - logo
116
+ - mark
117
+ - identity
118
+ related:
119
+ - Icon
120
+ - Avatar
121
+ - Image
@@ -45,7 +45,8 @@
45
45
  "anti_patterns": [],
46
46
  "category": "agent",
47
47
  "composes": [
48
- "button-ui"
48
+ "button-ui",
49
+ "option-card-ui"
49
50
  ],
50
51
  "events": {
51
52
  "questions-answer": {
@@ -41,20 +41,11 @@
41
41
 
42
42
  import { UIElement } from '../../core/element.js';
43
43
 
44
- function escapeText(s) {
45
- return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
46
- }
47
-
48
- function escapeAttr(s) {
49
- return String(s).replace(/"/g, '&quot;');
50
- }
51
-
52
44
  export class UIAgentQuestions extends UIElement {
53
- // §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
54
- // consumer markup). Aggregated by installIconLoadersForRegistered()
55
- // across all defined elements. Audited by check-required-icons.mjs
56
- // (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
57
- static requiredIcons = ['check'];
45
+ // gh issue 276 wave 4: options now render as <option-card-ui> (which
46
+ // composes icon-ui itself for the leading `icon` prop). This primitive
47
+ // no longer stamps any icon directly, so no requiredIcons entry is
48
+ // needed here see check-required-icons.mjs (slot 11).
58
49
 
59
50
  static properties = {
60
51
  multi: { type: Boolean, default: false, reflect: true },
@@ -87,6 +78,9 @@ export class UIAgentQuestions extends UIElement {
87
78
 
88
79
  disconnected() {
89
80
  this.#submitEl?.removeEventListener('press', this.#onSubmit);
81
+ this.#optionsEl?.removeEventListener('change', this.#onCardChange);
82
+ this.#optionsEl?.removeEventListener('click', this.#onOptionsClickCapture, true);
83
+ this.#optionsEl?.removeEventListener('keydown', this.#onOptionsKeydownCapture, true);
90
84
  this.#questionEl = this.#helperEl = this.#optionsEl = this.#submitEl = null;
91
85
  }
92
86
 
@@ -121,6 +115,20 @@ export class UIAgentQuestions extends UIElement {
121
115
 
122
116
  this.#optionsEl = document.createElement('div');
123
117
  this.#optionsEl.setAttribute('data-questions-options', '');
118
+ // Single-select: option-card-ui's own radiogroup machinery handles
119
+ // click + keyboard natively and fires `change` when a card becomes
120
+ // selected — we just listen for it.
121
+ this.#optionsEl.addEventListener('change', this.#onCardChange);
122
+ // Multi-select: option-card-ui is radio-only (its #select() no-ops
123
+ // once checked, and Enter/Space call that private method directly
124
+ // rather than dispatching a click), so there is no toggle-off hook
125
+ // to attach to. Intercept at the container's CAPTURE phase — before
126
+ // the card's own bubble-phase listeners ever run — and drive
127
+ // checked/aria state ourselves. Single mode never reaches these
128
+ // (guarded by `if (!this.multi) return`), so its native behavior is
129
+ // untouched.
130
+ this.#optionsEl.addEventListener('click', this.#onOptionsClickCapture, true);
131
+ this.#optionsEl.addEventListener('keydown', this.#onOptionsKeydownCapture, true);
124
132
 
125
133
  this.#submitEl = document.createElement('button-ui');
126
134
  this.#submitEl.setAttribute('text', this.submitLabel || 'Continue');
@@ -139,33 +147,28 @@ export class UIAgentQuestions extends UIElement {
139
147
  this.#optionsEl.innerHTML = '';
140
148
 
141
149
  for (const opt of this.#options) {
142
- const card = document.createElement('button');
143
- card.type = 'button';
150
+ const card = document.createElement('option-card-ui');
144
151
  card.setAttribute('data-questions-option', '');
145
- card.setAttribute('role', this.multi ? 'checkbox' : 'radio');
146
- card.setAttribute('aria-checked', this.#selected.has(opt.id) ? 'true' : 'false');
147
- if (this.#selected.has(opt.id)) card.setAttribute('data-selected', '');
148
- if (this.#answered) card.setAttribute('disabled', '');
149
-
150
- const iconHTML = opt.icon
151
- ? `<icon-ui name="${escapeAttr(opt.icon)}" color="muted" data-questions-option-icon></icon-ui>`
152
- : '';
153
- const checkHTML = `
154
- <span data-questions-option-check aria-hidden="true">
155
- <icon-ui name="check" size="sm" color="accent"></icon-ui>
156
- </span>`;
157
-
158
- card.innerHTML = `
159
- ${iconHTML}
160
- <span data-questions-option-body>
161
- <span data-questions-option-label>${escapeText(opt.label || '')}</span>
162
- ${opt.description ? `<span data-questions-option-desc>${escapeText(opt.description)}</span>` : ''}
163
- </span>
164
- ${checkHTML}
165
- `;
166
-
167
- card.addEventListener('click', () => this.#pick(opt));
152
+ card.setAttribute('data-questions-option-id', opt.id);
153
+ // Shared name → siblings form option-card-ui's own radiogroup
154
+ // (single mode). Inert in multi mode: #select() never runs there
155
+ // (see the capture-phase override below), so the shared name has
156
+ // no sibling-uncheck effect to guard against.
157
+ card.name = 'questions-option';
158
+ card.value = opt.id;
159
+ card.heading = opt.label || '';
160
+ card.description = opt.description || '';
161
+ card.icon = opt.icon || '';
162
+
168
163
  this.#optionsEl.appendChild(card);
164
+
165
+ // Multi mode: override the radio role option-card-ui sets on
166
+ // connect — this is a checkbox group, and aria-checked (which the
167
+ // primitive keeps in sync with `.checked` regardless of role) is
168
+ // valid for either.
169
+ if (this.multi) card.setAttribute('role', 'checkbox');
170
+ card.checked = this.#selected.has(opt.id);
171
+ card.disabled = this.#answered;
169
172
  }
170
173
 
171
174
  if (this.#submitEl) {
@@ -173,22 +176,62 @@ export class UIAgentQuestions extends UIElement {
173
176
  }
174
177
  }
175
178
 
176
- #pick(opt) {
177
- if (this.#answered) return;
178
- if (this.multi) {
179
- if (this.#selected.has(opt.id)) this.#selected.delete(opt.id);
180
- else this.#selected.add(opt.id);
181
- this.#render();
182
- } else {
183
- this.#selected.clear();
184
- this.#selected.add(opt.id);
185
- this.#answered = true;
186
- this.#render();
187
- this.dispatchEvent(new CustomEvent('questions-answer', {
188
- bubbles: true,
189
- detail: { selected: [opt.id], option: opt },
190
- }));
179
+ /** Single mode — option-card-ui's own radiogroup fired `change` because
180
+ * a card just became selected (it never fires on becoming unselected,
181
+ * so this is always a genuine new pick). */
182
+ #onCardChange = (e) => {
183
+ if (this.multi || this.#answered) return;
184
+ const id = e.detail?.value;
185
+ const opt = this.#options.find((o) => o.id === id);
186
+ if (!opt) return;
187
+ this.#selected.clear();
188
+ this.#selected.add(opt.id);
189
+ this.#answered = true;
190
+ this.#render();
191
+ this.dispatchEvent(new CustomEvent('questions-answer', {
192
+ bubbles: true,
193
+ detail: { selected: [opt.id], option: opt },
194
+ }));
195
+ };
196
+
197
+ /** Multi mode — capture-phase click override (see #build). Stops the
198
+ * event before option-card-ui's own bubble-phase click handler runs,
199
+ * since that handler can only select, never deselect. */
200
+ #onOptionsClickCapture = (e) => {
201
+ if (!this.multi) return;
202
+ const card = e.target.closest?.('option-card-ui[data-questions-option]');
203
+ if (!card || !this.#optionsEl.contains(card)) return;
204
+ e.stopPropagation();
205
+ this.#toggleMulti(card);
206
+ };
207
+
208
+ /** Multi mode — capture-phase keydown override. Space/Enter toggle
209
+ * (mirroring click); arrow keys are swallowed rather than forwarded
210
+ * to option-card-ui's own forced-select navigation, since checkbox
211
+ * groups here never had arrow-key nav (Tab-only, as before). */
212
+ #onOptionsKeydownCapture = (e) => {
213
+ if (!this.multi) return;
214
+ const card = e.target.closest?.('option-card-ui[data-questions-option]');
215
+ if (!card || !this.#optionsEl.contains(card)) return;
216
+ if (e.key === ' ' || e.key === 'Enter') {
217
+ e.preventDefault();
218
+ e.stopPropagation();
219
+ this.#toggleMulti(card);
220
+ return;
191
221
  }
222
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
223
+ e.stopPropagation();
224
+ }
225
+ };
226
+
227
+ #toggleMulti(card) {
228
+ if (this.#answered) return;
229
+ const id = card.dataset.questionsOptionId;
230
+ const opt = this.#options.find((o) => o.id === id);
231
+ if (!opt) return;
232
+ if (this.#selected.has(opt.id)) this.#selected.delete(opt.id);
233
+ else this.#selected.add(opt.id);
234
+ this.#render();
192
235
  }
193
236
 
194
237
  #onSubmit = () => {
@@ -18,8 +18,6 @@
18
18
  --agent-questions-option-bg-sel: var(--md-sys-color-primary-container);
19
19
  --agent-questions-option-border: var(--md-sys-color-neutral-outline-variant);
20
20
  --agent-questions-option-border-sel: var(--a-primary-bg);
21
- --agent-questions-check-bg: var(--a-primary-bg);
22
- --agent-questions-check-fg: var(--md-sys-color-primary-on-primary);
23
21
 
24
22
  /* ── Layout ── */
25
23
  --agent-questions-gap: var(--a-space-2);
@@ -27,9 +25,6 @@
27
25
  --agent-questions-option-padding: var(--a-space-3);
28
26
  --agent-questions-option-radius: var(--a-radius);
29
27
  --agent-questions-q-padding: var(--a-space-1);
30
- --agent-questions-option-body-gap: var(--a-space-0-5);
31
- /* Component-intrinsic measurement; no --a-space-* equivalent */
32
- --agent-questions-check-size: 20px;
33
28
 
34
29
  /* ── Motion ── */
35
30
  --agent-questions-duration: var(--a-duration-fast);
@@ -61,108 +56,34 @@
61
56
  gap: var(--agent-questions-gap);
62
57
  }
63
58
 
59
+ /* gh issue 276 wave 4: options render as <option-card-ui> now — it owns
60
+ its own box model, borders, hover/checked/disabled states, focus ring,
61
+ and heading/description/icon layout via its own @scope'd CSS. This
62
+ re-points its token surface to agent-questions' existing tokens so the
63
+ migration doesn't shift the look; it does not restyle the card itself.
64
+ One deliberate visual difference: the selected indicator is now
65
+ option-card-ui's authentic filled-radio-dot recipe (shared with
66
+ plan-picker-ui) rather than the bespoke check-glyph badge the native
67
+ buttons drew — that badge doesn't exist as an option-card-ui slot. */
64
68
  [data-questions-option] {
65
- display: flex;
66
- /* Top-align so the leading icon stays with the label's first line
67
- when a description wraps below; we restore visual centering on
68
- the icon and check by nudging them to the label's first-line
69
- center via margin-top. Centering the whole row dragged the icon
70
- down to the body's vertical midpoint, which sat between label
71
- and description rather than on the label. */
72
- align-items: start;
73
- gap: var(--agent-questions-option-gap);
74
- padding: var(--agent-questions-option-padding);
75
- border: 1px solid var(--agent-questions-option-border);
76
- border-radius: var(--agent-questions-option-radius);
77
- background: var(--agent-questions-option-bg);
78
- color: inherit;
79
- cursor: pointer;
80
- text-align: start;
81
- font: inherit;
82
- /* font-family floor — see segment.css rationale (inline default). */
83
- font-family: var(--agent-questions-font-family, var(--a-font-family-ui));
84
- transition:
85
- background var(--agent-questions-duration) var(--agent-questions-easing),
86
- border-color var(--agent-questions-duration) var(--agent-questions-easing);
87
- min-width: 0;
69
+ --option-card-padding-block: var(--agent-questions-option-padding);
70
+ --option-card-padding-inline: var(--agent-questions-option-padding);
71
+ --option-card-radius: var(--agent-questions-option-radius);
72
+ --option-card-gap-x: var(--agent-questions-option-gap);
73
+ --option-card-bg: var(--agent-questions-option-bg);
74
+ --option-card-border: var(--agent-questions-option-border);
75
+ --option-card-bg-hover: var(--agent-questions-option-bg-hover);
76
+ /* Match prior behavior: hover changed background only, never border. */
77
+ --option-card-border-hover: var(--agent-questions-option-border);
78
+ --option-card-bg-checked: var(--agent-questions-option-bg-sel);
79
+ --option-card-border-checked: var(--agent-questions-option-border-sel);
80
+ --option-card-heading-color: var(--agent-questions-label-fg);
81
+ --option-card-heading-weight: var(--agent-questions-label-weight);
82
+ --option-card-heading-size: var(--agent-questions-label-size);
83
+ --option-card-desc-color: var(--agent-questions-desc-fg);
84
+ --option-card-desc-size: var(--agent-questions-desc-size);
85
+ --option-card-duration: var(--agent-questions-duration);
86
+ --option-card-easing: var(--agent-questions-easing);
88
87
  width: 100%;
89
88
  }
90
-
91
- [data-questions-option]:hover {
92
- background: var(--agent-questions-option-bg-hover);
93
- }
94
-
95
- [data-questions-option][data-selected] {
96
- background: var(--agent-questions-option-bg-sel);
97
- border-color: var(--agent-questions-option-border-sel);
98
- }
99
-
100
- [data-questions-option][disabled] {
101
- cursor: default;
102
- opacity: 0.7;
103
- }
104
-
105
- [data-questions-option]:focus-visible {
106
- outline: 2px solid var(--agent-questions-option-border-sel);
107
- outline-offset: 2px;
108
- }
109
-
110
- [data-questions-option-icon] {
111
- flex-shrink: 0;
112
- /* Align the leading icon's center with the label's first-line
113
- center. `1.5em` matches the label's effective line-height
114
- (label is `font-size:` only — line-height inherits as 1.5).
115
- `var(--a-icon-size)` is icon-ui's rendered height. Centering
116
- the icon inside one line-box of the label's height puts its
117
- midpoint on the label's first-line midpoint. We use em on the
118
- icon's own font-size (which icon-ui keeps at the option's
119
- font-size) so the offset scales with whatever the consumer
120
- sets `--agent-questions-label-size` to. */
121
- margin-top: calc((1.5em - var(--a-icon-size, 1em)) / 2);
122
- }
123
-
124
- [data-questions-option-body] {
125
- flex: 1;
126
- min-width: 0;
127
- display: flex;
128
- flex-direction: column;
129
- gap: var(--agent-questions-option-body-gap);
130
- }
131
-
132
- [data-questions-option-label] {
133
- font-size: var(--agent-questions-label-size);
134
- font-weight: var(--agent-questions-label-weight);
135
- color: var(--agent-questions-label-fg);
136
- }
137
-
138
- [data-questions-option-desc] {
139
- font-size: var(--agent-questions-desc-size);
140
- color: var(--agent-questions-desc-fg);
141
- line-height: 1.4;
142
- }
143
-
144
- [data-questions-option-check] {
145
- width: var(--agent-questions-check-size);
146
- height: var(--agent-questions-check-size);
147
- border-radius: 50%;
148
- flex-shrink: 0;
149
- display: flex;
150
- align-items: center;
151
- justify-content: center;
152
- background: transparent;
153
- opacity: 0;
154
- transition: opacity var(--agent-questions-duration) var(--agent-questions-easing);
155
- /* Match the leading-icon offset so a row of [icon · label · check]
156
- reads as one horizontal line aligned to the label's first line. */
157
- margin-top: calc((1.5em - var(--agent-questions-check-size)) / 2);
158
- }
159
-
160
- [data-questions-option][data-selected] [data-questions-option-check] {
161
- background: var(--agent-questions-check-bg);
162
- opacity: 1;
163
- }
164
-
165
- [data-questions-option][data-selected] [data-questions-option-check] icon-ui {
166
- color: var(--agent-questions-check-fg);
167
- }
168
89
  }
@@ -19,6 +19,7 @@ description: |
19
19
  # do NOT auto-import them. Consumer (or demo shell) must explicitly import.
20
20
  composes:
21
21
  - button-ui
22
+ - option-card-ui # gh issue 276 wave 4
22
23
  props:
23
24
  helper:
24
25
  description: Helper text.
@@ -50,7 +50,11 @@ export class UIButton extends UIElement {
50
50
  static template = () => null;
51
51
 
52
52
  connected() {
53
- this.setAttribute('role', 'button');
53
+ // Respect consumer-authored ARIA — a composite may stamp button-ui as
54
+ // role="tab" (swiper dots), role="gridcell"-hosted day buttons
55
+ // (calendar), etc. Same contract as tabindex below: default only when
56
+ // the author said nothing (operator ruling 2026-07-17, gh#276 wave 4).
57
+ if (!this.hasAttribute('role')) this.setAttribute('role', 'button');
54
58
  // Respect a consumer-authored tabindex (e.g. a mouse-only tabindex="-1"
55
59
  // affordance like combobox's clear x, or a roving-tabindex composite) —
56
60
  // only default the focusable state when the author said nothing.
@@ -70,7 +70,9 @@
70
70
  }
71
71
  ],
72
72
  "category": "input",
73
- "composes": [],
73
+ "composes": [
74
+ "button-ui"
75
+ ],
74
76
  "events": {
75
77
  "change": {
76
78
  "description": "User selected a date. detail.value is the ISO string."
@@ -164,10 +164,13 @@ export class UICalendarGrid extends UIElement {
164
164
  const rangeTo = (rs && re && rs <= re) ? re : null;
165
165
 
166
166
  // Header — Prev / Title / Next
167
+ // <button-ui variant="ghost" icon="…"> (gh issue 276 wave 4) — button-ui
168
+ // respects the consumer-authored tabindex="-1", so the nav carets stay
169
+ // out of the roving-tabindex day-grid's tab sequence exactly as before.
167
170
  let h = `<div data-cal-header>
168
- <button data-cal-prev type="button" aria-label="Previous month" tabindex="-1"><icon-ui name="caret-left"></icon-ui></button>
171
+ <button-ui variant="ghost" size="xs" icon="caret-left" data-cal-prev aria-label="Previous month" tabindex="-1"></button-ui>
169
172
  <span data-cal-title>${MONTHS_LONG[month]} ${year}</span>
170
- <button data-cal-next type="button" aria-label="Next month" tabindex="-1"><icon-ui name="caret-right"></icon-ui></button>
173
+ <button-ui variant="ghost" size="xs" icon="caret-right" data-cal-next aria-label="Next month" tabindex="-1"></button-ui>
171
174
  </div>`;
172
175
 
173
176
  // Day-of-week header
@@ -183,7 +186,7 @@ export class UICalendarGrid extends UIElement {
183
186
  // Previous-month trailing days (outside, disabled)
184
187
  for (let i = firstDay - 1; i >= 0; i--) {
185
188
  const day = daysInPrev - i;
186
- h += `<button type="button" data-cal-day data-outside disabled tabindex="-1">${day}</button>`;
189
+ h += `<button-ui variant="ghost" data-cal-day data-outside disabled tabindex="-1" text="${day}"></button-ui>`;
187
190
  }
188
191
 
189
192
  // Current-month days
@@ -206,7 +209,6 @@ export class UICalendarGrid extends UIElement {
206
209
  const isRangeEnd = !!(rangeTo && sameDay(date, rangeTo));
207
210
 
208
211
  const attrs = [
209
- 'type="button"',
210
212
  'data-cal-day',
211
213
  `data-date="${iso}"`,
212
214
  isToday ? 'data-today' : '',
@@ -219,14 +221,17 @@ export class UICalendarGrid extends UIElement {
219
221
  `tabindex="${isFocused ? '0' : '-1'}"`,
220
222
  ].filter(Boolean).join(' ');
221
223
 
222
- h += `<button ${attrs}>${d}</button>`;
224
+ // <button-ui variant="ghost" text="…"> (gh issue 276 wave 4) — the day
225
+ // number moves from element text content to the `text` attribute,
226
+ // which button-ui renders via CSS attr(text) on its ::after.
227
+ h += `<button-ui variant="ghost" ${attrs} text="${d}"></button-ui>`;
223
228
  }
224
229
 
225
230
  // Next-month leading days
226
231
  const totalCells = firstDay + daysInMonth;
227
232
  const remaining = (7 - (totalCells % 7)) % 7;
228
233
  for (let d = 1; d <= remaining; d++) {
229
- h += `<button type="button" data-cal-day data-outside disabled tabindex="-1">${d}</button>`;
234
+ h += `<button-ui variant="ghost" data-cal-day data-outside disabled tabindex="-1" text="${d}"></button-ui>`;
230
235
  }
231
236
 
232
237
  h += '</div>';