@threadlabs/looma 0.17.3 → 0.18.0

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 (71) hide show
  1. package/components/ui-avatar/ui-avatar.html +8 -4
  2. package/components/ui-badge/ui-badge.html +28 -34
  3. package/components/ui-button/ui-button.html +5 -0
  4. package/components/ui-checkbox/ui-checkbox.html +5 -5
  5. package/components/ui-combobox/ui-combobox.html +4 -48
  6. package/components/ui-icon-button/ui-icon-button.html +7 -7
  7. package/components/ui-input/ui-input.html +1 -1
  8. package/components/ui-input-group/ui-input-group.html +2 -2
  9. package/components/ui-listbox/ui-listbox.html +88 -12
  10. package/components/ui-listbox/ui-listbox.js +162 -18
  11. package/components/ui-search-result-row/ui-search-result-row.html +2 -6
  12. package/components/ui-search-shell/ui-search-shell.html +2 -11
  13. package/components/ui-select/ui-select.html +1 -1
  14. package/components/ui-spinner/ui-spinner.html +45 -18
  15. package/components/ui-textarea/ui-textarea.html +1 -1
  16. package/components/ui-tooltip/ui-tooltip.html +2 -2
  17. package/dist/index.js +1 -1
  18. package/editor/chunks/extensions.js +5 -0
  19. package/package.json +1 -1
  20. package/styles/ui-avatar.css +6 -2
  21. package/styles/ui-badge.css +30 -40
  22. package/styles/ui-button.css +7 -0
  23. package/styles/ui-checkbox.css +6 -6
  24. package/styles/ui-combobox.css +0 -52
  25. package/styles/ui-icon-button.css +7 -7
  26. package/styles/ui-input-group.css +2 -2
  27. package/styles/ui-input.css +1 -1
  28. package/styles/ui-listbox.css +33 -8
  29. package/styles/ui-search-result-row.css +2 -6
  30. package/styles/ui-search-shell.css +2 -13
  31. package/styles/ui-select.css +1 -1
  32. package/styles/ui-spinner.css +38 -19
  33. package/styles/ui-textarea.css +1 -1
  34. package/styles/ui-tooltip.css +2 -2
  35. package/tokens.css +67 -2
  36. package/vanilla/UiCombobox.js +9 -1
  37. package/vanilla/UiListbox.d.ts +3 -1
  38. package/vanilla/UiListbox.js +65 -18
  39. package/vanilla/UiSpinner.js +19 -1
  40. package/vue/UiAvatar.vue +11 -2
  41. package/vue/UiBadge.vue +30 -36
  42. package/vue/UiButton.vue +5 -0
  43. package/vue/UiCheckbox.vue +5 -5
  44. package/vue/UiCombobox.vue +4 -49
  45. package/vue/UiIconButton.vue +10 -7
  46. package/vue/UiInput.vue +1 -1
  47. package/vue/UiInputGroup.vue +2 -2
  48. package/vue/UiListbox.d.ts +18 -5
  49. package/vue/UiListbox.vue +142 -25
  50. package/vue/UiSearchResultRow.vue +2 -6
  51. package/vue/UiSearchShell.vue +2 -13
  52. package/vue/UiSelect.vue +1 -1
  53. package/vue/UiSpinner.vue +45 -18
  54. package/vue/UiTextarea.vue +1 -1
  55. package/vue/UiTooltip.vue +2 -2
  56. package/vue/chunks/UiAvatar.js +2 -1
  57. package/vue/chunks/UiBadge.js +1 -1
  58. package/vue/chunks/UiButton.js +1 -1
  59. package/vue/chunks/UiCheckbox.js +1 -1
  60. package/vue/chunks/UiCombobox.js +11 -2
  61. package/vue/chunks/UiIconButton.js +1 -1
  62. package/vue/chunks/UiInput.js +1 -1
  63. package/vue/chunks/UiInputGroup.js +1 -1
  64. package/vue/chunks/UiListbox.js +134 -20
  65. package/vue/chunks/UiSearchResultRow.js +1 -1
  66. package/vue/chunks/UiSearchShell.js +1 -1
  67. package/vue/chunks/UiSelect.js +1 -1
  68. package/vue/chunks/UiSpinner.js +29 -3
  69. package/vue/chunks/UiTextarea.js +1 -1
  70. package/vue/chunks/UiTooltip.js +1 -1
  71. package/vue/components.css +426 -440
