@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,912 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tooltip } from '@delightstack/utilities';
|
|
3
|
+
import { getContext } from 'svelte';
|
|
4
|
+
import type { FormContext } from './Form.svelte';
|
|
5
|
+
|
|
6
|
+
const propId = $props.id();
|
|
7
|
+
let {
|
|
8
|
+
/** Current value: number for single, [number, number] for range mode */
|
|
9
|
+
value = $bindable(0) as number | [number, number],
|
|
10
|
+
|
|
11
|
+
/** Minimum value */
|
|
12
|
+
min = 0,
|
|
13
|
+
|
|
14
|
+
/** Maximum value */
|
|
15
|
+
max = 100,
|
|
16
|
+
|
|
17
|
+
/** Step increment */
|
|
18
|
+
step = 1,
|
|
19
|
+
|
|
20
|
+
/** Whether to show two thumbs for range selection */
|
|
21
|
+
range = false,
|
|
22
|
+
|
|
23
|
+
/** Whether the slider is disabled */
|
|
24
|
+
disabled = false,
|
|
25
|
+
|
|
26
|
+
/** Size preset: 0=small, 1=default, 2=medium, 3=large */
|
|
27
|
+
size = '1' as '0' | '1' | '2' | '3',
|
|
28
|
+
|
|
29
|
+
/** Whether to show the current value near the thumb */
|
|
30
|
+
show_value = false,
|
|
31
|
+
|
|
32
|
+
/** Whether to display stop indicator dots at each step */
|
|
33
|
+
show_ticks = false,
|
|
34
|
+
|
|
35
|
+
/** Custom labels for tick positions */
|
|
36
|
+
tick_labels = undefined as string[] | undefined,
|
|
37
|
+
|
|
38
|
+
/** Custom formatter for displayed values */
|
|
39
|
+
format_value = undefined as ((n: number) => string) | undefined,
|
|
40
|
+
|
|
41
|
+
/** Label text displayed above the slider */
|
|
42
|
+
label = undefined as string | undefined,
|
|
43
|
+
|
|
44
|
+
/** Tooltip message shown on hover */
|
|
45
|
+
tooltip: tooltip_message = undefined as string | undefined,
|
|
46
|
+
|
|
47
|
+
/** Whether the slider uses dense spacing */
|
|
48
|
+
dense = false,
|
|
49
|
+
|
|
50
|
+
/** Whether the slider uses comfortable spacing */
|
|
51
|
+
comfortable = false,
|
|
52
|
+
|
|
53
|
+
/** Whether to display the slider vertically */
|
|
54
|
+
vertical = false,
|
|
55
|
+
|
|
56
|
+
/** The id of the slider element */
|
|
57
|
+
id = propId,
|
|
58
|
+
|
|
59
|
+
/** Name attribute for hidden input(s) */
|
|
60
|
+
name = undefined as string | undefined,
|
|
61
|
+
|
|
62
|
+
/** Error message shown below the slider */
|
|
63
|
+
error = '',
|
|
64
|
+
|
|
65
|
+
/** Parses & validates the value (e.g. a database table form field's
|
|
66
|
+
* `parse`). Inside a Form it is registered with the form, which runs it
|
|
67
|
+
* on the form's validation timing. */
|
|
68
|
+
parse = undefined as ((value: unknown) => unknown) | undefined,
|
|
69
|
+
|
|
70
|
+
/** Accessible label for the slider thumb(s) when no visible `label` is
|
|
71
|
+
* shown (e.g. an icon-only slider). Takes precedence over `label`. */
|
|
72
|
+
aria_label = undefined as string | undefined,
|
|
73
|
+
|
|
74
|
+
/** Custom class name */
|
|
75
|
+
class: class_name = '',
|
|
76
|
+
|
|
77
|
+
/** Called when value changes (on pointerup / change) */
|
|
78
|
+
onchange = undefined as
|
|
79
|
+
| ((detail: { value: number | [number, number] }) => void)
|
|
80
|
+
| undefined,
|
|
81
|
+
|
|
82
|
+
/** Called during dragging */
|
|
83
|
+
oninput = undefined as
|
|
84
|
+
| ((detail: { value: number | [number, number] }) => void)
|
|
85
|
+
| undefined,
|
|
86
|
+
} = $props();
|
|
87
|
+
|
|
88
|
+
let lower_hovering = $state(false);
|
|
89
|
+
let upper_hovering = $state(false);
|
|
90
|
+
let lower_dragging = $state(false);
|
|
91
|
+
let upper_dragging = $state(false);
|
|
92
|
+
let drag_wrapper: HTMLElement | null = null;
|
|
93
|
+
let active_thumb = $state<'lower' | 'upper' | null>(null);
|
|
94
|
+
let overshoot_px = $state(0);
|
|
95
|
+
|
|
96
|
+
/* ------------------------------------------------------------------ */
|
|
97
|
+
/* Form context integration */
|
|
98
|
+
/* ------------------------------------------------------------------ */
|
|
99
|
+
|
|
100
|
+
const form_ctx = getContext<FormContext | undefined>('form');
|
|
101
|
+
let lower_input = $state<HTMLInputElement | undefined>(undefined);
|
|
102
|
+
|
|
103
|
+
/** Inside a Form with a name, the slider value flows through the form data */
|
|
104
|
+
const context_driven = !!(form_ctx && name);
|
|
105
|
+
|
|
106
|
+
/** Disabled merges the parent form's disabled/submitting state */
|
|
107
|
+
const effectively_disabled = $derived(disabled || (form_ctx?.disabled ?? false));
|
|
108
|
+
|
|
109
|
+
/** Error from the local prop or the parent form context */
|
|
110
|
+
const resolved_error = $derived.by(() => {
|
|
111
|
+
if (error) return error;
|
|
112
|
+
if (form_ctx && name && form_ctx.errors[name]) return form_ctx.errors[name];
|
|
113
|
+
return '';
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Register with a parent Form (focus-on-error target + field validator).
|
|
117
|
+
$effect(() => {
|
|
118
|
+
if (!form_ctx || !name) return;
|
|
119
|
+
if (lower_input) form_ctx.register(name, lower_input, parse);
|
|
120
|
+
return () => form_ctx.unregister(name);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Pull the value from the form data. Declared before the push effect so it
|
|
124
|
+
// runs first on mount — the form's value wins over the prop default.
|
|
125
|
+
$effect(() => {
|
|
126
|
+
if (!context_driven || !form_ctx || !name) return;
|
|
127
|
+
const ctx_value = form_ctx.getValue(name);
|
|
128
|
+
if (range) {
|
|
129
|
+
if (
|
|
130
|
+
Array.isArray(ctx_value) &&
|
|
131
|
+
(!Array.isArray(value) || ctx_value[0] !== value[0] || ctx_value[1] !== value[1])
|
|
132
|
+
) {
|
|
133
|
+
value = [Number(ctx_value[0]), Number(ctx_value[1])];
|
|
134
|
+
}
|
|
135
|
+
} else if (ctx_value != null) {
|
|
136
|
+
const next = Number(ctx_value);
|
|
137
|
+
if (!Number.isNaN(next) && next !== value) value = next;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Push the current value into the form data. Called synchronously from the
|
|
142
|
+
// value-change handlers (not a $effect) so the form data is fresh before the
|
|
143
|
+
// pull effect above re-runs — otherwise it would read the stale value and
|
|
144
|
+
// clobber the change.
|
|
145
|
+
function pushValue() {
|
|
146
|
+
if (context_driven && form_ctx && name) form_ctx.setValue(name, emitValue());
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const lower_value = $derived(
|
|
150
|
+
range && Array.isArray(value) ? value[0] : (value as number),
|
|
151
|
+
);
|
|
152
|
+
const upper_value = $derived(range && Array.isArray(value) ? value[1] : max);
|
|
153
|
+
|
|
154
|
+
const lower_pct = $derived(((lower_value - min) / (max - min)) * 100);
|
|
155
|
+
const upper_pct = $derived(((upper_value - min) / (max - min)) * 100);
|
|
156
|
+
|
|
157
|
+
// Native range inputs offset the thumb center from raw percentage by
|
|
158
|
+
// handleWidth * (0.5 - ratio). Compute this so track segments align with the thumb.
|
|
159
|
+
const lower_thumb_offset = $derived(0.5 - lower_pct / 100);
|
|
160
|
+
const upper_thumb_offset = $derived(0.5 - upper_pct / 100);
|
|
161
|
+
|
|
162
|
+
const is_dragging = $derived(lower_dragging || upper_dragging);
|
|
163
|
+
|
|
164
|
+
// Visual pixel offsets for track segments to follow the handle during magnetic drag
|
|
165
|
+
const lower_visual_offset = $derived(active_thumb === 'lower' ? overshoot_px : 0);
|
|
166
|
+
const upper_visual_offset = $derived(active_thumb === 'upper' ? overshoot_px : 0);
|
|
167
|
+
|
|
168
|
+
const tick_count = $derived(Math.floor((max - min) / step));
|
|
169
|
+
|
|
170
|
+
function formatDisplay(n: number): string {
|
|
171
|
+
if (format_value) return format_value(n);
|
|
172
|
+
return String(n);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function emitValue() {
|
|
176
|
+
return range ? ([lower_value, upper_value] as [number, number]) : lower_value;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function onLowerInput(e: Event) {
|
|
180
|
+
const input = e.currentTarget as HTMLInputElement;
|
|
181
|
+
const v = Number(input.value);
|
|
182
|
+
if (range) {
|
|
183
|
+
value = [v, Math.max(v, upper_value)] as [number, number];
|
|
184
|
+
} else {
|
|
185
|
+
value = v;
|
|
186
|
+
}
|
|
187
|
+
pushValue();
|
|
188
|
+
oninput?.({ value: emitValue() });
|
|
189
|
+
// Recompute overshoot now that value has snapped, so the DOM update
|
|
190
|
+
// includes an overshoot consistent with the new thumb position
|
|
191
|
+
if (is_dragging) updateOvershoot();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function onUpperInput(e: Event) {
|
|
195
|
+
const input = e.currentTarget as HTMLInputElement;
|
|
196
|
+
const v = Number(input.value);
|
|
197
|
+
value = [Math.min(v, lower_value), v] as [number, number];
|
|
198
|
+
pushValue();
|
|
199
|
+
oninput?.({ value: emitValue() });
|
|
200
|
+
if (is_dragging) updateOvershoot();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function onLowerChange() {
|
|
204
|
+
if (form_ctx && name) form_ctx.setTouched(name);
|
|
205
|
+
onchange?.({ value: emitValue() });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function onUpperChange() {
|
|
209
|
+
if (form_ctx && name) form_ctx.setTouched(name);
|
|
210
|
+
onchange?.({ value: emitValue() });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function rawPctFromPointer(e: PointerEvent): number {
|
|
214
|
+
if (!drag_wrapper) return 0;
|
|
215
|
+
const rect = drag_wrapper.getBoundingClientRect();
|
|
216
|
+
return vertical
|
|
217
|
+
? 1 - (e.clientY - rect.top) / rect.height
|
|
218
|
+
: (e.clientX - rect.left) / rect.width;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function valueFromPointer(e: PointerEvent): number {
|
|
222
|
+
const pct = Math.max(0, Math.min(1, rawPctFromPointer(e)));
|
|
223
|
+
const raw = min + pct * (max - min);
|
|
224
|
+
const snapped = Math.round(raw / step) * step;
|
|
225
|
+
return Math.max(min, Math.min(max, snapped));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function setValueForThumb(v: number) {
|
|
229
|
+
if (range && Array.isArray(value)) {
|
|
230
|
+
if (active_thumb === 'lower') {
|
|
231
|
+
value = [v, Math.max(v, upper_value)] as [number, number];
|
|
232
|
+
} else {
|
|
233
|
+
value = [Math.min(v, lower_value), v] as [number, number];
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
value = v;
|
|
237
|
+
}
|
|
238
|
+
pushValue();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// --- Track click-and-drag (custom pointer capture on wrapper) ---
|
|
242
|
+
|
|
243
|
+
function onTrackPointerDown(e: PointerEvent) {
|
|
244
|
+
if (effectively_disabled) return;
|
|
245
|
+
if (e.target instanceof HTMLInputElement) return;
|
|
246
|
+
e.preventDefault();
|
|
247
|
+
|
|
248
|
+
drag_wrapper = e.currentTarget as HTMLElement;
|
|
249
|
+
const v = valueFromPointer(e);
|
|
250
|
+
|
|
251
|
+
let thumb: 'lower' | 'upper' = 'lower';
|
|
252
|
+
if (range && Array.isArray(value)) {
|
|
253
|
+
const lower_dist = Math.abs(v - lower_value);
|
|
254
|
+
const upper_dist = Math.abs(v - upper_value);
|
|
255
|
+
thumb = lower_dist <= upper_dist ? 'lower' : 'upper';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
active_thumb = thumb;
|
|
259
|
+
drag_wrapper.setPointerCapture(e.pointerId);
|
|
260
|
+
if (thumb === 'lower') lower_dragging = true;
|
|
261
|
+
else upper_dragging = true;
|
|
262
|
+
|
|
263
|
+
setValueForThumb(v);
|
|
264
|
+
oninput?.({ value: emitValue() });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function onTrackPointerMove(e: PointerEvent) {
|
|
268
|
+
if (!active_thumb) return;
|
|
269
|
+
// Only update value for track-initiated drags (wrapper has capture, so
|
|
270
|
+
// target is the wrapper). For native thumb drags the event bubbles from
|
|
271
|
+
// the input and the native oninput handler manages the value instead.
|
|
272
|
+
if (!(e.target instanceof HTMLInputElement)) {
|
|
273
|
+
setValueForThumb(valueFromPointer(e));
|
|
274
|
+
oninput?.({ value: emitValue() });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function onTrackPointerUp() {
|
|
279
|
+
if (!active_thumb) return;
|
|
280
|
+
if (active_thumb === 'lower') lower_dragging = false;
|
|
281
|
+
else upper_dragging = false;
|
|
282
|
+
active_thumb = null;
|
|
283
|
+
overshoot_px = 0;
|
|
284
|
+
drag_wrapper = null;
|
|
285
|
+
if (form_ctx && name) form_ctx.setTouched(name);
|
|
286
|
+
onchange?.({ value: emitValue() });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// --- Native thumb drag detection via pointer capture events ---
|
|
290
|
+
// The browser internally sets pointer capture on the input when dragging
|
|
291
|
+
// the thumb. gotpointercapture/lostpointercapture fire regardless of
|
|
292
|
+
// the CSS pointer-events property.
|
|
293
|
+
|
|
294
|
+
function onThumbCaptureStart(thumb: 'lower' | 'upper', e: Event) {
|
|
295
|
+
if (active_thumb) return; // Already in a custom track drag
|
|
296
|
+
active_thumb = thumb;
|
|
297
|
+
drag_wrapper = (e.currentTarget as HTMLElement).parentElement as HTMLElement;
|
|
298
|
+
if (thumb === 'lower') lower_dragging = true;
|
|
299
|
+
else upper_dragging = true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function onThumbCaptureEnd(thumb: 'lower' | 'upper') {
|
|
303
|
+
if (active_thumb !== thumb) return; // Wasn't our drag
|
|
304
|
+
if (thumb === 'lower') lower_dragging = false;
|
|
305
|
+
else upper_dragging = false;
|
|
306
|
+
active_thumb = null;
|
|
307
|
+
overshoot_px = 0;
|
|
308
|
+
drag_wrapper = null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// --- Overshoot computation (shared by pointermove and oninput) ---
|
|
312
|
+
|
|
313
|
+
let last_pointer_coord = 0;
|
|
314
|
+
|
|
315
|
+
function updateOvershoot() {
|
|
316
|
+
if (!drag_wrapper) return;
|
|
317
|
+
const rect = drag_wrapper.getBoundingClientRect();
|
|
318
|
+
const raw_pct = vertical
|
|
319
|
+
? 1 - (last_pointer_coord - rect.top) / rect.height
|
|
320
|
+
: (last_pointer_coord - rect.left) / rect.width;
|
|
321
|
+
|
|
322
|
+
if (raw_pct < 0 || raw_pct > 1) {
|
|
323
|
+
// Edge rubber band
|
|
324
|
+
const overflow_px = (raw_pct < 0 ? raw_pct : raw_pct - 1) * rect.width;
|
|
325
|
+
const max_shift = 24;
|
|
326
|
+
overshoot_px = max_shift * Math.tanh(overflow_px / 100);
|
|
327
|
+
} else {
|
|
328
|
+
// Magnetic tick gravity — uses the ACTUAL current value (not our
|
|
329
|
+
// own snap) so the overshoot is always consistent with the thumb
|
|
330
|
+
// position. Uses a smooth easing that reaches full-follow at the
|
|
331
|
+
// midpoint between ticks, guaranteeing visual continuity at snaps.
|
|
332
|
+
const current_val = active_thumb === 'lower' ? lower_value : upper_value;
|
|
333
|
+
const snapped_pct = (current_val - min) / (max - min);
|
|
334
|
+
const pull_px = (raw_pct - snapped_pct) * rect.width;
|
|
335
|
+
const step_px = (step / (max - min)) * rect.width;
|
|
336
|
+
const half_step_px = step_px / 2;
|
|
337
|
+
|
|
338
|
+
if (half_step_px < 1) {
|
|
339
|
+
overshoot_px = 0;
|
|
340
|
+
} else {
|
|
341
|
+
// Smooth ease: starts at gravity rate near tick, reaches
|
|
342
|
+
// full follow at midpoint (continuity across snaps)
|
|
343
|
+
const t = Math.min(1, Math.abs(pull_px) / half_step_px);
|
|
344
|
+
const gravity = 0.15;
|
|
345
|
+
const eased = gravity * t + (1 - gravity) * t * t;
|
|
346
|
+
overshoot_px = Math.sign(pull_px) * eased * half_step_px;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// --- Window-level pointermove for overshoot (works for both drag sources) ---
|
|
352
|
+
|
|
353
|
+
$effect(() => {
|
|
354
|
+
if (!is_dragging) return;
|
|
355
|
+
|
|
356
|
+
function onMove(e: PointerEvent) {
|
|
357
|
+
last_pointer_coord = vertical ? e.clientY : e.clientX;
|
|
358
|
+
updateOvershoot();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
window.addEventListener('pointermove', onMove);
|
|
362
|
+
return () => window.removeEventListener('pointermove', onMove);
|
|
363
|
+
});
|
|
364
|
+
</script>
|
|
365
|
+
|
|
366
|
+
<div
|
|
367
|
+
class={['range-container', `size-${size}`, class_name].filter(Boolean).join(' ')}
|
|
368
|
+
class:disabled={effectively_disabled}
|
|
369
|
+
class:dense
|
|
370
|
+
class:comfortable
|
|
371
|
+
class:vertical
|
|
372
|
+
class:has-error={!!resolved_error}
|
|
373
|
+
class:has-tick-labels={show_ticks && !!tick_labels?.length}
|
|
374
|
+
class:dragging={is_dragging}
|
|
375
|
+
{@attach tooltip_message ? tooltip(tooltip_message) : () => {}}>
|
|
376
|
+
{#if label}
|
|
377
|
+
<label for={id}>{label}</label>
|
|
378
|
+
{/if}
|
|
379
|
+
|
|
380
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
381
|
+
<div
|
|
382
|
+
class="range-wrapper"
|
|
383
|
+
onpointerdown={onTrackPointerDown}
|
|
384
|
+
onpointermove={onTrackPointerMove}
|
|
385
|
+
onpointerup={onTrackPointerUp}>
|
|
386
|
+
{#if show_value}
|
|
387
|
+
<span
|
|
388
|
+
class="value-tooltip"
|
|
389
|
+
class:visible={lower_hovering || lower_dragging}
|
|
390
|
+
style:left="calc({lower_pct}% + var(--handle-width) * {lower_thumb_offset})">
|
|
391
|
+
{formatDisplay(lower_value)}
|
|
392
|
+
</span>
|
|
393
|
+
{#if range}
|
|
394
|
+
<span
|
|
395
|
+
class="value-tooltip"
|
|
396
|
+
class:visible={upper_hovering || upper_dragging}
|
|
397
|
+
style:left="calc({upper_pct}% + var(--handle-width) * {upper_thumb_offset})">
|
|
398
|
+
{formatDisplay(upper_value)}
|
|
399
|
+
</span>
|
|
400
|
+
{/if}
|
|
401
|
+
{/if}
|
|
402
|
+
|
|
403
|
+
<!-- Track segments follow the handle's visual position (including magnetic overshoot) -->
|
|
404
|
+
{#if range}
|
|
405
|
+
<div
|
|
406
|
+
class="track-segment inactive"
|
|
407
|
+
style:left="0"
|
|
408
|
+
style:width="calc({lower_pct}% + var(--handle-width) * {lower_thumb_offset} - var(--gap)
|
|
409
|
+
+ {lower_visual_offset}px)">
|
|
410
|
+
</div>
|
|
411
|
+
<div
|
|
412
|
+
class="track-segment active"
|
|
413
|
+
style:left="calc({lower_pct}% + var(--handle-width) * {lower_thumb_offset} + var(--gap)
|
|
414
|
+
+ {lower_visual_offset}px)"
|
|
415
|
+
style:width="calc({upper_pct - lower_pct}% + var(--handle-width) * {upper_thumb_offset -
|
|
416
|
+
lower_thumb_offset} - var(--gap) * 2 + {upper_visual_offset -
|
|
417
|
+
lower_visual_offset}px)">
|
|
418
|
+
</div>
|
|
419
|
+
<div
|
|
420
|
+
class="track-segment inactive"
|
|
421
|
+
style:left="calc({upper_pct}% + var(--handle-width) * {upper_thumb_offset} + var(--gap)
|
|
422
|
+
+ {upper_visual_offset}px)"
|
|
423
|
+
style:right="0">
|
|
424
|
+
</div>
|
|
425
|
+
{:else}
|
|
426
|
+
<div
|
|
427
|
+
class="track-segment active"
|
|
428
|
+
style:left="0"
|
|
429
|
+
style:width="calc({lower_pct}% + var(--handle-width) * {lower_thumb_offset} - var(--gap)
|
|
430
|
+
+ {lower_visual_offset}px)">
|
|
431
|
+
</div>
|
|
432
|
+
<div
|
|
433
|
+
class="track-segment inactive"
|
|
434
|
+
style:left="calc({lower_pct}% + var(--handle-width) * {lower_thumb_offset} + var(--gap)
|
|
435
|
+
+ {lower_visual_offset}px)"
|
|
436
|
+
style:right="0">
|
|
437
|
+
</div>
|
|
438
|
+
{/if}
|
|
439
|
+
|
|
440
|
+
<input
|
|
441
|
+
type="range"
|
|
442
|
+
bind:this={lower_input}
|
|
443
|
+
{id}
|
|
444
|
+
{name}
|
|
445
|
+
{min}
|
|
446
|
+
{max}
|
|
447
|
+
{step}
|
|
448
|
+
disabled={effectively_disabled}
|
|
449
|
+
value={lower_value}
|
|
450
|
+
class="lower"
|
|
451
|
+
aria-valuenow={lower_value}
|
|
452
|
+
aria-valuemin={min}
|
|
453
|
+
aria-valuemax={range ? upper_value : max}
|
|
454
|
+
aria-label={aria_label || label || 'Range value'}
|
|
455
|
+
oninput={onLowerInput}
|
|
456
|
+
onchange={onLowerChange}
|
|
457
|
+
onpointerenter={() => (lower_hovering = true)}
|
|
458
|
+
onpointerleave={() => (lower_hovering = false)}
|
|
459
|
+
ongotpointercapture={(e) => onThumbCaptureStart('lower', e)}
|
|
460
|
+
onlostpointercapture={() => onThumbCaptureEnd('lower')} />
|
|
461
|
+
|
|
462
|
+
{#if range}
|
|
463
|
+
<input
|
|
464
|
+
type="range"
|
|
465
|
+
{min}
|
|
466
|
+
{max}
|
|
467
|
+
{step}
|
|
468
|
+
disabled={effectively_disabled}
|
|
469
|
+
value={upper_value}
|
|
470
|
+
class="upper"
|
|
471
|
+
aria-valuenow={upper_value}
|
|
472
|
+
aria-valuemin={lower_value}
|
|
473
|
+
aria-valuemax={max}
|
|
474
|
+
aria-label={aria_label
|
|
475
|
+
? `${aria_label} upper`
|
|
476
|
+
: label
|
|
477
|
+
? `${label} upper`
|
|
478
|
+
: 'Range upper value'}
|
|
479
|
+
oninput={onUpperInput}
|
|
480
|
+
onchange={onUpperChange}
|
|
481
|
+
onpointerenter={() => (upper_hovering = true)}
|
|
482
|
+
onpointerleave={() => (upper_hovering = false)}
|
|
483
|
+
ongotpointercapture={(e) => onThumbCaptureStart('upper', e)}
|
|
484
|
+
onlostpointercapture={() => onThumbCaptureEnd('upper')} />
|
|
485
|
+
{/if}
|
|
486
|
+
|
|
487
|
+
<!-- Visual handle(s). Positioned from the raw value (like the track fill)
|
|
488
|
+
so they glide smoothly on programmatic value changes and never
|
|
489
|
+
step-snap the way a native range thumb does. The native input above
|
|
490
|
+
stays transparent but interactive (drag / keyboard / focus). -->
|
|
491
|
+
<div
|
|
492
|
+
class="handle lower"
|
|
493
|
+
class:hovering={lower_hovering}
|
|
494
|
+
class:dragging={lower_dragging}
|
|
495
|
+
style:left="calc({lower_pct}% + var(--handle-width) * {lower_thumb_offset}
|
|
496
|
+
+ {lower_visual_offset}px)">
|
|
497
|
+
</div>
|
|
498
|
+
{#if range}
|
|
499
|
+
<div
|
|
500
|
+
class="handle upper"
|
|
501
|
+
class:hovering={upper_hovering}
|
|
502
|
+
class:dragging={upper_dragging}
|
|
503
|
+
style:left="calc({upper_pct}% + var(--handle-width) * {upper_thumb_offset}
|
|
504
|
+
+ {upper_visual_offset}px)">
|
|
505
|
+
</div>
|
|
506
|
+
{/if}
|
|
507
|
+
|
|
508
|
+
{#if show_ticks && tick_count <= 50}
|
|
509
|
+
<div class="ticks" aria-hidden="true">
|
|
510
|
+
{#each { length: tick_count + 1 } as _, i}
|
|
511
|
+
{@const tick_value = min + i * step}
|
|
512
|
+
{@const tick_pct = ((tick_value - min) / (max - min)) * 100}
|
|
513
|
+
{@const tick_offset = 0.5 - tick_pct / 100}
|
|
514
|
+
<span
|
|
515
|
+
class="tick"
|
|
516
|
+
class:active={tick_value >= (range ? lower_value : min) &&
|
|
517
|
+
tick_value <= (range ? upper_value : lower_value)}
|
|
518
|
+
style:left="calc({tick_pct}% + var(--handle-width) * {tick_offset})">
|
|
519
|
+
{#if tick_labels && tick_labels[i] !== undefined}
|
|
520
|
+
<span class="tick-label">{tick_labels[i]}</span>
|
|
521
|
+
{/if}
|
|
522
|
+
</span>
|
|
523
|
+
{/each}
|
|
524
|
+
</div>
|
|
525
|
+
{/if}
|
|
526
|
+
</div>
|
|
527
|
+
|
|
528
|
+
{#if show_value && !show_ticks}
|
|
529
|
+
<div class="value-display">
|
|
530
|
+
<span>{formatDisplay(lower_value)}</span>
|
|
531
|
+
{#if range}
|
|
532
|
+
<span>{formatDisplay(upper_value)}</span>
|
|
533
|
+
{/if}
|
|
534
|
+
</div>
|
|
535
|
+
{/if}
|
|
536
|
+
|
|
537
|
+
{#if resolved_error}
|
|
538
|
+
<span class="error-text">{resolved_error}</span>
|
|
539
|
+
{/if}
|
|
540
|
+
</div>
|
|
541
|
+
|
|
542
|
+
<style>
|
|
543
|
+
.range-container {
|
|
544
|
+
--handle-width: 8px;
|
|
545
|
+
--handle-height: 24px;
|
|
546
|
+
--active-height: 6px;
|
|
547
|
+
--inactive-height: 4px;
|
|
548
|
+
--gap: 7px;
|
|
549
|
+
--fill-color: var(--color-action, hsl(220 70% 55%));
|
|
550
|
+
--track-bg: var(--color-bg-muted, hsl(0 0% 80%));
|
|
551
|
+
|
|
552
|
+
display: flex;
|
|
553
|
+
flex-direction: column;
|
|
554
|
+
gap: 0.5em;
|
|
555
|
+
width: 100%;
|
|
556
|
+
font-size: var(--control-font-1, 1rem);
|
|
557
|
+
|
|
558
|
+
&.dense {
|
|
559
|
+
gap: 0.25em;
|
|
560
|
+
}
|
|
561
|
+
&.comfortable {
|
|
562
|
+
gap: 0.75em;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.error-text {
|
|
566
|
+
font-size: 0.8em;
|
|
567
|
+
color: var(--color-error, #d32f2f);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/* Sizes */
|
|
571
|
+
&.size-0 {
|
|
572
|
+
--handle-width: 6px;
|
|
573
|
+
--handle-height: 20px;
|
|
574
|
+
--active-height: 4px;
|
|
575
|
+
--inactive-height: 4px;
|
|
576
|
+
--gap: 6px;
|
|
577
|
+
font-size: var(--control-font-0, 0.875rem);
|
|
578
|
+
}
|
|
579
|
+
&.size-1 {
|
|
580
|
+
--handle-width: 8px;
|
|
581
|
+
--handle-height: 24px;
|
|
582
|
+
--active-height: 6px;
|
|
583
|
+
--inactive-height: 4px;
|
|
584
|
+
--gap: 7px;
|
|
585
|
+
font-size: var(--control-font-1, 1rem);
|
|
586
|
+
}
|
|
587
|
+
&.size-2 {
|
|
588
|
+
--handle-width: 10px;
|
|
589
|
+
--handle-height: 28px;
|
|
590
|
+
--active-height: 7px;
|
|
591
|
+
--inactive-height: 4px;
|
|
592
|
+
--gap: 9px;
|
|
593
|
+
font-size: var(--control-font-2, 1.125rem);
|
|
594
|
+
}
|
|
595
|
+
&.size-3 {
|
|
596
|
+
--handle-width: 12px;
|
|
597
|
+
--handle-height: 32px;
|
|
598
|
+
--active-height: 8px;
|
|
599
|
+
--inactive-height: 5px;
|
|
600
|
+
--gap: 10px;
|
|
601
|
+
font-size: var(--control-font-3, 1.25rem);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
label {
|
|
606
|
+
color: var(--color-text, inherit);
|
|
607
|
+
font-weight: 500;
|
|
608
|
+
line-height: 1.4;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.range-wrapper {
|
|
612
|
+
position: relative;
|
|
613
|
+
height: var(--handle-height);
|
|
614
|
+
display: flex;
|
|
615
|
+
align-items: center;
|
|
616
|
+
cursor: pointer;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/* Track segments */
|
|
620
|
+
.track-segment {
|
|
621
|
+
position: absolute;
|
|
622
|
+
top: 50%;
|
|
623
|
+
transform: translateY(-50%);
|
|
624
|
+
border-radius: 999px;
|
|
625
|
+
pointer-events: none;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.track-segment.active {
|
|
629
|
+
height: var(--active-height);
|
|
630
|
+
background: var(--fill-color);
|
|
631
|
+
transition:
|
|
632
|
+
left 100ms ease,
|
|
633
|
+
width 100ms ease,
|
|
634
|
+
height 200ms ease,
|
|
635
|
+
box-shadow 200ms ease;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.track-segment.inactive {
|
|
639
|
+
height: var(--inactive-height);
|
|
640
|
+
background: var(--track-bg);
|
|
641
|
+
transition:
|
|
642
|
+
left 100ms ease,
|
|
643
|
+
width 100ms ease,
|
|
644
|
+
right 100ms ease,
|
|
645
|
+
height 200ms ease;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/* During drag: no position transition (track follows handle via reactive offset) */
|
|
649
|
+
.dragging .track-segment.active {
|
|
650
|
+
transition:
|
|
651
|
+
height 200ms ease,
|
|
652
|
+
box-shadow 200ms ease;
|
|
653
|
+
box-shadow: 0 0 8px rgb(from var(--fill-color) r g b / 0.35);
|
|
654
|
+
}
|
|
655
|
+
.dragging .track-segment.inactive {
|
|
656
|
+
transition: height 200ms ease;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/* Track grows on hover */
|
|
660
|
+
.range-container:not(.disabled):hover .track-segment.active {
|
|
661
|
+
height: calc(var(--active-height) + 2px);
|
|
662
|
+
}
|
|
663
|
+
.range-container:not(.disabled):hover .track-segment.inactive {
|
|
664
|
+
height: calc(var(--inactive-height) + 2px);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/* Native range inputs. Kept transparent and interactive for drag, keyboard
|
|
668
|
+
and focus — the visible handle is the `.handle` element below, positioned
|
|
669
|
+
from the raw value so it never step-snaps. */
|
|
670
|
+
input {
|
|
671
|
+
position: absolute;
|
|
672
|
+
width: 100%;
|
|
673
|
+
height: var(--handle-height);
|
|
674
|
+
margin: 0;
|
|
675
|
+
padding: 0;
|
|
676
|
+
background: transparent;
|
|
677
|
+
appearance: none;
|
|
678
|
+
-webkit-appearance: none;
|
|
679
|
+
pointer-events: none;
|
|
680
|
+
outline: none;
|
|
681
|
+
z-index: 2;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
input::-webkit-slider-runnable-track {
|
|
685
|
+
height: var(--active-height);
|
|
686
|
+
background: transparent;
|
|
687
|
+
border: none;
|
|
688
|
+
}
|
|
689
|
+
input::-moz-range-track {
|
|
690
|
+
height: var(--active-height);
|
|
691
|
+
background: transparent;
|
|
692
|
+
border: none;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/* Invisible interaction thumb — the visible bar is `.handle`. Kept sized so
|
|
696
|
+
the drag/keyboard hit target and the focus ring match the handle. */
|
|
697
|
+
input::-webkit-slider-thumb {
|
|
698
|
+
-webkit-appearance: none;
|
|
699
|
+
appearance: none;
|
|
700
|
+
width: var(--handle-width);
|
|
701
|
+
height: var(--handle-height);
|
|
702
|
+
background: transparent;
|
|
703
|
+
border: none;
|
|
704
|
+
cursor: pointer;
|
|
705
|
+
pointer-events: auto;
|
|
706
|
+
margin-top: calc((var(--active-height) - var(--handle-height)) / 2);
|
|
707
|
+
}
|
|
708
|
+
input::-moz-range-thumb {
|
|
709
|
+
width: var(--handle-width);
|
|
710
|
+
height: var(--handle-height);
|
|
711
|
+
background: transparent;
|
|
712
|
+
border: none;
|
|
713
|
+
cursor: pointer;
|
|
714
|
+
pointer-events: auto;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/* Focus ring — drawn on the (transparent) thumb so it traces the handle box */
|
|
718
|
+
input:focus-visible::-webkit-slider-thumb {
|
|
719
|
+
outline: 2px solid var(--color-border-active, currentColor);
|
|
720
|
+
outline-offset: 2px;
|
|
721
|
+
border-radius: calc(var(--handle-width) / 2);
|
|
722
|
+
}
|
|
723
|
+
input:focus-visible::-moz-range-thumb {
|
|
724
|
+
outline: 2px solid var(--color-border-active, currentColor);
|
|
725
|
+
outline-offset: 2px;
|
|
726
|
+
border-radius: calc(var(--handle-width) / 2);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
input:disabled {
|
|
730
|
+
cursor: not-allowed;
|
|
731
|
+
}
|
|
732
|
+
input:disabled::-webkit-slider-thumb {
|
|
733
|
+
cursor: not-allowed;
|
|
734
|
+
}
|
|
735
|
+
input:disabled::-moz-range-thumb {
|
|
736
|
+
cursor: not-allowed;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/* M3-style vertical bar handle (the visible thumb). */
|
|
740
|
+
.handle {
|
|
741
|
+
position: absolute;
|
|
742
|
+
top: 50%;
|
|
743
|
+
width: var(--handle-width);
|
|
744
|
+
height: var(--handle-height);
|
|
745
|
+
border-radius: calc(var(--handle-width) / 2);
|
|
746
|
+
background: var(--fill-color);
|
|
747
|
+
transform: translate(-50%, -50%);
|
|
748
|
+
pointer-events: none;
|
|
749
|
+
z-index: 2;
|
|
750
|
+
/* Glide to the new position on programmatic value changes (matches the
|
|
751
|
+
track segments). The transform/box-shadow ease the hover grow + halo. */
|
|
752
|
+
transition:
|
|
753
|
+
left 100ms ease,
|
|
754
|
+
transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1),
|
|
755
|
+
box-shadow 150ms ease;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/* Handle hover: widen + grow taller + halo */
|
|
759
|
+
.range-container:not(.disabled) .handle.hovering {
|
|
760
|
+
transform: translate(-50%, -50%) scale(1.5, 1.3);
|
|
761
|
+
box-shadow: 0 0 0 8px rgb(from var(--fill-color) r g b / 0.12);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/* During drag: bigger scale + larger halo, and instant position tracking
|
|
765
|
+
(no `left`/`transform` easing) so it stays pinned under the pointer. */
|
|
766
|
+
.range-container:not(.disabled) .handle.dragging {
|
|
767
|
+
transform: translate(-50%, -50%) scale(1.5, 1.15);
|
|
768
|
+
box-shadow: 0 0 0 12px rgb(from var(--fill-color) r g b / 0.18);
|
|
769
|
+
}
|
|
770
|
+
.handle.dragging {
|
|
771
|
+
transition: box-shadow 150ms ease;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.disabled .handle {
|
|
775
|
+
background: var(--color-action-disabled, hsl(0 0% 70%));
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.disabled {
|
|
779
|
+
opacity: 0.5;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/* Ticks — M3 stop indicator dots on the track */
|
|
783
|
+
.ticks {
|
|
784
|
+
position: absolute;
|
|
785
|
+
left: 0;
|
|
786
|
+
right: 0;
|
|
787
|
+
top: 0;
|
|
788
|
+
bottom: 0;
|
|
789
|
+
pointer-events: none;
|
|
790
|
+
z-index: 3;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.tick {
|
|
794
|
+
position: absolute;
|
|
795
|
+
top: 50%;
|
|
796
|
+
width: 4px;
|
|
797
|
+
height: 4px;
|
|
798
|
+
border-radius: 50%;
|
|
799
|
+
background: var(--fill-color);
|
|
800
|
+
opacity: 0.6;
|
|
801
|
+
transform: translate(-50%, -50%);
|
|
802
|
+
}
|
|
803
|
+
.tick.active {
|
|
804
|
+
background: var(--color-action-text, white);
|
|
805
|
+
opacity: 0.6;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.tick-label {
|
|
809
|
+
position: absolute;
|
|
810
|
+
top: calc(var(--handle-height) / 2 + 6px);
|
|
811
|
+
left: 50%;
|
|
812
|
+
transform: translateX(-50%);
|
|
813
|
+
font-size: 0.75em;
|
|
814
|
+
color: var(--color-text-muted, inherit);
|
|
815
|
+
white-space: nowrap;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/* Value tooltip */
|
|
819
|
+
.value-tooltip {
|
|
820
|
+
position: absolute;
|
|
821
|
+
bottom: calc(100% + 8px);
|
|
822
|
+
transform: translateX(-50%) translateY(4px);
|
|
823
|
+
background: var(--color-action-active, hsl(220 70% 50%));
|
|
824
|
+
color: var(--color-action-text, white);
|
|
825
|
+
padding: 2px 8px;
|
|
826
|
+
border-radius: 4px;
|
|
827
|
+
font-size: 0.8em;
|
|
828
|
+
font-weight: 600;
|
|
829
|
+
white-space: nowrap;
|
|
830
|
+
pointer-events: none;
|
|
831
|
+
z-index: 3;
|
|
832
|
+
opacity: 0;
|
|
833
|
+
visibility: hidden;
|
|
834
|
+
transition:
|
|
835
|
+
opacity 150ms ease,
|
|
836
|
+
transform 150ms ease,
|
|
837
|
+
visibility 150ms ease;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
.value-tooltip.visible {
|
|
841
|
+
opacity: 1;
|
|
842
|
+
visibility: visible;
|
|
843
|
+
transform: translateX(-50%) translateY(0);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/* Tooltip arrow */
|
|
847
|
+
.value-tooltip::after {
|
|
848
|
+
content: '';
|
|
849
|
+
position: absolute;
|
|
850
|
+
top: 100%;
|
|
851
|
+
left: 50%;
|
|
852
|
+
transform: translateX(-50%);
|
|
853
|
+
border: 4px solid transparent;
|
|
854
|
+
border-top-color: var(--color-action-active, hsl(220 70% 50%));
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/* Value display below */
|
|
858
|
+
.value-display {
|
|
859
|
+
display: flex;
|
|
860
|
+
justify-content: space-between;
|
|
861
|
+
color: var(--color-text-muted, inherit);
|
|
862
|
+
font-size: 0.85em;
|
|
863
|
+
font-variant-numeric: tabular-nums;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
.has-tick-labels .range-wrapper {
|
|
867
|
+
margin-bottom: 1.5em;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/* ========== Vertical mode ========== */
|
|
871
|
+
.range-container.vertical {
|
|
872
|
+
width: fit-content;
|
|
873
|
+
height: var(--range-height, 200px);
|
|
874
|
+
align-items: center;
|
|
875
|
+
overflow: visible;
|
|
876
|
+
|
|
877
|
+
.range-wrapper {
|
|
878
|
+
/* Rotate the horizontal slider to render vertically.
|
|
879
|
+
The wrapper's CSS "width" becomes the visual height. */
|
|
880
|
+
width: var(--range-height, 200px);
|
|
881
|
+
transform-origin: 0 0;
|
|
882
|
+
transform: rotate(-90deg) translateX(-100%);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/* Vertical tooltip: reposition to the right of the handle, counter-rotate text */
|
|
886
|
+
.value-tooltip {
|
|
887
|
+
/* In rotated space, "bottom: 100%" places the tooltip to the visual-left.
|
|
888
|
+
Switch to "top: 100%" so it appears to the visual-right instead. */
|
|
889
|
+
bottom: auto;
|
|
890
|
+
top: calc(100% + 8px);
|
|
891
|
+
transform: translateX(-50%) translateY(-4px) rotate(90deg) translateX(18px);
|
|
892
|
+
|
|
893
|
+
&.visible {
|
|
894
|
+
transform: translateX(-50%) translateY(0) rotate(90deg) translateX(10px);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/* Arrow points left instead of down */
|
|
898
|
+
&::after {
|
|
899
|
+
top: 50%;
|
|
900
|
+
left: auto;
|
|
901
|
+
right: 100%;
|
|
902
|
+
transform: translateY(-50%);
|
|
903
|
+
border-top-color: transparent;
|
|
904
|
+
border-right-color: var(--color-action-active, hsl(220 70% 50%));
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
.tick-label {
|
|
909
|
+
transform: translateX(-50%) rotate(90deg);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
</style>
|