@sveltia/ui 0.75.4 → 0.75.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.
@@ -290,7 +290,4 @@ button .label {
290
290
  }
291
291
  button > :global(*) {
292
292
  pointer-events: none;
293
- }
294
- button :global(:has([slot=start-icon] + [slot=end-icon])) {
295
- gap: 0;
296
293
  }</style>
@@ -94,15 +94,18 @@
94
94
  color: var(--sui-highlight-foreground-color);
95
95
  background-color: var(--sui-selected-background-color);
96
96
  }
97
- .select-button-group :global([aria-invalid=true]) :global(button) {
97
+ .select-button-group[aria-invalid=true] :global(button) {
98
98
  border-color: var(--sui-error-border-color);
99
99
  }
100
- .select-button-group :global([aria-disabled=false]) :global(button[aria-disabled=true]) {
100
+ .select-button-group[aria-disabled=false] :global(button[aria-disabled='true']) {
101
101
  filter: grayscale(0) opacity(1);
102
102
  }
103
- .select-button-group :global([aria-disabled=false]) :global(button[aria-disabled=true]) :global(*) {
103
+ .select-button-group[aria-disabled=false] :global(button[aria-disabled='true']) :global(*) {
104
104
  filter: grayscale(1) opacity(var(--sui-disabled-opacity, 0.35));
105
105
  }
106
+ .select-button-group[aria-readonly=true] :global(button) {
107
+ pointer-events: none;
108
+ }
106
109
 
107
110
  .inner {
108
111
  display: contents;
@@ -259,9 +259,10 @@
259
259
  .grid div.other-month {
260
260
  color: var(--sui-tertiary-foreground-color);
261
261
  }
262
- .grid div :global(.today) :global(button) {
263
- color: var(--sui-highlight-foreground-color);
264
- background-color: var(--sui-primary-accent-color);
262
+ .grid div.today :global(button) {
263
+ border-width: 1px;
264
+ border-color: var(--sui-primary-accent-color);
265
+ color: var(--sui-primary-accent-color-text);
265
266
  }
266
267
  .grid div :global(button) {
267
268
  justify-content: center;
@@ -277,6 +278,10 @@
277
278
  border-width: 1px;
278
279
  border-color: var(--sui-primary-accent-color-light);
279
280
  }
281
+ .grid div :global(button[aria-selected=true]) {
282
+ color: var(--sui-highlight-foreground-color);
283
+ background-color: var(--sui-primary-accent-color);
284
+ }
280
285
 
281
286
  .footer :global(button) {
282
287
  font-size: var(--sui-font-size-small);
@@ -223,6 +223,17 @@
223
223
  .checkbox label {
224
224
  cursor: inherit;
225
225
  }
226
+ .checkbox.readonly {
227
+ cursor: default;
228
+ }
229
+ .checkbox.readonly :global(button), .checkbox.readonly:is(:hover, :active) :global(button) {
230
+ background-color: var(--sui-checkbox-background-color);
231
+ }
232
+ .checkbox.readonly :global(button[aria-checked='true']:not([aria-invalid='true'])), .checkbox.readonly:is(:hover, :active) :global(button[aria-checked='true']:not([aria-invalid='true'])) {
233
+ border-color: var(--sui-readonly-accent-color);
234
+ color: var(--sui-readonly-accent-color-inverted);
235
+ background-color: var(--sui-readonly-accent-color);
236
+ }
226
237
 
227
238
  .inner {
228
239
  display: contents;
@@ -142,6 +142,9 @@
142
142
  [role=listbox].tabs :global(.option) :global(button) :global(.icon) {
143
143
  display: none;
144
144
  }
145
+ [role=listbox].tabs[aria-readonly=true] :global(.option button[aria-selected='true']) {
146
+ border-color: var(--sui-readonly-accent-color);
147
+ }
145
148
  [role=listbox].in-combobox:focus-visible {
146
149
  outline-color: transparent;
147
150
  }
@@ -173,7 +173,7 @@
173
173
  .option:focus-visible {
174
174
  outline-width: 0 !important;
175
175
  }
176
- .option :global(.wrap) :global(button) {
176
+ .option.wrap :global(button) {
177
177
  white-space: normal;
178
178
  }
179
179
  .option :global(button) {
@@ -210,4 +210,15 @@
210
210
  :global([role='listbox']:focus-visible) .option :global(.focused) {
211
211
  outline-color: var(--sui-focus-ring-color);
212
212
  outline-offset: calc(var(--sui-focus-ring-width) * -1);
213
+ }
214
+
215
+ :global([role='listbox'][aria-readonly='true']) .option :global(button) {
216
+ cursor: default;
217
+ }
218
+ :global([role='listbox'][aria-readonly='true']) .option :global(button:not(.focused):is(:hover, :active)) {
219
+ color: var(--sui-control-foreground-color);
220
+ background-color: transparent;
221
+ }
222
+ :global([role='listbox'][aria-readonly='true']) .option :global(button[aria-selected=true]) :global(.icon.check) {
223
+ color: var(--sui-readonly-accent-color);
213
224
  }</style>
@@ -184,19 +184,6 @@
184
184
  .menuitem :global(button:active) {
185
185
  background-color: var(--sui-active-background-color);
186
186
  }
187
- .menuitem :global(:hover) > :global([role=menu]) {
188
- opacity: 1;
189
- }
190
- .menuitem > :global([role=menu]) {
191
- position: absolute;
192
- inset-block-start: 2px;
193
- inset-block-end: auto;
194
- inset-inline-start: calc(100% + 4px);
195
- inset-inline-end: auto;
196
- }
197
- .menuitem > :global([role=menu]:hover) {
198
- opacity: 1;
199
- }
200
187
 
201
188
  .content {
202
189
  flex: auto;
@@ -61,7 +61,7 @@
61
61
  margin: 0;
62
62
  border-width: var(--sui-menu-border-width, 1px);
63
63
  border-style: var(--sui-menu-border-style, solid);
64
- border-color: var(--sui-menu-border-width, var(--sui-secondary-border-color));
64
+ border-color: var(--sui-menu-border-color, var(--sui-secondary-border-color));
65
65
  border-radius: var(--sui-menu-border-radius, 4px);
66
66
  padding: var(--sui-menu-padding, 4px);
67
67
  }
@@ -93,13 +93,13 @@
93
93
  gap: 8px;
94
94
  }
95
95
  }
96
- .radio-group :global([aria-invalid=true]) :global(button) {
96
+ .radio-group[aria-invalid=true] :global(button) {
97
97
  border-color: var(--sui-error-border-color);
98
98
  }
99
- .radio-group :global([aria-invalid=true]) :global(button[aria-checked=true]) {
99
+ .radio-group[aria-invalid=true] :global(button[aria-checked=true]) {
100
100
  border-color: var(--sui-error-border-color);
101
101
  }
102
- .radio-group :global([aria-invalid=true]) :global(button[aria-checked=true]::before) {
102
+ .radio-group[aria-invalid=true] :global(button[aria-checked=true]::before) {
103
103
  background-color: var(--sui-error-border-color);
104
104
  }
105
105
 
@@ -74,6 +74,13 @@
74
74
  const accessibleLabel = $derived(ariaLabel ?? restProps['aria-label']);
75
75
  const hasVisibleLabel = $derived(!!(children || label));
76
76
 
77
+ /**
78
+ * Check if the radio button belongs to a read-only `<RadioGroup>`.
79
+ * @returns {boolean} Result.
80
+ */
81
+ const isGroupReadOnly = () =>
82
+ !!buttonElement?.closest('[role="radiogroup"]')?.matches('[aria-readonly="true"]');
83
+
77
84
  // Sync `checked` with `group` and `value`
78
85
  $effect(() => {
79
86
  if (typeof group === 'string') {
@@ -119,7 +126,9 @@
119
126
  onclick={(event) => {
120
127
  event.preventDefault();
121
128
 
122
- if (disabled || checked) {
129
+ // A read-only group turns the selection down too, but only once this handler has run, so it
130
+ // has to be checked here as well
131
+ if (disabled || checked || isGroupReadOnly()) {
123
132
  return;
124
133
  }
125
134
 
@@ -158,20 +167,20 @@
158
167
  -webkit-user-select: none;
159
168
  user-select: none;
160
169
  }
161
- .radio :global(:hover) :global(button) {
170
+ .radio:hover :global(button) {
162
171
  background-color: var(--sui-hover-background-color);
163
172
  }
164
- .radio :global(:hover) :global(button[aria-checked=true]) {
173
+ .radio:hover :global(button[aria-checked=true]) {
165
174
  border-color: var(--sui-primary-accent-color-light);
166
175
  color: var(--sui-primary-accent-color-text);
167
176
  }
168
- .radio :global(:hover) :global(button[aria-checked=true]::before) {
177
+ .radio:hover :global(button[aria-checked=true]::before) {
169
178
  background-color: var(--sui-primary-accent-color-light);
170
179
  }
171
- .radio :global(:active) :global(button) {
180
+ .radio:active :global(button) {
172
181
  background-color: var(--sui-active-background-color);
173
182
  }
174
- .radio :global(:active) :global(button[aria-checked=true]) {
183
+ .radio:active :global(button[aria-checked=true]) {
175
184
  border-color: var(--sui-primary-accent-color-dark);
176
185
  color: var(--sui-primary-accent-color-dark);
177
186
  }
@@ -207,4 +216,17 @@
207
216
  }
208
217
  .radio label {
209
218
  cursor: inherit;
219
+ }
220
+
221
+ :global([role='radiogroup'][aria-readonly='true']) .radio {
222
+ cursor: default;
223
+ }
224
+ :global([role='radiogroup'][aria-readonly='true']) .radio :global(button), :global([role='radiogroup'][aria-readonly='true']) .radio:is(:hover, :active) :global(button) {
225
+ background-color: var(--sui-checkbox-background-color);
226
+ }
227
+ :global([role='radiogroup'][aria-readonly='true']) .radio :global(button[aria-checked='true']), :global([role='radiogroup'][aria-readonly='true']) .radio:is(:hover, :active) :global(button[aria-checked='true']) {
228
+ border-color: var(--sui-readonly-accent-color);
229
+ }
230
+ :global([role='radiogroup'][aria-readonly='true']) .radio :global(button[aria-checked='true']::before), :global([role='radiogroup'][aria-readonly='true']) .radio:is(:hover, :active) :global(button[aria-checked='true']::before) {
231
+ background-color: var(--sui-readonly-accent-color);
210
232
  }</style>
@@ -477,6 +477,12 @@
477
477
  .combobox div[role=combobox]:is(:hover, :focus) {
478
478
  background-color: var(--sui-hover-background-color);
479
479
  }
480
+ .combobox div[role=combobox][aria-readonly=true] {
481
+ cursor: default;
482
+ }
483
+ .combobox div[role=combobox][aria-readonly=true]:is(:hover, :focus):not(:focus-visible) {
484
+ background-color: var(--sui-disabled-background-color);
485
+ }
480
486
  .combobox div[role=combobox][aria-invalid=true] {
481
487
  border-color: var(--sui-error-border-color);
482
488
  }
@@ -506,9 +512,6 @@
506
512
  .combobox > :global(button[tabindex="-1"]) {
507
513
  pointer-events: none;
508
514
  }
509
- .combobox :global(:not(.editable)) > :global(button) {
510
- background-color: transparent !important;
511
- }
512
515
  .combobox :global(.text-input) {
513
516
  margin: 0 !important;
514
517
  width: 100% !important;
@@ -407,6 +407,19 @@
407
407
  .slider:active .base-bar {
408
408
  background-color: var(--sui-active-background-color);
409
409
  }
410
+ .slider.readonly .base-bar, .slider.readonly:is(:hover, :active) .base-bar {
411
+ background-color: var(--sui-slider-background-color, var(--sui-secondary-background-color));
412
+ }
413
+ .slider.readonly .base,
414
+ .slider.readonly [role=slider] {
415
+ cursor: default;
416
+ }
417
+ .slider.readonly:not(.invalid) .slider-bar {
418
+ background-color: var(--sui-readonly-accent-color);
419
+ }
420
+ .slider.readonly:not(.invalid) [role=slider] {
421
+ border-color: var(--sui-readonly-accent-color);
422
+ }
410
423
 
411
424
  .base {
412
425
  position: relative;
@@ -134,6 +134,19 @@ button[aria-checked=true] span:dir(ltr)::before {
134
134
  button[aria-checked=true] span:dir(rtl)::before {
135
135
  transform: translateX(calc((var(--translateX)) * -1));
136
136
  }
137
+ button[aria-readonly=true] {
138
+ cursor: default;
139
+ }
140
+ button[aria-readonly=true][aria-checked=false] span, button[aria-readonly=true]:is(:hover, :active)[aria-checked=false] span {
141
+ background-color: var(--sui-control-background-color);
142
+ }
143
+ button[aria-readonly=true][aria-checked=true] span, button[aria-readonly=true]:is(:hover, :active)[aria-checked=true] span {
144
+ background-color: var(--sui-readonly-accent-color);
145
+ }
146
+ button[aria-readonly=true][aria-checked=true] span::before, button[aria-readonly=true]:is(:hover, :active)[aria-checked=true] span::before {
147
+ border-color: var(--sui-readonly-accent-color);
148
+ background-color: var(--sui-readonly-accent-color-inverted);
149
+ }
137
150
 
138
151
  span {
139
152
  position: relative;
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Version of this package, used to resolve the prebuilt Shiki engine chunk from a CDN.
3
3
  */
4
- export const UI_VERSION: "0.75.4";
4
+ export const UI_VERSION: "0.75.5";
@@ -3,4 +3,4 @@
3
3
  /**
4
4
  * Version of this package, used to resolve the prebuilt Shiki engine chunk from a CDN.
5
5
  */
6
- export const UI_VERSION = '0.75.4';
6
+ export const UI_VERSION = '0.75.5';
@@ -216,6 +216,12 @@
216
216
  width: stretch;
217
217
  min-width: 0;
218
218
  }
219
+ .number-input:not(.disabled) :global(button[aria-disabled='true']) {
220
+ filter: grayscale(0) opacity(1);
221
+ }
222
+ .number-input:not(.disabled) :global(button[aria-disabled='true']) :global(*) {
223
+ filter: grayscale(1) opacity(var(--sui-disabled-opacity, 0.35));
224
+ }
219
225
  .number-input :global(:not(:first-child)) :global(input) {
220
226
  border-start-start-radius: 0;
221
227
  border-end-start-radius: 0;
@@ -224,12 +230,6 @@
224
230
  border-start-end-radius: 0;
225
231
  border-end-end-radius: 0;
226
232
  }
227
- .number-input :global(:not(.disabled)) :global(button[aria-disabled=true]) {
228
- filter: grayscale(0) opacity(1);
229
- }
230
- .number-input :global(:not(.disabled)) :global(button[aria-disabled=true]) :global(*) {
231
- filter: grayscale(1) opacity(var(--sui-disabled-opacity, 0.35));
232
- }
233
233
  .number-input :global(.text-input) {
234
234
  flex: auto;
235
235
  margin: 0 !important;
@@ -3,7 +3,181 @@
3
3
  Toast/snackbar notification. Use the Popover API if possible to acquire a non-modal top layer.
4
4
  @see https://w3c.github.io/aria/#alert
5
5
  @see https://developer.chrome.com/blog/introducing-popover-api/
6
+ @see https://github.com/whatwg/html/issues/9936
6
7
  -->
8
+ <script module>
9
+ /**
10
+ * Modal dialogs currently open, the topmost last. While a modal dialog is open, the rest of the
11
+ * document is inert, including a popover stacked on top of the dialog, so the toast base has to
12
+ * live inside the topmost dialog to stay interactive and be announced by screen readers.
13
+ * @type {HTMLDialogElement[]}
14
+ */
15
+ const modals = [];
16
+
17
+ /**
18
+ * Get the element the toast base should be placed in.
19
+ * @returns {HTMLElement} Topmost modal dialog, the app shell, or the `<body>`.
20
+ */
21
+ const getHost = () => {
22
+ // Drop the dialogs that have been closed or removed without us noticing
23
+ modals.splice(
24
+ 0,
25
+ modals.length,
26
+ ...modals.filter((dialog) => dialog.isConnected && dialog.matches(':modal')),
27
+ );
28
+
29
+ return modals.at(-1) ?? document.querySelector('.sui.app-shell') ?? document.body;
30
+ };
31
+
32
+ /**
33
+ * Move the toast base to the top of the top layer if it has any toast, or close it otherwise.
34
+ * It has to be reopened, because a modal dialog opened after the base is stacked on top of it.
35
+ * @param {HTMLElement} base Toast base.
36
+ */
37
+ const raiseBase = (base) => {
38
+ // Skip if the browser doesn’t support the Popover API
39
+ /* v8 ignore next */
40
+ if (!base.showPopover) {
41
+ return;
42
+ }
43
+
44
+ if (base.matches(':popover-open')) {
45
+ base.hidePopover();
46
+ }
47
+
48
+ if (base.querySelector('.toast')) {
49
+ base.showPopover();
50
+ }
51
+ };
52
+
53
+ /**
54
+ * Move the toast base to the host returned by {@link getHost}, if it’s not there yet. Use
55
+ * `moveBefore()` where supported, so the base stays open and the focus stays in a toast.
56
+ * @param {HTMLElement} base Toast base.
57
+ * @returns {boolean} Whether the base has been moved.
58
+ */
59
+ const moveBase = (base) => {
60
+ const host = getHost();
61
+
62
+ if (base.parentElement === host) {
63
+ return false;
64
+ }
65
+
66
+ const { moveBefore } = /** @type {any} */ (host);
67
+
68
+ if (typeof moveBefore === 'function' && base.isConnected) {
69
+ moveBefore.call(host, base, null);
70
+ } else {
71
+ host.appendChild(base);
72
+ }
73
+
74
+ return true;
75
+ };
76
+
77
+ /**
78
+ * Toast base shared by all the toasts. It’s created by the first toast mounted and removed with
79
+ * the last one unmounted, rather than rendered by a component, so it doesn’t go away with the
80
+ * toast that happened to create it.
81
+ * @type {HTMLElement | undefined}
82
+ */
83
+ let base;
84
+ /**
85
+ * Number of the toasts currently mounted.
86
+ */
87
+ let mountCount = 0;
88
+
89
+ /**
90
+ * Move the base into a modal dialog that has just been opened.
91
+ * @param {Event} event `toggle` event.
92
+ */
93
+ const onToggle = (event) => {
94
+ const { target, newState } = /** @type {ToggleEvent} */ (event);
95
+
96
+ if (
97
+ base &&
98
+ target instanceof HTMLDialogElement &&
99
+ newState === 'open' &&
100
+ target.matches(':modal') &&
101
+ !modals.includes(target)
102
+ ) {
103
+ modals.push(target);
104
+ moveBase(base);
105
+ raiseBase(base);
106
+ }
107
+ };
108
+
109
+ /**
110
+ * Move the base out of a modal dialog that is being closed, before the dialog is possibly
111
+ * removed from the DOM tree along with the base. `beforetoggle` is fired synchronously when the
112
+ * dialog is closed; `close` is a fallback for browsers that don’t fire it on a dialog.
113
+ * @param {Event} event `beforetoggle` or `close` event.
114
+ */
115
+ const onClose = (event) => {
116
+ const { target } = event;
117
+
118
+ if (
119
+ !base ||
120
+ !(target instanceof HTMLDialogElement) ||
121
+ (event.type === 'beforetoggle' && /** @type {ToggleEvent} */ (event).newState !== 'closed')
122
+ ) {
123
+ return;
124
+ }
125
+
126
+ const index = modals.indexOf(target);
127
+
128
+ if (index > -1) {
129
+ modals.splice(index, 1);
130
+ }
131
+
132
+ if (target.contains(base) && moveBase(base) && !base.matches(':popover-open')) {
133
+ raiseBase(base);
134
+ }
135
+ };
136
+
137
+ /**
138
+ * Get the toast base, creating it if needed, and start keeping it in the topmost modal dialog.
139
+ * Call {@link releaseBase} once done.
140
+ * @returns {HTMLElement} Toast base.
141
+ */
142
+ const acquireBase = () => {
143
+ mountCount += 1;
144
+
145
+ if (!base) {
146
+ base = document.createElement('div');
147
+ base.setAttribute('role', 'none');
148
+ base.className = 'sui toast-base enabled';
149
+ base.popover = 'manual';
150
+ modals.push(...document.querySelectorAll(/** @type {'dialog'} */ ('dialog:modal')));
151
+ // These events don’t bubble, so listen in the capture phase
152
+ document.addEventListener('toggle', onToggle, true);
153
+ document.addEventListener('beforetoggle', onClose, true);
154
+ document.addEventListener('close', onClose, true);
155
+ }
156
+
157
+ moveBase(base);
158
+
159
+ return base;
160
+ };
161
+
162
+ /**
163
+ * Release the toast base, removing it once no toast uses it.
164
+ */
165
+ const releaseBase = () => {
166
+ mountCount -= 1;
167
+
168
+ if (mountCount > 0 || !base) {
169
+ return;
170
+ }
171
+
172
+ document.removeEventListener('toggle', onToggle, true);
173
+ document.removeEventListener('beforetoggle', onClose, true);
174
+ document.removeEventListener('close', onClose, true);
175
+ base.remove();
176
+ base = undefined;
177
+ modals.length = 0;
178
+ };
179
+ </script>
180
+
7
181
  <script>
8
182
  import { onMount, untrack } from 'svelte';
9
183
 
@@ -38,10 +212,6 @@
38
212
  /* eslint-enable prefer-const */
39
213
  } = $props();
40
214
 
41
- /**
42
- * @type {HTMLElement | undefined}
43
- */
44
- let popoverBase = $state();
45
215
  /**
46
216
  * @type {HTMLElement | undefined}
47
217
  */
@@ -70,33 +240,12 @@
70
240
  let rendered = $state(false);
71
241
 
72
242
  onMount(() => {
73
- popover =
74
- /** @type {HTMLElement} */ (document.querySelector('.sui.toast-base.enabled')) ?? undefined;
75
-
76
- if (popover) {
77
- // eslint-disable-next-line svelte/no-dom-manipulating
78
- popoverBase?.remove();
79
- } else {
80
- popover = popoverBase;
81
-
82
- // The base is bound by the time the component is mounted
83
- /* v8 ignore else */
84
- if (popover) {
85
- popover.classList.add('enabled');
86
- (document.querySelector('.sui.app-shell') ?? document.body).appendChild(popover);
87
-
88
- // Move the element to top layer, unless the browser doesn’t support the Popover API
89
- /* v8 ignore else */
90
- if (popover.showPopover) {
91
- popover.popover = 'manual';
92
- popover.showPopover();
93
- }
94
- }
95
- }
243
+ popover = acquireBase();
96
244
 
97
245
  return () => {
98
246
  // eslint-disable-next-line svelte/no-dom-manipulating
99
247
  toast?.remove();
248
+ releaseBase();
100
249
  };
101
250
  });
102
251
 
@@ -124,10 +273,31 @@
124
273
 
125
274
  $effect(() => {
126
275
  // Both are in place by the time the effect first runs; see `onMount()` above
127
- /* v8 ignore else */
128
- if (popover && toast) {
129
- popover.appendChild(toast);
276
+ /* v8 ignore next */
277
+ if (!popover || !toast) {
278
+ return;
130
279
  }
280
+
281
+ // Keep the toast out of the base while it’s hidden, so faded-out toasts don’t pile up there
282
+ if (!rendered) {
283
+ // eslint-disable-next-line svelte/no-dom-manipulating
284
+ toast.remove();
285
+ // A detached element gets no `pointerleave` or `focusout`, so release the hold here, or the
286
+ // countdown would never start the next time the toast is shown
287
+ held = false;
288
+
289
+ // Close the base once the last toast is gone
290
+ if (!popover.querySelector('.toast') && popover.matches(':popover-open')) {
291
+ popover.hidePopover();
292
+ }
293
+
294
+ return;
295
+ }
296
+
297
+ // Make sure the base is in the topmost modal dialog, in case a dialog event has been missed
298
+ moveBase(popover);
299
+ popover.appendChild(toast);
300
+ raiseBase(popover);
131
301
  });
132
302
 
133
303
  $effect(() => {
@@ -169,8 +339,6 @@
169
339
  });
170
340
  </script>
171
341
 
172
- <div bind:this={popoverBase} role="none" class="sui toast-base"></div>
173
-
174
342
  <div
175
343
  {...restProps}
176
344
  bind:this={toast}
@@ -194,7 +362,7 @@
194
362
  {/if}
195
363
  </div>
196
364
 
197
- <style>.toast-base {
365
+ <style>:global(.sui.toast-base) {
198
366
  position: fixed;
199
367
  inset: 16px;
200
368
  z-index: 99999;
@@ -230,6 +398,13 @@
230
398
  opacity: 1;
231
399
  transition-duration: 250ms;
232
400
  will-change: opacity;
401
+ }
402
+ @starting-style {
403
+ .toast {
404
+ opacity: 0;
405
+ }
406
+ }
407
+ .toast {
233
408
  pointer-events: auto;
234
409
  }
235
410
  .toast[aria-hidden=true] {
@@ -7,6 +7,7 @@ type Toast = {
7
7
  * Toast/snackbar notification. Use the Popover API if possible to acquire a non-modal top layer.
8
8
  * @see https://w3c.github.io/aria/#alert
9
9
  * @see https://developer.chrome.com/blog/introducing-popover-api/
10
+ * @see https://github.com/whatwg/html/issues/9936
10
11
  */
11
12
  declare const Toast: import("svelte").Component<{
12
13
  /**
@@ -200,6 +200,20 @@
200
200
  [role=treeitem] > .row:active {
201
201
  background-color: var(--sui-active-background-color);
202
202
  }
203
+ :global([role='tree'][aria-readonly='true']) [role=treeitem] > .row {
204
+ cursor: default;
205
+ }
206
+ :global([role='tree'][aria-readonly='true']) [role=treeitem] > .row:is(:hover, :active) {
207
+ color: var(--sui-tree-foreground-color);
208
+ background-color: transparent;
209
+ }
210
+ :global([role='tree'][aria-readonly='true']) [role=treeitem] > .row > .chevron:not(.placeholder) {
211
+ cursor: pointer;
212
+ }
213
+ :global([role='tree'][aria-readonly='true']) [role=treeitem][aria-selected=true] > .row:is(:hover, :active) {
214
+ color: var(--sui-highlight-foreground-color);
215
+ background-color: var(--sui-selected-background-color);
216
+ }
203
217
  [role=treeitem][aria-expanded=true] > .row > .chevron:dir(ltr) {
204
218
  transform: rotate(90deg);
205
219
  }
@@ -161,7 +161,7 @@
161
161
  --sui-secondary-border-color: hsl(var(--sui-border-color-3-hsl));
162
162
  --sui-error-border-color: hsl(
163
163
  var(--sui-error-color-hue) var(--sui-alert-border-color-saturation)
164
- var(--sui-alert-border-color-lightness)
164
+ var(--sui-error-border-color-lightness, var(--sui-alert-border-color-lightness))
165
165
  );
166
166
  --sui-warning-border-color: hsl(
167
167
  var(--sui-warning-color-hue) var(--sui-alert-border-color-saturation)
@@ -231,6 +231,8 @@
231
231
  --sui-checkbox-height: 20px;
232
232
  --sui-checkbox-border-color: hsl(var(--sui-border-color-1-hsl));
233
233
  --sui-checkbox-background-color: var(--sui-control-background-color);
234
+ --sui-readonly-accent-color: var(--sui-tertiary-foreground-color);
235
+ --sui-readonly-accent-color-inverted: var(--sui-primary-background-color);
234
236
  --sui-option-border-radius: var(--sui-control-medium-border-radius);
235
237
  --sui-option-padding: calc((var(--sui-control-medium-height) / 6))
236
238
  calc((var(--sui-control-medium-height) / 5));
@@ -302,8 +304,8 @@
302
304
  --sui-background-color-4-hsl: var(--sui-base-hue) 5% 92%;
303
305
  --sui-background-color-5-hsl: var(--sui-base-hue) 5% 84%;
304
306
  --sui-border-color-1-hsl: var(--sui-base-hue) 5% 60%;
305
- --sui-border-color-2-hsl: var(--sui-base-hue) 5% 86%;
306
- --sui-border-color-3-hsl: var(--sui-base-hue) 5% 90%;
307
+ --sui-border-color-2-hsl: var(--sui-base-hue) 5% 72%;
308
+ --sui-border-color-3-hsl: var(--sui-base-hue) 5% 80%;
307
309
  --sui-shadow-color: var(--sui-base-hue) 10% 0%;
308
310
  --sui-primary-accent-color-text: hsl(var(--sui-base-hue) 80% 40%);
309
311
  --sui-primary-accent-color-light: hsl(var(--sui-base-hue) 80% 45%);
@@ -322,6 +324,7 @@
322
324
  --sui-alert-background-color-lightness: 90%;
323
325
  --sui-alert-border-color-saturation: 48%;
324
326
  --sui-alert-border-color-lightness: 68%;
327
+ --sui-error-border-color-lightness: 60%;
325
328
  }
326
329
  :global(:root[data-theme=dark]),
327
330
  :global(:host[data-theme=dark]) {
@@ -373,8 +376,8 @@
373
376
  --sui-background-color-4-hsl: var(--sui-base-hue) 5% 92%;
374
377
  --sui-background-color-5-hsl: var(--sui-base-hue) 5% 84%;
375
378
  --sui-border-color-1-hsl: var(--sui-base-hue) 5% 60%;
376
- --sui-border-color-2-hsl: var(--sui-base-hue) 5% 86%;
377
- --sui-border-color-3-hsl: var(--sui-base-hue) 5% 90%;
379
+ --sui-border-color-2-hsl: var(--sui-base-hue) 5% 72%;
380
+ --sui-border-color-3-hsl: var(--sui-base-hue) 5% 80%;
378
381
  --sui-shadow-color: var(--sui-base-hue) 10% 0%;
379
382
  --sui-primary-accent-color-text: hsl(var(--sui-base-hue) 80% 40%);
380
383
  --sui-primary-accent-color-light: hsl(var(--sui-base-hue) 80% 45%);
@@ -393,6 +396,7 @@
393
396
  --sui-alert-background-color-lightness: 90%;
394
397
  --sui-alert-border-color-saturation: 48%;
395
398
  --sui-alert-border-color-lightness: 68%;
399
+ --sui-error-border-color-lightness: 60%;
396
400
  }
397
401
  }
398
402
  @media (prefers-color-scheme: dark) {
@@ -451,6 +455,7 @@
451
455
  --sui-alert-foreground-color-lightness: 18%;
452
456
  --sui-alert-border-color-saturation: 55%;
453
457
  --sui-alert-border-color-lightness: 40%;
458
+ --sui-error-border-color-lightness: 40%;
454
459
  --sui-hover-background-color: hsl(var(--sui-background-color-5-hsl) / 60%);
455
460
  --sui-selected-background-color: hsl(var(--sui-background-color-5-hsl));
456
461
  --sui-active-background-color: hsl(var(--sui-background-color-5-hsl));
@@ -518,6 +523,7 @@
518
523
  --sui-alert-foreground-color-lightness: 18%;
519
524
  --sui-alert-border-color-saturation: 55%;
520
525
  --sui-alert-border-color-lightness: 40%;
526
+ --sui-error-border-color-lightness: 40%;
521
527
  --sui-hover-background-color: hsl(var(--sui-background-color-5-hsl) / 60%);
522
528
  --sui-selected-background-color: hsl(var(--sui-background-color-5-hsl));
523
529
  --sui-active-background-color: hsl(var(--sui-background-color-5-hsl));
@@ -588,6 +594,7 @@
588
594
  --sui-alert-foreground-color-lightness: 18%;
589
595
  --sui-alert-border-color-saturation: 55%;
590
596
  --sui-alert-border-color-lightness: 40%;
597
+ --sui-error-border-color-lightness: 40%;
591
598
  --sui-hover-background-color: hsl(var(--sui-background-color-5-hsl) / 60%);
592
599
  --sui-selected-background-color: hsl(var(--sui-background-color-5-hsl));
593
600
  --sui-active-background-color: hsl(var(--sui-background-color-5-hsl));
@@ -656,6 +663,7 @@
656
663
  --sui-alert-foreground-color-lightness: 18%;
657
664
  --sui-alert-border-color-saturation: 55%;
658
665
  --sui-alert-border-color-lightness: 40%;
666
+ --sui-error-border-color-lightness: 40%;
659
667
  --sui-hover-background-color: hsl(var(--sui-background-color-5-hsl) / 60%);
660
668
  --sui-selected-background-color: hsl(var(--sui-background-color-5-hsl));
661
669
  --sui-active-background-color: hsl(var(--sui-background-color-5-hsl));
@@ -11,8 +11,8 @@
11
11
  --sui-background-color-4-hsl: var(--sui-base-hue) 5% 92%; // tertiary/disabled
12
12
  --sui-background-color-5-hsl: var(--sui-base-hue) 5% 84%; // highlight
13
13
  --sui-border-color-1-hsl: var(--sui-base-hue) 5% 60%; // control
14
- --sui-border-color-2-hsl: var(--sui-base-hue) 5% 86%; // primary
15
- --sui-border-color-3-hsl: var(--sui-base-hue) 5% 90%; // secondary
14
+ --sui-border-color-2-hsl: var(--sui-base-hue) 5% 72%; // primary
15
+ --sui-border-color-3-hsl: var(--sui-base-hue) 5% 80%; // secondary
16
16
  --sui-shadow-color: var(--sui-base-hue) 10% 0%;
17
17
  // Accents
18
18
  --sui-primary-accent-color-text: hsl(var(--sui-base-hue) 80% 40%);
@@ -33,6 +33,8 @@
33
33
  --sui-alert-background-color-lightness: 90%;
34
34
  --sui-alert-border-color-saturation: 48%;
35
35
  --sui-alert-border-color-lightness: 68%;
36
+ // Darker than the other alert borders, as it also marks invalid controls (3:1, WCAG 1.4.11)
37
+ --sui-error-border-color-lightness: 60%;
36
38
  }
37
39
 
38
40
  @mixin dark-theme {
@@ -97,6 +99,7 @@
97
99
  --sui-alert-foreground-color-lightness: 18%;
98
100
  --sui-alert-border-color-saturation: 55%;
99
101
  --sui-alert-border-color-lightness: 40%;
102
+ --sui-error-border-color-lightness: 40%;
100
103
  @include high-contrast-common;
101
104
  }
102
105
 
@@ -239,7 +242,7 @@
239
242
  --sui-secondary-border-color: hsl(var(--sui-border-color-3-hsl));
240
243
  --sui-error-border-color: hsl(
241
244
  var(--sui-error-color-hue) var(--sui-alert-border-color-saturation)
242
- var(--sui-alert-border-color-lightness)
245
+ var(--sui-error-border-color-lightness, var(--sui-alert-border-color-lightness))
243
246
  );
244
247
  --sui-warning-border-color: hsl(
245
248
  var(--sui-warning-color-hue) var(--sui-alert-border-color-saturation)
@@ -314,6 +317,10 @@
314
317
  --sui-checkbox-height: 20px;
315
318
  --sui-checkbox-border-color: hsl(var(--sui-border-color-1-hsl));
316
319
  --sui-checkbox-background-color: var(--sui-control-background-color);
320
+ // Read-only choice controls draw their checked or selected state in a neutral colour instead
321
+ // of the accent, so they don’t look editable while keeping full contrast
322
+ --sui-readonly-accent-color: var(--sui-tertiary-foreground-color);
323
+ --sui-readonly-accent-color-inverted: var(--sui-primary-background-color);
317
324
  // Option & menu item
318
325
  --sui-option-border-radius: var(--sui-control-medium-border-radius);
319
326
  --sui-option-padding: calc((var(--sui-control-medium-height) / 6))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltia/ui",
3
- "version": "0.75.4",
3
+ "version": "0.75.5",
4
4
  "description": "A collection of Svelte components and utilities for building user interfaces.",
5
5
  "repository": {
6
6
  "type": "git",