@adia-ai/web-components 0.8.4 → 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.
Files changed (53) hide show
  1. package/CHANGELOG.md +18 -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/fields/fields.class.js +15 -12
  29. package/components/icon/icon.class.js +9 -5
  30. package/components/index.js +1 -0
  31. package/components/nav/nav.class.js +2 -1
  32. package/components/noodles/noodles.class.js +14 -10
  33. package/components/page/page.class.js +3 -2
  34. package/components/select/select.class.js +3 -2
  35. package/components/swatch/swatch.class.js +27 -24
  36. package/components/swiper/swiper.class.js +4 -1
  37. package/components/swiper/swiper.css +13 -4
  38. package/components/toolbar/toolbar.class.js +11 -6
  39. package/components/tree/tree.class.js +5 -2
  40. package/core/data-stream.js +21 -15
  41. package/core/element.js +36 -1
  42. package/core/element.test.js +67 -0
  43. package/core/icons-phosphor.js +86 -22
  44. package/core/icons.js +22 -8
  45. package/dist/theme-provider.min.js +2 -2
  46. package/dist/web-components.min.css +1 -1
  47. package/dist/web-components.min.js +107 -102
  48. package/dist/web-components.sheet.js +1 -1
  49. package/package.json +1 -5
  50. package/styles/components.css +1 -0
  51. package/traits/error-shake/error-shake.js +23 -17
  52. package/traits/fade-presence/fade-presence.js +19 -15
  53. package/traits/success-checkmark/success-checkmark.js +21 -16
@@ -232,14 +232,22 @@ calendar-picker-ui [data-cal-title] {
232
232
  color: var(--calendar-picker-title-fg);
233
233
  }
234
234
 
