@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.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/SKILL.md +149 -0
  4. package/bin/agents.js +63 -0
  5. package/dist/actions/Alert.svelte +202 -0
  6. package/dist/actions/Alert.svelte.d.ts +36 -0
  7. package/dist/actions/Alert.svelte.d.ts.map +1 -0
  8. package/dist/actions/Button.svelte +1450 -0
  9. package/dist/actions/Button.svelte.d.ts +56 -0
  10. package/dist/actions/Button.svelte.d.ts.map +1 -0
  11. package/dist/actions/ButtonGroup.svelte +111 -0
  12. package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
  13. package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
  14. package/dist/actions/CommandPalette.svelte +939 -0
  15. package/dist/actions/CommandPalette.svelte.d.ts +37 -0
  16. package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
  17. package/dist/actions/ContextMenu.svelte +138 -0
  18. package/dist/actions/ContextMenu.svelte.d.ts +54 -0
  19. package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
  20. package/dist/actions/Modal.svelte +474 -0
  21. package/dist/actions/Modal.svelte.d.ts +28 -0
  22. package/dist/actions/Modal.svelte.d.ts.map +1 -0
  23. package/dist/actions/Popover.svelte +1214 -0
  24. package/dist/actions/Popover.svelte.d.ts +31 -0
  25. package/dist/actions/Popover.svelte.d.ts.map +1 -0
  26. package/dist/actions/Portal.svelte +80 -0
  27. package/dist/actions/Portal.svelte.d.ts +17 -0
  28. package/dist/actions/Portal.svelte.d.ts.map +1 -0
  29. package/dist/actions/ThemeToggle.svelte +345 -0
  30. package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
  31. package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
  32. package/dist/actions/index.d.ts +13 -0
  33. package/dist/actions/index.d.ts.map +1 -0
  34. package/dist/actions/index.js +10 -0
  35. package/dist/actions/scrollbar.d.ts +48 -0
  36. package/dist/actions/scrollbar.d.ts.map +1 -0
  37. package/dist/actions/scrollbar.js +404 -0
  38. package/dist/display/Accordion.svelte +586 -0
  39. package/dist/display/Accordion.svelte.d.ts +41 -0
  40. package/dist/display/Accordion.svelte.d.ts.map +1 -0
  41. package/dist/display/Avatar.svelte +527 -0
  42. package/dist/display/Avatar.svelte.d.ts +22 -0
  43. package/dist/display/Avatar.svelte.d.ts.map +1 -0
  44. package/dist/display/AvatarGroup.svelte +298 -0
  45. package/dist/display/AvatarGroup.svelte.d.ts +31 -0
  46. package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
  47. package/dist/display/Calendar.svelte +1366 -0
  48. package/dist/display/Calendar.svelte.d.ts +58 -0
  49. package/dist/display/Calendar.svelte.d.ts.map +1 -0
  50. package/dist/display/Chart.svelte +1426 -0
  51. package/dist/display/Chart.svelte.d.ts +35 -0
  52. package/dist/display/Chart.svelte.d.ts.map +1 -0
  53. package/dist/display/Code.svelte +780 -0
  54. package/dist/display/Code.svelte.d.ts +19 -0
  55. package/dist/display/Code.svelte.d.ts.map +1 -0
  56. package/dist/display/Comparison.svelte +686 -0
  57. package/dist/display/Comparison.svelte.d.ts +22 -0
  58. package/dist/display/Comparison.svelte.d.ts.map +1 -0
  59. package/dist/display/Counter.svelte +285 -0
  60. package/dist/display/Counter.svelte.d.ts +21 -0
  61. package/dist/display/Counter.svelte.d.ts.map +1 -0
  62. package/dist/display/Expand.svelte +48 -0
  63. package/dist/display/Expand.svelte.d.ts +9 -0
  64. package/dist/display/Expand.svelte.d.ts.map +1 -0
  65. package/dist/display/List.svelte +294 -0
  66. package/dist/display/List.svelte.d.ts +40 -0
  67. package/dist/display/List.svelte.d.ts.map +1 -0
  68. package/dist/display/ListContextReset.svelte +19 -0
  69. package/dist/display/ListContextReset.svelte.d.ts +7 -0
  70. package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
  71. package/dist/display/ListItem.svelte +834 -0
  72. package/dist/display/ListItem.svelte.d.ts +22 -0
  73. package/dist/display/ListItem.svelte.d.ts.map +1 -0
  74. package/dist/display/QR.svelte +1193 -0
  75. package/dist/display/QR.svelte.d.ts +23 -0
  76. package/dist/display/QR.svelte.d.ts.map +1 -0
  77. package/dist/display/SplitPane.svelte +744 -0
  78. package/dist/display/SplitPane.svelte.d.ts +25 -0
  79. package/dist/display/SplitPane.svelte.d.ts.map +1 -0
  80. package/dist/display/Stat.svelte +439 -0
  81. package/dist/display/Stat.svelte.d.ts +24 -0
  82. package/dist/display/Stat.svelte.d.ts.map +1 -0
  83. package/dist/display/Table.svelte +4654 -0
  84. package/dist/display/Table.svelte.d.ts +249 -0
  85. package/dist/display/Table.svelte.d.ts.map +1 -0
  86. package/dist/display/TableCellEditor.svelte +935 -0
  87. package/dist/display/TableCellEditor.svelte.d.ts +58 -0
  88. package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
  89. package/dist/display/Timeline.svelte +1258 -0
  90. package/dist/display/Timeline.svelte.d.ts +43 -0
  91. package/dist/display/Timeline.svelte.d.ts.map +1 -0
  92. package/dist/display/Tree.svelte +1740 -0
  93. package/dist/display/Tree.svelte.d.ts +74 -0
  94. package/dist/display/Tree.svelte.d.ts.map +1 -0
  95. package/dist/display/Typewriter.svelte +338 -0
  96. package/dist/display/Typewriter.svelte.d.ts +22 -0
  97. package/dist/display/Typewriter.svelte.d.ts.map +1 -0
  98. package/dist/display/index.d.ts +24 -0
  99. package/dist/display/index.d.ts.map +1 -0
  100. package/dist/display/index.js +18 -0
  101. package/dist/feedback/Callout.svelte +529 -0
  102. package/dist/feedback/Callout.svelte.d.ts +24 -0
  103. package/dist/feedback/Callout.svelte.d.ts.map +1 -0
  104. package/dist/feedback/Confetti.svelte +631 -0
  105. package/dist/feedback/Confetti.svelte.d.ts +90 -0
  106. package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
  107. package/dist/feedback/Progress.svelte +382 -0
  108. package/dist/feedback/Progress.svelte.d.ts +25 -0
  109. package/dist/feedback/Progress.svelte.d.ts.map +1 -0
  110. package/dist/feedback/Toast.svelte +967 -0
  111. package/dist/feedback/Toast.svelte.d.ts +54 -0
  112. package/dist/feedback/Toast.svelte.d.ts.map +1 -0
  113. package/dist/feedback/index.d.ts +7 -0
  114. package/dist/feedback/index.d.ts.map +1 -0
  115. package/dist/feedback/index.js +4 -0
  116. package/dist/form/Checkbox.svelte +449 -0
  117. package/dist/form/Checkbox.svelte.d.ts +27 -0
  118. package/dist/form/Checkbox.svelte.d.ts.map +1 -0
  119. package/dist/form/Fieldset.svelte +410 -0
  120. package/dist/form/Fieldset.svelte.d.ts +22 -0
  121. package/dist/form/Fieldset.svelte.d.ts.map +1 -0
  122. package/dist/form/FileUpload.svelte +934 -0
  123. package/dist/form/FileUpload.svelte.d.ts +41 -0
  124. package/dist/form/FileUpload.svelte.d.ts.map +1 -0
  125. package/dist/form/Form.svelte +530 -0
  126. package/dist/form/Form.svelte.d.ts +120 -0
  127. package/dist/form/Form.svelte.d.ts.map +1 -0
  128. package/dist/form/Input.svelte +2858 -0
  129. package/dist/form/Input.svelte.d.ts +66 -0
  130. package/dist/form/Input.svelte.d.ts.map +1 -0
  131. package/dist/form/Radio.svelte +507 -0
  132. package/dist/form/Radio.svelte.d.ts +39 -0
  133. package/dist/form/Radio.svelte.d.ts.map +1 -0
  134. package/dist/form/Range.svelte +912 -0
  135. package/dist/form/Range.svelte.d.ts +33 -0
  136. package/dist/form/Range.svelte.d.ts.map +1 -0
  137. package/dist/form/Rating.svelte +429 -0
  138. package/dist/form/Rating.svelte.d.ts +28 -0
  139. package/dist/form/Rating.svelte.d.ts.map +1 -0
  140. package/dist/form/Select.svelte +1933 -0
  141. package/dist/form/Select.svelte.d.ts +54 -0
  142. package/dist/form/Select.svelte.d.ts.map +1 -0
  143. package/dist/form/Toggle.svelte +645 -0
  144. package/dist/form/Toggle.svelte.d.ts +50 -0
  145. package/dist/form/Toggle.svelte.d.ts.map +1 -0
  146. package/dist/form/index.d.ts +15 -0
  147. package/dist/form/index.d.ts.map +1 -0
  148. package/dist/form/index.js +10 -0
  149. package/dist/index.d.ts +7 -0
  150. package/dist/index.d.ts.map +1 -0
  151. package/dist/index.js +6 -0
  152. package/dist/layout/README.md +172 -0
  153. package/dist/media/Carousel.svelte +2424 -0
  154. package/dist/media/Carousel.svelte.d.ts +47 -0
  155. package/dist/media/Carousel.svelte.d.ts.map +1 -0
  156. package/dist/media/Gallery.svelte +2881 -0
  157. package/dist/media/Gallery.svelte.d.ts +82 -0
  158. package/dist/media/Gallery.svelte.d.ts.map +1 -0
  159. package/dist/media/Image.svelte +389 -0
  160. package/dist/media/Image.svelte.d.ts +33 -0
  161. package/dist/media/Image.svelte.d.ts.map +1 -0
  162. package/dist/media/PDF.svelte +1793 -0
  163. package/dist/media/PDF.svelte.d.ts +44 -0
  164. package/dist/media/PDF.svelte.d.ts.map +1 -0
  165. package/dist/media/Panorama.svelte +1391 -0
  166. package/dist/media/Panorama.svelte.d.ts +47 -0
  167. package/dist/media/Panorama.svelte.d.ts.map +1 -0
  168. package/dist/media/Video.svelte +2501 -0
  169. package/dist/media/Video.svelte.d.ts +58 -0
  170. package/dist/media/Video.svelte.d.ts.map +1 -0
  171. package/dist/media/carousel.d.ts +211 -0
  172. package/dist/media/carousel.d.ts.map +1 -0
  173. package/dist/media/carousel.js +408 -0
  174. package/dist/media/index.d.ts +11 -0
  175. package/dist/media/index.d.ts.map +1 -0
  176. package/dist/media/index.js +5 -0
  177. package/dist/navigation/BottomSheet.svelte +636 -0
  178. package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
  179. package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
  180. package/dist/navigation/Breadcrumbs.svelte +611 -0
  181. package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
  182. package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
  183. package/dist/navigation/Pagination.svelte +641 -0
  184. package/dist/navigation/Pagination.svelte.d.ts +27 -0
  185. package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
  186. package/dist/navigation/Steps.svelte +965 -0
  187. package/dist/navigation/Steps.svelte.d.ts +43 -0
  188. package/dist/navigation/Steps.svelte.d.ts.map +1 -0
  189. package/dist/navigation/Tabs.svelte +698 -0
  190. package/dist/navigation/Tabs.svelte.d.ts +41 -0
  191. package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
  192. package/dist/navigation/index.d.ts +8 -0
  193. package/dist/navigation/index.d.ts.map +1 -0
  194. package/dist/navigation/index.js +5 -0
  195. 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>