@delightstack/components 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/SKILL.md +149 -0
- package/bin/agents.js +63 -0
- package/dist/actions/Alert.svelte +202 -0
- package/dist/actions/Alert.svelte.d.ts +36 -0
- package/dist/actions/Alert.svelte.d.ts.map +1 -0
- package/dist/actions/Button.svelte +1450 -0
- package/dist/actions/Button.svelte.d.ts +56 -0
- package/dist/actions/Button.svelte.d.ts.map +1 -0
- package/dist/actions/ButtonGroup.svelte +111 -0
- package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
- package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
- package/dist/actions/CommandPalette.svelte +939 -0
- package/dist/actions/CommandPalette.svelte.d.ts +37 -0
- package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
- package/dist/actions/ContextMenu.svelte +138 -0
- package/dist/actions/ContextMenu.svelte.d.ts +54 -0
- package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
- package/dist/actions/Modal.svelte +474 -0
- package/dist/actions/Modal.svelte.d.ts +28 -0
- package/dist/actions/Modal.svelte.d.ts.map +1 -0
- package/dist/actions/Popover.svelte +1214 -0
- package/dist/actions/Popover.svelte.d.ts +31 -0
- package/dist/actions/Popover.svelte.d.ts.map +1 -0
- package/dist/actions/Portal.svelte +80 -0
- package/dist/actions/Portal.svelte.d.ts +17 -0
- package/dist/actions/Portal.svelte.d.ts.map +1 -0
- package/dist/actions/ThemeToggle.svelte +345 -0
- package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
- package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
- package/dist/actions/index.d.ts +13 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +10 -0
- package/dist/actions/scrollbar.d.ts +48 -0
- package/dist/actions/scrollbar.d.ts.map +1 -0
- package/dist/actions/scrollbar.js +404 -0
- package/dist/display/Accordion.svelte +586 -0
- package/dist/display/Accordion.svelte.d.ts +41 -0
- package/dist/display/Accordion.svelte.d.ts.map +1 -0
- package/dist/display/Avatar.svelte +527 -0
- package/dist/display/Avatar.svelte.d.ts +22 -0
- package/dist/display/Avatar.svelte.d.ts.map +1 -0
- package/dist/display/AvatarGroup.svelte +298 -0
- package/dist/display/AvatarGroup.svelte.d.ts +31 -0
- package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
- package/dist/display/Calendar.svelte +1366 -0
- package/dist/display/Calendar.svelte.d.ts +58 -0
- package/dist/display/Calendar.svelte.d.ts.map +1 -0
- package/dist/display/Chart.svelte +1426 -0
- package/dist/display/Chart.svelte.d.ts +35 -0
- package/dist/display/Chart.svelte.d.ts.map +1 -0
- package/dist/display/Code.svelte +780 -0
- package/dist/display/Code.svelte.d.ts +19 -0
- package/dist/display/Code.svelte.d.ts.map +1 -0
- package/dist/display/Comparison.svelte +686 -0
- package/dist/display/Comparison.svelte.d.ts +22 -0
- package/dist/display/Comparison.svelte.d.ts.map +1 -0
- package/dist/display/Counter.svelte +285 -0
- package/dist/display/Counter.svelte.d.ts +21 -0
- package/dist/display/Counter.svelte.d.ts.map +1 -0
- package/dist/display/Expand.svelte +48 -0
- package/dist/display/Expand.svelte.d.ts +9 -0
- package/dist/display/Expand.svelte.d.ts.map +1 -0
- package/dist/display/List.svelte +294 -0
- package/dist/display/List.svelte.d.ts +40 -0
- package/dist/display/List.svelte.d.ts.map +1 -0
- package/dist/display/ListContextReset.svelte +19 -0
- package/dist/display/ListContextReset.svelte.d.ts +7 -0
- package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
- package/dist/display/ListItem.svelte +834 -0
- package/dist/display/ListItem.svelte.d.ts +22 -0
- package/dist/display/ListItem.svelte.d.ts.map +1 -0
- package/dist/display/QR.svelte +1193 -0
- package/dist/display/QR.svelte.d.ts +23 -0
- package/dist/display/QR.svelte.d.ts.map +1 -0
- package/dist/display/SplitPane.svelte +744 -0
- package/dist/display/SplitPane.svelte.d.ts +25 -0
- package/dist/display/SplitPane.svelte.d.ts.map +1 -0
- package/dist/display/Stat.svelte +439 -0
- package/dist/display/Stat.svelte.d.ts +24 -0
- package/dist/display/Stat.svelte.d.ts.map +1 -0
- package/dist/display/Table.svelte +4654 -0
- package/dist/display/Table.svelte.d.ts +249 -0
- package/dist/display/Table.svelte.d.ts.map +1 -0
- package/dist/display/TableCellEditor.svelte +935 -0
- package/dist/display/TableCellEditor.svelte.d.ts +58 -0
- package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
- package/dist/display/Timeline.svelte +1258 -0
- package/dist/display/Timeline.svelte.d.ts +43 -0
- package/dist/display/Timeline.svelte.d.ts.map +1 -0
- package/dist/display/Tree.svelte +1740 -0
- package/dist/display/Tree.svelte.d.ts +74 -0
- package/dist/display/Tree.svelte.d.ts.map +1 -0
- package/dist/display/Typewriter.svelte +338 -0
- package/dist/display/Typewriter.svelte.d.ts +22 -0
- package/dist/display/Typewriter.svelte.d.ts.map +1 -0
- package/dist/display/index.d.ts +24 -0
- package/dist/display/index.d.ts.map +1 -0
- package/dist/display/index.js +18 -0
- package/dist/feedback/Callout.svelte +529 -0
- package/dist/feedback/Callout.svelte.d.ts +24 -0
- package/dist/feedback/Callout.svelte.d.ts.map +1 -0
- package/dist/feedback/Confetti.svelte +631 -0
- package/dist/feedback/Confetti.svelte.d.ts +90 -0
- package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
- package/dist/feedback/Progress.svelte +382 -0
- package/dist/feedback/Progress.svelte.d.ts +25 -0
- package/dist/feedback/Progress.svelte.d.ts.map +1 -0
- package/dist/feedback/Toast.svelte +967 -0
- package/dist/feedback/Toast.svelte.d.ts +54 -0
- package/dist/feedback/Toast.svelte.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +7 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/index.js +4 -0
- package/dist/form/Checkbox.svelte +449 -0
- package/dist/form/Checkbox.svelte.d.ts +27 -0
- package/dist/form/Checkbox.svelte.d.ts.map +1 -0
- package/dist/form/Fieldset.svelte +410 -0
- package/dist/form/Fieldset.svelte.d.ts +22 -0
- package/dist/form/Fieldset.svelte.d.ts.map +1 -0
- package/dist/form/FileUpload.svelte +934 -0
- package/dist/form/FileUpload.svelte.d.ts +41 -0
- package/dist/form/FileUpload.svelte.d.ts.map +1 -0
- package/dist/form/Form.svelte +530 -0
- package/dist/form/Form.svelte.d.ts +120 -0
- package/dist/form/Form.svelte.d.ts.map +1 -0
- package/dist/form/Input.svelte +2858 -0
- package/dist/form/Input.svelte.d.ts +66 -0
- package/dist/form/Input.svelte.d.ts.map +1 -0
- package/dist/form/Radio.svelte +507 -0
- package/dist/form/Radio.svelte.d.ts +39 -0
- package/dist/form/Radio.svelte.d.ts.map +1 -0
- package/dist/form/Range.svelte +912 -0
- package/dist/form/Range.svelte.d.ts +33 -0
- package/dist/form/Range.svelte.d.ts.map +1 -0
- package/dist/form/Rating.svelte +429 -0
- package/dist/form/Rating.svelte.d.ts +28 -0
- package/dist/form/Rating.svelte.d.ts.map +1 -0
- package/dist/form/Select.svelte +1933 -0
- package/dist/form/Select.svelte.d.ts +54 -0
- package/dist/form/Select.svelte.d.ts.map +1 -0
- package/dist/form/Toggle.svelte +645 -0
- package/dist/form/Toggle.svelte.d.ts +50 -0
- package/dist/form/Toggle.svelte.d.ts.map +1 -0
- package/dist/form/index.d.ts +15 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +10 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/layout/README.md +172 -0
- package/dist/media/Carousel.svelte +2424 -0
- package/dist/media/Carousel.svelte.d.ts +47 -0
- package/dist/media/Carousel.svelte.d.ts.map +1 -0
- package/dist/media/Gallery.svelte +2881 -0
- package/dist/media/Gallery.svelte.d.ts +82 -0
- package/dist/media/Gallery.svelte.d.ts.map +1 -0
- package/dist/media/Image.svelte +389 -0
- package/dist/media/Image.svelte.d.ts +33 -0
- package/dist/media/Image.svelte.d.ts.map +1 -0
- package/dist/media/PDF.svelte +1793 -0
- package/dist/media/PDF.svelte.d.ts +44 -0
- package/dist/media/PDF.svelte.d.ts.map +1 -0
- package/dist/media/Panorama.svelte +1391 -0
- package/dist/media/Panorama.svelte.d.ts +47 -0
- package/dist/media/Panorama.svelte.d.ts.map +1 -0
- package/dist/media/Video.svelte +2501 -0
- package/dist/media/Video.svelte.d.ts +58 -0
- package/dist/media/Video.svelte.d.ts.map +1 -0
- package/dist/media/carousel.d.ts +211 -0
- package/dist/media/carousel.d.ts.map +1 -0
- package/dist/media/carousel.js +408 -0
- package/dist/media/index.d.ts +11 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +5 -0
- package/dist/navigation/BottomSheet.svelte +636 -0
- package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
- package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
- package/dist/navigation/Breadcrumbs.svelte +611 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
- package/dist/navigation/Pagination.svelte +641 -0
- package/dist/navigation/Pagination.svelte.d.ts +27 -0
- package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
- package/dist/navigation/Steps.svelte +965 -0
- package/dist/navigation/Steps.svelte.d.ts +43 -0
- package/dist/navigation/Steps.svelte.d.ts.map +1 -0
- package/dist/navigation/Tabs.svelte +698 -0
- package/dist/navigation/Tabs.svelte.d.ts +41 -0
- package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
- package/dist/navigation/index.d.ts +8 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +5 -0
- package/package.json +139 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface SelectOption {
|
|
2
|
+
/** The value committed when this option is chosen */
|
|
3
|
+
value: unknown;
|
|
4
|
+
/** Display text for the option */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Whether this option cannot be selected */
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
/** Secondary descriptive text shown under the label */
|
|
9
|
+
description?: string;
|
|
10
|
+
/** Group heading this option is listed under */
|
|
11
|
+
group?: string;
|
|
12
|
+
}
|
|
13
|
+
import { type Snippet } from 'svelte';
|
|
14
|
+
declare const Select: import("svelte").Component<{
|
|
15
|
+
value?: unknown;
|
|
16
|
+
options?: SelectOption[];
|
|
17
|
+
multiple?: boolean;
|
|
18
|
+
searchable?: boolean;
|
|
19
|
+
clearable?: boolean;
|
|
20
|
+
creatable?: boolean;
|
|
21
|
+
loading?: boolean;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
placeholder?: string | undefined;
|
|
24
|
+
label?: string | undefined;
|
|
25
|
+
error?: string | undefined;
|
|
26
|
+
parse?: ((value: unknown) => unknown) | undefined;
|
|
27
|
+
description?: string | undefined;
|
|
28
|
+
required?: boolean;
|
|
29
|
+
size?: "0" | "1" | "2" | "3";
|
|
30
|
+
skeleton?: boolean;
|
|
31
|
+
tooltip?: string | undefined;
|
|
32
|
+
dense?: boolean;
|
|
33
|
+
comfortable?: boolean;
|
|
34
|
+
filled?: boolean;
|
|
35
|
+
id?: string;
|
|
36
|
+
name?: string | undefined;
|
|
37
|
+
class?: string;
|
|
38
|
+
onchange?: ((detail: {
|
|
39
|
+
value: unknown;
|
|
40
|
+
}) => void) | undefined;
|
|
41
|
+
onsearch?: ((detail: {
|
|
42
|
+
query: string;
|
|
43
|
+
}) => void) | undefined;
|
|
44
|
+
oncreate?: ((detail: {
|
|
45
|
+
value: string;
|
|
46
|
+
}) => boolean | void | SelectOption) | undefined;
|
|
47
|
+
onopen?: (() => void) | undefined;
|
|
48
|
+
onclose?: (() => void) | undefined;
|
|
49
|
+
render_value?: Snippet<[SelectOption | SelectOption[]]> | undefined;
|
|
50
|
+
option?: Snippet<[SelectOption]> | undefined;
|
|
51
|
+
}, {}, "value">;
|
|
52
|
+
type Select = ReturnType<typeof Select>;
|
|
53
|
+
export default Select;
|
|
54
|
+
//# sourceMappingURL=Select.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Select.svelte.d.ts","sourceRoot":"","sources":["../../src/form/Select.svelte.ts"],"names":[],"mappings":"AAGC,MAAM,WAAW,YAAY;IAC5B,qDAAqD;IACrD,KAAK,EAAE,OAAO,CAAC;IACf,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAIF,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AA45BlD,QAAA,MAAM,MAAM;YA94BqE,OAAO;cAAY,YAAY,EAAE;eAAa,OAAO;iBAAe,OAAO;gBAAc,OAAO;gBAAc,OAAO;cAAY,OAAO;eAAa,OAAO;kBAAgB,MAAM,GAAG,SAAS;YAAU,MAAM,GAAG,SAAS;YAAU,MAAM,GAAG,SAAS;YAAU,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,GAAG,SAAS;kBAAgB,MAAM,GAAG,SAAS;eAAa,OAAO;WAAS,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG;eAAa,OAAO;cAAY,MAAM,GAAG,SAAS;YAAU,OAAO;kBAAgB,OAAO;aAAW,OAAO;;WAA6B,MAAM,GAAG,SAAS;YAAU,MAAM;eAAa,CAAC,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,SAAS;eAAa,CAAC,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,SAAS;eAAe,CAAC,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,GAAG,IAAI,GAAG,YAAY,CAAC,GAC9zB,SAAS;aAAW,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS;cAAY,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS;mBAAiB,OAAO,CAAC,CAAC,YAAY,GAAG,YAAY,EAAE,CAAC,CAAC,GAAG,SAAS;aAAW,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,GAAG,SAAS;eA64BpJ,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
<script lang="ts" generics="Indeterminate extends boolean = false">
|
|
2
|
+
import { tooltip } from '@delightstack/utilities';
|
|
3
|
+
import { getContext, type Snippet } from 'svelte';
|
|
4
|
+
import type { FormContext } from './Form.svelte';
|
|
5
|
+
|
|
6
|
+
/** `boolean` normally; widened to `boolean | null` in indeterminate mode */
|
|
7
|
+
type Checked = Indeterminate extends true ? boolean | null : boolean;
|
|
8
|
+
|
|
9
|
+
const propId = $props.id();
|
|
10
|
+
let {
|
|
11
|
+
/**
|
|
12
|
+
* Whether the toggle is checked. In three-state mode this can also be
|
|
13
|
+
* `null` — the in-between state, shown as a third stop in the middle of
|
|
14
|
+
* the track. When omitted inside a Form (with a name), the state is
|
|
15
|
+
* driven by the form data instead.
|
|
16
|
+
*/
|
|
17
|
+
checked = $bindable() as Checked | undefined,
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether the toggle supports a third, in-between state. When true,
|
|
21
|
+
* `checked` is `boolean | null` and clicking cycles
|
|
22
|
+
* false → null → true → false; the track also lengthens so all three
|
|
23
|
+
* thumb stops keep distinct touch targets.
|
|
24
|
+
*/
|
|
25
|
+
indeterminate = false as Indeterminate,
|
|
26
|
+
|
|
27
|
+
/** Tri-state mode (set by optional non-defaulted boolean form fields):
|
|
28
|
+
* enables the same three-stop track as `indeterminate`, with
|
|
29
|
+
* null/undefined meaning "unanswered" (the middle stop). Unlike
|
|
30
|
+
* Checkbox, the user can cycle back to the middle state. */
|
|
31
|
+
tristate = false,
|
|
32
|
+
|
|
33
|
+
/** The field's default value (set by defaulted boolean form fields):
|
|
34
|
+
* shown when the form data has no value yet, so the display matches
|
|
35
|
+
* what saving would persist. */
|
|
36
|
+
default_checked = undefined as boolean | undefined,
|
|
37
|
+
|
|
38
|
+
/** An error message shown below the toggle */
|
|
39
|
+
error = undefined as string | undefined,
|
|
40
|
+
|
|
41
|
+
/** Parses & validates the value (e.g. a database table form field's
|
|
42
|
+
* `parse`). Inside a Form it is registered with the form, which runs
|
|
43
|
+
* it on the form's validation timing. */
|
|
44
|
+
parse = undefined as ((value: unknown) => unknown) | undefined,
|
|
45
|
+
|
|
46
|
+
/** Whether the toggle is disabled */
|
|
47
|
+
disabled = false,
|
|
48
|
+
|
|
49
|
+
/** Size preset: 0=32x18, 1=44x24, 2=52x28, 3=68x36 */
|
|
50
|
+
size = '1' as '0' | '1' | '2' | '3',
|
|
51
|
+
|
|
52
|
+
/** Label text displayed alongside the toggle */
|
|
53
|
+
label = undefined as string | undefined,
|
|
54
|
+
|
|
55
|
+
/** Position of the label relative to the toggle */
|
|
56
|
+
label_position = 'end' as 'start' | 'end',
|
|
57
|
+
|
|
58
|
+
/** Label displayed when toggle is on */
|
|
59
|
+
on_label = undefined as string | undefined,
|
|
60
|
+
|
|
61
|
+
/** Label displayed when toggle is off */
|
|
62
|
+
off_label = undefined as string | undefined,
|
|
63
|
+
|
|
64
|
+
/** Name attribute for the hidden input */
|
|
65
|
+
name = undefined as string | undefined,
|
|
66
|
+
|
|
67
|
+
/** Value attribute for the hidden input */
|
|
68
|
+
value = undefined as string | undefined,
|
|
69
|
+
|
|
70
|
+
/** Tooltip message shown on hover */
|
|
71
|
+
tooltip: tooltip_message = undefined as string | undefined,
|
|
72
|
+
|
|
73
|
+
/** Whether the toggle uses dense spacing */
|
|
74
|
+
dense = false,
|
|
75
|
+
|
|
76
|
+
/** Whether the toggle uses comfortable spacing */
|
|
77
|
+
comfortable = false,
|
|
78
|
+
|
|
79
|
+
/** The id of the toggle element */
|
|
80
|
+
id = propId,
|
|
81
|
+
|
|
82
|
+
/** Custom class name */
|
|
83
|
+
class: class_name = '',
|
|
84
|
+
|
|
85
|
+
/** Snippet for a custom icon inside the thumb */
|
|
86
|
+
thumb_icon = undefined as Snippet | undefined,
|
|
87
|
+
|
|
88
|
+
/** Called when the toggle value changes */
|
|
89
|
+
onchange = undefined as ((detail: { checked: Checked }) => void) | undefined,
|
|
90
|
+
} = $props();
|
|
91
|
+
|
|
92
|
+
let pressed = $state(false);
|
|
93
|
+
|
|
94
|
+
/* ------------------------------------------------------------------ */
|
|
95
|
+
/* Form context integration */
|
|
96
|
+
/* ------------------------------------------------------------------ */
|
|
97
|
+
|
|
98
|
+
const form_ctx = getContext<FormContext | undefined>('form');
|
|
99
|
+
let track_element = $state<HTMLElement | undefined>(undefined);
|
|
100
|
+
|
|
101
|
+
$effect(() => {
|
|
102
|
+
if (!form_ctx || !name) return;
|
|
103
|
+
if (track_element) form_ctx.register(name, track_element, parse);
|
|
104
|
+
return () => {
|
|
105
|
+
if (name) form_ctx.unregister(name);
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/** Whether the three-stop track is active (explicit prop or tri-state field) */
|
|
110
|
+
const three_state = $derived(!!indeterminate || tristate);
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Context-driven mode: inside a Form, with a name, and no checked prop,
|
|
114
|
+
* the toggle mirrors the form data (e.g. an entity's draft) —
|
|
115
|
+
* `<Toggle {...field.is_public} />` needs no bind:checked.
|
|
116
|
+
*/
|
|
117
|
+
const context_driven = !!(form_ctx && name && checked === undefined);
|
|
118
|
+
|
|
119
|
+
$effect(() => {
|
|
120
|
+
if (!context_driven || !form_ctx || !name) return;
|
|
121
|
+
const ctx_value = form_ctx.getValue(name);
|
|
122
|
+
let next: Checked;
|
|
123
|
+
if (ctx_value === undefined || ctx_value === null) {
|
|
124
|
+
// Unanswered: three-state shows the middle stop, defaulted fields
|
|
125
|
+
// show their default, plain booleans show off
|
|
126
|
+
next = (three_state ? null : (default_checked ?? false)) as Checked;
|
|
127
|
+
} else {
|
|
128
|
+
next = Boolean(ctx_value) as Checked;
|
|
129
|
+
}
|
|
130
|
+
if (next !== checked) checked = next;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/** Error from running `parse` standalone. Inside a Form the form runs
|
|
134
|
+
* `parse` instead (it was registered above), so this never sets there. */
|
|
135
|
+
let parse_error = $state<string | undefined>(undefined);
|
|
136
|
+
|
|
137
|
+
function runParse() {
|
|
138
|
+
if (!parse || form_ctx) return;
|
|
139
|
+
try {
|
|
140
|
+
parse(checked);
|
|
141
|
+
parse_error = undefined;
|
|
142
|
+
} catch (e) {
|
|
143
|
+
parse_error = e instanceof Error ? e.message : 'Invalid value';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Error from the local prop, standalone parse, or form context */
|
|
148
|
+
const resolved_error = $derived.by(() => {
|
|
149
|
+
if (error !== undefined) return error;
|
|
150
|
+
if (parse_error) return parse_error;
|
|
151
|
+
if (form_ctx && name && form_ctx.errors[name]) return form_ctx.errors[name];
|
|
152
|
+
return undefined;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
/** The effective state — an omitted checked prop means the middle stop
|
|
156
|
+
* (three-state) or off, until the form context supplies a value. */
|
|
157
|
+
const current = $derived(
|
|
158
|
+
(checked === undefined
|
|
159
|
+
? three_state && context_driven
|
|
160
|
+
? null
|
|
161
|
+
: false
|
|
162
|
+
: checked) as Checked,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const state_label = $derived(
|
|
166
|
+
current === true ? on_label : current === false ? off_label : undefined,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
function setChecked(next: Checked) {
|
|
170
|
+
if (next === checked) return;
|
|
171
|
+
checked = next;
|
|
172
|
+
if (form_ctx && name) {
|
|
173
|
+
form_ctx.setValue(name, checked);
|
|
174
|
+
form_ctx.setTouched(name);
|
|
175
|
+
} else {
|
|
176
|
+
runParse();
|
|
177
|
+
}
|
|
178
|
+
onchange?.({ checked });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function toggle() {
|
|
182
|
+
if (disabled) return;
|
|
183
|
+
if (three_state) {
|
|
184
|
+
// Cycle off → middle → on → off (matching the legacy three-state toggle)
|
|
185
|
+
setChecked((current === false ? null : current === null ? true : false) as Checked);
|
|
186
|
+
} else {
|
|
187
|
+
setChecked(!current as Checked);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
192
|
+
if (e.key === ' ' || e.key === 'Enter') {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
toggle();
|
|
195
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
|
196
|
+
// Arrows step between stops directly (no cycling), so a three-state
|
|
197
|
+
// toggle can go null -> false without passing through true.
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
const order = (three_state ? [false, null, true] : [false, true]) as Checked[];
|
|
200
|
+
const next = order[order.indexOf(current) + (e.key === 'ArrowRight' ? 1 : -1)];
|
|
201
|
+
if (next !== undefined) setChecked(next);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* ------------------------------------------------------------------ */
|
|
206
|
+
/* Thumb dragging */
|
|
207
|
+
/* */
|
|
208
|
+
/* The thumb can be dragged straight to any stop (so a three-state */
|
|
209
|
+
/* toggle can go null -> false in one gesture). While dragging, the */
|
|
210
|
+
/* thumb follows the pointer through a magnetic "stop gravity" curve */
|
|
211
|
+
/* (same shape as Range's tick gravity): it lags near a stop and only */
|
|
212
|
+
/* reaches full pointer-follow at the midpoint between stops, so each */
|
|
213
|
+
/* stop — including the centre — has a felt basin. Past the track */
|
|
214
|
+
/* ends a tanh rubber band resists harder the further you pull. On */
|
|
215
|
+
/* release the inline transform is dropped and the thumb springs to */
|
|
216
|
+
/* its stop via the CSS spring transition. */
|
|
217
|
+
/* ------------------------------------------------------------------ */
|
|
218
|
+
|
|
219
|
+
let dragging = $state(false);
|
|
220
|
+
let drag_x = $state(0);
|
|
221
|
+
/** Swallows the click the label synthesizes right after a drag ends */
|
|
222
|
+
let recently_dragged = false;
|
|
223
|
+
let drag_origin = 0; // viewport x where translateX(0) puts the thumb's left edge
|
|
224
|
+
let drag_travel = 0; // max translateX while pressed (thumb is press-widened)
|
|
225
|
+
let drag_half_thumb = 0;
|
|
226
|
+
let drag_start_client_x = 0;
|
|
227
|
+
|
|
228
|
+
/** The thumb stops — translateX px paired with the value each represents */
|
|
229
|
+
function dragStops(): { x: number; value: Checked }[] {
|
|
230
|
+
if (three_state) {
|
|
231
|
+
return [
|
|
232
|
+
{ x: 0, value: false as Checked },
|
|
233
|
+
{ x: drag_travel / 2, value: null as Checked },
|
|
234
|
+
{ x: drag_travel, value: true as Checked },
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
return [
|
|
238
|
+
{ x: 0, value: false as Checked },
|
|
239
|
+
{ x: drag_travel, value: true as Checked },
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function onTrackPointerDown(e: PointerEvent) {
|
|
244
|
+
if (disabled) return;
|
|
245
|
+
pressed = true;
|
|
246
|
+
const track = e.currentTarget as HTMLElement;
|
|
247
|
+
const rect = track.getBoundingClientRect();
|
|
248
|
+
const cs = getComputedStyle(track);
|
|
249
|
+
const thumb_size = parseFloat(cs.getPropertyValue('--thumb-size'));
|
|
250
|
+
const offset = parseFloat(cs.getPropertyValue('--thumb-offset'));
|
|
251
|
+
const grow = parseFloat(cs.getPropertyValue('--thumb-press-grow'));
|
|
252
|
+
/* Measure against the press-widened thumb so the drag stops land exactly
|
|
253
|
+
on the .pressed CSS stop positions. */
|
|
254
|
+
const thumb_w = thumb_size + grow;
|
|
255
|
+
drag_origin = rect.left + offset;
|
|
256
|
+
drag_travel = rect.width - thumb_w - offset * 2;
|
|
257
|
+
drag_half_thumb = thumb_w / 2;
|
|
258
|
+
drag_start_client_x = e.clientX;
|
|
259
|
+
try {
|
|
260
|
+
track.setPointerCapture(e.pointerId);
|
|
261
|
+
} catch {
|
|
262
|
+
/* pointer already gone */
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function onTrackPointerMove(e: PointerEvent) {
|
|
267
|
+
if (!pressed || disabled) return;
|
|
268
|
+
if (!dragging) {
|
|
269
|
+
/* A few px of slop so taps stay clicks (the label's native click
|
|
270
|
+
handles those) */
|
|
271
|
+
if (Math.abs(e.clientX - drag_start_client_x) < 3) return;
|
|
272
|
+
dragging = true;
|
|
273
|
+
}
|
|
274
|
+
updateDrag(e.clientX);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function updateDrag(client_x: number) {
|
|
278
|
+
const desired = client_x - drag_origin - drag_half_thumb;
|
|
279
|
+
const stops = dragStops();
|
|
280
|
+
const last = stops[stops.length - 1];
|
|
281
|
+
|
|
282
|
+
if (desired < 0 || desired > last.x) {
|
|
283
|
+
/* Rubber band past the ends — tanh saturates, so resistance grows the
|
|
284
|
+
further you pull and the track feels like it's pulling back. */
|
|
285
|
+
const edge = desired < 0 ? stops[0] : last;
|
|
286
|
+
const overflow = desired - edge.x;
|
|
287
|
+
const max_shift = drag_half_thumb * 0.8;
|
|
288
|
+
drag_x = edge.x + max_shift * Math.tanh(overflow / 40);
|
|
289
|
+
setChecked(edge.value);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* Magnetic stop gravity: ease from a slow near-stop crawl to full
|
|
294
|
+
pointer-follow exactly at the midpoint between stops — continuous
|
|
295
|
+
across basins, so the thumb never jumps as the value snaps. */
|
|
296
|
+
let nearest = stops[0];
|
|
297
|
+
for (const s of stops) {
|
|
298
|
+
if (Math.abs(desired - s.x) < Math.abs(desired - nearest.x)) nearest = s;
|
|
299
|
+
}
|
|
300
|
+
const half_step = drag_travel / (stops.length - 1) / 2;
|
|
301
|
+
const pull = desired - nearest.x;
|
|
302
|
+
const t = half_step > 0 ? Math.min(1, Math.abs(pull) / half_step) : 1;
|
|
303
|
+
const gravity = 0.15;
|
|
304
|
+
const eased = gravity * t + (1 - gravity) * t * t;
|
|
305
|
+
drag_x = nearest.x + Math.sign(pull) * eased * half_step;
|
|
306
|
+
setChecked(nearest.value);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* Fires after pointerup (capture auto-releases) AND on pointercancel, so
|
|
310
|
+
one handler ends the gesture for taps, drags and aborted touches alike. */
|
|
311
|
+
function endDrag() {
|
|
312
|
+
pressed = false;
|
|
313
|
+
if (!dragging) return;
|
|
314
|
+
dragging = false;
|
|
315
|
+
recently_dragged = true;
|
|
316
|
+
setTimeout(() => (recently_dragged = false), 300);
|
|
317
|
+
}
|
|
318
|
+
</script>
|
|
319
|
+
|
|
320
|
+
<label
|
|
321
|
+
class={['toggle', `size-${size}`, class_name].filter(Boolean).join(' ')}
|
|
322
|
+
class:checked={current === true}
|
|
323
|
+
class:mixed={current === null}
|
|
324
|
+
class:indeterminate={three_state}
|
|
325
|
+
class:has-error={!!resolved_error}
|
|
326
|
+
class:disabled
|
|
327
|
+
class:dense
|
|
328
|
+
class:comfortable
|
|
329
|
+
class:pressed
|
|
330
|
+
class:dragging
|
|
331
|
+
class:label-start={label_position === 'start'}
|
|
332
|
+
for={id}
|
|
333
|
+
{@attach tooltip_message ? tooltip(tooltip_message) : () => {}}>
|
|
334
|
+
{#if label && label_position === 'start'}
|
|
335
|
+
<span class="label">{label}</span>
|
|
336
|
+
{/if}
|
|
337
|
+
|
|
338
|
+
<input
|
|
339
|
+
type="checkbox"
|
|
340
|
+
{name}
|
|
341
|
+
{value}
|
|
342
|
+
{id}
|
|
343
|
+
{disabled}
|
|
344
|
+
checked={current === true}
|
|
345
|
+
indeterminate={current === null}
|
|
346
|
+
onclick={(e) => {
|
|
347
|
+
/* A drag just set the value directly — swallow the synthesized label
|
|
348
|
+
click so it can't immediately cycle the value again. */
|
|
349
|
+
if (recently_dragged) {
|
|
350
|
+
e.preventDefault();
|
|
351
|
+
e.stopPropagation();
|
|
352
|
+
}
|
|
353
|
+
}}
|
|
354
|
+
onchange={(e) => {
|
|
355
|
+
toggle();
|
|
356
|
+
/* The native click already flipped the box; pin the DOM back to the
|
|
357
|
+
component's (possibly three-state) value. The reactive attributes
|
|
358
|
+
above can't be relied on here — when e.g. false → null, the derived
|
|
359
|
+
`checked === true` is false both before and after, so Svelte sees
|
|
360
|
+
no change to flush while the browser has flipped the property. */
|
|
361
|
+
e.currentTarget.checked = current === true;
|
|
362
|
+
e.currentTarget.indeterminate = current === null;
|
|
363
|
+
}} />
|
|
364
|
+
|
|
365
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
366
|
+
<!-- The dynamic role is always interactive (checkbox/switch), Svelte just
|
|
367
|
+
can't see that statically -->
|
|
368
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
369
|
+
<span
|
|
370
|
+
bind:this={track_element}
|
|
371
|
+
class="track"
|
|
372
|
+
role={three_state ? 'checkbox' : 'switch'}
|
|
373
|
+
aria-checked={current === null ? 'mixed' : current === true}
|
|
374
|
+
tabindex={disabled ? -1 : 0}
|
|
375
|
+
onkeydown={onKeyDown}
|
|
376
|
+
onpointerdown={onTrackPointerDown}
|
|
377
|
+
onpointermove={onTrackPointerMove}
|
|
378
|
+
onlostpointercapture={endDrag}>
|
|
379
|
+
<span
|
|
380
|
+
class="thumb"
|
|
381
|
+
style:transform={dragging ? `translateX(${drag_x}px)` : undefined}>
|
|
382
|
+
{#if thumb_icon}
|
|
383
|
+
<span class="thumb-icon">{@render thumb_icon()}</span>
|
|
384
|
+
{/if}
|
|
385
|
+
</span>
|
|
386
|
+
</span>
|
|
387
|
+
|
|
388
|
+
{#if state_label}
|
|
389
|
+
<span class="state-label">{state_label}</span>
|
|
390
|
+
{/if}
|
|
391
|
+
|
|
392
|
+
{#if label && label_position === 'end'}
|
|
393
|
+
<span class="label">{label}</span>
|
|
394
|
+
{/if}
|
|
395
|
+
|
|
396
|
+
{#if resolved_error}
|
|
397
|
+
<span class="error-text">{resolved_error}</span>
|
|
398
|
+
{/if}
|
|
399
|
+
</label>
|
|
400
|
+
|
|
401
|
+
<style>
|
|
402
|
+
/* Visually-hidden native checkbox, kept for form submission + a11y */
|
|
403
|
+
input {
|
|
404
|
+
position: absolute;
|
|
405
|
+
width: 1px;
|
|
406
|
+
height: 1px;
|
|
407
|
+
padding: 0;
|
|
408
|
+
margin: -1px;
|
|
409
|
+
overflow: hidden;
|
|
410
|
+
clip: rect(0, 0, 0, 0);
|
|
411
|
+
white-space: nowrap;
|
|
412
|
+
border: 0;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.toggle {
|
|
416
|
+
--track-width: 44px;
|
|
417
|
+
--track-height: 24px;
|
|
418
|
+
--thumb-size: 18px;
|
|
419
|
+
--thumb-offset: 3px;
|
|
420
|
+
/* The rendered track width — indeterminate mode stretches it (below) */
|
|
421
|
+
--_track-width: var(--track-width);
|
|
422
|
+
--thumb-travel: calc(
|
|
423
|
+
var(--_track-width) - var(--thumb-size) - var(--thumb-offset) * 2
|
|
424
|
+
);
|
|
425
|
+
--thumb-press-grow: 4px;
|
|
426
|
+
|
|
427
|
+
/* Off-state palette: a mid-tone neutral track (clearly visible against
|
|
428
|
+
the page bg in BOTH schemes — bg-muted all but vanished in dark mode)
|
|
429
|
+
under a near-white neutral thumb. High handle/track contrast, and no
|
|
430
|
+
brand-tinted thumb fighting a gray track; the on state keeps the
|
|
431
|
+
saturated action-colored pair. */
|
|
432
|
+
--_track-off: var(--color-border-active, light-dark(hsl(0 0% 72%), hsl(0 0% 52%)));
|
|
433
|
+
--_thumb-off: light-dark(#fff, hsl(0 0% 95%));
|
|
434
|
+
|
|
435
|
+
display: inline-flex;
|
|
436
|
+
align-items: center;
|
|
437
|
+
flex-wrap: wrap;
|
|
438
|
+
gap: 0.625em;
|
|
439
|
+
cursor: pointer;
|
|
440
|
+
user-select: none;
|
|
441
|
+
-webkit-tap-highlight-color: transparent;
|
|
442
|
+
position: relative;
|
|
443
|
+
|
|
444
|
+
&.label-start {
|
|
445
|
+
flex-direction: row-reverse;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* Sizes */
|
|
449
|
+
&.size-0 {
|
|
450
|
+
--track-width: 32px;
|
|
451
|
+
--track-height: 18px;
|
|
452
|
+
--thumb-size: 12px;
|
|
453
|
+
--thumb-offset: 3px;
|
|
454
|
+
--thumb-press-grow: 2px;
|
|
455
|
+
font-size: var(--control-font-0, 0.875rem);
|
|
456
|
+
}
|
|
457
|
+
&.size-1 {
|
|
458
|
+
--track-width: 44px;
|
|
459
|
+
--track-height: 24px;
|
|
460
|
+
--thumb-size: 18px;
|
|
461
|
+
--thumb-offset: 3px;
|
|
462
|
+
--thumb-press-grow: 4px;
|
|
463
|
+
font-size: var(--control-font-1, 1rem);
|
|
464
|
+
}
|
|
465
|
+
&.size-2 {
|
|
466
|
+
--track-width: 52px;
|
|
467
|
+
--track-height: 28px;
|
|
468
|
+
--thumb-size: 22px;
|
|
469
|
+
--thumb-offset: 3px;
|
|
470
|
+
--thumb-press-grow: 4px;
|
|
471
|
+
font-size: var(--control-font-2, 1.125rem);
|
|
472
|
+
}
|
|
473
|
+
&.size-3 {
|
|
474
|
+
--track-width: 68px;
|
|
475
|
+
--track-height: 36px;
|
|
476
|
+
--thumb-size: 28px;
|
|
477
|
+
--thumb-offset: 4px;
|
|
478
|
+
--thumb-press-grow: 6px;
|
|
479
|
+
font-size: var(--control-font-3, 1.25rem);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
&.dense {
|
|
483
|
+
gap: 0.375em;
|
|
484
|
+
}
|
|
485
|
+
&.comfortable {
|
|
486
|
+
gap: 1em;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* Indeterminate mode adds a third (middle) thumb stop, so the track gets
|
|
490
|
+
more runway — each stop keeps a distinct, comfortably-sized touch
|
|
491
|
+
target. --thumb-travel derives from --_track-width, so the stops spread
|
|
492
|
+
out with it automatically. */
|
|
493
|
+
&.indeterminate {
|
|
494
|
+
--_track-width: calc(var(--track-width) * 1.25);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/* Track */
|
|
499
|
+
.track {
|
|
500
|
+
position: relative;
|
|
501
|
+
display: inline-flex;
|
|
502
|
+
align-items: center;
|
|
503
|
+
width: var(--_track-width);
|
|
504
|
+
height: var(--track-height);
|
|
505
|
+
border-radius: var(--track-height);
|
|
506
|
+
background-color: var(--_track-off);
|
|
507
|
+
transition:
|
|
508
|
+
background-color 0.2s ease,
|
|
509
|
+
transform 200ms ease;
|
|
510
|
+
flex-shrink: 0;
|
|
511
|
+
outline: none;
|
|
512
|
+
/* Horizontal drags are ours; vertical pans stay with the browser (a
|
|
513
|
+
vertical scroll mid-gesture fires pointercancel and ends the drag). */
|
|
514
|
+
touch-action: pan-y;
|
|
515
|
+
|
|
516
|
+
/* Pressed dip — perspective is baked into the transform so the recede
|
|
517
|
+
is centred on the track itself, not on the whole labelled control
|
|
518
|
+
(a parent `perspective` made the track lean toward the label). */
|
|
519
|
+
&:active {
|
|
520
|
+
transform: perspective(100px)
|
|
521
|
+
translate3d(0, 1px, clamp(-10px, calc(0.2em - 12px), -2px));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.disabled .track:active {
|
|
526
|
+
transform: none;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.track:focus-visible {
|
|
530
|
+
outline: 2px solid var(--color-border-active, currentColor);
|
|
531
|
+
outline-offset: 2px;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.checked .track {
|
|
535
|
+
background-color: var(--color-action, hsl(220 70% 55%));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/* Thumb */
|
|
539
|
+
.thumb {
|
|
540
|
+
position: absolute;
|
|
541
|
+
left: var(--thumb-offset);
|
|
542
|
+
width: var(--thumb-size);
|
|
543
|
+
height: var(--thumb-size);
|
|
544
|
+
border-radius: 50%;
|
|
545
|
+
background-color: var(--_thumb-off);
|
|
546
|
+
display: flex;
|
|
547
|
+
align-items: center;
|
|
548
|
+
justify-content: center;
|
|
549
|
+
transform: translateX(0);
|
|
550
|
+
cursor: grab;
|
|
551
|
+
transition:
|
|
552
|
+
transform 300ms var(--ease-spring, cubic-bezier(0.34, 1.56, 0.64, 1)),
|
|
553
|
+
background-color 0.2s ease,
|
|
554
|
+
width 0.15s ease,
|
|
555
|
+
left 0.15s ease;
|
|
556
|
+
box-shadow: 0 1px 3px rgb(0 0 0 / 0.2);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/* Instant pointer tracking while dragging — the inline transform drives
|
|
560
|
+
the position; the spring above only plays on release/settle. */
|
|
561
|
+
.dragging .thumb {
|
|
562
|
+
cursor: grabbing;
|
|
563
|
+
transition:
|
|
564
|
+
background-color 0.2s ease,
|
|
565
|
+
width 0.15s ease,
|
|
566
|
+
left 0.15s ease;
|
|
567
|
+
}
|
|
568
|
+
.dragging .track {
|
|
569
|
+
cursor: grabbing;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/* On: thumb returns to the action-text tint so it pairs with the action
|
|
573
|
+
track (the off thumb is neutral — see --_thumb-off). */
|
|
574
|
+
.checked .thumb {
|
|
575
|
+
transform: translateX(var(--thumb-travel));
|
|
576
|
+
background-color: var(--color-action-text, white);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/* Middle stop (indeterminate `null`) — halfway along the track */
|
|
580
|
+
.mixed .thumb {
|
|
581
|
+
transform: translateX(calc(var(--thumb-travel) / 2));
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/* Press state: widen thumb */
|
|
585
|
+
.pressed:not(.disabled) .thumb {
|
|
586
|
+
width: calc(var(--thumb-size) + var(--thumb-press-grow));
|
|
587
|
+
}
|
|
588
|
+
.pressed.checked:not(.disabled) .thumb {
|
|
589
|
+
width: calc(var(--thumb-size) + var(--thumb-press-grow));
|
|
590
|
+
transform: translateX(calc(var(--thumb-travel) - var(--thumb-press-grow)));
|
|
591
|
+
}
|
|
592
|
+
/* A pressed middle thumb grows symmetrically so it stays centred */
|
|
593
|
+
.pressed.mixed:not(.disabled) .thumb {
|
|
594
|
+
width: calc(var(--thumb-size) + var(--thumb-press-grow));
|
|
595
|
+
transform: translateX(calc((var(--thumb-travel) - var(--thumb-press-grow)) / 2));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.thumb-icon {
|
|
599
|
+
display: flex;
|
|
600
|
+
align-items: center;
|
|
601
|
+
justify-content: center;
|
|
602
|
+
font-size: calc(var(--thumb-size) * 0.6);
|
|
603
|
+
line-height: 1;
|
|
604
|
+
color: var(--color-action, hsl(220 70% 55%));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/* Disabled */
|
|
608
|
+
.disabled {
|
|
609
|
+
cursor: not-allowed;
|
|
610
|
+
opacity: 0.5;
|
|
611
|
+
pointer-events: none;
|
|
612
|
+
}
|
|
613
|
+
.disabled .track,
|
|
614
|
+
.disabled .thumb {
|
|
615
|
+
pointer-events: auto;
|
|
616
|
+
cursor: not-allowed;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/* Labels — the pressed dip bakes its own perspective like the track, so
|
|
620
|
+
each piece recedes toward its own centre */
|
|
621
|
+
.label {
|
|
622
|
+
color: var(--color-text, inherit);
|
|
623
|
+
line-height: 1.4;
|
|
624
|
+
transition: transform 200ms ease;
|
|
625
|
+
&:active {
|
|
626
|
+
transform: perspective(100px)
|
|
627
|
+
translate3d(0, 1px, clamp(-10px, calc(0.2em - 12px), -2px));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
.state-label {
|
|
631
|
+
color: var(--color-text, inherit);
|
|
632
|
+
font-size: 0.875em;
|
|
633
|
+
line-height: 1.4;
|
|
634
|
+
transition: transform 200ms ease;
|
|
635
|
+
&:active {
|
|
636
|
+
transform: perspective(100px)
|
|
637
|
+
translate3d(0, 1px, clamp(-10px, calc(0.2em - 12px), -2px));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
.error-text {
|
|
641
|
+
width: 100%;
|
|
642
|
+
font-size: 0.8em;
|
|
643
|
+
color: var(--color-error, #d32f2f);
|
|
644
|
+
}
|
|
645
|
+
</style>
|