@threadlabs/looma 0.17.4 → 0.18.1

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 (61) 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 +1 -1
  4. package/components/ui-combobox/ui-combobox.html +4 -48
  5. package/components/ui-icon-button/ui-icon-button.html +10 -0
  6. package/components/ui-input/ui-input.html +1 -1
  7. package/components/ui-input-group/ui-input-group.html +1 -1
  8. package/components/ui-listbox/ui-listbox.html +88 -12
  9. package/components/ui-listbox/ui-listbox.js +162 -18
  10. package/components/ui-menu/ui-menu.js +14 -2
  11. package/components/ui-menu-item/ui-menu-item.html +3 -0
  12. package/components/ui-popover/ui-popover.js +13 -2
  13. package/components/ui-select/ui-select.html +1 -1
  14. package/components/ui-spinner/ui-spinner.html +47 -14
  15. package/components/ui-textarea/ui-textarea.html +1 -1
  16. package/dist/index.js +1 -1
  17. package/editor/chunks/extensions.js +5 -0
  18. package/package.json +1 -1
  19. package/styles/ui-avatar.css +6 -2
  20. package/styles/ui-badge.css +30 -40
  21. package/styles/ui-button.css +1 -1
  22. package/styles/ui-combobox.css +0 -52
  23. package/styles/ui-icon-button.css +10 -0
  24. package/styles/ui-input-group.css +1 -1
  25. package/styles/ui-input.css +1 -1
  26. package/styles/ui-listbox.css +33 -8
  27. package/styles/ui-menu-item.css +3 -0
  28. package/styles/ui-select.css +1 -1
  29. package/styles/ui-spinner.css +40 -15
  30. package/styles/ui-textarea.css +1 -1
  31. package/tokens.css +67 -2
  32. package/vanilla/UiCombobox.js +9 -1
  33. package/vanilla/UiListbox.d.ts +3 -1
  34. package/vanilla/UiListbox.js +65 -18
  35. package/vanilla/UiSpinner.js +19 -1
  36. package/vue/UiAvatar.vue +11 -2
  37. package/vue/UiBadge.vue +30 -36
  38. package/vue/UiButton.vue +1 -1
  39. package/vue/UiCombobox.vue +4 -49
  40. package/vue/UiIconButton.vue +13 -0
  41. package/vue/UiInput.vue +1 -1
  42. package/vue/UiInputGroup.vue +1 -1
  43. package/vue/UiListbox.d.ts +18 -5
  44. package/vue/UiListbox.vue +142 -25
  45. package/vue/UiMenuItem.vue +3 -0
  46. package/vue/UiSelect.vue +1 -1
  47. package/vue/UiSpinner.vue +47 -14
  48. package/vue/UiTextarea.vue +1 -1
  49. package/vue/chunks/UiAvatar.js +2 -1
  50. package/vue/chunks/UiBadge.js +1 -1
  51. package/vue/chunks/UiButton.js +1 -1
  52. package/vue/chunks/UiCombobox.js +11 -2
  53. package/vue/chunks/UiIconButton.js +1 -1
  54. package/vue/chunks/UiInput.js +1 -1
  55. package/vue/chunks/UiInputGroup.js +1 -1
  56. package/vue/chunks/UiListbox.js +134 -20
  57. package/vue/chunks/UiMenuItem.js +1 -1
  58. package/vue/chunks/UiSelect.js +1 -1
  59. package/vue/chunks/UiSpinner.js +29 -3
  60. package/vue/chunks/UiTextarea.js +1 -1
  61. package/vue/components.css +354 -342
@@ -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.4",
3
+ "version": "0.18.1",
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
  }
@@ -250,7 +250,7 @@
250
250
  border-color: transparent;
251
251
  }
252
252
 
