@adia-ai/web-components 0.8.2 → 0.8.4
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.
- package/CHANGELOG.md +20 -0
- package/components/accordion/accordion.css +5 -0
- package/components/agent-artifact/agent-artifact.css +1 -0
- package/components/agent-reasoning/agent-reasoning.css +9 -5
- package/components/button/button.class.js +4 -1
- package/components/button/button.css +1 -1
- package/components/calendar-picker/calendar-picker.css +2 -0
- package/components/chat-thread/chat-input.a2ui.json +3 -1
- package/components/chat-thread/chat-input.css +12 -13
- package/components/chat-thread/chat-input.js +6 -2
- package/components/chat-thread/chat-input.yaml +3 -0
- package/components/check/check.css +1 -1
- package/components/combobox/combobox.a2ui.json +3 -1
- package/components/combobox/combobox.class.js +1 -3
- package/components/combobox/combobox.css +9 -12
- package/components/combobox/combobox.yaml +2 -0
- package/components/date-range-picker/date-range-picker.css +1 -0
- package/components/input/input.css +6 -2
- package/components/nav-group/nav-group.css +5 -6
- package/components/option-card/option-card.css +1 -0
- package/components/radio/radio.css +1 -1
- package/components/range/range.css +2 -0
- package/components/select/select.a2ui.json +2 -1
- package/components/select/select.class.js +9 -9
- package/components/select/select.css +29 -40
- package/components/select/select.test.js +6 -2
- package/components/select/select.yaml +1 -0
- package/components/swatch/swatch.a2ui.json +3 -1
- package/components/swatch/swatch.class.js +14 -8
- package/components/swatch/swatch.css +13 -18
- package/components/swatch/swatch.yaml +2 -0
- package/components/switch/switch.css +1 -1
- package/components/tag/tag.a2ui.json +4 -1
- package/components/tag/tag.class.js +3 -1
- package/components/tag/tag.css +13 -15
- package/components/tag/tag.yaml +3 -0
- package/components/tags-input/tags-input.css +1 -0
- package/components/textarea/textarea.css +2 -0
- package/components/time-picker/time-picker.css +2 -0
- package/components/timeline/timeline.a2ui.json +2 -1
- package/components/timeline/timeline.class.js +7 -8
- package/components/timeline/timeline.css +14 -26
- package/components/timeline/timeline.yaml +1 -0
- package/components/toggle-group/toggle-group.css +1 -0
- package/components/tree/tree.css +5 -2
- package/components/upload/upload.css +1 -0
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +7 -11
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -1
- package/styles/colors/semantics/features.css +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
# Changelog — @adia-ai/web-components
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Focus rings pair with transparent borders** (operator ruling 2026-07-16): a control showing its `--a-focus-ring` box-shadow sets `border-color: transparent` in the same rule — border + gap + ring double-drew the edge (visible on the chat composer). Applied to the 21 ring-bearing controls with visible borders (input, textarea, select, combobox, check, radio, switch, range, tag[tone=outline], tags-input's missing invalid-state half, time/calendar/date-range pickers, upload dropzone, option-card, accordion[contained], agent-artifact header, button, toggle-group, chat-input); the pairing law is stated in `styles/colors/semantics/features.css` and the component-token contract.
|
|
7
|
+
- **Sidebar carets read the universal `--a-caret-size` (14px)**: tree carets were an 8px outlier (`--a-space-2`), accordion carets rode the 1rem icon default, select's trigger caret rode the ambient icon size (admin-shell's 18px bump), timeline's outcomes toggle likewise, and nav-group's collapsed block carried two `var()`-less declarations whose value was the literal token name (silently invalid — removed).
|
|
8
|
+
- **`agent-reasoning-ui` connector spine ~2px right of the dot centers**: the spine read `--timeline-marker-w` from outside timeline-ui's scope (unreachable — always the 1.25rem fallback) while the composed `size="sm"` dots center in a 1rem column; now keyed to the component's own `--agent-reasoning-marker-w`. Probe: 0px delta.
|
|
9
|
+
- **`theme-provider.min.js` inlined a stale foundation sheet**: the bundle embeds the GENERATED `dist/web-components.sheet.js`, and building JS before CSS baked the previous iteration in. `npm run build:bundles` now encodes the css→js order.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **No native `<button>` in stamped component DOM (gh#276)**: `select-ui` (clear-all / select-all / +N overflow), `tag-ui` (dismiss), `swatch-ui` (copy), `timeline-item-ui` (outcomes toggle), `chat-input-ui` (attachment remove), and `combobox-ui` (clear x) stamp `<button-ui variant="ghost" size="xs">` with token re-points — shape, semantic intent, theme tokens, and focus-ring discipline come from the primitive. Data-hooks, aria-labels, and event wiring unchanged. Parked with reasons (in `scripts/dev/audit-stamped-native-buttons.mjs`, the new CI gate): table, agent-questions option cards, swiper dots, calendar day grids.
|
|
13
|
+
- **`button-ui` respects a consumer-authored `tabindex`** instead of clobbering it with `0` on connect — enables mouse-only affordances (combobox's clear keeps `tabindex="-1"`) and roving-tabindex composition.
|
|
14
|
+
|
|
15
|
+
### Maintenance
|
|
16
|
+
- **`dist/` bundles rebuilt** (focus-ring/caret/button-ui payloads across `web-components.min.{css,js}`, `web-components.sheet.js`, `theme-provider.min.js`).
|
|
17
|
+
|
|
18
|
+
## [0.8.3] — 2026-07-16
|
|
19
|
+
|
|
20
|
+
### Maintenance
|
|
21
|
+
- **Lockstep version bump only.** No source changes in this package; bumped to maintain the 11-package version coherence enforced by `scripts/release/check-lockstep.mjs`. Substantive v0.8.3 work shipped in the two Claude Code plugins — the adia-tokens knowledge pack + command refresh (adia-ui-factory) and the release-tooling invariant-3 reconciliation (adia-ui-forge); see `packages/plugins/adia-ui-factory/CHANGELOG.md#083--2026-07-16`.
|
|
2
22
|
## [0.8.2] — 2026-07-16
|
|
3
23
|
|
|
4
24
|
### Fixed
|
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
:scope[variant="contained"]:has(> :focus-visible),
|
|
114
114
|
:where(accordion-ui[variant="contained"]) > :scope:not([variant="flat"]):has(> :focus-visible) {
|
|
115
115
|
box-shadow: var(--accordion-item-focus-ring);
|
|
116
|
+
border-color: transparent;
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
/* ── Header ── */
|
|
@@ -172,6 +173,10 @@
|
|
|
172
173
|
/* ── Caret icon ── */
|
|
173
174
|
:scope [slot="caret"] {
|
|
174
175
|
flex-shrink: 0;
|
|
176
|
+
/* Universal caret size (14px default) — one caret size across
|
|
177
|
+
nav-group / tree / accordion (ruling 2026-07-16); unset, the icon
|
|
178
|
+
rendered at the 1rem icon default and read oversized in sidebars. */
|
|
179
|
+
--a-icon-size: var(--a-caret-size);
|
|
175
180
|
color: var(--accordion-item-caret-fg);
|
|
176
181
|
transition:
|
|
177
182
|
transform var(--accordion-item-duration) var(--accordion-item-easing),
|
|
@@ -132,14 +132,18 @@
|
|
|
132
132
|
the last, bridging plan / thought / iteration interstitials so the run
|
|
133
133
|
reads as one timeline rather than fragmented per-block stubs. Endpoints are
|
|
134
134
|
JS-measured into --reasoning-spine-top / -h (#layoutSpine) — interstitial
|
|
135
|
-
heights are dynamic, so pure CSS can't place them. The x
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
heights are dynamic, so pure CSS can't place them. The x is half the
|
|
136
|
+
component's OWN marker column (--agent-reasoning-marker-w, 1rem — matched
|
|
137
|
+
by the composed <timeline-ui size="sm">, whose dots center in that same
|
|
138
|
+
column). NOT --timeline-marker-w: that token lives inside timeline-ui's
|
|
139
|
+
scope, a DESCENDANT — unreachable here, so reading it silently took the
|
|
140
|
+
1.25rem fallback and drew the spine ~2px right of the dot centers.
|
|
141
|
+
Sits behind the dots (z-index 0 < the dots' z-index 1). The per-item
|
|
142
|
+
timeline connectors are suppressed below so they don't double-draw. */
|
|
139
143
|
[data-reasoning-body][data-spine]::before {
|
|
140
144
|
content: '';
|
|
141
145
|
position: absolute;
|
|
142
|
-
left: calc(var(--
|
|
146
|
+
left: calc(var(--agent-reasoning-marker-w, 1rem) / 2 - var(--agent-reasoning-line-w, 1.5px) / 2);
|
|
143
147
|
top: var(--reasoning-spine-top, 0);
|
|
144
148
|
height: var(--reasoning-spine-h, 0);
|
|
145
149
|
width: var(--agent-reasoning-line-w, 1.5px);
|
|
@@ -51,7 +51,10 @@ export class UIButton extends UIElement {
|
|
|
51
51
|
|
|
52
52
|
connected() {
|
|
53
53
|
this.setAttribute('role', 'button');
|
|
54
|
-
|
|
54
|
+
// Respect a consumer-authored tabindex (e.g. a mouse-only tabindex="-1"
|
|
55
|
+
// affordance like combobox's clear x, or a roving-tabindex composite) —
|
|
56
|
+
// only default the focusable state when the author said nothing.
|
|
57
|
+
if (!this.hasAttribute('tabindex')) this.setAttribute('tabindex', '0');
|
|
55
58
|
this.addEventListener('click', this.#onClick);
|
|
56
59
|
this.addEventListener('keydown', this.#onKey);
|
|
57
60
|
}
|
|
@@ -198,7 +198,7 @@ button-ui[variant="ghost"][color="warning"]:not([disabled]):hover { --button-f
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
/* :scope:active moved outside @scope — see Safari 17.x bug note at top. */
|
|
201
|
-
:scope:focus-visible { outline: none; box-shadow: var(--button-focus-ring); }
|
|
201
|
+
:scope:focus-visible { outline: none; box-shadow: var(--button-focus-ring); border-color: transparent; }
|
|
202
202
|
|
|
203
203
|
/* ── Variants (rest state) ──
|
|
204
204
|
Variant/state blocks re-point the component tokens (--button-bg etc.);
|
|
@@ -156,10 +156,12 @@
|
|
|
156
156
|
:scope:focus-visible [slot="trigger"] {
|
|
157
157
|
/* Canonical ring via L3 token (see semantics.css FOCUS block). */
|
|
158
158
|
box-shadow: var(--calendar-picker-trigger-focus-ring);
|
|
159
|
+
border-color: transparent;
|
|
159
160
|
}
|
|
160
161
|
:scope[aria-invalid="true"]:focus-visible [slot="trigger"],
|
|
161
162
|
:scope[error]:focus-visible [slot="trigger"] {
|
|
162
163
|
box-shadow: var(--calendar-picker-trigger-focus-ring-invalid);
|
|
164
|
+
border-color: transparent;
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
[slot="display"] {
|
|
@@ -59,7 +59,9 @@
|
|
|
59
59
|
}
|
|
60
60
|
],
|
|
61
61
|
"category": "agent",
|
|
62
|
-
"composes": [
|
|
62
|
+
"composes": [
|
|
63
|
+
"button-ui"
|
|
64
|
+
],
|
|
63
65
|
"events": {
|
|
64
66
|
"submit": {
|
|
65
67
|
"description": "Fires when the user presses Enter (without Shift) in the textarea OR clicks the built-in send button. Unconditional — there is no [submit-on-enter] opt-in or opt-out attribute; Enter→submit is delegated from the inner <textarea-ui> which always emits a `submit` event on Enter without Shift. The composer suppresses submission while `[loading]` is set.",
|
|
@@ -122,10 +122,12 @@ chat-input-ui textarea-ui:not([disabled]) [slot="text"]:hover {
|
|
|
122
122
|
─────────────────────────────────────────────────────────────── */
|
|
123
123
|
:scope:focus-within {
|
|
124
124
|
box-shadow: var(--chat-input-focus-ring);
|
|
125
|
+
border-color: transparent;
|
|
125
126
|
}
|
|
126
127
|
:scope[aria-invalid="true"]:focus-within,
|
|
127
128
|
:scope[error]:focus-within {
|
|
128
129
|
box-shadow: var(--chat-input-focus-ring-invalid);
|
|
130
|
+
border-color: transparent;
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
/* ── Nested-control color isolation ─────────────────────────────────
|
|
@@ -239,23 +241,20 @@ chat-input-ui textarea-ui:not([disabled]) [slot="text"]:hover {
|
|
|
239
241
|
border: 1px solid var(--chat-input-image-border);
|
|
240
242
|
}
|
|
241
243
|
|
|
244
|
+
/* Attachment-remove — a stamped <button-ui variant="ghost" size="xs"
|
|
245
|
+
icon="x"> (gh issue 276: no native <button> in stamped DOM); the
|
|
246
|
+
floating filled-circle look rides button token re-points. */
|
|
242
247
|
[slot="preview"] [data-preview-remove] {
|
|
243
248
|
position: absolute;
|
|
244
249
|
top: calc(-1 * var(--chat-input-remove-offset));
|
|
245
250
|
right: calc(-1 * var(--chat-input-remove-offset));
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
line-height: 1;
|
|
254
|
-
cursor: pointer;
|
|
255
|
-
display: flex;
|
|
256
|
-
align-items: center;
|
|
257
|
-
justify-content: center;
|
|
258
|
-
padding: 0;
|
|
251
|
+
--button-height: var(--chat-input-remove-size);
|
|
252
|
+
--button-radius: var(--a-radius-full);
|
|
253
|
+
--button-bg: var(--chat-input-remove-bg);
|
|
254
|
+
--button-fg: var(--chat-input-remove-fg);
|
|
255
|
+
--button-bg-ghost-hover: var(--chat-input-remove-bg);
|
|
256
|
+
--button-fg-ghost-hover: var(--chat-input-remove-fg);
|
|
257
|
+
--a-icon-size: var(--chat-input-remove-font);
|
|
259
258
|
}
|
|
260
259
|
|
|
261
260
|
/* Disabled / streaming — host-level chrome. `color` cascades to the
|
|
@@ -228,8 +228,12 @@ class UIChatInput extends UIElement {
|
|
|
228
228
|
const img = document.createElement('img');
|
|
229
229
|
img.src = a.dataUrl;
|
|
230
230
|
img.alt = a.name || 'attachment';
|
|
231
|
-
|
|
232
|
-
btn
|
|
231
|
+
// button-ui, not a native <button> (gh issue 276).
|
|
232
|
+
const btn = document.createElement('button-ui');
|
|
233
|
+
btn.setAttribute('variant', 'ghost');
|
|
234
|
+
btn.setAttribute('size', 'xs');
|
|
235
|
+
btn.setAttribute('icon', 'x');
|
|
236
|
+
btn.setAttribute('aria-label', `Remove ${a.name || 'attachment'}`);
|
|
233
237
|
btn.setAttribute('data-preview-remove', '');
|
|
234
238
|
btn.addEventListener('click', () => this.#removeAttachment(i));
|
|
235
239
|
wrap.appendChild(img);
|
|
@@ -61,6 +61,9 @@ description: |
|
|
|
61
61
|
autocomplete, trailing/leading controls), wrap inside
|
|
62
62
|
`<chat-composer>` — see ChatComposer for the module-tier shape.
|
|
63
63
|
|
|
64
|
+
composes:
|
|
65
|
+
- button-ui # send button + attachment-remove (gh issue 276 — no native <button> in stamped DOM)
|
|
66
|
+
|
|
64
67
|
props:
|
|
65
68
|
disabled:
|
|
66
69
|
description: |
|
|
@@ -123,7 +123,7 @@ check-ui[indeterminate] {
|
|
|
123
123
|
:scope[label]::after { content: attr(label); }
|
|
124
124
|
|
|
125
125
|
/* Focus */
|
|
126
|
-
:scope:focus-visible [slot="box"] { box-shadow: var(--check-focus-ring); }
|
|
126
|
+
:scope:focus-visible [slot="box"] { box-shadow: var(--check-focus-ring); border-color: transparent; }
|
|
127
127
|
|
|
128
128
|
/* Size handled by universal [size] attribute system. */
|
|
129
129
|
}
|
|
@@ -241,9 +241,7 @@ export class UICombobox extends UIFormElement {
|
|
|
241
241
|
aria-controls="${listboxId}"
|
|
242
242
|
aria-labelledby="${labelId}"
|
|
243
243
|
data-placeholder="${escapeHTML(this.placeholder || '')}"></span>
|
|
244
|
-
<button
|
|
245
|
-
<icon-ui name="x-circle"></icon-ui>
|
|
246
|
-
</button>
|
|
244
|
+
<button-ui variant="ghost" size="xs" data-clear aria-label="Clear" tabindex="-1" style="display:none"><icon-ui slot="icon" name="x-circle"></icon-ui></button-ui>
|
|
247
245
|
<span data-suffix>
|
|
248
246
|
<icon-ui name="caret-down"></icon-ui>
|
|
249
247
|
</span>
|
|
@@ -102,10 +102,12 @@
|
|
|
102
102
|
|
|
103
103
|
:scope:focus-within [data-field] {
|
|
104
104
|
box-shadow: var(--combobox-focus-ring);
|
|
105
|
+
border-color: transparent;
|
|
105
106
|
}
|
|
106
107
|
:scope[aria-invalid="true"]:focus-within [data-field],
|
|
107
108
|
:scope[error]:focus-within [data-field] {
|
|
108
109
|
box-shadow: var(--combobox-focus-ring-invalid);
|
|
110
|
+
border-color: transparent;
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
[data-prefix],
|
|
@@ -159,20 +161,15 @@
|
|
|
159
161
|
align-items: center;
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
/* Clear x — a stamped <button-ui variant="ghost" size="xs"> kept
|
|
165
|
+
mouse-only via tabindex="-1" (gh issue 276: no native <button> in
|
|
166
|
+
stamped DOM); colors/geometry ride button token re-points. */
|
|
162
167
|
[data-clear] {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
padding: 0;
|
|
167
|
-
margin: 0;
|
|
168
|
-
border: 0;
|
|
169
|
-
background: transparent;
|
|
170
|
-
color: var(--combobox-fg-muted);
|
|
171
|
-
cursor: pointer;
|
|
168
|
+
--button-fg: var(--combobox-fg-muted);
|
|
169
|
+
--button-fg-ghost-hover: var(--combobox-fg);
|
|
170
|
+
--button-height: calc(var(--combobox-font-size) + var(--a-space-1));
|
|
172
171
|
--a-icon-size: var(--combobox-font-size);
|
|
173
|
-
|
|
174
|
-
[data-clear]:hover {
|
|
175
|
-
color: var(--combobox-fg);
|
|
172
|
+
flex-shrink: 0;
|
|
176
173
|
}
|
|
177
174
|
}
|
|
178
175
|
|
|
@@ -20,6 +20,8 @@ description: |
|
|
|
20
20
|
# do NOT auto-import them. Consumer (or demo shell) must explicitly import.
|
|
21
21
|
composes:
|
|
22
22
|
- icon-ui # caret-down / x / magnifying-glass affixes (created in render)
|
|
23
|
+
- button-ui # clear-x affordance, mouse-only via tabindex=-1 (gh issue 276 — no native <button> in stamped DOM)
|
|
24
|
+
- spinner-ui # loading row in the stamped listbox
|
|
23
25
|
props:
|
|
24
26
|
name:
|
|
25
27
|
description: Form control name for form data submission
|
|
@@ -107,14 +107,18 @@ input-ui:not([disabled]) [slot="field"]:hover [slot="suffix"] {
|
|
|
107
107
|
/* hover rules moved outside @scope — see Safari 17.x bug note at top. */
|
|
108
108
|
:scope:not([disabled]):focus-within [slot="field"] {
|
|
109
109
|
/* Canonical ring — consumes the L3 --input-focus-ring token
|
|
110
|
-
which aliases --a-focus-ring.
|
|
111
|
-
|
|
110
|
+
which aliases --a-focus-ring. The border goes transparent while
|
|
111
|
+
the ring paints (operator ruling 2026-07-16): the ring alone is
|
|
112
|
+
the focus affordance; contrast obligations move to the ring
|
|
113
|
+
(--a-focus-color) rather than the border (WCAG 2.2 SC 2.4.11/2.4.13). */
|
|
112
114
|
box-shadow: var(--input-focus-ring);
|
|
113
115
|
color: var(--input-fg-focus);
|
|
116
|
+
border-color: transparent;
|
|
114
117
|
}
|
|
115
118
|
:scope[aria-invalid="true"]:not([disabled]):focus-within [slot="field"],
|
|
116
119
|
:scope[error]:not([disabled]):focus-within [slot="field"] {
|
|
117
120
|
box-shadow: var(--input-focus-ring-invalid);
|
|
121
|
+
border-color: transparent;
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
/* Text (contenteditable span) */
|
|
@@ -182,12 +182,11 @@
|
|
|
182
182
|
justify-content: center;
|
|
183
183
|
padding: 0;
|
|
184
184
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
185
|
+
/* (Removed two `--a-icon-size: --a-icon-size/--a-caret-size` lines —
|
|
186
|
+
both were missing var() so the value was the literal token NAME,
|
|
187
|
+
invalid at the icon's var() site: a silent no-op. The caret is
|
|
188
|
+
display:none in collapsed mode anyway; the icon keeps the
|
|
189
|
+
inherited --a-icon-size.) */
|
|
191
190
|
:scope {
|
|
192
191
|
align-self: stretch;
|
|
193
192
|
}
|
|
@@ -100,7 +100,7 @@ radio-ui[checked] [slot="dot"]::after {
|
|
|
100
100
|
:scope[label]::after { content: attr(label); }
|
|
101
101
|
|
|
102
102
|
/* Focus */
|
|
103
|
-
:scope:focus-visible [slot="dot"] { box-shadow: var(--radio-focus-ring); }
|
|
103
|
+
:scope:focus-visible [slot="dot"] { box-shadow: var(--radio-focus-ring); border-color: transparent; }
|
|
104
104
|
|
|
105
105
|
/* Size handled by universal [size] attribute system. */
|
|
106
106
|
}
|
|
@@ -79,10 +79,12 @@ range-ui:not([disabled]) [slot="field"]:hover [data-layer="fill"] {
|
|
|
79
79
|
:scope:focus-visible [slot="field"] {
|
|
80
80
|
/* Canonical ring via L3 token (see semantics.css FOCUS block). */
|
|
81
81
|
box-shadow: var(--range-focus-ring);
|
|
82
|
+
border-color: transparent;
|
|
82
83
|
}
|
|
83
84
|
:scope[aria-invalid="true"]:focus-visible [slot="field"],
|
|
84
85
|
:scope[error]:focus-visible [slot="field"] {
|
|
85
86
|
box-shadow: var(--range-focus-ring-invalid);
|
|
87
|
+
border-color: transparent;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
/* ── Dual-layer fill: identical layouts, overlay clipped to fill % ── */
|
|
@@ -442,9 +442,7 @@ export class UISelect extends UIFormElement {
|
|
|
442
442
|
${leading}
|
|
443
443
|
<span data-chips></span>
|
|
444
444
|
${displayMarkup}
|
|
445
|
-
<button
|
|
446
|
-
<icon-ui name="x"></icon-ui>
|
|
447
|
-
</button>
|
|
445
|
+
<button-ui variant="ghost" size="xs" icon="x" aria-label="Clear all" data-clear-all hidden></button-ui>
|
|
448
446
|
<icon-ui name="caret-down" slot="caret"></icon-ui>
|
|
449
447
|
</span>
|
|
450
448
|
${hintMarkup}
|
|
@@ -725,10 +723,11 @@ export class UISelect extends UIFormElement {
|
|
|
725
723
|
if (this.multiple && this.selectAll) {
|
|
726
724
|
const header = document.createElement('div');
|
|
727
725
|
header.setAttribute('data-select-all', '');
|
|
728
|
-
const btn = document.createElement('button');
|
|
729
|
-
btn.
|
|
726
|
+
const btn = document.createElement('button-ui');
|
|
727
|
+
btn.setAttribute('variant', 'ghost');
|
|
728
|
+
btn.setAttribute('size', 'xs');
|
|
730
729
|
btn.dataset.selectAllBtn = '';
|
|
731
|
-
btn.
|
|
730
|
+
btn.setAttribute('text', this.#allSelected() ? 'Clear' : 'Select all');
|
|
732
731
|
btn.addEventListener('click', this.#onSelectAllClick);
|
|
733
732
|
header.appendChild(btn);
|
|
734
733
|
this.#listbox.appendChild(header);
|
|
@@ -854,10 +853,11 @@ export class UISelect extends UIFormElement {
|
|
|
854
853
|
slot.appendChild(tag);
|
|
855
854
|
}
|
|
856
855
|
if (overflow > 0) {
|
|
857
|
-
const more = document.createElement('button');
|
|
858
|
-
more.
|
|
856
|
+
const more = document.createElement('button-ui');
|
|
857
|
+
more.setAttribute('variant', 'ghost');
|
|
858
|
+
more.setAttribute('size', 'xs');
|
|
859
859
|
more.dataset.more = '';
|
|
860
|
-
more.
|
|
860
|
+
more.setAttribute('text', `+${overflow} more`);
|
|
861
861
|
more.addEventListener('click', this.#onMoreClick);
|
|
862
862
|
slot.appendChild(more);
|
|
863
863
|
}
|
|
@@ -108,10 +108,12 @@
|
|
|
108
108
|
/* Canonical ring via L3 token (see semantics.css FOCUS block). */
|
|
109
109
|
box-shadow: var(--select-focus-ring);
|
|
110
110
|
color: var(--select-fg);
|
|
111
|
+
border-color: transparent;
|
|
111
112
|
}
|
|
112
113
|
:scope[aria-invalid="true"]:focus-visible [slot="trigger"],
|
|
113
114
|
:scope[error]:focus-visible [slot="trigger"] {
|
|
114
115
|
box-shadow: var(--select-focus-ring-invalid);
|
|
116
|
+
border-color: transparent;
|
|
115
117
|
}
|
|
116
118
|
:scope:focus-visible::before {
|
|
117
119
|
color: var(--select-fg-subtle);
|
|
@@ -167,6 +169,11 @@
|
|
|
167
169
|
:scope [slot="trigger"] [slot="caret"] {
|
|
168
170
|
color: var(--select-caret-fg);
|
|
169
171
|
flex-shrink: 0;
|
|
172
|
+
/* Universal caret size (14px default) — without this the caret rides
|
|
173
|
+
the ambient icon size instead (admin-shell's global 18px bump made
|
|
174
|
+
sidebar workspace-select carets outsized; ruling 2026-07-16: carets
|
|
175
|
+
read --a-caret-size everywhere, like nav-group / tree / accordion). */
|
|
176
|
+
--a-icon-size: var(--a-caret-size);
|
|
170
177
|
transition: rotate var(--a-duration-fast) var(--a-easing);
|
|
171
178
|
}
|
|
172
179
|
|
|
@@ -218,41 +225,31 @@
|
|
|
218
225
|
/* Chip styling tracks the active variant via tag-ui's own tokens. */
|
|
219
226
|
--tag-size: var(--a-ui-tiny);
|
|
220
227
|
}
|
|
228
|
+
/* "+N more" overflow pill — a stamped <button-ui variant="ghost" size="xs">
|
|
229
|
+
(gh issue 276: no native <button> in stamped DOM); pill shape + chip-row
|
|
230
|
+
height via button token re-points, chrome stays button-ui's own. */
|
|
221
231
|
:scope[data-multi-chips] [data-chips] [data-more] {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
padding: 0 var(--a-space-2);
|
|
228
|
-
font: inherit;
|
|
229
|
-
font-size: var(--a-ui-tiny);
|
|
230
|
-
line-height: calc(var(--a-size) - var(--a-space-3));
|
|
231
|
-
cursor: pointer;
|
|
232
|
-
}
|
|
233
|
-
:scope[data-multi-chips] [data-chips] [data-more]:hover {
|
|
234
|
-
background: var(--a-bg-hover);
|
|
232
|
+
--button-radius: var(--a-radius-full);
|
|
233
|
+
--button-height: calc(var(--a-size) - var(--a-space-3));
|
|
234
|
+
--button-fg: var(--select-fg-muted);
|
|
235
|
+
|
|
236
|
+
flex-shrink: 0;
|
|
235
237
|
}
|
|
236
238
|
|
|
237
|
-
/* Clear-all `x` affordance
|
|
239
|
+
/* Clear-all `x` affordance — stamped <button-ui variant="ghost" size="xs"
|
|
240
|
+
icon="x"> (gh issue 276). Visible only when [clearable] AND chips. */
|
|
238
241
|
:scope[data-multi-chips] [data-clear-all] {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
padding: var(--a-space-0);
|
|
243
|
-
margin-inline-start: var(--a-space-1);
|
|
244
|
-
color: var(--select-fg-muted);
|
|
245
|
-
cursor: pointer;
|
|
246
|
-
border-radius: var(--a-radius-full);
|
|
242
|
+
--button-radius: var(--a-radius-full);
|
|
243
|
+
--button-height: calc(var(--a-size) - var(--a-space-3));
|
|
244
|
+
--button-fg: var(--select-fg-muted);
|
|
247
245
|
--a-icon-size: var(--a-ui-sm);
|
|
248
|
-
|
|
249
|
-
|
|
246
|
+
|
|
247
|
+
margin-inline-start: var(--a-space-1);
|
|
248
|
+
flex-shrink: 0;
|
|
250
249
|
}
|
|
250
|
+
/* button-ui's own :scope display wins over the UA [hidden] rule —
|
|
251
|
+
keep the explicit guard. */
|
|
251
252
|
:scope[data-multi-chips] [data-clear-all][hidden] { display: none; }
|
|
252
|
-
:scope[data-multi-chips] [data-clear-all]:hover {
|
|
253
|
-
color: var(--select-fg);
|
|
254
|
-
background: var(--a-bg-hover);
|
|
255
|
-
}
|
|
256
253
|
|
|
257
254
|
/* In multi-select with chips present, the trigger display element is
|
|
258
255
|
the rest-of-row slot — make it flex-grow but not push chips off. */
|
|
@@ -429,19 +426,11 @@ select-ui [slot="listbox"] [data-select-all] {
|
|
|
429
426
|
border-bottom: 1px solid var(--md-sys-color-neutral-outline-variant);
|
|
430
427
|
margin-bottom: var(--a-space-1);
|
|
431
428
|
}
|
|
429
|
+
/* Select-all / Clear — stamped <button-ui variant="ghost" size="xs">
|
|
430
|
+
(gh issue 276); link-colored via the button fg token, chrome is button-ui's. */
|
|
432
431
|
select-ui [slot="listbox"] [data-select-all-btn] {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
border: none;
|
|
436
|
-
font: inherit;
|
|
437
|
-
font-size: var(--a-ui-xs);
|
|
438
|
-
color: var(--md-sys-color-primary);
|
|
439
|
-
cursor: pointer;
|
|
440
|
-
padding: var(--a-space-1) var(--a-space-2);
|
|
441
|
-
border-radius: var(--a-radius-sm);
|
|
442
|
-
}
|
|
443
|
-
select-ui [slot="listbox"] [data-select-all-btn]:hover {
|
|
444
|
-
background: var(--a-bg-hover);
|
|
432
|
+
--button-fg: var(--md-sys-color-primary);
|
|
433
|
+
--button-radius: var(--a-radius-sm);
|
|
445
434
|
}
|
|
446
435
|
|
|
447
436
|
/* Multi-select option rows — leading checkbox indicator + label.
|
|
@@ -272,7 +272,9 @@ describe('select-ui', () => {
|
|
|
272
272
|
expect(chips.length).toBe(2);
|
|
273
273
|
const more = s.querySelector('[slot="trigger"] [data-more]');
|
|
274
274
|
expect(more).toBeTruthy();
|
|
275
|
-
|
|
275
|
+
// Stamped as <button-ui> (gh issue 276) — label rides the text attr.
|
|
276
|
+
expect(more.tagName).toBe('BUTTON-UI');
|
|
277
|
+
expect(more.getAttribute('text')).toBe('+2 more');
|
|
276
278
|
});
|
|
277
279
|
|
|
278
280
|
it('multi-select: [clearable] reveals the clear-all button when chips present', async () => {
|
|
@@ -300,7 +302,9 @@ describe('select-ui', () => {
|
|
|
300
302
|
expect(header).toBeTruthy();
|
|
301
303
|
const btn = header.querySelector('[data-select-all-btn]');
|
|
302
304
|
expect(btn).toBeTruthy();
|
|
303
|
-
|
|
305
|
+
// Stamped as <button-ui> (gh issue 276) — label rides the text attr.
|
|
306
|
+
expect(btn.tagName).toBe('BUTTON-UI');
|
|
307
|
+
expect(btn.getAttribute('text')).toBe('Select all');
|
|
304
308
|
});
|
|
305
309
|
|
|
306
310
|
it('multi-select: chip remove event empties that selection from value', async () => {
|
|
@@ -25,6 +25,7 @@ description: |
|
|
|
25
25
|
composes:
|
|
26
26
|
- icon-ui # caret + option-row affixes (created in render)
|
|
27
27
|
- tag-ui # multi-select chip per selected option in the trigger
|
|
28
|
+
- button-ui # clear-all / select-all / +N-overflow affordances (gh issue 276 — no native <button> in stamped DOM)
|
|
28
29
|
props:
|
|
29
30
|
name:
|
|
30
31
|
description: Form control name for form data submission
|
|
@@ -100,7 +100,9 @@
|
|
|
100
100
|
"x-adiaui": {
|
|
101
101
|
"anti_patterns": [],
|
|
102
102
|
"category": "display",
|
|
103
|
-
"composes": [
|
|
103
|
+
"composes": [
|
|
104
|
+
"button-ui"
|
|
105
|
+
],
|
|
104
106
|
"events": {
|
|
105
107
|
"select": {
|
|
106
108
|
"description": "Fired when a selectable swatch is activated (click / Enter / Space). detail carries the swatch's value, color, and label.",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
* <span data-badge></span> gamut / contrast marker (optional)
|
|
64
64
|
* <span data-label></span> label text
|
|
65
65
|
* <span data-detail></span> secondary line (optional)
|
|
66
|
-
* <button data-copy></button>
|
|
66
|
+
* <button-ui data-copy></button-ui> copy-to-clipboard (optional)
|
|
67
67
|
*
|
|
68
68
|
* Accessibility:
|
|
69
69
|
* - When [selectable], host is role="button" + tabindex="0";
|
|
@@ -380,12 +380,15 @@ export class UISwatch extends UIElement {
|
|
|
380
380
|
this.#detailEl.setAttribute('hidden', '');
|
|
381
381
|
this.appendChild(this.#detailEl);
|
|
382
382
|
|
|
383
|
-
|
|
383
|
+
// button-ui, not a native <button> (gh issue 276) — the ⧉/✓/⚠ glyph
|
|
384
|
+
// rides the text attribute (button-ui renders it via attr(text)).
|
|
385
|
+
this.#copyEl = document.createElement('button-ui');
|
|
384
386
|
this.#copyEl.setAttribute('data-copy', '');
|
|
385
|
-
this.#copyEl.setAttribute('
|
|
387
|
+
this.#copyEl.setAttribute('variant', 'ghost');
|
|
388
|
+
this.#copyEl.setAttribute('size', 'xs');
|
|
386
389
|
this.#copyEl.setAttribute('aria-label', 'Copy value');
|
|
387
390
|
this.#copyEl.setAttribute('hidden', '');
|
|
388
|
-
this.#copyEl.
|
|
391
|
+
this.#copyEl.setAttribute('text', '⧉');
|
|
389
392
|
this.appendChild(this.#copyEl);
|
|
390
393
|
|
|
391
394
|
this.#stamped = true;
|
|
@@ -610,8 +613,11 @@ export class UISwatch extends UIElement {
|
|
|
610
613
|
this.#onHostKey = (e) => {
|
|
611
614
|
if (!this.selectable) return;
|
|
612
615
|
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
613
|
-
// Don't steal Space when focus is in the copy button
|
|
614
|
-
|
|
616
|
+
// Don't steal Space when focus is in the copy button (the focused
|
|
617
|
+
// element is button-ui's INNER control, so identity-compare via the
|
|
618
|
+
// event path, not e.target).
|
|
619
|
+
const keyPath = e.composedPath ? e.composedPath() : [e.target];
|
|
620
|
+
if (keyPath.includes(this.#copyEl)) return;
|
|
615
621
|
e.preventDefault();
|
|
616
622
|
this.#activate();
|
|
617
623
|
};
|
|
@@ -660,11 +666,11 @@ export class UISwatch extends UIElement {
|
|
|
660
666
|
if (!this.#copyEl) return;
|
|
661
667
|
if (this.#copyResetTimer != null) clearTimeout(this.#copyResetTimer);
|
|
662
668
|
this.#copyEl.setAttribute('data-copy-state', ok ? 'ok' : 'fail');
|
|
663
|
-
this.#copyEl.
|
|
669
|
+
this.#copyEl.setAttribute('text', ok ? '✓' : '⚠');
|
|
664
670
|
this.#copyResetTimer = setTimeout(() => {
|
|
665
671
|
if (!this.#copyEl) return;
|
|
666
672
|
this.#copyEl.removeAttribute('data-copy-state');
|
|
667
|
-
this.#copyEl.
|
|
673
|
+
this.#copyEl.setAttribute('text', '⧉');
|
|
668
674
|
this.#copyResetTimer = null;
|
|
669
675
|
}, 1200);
|
|
670
676
|
}
|