@sveltia/ui 0.75.3 → 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.
Files changed (30) hide show
  1. package/dist/components/button/button.svelte +2 -4
  2. package/dist/components/button/select-button-group.svelte +6 -3
  3. package/dist/components/calendar/calendar.svelte +8 -3
  4. package/dist/components/checkbox/checkbox.svelte +11 -0
  5. package/dist/components/listbox/listbox.svelte +3 -0
  6. package/dist/components/listbox/option.svelte +14 -2
  7. package/dist/components/menu/menu-item.svelte +0 -13
  8. package/dist/components/menu/menu.svelte +1 -1
  9. package/dist/components/radio/radio-group.svelte +3 -3
  10. package/dist/components/radio/radio.svelte +30 -8
  11. package/dist/components/radio/radio.svelte.d.ts +4 -4
  12. package/dist/components/select/combobox.svelte +6 -3
  13. package/dist/components/select/select-tags.svelte +3 -2
  14. package/dist/components/slider/slider.svelte +24 -13
  15. package/dist/components/switch/switch.svelte +13 -0
  16. package/dist/components/text-editor/shiki/version.d.ts +1 -1
  17. package/dist/components/text-editor/shiki/version.js +1 -1
  18. package/dist/components/text-field/number-input.svelte +6 -6
  19. package/dist/components/toast/toast.svelte +208 -33
  20. package/dist/components/toast/toast.svelte.d.ts +1 -0
  21. package/dist/components/tree/tree-item.svelte +18 -3
  22. package/dist/components/tree/tree-item.svelte.d.ts +4 -4
  23. package/dist/components/util/app-shell.svelte +13 -5
  24. package/dist/services/select.svelte.js +2 -14
  25. package/dist/services/value.d.ts +2 -0
  26. package/dist/services/value.js +42 -0
  27. package/dist/styles/variables.scss +10 -3
  28. package/dist/typedefs.d.ts +2 -2
  29. package/dist/typedefs.js +2 -2
  30. package/package.json +6 -6
@@ -6,6 +6,7 @@
6
6
  -->
7
7
  <script>
8
8
  import { activateKeyShortcuts } from '@sveltia/utils/events';
9
+ import { getValueType } from '../../services/value.js';
9
10
  import TruncatedText from '../typography/truncated-text.svelte';
10
11
  import Popup from '../util/popup.svelte';
11
12
 
@@ -25,7 +26,7 @@
25
26
  name = undefined,
26
27
  value = undefined,
27
28
  // svelte-ignore state_referenced_locally
28
- valueType = typeof value,
29
+ valueType = getValueType(value),
29
30
  hidden = false,
30
31
  disabled = false,
31
32
  readonly = false,
@@ -289,7 +290,4 @@ button .label {
289
290
  }
290
291
  button > :global(*) {
291
292
  pointer-events: none;
292
- }
293
- button :global(:has([slot=start-icon] + [slot=end-icon])) {
294
- gap: 0;
295
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
  }
@@ -7,6 +7,7 @@
7
7
  <script>
8
8
  import { onDestroy } from 'svelte';
9
9
  import { getOptionRegistry } from '../../services/select.svelte.js';
10
+ import { getValueType } from '../../services/value.js';
10
11
  import Button from '../button/button.svelte';
11
12
  import Icon from '../icon/icon.svelte';
12
13
 
@@ -92,7 +93,7 @@
92
93
  * @returns {string} Type.
93
94
  */
94
95
  get type() {
95
- return restProps.valueType ?? typeof value;
96
+ return restProps.valueType ?? getValueType(value);
96
97
  },
