@adia-ai/web-components 0.0.21 → 0.0.23

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 (40) hide show
  1. package/README.md +1 -1
  2. package/components/action-list/action-list.css +13 -4
  3. package/components/agent-reasoning/agent-reasoning.js +6 -3
  4. package/components/alert/alert.css +19 -15
  5. package/components/alert/alert.js +8 -13
  6. package/components/avatar/avatar.css +8 -9
  7. package/components/button/button.css +27 -24
  8. package/components/card/card.css +1 -0
  9. package/components/check/check.css +15 -10
  10. package/components/col/col.css +3 -0
  11. package/components/description-list/description-list.css +9 -9
  12. package/components/grid/grid.css +6 -1
  13. package/components/input/input.css +15 -4
  14. package/components/list/list.css +8 -0
  15. package/components/menu/menu.css +22 -11
  16. package/components/modal/modal.js +40 -4
  17. package/components/option-card/option-card.css +9 -5
  18. package/components/progress/progress.css +8 -0
  19. package/components/progress-row/progress-row.js +9 -0
  20. package/components/radio/radio.css +13 -9
  21. package/components/rating/rating.css +7 -4
  22. package/components/richtext/richtext.css +138 -0
  23. package/components/row/row.css +7 -1
  24. package/components/segment/segment.css +8 -5
  25. package/components/select/select.css +4 -1
  26. package/components/stack/stack.css +3 -0
  27. package/components/stepper/stepper.css +12 -4
  28. package/components/stepper/stepper.js +7 -7
  29. package/components/swiper/swiper.css +10 -0
  30. package/components/swiper/swiper.js +77 -0
  31. package/components/switch/switch.css +15 -9
  32. package/components/tag/tag.css +7 -4
  33. package/components/toggle-group/toggle-group.css +9 -4
  34. package/package.json +1 -1
  35. package/patterns/app-nav/app-nav.js +13 -0
  36. package/patterns/app-nav-item/app-nav-item.css +15 -9
  37. package/patterns/section-nav-item/section-nav-item.css +13 -2
  38. package/styles/colors/semantics.css +5 -5
  39. package/styles/prose.css +5 -5
  40. package/styles/tokens.css +57 -0
