@marianmeres/stuic 3.143.0 → 3.145.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.
@@ -0,0 +1,521 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+ import type {
5
+ ValidateOptions,
6
+ ValidationResult,
7
+ } from "../../actions/validate.svelte.js";
8
+
9
+ export type SliderIntent = "primary" | "accent" | "success" | "warning" | "destructive";
10
+
11
+ export type SliderOrientation = "horizontal" | "vertical";
12
+
13
+ export type SliderThumbPosition = "value" | "start";
14
+
15
+ /** Context passed to the `thumb` and `valueLabel` snippets. */
16
+ export interface SliderRenderCtx {
17
+ value: number;
18
+ /** Normalized value position, 0..1 */
19
+ ratio: number;
20
+ /** Normalized value position, 0..100 */
21
+ percent: number;
22
+ dragging: boolean;
23
+ }
24
+
25
+ export interface Props extends Omit<
26
+ HTMLAttributes<HTMLDivElement>,
27
+ | "children"
28
+ | "oninput"
29
+ | "onchange"
30
+ // Reserved by the slider's own drag machinery (compile-time guard against
31
+ // silently clobbering them via the rest spread).
32
+ | "onpointerdown"
33
+ | "onpointermove"
34
+ | "onpointerup"
35
+ | "onpointercancel"
36
+ > {
37
+ /**
38
+ * Current value (bindable). Defaults to `min`. Non-finite, out-of-range and
39
+ * off-step-grid writes are normalized back into the binding, so the bound
40
+ * value always equals what is rendered and submitted.
41
+ */
42
+ value?: number;
43
+ min?: number;
44
+ max?: number;
45
+ /**
46
+ * Snap increment. Use "any" for continuous (no snapping). Non-positive
47
+ * numbers are treated as "any".
48
+ */
49
+ step?: number | "any";
50
+ orientation?: SliderOrientation;
51
+ /** Cross-axis thickness preset (main-axis length via `--stuic-slider-length` or class) */
52
+ size?: "sm" | "md" | "lg" | string;
53
+ /** Semantic color intent (colors the fill) */
54
+ intent?: SliderIntent;
55
+ disabled?: boolean;
56
+ /** Screen reader label for the underlying range input */
57
+ label?: string;
58
+ /** Id for the underlying range input (enables `<label for>` association) */
59
+ id?: string;
60
+ /** Form field name for the hidden range input */
61
+ name?: string;
62
+ /**
63
+ * Forwarded to the hidden range input. NOTE: per HTML spec `required` has
64
+ * no effect on range inputs (they always have a value) — use
65
+ * `validate.customValidator` for custom rules.
66
+ */
67
+ required?: boolean;
68
+ /** Skip all default styling, use only custom classes */
69
+ unstyled?: boolean;
70
+ class?: string;
71
+ /** Classes for the track (pill background) element */
72
+ trackClass?: string;
73
+ /** Classes for the fill (value indicator) element */
74
+ fillClass?: string;
75
+ /** Classes for the thumb element */
76
+ thumbClass?: string;
77
+ /** Classes for each tick mark element */
78
+ tickClass?: string;
79
+ /** Classes for the floating value label wrapper */
80
+ valueClass?: string;
81
+ /**
82
+ * Thumb rendering: `true` (default) renders an empty thumb, `false` hides it
83
+ * (fill-only look), a snippet renders custom content inside the thumb.
84
+ */
85
+ thumb?: boolean | Snippet<[SliderRenderCtx]>;
86
+ /**
87
+ * Where the thumb sits: `"value"` (default) rides the fill edge, `"start"`
88
+ * pins it to the start of the track (left / bottom) so only the bar moves —
89
+ * the true iOS volume look with a fixed icon.
90
+ */
91
+ thumbPosition?: SliderThumbPosition;
92
+ /**
93
+ * Round the fill's leading edge (instead of a flat cut), giving the
94
+ * "pill inside a pill" look.
95
+ */
96
+ fillRounded?: boolean;
97
+ /**
98
+ * Tick marks along the track: `true` renders one at every `step` (requires a
99
+ * positive numeric step; auto ticks are skipped above 101 — pass an explicit
100
+ * array instead), an array renders them at the given (in-range) values.
101
+ */
102
+ ticks?: boolean | number[];
103
+ /** Floating current-value label (tracks the value along the track, outside it) */
104
+ valueLabel?: Snippet<[SliderRenderCtx]>;
105
+ /** Bindable root element reference */
106
+ el?: HTMLDivElement;
107
+ /** Bindable hidden range input reference */
108
+ inputEl?: HTMLInputElement;
109
+ /** Fires on every actual value change (dragging, keyboard) */
110
+ oninput?: (value: number) => void;
111
+ /** Fires when a changed value is committed (drag released, keyboard) */
112
+ onchange?: (value: number) => void;
113
+ validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
114
+ setValidationResult?: (res: ValidationResult) => void;
115
+ }
116
+ </script>
117
+
118
+ <script lang="ts">
119
+ import { twMerge } from "../../utils/tw-merge.js";
120
+ import { validate as validateAction } from "../../actions/validate.svelte.js";
121
+
122
+ let {
123
+ value = $bindable(),
124
+ min = 0,
125
+ max = 100,
126
+ step = 1,
127
+ orientation = "horizontal",
128
+ size = "md",
129
+ intent,
130
+ disabled,
131
+ label,
132
+ id,
133
+ name,
134
+ required,
135
+ unstyled = false,
136
+ class: classProp,
137
+ trackClass,
138
+ fillClass,
139
+ thumbClass,
140
+ tickClass,
141
+ valueClass,
142
+ thumb = true,
143
+ thumbPosition = "value",
144
+ fillRounded = false,
145
+ ticks,
146
+ valueLabel,
147
+ el = $bindable(),
148
+ inputEl = $bindable(),
149
+ oninput,
150
+ onchange,
151
+ // Renamed local binding to avoid collision with `export function validate()` below.
152
+ validate: validateProp,
153
+ setValidationResult,
154
+ ...rest
155
+ }: Props = $props();
156
+
157
+ const MAX_AUTO_TICKS = 100;
158
+
159
+ let _dragging = $state(false);
160
+ // Active drag pointer (ignore other concurrent pointers) and the main-axis px
161
+ // offset between the grab point and the thumb center, so grabbing the thumb
162
+ // off-center does not jump the value.
163
+ let _pointerId: number | null = null;
164
+ let _grabOffset = 0;
165
+ // Value at drag start — commit (change) fires only if the drag changed it.
166
+ let _dragStartValue = 0;
167
+
168
+ // Only a value-tracking thumb insets the usable travel; a start-pinned thumb
169
+ // (or none) lets the fill map linearly across the whole track.
170
+ let _thumbTravels = $derived(thumb !== false && thumbPosition === "value");
171
+
172
+ // Bounds must be finite before anything else: a NaN bound would make every
173
+ // normalization write NaN, and NaN !== NaN would re-trigger the effect below
174
+ // forever (Svelte sources compare with ===) — an app-killing update-depth error.
175
+ let _rawMin = $derived(Number.isFinite(min) ? min : 0);
176
+ let _rawMax = $derived(Number.isFinite(max) ? max : 100);
177
+ let _min = $derived(Math.min(_rawMin, _rawMax));
178
+ let _max = $derived(Math.max(_rawMin, _rawMax));
179
+ let _span = $derived(_max - _min);
180
+ // The hidden input must never receive an invalid step (the browser would fall
181
+ // back to step=1 and re-sanitize values the JS side left continuous).
182
+ let _stepAttr = $derived(typeof step === "number" && step > 0 ? step : "any");
183
+
184
+ // Normalize the binding: undefined/non-finite becomes `min`, out-of-range is
185
+ // clamped, off-grid is snapped — so the bound value always matches what is
186
+ // rendered AND what the hidden input holds (the browser sanitizes off-grid
187
+ // values onto the step grid, which would otherwise diverge silently).
188
+ $effect(() => {
189
+ const next = _normalize(value);
190
+ if (!Object.is(value, next)) value = next;
191
+ });
192
+
193
+ let _value = $derived(_normalize(value));
194
+ let _ratio = $derived(_span ? (_value - _min) / _span : 0);
195
+ let _ctx: SliderRenderCtx = $derived({
196
+ value: _value,
197
+ ratio: _ratio,
198
+ percent: _ratio * 100,
199
+ dragging: _dragging,
200
+ });
201
+
202
+ function _clamp(v: number): number {
203
+ if (!Number.isFinite(v)) return _min;
204
+ return Math.min(Math.max(v, _min), _max);
205
+ }
206
+
207
+ /** The canonical value for any input: always finite, in range, and on-grid. */
208
+ function _normalize(v: number | undefined): number {
209
+ if (v === undefined || !Number.isFinite(v)) return _min;
210
+ return _snap(v);
211
+ }
212
+
213
+ function _decimals(n: number): number {
214
+ const s = String(Math.abs(n));
215
+ const e = s.indexOf("e");
216
+ if (e === -1) {
217
+ const i = s.indexOf(".");
218
+ return i === -1 ? 0 : s.length - i - 1;
219
+ }
220
+ // Exponential notation (e.g. "1e-7", "1.5e-7")
221
+ const exp = Number(s.slice(e + 1));
222
+ const mant = s.slice(0, e);
223
+ const mi = mant.indexOf(".");
224
+ const mantDec = mi === -1 ? 0 : mant.length - mi - 1;
225
+ return Math.max(0, mantDec - exp);
226
+ }
227
+
228
+ /**
229
+ * Snap to the step grid (anchored at `min`). Values beyond the last reachable
230
+ * grid point resolve to that grid point — NOT to an off-grid `max` — matching
231
+ * the browser's own range value sanitization (so the hidden input never
232
+ * re-sanitizes to a different number than the bound value).
233
+ */
234
+ function _snap(v: number): number {
235
+ if (!(typeof step === "number" && step > 0)) return _clamp(v);
236
+ let snapped = _min + Math.round((v - _min) / step) * step;
237
+ if (snapped > _max) snapped = _min + Math.floor(_span / step + 1e-9) * step;
238
+ if (snapped < _min) snapped = _min;
239
+ // Trim floating point noise (e.g. 0.1 + 0.2)
240
+ const decimals = Math.min(Math.max(_decimals(step), _decimals(_min)), 20);
241
+ return Number(snapped.toFixed(decimals));
242
+ }
243
+
244
+ interface Geometry {
245
+ horizontal: boolean;
246
+ rtl: boolean;
247
+ rect: DOMRect;
248
+ trackLen: number;
249
+ /** Dead zone at each end of the track (half the thumb size incl. inset) */
250
+ pad: number;
251
+ /** Usable px distance the value maps onto */
252
+ travel: number;
253
+ }
254
+
255
+ function _geometry(): Geometry {
256
+ const rect = el!.getBoundingClientRect();
257
+ const horizontal = orientation === "horizontal";
258
+ const rtl = horizontal && getComputedStyle(el!).direction === "rtl";
259
+ const trackLen = horizontal ? rect.width : rect.height;
260
+ const thickness = horizontal ? rect.height : rect.width;
261
+ // With a thumb, the thumb center travels within [thickness/2, len - thickness/2]
262
+ // (mirrors the CSS `(100% - thickness) * ratio` positioning); without it, the
263
+ // value maps linearly across the whole track. NOTE: this assumes the rendered
264
+ // cross-axis size equals --_thickness — size the cross-axis via the size
265
+ // presets or --stuic-slider-thickness, not via utility classes (see README).
266
+ const pad = _thumbTravels ? thickness / 2 : 0;
267
+ const travel = Math.max(1, trackLen - 2 * pad);
268
+ return { horizontal, rtl, rect, trackLen, pad, travel };
269
+ }
270
+
271
+ /** Pointer position in track coords, measured from the value=min end. */
272
+ function _pointerPos(e: PointerEvent, g: Geometry): number {
273
+ if (!g.horizontal) return g.rect.bottom - e.clientY;
274
+ return g.rtl ? g.rect.right - e.clientX : e.clientX - g.rect.left;
275
+ }
276
+
277
+ function _posToValue(e: PointerEvent, g: Geometry): number {
278
+ const r = (_pointerPos(e, g) - _grabOffset - g.pad) / g.travel;
279
+ return _snap(_min + r * _span);
280
+ }
281
+
282
+ function _apply(v: number) {
283
+ const prev = _value;
284
+ if (v === prev) return;
285
+ value = v;
286
+ // Re-read through the derived: a bound parent may have transformed or
287
+ // rejected the write synchronously.
288
+ const current = _value;
289
+ // Sync the hidden input synchronously (Svelte's template update is async
290
+ // and skips entirely when the parent nets the value back to what it was).
291
+ if (inputEl && inputEl.value !== String(current)) inputEl.value = String(current);
292
+ if (current !== prev) oninput?.(current);
293
+ }
294
+
295
+ function _endDrag(e: PointerEvent) {
296
+ _dragging = false;
297
+ _pointerId = null;
298
+ if (el?.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
299
+ }
300
+
301
+ function _onpointerdown(e: PointerEvent) {
302
+ // Set before the early-returns: any pointer press over the component means
303
+ // the focus that follows is pointer-initiated and must not show the ring
304
+ // (a right-click still focuses the full-size hidden input).
305
+ _pointerFocus = true;
306
+ if (disabled || _dragging || e.button !== 0) return;
307
+ e.preventDefault();
308
+ const g = _geometry();
309
+ const pos = _pointerPos(e, g);
310
+ // Grabbing (near) the thumb must not jump the value — detected geometrically
311
+ // (the full-size hidden input is the topmost hit target, so DOM-target
312
+ // checks would never match the thumb element).
313
+ const thumbCenter = g.pad + g.travel * _ratio;
314
+ _grabOffset =
315
+ _thumbTravels && Math.abs(pos - thumbCenter) <= g.pad ? pos - thumbCenter : 0;
316
+ try {
317
+ el?.setPointerCapture(e.pointerId);
318
+ } catch {
319
+ // NotFoundError for a pointerId that is not actively down (synthetic
320
+ // events) — capture is an optimization here, the drag works without it.
321
+ }
322
+ _pointerId = e.pointerId;
323
+ _dragging = true;
324
+ _dragStartValue = _value;
325
+ _apply(_posToValue(e, g));
326
+ inputEl?.focus();
327
+ }
328
+
329
+ function _onpointermove(e: PointerEvent) {
330
+ if (!_dragging || e.pointerId !== _pointerId) return;
331
+ if (disabled) return _endDrag(e);
332
+ _apply(_posToValue(e, _geometry()));
333
+ }
334
+
335
+ function _onpointerup(e: PointerEvent) {
336
+ if (!_dragging || e.pointerId !== _pointerId) return;
337
+ const wasDisabled = disabled;
338
+ _endDrag(e);
339
+ if (wasDisabled) return;
340
+ // Native "change" (commit) semantics — only when the drag actually changed
341
+ // the value. Also triggers the validate action and the `onchange` handler
342
+ // on the hidden input below.
343
+ if (_value !== _dragStartValue) {
344
+ inputEl?.dispatchEvent(new Event("change", { bubbles: true }));
345
+ }
346
+ }
347
+
348
+ let _tickRatios = $derived.by(() => {
349
+ if (!ticks || !_span) return [];
350
+ let vals: number[];
351
+ if (Array.isArray(ticks)) {
352
+ vals = ticks;
353
+ } else if (typeof step === "number" && step > 0) {
354
+ const count = Math.floor(_span / step + 1e-9);
355
+ if (count > MAX_AUTO_TICKS) {
356
+ console.warn(
357
+ `[stuic] Slider: ${count + 1} auto ticks exceed the limit of ${MAX_AUTO_TICKS + 1}, ` +
358
+ `skipping (pass an explicit \`ticks\` array instead)`
359
+ );
360
+ return [];
361
+ }
362
+ // Through _snap so float accumulation (0 + 3 * 0.1 = 0.30000000000000004)
363
+ // cannot push the last tick past _max and drop it.
364
+ vals = Array.from({ length: count + 1 }, (_, i) => _snap(_min + i * step));
365
+ } else {
366
+ return [];
367
+ }
368
+ return [...new Set(vals)]
369
+ .filter((v) => v >= _min && v <= _max)
370
+ .map((v) => (v - _min) / _span);
371
+ });
372
+
373
+ // Deterministic, cross-browser focus ring: shown for keyboard-initiated focus
374
+ // only (:focus-visible leaks on pointer-initiated programmatic focus in
375
+ // Chromium/WebKit).
376
+ let _showRing = $state(false);
377
+ let _pointerFocus = false;
378
+
379
+ //
380
+ let _doValidate: (() => void) | undefined = $state();
381
+ // Local copy of the last validation result so getValidation() works even
382
+ // when no external setValidationResult was provided.
383
+ let _validation: ValidationResult | undefined = $state();
384
+
385
+ /** Trigger validation now. Reaches the parent via `setValidationResult`. */
386
+ export function validate(): ValidationResult | undefined {
387
+ _doValidate?.();
388
+ return _validation;
389
+ }
390
+
391
+ /** Clear the inline validation message and reset `setCustomValidity`. */
392
+ export function clearValidation(): void {
393
+ _validation = undefined;
394
+ inputEl?.setCustomValidity?.("");
395
+ }
396
+
397
+ /** Current validation state. */
398
+ export function getValidation(): ValidationResult | undefined {
399
+ return _validation;
400
+ }
401
+
402
+ /** Focus the underlying range input. */
403
+ export function focus(): void {
404
+ inputEl?.focus?.();
405
+ }
406
+
407
+ /** Scroll the slider into view. */
408
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
409
+ el?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
410
+ }
411
+ </script>
412
+
413
+ <!-- `rest` is spread first intentionally: the pointer handlers and computed
414
+ attributes below are functionally required and must not be clobbered.
415
+ `data-stuic-slider` is always emitted (even when `unstyled`) — index.css
416
+ hangs the FUNCTIONAL declarations off it (touch-action, position, …) so
417
+ dragging survives `unstyled`, while staying attribute-specificity so a
418
+ consumer utility class can still override it. -->
419
+ <div
420
+ {...rest}
421
+ bind:this={el}
422
+ class={unstyled ? classProp : twMerge("stuic-slider", classProp)}
423
+ style:--_ratio={_ratio}
424
+ data-stuic-slider=""
425
+ data-orientation={orientation}
426
+ data-thumb={thumb !== false ? "true" : "false"}
427
+ data-thumb-position={thumb !== false ? thumbPosition : undefined}
428
+ data-thumb-travels={_thumbTravels ? "true" : "false"}
429
+ data-fill-rounded={fillRounded ? "true" : undefined}
430
+ data-size={!unstyled ? size : undefined}
431
+ data-intent={!unstyled ? intent : undefined}
432
+ data-disabled={disabled ? "true" : undefined}
433
+ data-dragging={_dragging ? "true" : undefined}
434
+ data-ring={_showRing ? "true" : undefined}
435
+ onpointerdown={_onpointerdown}
436
+ onpointermove={_onpointermove}
437
+ onpointerup={_onpointerup}
438
+ onpointercancel={_onpointerup}
439
+ >
440
+ <div class={twMerge("track", trackClass)}>
441
+ <div class={twMerge("fill", fillClass)}></div>
442
+ {#if _tickRatios.length}
443
+ <!-- Styled mode renders two complementary layers — the base one clipped
444
+ to the unfilled part of the track, the `on-fill` one to the filled
445
+ part — so ticks stay readable over both. `unstyled` has no CSS to
446
+ clip them, so it gets the single plain layer. -->
447
+ {#each unstyled ? [false] : [false, true] as onFill}
448
+ <div class="ticks" data-on-fill={onFill ? "true" : undefined} aria-hidden="true">
449
+ {#each _tickRatios as r}
450
+ <div class={twMerge("tick", tickClass)} style:--_tick-ratio={r}></div>
451
+ {/each}
452
+ </div>
453
+ {/each}
454
+ {/if}
455
+ </div>
456
+ {#if thumb !== false}
457
+ <div class={twMerge("thumb", thumbClass)} aria-hidden="true">
458
+ {#if typeof thumb === "function"}
459
+ {@render thumb(_ctx)}
460
+ {/if}
461
+ </div>
462
+ {/if}
463
+ {#if valueLabel}
464
+ <div class={twMerge("value", valueClass)} aria-hidden="true">
465
+ {@render valueLabel(_ctx)}
466
+ </div>
467
+ {/if}
468
+ <!-- Visually hidden but full-size, so touch-based assistive tech (VoiceOver /
469
+ TalkBack explore-by-touch) can hit-test the real slider role. Pointer
470
+ events bubble to the root handler, which preventDefault()s the native
471
+ drag behavior. -->
472
+ <input
473
+ bind:this={inputEl}
474
+ type="range"
475
+ class="absolute inset-0 size-full opacity-0"
476
+ value={_value}
477
+ min={_min}
478
+ max={_max}
479
+ step={_stepAttr}
480
+ {id}
481
+ {name}
482
+ {required}
483
+ {disabled}
484
+ aria-label={label}
485
+ aria-orientation={orientation === "vertical" ? "vertical" : undefined}
486
+ onfocus={() => {
487
+ _showRing = !_pointerFocus;
488
+ _pointerFocus = false;
489
+ }}
490
+ onblur={() => {
491
+ _showRing = false;
492
+ _pointerFocus = false;
493
+ }}
494
+ onkeydown={() => (_showRing = true)}
495
+ oninput={(e) => {
496
+ const t = e.currentTarget;
497
+ const v = t.valueAsNumber;
498
+ if (!Number.isNaN(v) && v !== value) {
499
+ const prev = _value;
500
+ value = v;
501
+ if (_value !== prev) oninput?.(_value);
502
+ }
503
+ // Resync the DOM in case a bound parent transformed/rejected the write
504
+ // (the template update skips when the parent nets the value back).
505
+ if (t.value !== String(_value)) t.value = String(_value);
506
+ }}
507
+ onchange={(e) => {
508
+ const v = e.currentTarget.valueAsNumber;
509
+ if (!Number.isNaN(v)) onchange?.(v);
510
+ }}
511
+ use:validateAction={() => ({
512
+ enabled: validateProp !== false,
513
+ ...(typeof validateProp === "boolean" ? {} : validateProp),
514
+ setValidationResult: (res) => {
515
+ _validation = res;
516
+ setValidationResult?.(res);
517
+ },
518
+ setDoValidate: (fn) => (_doValidate = fn),
519
+ })}
520
+ />
521
+ </div>
@@ -0,0 +1,104 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import type { ValidateOptions, ValidationResult } from "../../actions/validate.svelte.js";
4
+ export type SliderIntent = "primary" | "accent" | "success" | "warning" | "destructive";
5
+ export type SliderOrientation = "horizontal" | "vertical";
6
+ export type SliderThumbPosition = "value" | "start";
7
+ /** Context passed to the `thumb` and `valueLabel` snippets. */
8
+ export interface SliderRenderCtx {
9
+ value: number;
10
+ /** Normalized value position, 0..1 */
11
+ ratio: number;
12
+ /** Normalized value position, 0..100 */
13
+ percent: number;
14
+ dragging: boolean;
15
+ }
16
+ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "oninput" | "onchange" | "onpointerdown" | "onpointermove" | "onpointerup" | "onpointercancel"> {
17
+ /**
18
+ * Current value (bindable). Defaults to `min`. Non-finite, out-of-range and
19
+ * off-step-grid writes are normalized back into the binding, so the bound
20
+ * value always equals what is rendered and submitted.
21
+ */
22
+ value?: number;
23
+ min?: number;
24
+ max?: number;
25
+ /**
26
+ * Snap increment. Use "any" for continuous (no snapping). Non-positive
27
+ * numbers are treated as "any".
28
+ */
29
+ step?: number | "any";
30
+ orientation?: SliderOrientation;
31
+ /** Cross-axis thickness preset (main-axis length via `--stuic-slider-length` or class) */
32
+ size?: "sm" | "md" | "lg" | string;
33
+ /** Semantic color intent (colors the fill) */
34
+ intent?: SliderIntent;
35
+ disabled?: boolean;
36
+ /** Screen reader label for the underlying range input */
37
+ label?: string;
38
+ /** Id for the underlying range input (enables `<label for>` association) */
39
+ id?: string;
40
+ /** Form field name for the hidden range input */
41
+ name?: string;
42
+ /**
43
+ * Forwarded to the hidden range input. NOTE: per HTML spec `required` has
44
+ * no effect on range inputs (they always have a value) — use
45
+ * `validate.customValidator` for custom rules.
46
+ */
47
+ required?: boolean;
48
+ /** Skip all default styling, use only custom classes */
49
+ unstyled?: boolean;
50
+ class?: string;
51
+ /** Classes for the track (pill background) element */
52
+ trackClass?: string;
53
+ /** Classes for the fill (value indicator) element */
54
+ fillClass?: string;
55
+ /** Classes for the thumb element */
56
+ thumbClass?: string;
57
+ /** Classes for each tick mark element */
58
+ tickClass?: string;
59
+ /** Classes for the floating value label wrapper */
60
+ valueClass?: string;
61
+ /**
62
+ * Thumb rendering: `true` (default) renders an empty thumb, `false` hides it
63
+ * (fill-only look), a snippet renders custom content inside the thumb.
64
+ */
65
+ thumb?: boolean | Snippet<[SliderRenderCtx]>;
66
+ /**
67
+ * Where the thumb sits: `"value"` (default) rides the fill edge, `"start"`
68
+ * pins it to the start of the track (left / bottom) so only the bar moves —
69
+ * the true iOS volume look with a fixed icon.
70
+ */
71
+ thumbPosition?: SliderThumbPosition;
72
+ /**
73
+ * Round the fill's leading edge (instead of a flat cut), giving the
74
+ * "pill inside a pill" look.
75
+ */
76
+ fillRounded?: boolean;
77
+ /**
78
+ * Tick marks along the track: `true` renders one at every `step` (requires a
79
+ * positive numeric step; auto ticks are skipped above 101 — pass an explicit
80
+ * array instead), an array renders them at the given (in-range) values.
81
+ */
82
+ ticks?: boolean | number[];
83
+ /** Floating current-value label (tracks the value along the track, outside it) */
84
+ valueLabel?: Snippet<[SliderRenderCtx]>;
85
+ /** Bindable root element reference */
86
+ el?: HTMLDivElement;
87
+ /** Bindable hidden range input reference */
88
+ inputEl?: HTMLInputElement;
89
+ /** Fires on every actual value change (dragging, keyboard) */
90
+ oninput?: (value: number) => void;
91
+ /** Fires when a changed value is committed (drag released, keyboard) */
92
+ onchange?: (value: number) => void;
93
+ validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
94
+ setValidationResult?: (res: ValidationResult) => void;
95
+ }
96
+ declare const Slider: import("svelte").Component<Props, {
97
+ validate: () => ValidationResult | undefined;
98
+ clearValidation: () => void;
99
+ getValidation: () => ValidationResult | undefined;
100
+ focus: () => void;
101
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
102
+ }, "el" | "value" | "inputEl">;
103
+ type Slider = ReturnType<typeof Slider>;
104
+ export default Slider;