235
- calendar-picker-ui [data-cal-prev],
236
- calendar-picker-ui [data-cal-next] {
235
+ /* Stamped <button-ui variant="ghost" size="xs" icon="caret-…"> (gh issue 276
236
+ wave 4) — icon-only (no [text], no child text) so button-ui's own
237
+ [data-icon-only] rule already squares it to --button-height; pointing
238
+ --button-height at the nav size (not just literal height below) avoids
239
+ the min-width/min-height floor trap. Literal background/color below
240
+ out-specify button.css's own via the `button-ui` type-selector bump
241
+ (same technique as swiper's dots). */
242
+ calendar-picker-ui button-ui[data-cal-prev],
243
+ calendar-picker-ui button-ui[data-cal-next] {
244
+ --button-height: var(--calendar-picker-nav-size);
245
+ --a-icon-size: var(--calendar-picker-nav-icon-size);
246
+
237
247
  display: inline-flex;
238
248
  align-items: center;
239
249
  justify-content: center;
240
- width: var(--calendar-picker-nav-size);
241
250
  height: var(--calendar-picker-nav-size);
242
- padding: 0;
243
251
  border: none;
244
252
  border-radius: var(--calendar-picker-nav-radius);
245
253
  background: var(--calendar-picker-nav-bg);
@@ -248,10 +256,9 @@ calendar-picker-ui [data-cal-next] {
248
256
  transition:
249
257
  background var(--calendar-picker-duration) var(--calendar-picker-easing),
250
258
  color var(--calendar-picker-duration) var(--calendar-picker-easing);
251
- --a-icon-size: var(--calendar-picker-nav-icon-size);
252
259
  }
253
- calendar-picker-ui [data-cal-prev]:hover,
254
- calendar-picker-ui [data-cal-next]:hover {
260
+ calendar-picker-ui button-ui[data-cal-prev]:hover,
261
+ calendar-picker-ui button-ui[data-cal-next]:hover {
255
262
  background: var(--calendar-picker-nav-bg-hover);
256
263
  color: var(--calendar-picker-nav-fg-hover);
257
264
  }
@@ -281,8 +288,18 @@ calendar-picker-ui [data-cal-grid] {
281
288
  gap: var(--calendar-picker-day-grid-gap);
282
289
  }
283
290
 
284
- /* Day buttons */
285
- calendar-picker-ui [data-cal-day] {
291
+ /* Day buttons — stamped <button-ui variant="ghost" text="…"> (gh issue 276
292
+ wave 4). button-ui floors min-width/min-height at --button-height — point
293
+ it at the day-cell size (not just the literal width/height below) so the
294
+ floor never clamps a smaller [size] tier. The literal geometry/color
295
+ declarations below out-specify button-ui's own equivalents via the
296
+ `button-ui` type-selector bump (same technique as swiper's dots).
297
+ button-ui renders its label via `::after { content: attr(text) }` — the
298
+ today-indicator dot below therefore uses `::before` (kept free), not
299
+ `::after`, to avoid the two fighting over the same pseudo-element. */
300
+ calendar-picker-ui button-ui[data-cal-day] {
301
+ --button-height: var(--calendar-picker-day-size);
302
+
286
303
  display: flex;
287
304
  align-items: center;
288
305
  justify-content: center;
@@ -304,25 +321,25 @@ calendar-picker-ui [data-cal-day] {
304
321
  }
305
322
 
306
323
  /* ── State: hover ── */
307
- calendar-picker-ui [data-cal-day]:not([disabled]):not([data-outside]):hover {
324
+ calendar-picker-ui button-ui[data-cal-day]:not([disabled]):not([data-outside]):hover {
308
325
  background: var(--calendar-picker-day-bg-hover);
309
326
  color: var(--calendar-picker-day-fg-hover);
310
327
  }
311
328
 
312
329
  /* ── State: focus ── */
313
- calendar-picker-ui [data-cal-day]:not([disabled]):not([data-outside]):focus-visible,
314
- calendar-picker-ui [data-cal-day][data-focused] {
330
+ calendar-picker-ui button-ui[data-cal-day]:not([disabled]):not([data-outside]):focus-visible,
331
+ calendar-picker-ui button-ui[data-cal-day][data-focused] {
315
332
  outline: none;
316
333
  box-shadow: var(--calendar-picker-day-focus-ring);
317
334
  }
318
335
 
319
336
  /* ── State: selected ── */
320
- calendar-picker-ui [data-cal-day][data-selected] {
337
+ calendar-picker-ui button-ui[data-cal-day][data-selected] {
321
338
  background: var(--calendar-picker-day-bg-selected);
322
339
  color: var(--calendar-picker-day-fg-selected);
323
340
  font-weight: var(--calendar-picker-selected-weight);
324
341
  }
325
- calendar-picker-ui [data-cal-day][data-selected]:not([disabled]):hover {
342
+ calendar-picker-ui button-ui[data-cal-day][data-selected]:not([disabled]):hover {
326
343
  background: var(--calendar-picker-day-bg-selected-hover);
327
344
  color: var(--calendar-picker-day-fg-selected);
328
345
  }
@@ -330,12 +347,12 @@ calendar-picker-ui [data-cal-day][data-selected]:not([disabled]):hover {
330
347
  /* ── State: today (unselected) ──
331
348
  Use a small bottom dot so it does not compete with the rounded
332
349
  focus-ring box-shadow (that looked like a double outline). */
333
- calendar-picker-ui [data-cal-day][data-today]:not([data-selected]) {
350
+ calendar-picker-ui button-ui[data-cal-day][data-today]:not([data-selected]) {
334
351
  position: relative;
335
352
  color: var(--calendar-picker-day-today-border);
336
353
  font-weight: var(--calendar-picker-today-weight);
337
354
  }
338
- calendar-picker-ui [data-cal-day][data-today]:not([data-selected])::after {
355
+ calendar-picker-ui button-ui[data-cal-day][data-today]:not([data-selected])::before {
339
356
  content: "";
340
357
  position: absolute;
341
358
  left: 50%;
@@ -349,13 +366,16 @@ calendar-picker-ui [data-cal-day][data-today]:not([data-selected])::after {
349
366
  }
350
367
 
351
368
  /* ── State: outside month ── */
352
- calendar-picker-ui [data-cal-day][data-outside] {
369
+ calendar-picker-ui button-ui[data-cal-day][data-outside] {
353
370
  color: var(--calendar-picker-day-fg-outside);
354
371
  cursor: default;
355
372
  }
356
373
 
357
- /* ── State: disabled ── */
358
- calendar-picker-ui [data-cal-day]:disabled:not([data-outside]) {
374
+ /* ── State: disabled ──
375
+ `[disabled]` — NOT the `:disabled` pseudo-class, which only matches
376
+ native form controls; button-ui is a custom element, so `:disabled`
377
+ silently never matches it. */
378
+ calendar-picker-ui button-ui[data-cal-day][disabled]:not([data-outside]) {
359
379
  color: var(--calendar-picker-day-fg-disabled);
360
380
  cursor: not-allowed;
361
381
  }
@@ -11,6 +11,11 @@ description: >-
11
11
  supports min/max and disabled-date masks. Use for single-date input in
12
12
  forms; for date ranges compose two pickers or use a dedicated range
13
13
  component.
14
+ # Per ADR-0027 — primitives that programmatically create other primitives
15
+ # in their render() do NOT auto-import them. Consumer (or demo shell) must
16
+ # explicitly import each composed primitive.
17
+ composes:
18
+ - button-ui # day cells + month nav (gh issue 276 wave 4)
14
19
  props:
15
20
  name:
16
21
  description: Form field name
@@ -341,21 +341,24 @@ export class UIChart extends UIElement {
341
341
  this.addEventListener('pointerdown', this.#onPointerDown);
342
342
  this.addEventListener('click', this.#onClick);
343
343
 
344
- this.#resizeObs = new ResizeObserver((entries) => {
345
- const { inlineSize: w, blockSize: h } = entries[0].contentBoxSize[0];
346
- if (!this.#data.length) return;
347
- const rw = Math.round(w);
348
- const rh = Math.round(h);
349
- if (rw === this.#lastW && (!this.hasAttribute('resize') || rh === this.#lastH)) return;
350
- this.#lastW = rw;
351
- this.#lastH = rh;
352
- if (this.#resizeRaf) return;
353
- this.#resizeRaf = requestAnimationFrame(() => {
354
- this.#resizeRaf = null;
355
- this.#renderChart();
344
+ // gh#285 SSR DOM shims (linkedom) have no ResizeObserver global.
345
+ if (typeof ResizeObserver !== 'undefined') {
346
+ this.#resizeObs = new ResizeObserver((entries) => {
347
+ const { inlineSize: w, blockSize: h } = entries[0].contentBoxSize[0];
348
+ if (!this.#data.length) return;
349
+ const rw = Math.round(w);
350
+ const rh = Math.round(h);
351
+ if (rw === this.#lastW && (!this.hasAttribute('resize') || rh === this.#lastH)) return;
352
+ this.#lastW = rw;
353
+ this.#lastH = rh;
354
+ if (this.#resizeRaf) return;
355
+ this.#resizeRaf = requestAnimationFrame(() => {
356
+ this.#resizeRaf = null;
357
+ this.#renderChart();
358
+ });
356
359
  });
357
- });
358
- this.#resizeObs.observe(this);
360
+ this.#resizeObs.observe(this);
361
+ }
359
362
  }
360
363
 
361
364
  render() {
@@ -169,11 +169,14 @@ export class UIColorInput extends UIFormElement {
169
169
  // Store the observer in a #field so disconnected() can clean it up
170
170
  // — without the explicit reference, the observer survives the host
171
171
  // element across mount/unmount cycles (audit-lifecycle-leak).
172
- this.#popoverObserver = new MutationObserver(sync);
173
- this.#popoverObserver.observe(this.#popover, {
174
- attributes: true,
175
- attributeFilter: ['open'],
176
- });
172
+ // gh#285 SSR DOM shims (linkedom) have no MutationObserver global.
173
+ if (typeof MutationObserver !== 'undefined') {
174
+ this.#popoverObserver = new MutationObserver(sync);
175
+ this.#popoverObserver.observe(this.#popover, {
176
+ attributes: true,
177
+ attributeFilter: ['open'],
178
+ });
179
+ }
177
180
  }
178
181
 
179
182
  #onPickerChange = (e) => {
@@ -362,9 +362,12 @@ export class UIColorPicker extends UIFormElement {
362
362
 
363
363
  this.#hueTrack.addEventListener('keydown', this.#onHueKeydown);
364
364
 
365
- // Observe area resize to redraw canvas
366
- this.#areaResizeObs = new ResizeObserver(() => this.#drawArea());
367
- this.#areaResizeObs.observe(this.#areaEl);
365
+ // Observe area resize to redraw canvas.
366
+ // gh#285 SSR DOM shims (linkedom) have no ResizeObserver global.
367
+ if (typeof ResizeObserver !== 'undefined') {
368
+ this.#areaResizeObs = new ResizeObserver(() => this.#drawArea());
369
+ this.#areaResizeObs.observe(this.#areaEl);
370
+ }
368
371
  }
369
372
 
370
373
  disconnected() {
@@ -95,13 +95,16 @@ export class UIField extends UIElement {
95
95
  this.#bindForToControl();
96
96
  this.#bindErrorListener();
97
97
  this.#labelEl?.addEventListener('click', this.#onLabelClick);
98
- this.#mo = new MutationObserver(() => {
99
- this.#bindForToControl();
100
- this.#bindErrorListener();
101
- this.#wireAriaDescribedBy();
102
- this.#syncErrorMessage();
103
- });
104
- this.#mo.observe(this, { childList: true });
98
+ // gh#285 SSR DOM shims (linkedom) have no MutationObserver global.
99
+ if (typeof MutationObserver !== 'undefined') {
100
+ this.#mo = new MutationObserver(() => {
101
+ this.#bindForToControl();
102
+ this.#bindErrorListener();
103
+ this.#wireAriaDescribedBy();
104
+ this.#syncErrorMessage();
105
+ });
106
+ this.#mo.observe(this, { childList: true });
107
+ }
105
108
  this.#syncErrorMessage();
106
109
  }
107
110
 
@@ -51,19 +51,22 @@ export class UIFields extends UIElement {
51
51
 
52
52
  connected() {
53
53
  this.#syncInline();
54
- this.#mo = new MutationObserver((records) => {
55
- // Only re-sync on childList changes that involve direct <field-ui>
56
- // children attribute mutations on grandchildren aren't ours to
57
- // care about.
58
- for (const r of records) {
59
- if (r.type !== 'childList') continue;
60
- if (this.#hasFieldChild(r.addedNodes) || this.#hasFieldChild(r.removedNodes)) {
61
- this.#syncInline();
62
- return;
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
- this.#mo.observe(this, { childList: true });
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
- ` The zero-config import.meta.glob path does not work from inside a published dependency under Vite 8 / rolldown.\n` +
34
- ` Fix from your APP entry (where import.meta.glob is transformed):\n` +
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
- ` installIconLoaders({ regular: import.meta.glob(\n` +
38
- ` '/node_modules/@phosphor-icons/core/assets/regular/*.svg',\n` +
39
- ` { query: '?raw', import: 'default', eager: true }) });\n` +
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);
@@ -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';
@@ -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
- if (this.variant !== 'section') {
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
  }
@@ -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
- this.#resizeObs = new ResizeObserver(() => this.#scheduleUpdate());
113
- this.#mutationObs = new MutationObserver(() => {
114
- this.#rebuildPortIndicators();
115
- this.#scheduleUpdate();
116
- });
117
-
118
- this.#resizeObs.observe(this);
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.observe(child);
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.observe(child);
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
- if (!this.#observer) {
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.observe(this.#sentinel);
88
+ this.#observer?.observe(this.#sentinel);
88
89
  }
89
90
 
90
91
  #teardownSticky() {
@@ -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
- if (!this.#optionObserver) {
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.observe(this, { childList: true, subtree: true });
398
+ this.#optionObserver?.observe(this, { childList: true, subtree: true });
398
399
  }
399
400
 
400
401
  render() {
@@ -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
- this.#chromeObserver = new MutationObserver((mutations) => {
212
- let hasChrome = false;
213
- for (const m of mutations) {
214
- for (const n of m.addedNodes) {
215
- if (n.nodeType !== 1) continue;
216
- // Direct chrome child OR a display:contents wrapper-span carrying chrome.
217
- if (n.getAttribute?.('slot') === 'chrome') { hasChrome = true; break; }
218
- if (
219
- n.tagName === 'SPAN' &&
220
- n.style?.display === 'contents' &&
221
- n.querySelector?.('[slot="chrome"]')
222
- ) { hasChrome = true; break; }
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) break;
225
- }
226
- if (!hasChrome) return;
227
- // Re-entrancy guard: #absorbChromeSlot() calls insertBefore() which itself
228
- // generates childList mutations on the host. Without this guard, every
229
- // absorb pass would re-trigger the observer indefinitely. takeRecords()
230
- // drains the queue without dispatch so the next observation cycle starts
231
- // clean.
232
- this.#absorbChromeSlot();
233
- this.#chromeObserver.takeRecords();
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() {
@@ -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
- const dot = document.createElement('button');
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)));
@@ -191,7 +191,16 @@
191
191
  `--swiper-dot-size` for idle dots and `--swiper-dot-size-active`
192
192
  for the active one (one space token larger). Padding is computed
193
193
  from the size so the dot stays centered in the hitbox. */
194
- :scope [data-swiper-dots] > button {
194
+ :scope [data-swiper-dots] > button-ui {
195
+ /* Stamped <button-ui variant="ghost" role="tab"> (gh issue 276 wave 4).
196
+ The dot look is bespoke (content-box clip keeps a 1rem hitbox around
197
+ a --swiper-dot-size circle) — these host declarations out-specify
198
+ button-ui's token-driven chrome; the ghost variant keeps its own
199
+ hover fill out of the way. */
200
+ /* button-ui floors min-width/height at --button-height — point it at
201
+ the VISIBLE circle so the padding (not the min-floor) builds the
202
+ 1rem hitbox, exactly like the native dot did. */
203
+ --button-height: var(--swiper-dot-size);
195
204
  width: var(--swiper-dot-size);
196
205
  height: var(--swiper-dot-size);
197
206
  border-radius: var(--swiper-dot-radius);
@@ -208,18 +217,18 @@
208
217
  `background-clip: content-box` from the base rule isn't reset to
209
218
  its initial `border-box` value — the colored area would otherwise
210
219
  fill the full 1rem hitbox and turn into a huge circle. */
211
- :scope [data-swiper-dots] > button:hover {
220
+ :scope [data-swiper-dots] > button-ui:hover {
212
221
  background-color: var(--swiper-dot-bg-active);
213
222
  }
214
223
 
215
224
  /* Focus ring is rendered INSIDE the padding area (inset shadow), so
216
225
  the dot's visual footprint stays the size of every other dot. */
217
- :scope [data-swiper-dots] > button:focus-visible {
226
+ :scope [data-swiper-dots] > button-ui:focus-visible {
218
227
  outline: none;
219
228
  box-shadow: inset 0 0 0 var(--a-focus-width) var(--a-focus-color);
220
229
  }
221
230
 
222
- :scope [data-swiper-dots] > button[aria-current="true"] {
231
+ :scope [data-swiper-dots] > button-ui[aria-current="true"] {
223
232
  background-color: var(--swiper-dot-bg-active);
224
233
  width: var(--swiper-dot-size-active);
225
234
  height: var(--swiper-dot-size-active);
@@ -114,12 +114,17 @@ export class UIToolbar extends UIElement {
114
114
  connected() {
115
115
  this.setAttribute('role', 'toolbar');
116
116
  this.#ensureSpillover();
117
- this.#ro = new ResizeObserver(() => this.#queueReflow());
118
- this.#ro.observe(this);
119
- this.#mo = new MutationObserver(() => {
120
- if (this.#measuring) return;
121
- this.#queueReflow();
122
- });
117
+ // gh#285 SSR DOM shims (linkedom) have neither observer global.
118
+ if (typeof ResizeObserver !== 'undefined') {
119
+ this.#ro = new ResizeObserver(() => this.#queueReflow());
120
+ this.#ro.observe(this);
121
+ }
122
+ if (typeof MutationObserver !== 'undefined') {
123
+ this.#mo = new MutationObserver(() => {
124
+ if (this.#measuring) return;
125
+ this.#queueReflow();
126
+ });
127
+ }
123
128
  this.#moObserve();
124
129
  this.#queueReflow();
125
130
  }
@@ -324,8 +324,11 @@ export class UITreeItem extends UIElement {
324
324
  // role=button child of the group/tree (axe `aria-required-children`). Adopt
325
325
  // what's present now, then observe for the late arrivals.
326
326
  this.#adoptSlotted();
327
- this.#slotObserver = new MutationObserver(() => this.#adoptSlotted());
328
- this.#slotObserver.observe(this, { childList: true, subtree: true });
327
+ // gh#285 SSR DOM shims (linkedom) have no MutationObserver global.
328
+ if (typeof MutationObserver !== 'undefined') {
329
+ this.#slotObserver = new MutationObserver(() => this.#adoptSlotted());
330
+ this.#slotObserver.observe(this, { childList: true, subtree: true });
331
+ }
329
332
  }
330
333
 
331
334
  #stamp() {
@@ -453,24 +453,30 @@ function visitSubtree(root, fn) {
453
453
  }
454
454
  }
455
455
 
456
- const observer = new MutationObserver((mutations) => {
457
- for (const m of mutations) {
458
- if (m.type === 'attributes' && ATTR_FILTER.includes(m.attributeName)) {
459
- const el = m.target;
460
- if (isStreamingEl(el)) start(el);
461
- else stop(el);
462
- continue;
463
- }
464
- if (m.type === 'childList') {
465
- m.addedNodes.forEach(n => visitSubtree(n, start));
466
- m.removedNodes.forEach(n => visitSubtree(n, stop));
467
- }
468
- }
469
- });
456
+ // gh#285 SSR DOM shims (linkedom) have no MutationObserver global.
457
+ // Constructed lazily so import-time evaluation never throws; bootstrap()
458
+ // no-ops the document-level watch when the global is missing (existing
459
+ // stream elements found via querySelectorAll still get started once).
460
+ const observer = typeof MutationObserver !== 'undefined'
461
+ ? new MutationObserver((mutations) => {
462
+ for (const m of mutations) {
463
+ if (m.type === 'attributes' && ATTR_FILTER.includes(m.attributeName)) {
464
+ const el = m.target;
465
+ if (isStreamingEl(el)) start(el);
466
+ else stop(el);
467
+ continue;
468
+ }
469
+ if (m.type === 'childList') {
470
+ m.addedNodes.forEach(n => visitSubtree(n, start));
471
+ m.removedNodes.forEach(n => visitSubtree(n, stop));
472
+ }
473
+ }
474
+ })
475
+ : null;
470
476
 
471
477
  function bootstrap() {
472
478
  if (typeof document === 'undefined') return;
473
- observer.observe(document.documentElement, {
479
+ observer?.observe(document.documentElement, {
474
480
  childList: true, subtree: true,
475
481
  attributes: true, attributeFilter: ATTR_FILTER,
476
482
  });