@@ -1,3 +1,15 @@
1
+ /* Safari 17.x bug: `:scope*:hover [descendant]` inside `@scope` doesn't
2
+ match the scope root. The entire selector (including the descendant)
3
+ moves out together. See docs/BROWSER-COMPAT.md §3a. */
4
+ radio-ui:not([disabled]):hover [slot="dot"] {
5
+ border-color: var(--radio-border-hover);
6
+ background: var(--radio-bg-hover);
7
+ }
8
+ radio-ui[checked]:not([disabled]):hover [slot="dot"] {
9
+ background: var(--radio-bg-checked-hover);
10
+ border-color: var(--radio-bg-checked-hover);
11
+ }
12
+
1
13
  @scope (radio-ui) {
2
14
  :where(:scope) {
3
15
  /* ── Layout ── (size scales with universal [size] attribute via --a-toggle-size) */
@@ -77,21 +89,13 @@
77
89
  height: calc(var(--radio-size) * 0.6);
78
90
  }
79
91
 
80
- :scope:not([disabled]):hover [slot="dot"] {
81
- border-color: var(--radio-border-hover);
82
- background: var(--radio-bg-hover);
83
- }
92
+ /* hover rules moved outside @scope — see Safari 17.x bug note at top. */
84
93
 
85
94
  :scope[checked] [slot="dot"] {
86
95
  background: var(--radio-bg-checked);
87
96
  border-color: var(--radio-border-checked);
88
97
  }
89
98
 
90
- :scope[checked]:not([disabled]):hover [slot="dot"] {
91
- background: var(--radio-bg-checked-hover);
92
- border-color: var(--radio-bg-checked-hover);
93
- }
94
-
95
99
  /* Label */
96
100
  :scope[label]::after { content: attr(label); }
97
101
 
@@ -1,3 +1,10 @@
1
+ /* Safari 17.x bug: `:scope:hover` inside `@scope` doesn't match the scope
2
+ root. Plain selector outside the @scope works. See
3
+ docs/BROWSER-COMPAT.md §3a. */
4
+ rating-ui:hover [data-rating-symbol] {
5
+ color: var(--rating-fg-hover);
6
+ }
7
+
1
8
  @scope (rating-ui) {
2
9
  :where(:scope) {
3
10
  /* ── Layout ── */
@@ -72,10 +79,6 @@
72
79
  clip-path: inset(0 0 0 0);
73
80
  }
74
81
 
75
- :scope:hover [data-rating-symbol] {
76
- color: var(--rating-fg-hover);
77
- }
78
-
79
82
  /* ── Variants override TOKENS only ── */
80
83
  :scope[variant="accent"] {
81
84
  --rating-fg-filled: var(--a-accent-bg);
@@ -128,6 +128,29 @@
128
128
  margin: var(--richtext-h4-margin-top) 0 var(--richtext-h4-margin-bottom);
129
129
  }
130
130
 
131
+ [data-richtext-body] h5 {
132
+ font-size: var(--richtext-body-size);
133
+ font-weight: var(--richtext-h4-weight);
134
+ color: var(--richtext-fg-muted);
135
+ text-transform: uppercase;
136
+ letter-spacing: 0.04em;
137
+ margin: var(--richtext-h4-margin-top) 0 var(--richtext-h4-margin-bottom);
138
+ }
139
+
140
+ [data-richtext-body] h6 {
141
+ font-size: var(--richtext-code-size);
142
+ font-weight: var(--richtext-h4-weight);
143
+ color: var(--richtext-fg-muted);
144
+ text-transform: uppercase;
145
+ letter-spacing: 0.06em;
146
+ margin: var(--richtext-h4-margin-top) 0 var(--richtext-h4-margin-bottom);
147
+ }
148
+
149
+ /* First-element rhythm reset — first heading shouldn't push content
150
+ down with its top margin (already controlled at the body inset). */
151
+ [data-richtext-body] > :first-child { margin-top: 0; }
152
+ [data-richtext-body] > :last-child { margin-bottom: 0; }
153
+
131
154
  /* ── Body text ── */
132
155
 
133
156
  [data-richtext-body] p {
@@ -180,6 +203,112 @@
180
203
  margin-bottom: var(--richtext-li-margin-bottom);
181
204
  }
182
205
 
206
+ /* Nested lists — collapse the vertical block margin so children
207
+ don't add a full paragraph gap before/after the parent <li>. */
208
+ [data-richtext-body] li > ul,
209
+ [data-richtext-body] li > ol {
210
+ margin-block: var(--a-space-1);
211
+ }
212
+
213
+ /* ── Definition lists ── */
214
+ [data-richtext-body] dl {
215
+ margin: var(--richtext-list-my) 0;
216
+ }
217
+ [data-richtext-body] dt {
218
+ font-weight: var(--richtext-strong-weight);
219
+ color: var(--richtext-fg);
220
+ margin-top: var(--a-space-2);
221
+ }
222
+ [data-richtext-body] dd {
223
+ margin: 0 0 var(--richtext-li-margin-bottom) var(--richtext-list-indent);
224
+ color: var(--richtext-fg-muted);
225
+ }
226
+
227
+ /* ── Blockquote ── */
228
+ [data-richtext-body] blockquote {
229
+ margin: var(--richtext-block-my) 0;
230
+ padding: var(--a-space-1) var(--a-space-4);
231
+ border-inline-start: 3px solid var(--richtext-border);
232
+ color: var(--richtext-fg-muted);
233
+ font-style: italic;
234
+ }
235
+ [data-richtext-body] blockquote > :last-child { margin-bottom: 0; }
236
+
237
+ /* ── Inline emphasis & semantic glyphs ── */
238
+ [data-richtext-body] mark {
239
+ background: var(--a-warning-muted, var(--richtext-code-bg));
240
+ color: inherit;
241
+ padding: 0 0.2em;
242
+ border-radius: var(--richtext-code-radius);
243
+ }
244
+ [data-richtext-body] del,
245
+ [data-richtext-body] s {
246
+ color: var(--richtext-fg-muted);
247
+ text-decoration: line-through;
248
+ }
249
+ [data-richtext-body] kbd {
250
+ font-family: var(--richtext-font-code);
251
+ font-size: 0.85em;
252
+ background: var(--richtext-code-bg);
253
+ border: 1px solid var(--richtext-border);
254
+ border-bottom-width: 2px;
255
+ border-radius: var(--richtext-code-radius);
256
+ padding: 0.05em 0.4em;
257
+ box-shadow: inset 0 -1px 0 var(--richtext-border);
258
+ white-space: nowrap;
259
+ }
260
+ [data-richtext-body] sup,
261
+ [data-richtext-body] sub {
262
+ font-size: 0.75em;
263
+ line-height: 0;
264
+ }
265
+ [data-richtext-body] abbr[title] {
266
+ text-decoration: underline dotted;
267
+ text-underline-offset: 2px;
268
+ cursor: help;
269
+ }
270
+
271
+ /* ── Media ── */
272
+ [data-richtext-body] img,
273
+ [data-richtext-body] video {
274
+ max-width: 100%;
275
+ height: auto;
276
+ display: block;
277
+ margin: var(--richtext-block-my) auto;
278
+ border-radius: var(--richtext-pre-radius);
279
+ }
280
+ [data-richtext-body] figure {
281
+ margin: var(--richtext-block-my) 0;
282
+ }
283
+ [data-richtext-body] figcaption {
284
+ font-size: var(--richtext-code-size);
285
+ color: var(--richtext-fg-muted);
286
+ text-align: center;
287
+ margin-top: var(--a-space-1);
288
+ }
289
+
290
+ /* ── Disclosure ── */
291
+ [data-richtext-body] details {
292
+ margin: var(--richtext-block-my) 0;
293
+ border: 1px solid var(--richtext-border);
294
+ border-radius: var(--richtext-pre-radius);
295
+ padding: var(--a-space-3) var(--a-space-4);
296
+ }
297
+ [data-richtext-body] details[open] {
298
+ background: var(--richtext-code-bg);
299
+ }
300
+ [data-richtext-body] summary {
301
+ cursor: pointer;
302
+ font-weight: var(--richtext-strong-weight);
303
+ margin: calc(-1 * var(--a-space-3)) calc(-1 * var(--a-space-4));
304
+ padding: var(--a-space-3) var(--a-space-4);
305
+ list-style-position: inside;
306
+ }
307
+ [data-richtext-body] details[open] summary {
308
+ margin-bottom: var(--a-space-2);
309
+ border-bottom: 1px solid var(--richtext-border);
310
+ }
311
+
183
312
  /* ── Tables ── */
184
313
 
185
314
  [data-richtext-body] table {
@@ -217,9 +346,18 @@
217
346
  color: var(--richtext-link);
218
347
  text-decoration: underline;
219
348
  text-underline-offset: 2px;
349
+ transition: color var(--a-duration-fast) var(--a-easing);
220
350
  }
221
351
 
222
352
  [data-richtext-body] a:hover {
223
353
  color: var(--richtext-link-hover);
224
354
  }
355
+
356
+ [data-richtext-body] a:focus-visible {
357
+ outline: var(--a-focus-ring);
358
+ outline-offset: 2px;
359
+ border-radius: var(--a-radius-sm);
360
+ }
361
+
362
+ [data-richtext-body] small { font-size: var(--richtext-code-size); color: var(--richtext-fg-muted); }
225
363
  }
@@ -1,6 +1,9 @@
1
1
  @scope (row-ui) {
2
2
  :where(:scope) {
3
- --row-gap: var(--a-gap-md);
3
+ /* `--a-gap` defaults to `--a-gap-md` at :root in tokens.css; the
4
+ universal `[gap="N"]` rules override it on the element, so reading
5
+ `--a-gap` here picks up `<row-ui gap="…">` automatically. */
6
+ --row-gap: var(--a-gap);
4
7
  --row-justify: flex-start;
5
8
  --row-align: center;
6
9
  --row-drag-bg-active: var(--a-accent-muted);
@@ -13,6 +16,9 @@
13
16
  gap: var(--row-gap);
14
17
  justify-content: var(--row-justify);
15
18
  align-items: var(--row-align);
19
+ /* Universal [padding] / [margin] opt-in — see tokens.css for scale. */
20
+ padding: var(--a-padding, 0);
21
+ margin: var(--a-margin, 0);
16
22
  text-align: inherit;
17
23
  }
18
24
 
@@ -1,3 +1,10 @@
1
+ /* Safari 17.x bug: `:scope:not(...):hover` inside `@scope` doesn't match
2
+ the scope root. Plain selector outside works. See
3
+ docs/BROWSER-COMPAT.md §3a. */
4
+ segment-ui:not([disabled]):not([selected]):hover {
5
+ color: var(--segment-fg-hover);
6
+ }
7
+
1
8
  @scope (segment-ui) {
2
9
  :where(:scope) {
3
10
  /* ── Layout ── */
@@ -73,11 +80,7 @@
73
80
  white-space: nowrap;
74
81
  }
75
82
 
76
- /* States */
77
- :scope:not([disabled]):not([selected]):hover {
78
- color: var(--segment-fg-hover);
79
- }
80
-
83
+ /* States — hover rule moved outside @scope; see Safari 17.x bug note at top. */
81
84
  :scope[selected] {
82
85
  color: var(--segment-fg-selected);
83
86
  }
@@ -84,9 +84,12 @@
84
84
  background: var(--select-bg);
85
85
  line-height: 1;
86
86
  cursor: pointer;
87
+ /* Match every property the hover / focus / invalid states change. */
87
88
  transition:
89
+ background var(--select-duration) var(--select-easing),
88
90
  border-color var(--select-duration) var(--select-easing),
89
- background var(--select-duration) var(--select-easing);
91
+ color var(--select-duration) var(--select-easing),
92
+ box-shadow var(--select-duration) var(--select-easing);
90
93
  }
91
94
  [slot="trigger"]:hover {
92
95
  border-color: var(--select-border-hover);
@@ -7,6 +7,9 @@
7
7
  box-sizing: border-box;
8
8
  display: grid;
9
9
  place-items: var(--stack-align);
10
+ /* Universal [padding] / [margin] opt-in — see tokens.css for scale. */
11
+ padding: var(--a-padding, 0);
12
+ margin: var(--a-margin, 0);
10
13
  /* Same UA override as row-ui/col-ui: [align=...] is for flex/grid, not text. */
11
14
  text-align: inherit;
12
15
  }
@@ -32,10 +32,17 @@
32
32
  --stepper-line-done: var(--a-accent);
33
33
  --stepper-line-size: 2px;
34
34
 
35
- /* ── Layout ── */
35
+ /* ── Layout ──
36
+ Parent tokens use the bare `--stepper-*` name (no `-item-` infix)
37
+ so the child's `:where(stepper-item-ui)` block can pull from
38
+ parent → child via `var(--stepper-X, …)` without name collisions.
39
+ A self-named child declaration like
40
+ `--stepper-item-gap-sm: var(--stepper-item-gap-sm, …)` resolves
41
+ to invalid (cycle), which silently breaks any calc() that
42
+ consumes it. */
36
43
  --stepper-radius-full: var(--a-radius-full);
37
- --stepper-item-gap: var(--a-space-3);
38
- --stepper-item-gap-sm: var(--a-space-2);
44
+ --stepper-gap: var(--a-space-3);
45
+ --stepper-gap-sm: var(--a-space-2);
39
46
  --stepper-pad-x: var(--a-space-4);
40
47
  --stepper-pad-y: var(--a-space-6);
41
48
  --stepper-offset-xs: var(--a-space-1);
@@ -82,7 +89,8 @@
82
89
  --stepper-item-line-size: var(--stepper-line-size, 2px);
83
90
 
84
91
  --stepper-item-radius-full: var(--a-radius-full);
85
- --stepper-item-gap-sm: var(--stepper-item-gap-sm, var(--a-space-2));
92
+ --stepper-item-gap: var(--stepper-gap, var(--a-space-3));
93
+ --stepper-item-gap-sm: var(--stepper-gap-sm, var(--a-space-2));
86
94
  --stepper-item-pad-x: var(--stepper-pad-x, var(--a-space-4));
87
95
  --stepper-item-pad-y: var(--stepper-pad-y, var(--a-space-6));
88
96
  --stepper-item-offset-xs: var(--stepper-offset-xs, var(--a-space-1));
@@ -38,18 +38,18 @@ class AdiaStepper extends AdiaElement {
38
38
  const last = items[items.length - 1];
39
39
  if (last) last.setAttribute('data-last', '');
40
40
 
41
- // Parent drives child state via step index
41
+ // Parent drives child state via step index. Canonical Phase 6
42
+ // contract: `status` enum (`completed` / `active` / `idle`).
43
+ // CSS only matches `[status="…"]`; the legacy `[completed]` /
44
+ // `[active]` Booleans were removed in 0.0.20.
42
45
  items.forEach((item, i) => {
43
46
  item.setAttribute('data-index', i);
44
47
  if (i < this.step) {
45
- item.setAttribute('completed', '');
46
- item.removeAttribute('active');
48
+ item.setAttribute('status', 'completed');
47
49
  } else if (i === this.step) {
48
- item.removeAttribute('completed');
49
- item.setAttribute('active', '');
50
+ item.setAttribute('status', 'active');
50
51
  } else {
51
- item.removeAttribute('completed');
52
- item.removeAttribute('active');
52
+ item.setAttribute('status', 'idle');
53
53
  }
54
54
  });
55
55
  }
@@ -89,6 +89,16 @@
89
89
 
90
90
  :scope > [data-swiper-track]::-webkit-scrollbar { display: none; }
91
91
 
92
+ /* Click-and-drag affordance for cursor users — touch keeps native pan. */
93
+ @media (hover: hover) {
94
+ :scope > [data-swiper-track] { cursor: grab; }
95
+ :scope > [data-swiper-track][data-dragging] {
96
+ cursor: grabbing;
97
+ scroll-snap-type: none; /* let the pointer track 1:1 during drag */
98
+ user-select: none;
99
+ }
100
+ }
101
+
92
102
  /* ── Slides ── */
93
103
 
94
104
  :scope > [data-swiper-track] > * {
@@ -43,6 +43,7 @@ class AdiaSwiper extends AdiaElement {
43
43
  #activeIndex = 0;
44
44
  #bound = false;
45
45
  #fallbackNav = false;
46
+ #drag = null; /* { pointerId, startX, startScrollLeft, hasMoved } */
46
47
 
47
48
  get slides() {
48
49
  if (!this.#track) return [];
@@ -88,6 +89,14 @@ class AdiaSwiper extends AdiaElement {
88
89
 
89
90
  // Keyboard navigation
90
91
  this.addEventListener('keydown', this.#onKeydown);
92
+
93
+ // Click + drag (mouse / pen). Touch keeps native pan via scroll-snap.
94
+ this.#track.addEventListener('pointerdown', this.#onPointerDown);
95
+ this.#track.addEventListener('pointermove', this.#onPointerMove);
96
+ this.#track.addEventListener('pointerup', this.#onPointerUp);
97
+ this.#track.addEventListener('pointercancel', this.#onPointerUp);
98
+ // Capture-phase click suppression so a drag-on-button doesn't activate it.
99
+ this.#track.addEventListener('click', this.#onClickCapture, true);
91
100
  }
92
101
 
93
102
  if (this.autoplay) this.play();
@@ -109,6 +118,14 @@ class AdiaSwiper extends AdiaElement {
109
118
  this.removeEventListener('focusin', this.#onPauseFocus);
110
119
  this.removeEventListener('focusout', this.#onResumeFocus);
111
120
  this.removeEventListener('keydown', this.#onKeydown);
121
+ if (this.#track) {
122
+ this.#track.removeEventListener('pointerdown', this.#onPointerDown);
123
+ this.#track.removeEventListener('pointermove', this.#onPointerMove);
124
+ this.#track.removeEventListener('pointerup', this.#onPointerUp);
125
+ this.#track.removeEventListener('pointercancel', this.#onPointerUp);
126
+ this.#track.removeEventListener('click', this.#onClickCapture, true);
127
+ }
128
+ this.#drag = null;
112
129
  this.#track = null;
113
130
  this.#bound = false;
114
131
  }
@@ -280,6 +297,66 @@ class AdiaSwiper extends AdiaElement {
280
297
  if (e.key === 'ArrowRight') { e.preventDefault(); this.next(); }
281
298
  if (e.key === 'ArrowLeft') { e.preventDefault(); this.prev(); }
282
299
  };
300
+
301
+ // ── Click + drag (mouse / pen) ──
302
+ // Native touch already scroll-pans the track; we only intercept mouse and
303
+ // pen so cursor users get the same swipe affordance as touch users.
304
+
305
+ #DRAG_THRESHOLD_PX = 5;
306
+
307
+ #onPointerDown = (e) => {
308
+ if (e.pointerType === 'touch') return; /* native pan handles touch */
309
+ if (e.button !== 0) return; /* primary button only */
310
+ if (e.target.closest('button, button-ui, a, input, select, textarea')) return;
311
+ this.#drag = {
312
+ pointerId: e.pointerId,
313
+ startX: e.clientX,
314
+ startScrollLeft: this.#track.scrollLeft,
315
+ hasMoved: false,
316
+ };
317
+ /* Disable smooth scroll during drag so the bar tracks the pointer 1:1. */
318
+ this.#track.style.scrollBehavior = 'auto';
319
+ this.#track.setAttribute('data-dragging', '');
320
+ };
321
+
322
+ #onPointerMove = (e) => {
323
+ if (!this.#drag || e.pointerId !== this.#drag.pointerId) return;
324
+ const dx = e.clientX - this.#drag.startX;
325
+ if (Math.abs(dx) >= this.#DRAG_THRESHOLD_PX) {
326
+ if (!this.#drag.hasMoved) {
327
+ this.#drag.hasMoved = true;
328
+ /* Capture pointer so we keep tracking even if it leaves the track. */
329
+ try { this.#track.setPointerCapture(e.pointerId); } catch { /* noop */ }
330
+ }
331
+ this.#track.scrollLeft = this.#drag.startScrollLeft - dx;
332
+ e.preventDefault(); /* suppress text selection while dragging */
333
+ }
334
+ };
335
+
336
+ #onPointerUp = (e) => {
337
+ if (!this.#drag || e.pointerId !== this.#drag.pointerId) return;
338
+ const wasMoved = this.#drag.hasMoved;
339
+ if (wasMoved) {
340
+ try { this.#track.releasePointerCapture(e.pointerId); } catch { /* noop */ }
341
+ }
342
+ this.#drag = null;
343
+ /* Re-enable smooth scrolling; scroll-snap will glide to the nearest slide. */
344
+ this.#track.style.scrollBehavior = '';
345
+ this.#track.removeAttribute('data-dragging');
346
+ /* Ensure dragging stays "active" through the click event that follows
347
+ on the same gesture so #onClickCapture can suppress it. */
348
+ if (wasMoved) {
349
+ this.#track.setAttribute('data-just-dragged', '');
350
+ requestAnimationFrame(() => this.#track?.removeAttribute('data-just-dragged'));
351
+ }
352
+ };
353
+
354
+ #onClickCapture = (e) => {
355
+ if (this.#track?.hasAttribute('data-just-dragged')) {
356
+ e.preventDefault();
357
+ e.stopPropagation();
358
+ }
359
+ };
283
360
  }
284
361
 
285
362
  customElements.define('swiper-ui', AdiaSwiper);
@@ -1,3 +1,14 @@
1
+ /* Safari 17.x bug: `:scope[attr]:hover` inside `@scope` doesn't match the
2
+ scope root. Plain selectors outside work. See docs/BROWSER-COMPAT.md §3a. */
3
+ switch-ui:not([disabled]):hover {
4
+ --switch-track-bg: var(--switch-track-bg-hover);
5
+ --switch-track-border: var(--switch-track-border-hover);
6
+ }
7
+ switch-ui[checked]:not([disabled]):hover {
8
+ --switch-track-bg: var(--switch-track-bg-checked-hover);
9
+ --switch-track-border: var(--switch-track-border-checked-hover);
10
+ }
11
+
1
12
  @scope (switch-ui) {
2
13
  :where(:scope) {
3
14
  /* ── Layout ── */
@@ -66,19 +77,12 @@
66
77
  background var(--switch-duration) var(--switch-easing),
67
78
  border-color var(--switch-duration) var(--switch-easing);
68
79
  }
69
- :scope:not([disabled]):hover {
70
- --switch-track-bg: var(--switch-track-bg-hover);
71
- --switch-track-border: var(--switch-track-border-hover);
72
- }
80
+ /* hover rules moved outside @scope — see Safari 17.x bug note at top. */
73
81
  :scope[checked] {
74
82
  --switch-track-bg: var(--switch-track-bg-checked);
75
83
  --switch-track-border: var(--switch-track-border-checked);
76
84
  --switch-thumb-bg: var(--switch-thumb-bg-checked);
77
85
  }
78
- :scope[checked]:not([disabled]):hover {
79
- --switch-track-bg: var(--switch-track-bg-checked-hover);
80
- --switch-track-border: var(--switch-track-border-checked-hover);
81
- }
82
86
 
83
87
  [slot="thumb"] {
84
88
  position: absolute;
@@ -89,7 +93,9 @@
89
93
  border-radius: var(--switch-radius);
90
94
  background: var(--switch-thumb-bg);
91
95
  box-shadow: var(--switch-thumb-shadow);
92
- transition: transform var(--switch-duration) var(--switch-easing);
96
+ transition:
97
+ transform var(--switch-duration) var(--switch-easing),
98
+ background var(--switch-duration) var(--switch-easing);
93
99
  }
94
100
  :scope[checked] [slot="thumb"] {
95
101
  transform: translateX(var(--switch-thumb-travel));
@@ -1,3 +1,9 @@
1
+ /* Safari 17.x bug: `:scope[attr]:hover` inside `@scope` doesn't match the
2
+ scope root. Plain selector outside works. See docs/BROWSER-COMPAT.md §3a. */
3
+ tag-ui[removable]:not([disabled]):hover {
4
+ --tag-bg: var(--tag-bg-hover);
5
+ }
6
+
1
7
  @scope (tag-ui) {
2
8
  :where(:scope) {
3
9
  /* ── Tokens ── */
@@ -75,10 +81,7 @@
75
81
 
76
82
  /* Size handled by universal [size] attribute system. */
77
83
 
78
- /* ── Hover (only interactive/removable tags) ── */
79
- :scope[removable]:not([disabled]):hover {
80
- --tag-bg: var(--tag-bg-hover);
81
- }
84
+ /* hover rule moved outside @scope see Safari 17.x bug note at top. */
82
85
 
83
86
  /* ── Focus ── */
84
87
  :scope:focus-visible {
@@ -1,3 +1,11 @@
1
+ /* Safari 17.x bug: `:scope:not(...):hover` inside `@scope` doesn't match
2
+ the scope root. Plain selector outside works. See
3
+ docs/BROWSER-COMPAT.md §3a. */
4
+ toggle-group-ui:not([disabled]):hover {
5
+ --toggle-option-bg: var(--toggle-option-bg-hover);
6
+ --toggle-option-fg: var(--toggle-option-fg-hover);
7
+ }
8
+
1
9
  @scope (toggle-group-ui) {
2
10
  :where(:scope) {
3
11
  /* ── Tokens ── */
@@ -69,10 +77,7 @@
69
77
  border-inline-start: var(--toggle-option-border-width) solid var(--toggle-option-border-color);
70
78
  }
71
79
 
72
- :scope:not([disabled]):hover {
73
- --toggle-option-bg: var(--toggle-option-bg-hover);
74
- --toggle-option-fg: var(--toggle-option-fg-hover);
75
- }
80
+ /* hover rule moved outside @scope — see Safari 17.x bug note at top. */
76
81
 
77
82
  :scope:focus-visible {
78
83
  outline: none;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/web-components",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "AdiaUI web components — vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-utils.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -71,6 +71,7 @@ class AdiaAppNav extends AdiaElement {
71
71
  const item = e.target.closest('app-nav-item-ui');
72
72
  if (item && this.contains(item)) {
73
73
  this.select(item);
74
+ this.#flushHoverState();
74
75
  return;
75
76
  }
76
77
 
@@ -88,6 +89,18 @@ class AdiaAppNav extends AdiaElement {
88
89
  }
89
90
  };
90
91
 
92
+ // Safari macOS leaves `:hover` stuck on items the cursor passed through
93
+ // when the DOM mutates during click+route navigation (no `mouseleave`
94
+ // fires). Toggling pointer-events on the container forces Safari to
95
+ // re-evaluate hover state on next paint without flickering layout.
96
+ // Documented in docs/BROWSER-COMPAT.md §3a.
97
+ #flushHoverState() {
98
+ this.style.pointerEvents = 'none';
99
+ requestAnimationFrame(() => {
100
+ this.style.pointerEvents = '';
101
+ });
102
+ }
103
+
91
104
  disconnected() {
92
105
  this.removeEventListener('click', this.#onClick);
93
106
  this.#ro?.disconnect();
@@ -1,3 +1,18 @@
1
+ /* Safari 17.x bug: `:scope:hover` inside `@scope` doesn't match the scope
2
+ root. Plain selector outside the @scope works — custom props from the
3
+ `:where(:scope)` block are still set on the element via inheritance, so
4
+ `var(--token)` lookups resolve correctly. Specificity (0,1,1) is
5
+ preserved. Safari < 18 is below the §1 floor (ADR-0007); this fix
6
+ protects opt-in consumers extending the floor downward and is
7
+ harmlessly redundant on engines without the bug. */
8
+ app-nav-item-ui:hover {
9
+ background: var(--nav-item-bg-hover);
10
+ color: var(--nav-item-fg-hover);
11
+ }
12
+ app-nav-item-ui:hover [slot="icon"] {
13
+ color: var(--nav-item-fg-hover);
14
+ }
15
+
1
16
  @scope (app-nav-item-ui) {
2
17
  :where(:scope) {
3
18
  --nav-item-accent: var(--a-accent);
@@ -40,15 +55,6 @@
40
55
  outline: none;
41
56
  }
42
57
 
43
- :scope:hover {
44
- background: var(--nav-item-bg-hover);
45
- color: var(--nav-item-fg-hover);
46
- }
47
-
48
- :scope:hover [slot="icon"] {
49
- color: var(--nav-item-fg-hover);
50
- }
51
-
52
58
  :scope:focus-visible {
53
59
  background: var(--nav-item-bg-hover);
54
60
  color: var(--nav-item-fg-hover);
@@ -1,3 +1,16 @@
1
+ /* Safari 17.x bug: `:scope:hover` inside `@scope` doesn't match the scope
2
+ root. Plain selector outside the @scope works. `:focus-visible` matches
3
+ correctly inside @scope, so the combined `:hover, :focus-visible` rule
4
+ is split — `:hover` moves out, `:focus-visible` stays. See
5
+ docs/BROWSER-COMPAT.md §3a. */
6
+ section-nav-item-ui:hover:not([disabled]) {
7
+ background: var(--section-nav-item-bg-hover);
8
+ color: var(--section-nav-item-fg-hover);
9
+ }
10
+ section-nav-item-ui:hover:not([disabled]) > icon-ui {
11
+ color: var(--section-nav-item-fg-hover);
12
+ }
13
+
1
14
  @scope (section-nav-item-ui) {
2
15
  :where(:scope) {
3
16
  --section-nav-item-row-height: var(--a-size);
@@ -58,12 +71,10 @@
58
71
  text-overflow: ellipsis;
59
72
  }
60
73
 
61
- :scope:hover:not([disabled]),
62
74
  :scope:focus-visible:not([disabled]) {
63
75
  background: var(--section-nav-item-bg-hover);
64
76
  color: var(--section-nav-item-fg-hover);
65
77
  }
66
- :scope:hover:not([disabled]) > icon-ui,
67
78
  :scope:focus-visible:not([disabled]) > icon-ui {
68
79
  color: var(--section-nav-item-fg-hover);
69
80
  }