97
98
  /**
98
99
  * Get whether the option is selected.
@@ -172,7 +173,7 @@
172
173
  .option:focus-visible {
173
174
  outline-width: 0 !important;
174
175
  }
175
- .option :global(.wrap) :global(button) {
176
+ .option.wrap :global(button) {
176
177
  white-space: normal;
177
178
  }
178
179
  .option :global(button) {
@@ -209,4 +210,15 @@
209
210
  :global([role='listbox']:focus-visible) .option :global(.focused) {
210
211
  outline-color: var(--sui-focus-ring-color);
211
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);
212
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
 
@@ -22,8 +22,8 @@
22
22
  * attribute.
23
23
  * @property {string} [name] The `data-name` attribute on the `<button>` element.
24
24
  * @property {any} [value] The `data-value` attribute on the `<button>` element.
25
- * @property {string} [valueType] Data type of the `value`. Typically `string`, `number` or
26
- * `boolean`. Default: auto detect.
25
+ * @property {string} [valueType] Data type of the `value`. Typically `string`, `number`,
26
+ * `boolean` or `null`. Default: auto detect.
27
27
  * @property {string} [label] Text label displayed next to the radio button.
28
28
  * @property {string} [ariaLabel] `aria-label` attribute on the `<button>` element, for a radio
29
29
  * button without a visible label.
@@ -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>
@@ -37,8 +37,8 @@ declare const Radio: import("svelte").Component<{
37
37
  */
38
38
  value?: any;
39
39
  /**
40
- * Data type of the `value`. Typically `string`, `number` or
41
- * `boolean`. Default: auto detect.
40
+ * Data type of the `value`. Typically `string`, `number`,
41
+ * `boolean` or `null`. Default: auto detect.
42
42
  */
43
43
  valueType?: string | undefined;
44
44
  /**
@@ -101,8 +101,8 @@ type Props = {
101
101
  */
102
102
  value?: any;
103
103
  /**
104
- * Data type of the `value`. Typically `string`, `number` or
105
- * `boolean`. Default: auto detect.
104
+ * Data type of the `value`. Typically `string`, `number`,
105
+ * `boolean` or `null`. Default: auto detect.
106
106
  */
107
107
  valueType?: string | undefined;
108
108
  /**
@@ -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;
@@ -250,9 +250,10 @@
250
250
  {required}
251
251
  {invalid}
252
252
  onChange={() => {
253
- // The select only reports a change once an option has been picked
253
+ // The select only reports a change once an option has been picked. Check for `undefined`
254
+ // rather than a falsy value, as `null`, `false`, `0` and an empty string are valid values
254
255
  /* v8 ignore else */