253
- :scope[data-ui-button-state~="variant=ghost"]:active:not(:disabled, [aria-disabled="true"]) {
253
+ :scope[data-ui-button-state~="variant=ghost"]:is(:active, [aria-expanded="true"]):not(:disabled, [aria-disabled="true"]) {
254
254
  background-color: var(--ui-button-ghost-active-surface, oklch(from var(--_tone) l c h / 16%));
255
255
  border-color: transparent;
256
256
  box-shadow: var(--ui-pressed);
@@ -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. */
@@ -3,6 +3,10 @@
3
3
  syntax: "*";
4
4
  inherits: false;
5
5
  }
6
+ @property --ui-icon-button-active-surface {
7
+ syntax: "*";
8
+ inherits: false;
9
+ }
6
10
  @property --ui-icon-button-border {
7
11
  syntax: "*";
8
12
  inherits: false;
@@ -247,6 +251,12 @@
247
251
  background-color: var(--ui-icon-button-solid-hover-surface, var(--ui-accent-hover));
248
252
  }
249
253
 
254
+ :scope[data-ui-icon-button-state~="variant=ghost"][aria-expanded="true"]:enabled:not([aria-disabled="true"]) {
255
+ background-color: var(--ui-icon-button-active-surface, color-mix(in srgb, currentColor 16%, transparent));
256
+ color: var(--ui-icon-button-hover-text, var(--ui-text-primary));
257
+ box-shadow: var(--ui-pressed);
258
+ }
259
+
250
260
  :scope:active:enabled:not([aria-disabled="true"]) {
251
261
  transform: scale(var(--ui-icon-button-active-scale, 0.96));
252
262
  }
@@ -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
 
@@ -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;
@@ -46,6 +46,9 @@
46
46
  }
47
47
 
48
48
  .label {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: var(--ui-space-2);
49
52
  flex: 1;
50
53
  min-inline-size: 0;
51
54
  }
@@ -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
 
@@ -1,18 +1,25 @@
1
1
  /* Generated by HTML Next 1.0.0-alpha.7. Do not edit. */
