@marianmeres/stuic 3.145.0 → 3.148.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.
|
@@ -10,37 +10,38 @@ variant for custom UI (volume/brightness controls, dashboards, media players).
|
|
|
10
10
|
|
|
11
11
|
## Props
|
|
12
12
|
|
|
13
|
-
| Prop | Type | Default | Description
|
|
14
|
-
| --------------- | ------------------------------------------------------------------ | -------------- |
|
|
15
|
-
| `value` | `number` | `min` | Current value (bindable; non-finite / out-of-range / off-grid writes are normalized back)
|
|
16
|
-
| `min` | `number` | `0` | Minimum value
|
|
17
|
-
| `max` | `number` | `100` | Maximum value
|
|
18
|
-
| `step` | `number \| "any"` | `1` | Snap increment (`"any"` or non-positive = continuous)
|
|
19
|
-
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Slider direction (vertical fills bottom-up)
|
|
20
|
-
| `size` | `"sm" \| "md" \| "lg" \| string` | `"md"` | Cross-axis thickness preset
|
|
21
|
-
| `intent` | `"primary" \| "accent" \| "success" \| "warning" \| "destructive"` | - | Semantic fill color
|
|
22
|
-
| `thumb` | `boolean \| Snippet<[SliderRenderCtx]>` | `true` | `false` hides the thumb (fill-only look), snippet renders inside thumb
|
|
23
|
-
| `thumbPosition` | `"value" \| "start"` | `"value"` | `"value"` rides the fill edge; `"start"` pins it to the left/bottom so only the bar moves (true iOS volume look)
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
13
|
+
| Prop | Type | Default | Description |
|
|
14
|
+
| --------------- | ------------------------------------------------------------------ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
15
|
+
| `value` | `number` | `min` | Current value (bindable; non-finite / out-of-range / off-grid writes are normalized back) |
|
|
16
|
+
| `min` | `number` | `0` | Minimum value |
|
|
17
|
+
| `max` | `number` | `100` | Maximum value |
|
|
18
|
+
| `step` | `number \| "any"` | `1` | Snap increment (`"any"` or non-positive = continuous) |
|
|
19
|
+
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Slider direction (vertical fills bottom-up) |
|
|
20
|
+
| `size` | `"sm" \| "md" \| "lg" \| string` | `"md"` | Cross-axis thickness preset |
|
|
21
|
+
| `intent` | `"primary" \| "accent" \| "success" \| "warning" \| "destructive"` | - | Semantic fill color |
|
|
22
|
+
| `thumb` | `boolean \| Snippet<[SliderRenderCtx]>` | `true` | `false` hides the thumb (fill-only look), snippet renders inside thumb |
|
|
23
|
+
| `thumbPosition` | `"value" \| "start"` | `"value"` | `"value"` rides the fill edge; `"start"` pins it to the left/bottom so only the bar moves (true iOS volume look) |
|
|
24
|
+
| `thumbReserve` | `boolean` | `true` | Reserve the thumb's footprint along the track, so the fill never shrinks below the thumb. `false` = linear mapping across the full track (fill collapses to zero at `min`); only honored with `thumbPosition="start"` |
|
|
25
|
+
| `fillRounded` | `boolean` | `false` | Round the fill's leading edge ("pill inside a pill") |
|
|
26
|
+
| `ticks` | `boolean \| number[]` | - | `true` = tick at every `step` (positive numeric step only; skipped above 101 auto ticks — pass an array), array = ticks at given in-range values |
|
|
27
|
+
| `valueLabel` | `Snippet<[SliderRenderCtx]>` | - | Floating label at the current value along the track |
|
|
28
|
+
| `disabled` | `boolean` | `false` | Disable interaction |
|
|
29
|
+
| `label` | `string` | - | Screen reader label for the underlying input |
|
|
30
|
+
| `id` | `string` | - | Id for the underlying input (enables `<label for>` association) |
|
|
31
|
+
| `name` | `string` | - | Form field name for the hidden range input |
|
|
32
|
+
| `required` | `boolean` | `false` | Forwarded to the input; per HTML spec inert on range inputs (use `validate.customValidator` for custom rules) |
|
|
33
|
+
| `oninput` | `(value: number) => void` | - | Fires on every value change (drag, keyboard) |
|
|
34
|
+
| `onchange` | `(value: number) => void` | - | Fires when a change is committed (drag release, keyboard) |
|
|
35
|
+
| `validate` | `boolean \| ValidateOptions` | - | Enable validation (stuic validate action) |
|
|
36
|
+
| `unstyled` | `boolean` | `false` | Skip all default styling |
|
|
37
|
+
| `class` | `string` | - | Classes for the root element |
|
|
38
|
+
| `trackClass` | `string` | - | Classes for the track (pill background) |
|
|
39
|
+
| `fillClass` | `string` | - | Classes for the fill (value indicator) |
|
|
40
|
+
| `thumbClass` | `string` | - | Classes for the thumb |
|
|
41
|
+
| `tickClass` | `string` | - | Classes for each tick mark |
|
|
42
|
+
| `valueClass` | `string` | - | Classes for the value label wrapper |
|
|
43
|
+
| `el` | `HTMLDivElement` | - | Root element reference (bindable) |
|
|
44
|
+
| `inputEl` | `HTMLInputElement` | - | Hidden range input reference (bindable) |
|
|
44
45
|
|
|
45
46
|
`SliderRenderCtx` (passed to the `thumb` and `valueLabel` snippets):
|
|
46
47
|
`{ value: number; ratio: number /* 0..1 */; percent: number /* 0..100 */; dragging: boolean }`
|
|
@@ -92,6 +93,9 @@ Remaining props are spread onto the root `<div>`.
|
|
|
92
93
|
<Slider thumb={false} bind:value={brightness} />
|
|
93
94
|
```
|
|
94
95
|
|
|
96
|
+
With no thumb there is nothing to reserve: the value maps linearly across the whole
|
|
97
|
+
track and the fill collapses to zero at `min`.
|
|
98
|
+
|
|
95
99
|
### True iOS volume: fixed icon at the start, only the bar moves
|
|
96
100
|
|
|
97
101
|
```svelte
|
|
@@ -116,8 +120,19 @@ Remaining props are spread onto the root `<div>`.
|
|
|
116
120
|
</Slider>
|
|
117
121
|
```
|
|
118
122
|
|
|
119
|
-
With `thumbPosition="start"` the thumb
|
|
120
|
-
|
|
123
|
+
With `thumbPosition="start"` the thumb never moves — only the bar does. It is purely
|
|
124
|
+
decorative (the full-size hidden input is always the topmost hit target, so the thumb
|
|
125
|
+
never receives pointer events), but it still **reserves its footprint** along the track:
|
|
126
|
+
the fill can never shrink below it, and at `min` it is exactly a thumb-sized nub — with
|
|
127
|
+
`fillRounded`, a circle enclosing the pinned thumb, same as the default
|
|
128
|
+
`thumbPosition="value"` look.
|
|
129
|
+
|
|
130
|
+
Opt out with `thumbReserve={false}` for a linear mapping across the full track, where
|
|
131
|
+
the fill collapses to zero at `min` (and, at low values, is shorter than the thumb):
|
|
132
|
+
|
|
133
|
+
```svelte
|
|
134
|
+
<Slider thumbPosition="start" thumbReserve={false} bind:value={volume} />
|
|
135
|
+
```
|
|
121
136
|
|
|
122
137
|
### Steps and ticks
|
|
123
138
|
|
|
@@ -156,6 +171,10 @@ value maps linearly across the full track — there is no thumb-travel inset.
|
|
|
156
171
|
the thumb itself does not jump (drag continues from the grab point).
|
|
157
172
|
- **Keyboard**: focus and use Arrow keys / PageUp / PageDown / Home / End — native
|
|
158
173
|
`input[type=range]` behavior (the real input is visually hidden but focusable).
|
|
174
|
+
- **Thumb reserve**: with a thumb rendered (traveling or pinned) the value maps onto the
|
|
175
|
+
thumb-center travel — the outer half-thumb at each end of the track resolves to
|
|
176
|
+
`min`/`max`, and the fill never shrinks below the thumb. `thumb={false}` or
|
|
177
|
+
`thumbReserve={false}` maps linearly across the whole track instead.
|
|
159
178
|
- **Vertical**: bottom is `min`, top is `max`; ArrowUp increases.
|
|
160
179
|
- **RTL**: horizontal sliders flip automatically (logical CSS properties + pointer math).
|
|
161
180
|
- **Commit semantics**: `oninput` fires only on actual value changes; `onchange` only
|
|
@@ -89,6 +89,16 @@
|
|
|
89
89
|
* the true iOS volume look with a fixed icon.
|
|
90
90
|
*/
|
|
91
91
|
thumbPosition?: SliderThumbPosition;
|
|
92
|
+
/**
|
|
93
|
+
* Reserve the thumb's footprint at both ends of the track: the value maps
|
|
94
|
+
* onto the thumb-center travel, so the fill can never shrink below the
|
|
95
|
+
* thumb (at `min` it is exactly a thumb-sized nub). `false` maps the value
|
|
96
|
+
* linearly across the whole track instead, letting the fill collapse to
|
|
97
|
+
* zero — only honored with `thumbPosition="start"` (a traveling thumb's
|
|
98
|
+
* position is defined against the reserved travel) and irrelevant without
|
|
99
|
+
* a thumb (`thumb={false}` never reserves).
|
|
100
|
+
*/
|
|
101
|
+
thumbReserve?: boolean;
|
|
92
102
|
/**
|
|
93
103
|
* Round the fill's leading edge (instead of a flat cut), giving the
|
|
94
104
|
* "pill inside a pill" look.
|
|
@@ -141,6 +151,7 @@
|
|
|
141
151
|
valueClass,
|
|
142
152
|
thumb = true,
|
|
143
153
|
thumbPosition = "value",
|
|
154
|
+
thumbReserve = true,
|
|
144
155
|
fillRounded = false,
|
|
145
156
|
ticks,
|
|
146
157
|
valueLabel,
|
|
@@ -165,9 +176,18 @@
|
|
|
165
176
|
// Value at drag start — commit (change) fires only if the drag changed it.
|
|
166
177
|
let _dragStartValue = 0;
|
|
167
178
|
|
|
168
|
-
//
|
|
169
|
-
//
|
|
179
|
+
// Two related but distinct concepts:
|
|
180
|
+
// - _thumbTravels: the thumb rides the fill edge (drives only its CSS position
|
|
181
|
+
// and the grab-offset detection).
|
|
182
|
+
// - _thumbReserved: the thumb's footprint is reserved at both ends, so the
|
|
183
|
+
// value maps onto the thumb-center travel and the fill/ticks/value label
|
|
184
|
+
// follow that same travel. This is what keeps the fill from ever shrinking
|
|
185
|
+
// below the thumb (at min it is exactly a thumb-sized nub).
|
|
186
|
+
// A traveling thumb always reserves (its position is defined against that
|
|
187
|
+
// travel); a pinned one reserves by default but can opt out via
|
|
188
|
+
// `thumbReserve={false}`, and no thumb never reserves.
|
|
170
189
|
let _thumbTravels = $derived(thumb !== false && thumbPosition === "value");
|
|
190
|
+
let _thumbReserved = $derived(thumb !== false && (_thumbTravels || thumbReserve));
|
|
171
191
|
|
|
172
192
|
// Bounds must be finite before anything else: a NaN bound would make every
|
|
173
193
|
// normalization write NaN, and NaN !== NaN would re-trigger the effect below
|
|
@@ -258,12 +278,12 @@
|
|
|
258
278
|
const rtl = horizontal && getComputedStyle(el!).direction === "rtl";
|
|
259
279
|
const trackLen = horizontal ? rect.width : rect.height;
|
|
260
280
|
const thickness = horizontal ? rect.height : rect.width;
|
|
261
|
-
// With a thumb
|
|
262
|
-
// (mirrors the CSS `(100% - thickness) * ratio` positioning); without
|
|
263
|
-
//
|
|
264
|
-
// cross-axis size equals --_thickness — size the cross-axis via the
|
|
265
|
-
// presets or --stuic-slider-thickness, not via utility classes (see README).
|
|
266
|
-
const pad =
|
|
281
|
+
// With a reserved thumb the value maps onto [thickness/2, len - thickness/2]
|
|
282
|
+
// (mirrors the CSS `(100% - thickness) * ratio` sizing/positioning); without
|
|
283
|
+
// the reserve it maps linearly across the whole track. NOTE: this assumes the
|
|
284
|
+
// rendered cross-axis size equals --_thickness — size the cross-axis via the
|
|
285
|
+
// size presets or --stuic-slider-thickness, not via utility classes (see README).
|
|
286
|
+
const pad = _thumbReserved ? thickness / 2 : 0;
|
|
267
287
|
const travel = Math.max(1, trackLen - 2 * pad);
|
|
268
288
|
return { horizontal, rtl, rect, trackLen, pad, travel };
|
|
269
289
|
}
|
|
@@ -426,6 +446,7 @@
|
|
|
426
446
|
data-thumb={thumb !== false ? "true" : "false"}
|
|
427
447
|
data-thumb-position={thumb !== false ? thumbPosition : undefined}
|
|
428
448
|
data-thumb-travels={_thumbTravels ? "true" : "false"}
|
|
449
|
+
data-thumb-reserved={_thumbReserved ? "true" : "false"}
|
|
429
450
|
data-fill-rounded={fillRounded ? "true" : undefined}
|
|
430
451
|
data-size={!unstyled ? size : undefined}
|
|
431
452
|
data-intent={!unstyled ? intent : undefined}
|
|
@@ -69,6 +69,16 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children" |
|
|
|
69
69
|
* the true iOS volume look with a fixed icon.
|
|
70
70
|
*/
|
|
71
71
|
thumbPosition?: SliderThumbPosition;
|
|
72
|
+
/**
|
|
73
|
+
* Reserve the thumb's footprint at both ends of the track: the value maps
|
|
74
|
+
* onto the thumb-center travel, so the fill can never shrink below the
|
|
75
|
+
* thumb (at `min` it is exactly a thumb-sized nub). `false` maps the value
|
|
76
|
+
* linearly across the whole track instead, letting the fill collapse to
|
|
77
|
+
* zero — only honored with `thumbPosition="start"` (a traveling thumb's
|
|
78
|
+
* position is defined against the reserved travel) and irrelevant without
|
|
79
|
+
* a thumb (`thumb={false}` never reserves).
|
|
80
|
+
*/
|
|
81
|
+
thumbReserve?: boolean;
|
|
72
82
|
/**
|
|
73
83
|
* Round the fill's leading edge (instead of a flat cut), giving the
|
|
74
84
|
* "pill inside a pill" look.
|
|
@@ -116,8 +116,12 @@
|
|
|
116
116
|
position: absolute;
|
|
117
117
|
background: var(--_fill);
|
|
118
118
|
}
|
|
119
|
-
/* With a
|
|
120
|
-
|
|
119
|
+
/* With a reserved thumb (traveling OR pinned), the fill spans the thumb-center
|
|
120
|
+
travel plus the thumb itself: it reaches the far edge of a traveling thumb and,
|
|
121
|
+
at ratio 0, is exactly one thickness — a thumb-sized nub that the fill can
|
|
122
|
+
never shrink below. `thumbReserve={false}` (and no thumb) falls back to the
|
|
123
|
+
plain linear `ratio * 100%` above, which collapses to zero at min. */
|
|
124
|
+
.stuic-slider[data-thumb-reserved="true"] {
|
|
121
125
|
--_fill-len: calc(var(--_ratio) * (100% - var(--_thickness)) + var(--_thickness));
|
|
122
126
|
}
|
|
123
127
|
.stuic-slider[data-orientation="horizontal"] .fill {
|
|
@@ -164,7 +168,9 @@
|
|
|
164
168
|
bottom: var(--_thumb-inset);
|
|
165
169
|
}
|
|
166
170
|
/* thumbPosition="value": ride the fill edge (default). thumbPosition="start"
|
|
167
|
-
keeps the offsets above, so only the bar moves (iOS volume look)
|
|
171
|
+
keeps the offsets above, so only the bar moves (iOS volume look) — note this
|
|
172
|
+
is data-thumb-travels, NOT data-thumb-reserved: a pinned thumb reserves the
|
|
173
|
+
travel (for the fill/ticks/value label) without moving itself. */
|
|
168
174
|
.stuic-slider[data-thumb-travels="true"][data-orientation="horizontal"] > .thumb {
|
|
169
175
|
inset-inline-start: calc(
|
|
170
176
|
var(--_thumb-inset) + (100% - var(--_thickness)) * var(--_ratio)
|
|
@@ -234,7 +240,7 @@
|
|
|
234
240
|
border-radius: 9999px;
|
|
235
241
|
background: var(--_tick);
|
|
236
242
|
}
|
|
237
|
-
/* Without a
|
|
243
|
+
/* Without a reserved thumb there is no thickness inset to hide behind, so the
|
|
238
244
|
travel is inset by half a tick — otherwise the ticks at ratio 0 and 1 sit
|
|
239
245
|
centered on the track edges and the overflow:hidden track halves them. */
|
|
240
246
|
.stuic-slider[data-orientation="horizontal"] .tick {
|
|
@@ -249,13 +255,13 @@
|
|
|
249
255
|
bottom: calc(var(--_tick-size) / 2 + (100% - var(--_tick-size)) * var(--_tick-ratio));
|
|
250
256
|
transform: translate(-50%, 50%);
|
|
251
257
|
}
|
|
252
|
-
/* With a
|
|
253
|
-
.stuic-slider[data-thumb-
|
|
258
|
+
/* With a reserved thumb, ticks align to the thumb-center travel */
|
|
259
|
+
.stuic-slider[data-thumb-reserved="true"][data-orientation="horizontal"] .tick {
|
|
254
260
|
inset-inline-start: calc(
|
|
255
261
|
var(--_thickness) / 2 + (100% - var(--_thickness)) * var(--_tick-ratio)
|
|
256
262
|
);
|
|
257
263
|
}
|
|
258
|
-
.stuic-slider[data-thumb-
|
|
264
|
+
.stuic-slider[data-thumb-reserved="true"][data-orientation="vertical"] .tick {
|
|
259
265
|
bottom: calc(var(--_thickness) / 2 + (100% - var(--_thickness)) * var(--_tick-ratio));
|
|
260
266
|
}
|
|
261
267
|
/* Logical-property positioning + physical translate: flip centering in RTL */
|
|
@@ -284,12 +290,12 @@
|
|
|
284
290
|
bottom: calc(var(--_ratio) * 100%);
|
|
285
291
|
transform: translateY(50%);
|
|
286
292
|
}
|
|
287
|
-
.stuic-slider[data-thumb-
|
|
293
|
+
.stuic-slider[data-thumb-reserved="true"][data-orientation="horizontal"] > .value {
|
|
288
294
|
inset-inline-start: calc(
|
|
289
295
|
var(--_thickness) / 2 + (100% - var(--_thickness)) * var(--_ratio)
|
|
290
296
|
);
|
|
291
297
|
}
|
|
292
|
-
.stuic-slider[data-thumb-
|
|
298
|
+
.stuic-slider[data-thumb-reserved="true"][data-orientation="vertical"] > .value {
|
|
293
299
|
bottom: calc(var(--_thickness) / 2 + (100% - var(--_thickness)) * var(--_ratio));
|
|
294
300
|
}
|
|
295
301
|
.stuic-slider[data-orientation="horizontal"]:dir(rtl) > .value {
|