@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.
- package/dist/components/button/button.svelte +2 -4
- package/dist/components/button/select-button-group.svelte +6 -3
- package/dist/components/calendar/calendar.svelte +8 -3
- package/dist/components/checkbox/checkbox.svelte +11 -0
- package/dist/components/listbox/listbox.svelte +3 -0
- package/dist/components/listbox/option.svelte +14 -2
- package/dist/components/menu/menu-item.svelte +0 -13
- package/dist/components/menu/menu.svelte +1 -1
- package/dist/components/radio/radio-group.svelte +3 -3
- package/dist/components/radio/radio.svelte +30 -8
- package/dist/components/radio/radio.svelte.d.ts +4 -4
- package/dist/components/select/combobox.svelte +6 -3
- package/dist/components/select/select-tags.svelte +3 -2
- package/dist/components/slider/slider.svelte +24 -13
- package/dist/components/switch/switch.svelte +13 -0
- package/dist/components/text-editor/shiki/version.d.ts +1 -1
- package/dist/components/text-editor/shiki/version.js +1 -1
- package/dist/components/text-field/number-input.svelte +6 -6
- package/dist/components/toast/toast.svelte +208 -33
- package/dist/components/toast/toast.svelte.d.ts +1 -0
- package/dist/components/tree/tree-item.svelte +18 -3
- package/dist/components/tree/tree-item.svelte.d.ts +4 -4
- package/dist/components/util/app-shell.svelte +13 -5
- package/dist/services/select.svelte.js +2 -14
- package/dist/services/value.d.ts +2 -0
- package/dist/services/value.js +42 -0
- package/dist/styles/variables.scss +10 -3
- package/dist/typedefs.d.ts +2 -2
- package/dist/typedefs.js +2 -2
- 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 =
|
|
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
|
|
97
|
+
.select-button-group[aria-invalid=true] :global(button) {
|
|
98
98
|
border-color: var(--sui-error-border-color);
|
|
99
99
|
}
|
|
100
|
-
.select-button-group
|
|
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
|
|
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
|
|
263
|
-
|
|
264
|
-
|
|
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 ??
|
|
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
|
|
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-
|
|
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
|
|
96
|
+
.radio-group[aria-invalid=true] :global(button) {
|
|
97
97
|
border-color: var(--sui-error-border-color);
|
|
98
98
|
}
|
|
99
|
-
.radio-group
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
170
|
+
.radio:hover :global(button) {
|
|
162
171
|
background-color: var(--sui-hover-background-color);
|
|
163
172
|
}
|
|
164
|
-
.radio
|
|
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
|
|
177
|
+
.radio:hover :global(button[aria-checked=true]::before) {
|
|
169
178
|
background-color: var(--sui-primary-accent-color-light);
|
|
170
179
|
}
|
|
171
|
-
.radio
|
|
180
|
+
.radio:active :global(button) {
|
|
172
181
|
background-color: var(--sui-active-background-color);
|
|
173
182
|
}
|
|
174
|
-
.radio
|
|
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
|
|
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
|
|
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 (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
170
|
+
if (
|
|
171
|
+
multiThumb &&
|
|
172
|
+
wouldCrossThumbs({ valueIndex, targetPosition: positionList[index], sliderPositions })
|
|
173
|
+
) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
177
176
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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;
|
|
@@ -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
|
|
128
|
-
if (popover
|
|
129
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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%
|
|
306
|
-
--sui-border-color-3-hsl: var(--sui-base-hue) 5%
|
|
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%
|
|
377
|
-
--sui-border-color-3-hsl: var(--sui-base-hue) 5%
|
|
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
|
-
|
|
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,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%
|
|
15
|
-
--sui-border-color-3-hsl: var(--sui-base-hue) 5%
|
|
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/dist/typedefs.d.ts
CHANGED
|
@@ -24,8 +24,8 @@ export type ButtonProps = {
|
|
|
24
24
|
*/
|
|
25
25
|
value?: any;
|
|
26
26
|
/**
|
|
27
|
-
* Data type of the `value`. Typically `string`, `number`
|
|
28
|
-
* `
|
|
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`
|
|
16
|
-
* `
|
|
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
|
+
"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.
|
|
68
|
-
"eslint-plugin-package-json": "^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.
|
|
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.
|
|
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.
|
|
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",
|