2
- @keyframes ui-spinner-chase {
2
+ @keyframes ui-spinner-rotate {
3
+ 100% {
4
+ transform: rotate(1turn);
5
+ }
6
+ }
7
+ @keyframes ui-spinner-dash {
3
8
  0% {
4
- transform: rotate(0);
9
+ stroke-dasharray: 1, 150;
10
+ stroke-dashoffset: 0;
5
11
  }
6
- 70% {
7
- transform: rotate(160deg);
12
+ 50% {
13
+ stroke-dasharray: 90, 150;
14
+ stroke-dashoffset: -35;
8
15
  }
9
16
  100% {
10
- transform: rotate(1turn);
17
+ stroke-dasharray: 90, 150;
18
+ stroke-dashoffset: -124;
11
19
  }
12
20
  }
13
21
  @scope ([data-component~="ui-spinner"]) to ([data-component], [data-slotted]) {
14
- /* Something is on its way. The ring chases in the current text colour; with reduced motion it holds still,
15
- and the label (or the text beside it) still says what is happening. */
22
+ /* A rounded SVG arc grows and recedes over a quiet full-circle track. */
16
23
  :scope {
17
24
  display: inline-flex;
18
25
  flex: none;
@@ -21,22 +28,41 @@
21
28
  box-sizing: border-box;
22
29
  inline-size: 1em;
23
30
  block-size: 1em;
24
- border: 2px solid transparent;
25
- border-block-start-color: currentColor;
26
- border-inline-end-color: color-mix(in srgb, currentColor 52%, transparent);
27
- border-radius: 50%;
28
31
  color: currentColor;
29
32
  font-size: var(--ui-font-size-lg);
30
33
  line-height: 1;
31
- animation: ui-spinner-chase 1s linear infinite;
32
34
  }
33
35
 
34
36
  :scope[data-ui-spinner-state~="size=sm"] {
35
37
  font-size: var(--ui-font-size-md);
36
38
  }
37
39
 
40
+ svg {
41
+ display: block;
42
+ inline-size: 100%;
43
+ block-size: 100%;
44
+ animation: ui-spinner-rotate 2s linear infinite;
45
+ }
46
+
47
+ circle {
48
+ stroke-width: 3;
49
+ stroke-linecap: round;
50
+ }
51
+
52
+ .track {
53
+ stroke: color-mix(in srgb, currentColor 16%, transparent);
54
+ }
55
+
56
+ .arc {
57
+ stroke: currentColor;
58
+ stroke-dasharray: 90, 150;
59
+ stroke-dashoffset: 0;
60
+ animation: ui-spinner-dash 1.5s ease-in-out infinite;
61
+ }
62
+
38
63
  @media (prefers-reduced-motion: reduce) {
39
- :scope {
64
+ svg,
65
+ .arc {
40
66
  animation: none;
41
67
  }
42
68
  }
@@ -47,8 +73,7 @@
47
73
  }
48
74
  }
49
75
  @scope ([data-component~="ui-spinner"]) to ([data-component]) {
50
- /* Something is on its way. The ring chases in the current text colour; with reduced motion it holds still,
51
- and the label (or the text beside it) still says what is happening. */
76
+ /* A rounded SVG arc grows and recedes over a quiet full-circle track. */
52
77
 
53
78
  @media (prefers-reduced-motion: reduce) {
54
79
  }
@@ -57,7 +57,7 @@
57
57
  opacity: 1;
58
58
  }
59
59
 
60
- :scope:hover:not(:focus):not(:disabled) {
60
+ :scope:hover:not(:focus):not(:disabled):not(:read-only):not([aria-invalid="true"]):not(:is(:user-invalid, [data-user-invalid])) {
61
61
  border-color: var(--ui-control-border-hover, var(--ui-text-secondary));
62
62
  }
63
63
 
package/tokens.css CHANGED
@@ -134,7 +134,7 @@
134
134
  /* Lines divide content; stronger edges enclose surfaces; control edges identify inputs. */
135
135
  --ui-border-divider: color-mix(in oklab, var(--ui-border) 50%, var(--ui-border-strong));
136
136
  --ui-border-strong: color-mix(in oklab, var(--ui-text) 25%, var(--ui-surface));
137
- --ui-control-border: color-mix(in oklab, var(--ui-text) 48%, var(--ui-surface));
137
+ --ui-control-border: color-mix(in oklab, var(--ui-text) 43%, var(--ui-surface));
138
138
 
139
139
  /* States: toward the ink for pressure, toward the page for a tint. In a dark theme the ink is
140
140
  light, so the same mixes brighten instead of darkening, without a second set of values. */
@@ -205,7 +205,7 @@
205
205
  --ui-action-secondary-text: var(--ui-text);
206
206
  --ui-control-surface: var(--ui-surface);
207
207
  --ui-control-surface-disabled: var(--ui-surface-muted);
208
- --ui-control-border-hover: var(--ui-border-strong);
208
+ --ui-control-border-hover: color-mix(in oklab, var(--ui-text) 50%, var(--ui-surface));
209
209
  --ui-control-placeholder: var(--ui-text-secondary);
210
210
  --ui-control-focus: var(--ui-focus-ring);
211
211
  /* Derived from the ring, not a near-white tint, so the halo shows on any surface. */
@@ -287,3 +287,68 @@
287
287
  outline: none;
288
288
  }
289
289
  }
290
+
291
+ /* Combobox suggestions and visible Listbox choices share the same row and checkmark. */
292
+ @layer components {
293
+ [data-ui-choice-option] {
294
+ padding: var(--ui-space-2) var(--ui-space-3);
295
+ border-radius: var(--ui-radius-sm);
296
+ cursor: pointer;
297
+ overflow-wrap: anywhere;
298
+ }
299
+
300
+ [data-ui-choice-option]:is(:hover, .active) {
301
+ background: var(--ui-surface-muted);
302
+ outline: 1px solid var(--ui-focus-ring);
303
+ outline-offset: -1px;
304
+ }
305
+
306
+ [data-ui-choice-option][aria-disabled="true"],
307
+ [data-ui-choice-option][aria-disabled="true"]:hover {
308
+ color: var(--ui-disabled-text);
309
+ background: transparent;
310
+ outline: none;
311
+ cursor: not-allowed;
312
+ }
313
+
314
+ [data-ui-choice-option]:not([data-multiple])[aria-selected="true"] {
315
+ background: color-mix(in srgb, var(--ui-accent-solid) 10%, var(--ui-control-surface));
316
+ box-shadow: inset 2px 0 var(--ui-accent-solid);
317
+ }
318
+
319
+ [data-ui-choice-option][data-multiple] {
320
+ display: flex;
321
+ align-items: center;
322
+ gap: var(--ui-space-2);
323
+ }
324
+
325
+ [data-ui-choice-option][data-multiple]::before {
326
+ content: "";
327
+ flex: none;
328
+ inline-size: 1.125rem;
329
+ block-size: 1.125rem;
330
+ border: 1px solid var(--ui-control-border-hover, var(--ui-border-strong));
331
+ border-radius: var(--ui-radius-sm);
332
+ background-color: var(--ui-control-surface);
333
+ background-repeat: no-repeat;
334
+ background-position: center;
335
+ background-size: 0.7rem;
336
+ }
337
+
338
+ [data-ui-choice-option][data-multiple][aria-selected="true"]::before {
339
+ border-color: var(--ui-accent-solid);
340
+ background-color: var(--ui-accent-solid);
341
+ background-image: var(
342
+ --_ui-combobox-option-check,
343
+ var(
344
+ --ui-choice-check-icon,
345
+ 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")
346
+ )
347
+ );
348
+ }
349
+
350
+ [aria-disabled="true"] [data-ui-choice-option] {
351
+ color: var(--ui-disabled-text);
352
+ cursor: not-allowed;
353
+ }
354
+ }