@@ -8638,6 +8638,11 @@ function parseHtmlSlice(schema, value) {
8638
8638
  if (/\s$/.test(child.textContent) || /^\s/.test(next.textContent)) continue;
8639
8639
  wrapper.insertBefore(parsed.createTextNode(" "), next);
8640
8640
  }
8641
+ for (const pre of parsed.body.querySelectorAll("pre")) {
8642
+ let last = pre;
8643
+ while (last.lastChild) last = last.lastChild;
8644
+ if (last.nodeType === Node.TEXT_NODE) last.textContent = last.textContent?.replace(/\r?\n$/, "") ?? "";
8645
+ }
8641
8646
  const container = document.createElement("div");
8642
8647
  container.append(purify.sanitize(parsed.body.innerHTML, {
8643
8648
  USE_PROFILES: { html: true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.17.3",
3
+ "version": "0.18.0",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  inline-size: 2.5rem;
31
31
  block-size: 2.5rem;
32
32
  border-radius: var(--ui-avatar-radius, var(--ui-radius-round));
33
- border: 1px solid var(--ui-avatar-border, var(--ui-border-strong));
33
+ border: 1px solid var(--ui-avatar-border, color-mix(in srgb, var(--ui-accent-solid) 35%, var(--ui-surface-default)));
34
34
  background: var(--ui-avatar-surface, var(--ui-accent-soft));
35
35
  color: var(--ui-avatar-text, var(--ui-accent-subtle-text, var(--ui-accent-solid)));
36
36
  font: inherit;
@@ -46,6 +46,10 @@
46
46
  font-size: var(--ui-font-size-xs);
47
47
  }
48
48
 
49
+ :scope[data-ui-avatar-state~="hasImage"] {
50
+ border-color: var(--ui-avatar-border, var(--ui-border-strong));
51
+ }
52
+
49
53
  :scope[data-ui-avatar-state~="shape=square"] {
50
54
  border-radius: var(--ui-avatar-radius, var(--ui-radius-md));
51
55
  }
@@ -53,7 +57,7 @@
53
57
  /* An outline, not a dot: the avatar clips its content to the circle, and an outline is drawn
54
58
  outside the box, so the ring survives the clip and the overlap in an Avatar Group. */
55
59
  :scope[data-ui-avatar-state~="active"] {
56
- outline: 2px solid var(--ui-avatar-active-ring, var(--ui-success));
60
+ outline: 1px solid var(--ui-avatar-active-ring, var(--ui-accent-solid));
57
61
  outline-offset: 1px;
58
62
  }
59
63
 
@@ -47,7 +47,7 @@
47
47
  /* This component's hooks style the element they are set on, and never a nested one. */
48
48
 
49
49
  :scope {
50
- --_badge-surface: var(--ui-surface-subtle);
50
+ --_badge-surface: color-mix(in srgb, var(--ui-surface-subtle) 80%, var(--ui-surface-default));
51
51
  --_badge-text: var(--ui-text-secondary);
52
52
  /* Sit between the soft fill and its stronger theme ink. */
53
53
  --_badge-border: color-mix(in srgb, var(--_badge-surface) 75%, var(--_badge-text) 25%);
@@ -55,7 +55,7 @@
55
55
  surface, text, and border hooks. Each value is invalid, so the tone shows, while it is unset. The
56
56
  mixes keep 4.5:1 text contrast for any colour, in a light theme or a dark one. */
57
57
  --_ui-badge-color: var(--ui-badge-color);
58
- --_badge-color-wash: color-mix(in oklab, var(--_ui-badge-color) 15%, var(--ui-surface-default));
58
+ --_badge-color-wash: color-mix(in oklab, var(--_ui-badge-color) 13%, var(--ui-surface-default));
59
59
  --_badge-color-ink: color-mix(in oklab, var(--_ui-badge-color) 35%, var(--ui-text-primary));
60
60
  --_badge-color-surface: var(--_badge-color-wash);
61
61
  --_badge-color-border: color-mix(in srgb, var(--_badge-color-wash) 75%, var(--_badge-color-ink) 25%);
@@ -123,32 +123,32 @@
123
123
  }
124
124
 
125
125
  :scope[data-ui-badge-state~="variant=subtle"][data-ui-badge-state~="tone=accent"] {
126
- --_badge-surface: color-mix(in srgb, var(--ui-accent-solid) 22%, var(--ui-surface-default));
127
- --_badge-border: color-mix(in srgb, var(--ui-accent-solid) 28%, var(--ui-surface-default));
126
+ --_badge-surface: color-mix(in srgb, var(--ui-accent-solid) 19%, var(--ui-surface-default));
127
+ --_badge-border: color-mix(in srgb, var(--ui-accent-solid) 25%, var(--ui-surface-default));
128
128
  --_badge-text: var(--ui-accent-subtle-text, var(--ui-accent-solid));
129
129
  }
130
130
 
131
131
  :scope[data-ui-badge-state~="variant=subtle"][data-ui-badge-state~="tone=danger"] {
132
- --_badge-surface: color-mix(in srgb, var(--ui-danger-solid) 22%, var(--ui-surface-default));
133
- --_badge-border: color-mix(in srgb, var(--ui-danger-solid) 28%, var(--ui-surface-default));
132
+ --_badge-surface: color-mix(in srgb, var(--ui-danger-solid) 19%, var(--ui-surface-default));
133
+ --_badge-border: color-mix(in srgb, var(--ui-danger-solid) 25%, var(--ui-surface-default));
134
134
  --_badge-text: var(--ui-danger-subtle-text, var(--ui-danger-solid));
135
135
  }
136
136
 
137
137
  :scope[data-ui-badge-state~="variant=subtle"][data-ui-badge-state~="tone=success"] {
138
- --_badge-surface: color-mix(in srgb, var(--ui-success-solid) 22%, var(--ui-surface-default));
139
- --_badge-border: color-mix(in srgb, var(--ui-success-solid) 28%, var(--ui-surface-default));
138
+ --_badge-surface: color-mix(in srgb, var(--ui-success-solid) 19%, var(--ui-surface-default));
139
+ --_badge-border: color-mix(in srgb, var(--ui-success-solid) 25%, var(--ui-surface-default));
140
140
  --_badge-text: var(--ui-success-subtle-text, var(--ui-success-solid));
141
141
  }
142
142
 
143
143
  :scope[data-ui-badge-state~="variant=subtle"][data-ui-badge-state~="tone=warning"] {
144
- --_badge-surface: color-mix(in srgb, var(--ui-warning-solid) 22%, var(--ui-surface-default));
145
- --_badge-border: color-mix(in srgb, var(--ui-warning-solid) 28%, var(--ui-surface-default));
144
+ --_badge-surface: color-mix(in srgb, var(--ui-warning-solid) 19%, var(--ui-surface-default));
145
+ --_badge-border: color-mix(in srgb, var(--ui-warning-solid) 25%, var(--ui-surface-default));
146
146
  --_badge-text: var(--ui-warning-subtle-text, var(--ui-warning-solid));
147
147
  }
148
148
 
149
149
  :scope[data-ui-badge-state~="variant=subtle"][data-ui-badge-state~="tone=info"] {
150
- --_badge-surface: color-mix(in srgb, var(--ui-info-solid) 22%, var(--ui-surface-default));
151
- --_badge-border: color-mix(in srgb, var(--ui-info-solid) 28%, var(--ui-surface-default));
150
+ --_badge-surface: color-mix(in srgb, var(--ui-info-solid) 19%, var(--ui-surface-default));
151
+ --_badge-border: color-mix(in srgb, var(--ui-info-solid) 25%, var(--ui-surface-default));
152
152
  --_badge-text: var(--ui-info-subtle-text, var(--ui-info-solid));
153
153
  }
154
154
 
@@ -194,54 +194,48 @@
194
194
  --_badge-text: var(--ui-info-subtle-text, var(--ui-info-solid));
195
195
  }
196
196
 
197
- /* A tag is flat at the start and points at the end. The point is cut from the box, so it has no
198
- outline to follow: the edge is the fill. The end padding grows by the point's depth so the
199
- label never runs into it. */
197
+ /* The clipped point needs an inset surface over an outer border-colour shape: a native border
198
+ has no diagonal edge to follow. Keep this edge for every tag variant. */
200
199
  :scope[data-ui-badge-state~="shape=tag"] {
201
200
  --_badge-point: var(--ui-badge-point, var(--ui-space-2));
202
201
 
202
+ position: relative;
203
+ isolation: isolate;
203
204
  border-color: transparent;
204
205
  border-radius: 0;
206
+ background: var(--ui-badge-border, var(--_badge-color-border, var(--_badge-border)));
205
207
  padding-inline-end: calc(var(--ui-badge-padding-inline, var(--ui-space-2)) + var(--_badge-point));
206
208
  clip-path: polygon(0 0, calc(100% - var(--_badge-point)) 0, 100% 50%, calc(100% - var(--_badge-point)) 100%, 0 100%);
207
209
  }
208
210
 
209
- :scope[data-ui-badge-state~="shape=tag"]:dir(rtl) {
210
- padding-inline-end: var(--ui-badge-padding-inline, var(--ui-space-2));
211
- padding-inline-start: calc(var(--ui-badge-padding-inline, var(--ui-space-2)) + var(--_badge-point));
212
- clip-path: polygon(var(--_badge-point) 0, 100% 0, 100% 100%, var(--_badge-point) 100%, 0 50%);
213
- }
214
-
215
- /* The tag clips its point, including the native border. Draw the outer shape in the border
216
- colour and an inset inner shape in the surface colour to keep a visible diagonal edge. */
217
- :scope[data-ui-badge-state~="variant=outline"][data-ui-badge-state~="shape=tag"] {
218
- position: relative;
219
- isolation: isolate;
220
- background: var(--ui-badge-border, var(--_badge-color-border, var(--_badge-border)));
221
- }
222
-
223
- :scope[data-ui-badge-state~="variant=outline"][data-ui-badge-state~="shape=tag"]::before {
211
+ :scope[data-ui-badge-state~="shape=tag"]::before {
224
212
  content: "";
225
213
  position: absolute;
226
- inset: 1px;
214
+ inset: 0;
227
215
  z-index: -1;
228
216
  background: var(--_badge-outline-surface);
229
217
  clip-path: polygon(0 0, calc(100% - var(--_badge-point)) 0, 100% 50%, calc(100% - var(--_badge-point)) 100%, 0 100%);
230
218
  pointer-events: none;
231
219
  }
232
220
 
233
- :scope[data-ui-badge-state~="variant=outline"][data-ui-badge-state~="shape=tag"]:dir(rtl)::before {
221
+ :scope[data-ui-badge-state~="shape=tag"]:dir(rtl) {
222
+ padding-inline-end: var(--ui-badge-padding-inline, var(--ui-space-2));
223
+ padding-inline-start: calc(var(--ui-badge-padding-inline, var(--ui-space-2)) + var(--_badge-point));
224
+ clip-path: polygon(var(--_badge-point) 0, 100% 0, 100% 100%, var(--_badge-point) 100%, 0 50%);
225
+ }
226
+
227
+ :scope[data-ui-badge-state~="shape=tag"]:dir(rtl)::before {
234
228
  clip-path: polygon(var(--_badge-point) 0, 100% 0, 100% 100%, var(--_badge-point) 100%, 0 50%);
235
229
  }
236
230
 
237
231
  @media (forced-colors: active) {
238
- :scope[data-ui-badge-state~="variant=outline"][data-ui-badge-state~="shape=tag"] {
232
+ :scope[data-ui-badge-state~="shape=tag"] {
239
233
  forced-color-adjust: none;
240
234
  background: CanvasText;
241
235
  color: CanvasText;
242
236
  }
243
237
 
244
- :scope[data-ui-badge-state~="variant=outline"][data-ui-badge-state~="shape=tag"]::before {
238
+ :scope[data-ui-badge-state~="shape=tag"]::before {
245
239
  background: Canvas;
246
240
  }
247
241
  }
@@ -263,12 +257,8 @@
263
257
  /* A solid colour fills with the ink. Its text is the page surface, mixed at 0% only so that it too is
264
258
  invalid while the hook is unset. */
265
259
 
266
- /* A tag is flat at the start and points at the end. The point is cut from the box, so it has no
267
- outline to follow: the edge is the fill. The end padding grows by the point's depth so the
268
- label never runs into it. */
269
-
270
- /* The tag clips its point, including the native border. Draw the outer shape in the border
271
- colour and an inset inner shape in the surface colour to keep a visible diagonal edge. */
260
+ /* The clipped point needs an inset surface over an outer border-colour shape: a native border
261
+ has no diagonal edge to follow. Keep this edge for every tag variant. */
272
262
 
273
263
  @media (forced-colors: active) {
274
264
  }
@@ -131,6 +131,11 @@
131
131
  margin: 0;
132
132
  }
133
133
 
134
+ /* An anchor keeps the boxed button look even when an app styles ordinary links. */
135
+ :scope:is(a) {
136
+ text-decoration: none;
137
+ }
138
+
134
139
  .spinner-wrap {
135
140
  display: inline-flex;
136
141
  flex: none;
@@ -363,6 +368,8 @@
363
368
  @scope ([data-component~="ui-button"]) to ([data-component]) {
364
369
  /* This component's hooks style the element they are set on, and never a nested one. */
365
370
 
371
+ /* An anchor keeps the boxed button look even when an app styles ordinary links. */
372
+
366
373
  @supports (text-box-trim: trim-both) {
367
374
  }
368
375
 
@@ -14,15 +14,15 @@
14
14
  @keyframes ui-checkbox-check {
15
15
  from {
16
16
  opacity: 0;
17
- transform: translate(0.07rem, -0.09rem) rotate(45deg) scale(0.55);
17
+ transform: translateY(-0.09rem) rotate(45deg) scale(0.55);
18
18
  }
19
19
  65% {
20
20
  opacity: 1;
21
- transform: translate(0.07rem, -0.09rem) rotate(45deg) scale(1.12);
21
+ transform: translateY(-0.09rem) rotate(45deg) scale(1.12);
22
22
  }
23
23
  to {
24
24
  opacity: 1;
25
- transform: translate(0.07rem, -0.09rem) rotate(45deg) scale(1);
25
+ transform: translateY(-0.09rem) rotate(45deg) scale(1);
26
26
  }
27
27
  }
28
28
  @scope ([data-component~="ui-checkbox"]) to ([data-component], [data-slotted]) {
@@ -97,7 +97,7 @@
97
97
  box-shadow: var(--ui-control-inset);
98
98
  }
99
99
 
100
- /* Centre the tick's box, then lift it: a rotated L's ink sits ~0.11rem below its box centre. */
100
+ /* Centre the tick's box and lift it slightly for the rotated check shape. */
101
101
  input:checked::after {
102
102
  inset: 0;
103
103
  margin: auto;
@@ -109,7 +109,7 @@
109
109
  border-color: var(--ui-text-on-accent);
110
110
  border-width: 0 0.125rem 0.125rem 0;
111
111
  opacity: 1;
112
- transform: translate(0.07rem, -0.09rem) rotate(45deg);
112
+ transform: translateY(-0.09rem) rotate(45deg);
113
113
  animation: ui-checkbox-check 180ms var(--ui-motion-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both;
114
114
  }
115
115
 
@@ -197,7 +197,7 @@
197
197
 
198
198
  /* The label, and under it an optional line saying what the choice means. */
199
199
 
200
- /* Centre the tick's box, then lift it: a rotated L's ink sits ~0.11rem below its box centre. */
200
+ /* Centre the tick's box and lift it slightly for the rotated check shape. */
201
201
 
202
202
  @media (prefers-reduced-motion: reduce) {
203
203
  }
@@ -254,21 +254,6 @@
254
254
  [hidden] {
255
255
  display: none;
256
256
  }
257
- .option {
258
- padding: var(--ui-space-2) var(--ui-space-3);
259
- border-radius: var(--ui-radius-sm);
260
- cursor: pointer;
261
- overflow-wrap: anywhere;
262
- }
263
- .option:hover, .option.active {
264
- background: var(--ui-surface-muted);
265
- outline: 1px solid var(--ui-focus-ring);
266
- outline-offset: -1px;
267
- }
268
- .option[aria-disabled="true"] {
269
- color: var(--ui-disabled-text);
270
- cursor: not-allowed;
271
- }
272
257
  .option[aria-disabled="true"] .primary,
273
258
  .option[aria-disabled="true"] .secondary {
274
259
  color: var(--ui-disabled-text);
@@ -336,39 +321,6 @@
336
321
  }
337
322
  }
338
323
 
339
- /* Multiple mode: every option carries a checkbox, so a list that takes several answers says so
340
- before anything is picked. aria-selected is the state; this draws it.
341
- Longhands, not shorthands: lowering drops a shorthand that omits a component. */
342
- :scope[data-ui-combobox-state~="multiple"] .option {
343
- display: flex;
344
- gap: var(--ui-space-2);
345
- align-items: center;
346
- }
347
-
348
- :scope[data-ui-combobox-state~="multiple"] .option::before {
349
- content: "";
350
- flex: none;
351
- inline-size: 1.125rem;
352
- block-size: 1.125rem;
353
- border-style: solid;
354
- border-width: 1px;
355
- border-color: var(--ui-control-border-hover, var(--ui-border-strong));
356
- border-radius: var(--ui-radius-sm);
357
- background-color: var(--ui-control-surface, var(--ui-surface-default));
358
- background-repeat: no-repeat;
359
- background-position: center;
360
- background-size: 0.7rem;
361
- }
362
-
363
- :scope[data-ui-combobox-state~="multiple"] .option[aria-selected="true"]::before {
364
- border-color: var(--ui-accent-solid);
365
- background-color: var(--ui-accent-solid);
366
- background-image: var(
367
- --_ui-combobox-option-check,
368
- url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12 5 5 9-9'/%3E%3C/svg%3E")
369
- );
370
- }
371
-
372
324
  /* Multiple mode keeps one control height. Chips and the input scroll within the entry,
373
325
  while the clear and disclosure controls remain fixed at the end of the field. */
374
326
  :scope[data-ui-combobox-state~="multiple"] .field {
@@ -448,10 +400,6 @@
448
400
  @media (forced-colors: active) {
449
401
  }
450
402
 
451
- /* Multiple mode: every option carries a checkbox, so a list that takes several answers says so
452
- before anything is picked. aria-selected is the state; this draws it.
453
- Longhands, not shorthands: lowering drops a shorthand that omits a component. */
454
-
455
403
  /* Multiple mode keeps one control height. Chips and the input scroll within the entry,
456
404
  while the clear and disclosure controls remain fixed at the end of the field. */
457
405
  /* Size variants keep their control height as selections are added. */
@@ -117,7 +117,7 @@
117
117
  @media (hover: hover) and (pointer: fine) {
118
118
  :scope[data-ui-icon-button-state~="anticipatory"]:not(:scope[data-ui-icon-button-state~="variant=solid"], :scope[data-ui-icon-button-state~="loading"], :scope[data-ui-icon-button-state~="pending"], :disabled, [data-ui-proximity="near"], :hover, :focus-visible) {
119
119
  border-color: transparent;
120
- background: transparent;
120
+ background-color: transparent;
121
121
  box-shadow: none;
122
122
  }
123
123
 
@@ -145,7 +145,7 @@
145
145
  margin: 0;
146
146
  border: 1px solid transparent;
147
147
  border-radius: var(--ui-icon-button-radius, var(--ui-action-radius, var(--ui-radius-md)));
148
- background: var(--ui-icon-button-surface, transparent);
148
+ background-color: var(--ui-icon-button-surface, transparent);
149
149
  color: inherit;
150
150
  display: inline-flex;
151
151
  align-items: center;
@@ -217,14 +217,14 @@
217
217
 
218
218
  :scope[data-ui-icon-button-state~="variant=outline"] {
219
219
  border-color: var(--ui-icon-button-border, var(--ui-control-border, var(--ui-border-strong)));
220
- background: var(--ui-icon-button-outline-surface, var(--ui-surface-default));
220
+ background-color: var(--ui-icon-button-outline-surface, var(--ui-surface-default));
221
221
  background-image: var(--ui-raised-highlight);
222
222
  color: var(--ui-icon-button-outline-text, var(--ui-text-primary));
223
223
  box-shadow: var(--ui-raised);
224
224
  }
225
225
 
226
226
  :scope[data-ui-icon-button-state~="variant=solid"] {
227
- background: var(--ui-icon-button-solid-surface, var(--ui-accent-solid));
227
+ background-color: var(--ui-icon-button-solid-surface, var(--ui-accent-solid));
228
228
  background-image: var(--ui-raised-highlight);
229
229
  color: var(--ui-icon-button-solid-text, var(--ui-text-on-accent));
230
230
  box-shadow: var(
@@ -235,7 +235,7 @@
235
235
 
236
236
  :scope:hover:enabled:not([aria-disabled="true"]) {
237
237
  /* Derived from the text colour so the ghost hover shows on any background. */
238
- background: var(--ui-icon-button-hover-surface, color-mix(in srgb, currentColor 8%, transparent));
238
+ background-color: var(--ui-icon-button-hover-surface, color-mix(in srgb, currentColor 8%, transparent));
239
239
  color: var(--ui-icon-button-hover-text, var(--ui-text-primary));
240
240
  }
241
241
 
@@ -244,7 +244,7 @@
244
244
  }
245
245
 
246
246
  :scope[data-ui-icon-button-state~="variant=solid"]:hover:enabled:not([aria-disabled="true"]) {
247
- background: var(--ui-icon-button-solid-hover-surface, var(--ui-accent-hover));
247
+ background-color: var(--ui-icon-button-solid-hover-surface, var(--ui-accent-hover));
248
248
  }
249
249
 
250
250
  :scope:active:enabled:not([aria-disabled="true"]) {
@@ -279,7 +279,7 @@
279
279
  /* Disabled is a flat neutral control, independent of the active variant's tone. */
280
280
  :scope:disabled {
281
281
  border-color: var(--ui-disabled-border);
282
- background: var(--ui-disabled-surface);
282
+ background-color: var(--ui-disabled-surface);
283
283
  color: var(--ui-disabled-text);
284
284
  filter: var(--ui-icon-button-disabled-filter, var(--ui-disabled-filter));
285
285
  opacity: var(--ui-icon-button-disabled-opacity, 1);
@@ -68,7 +68,7 @@
68
68
  }
69
69
  }
70
70
 
71
- :scope:hover:not(:has(input:focus, input:disabled)) {
71
+ :scope:hover:not(:has(input:focus, input:disabled, input:read-only, [aria-invalid="true"], :is(:user-invalid, [data-user-invalid]))) {
72
72
  border-color: var(--ui-control-border-hover, var(--ui-text-secondary));
73
73
  }
74
74
 
@@ -119,7 +119,7 @@
119
119
  flex: none;
120
120
  align-items: center;
121
121
  padding-inline: var(--ui-space-2);
122
- color: var(--ui-text-secondary);
122
+ color: var(--ui-text-muted);
123
123
  font-family: var(--ui-font-family-mono);
124
124
  font-size: var(--ui-font-size-sm);
125
125
  white-space: nowrap;
@@ -111,7 +111,7 @@
111
111
  opacity: 1;
112
112
  }
113
113
 
114
- :scope:hover:not(:focus):not(:disabled) {
114
+ :scope:hover:not(:focus):not(:disabled):not(:read-only):not([aria-invalid="true"]):not(:is(:user-invalid, [data-user-invalid])) {
115
115
  border-color: var(--ui-input-border-hover, var(--_ui-default-input-border-hover, var(--ui-control-border-hover, var(--ui-text-secondary))));
116
116
  }
117
117
 
@@ -21,29 +21,55 @@
21
21
  }
22
22
  @scope ([data-component~="ui-listbox"]) to ([data-component], [data-slotted]) {
23
23
  :scope {
24
+ position: relative;
24
25
  box-sizing: border-box;
25
26
  display: block;
26
27
  inline-size: 100%;
27
28
  min-inline-size: 0;
28
- min-block-size: var(--ui-control-min-block-size);
29
29
  margin: 0;
30
30
  padding: var(--ui-space-1);
31
31
  border: 1px solid var(--ui-listbox-border, var(--ui-control-border));
32
32
  border-radius: var(--ui-listbox-radius, var(--ui-field-radius, var(--ui-radius-md)));
33
33
  background: var(--ui-listbox-surface, var(--ui-control-surface));
34
34
  color: var(--ui-text-primary);
35
- color-scheme: light dark;
36
35
  font: inherit;
37
36
  line-height: var(--ui-line-height-md);
38
37
  box-shadow: var(--ui-control-inset);
38
+ outline: none;
39
39
  }
40
40
 
41
- :scope option,
42
- :scope optgroup {
43
- padding: var(--ui-space-2);
41
+ .choices {
42
+ max-block-size: calc(max(2, var(--_rows, 5)) * var(--ui-control-size-md));
43
+ }
44
+
45
+ :scope:not([data-enhanced]) {
46
+ padding: 0;
47
+ border: 0;
48
+ background: transparent;
49
+ box-shadow: none;
50
+ }
51
+
52
+ :scope:not([data-enhanced]) .choices,
53
+ :scope[data-enhanced] .fallback {
54
+ display: none;
55
+ }
56
+
57
+ .fallback {
58
+ box-sizing: border-box;
59
+ inline-size: 100%;
44
60
  font: inherit;
45
61
  }
46
62
 
63
+ .validity {
64
+ position: absolute;
65
+ inline-size: 1px;
66
+ block-size: 1px;
67
+ overflow: hidden;
68
+ clip-path: inset(50%);
69
+ opacity: 0;
70
+ pointer-events: none;
71
+ }
72
+
47
73
  :scope:focus-visible {
48
74
  border-color: var(--ui-control-focus);
49
75
  outline: 2px solid var(--ui-focus-ring);
@@ -51,12 +77,11 @@
51
77
  box-shadow: var(--ui-listbox-focus-shadow, var(--ui-focus-halo));
52
78
  }
53
79
 
54
- :scope[aria-invalid="true"],
55
- :scope:is(:user-invalid, [data-user-invalid]) {
80
+ :scope[aria-invalid="true"] {
56
81
  border-color: var(--ui-listbox-invalid-border, var(--ui-field-danger, var(--ui-danger)));
57
82
  }
58
83
 
59
- :scope:disabled {
84
+ :scope[aria-disabled="true"] {
60
85
  background: var(--ui-control-surface-disabled);
61
86
  color: var(--ui-disabled-text);
62
87
  box-shadow: none;
@@ -45,18 +45,14 @@
45
45
  align-items: flex-start;
46
46
  gap: var(--ui-search-result-row-gap, var(--ui-space-3));
47
47
  background: var(--ui-search-result-row-surface, transparent);
48
- border: none;
49
- border-block-end: 1px solid var(--ui-search-result-row-border, var(--ui-border-divider));
48
+ border: 1px solid var(--ui-search-result-row-border, transparent);
49
+ border-radius: var(--ui-radius-md);
50
50
  color: inherit;
51
51
  cursor: pointer;
52
52
  text-align: left;
53
53
  transition: background-color var(--ui-motion-fast) var(--ui-motion-ease);
54
54
  }
55
55
 
56
- :scope:last-child {
57
- border-block-end: none;
58
- }
59
-
60
56
  :scope:hover:enabled {
61
57
  background: var(--ui-search-result-row-hover-surface, color-mix(in srgb, var(--ui-surface) 85%, black 3%));
62
58
  }
@@ -96,14 +96,6 @@
96
96
 
97
97
  .search {
98
98
  padding: var(--ui-space-3);
99
- border-block-end: 1px solid var(--ui-border-divider);
100
- transition: border-color var(--ui-motion-fast) var(--ui-motion-ease), box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
101
- }
102
-
103
- /* The search region shows where typing goes, whatever the author put in it. */
104
- .search:focus-within {
105
- border-block-end-color: var(--_ui-search-shell-focus-color, var(--ui-control-focus));
106
- box-shadow: inset 0 -1px 0 var(--_ui-search-shell-focus-color, var(--ui-control-focus));
107
99
  }
108
100
 
109
101
  .status,
@@ -115,7 +107,7 @@
115
107
  }
116
108
 
117
109
  .status {
118
- border-block-end: 1px solid var(--ui-border-divider);
110
+ padding-block-start: 0;
119
111
  }
120
112
 
121
113
  /* Laid out like the dialog's action row: actions right-aligned, spaced as buttons, no fill. */
@@ -126,7 +118,6 @@
126
118
  justify-content: flex-end;
127
119
  gap: var(--ui-space-2);
128
120
  padding: var(--ui-space-3) var(--ui-space-4);
129
- border-block-start: 1px solid var(--ui-border-divider);
130
121
  }
131
122
 
132
123
  .status:empty,
@@ -180,8 +171,6 @@
180
171
  @scope ([data-component~="ui-search-shell"]) to ([data-component]) {
181
172
  /* This component's hooks style the element they are set on, and never a nested one. */
182
173
 
183
- /* The search region shows where typing goes, whatever the author put in it. */
184
-
185
174
  :where([data-slotted], [data-slotted] *):is(input[slot="search"]) {
186
175
  box-sizing: border-box;
187
176
  inline-size: 100%;
@@ -196,7 +185,7 @@
196
185
  }
197
186
 
198
187
  :where([data-slotted], [data-slotted] *):is(input[slot="search"]:focus-visible) {
199
- border-color: var(--ui-control-focus);
188
+ border-color: var(--_ui-search-shell-focus-color, var(--ui-control-focus));
200
189
  outline: 2px solid var(--ui-focus-ring);
201
190
  outline-offset: 2px;
202
191
  }
@@ -78,7 +78,7 @@
78
78
  background-size: 1.125rem 1.125rem;
79
79
  }
80
80
 
81
- :scope:hover:not(:focus):not(:disabled) {
81
+ :scope:hover:not(:focus):not(:disabled):not([aria-invalid="true"]):not(:is(:user-invalid, [data-user-invalid])) {
82
82
  border-color: var(--ui-select-border-hover, var(--ui-control-border-hover, var(--ui-text-secondary)));
83
83
  }
84
84