255
- if (selectedValue) {
256
+ if (selectedValue !== undefined) {
256
257
  values = [...values, selectedValue];
257
258
  onAddValue?.(new CustomEvent('AddValue', { detail: { value: selectedValue } }));
258
259
  // Reset the combobox
@@ -167,20 +167,18 @@
167
167
  event.preventDefault();
168
168
  event.stopPropagation();
169
169
 
170
- if (index > -1) {
171
- if (
172
- multiThumb &&
173
- wouldCrossThumbs({ valueIndex, targetPosition: positionList[index], sliderPositions })
174
- ) {
175
- return;
176
- }
170
+ if (
171
+ multiThumb &&
172
+ wouldCrossThumbs({ valueIndex, targetPosition: positionList[index], sliderPositions })
173
+ ) {
174
+ return;
175
+ }
177
176
 
178
- if (multiThumb) {
179
- /** @type {[number, number]} */ (values)[valueIndex] = valueList[index];
180
- values = [.../** @type {[number, number]} */ (values)];
181
- } else {
182
- value = valueList[index];
183
- }
177
+ if (multiThumb) {
178
+ /** @type {[number, number]} */ (values)[valueIndex] = valueList[index];
179
+ values = [.../** @type {[number, number]} */ (values)];
180
+ } else {
181
+ value = valueList[index];
184
182
  }
185
183
  };
186
184
 
@@ -409,6 +407,19 @@
409
407
  .slider:active .base-bar {
410
408
  background-color: var(--sui-active-background-color);
411
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
+ }
412
423
 
413
424
  .base {
414
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.3";
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.3';
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
  /**
@@ -8,6 +8,7 @@
8
8
  <script>
9
9
  import { isRTL } from '@sveltia/i18n';
10
10
  import { getContext, setContext } from 'svelte';
11
+ import { getValueType } from '../../services/value.js';
11
12
  import Icon from '../icon/icon.svelte';
12
13
 
13
14
  /**
@@ -29,8 +30,8 @@
29
30
  * attribute. Ignored if the item has no `items` slot content.
30
31
  * @property {string} [label] Text label displayed on the item.
31
32
  * @property {any} [value] The `data-value` attribute on the item. Default: the `label`.
32
- * @property {string} [valueType] Data type of the `value`. Typically `string`, `number` or
33
- * `boolean`. Default: auto detect.
33
+ * @property {string} [valueType] Data type of the `value`. Typically `string`, `number`,
34
+ * `boolean` or `null`. Default: auto detect.
34
35
  * @property {Snippet} [children] Primary slot content, used instead of the `label`.
35
36
  * @property {Snippet} [items] Child items slot content, which makes the item a parent node.
36
37
  * @property {Snippet} [startIcon] Start icon slot content.
@@ -57,7 +58,7 @@
57
58
  // svelte-ignore state_referenced_locally
58
59
  value = label,
59
60
  // svelte-ignore state_referenced_locally
60
- valueType = typeof value,
61
+ valueType = getValueType(value),
61
62
  children,
62
63
  items,
63
64
  startIcon,
@@ -199,6 +200,20 @@
199
200
  [role=treeitem] > .row:active {
200
201
  background-color: var(--sui-active-background-color);
201
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
+ }
202
217
  [role=treeitem][aria-expanded=true] > .row > .chevron:dir(ltr) {
203
218
  transform: rotate(90deg);
204
219
  }
@@ -43,8 +43,8 @@ declare const TreeItem: import("svelte").Component<import("../../typedefs").Keyb
43
43
  */
44
44
  value?: any;
45
45
  /**
46
- * Data type of the `value`. Typically `string`, `number` or
47
- * `boolean`. Default: auto detect.
46
+ * Data type of the `value`. Typically `string`, `number`,
47
+ * `boolean` or `null`. Default: auto detect.
48
48
  */
49
49
  valueType?: string | undefined;
50
50
  /**
@@ -116,8 +116,8 @@ type Props = {
116
116
  */
117
117
  value?: any;
118
118
  /**
119
- * Data type of the `value`. Typically `string`, `number` or
120
- * `boolean`. Default: auto detect.
119
+ * Data type of the `value`. Typically `string`, `number`,
120
+ * `boolean` or `null`. Default: auto detect.
121
121
  */
122
122
  valueType?: string | undefined;
123
123
  /**
@@ -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));
@@ -1,4 +1,5 @@
1
1
  import { getContext, setContext } from 'svelte';
2
+ import { castValue } from './value.js';
2
3
 
3
4
  /**
4
5
  * @import { OptionEntry, SelectedItemDetail } from '../typedefs';
@@ -115,20 +116,7 @@ export const getOptionRegistry = () => getContext(CONTEXT_KEY);
115
116
  */
116
117
  export const getSelectedItemDetail = (target) => {
117
118
  const { type = 'string', name, label } = target.dataset;
118
- /** @type {any} */
119
- let { value } = target.dataset;
120
-
121
- if (type === 'number') {
122
- value = Number(value);
123
-
124
- if (Number.isNaN(value)) {
125
- value = null;
126
- }
127
- } else if (type === 'boolean') {
128
- value = value === 'true';
129
- } else if (type === 'string') {
130
- value = value ? String(value) : '';
131
- }
119
+ const value = castValue(target.dataset.value, type);
132
120
 
133
121
  return { target, type, name, label, value };
134
122
  };
@@ -0,0 +1,2 @@
1
+ export function getValueType(value: any): string;
2
+ export function castValue(value: string | undefined, type: string): any;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Conversion of the values held by buttons and other items: a value is stored in the `data-value`
3
+ * attribute along with its type in the `data-type` attribute, so it can be cast back to the
4
+ * original type when the item is selected.
5
+ */
6
+
7
+ /**
8
+ * Get the data type of a value, to be stored in the `data-type` attribute. `null` gets its own type
9
+ * because `typeof null` is `object`, and the `data-value` attribute is omitted for `null`.
10
+ * @param {any} value Value.
11
+ * @returns {string} Data type, e.g. `string`, `number`, `boolean` or `null`.
12
+ */
13
+ export const getValueType = (value) => (value === null ? 'null' : typeof value);
14
+
15
+ /**
16
+ * Cast a value read from the `data-value` attribute back to its original type.
17
+ * @param {string | undefined} value Attribute value, or `undefined` if the attribute is omitted.
18
+ * @param {string} type Data type stored in the `data-type` attribute.
19
+ * @returns {any} Cast value. Any other type than `number`, `boolean`, `null` and `string` is
20
+ * returned as is.
21
+ */
22
+ export const castValue = (value, type) => {
23
+ if (type === 'number') {
24
+ const number = Number(value);
25
+
26
+ return Number.isNaN(number) ? null : number;
27
+ }
28
+
29
+ if (type === 'boolean') {
30
+ return value === 'true';
31
+ }
32
+
33
+ if (type === 'null') {
34
+ return null;
35
+ }
36
+
37
+ if (type === 'string') {
38
+ return value ? String(value) : '';
39
+ }
40
+
41
+ return value;
42
+ };
@@ -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))
@@ -24,8 +24,8 @@ export type ButtonProps = {
24
24
  */
25
25
  value?: any;
26
26
  /**
27
- * Data type of the `value`. Typically `string`, `number` or
28
- * `boolean`. Default: auto detect.
27
+ * Data type of the `value`. Typically `string`, `number`, `boolean`
28
+ * or `null`. Default: auto detect.
29
29
  */
30
30
  valueType?: string | undefined;
31
31
  /**
package/dist/typedefs.js CHANGED
@@ -12,8 +12,8 @@
12
12
  * @property {string} [role] The `role` attribute on the `<button>` element.
13
13
  * @property {string} [name] The `data-name` attribute on the `<button>` element.
14
14
  * @property {any} [value] The `data-value` attribute on the `<button>` element.
15
- * @property {string} [valueType] Data type of the `value`. Typically `string`, `number` or
16
- * `boolean`. Default: auto detect.
15
+ * @property {string} [valueType] Data type of the `value`. Typically `string`, `number`, `boolean`
16
+ * or `null`. Default: auto detect.
17
17
  * @property {boolean} [hidden] Whether to hide the widget. An alias of the `aria-hidden` attribute.
18
18
  * @property {boolean} [disabled] Whether to disable the widget. An alias of the `aria-disabled`
19
19
  * attribute.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltia/ui",
3
- "version": "0.75.3",
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",
@@ -64,16 +64,16 @@
64
64
  "eslint-config-airbnb-extended": "^3.2.0",
65
65
  "eslint-config-prettier": "^10.1.8",
66
66
  "eslint-plugin-import": "^2.32.0",
67
- "eslint-plugin-jsdoc": "^64.5.2",
68
- "eslint-plugin-package-json": "^1.8.1",
67
+ "eslint-plugin-jsdoc": "^64.5.4",
68
+ "eslint-plugin-package-json": "^1.9.0",
69
69
  "eslint-plugin-svelte": "^3.23.0",
70
70
  "globals": "^17.12.0",
71
71
  "happy-dom": "^20.14.5",
72
- "oxlint": "^1.83.0",
72
+ "oxlint": "^1.85.0",
73
73
  "playwright": "^1.63.0",
74
74
  "postcss": "^8.5.28",
75
75
  "postcss-html": "^2.0.0",
76
- "prettier": "^3.9.7",
76
+ "prettier": "^3.9.8",
77
77
  "prettier-plugin-svelte": "^4.1.1",
78
78
  "rolldown": "^1.2.9",
79
79
  "sass": "^1.104.1",
@@ -81,7 +81,7 @@
81
81
  "stylelint": "^17.15.0",
82
82
  "stylelint-config-recommended-scss": "^17.0.1",
83
83
  "stylelint-scss": "^7.3.0",
84
- "svelte": "^5.57.0",
84
+ "svelte": "^5.57.1",
85
85
  "svelte-check": "^4.7.6",
86
86
  "svelte-preprocess": "^6.0.5",
87
87
  "tslib": "^2.8.1",