@adia-ai/web-components 0.8.3 → 0.8.5
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 +33 -0
- package/USAGE.md +60 -25
- package/components/accordion/accordion.css +5 -0
- package/components/adia-mark/adia-mark.a2ui.json +123 -0
- package/components/adia-mark/adia-mark.class.js +64 -0
- package/components/adia-mark/adia-mark.css +48 -0
- package/components/adia-mark/adia-mark.d.ts +20 -0
- package/components/adia-mark/adia-mark.examples.md +26 -0
- package/components/adia-mark/adia-mark.js +17 -0
- package/components/adia-mark/adia-mark.test.js +69 -0
- package/components/adia-mark/adia-mark.yaml +121 -0
- package/components/agent-artifact/agent-artifact.css +1 -0
- package/components/agent-questions/agent-questions.a2ui.json +2 -1
- package/components/agent-questions/agent-questions.class.js +96 -53
- package/components/agent-questions/agent-questions.css +27 -106
- package/components/agent-questions/agent-questions.yaml +1 -0
- package/components/agent-reasoning/agent-reasoning.css +9 -5
- package/components/button/button.class.js +9 -2
- package/components/button/button.css +1 -1
- package/components/calendar-grid/calendar-grid.a2ui.json +3 -1
- package/components/calendar-grid/calendar-grid.class.js +11 -6
- package/components/calendar-grid/calendar-grid.css +42 -20
- package/components/calendar-grid/calendar-grid.yaml +5 -0
- package/components/calendar-picker/calendar-picker.a2ui.json +3 -1
- package/components/calendar-picker/calendar-picker.class.js +11 -6
- package/components/calendar-picker/calendar-picker.css +41 -19
- package/components/calendar-picker/calendar-picker.yaml +5 -0
- package/components/chart/chart.class.js +17 -14
- 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/color-input/color-input.class.js +8 -5
- package/components/color-picker/color-picker.class.js +6 -3
- 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/field/field.class.js +10 -7
- package/components/fields/fields.class.js +15 -12
- package/components/icon/icon.class.js +9 -5
- package/components/index.js +1 -0
- package/components/input/input.css +6 -2
- package/components/nav/nav.class.js +2 -1
- package/components/nav-group/nav-group.css +5 -6
- package/components/noodles/noodles.class.js +14 -10
- package/components/option-card/option-card.css +1 -0
- package/components/page/page.class.js +3 -2
- 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 +12 -11
- 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 +41 -32
- package/components/swatch/swatch.css +13 -18
- package/components/swatch/swatch.yaml +2 -0
- package/components/swiper/swiper.class.js +4 -1
- package/components/swiper/swiper.css +13 -4
- 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/toolbar/toolbar.class.js +11 -6
- package/components/tree/tree.class.js +5 -2
- package/components/tree/tree.css +5 -2
- package/components/upload/upload.css +1 -0
- package/core/data-stream.js +21 -15
- package/core/element.js +36 -1
- package/core/element.test.js +67 -0
- package/core/icons-phosphor.js +86 -22
- package/core/icons.js +22 -8
- package/dist/theme-provider.min.js +2 -2
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +109 -108
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -5
- package/styles/colors/semantics/features.css +6 -1
- package/styles/components.css +1 -0
- package/traits/error-shake/error-shake.js +23 -17
- package/traits/fade-presence/fade-presence.js +19 -15
- package/traits/success-checkmark/success-checkmark.js +21 -16
|
@@ -51,19 +51,22 @@ export class UIFields extends UIElement {
|
|
|
51
51
|
|
|
52
52
|
connected() {
|
|
53
53
|
this.#syncInline();
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
// gh#285 — SSR DOM shims (linkedom) have no MutationObserver global.
|
|
55
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
56
|
+
this.#mo = new MutationObserver((records) => {
|
|
57
|
+
// Only re-sync on childList changes that involve direct <field-ui>
|
|
58
|
+
// children — attribute mutations on grandchildren aren't ours to
|
|
59
|
+
// care about.
|
|
60
|
+
for (const r of records) {
|
|
61
|
+
if (r.type !== 'childList') continue;
|
|
62
|
+
if (this.#hasFieldChild(r.addedNodes) || this.#hasFieldChild(r.removedNodes)) {
|
|
63
|
+
this.#syncInline();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
63
66
|
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
});
|
|
68
|
+
this.#mo.observe(this, { childList: true });
|
|
69
|
+
}
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
disconnected() {
|
|
@@ -30,13 +30,17 @@ function warnIfIconRegistryUnwired() {
|
|
|
30
30
|
// eslint-disable-next-line no-console
|
|
31
31
|
console.error(
|
|
32
32
|
`[icon-ui] No icons are registered and no icon loaders were installed — every <icon-ui> renders blank.\n` +
|
|
33
|
-
`
|
|
34
|
-
`
|
|
33
|
+
` Two known causes (gh#287): (1) Vite's dev-server optimizeDeps pre-bundled this package, which empties\n` +
|
|
34
|
+
` the auto-discovery glob — add to your Vite config:\n` +
|
|
35
|
+
` optimizeDeps: { exclude: ['@adia-ai/web-components', '@adia-ai/web-modules'] }\n` +
|
|
36
|
+
` (2) @phosphor-icons/core is missing or this file was re-nested by a packaging step (Yarn PnP, a custom\n` +
|
|
37
|
+
` bundler). If neither applies, install loaders manually with plain imports from your APP entry:\n` +
|
|
35
38
|
` npm i @phosphor-icons/core\n` +
|
|
36
39
|
` import { installIconLoaders } from '@adia-ai/web-components/core/icons';\n` +
|
|
37
|
-
`
|
|
38
|
-
`
|
|
39
|
-
`
|
|
40
|
+
` import caretRight from '@phosphor-icons/core/assets/regular/caret-right.svg?raw';\n` +
|
|
41
|
+
` installIconLoaders({ regular: {\n` +
|
|
42
|
+
` '/node_modules/@phosphor-icons/core/assets/regular/caret-right.svg': caretRight,\n` +
|
|
43
|
+
` } });\n` +
|
|
40
44
|
` See USAGE.md § Icons.`,
|
|
41
45
|
);
|
|
42
46
|
}, 0);
|
package/components/index.js
CHANGED
|
@@ -58,6 +58,7 @@ export { UISwiper } from './swiper/swiper.js';
|
|
|
58
58
|
export { UIUpload } from './upload/upload.js';
|
|
59
59
|
export { UICard } from './card/card.js';
|
|
60
60
|
export { UIAvatar, UIAvatarGroup } from './avatar/avatar.js';
|
|
61
|
+
export { UIAdiaMark } from './adia-mark/adia-mark.js';
|
|
61
62
|
export { UIProgress } from './progress/progress.js';
|
|
62
63
|
export { UIStepProgress } from './step-progress/step-progress.js';
|
|
63
64
|
export { UISkeleton } from './skeleton/skeleton.js';
|
|
@@ -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) */
|
|
@@ -61,7 +61,8 @@ export class UINav extends UIElement {
|
|
|
61
61
|
this.addEventListener('click', this.#onClick);
|
|
62
62
|
|
|
63
63
|
// ResizeObserver only meaningful for primary variant — section is static.
|
|
64
|
-
|
|
64
|
+
// gh#285 — SSR DOM shims (linkedom) have no ResizeObserver global.
|
|
65
|
+
if (this.variant !== 'section' && typeof ResizeObserver !== 'undefined') {
|
|
65
66
|
this.#ro = new ResizeObserver(() => this.#updateTooltips());
|
|
66
67
|
this.#ro.observe(this);
|
|
67
68
|
}
|
|
@@ -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
|
}
|
|
@@ -108,19 +108,23 @@ export class UINoodles extends UIElement {
|
|
|
108
108
|
this.#svg.setAttribute('data-noodle-svg', '');
|
|
109
109
|
this.prepend(this.#svg);
|
|
110
110
|
|
|
111
|
-
// Observers for child layout changes
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
this.#
|
|
115
|
-
this.#
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
// Observers for child layout changes.
|
|
112
|
+
// gh#285 — SSR DOM shims (linkedom) have neither observer global.
|
|
113
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
114
|
+
this.#resizeObs = new ResizeObserver(() => this.#scheduleUpdate());
|
|
115
|
+
this.#resizeObs.observe(this);
|
|
116
|
+
}
|
|
117
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
118
|
+
this.#mutationObs = new MutationObserver(() => {
|
|
119
|
+
this.#rebuildPortIndicators();
|
|
120
|
+
this.#scheduleUpdate();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
119
123
|
this.#observeMutations();
|
|
120
124
|
|
|
121
125
|
// Observe each port-bearing child
|
|
122
126
|
for (const child of this.#getPortChildren()) {
|
|
123
|
-
this.#resizeObs
|
|
127
|
+
this.#resizeObs?.observe(child);
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
this.#rebuildPortIndicators();
|
|
@@ -181,7 +185,7 @@ export class UINoodles extends UIElement {
|
|
|
181
185
|
// Sync resize observer with current port children
|
|
182
186
|
const portChildren = this.#getPortChildren();
|
|
183
187
|
for (const child of portChildren) {
|
|
184
|
-
this.#resizeObs
|
|
188
|
+
this.#resizeObs?.observe(child);
|
|
185
189
|
}
|
|
186
190
|
|
|
187
191
|
// Update port indicator positions
|
|
@@ -78,13 +78,14 @@ export class UIPage extends UIElement {
|
|
|
78
78
|
header.insertAdjacentElement('beforebegin', this.#sentinel);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
// gh#285 — SSR DOM shims (linkedom) have no IntersectionObserver global.
|
|
82
|
+
if (!this.#observer && typeof IntersectionObserver !== 'undefined') {
|
|
82
83
|
this.#observer = new IntersectionObserver((entries) => {
|
|
83
84
|
this.toggleAttribute('data-header-stuck', !entries[0].isIntersecting);
|
|
84
85
|
}, { threshold: 0 });
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
this.#observer
|
|
88
|
+
this.#observer?.observe(this.#sentinel);
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
#teardownSticky() {
|
|
@@ -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 % ── */
|
|
@@ -381,7 +381,8 @@ export class UISelect extends UIFormElement {
|
|
|
381
381
|
// options are already parsed/set, and skip unless there's a real
|
|
382
382
|
// <option>/<optgroup> to parse — so the component's own listbox/empty-state
|
|
383
383
|
// renders (and #parseOptions's own option removals) don't re-enter or loop.
|
|
384
|
-
|
|
384
|
+
// gh#285 — SSR DOM shims (linkedom) have no MutationObserver global.
|
|
385
|
+
if (!this.#optionObserver && typeof MutationObserver !== 'undefined') {
|
|
385
386
|
this.#optionObserver = new MutationObserver(() => {
|
|
386
387
|
if (this.#options.length) return;
|
|
387
388
|
const kids = this.#logicalOptionChildren();
|
|
@@ -394,7 +395,7 @@ export class UISelect extends UIFormElement {
|
|
|
394
395
|
}
|
|
395
396
|
});
|
|
396
397
|
}
|
|
397
|
-
this.#optionObserver
|
|
398
|
+
this.#optionObserver?.observe(this, { childList: true, subtree: true });
|
|
398
399
|
}
|
|
399
400
|
|
|
400
401
|
render() {
|
|
@@ -442,9 +443,7 @@ export class UISelect extends UIFormElement {
|
|
|
442
443
|
${leading}
|
|
443
444
|
<span data-chips></span>
|
|
444
445
|
${displayMarkup}
|
|
445
|
-
<button
|
|
446
|
-
<icon-ui name="x"></icon-ui>
|
|
447
|
-
</button>
|
|
446
|
+
<button-ui variant="ghost" size="xs" icon="x" aria-label="Clear all" data-clear-all hidden></button-ui>
|
|
448
447
|
<icon-ui name="caret-down" slot="caret"></icon-ui>
|
|
449
448
|
</span>
|
|
450
449
|
${hintMarkup}
|
|
@@ -725,10 +724,11 @@ export class UISelect extends UIFormElement {
|
|
|
725
724
|
if (this.multiple && this.selectAll) {
|
|
726
725
|
const header = document.createElement('div');
|
|
727
726
|
header.setAttribute('data-select-all', '');
|
|
728
|
-
const btn = document.createElement('button');
|
|
729
|
-
btn.
|
|
727
|
+
const btn = document.createElement('button-ui');
|
|
728
|
+
btn.setAttribute('variant', 'ghost');
|
|
729
|
+
btn.setAttribute('size', 'xs');
|
|
730
730
|
btn.dataset.selectAllBtn = '';
|
|
731
|
-
btn.
|
|
731
|
+
btn.setAttribute('text', this.#allSelected() ? 'Clear' : 'Select all');
|
|
732
732
|
btn.addEventListener('click', this.#onSelectAllClick);
|
|
733
733
|
header.appendChild(btn);
|
|
734
734
|
this.#listbox.appendChild(header);
|
|
@@ -854,10 +854,11 @@ export class UISelect extends UIFormElement {
|
|
|
854
854
|
slot.appendChild(tag);
|
|
855
855
|
}
|
|
856
856
|
if (overflow > 0) {
|
|
857
|
-
const more = document.createElement('button');
|
|
858
|
-
more.
|
|
857
|
+
const more = document.createElement('button-ui');
|
|
858
|
+
more.setAttribute('variant', 'ghost');
|
|
859
|
+
more.setAttribute('size', 'xs');
|
|
859
860
|
more.dataset.more = '';
|
|
860
|
-
more.
|
|
861
|
+
more.setAttribute('text', `+${overflow} more`);
|
|
861
862
|
more.addEventListener('click', this.#onMoreClick);
|
|
862
863
|
slot.appendChild(more);
|
|
863
864
|
}
|
|
@@ -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";
|
|
@@ -208,31 +208,34 @@ export class UISwatch extends UIElement {
|
|
|
208
208
|
// the canonical post-tile region. MutationObserver closes this gap for
|
|
209
209
|
// every arrival path: single-template ordering skew, separate render
|
|
210
210
|
// cycle, or imperative host.appendChild().
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
n.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
211
|
+
// gh#285 — SSR DOM shims (linkedom) have no MutationObserver global.
|
|
212
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
213
|
+
this.#chromeObserver = new MutationObserver((mutations) => {
|
|
214
|
+
let hasChrome = false;
|
|
215
|
+
for (const m of mutations) {
|
|
216
|
+
for (const n of m.addedNodes) {
|
|
217
|
+
if (n.nodeType !== 1) continue;
|
|
218
|
+
// Direct chrome child OR a display:contents wrapper-span carrying chrome.
|
|
219
|
+
if (n.getAttribute?.('slot') === 'chrome') { hasChrome = true; break; }
|
|
220
|
+
if (
|
|
221
|
+
n.tagName === 'SPAN' &&
|
|
222
|
+
n.style?.display === 'contents' &&
|
|
223
|
+
n.querySelector?.('[slot="chrome"]')
|
|
224
|
+
) { hasChrome = true; break; }
|
|
225
|
+
}
|
|
226
|
+
if (hasChrome) break;
|
|
223
227
|
}
|
|
224
|
-
if (hasChrome)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
this.#chromeObserver.
|
|
234
|
-
}
|
|
235
|
-
this.#chromeObserver.observe(this, { childList: true });
|
|
228
|
+
if (!hasChrome) return;
|
|
229
|
+
// Re-entrancy guard: #absorbChromeSlot() calls insertBefore() which itself
|
|
230
|
+
// generates childList mutations on the host. Without this guard, every
|
|
231
|
+
// absorb pass would re-trigger the observer indefinitely. takeRecords()
|
|
232
|
+
// drains the queue without dispatch so the next observation cycle starts
|
|
233
|
+
// clean.
|
|
234
|
+
this.#absorbChromeSlot();
|
|
235
|
+
this.#chromeObserver.takeRecords();
|
|
236
|
+
});
|
|
237
|
+
this.#chromeObserver.observe(this, { childList: true });
|
|
238
|
+
}
|
|
236
239
|
}
|
|
237
240
|
|
|
238
241
|
render() {
|
|
@@ -380,12 +383,15 @@ export class UISwatch extends UIElement {
|
|
|
380
383
|
this.#detailEl.setAttribute('hidden', '');
|
|
381
384
|
this.appendChild(this.#detailEl);
|
|
382
385
|
|
|
383
|
-
|
|
386
|
+
// button-ui, not a native <button> (gh issue 276) — the ⧉/✓/⚠ glyph
|
|
387
|
+
// rides the text attribute (button-ui renders it via attr(text)).
|
|
388
|
+
this.#copyEl = document.createElement('button-ui');
|
|
384
389
|
this.#copyEl.setAttribute('data-copy', '');
|
|
385
|
-
this.#copyEl.setAttribute('
|
|
390
|
+
this.#copyEl.setAttribute('variant', 'ghost');
|
|
391
|
+
this.#copyEl.setAttribute('size', 'xs');
|
|
386
392
|
this.#copyEl.setAttribute('aria-label', 'Copy value');
|
|
387
393
|
this.#copyEl.setAttribute('hidden', '');
|
|
388
|
-
this.#copyEl.
|
|
394
|
+
this.#copyEl.setAttribute('text', '⧉');
|
|
389
395
|
this.appendChild(this.#copyEl);
|
|
390
396
|
|
|
391
397
|
this.#stamped = true;
|
|
@@ -610,8 +616,11 @@ export class UISwatch extends UIElement {
|
|
|
610
616
|
this.#onHostKey = (e) => {
|
|
611
617
|
if (!this.selectable) return;
|
|
612
618
|
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
613
|
-
// Don't steal Space when focus is in the copy button
|
|
614
|
-
|
|
619
|
+
// Don't steal Space when focus is in the copy button (the focused
|
|
620
|
+
// element is button-ui's INNER control, so identity-compare via the
|
|
621
|
+
// event path, not e.target).
|
|
622
|
+
const keyPath = e.composedPath ? e.composedPath() : [e.target];
|
|
623
|
+
if (keyPath.includes(this.#copyEl)) return;
|
|
615
624
|
e.preventDefault();
|
|
616
625
|
this.#activate();
|
|
617
626
|
};
|
|
@@ -660,11 +669,11 @@ export class UISwatch extends UIElement {
|
|
|
660
669
|
if (!this.#copyEl) return;
|
|
661
670
|
if (this.#copyResetTimer != null) clearTimeout(this.#copyResetTimer);
|
|
662
671
|
this.#copyEl.setAttribute('data-copy-state', ok ? 'ok' : 'fail');
|
|
663
|
-
this.#copyEl.
|
|
672
|
+
this.#copyEl.setAttribute('text', ok ? '✓' : '⚠');
|
|
664
673
|
this.#copyResetTimer = setTimeout(() => {
|
|
665
674
|
if (!this.#copyEl) return;
|
|
666
675
|
this.#copyEl.removeAttribute('data-copy-state');
|
|
667
|
-
this.#copyEl.
|
|
676
|
+
this.#copyEl.setAttribute('text', '⧉');
|
|
668
677
|
this.#copyResetTimer = null;
|
|
669
678
|
}, 1200);
|
|
670
679
|
}
|
|
@@ -288,29 +288,24 @@
|
|
|
288
288
|
color: color-mix(in oklab, var(--a-chrome-dark, #111) 70%, transparent);
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
/* ═══════ Copy button
|
|
291
|
+
/* ═══════ Copy button — a stamped <button-ui variant="ghost" size="xs">
|
|
292
|
+
(gh issue 276: no native <button> in stamped DOM). Geometry/colors
|
|
293
|
+
ride button token re-points; focus ring is button-ui's own. ═══════ */
|
|
292
294
|
:scope > [data-copy] {
|
|
293
|
-
|
|
294
|
-
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
--button-fg: var(--swatch-copy-fg);
|
|
296
|
+
--button-fg-ghost-hover: var(--md-sys-color-neutral-on-surface);
|
|
297
|
+
--button-bg-ghost-hover: var(--a-bg-muted);
|
|
298
|
+
--button-radius: var(--a-radius-xs);
|
|
299
|
+
--button-height: calc(var(--a-ui-sm) + var(--a-space-1));
|
|
300
|
+
|
|
298
301
|
margin-inline-start: var(--a-space-1);
|
|
299
|
-
color: var(--swatch-copy-fg);
|
|
300
302
|
font-size: var(--a-ui-sm);
|
|
301
|
-
|
|
302
|
-
cursor: pointer;
|
|
303
|
-
border-radius: var(--a-radius-xs);
|
|
304
|
-
transition: color var(--a-duration-fast) var(--a-easing-out), background var(--a-duration-fast) var(--a-easing-out);
|
|
305
|
-
}
|
|
306
|
-
:scope > [data-copy]:hover { color: var(--md-sys-color-neutral-on-surface); background: var(--a-bg-muted); }
|
|
307
|
-
:scope > [data-copy]:focus-visible {
|
|
308
|
-
outline: var(--swatch-select-ring-width) solid var(--swatch-select-ring);
|
|
309
|
-
outline-offset: 1px;
|
|
303
|
+
flex-shrink: 0;
|
|
310
304
|
}
|
|
305
|
+
/* button-ui's own :scope display wins over the UA [hidden] rule. */
|
|
311
306
|
:scope > [data-copy][hidden] { display: none; }
|
|
312
|
-
:scope > [data-copy][data-copy-state="ok"] {
|
|
313
|
-
:scope > [data-copy][data-copy-state="fail"] {
|
|
307
|
+
:scope > [data-copy][data-copy-state="ok"] { --button-fg: var(--swatch-copy-fg-ok); }
|
|
308
|
+
:scope > [data-copy][data-copy-state="fail"] { --button-fg: var(--swatch-copy-fg-fail); }
|
|
314
309
|
|
|
315
310
|
/* ═══════ Selectable + selected states ═══════
|
|
316
311
|
[selectable] makes the whole host focusable; ring on :focus-visible.
|
|
@@ -11,6 +11,8 @@ description: |
|
|
|
11
11
|
shape; pairs with an optional label slot or attribute. Composed by chart-
|
|
12
12
|
legend-ui's per-row swatch and consumed directly by the token-colors /
|
|
13
13
|
spacing demo pages.
|
|
14
|
+
composes:
|
|
15
|
+
- button-ui # [copyable] copy affordance (gh issue 276 — no native <button> in stamped DOM)
|
|
14
16
|
props:
|
|
15
17
|
shape:
|
|
16
18
|
description: Visual shape — block (filled tile), dot (small circle), square (small filled square), line (solid hairline), dashed (dashed hairline).
|
|
@@ -436,7 +436,10 @@ export class UISwiper extends UIElement {
|
|
|
436
436
|
if (dots.children.length !== desired) {
|
|
437
437
|
while (dots.firstChild) dots.removeChild(dots.firstChild);
|
|
438
438
|
for (let i = 0; i < desired; i++) {
|
|
439
|
-
|
|
439
|
+
// button-ui, not a native <button> (gh issue 276 wave 4) — it
|
|
440
|
+
// respects the consumer-set role, so the tab semantics survive.
|
|
441
|
+
const dot = document.createElement('button-ui');
|
|
442
|
+
dot.setAttribute('variant', 'ghost');
|
|
440
443
|
dot.setAttribute('role', 'tab');
|
|
441
444
|
dot.setAttribute('aria-label', `Page ${i + 1}`);
|
|
442
445
|
dot.addEventListener('click', () => this.goTo(this.#pageToSlideIndex(i)));
|