@marianmeres/stuic 3.144.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.
- package/AGENTS.md +2 -2
- package/API.md +1 -0
- package/README.md +1 -1
- package/dist/components/Slider/README.md +205 -0
- package/dist/components/Slider/Slider.svelte +521 -0
- package/dist/components/Slider/Slider.svelte.d.ts +104 -0
- package/dist/components/Slider/index.css +337 -0
- package/dist/components/Slider/index.d.ts +1 -0
- package/dist/components/Slider/index.js +1 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/docs/domains/components.md +2 -1
- package/package.json +4 -3
package/AGENTS.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
```
|
|
25
25
|
src/lib/
|
|
26
|
-
├── components/ #
|
|
26
|
+
├── components/ # 63 component directories
|
|
27
27
|
├── actions/ # 15 Svelte actions (use: directives)
|
|
28
28
|
├── attachments/ # Svelte attachments ({@attach} — preferred for new DOM helpers)
|
|
29
29
|
├── utils/ # 44 utility modules
|
|
@@ -128,7 +128,7 @@ Global tokens that control cross-component visual properties. Defined in `src/li
|
|
|
128
128
|
|
|
129
129
|
### Domain Docs
|
|
130
130
|
|
|
131
|
-
- [Components](./docs/domains/components.md) —
|
|
131
|
+
- [Components](./docs/domains/components.md) — 63 component directories, Props pattern, snippets
|
|
132
132
|
- [Theming](./docs/domains/theming.md) — CSS tokens, dark mode, themes
|
|
133
133
|
- [Actions](./docs/domains/actions.md) — 15 Svelte directives
|
|
134
134
|
- [Attachments](./docs/domains/attachments.md) — `{@attach}` DOM helpers (preferred for new ones)
|
package/API.md
CHANGED
|
@@ -2178,6 +2178,7 @@ Each component defines customization tokens. Override globally in `:root {}` or
|
|
|
2178
2178
|
| ------------------ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2179
2179
|
| Button | `--stuic-button-*` | `bg`, `text`, `border`, `ring-color`, `radius`, `padding-x-{size}` |
|
|
2180
2180
|
| Switch | `--stuic-switch-*` | `accent` |
|
|
2181
|
+
| Slider | `--stuic-slider-*` | `track`, `fill`, `thumb`, `tick`, `tick-on-fill`, `thickness`, `length`, `radius`, `fill-radius` |
|
|
2181
2182
|
| Input | `--stuic-input-*` | `accent`, `accent-error` |
|
|
2182
2183
|
| Progress | `--stuic-progress-*` | `bg`, `accent` |
|
|
2183
2184
|
| ListItemButton | `--stuic-list-item-button-*` | `bg`, `text`, `border`, `bg-hover`, `text-hover` |
|
package/README.md
CHANGED
|
@@ -175,7 +175,7 @@ FieldInput, FieldMoney, FieldTextarea, FieldSelect, FieldCheckbox, FieldRadios,
|
|
|
175
175
|
|
|
176
176
|
### Buttons & Controls
|
|
177
177
|
|
|
178
|
-
Button, ButtonGroupRadio, Switch, TwCheck, ListItemButton, X
|
|
178
|
+
Button, ButtonGroupRadio, Switch, Slider, TwCheck, ListItemButton, X
|
|
179
179
|
|
|
180
180
|
### Feedback & Notifications
|
|
181
181
|
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Slider
|
|
2
|
+
|
|
3
|
+
A fancy `input[type="range"]` wrap — a pill-shaped track that fills with the value, with
|
|
4
|
+
an optional icon-capable thumb riding the fill edge (think iOS volume control). Supports
|
|
5
|
+
horizontal and vertical orientation, pointer dragging with step snapping, native keyboard
|
|
6
|
+
interaction, tick marks, a floating value label, form participation, and validation.
|
|
7
|
+
|
|
8
|
+
Not a replacement for `FieldInput type="range"` — this is the special-case "fancy"
|
|
9
|
+
variant for custom UI (volume/brightness controls, dashboards, media players).
|
|
10
|
+
|
|
11
|
+
## Props
|
|
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
|
+
| `fillRounded` | `boolean` | `false` | Round the fill's leading edge ("pill inside a pill") |
|
|
25
|
+
| `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 |
|
|
26
|
+
| `valueLabel` | `Snippet<[SliderRenderCtx]>` | - | Floating label at the current value along the track |
|
|
27
|
+
| `disabled` | `boolean` | `false` | Disable interaction |
|
|
28
|
+
| `label` | `string` | - | Screen reader label for the underlying input |
|
|
29
|
+
| `id` | `string` | - | Id for the underlying input (enables `<label for>` association) |
|
|
30
|
+
| `name` | `string` | - | Form field name for the hidden range input |
|
|
31
|
+
| `required` | `boolean` | `false` | Forwarded to the input; per HTML spec inert on range inputs (use `validate.customValidator` for custom rules) |
|
|
32
|
+
| `oninput` | `(value: number) => void` | - | Fires on every value change (drag, keyboard) |
|
|
33
|
+
| `onchange` | `(value: number) => void` | - | Fires when a change is committed (drag release, keyboard) |
|
|
34
|
+
| `validate` | `boolean \| ValidateOptions` | - | Enable validation (stuic validate action) |
|
|
35
|
+
| `unstyled` | `boolean` | `false` | Skip all default styling |
|
|
36
|
+
| `class` | `string` | - | Classes for the root element |
|
|
37
|
+
| `trackClass` | `string` | - | Classes for the track (pill background) |
|
|
38
|
+
| `fillClass` | `string` | - | Classes for the fill (value indicator) |
|
|
39
|
+
| `thumbClass` | `string` | - | Classes for the thumb |
|
|
40
|
+
| `tickClass` | `string` | - | Classes for each tick mark |
|
|
41
|
+
| `valueClass` | `string` | - | Classes for the value label wrapper |
|
|
42
|
+
| `el` | `HTMLDivElement` | - | Root element reference (bindable) |
|
|
43
|
+
| `inputEl` | `HTMLInputElement` | - | Hidden range input reference (bindable) |
|
|
44
|
+
|
|
45
|
+
`SliderRenderCtx` (passed to the `thumb` and `valueLabel` snippets):
|
|
46
|
+
`{ value: number; ratio: number /* 0..1 */; percent: number /* 0..100 */; dragging: boolean }`
|
|
47
|
+
|
|
48
|
+
Remaining props are spread onto the root `<div>`.
|
|
49
|
+
|
|
50
|
+
### Exported methods (via component instance binding)
|
|
51
|
+
|
|
52
|
+
| Method | Description |
|
|
53
|
+
| ----------------------- | ----------------------------------- |
|
|
54
|
+
| `validate()` | Trigger validation now |
|
|
55
|
+
| `clearValidation()` | Clear the current validation result |
|
|
56
|
+
| `getValidation()` | Read the current validation result |
|
|
57
|
+
| `focus()` | Focus the underlying range input |
|
|
58
|
+
| `scrollIntoView(opts?)` | Scroll the slider into view |
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
### Basic
|
|
63
|
+
|
|
64
|
+
```svelte
|
|
65
|
+
<script lang="ts">
|
|
66
|
+
import { Slider } from "@marianmeres/stuic";
|
|
67
|
+
|
|
68
|
+
let volume = $state(30);
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<Slider bind:value={volume} />
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Vertical volume control (thumb rides the fill edge)
|
|
75
|
+
|
|
76
|
+
```svelte
|
|
77
|
+
<Slider
|
|
78
|
+
orientation="vertical"
|
|
79
|
+
size="lg"
|
|
80
|
+
bind:value={volume}
|
|
81
|
+
style="--stuic-slider-length: 10rem; --stuic-slider-fill: white; --stuic-slider-track: rgb(255 255 255 / .25);"
|
|
82
|
+
>
|
|
83
|
+
{#snippet thumb()}
|
|
84
|
+
{@html iconVolume({ size: 16 })}
|
|
85
|
+
{/snippet}
|
|
86
|
+
</Slider>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Fill-only (no thumb)
|
|
90
|
+
|
|
91
|
+
```svelte
|
|
92
|
+
<Slider thumb={false} bind:value={brightness} />
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### True iOS volume: fixed icon at the start, only the bar moves
|
|
96
|
+
|
|
97
|
+
```svelte
|
|
98
|
+
<!-- bare icon (no knob): drop the thumb's own background/shadow -->
|
|
99
|
+
<Slider
|
|
100
|
+
orientation="vertical"
|
|
101
|
+
thumbPosition="start"
|
|
102
|
+
fillRounded
|
|
103
|
+
thumbClass="bg-transparent shadow-none"
|
|
104
|
+
bind:value={volume}
|
|
105
|
+
>
|
|
106
|
+
{#snippet thumb()}
|
|
107
|
+
{@html iconVolume({ size: 18 })}
|
|
108
|
+
{/snippet}
|
|
109
|
+
</Slider>
|
|
110
|
+
|
|
111
|
+
<!-- or keep the white knob pinned at the bottom -->
|
|
112
|
+
<Slider orientation="vertical" thumbPosition="start" bind:value={volume}>
|
|
113
|
+
{#snippet thumb()}
|
|
114
|
+
{@html iconVolume({ size: 16 })}
|
|
115
|
+
{/snippet}
|
|
116
|
+
</Slider>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
With `thumbPosition="start"` the thumb is decorative (`pointer-events: none`) and the
|
|
120
|
+
value maps linearly across the full track — there is no thumb-travel inset.
|
|
121
|
+
|
|
122
|
+
### Steps and ticks
|
|
123
|
+
|
|
124
|
+
```svelte
|
|
125
|
+
<Slider min={0} max={10} step={1} ticks bind:value={rating} />
|
|
126
|
+
<Slider min={0} max={100} step={0.5} ticks={[0, 25, 50, 75, 100]} />
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Value label
|
|
130
|
+
|
|
131
|
+
```svelte
|
|
132
|
+
<Slider bind:value={percent}>
|
|
133
|
+
{#snippet valueLabel({ value })}
|
|
134
|
+
{value}%
|
|
135
|
+
{/snippet}
|
|
136
|
+
</Slider>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Continuous (no snapping)
|
|
140
|
+
|
|
141
|
+
```svelte
|
|
142
|
+
<Slider step="any" bind:value={gain} />
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### In a form
|
|
146
|
+
|
|
147
|
+
```svelte
|
|
148
|
+
<form onsubmit={...}>
|
|
149
|
+
<Slider name="volume" bind:value={volume} />
|
|
150
|
+
</form>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Interaction
|
|
154
|
+
|
|
155
|
+
- **Pointer**: press anywhere on the track to jump the value there, then drag. Grabbing
|
|
156
|
+
the thumb itself does not jump (drag continues from the grab point).
|
|
157
|
+
- **Keyboard**: focus and use Arrow keys / PageUp / PageDown / Home / End — native
|
|
158
|
+
`input[type=range]` behavior (the real input is visually hidden but focusable).
|
|
159
|
+
- **Vertical**: bottom is `min`, top is `max`; ArrowUp increases.
|
|
160
|
+
- **RTL**: horizontal sliders flip automatically (logical CSS properties + pointer math).
|
|
161
|
+
- **Commit semantics**: `oninput` fires only on actual value changes; `onchange` only
|
|
162
|
+
when a drag/keypress committed a _different_ value (native-faithful — a no-move tap
|
|
163
|
+
fires neither).
|
|
164
|
+
- **Off-grid max**: when `max` is not on the step grid (e.g. `min=0 max=95 step=10`),
|
|
165
|
+
the largest reachable value is the last grid point (`90`), matching native range
|
|
166
|
+
sanitization.
|
|
167
|
+
|
|
168
|
+
## Caveats
|
|
169
|
+
|
|
170
|
+
- **Cross-axis sizing**: size the thickness via `size` presets or
|
|
171
|
+
`--stuic-slider-thickness` — not via `h-*`/`w-*` utility classes. The pointer math
|
|
172
|
+
and the CSS thumb positioning both derive from the thickness; a utility class
|
|
173
|
+
resizes the box without updating `--_thickness`, misaligning them. (Main-axis
|
|
174
|
+
length via a class — e.g. `class="h-40"` on a vertical slider — is fine.)
|
|
175
|
+
- **Root pointer handlers are reserved**: `onpointerdown/move/up/cancel` are excluded
|
|
176
|
+
from `Props` (the drag machinery owns them). Wrap the slider if you need them.
|
|
177
|
+
- **Touch**: the slider claims the whole touch gesture (`touch-action: none`) —
|
|
178
|
+
a touch starting on it adjusts the value and never scrolls the page.
|
|
179
|
+
- **The wrapper is not the control**: props spread onto the root `<div>` — including
|
|
180
|
+
`aria-*` and `onfocus`/`onblur` — never reach the underlying `<input type="range">`
|
|
181
|
+
and are inert for assistive tech. Use `label` (→ `aria-label`) and `id` (→ `<label for>`);
|
|
182
|
+
for anything else, bind `inputEl` and wire it imperatively.
|
|
183
|
+
|
|
184
|
+
## CSS Variables
|
|
185
|
+
|
|
186
|
+
| Variable | Default | Description |
|
|
187
|
+
| --------------------------------- | -------------------------- | --------------------------------- |
|
|
188
|
+
| `--stuic-slider-track` | `--stuic-color-muted` | Track (pill background) color |
|
|
189
|
+
| `--stuic-slider-fill` | `--stuic-color-primary` | Fill color |
|
|
190
|
+
| `--stuic-slider-thumb` | `--color-white` | Thumb background |
|
|
191
|
+
| `--stuic-slider-thumb-foreground` | `--stuic-color-foreground` | Thumb content color |
|
|
192
|
+
| `--stuic-slider-tick` | foreground 25% mix | Tick mark color (over the track) |
|
|
193
|
+
| `--stuic-slider-tick-on-fill` | background 55% mix | Tick mark color (over the fill) |
|
|
194
|
+
| `--stuic-slider-ring-width` | `4px` | Focus ring width |
|
|
195
|
+
| `--stuic-slider-ring-color` | `--stuic-color-ring` | Focus ring color |
|
|
196
|
+
| `--stuic-slider-thickness` | `2rem` (`sm` 1.25, `lg` 3) | Cross-axis size |
|
|
197
|
+
| `--stuic-slider-length` | `10rem` | Main-axis size |
|
|
198
|
+
| `--stuic-slider-thumb-inset` | `3px` | Gap between thumb and track edge |
|
|
199
|
+
| `--stuic-slider-radius` | `9999px` | Track corner radius |
|
|
200
|
+
| `--stuic-slider-fill-radius` | `--stuic-slider-radius` | Fill radius (when `fillRounded`) |
|
|
201
|
+
| `--stuic-slider-thumb-radius` | `9999px` | Thumb corner radius |
|
|
202
|
+
| `--stuic-slider-thumb-shadow` | `--stuic-shadow` | Thumb shadow |
|
|
203
|
+
| `--stuic-slider-tick-size` | `4px` | Tick mark diameter |
|
|
204
|
+
| `--stuic-slider-value-gap` | `0.375rem` | Gap between track and value label |
|
|
205
|
+
| `--stuic-slider-transition` | `--stuic-transition` | Fill/thumb movement transition |
|
|
@@ -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;
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
SLIDER COMPONENT TOKENS
|
|
3
|
+
Override globally: :root { --stuic-slider-fill: var(--color-green-500); }
|
|
4
|
+
Override locally: <Slider style="--stuic-slider-fill: var(--color-green-500);">
|
|
5
|
+
|
|
6
|
+
Sizing (structural, override at any scope — resolved at the element):
|
|
7
|
+
--stuic-slider-thickness cross-axis size (also set by size="sm|md|lg")
|
|
8
|
+
--stuic-slider-length main-axis size (or use a width/height class)
|
|
9
|
+
--stuic-slider-thumb-inset gap between thumb and track edge
|
|
10
|
+
============================================================================ */
|
|
11
|
+
|
|
12
|
+
:root {
|
|
13
|
+
/* Track (pill background) */
|
|
14
|
+
--stuic-slider-track: var(--stuic-color-muted);
|
|
15
|
+
|
|
16
|
+
/* Fill (value indicator) */
|
|
17
|
+
--stuic-slider-fill: var(--stuic-color-primary);
|
|
18
|
+
|
|
19
|
+
/* Thumb */
|
|
20
|
+
--stuic-slider-thumb: var(--color-white);
|
|
21
|
+
--stuic-slider-thumb-foreground: var(--stuic-color-foreground);
|
|
22
|
+
|
|
23
|
+
/* Tick marks (a second, fill-clipped layer keeps them readable over the fill) */
|
|
24
|
+
--stuic-slider-tick: color-mix(in srgb, var(--stuic-color-foreground) 25%, transparent);
|
|
25
|
+
--stuic-slider-tick-on-fill: color-mix(
|
|
26
|
+
in srgb,
|
|
27
|
+
var(--stuic-color-background) 55%,
|
|
28
|
+
transparent
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/* Focus ring */
|
|
32
|
+
--stuic-slider-ring-width: 4px;
|
|
33
|
+
--stuic-slider-ring-color: var(--stuic-color-ring);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@layer components {
|
|
37
|
+
/* ============================================================================
|
|
38
|
+
BASE / ROOT
|
|
39
|
+
============================================================================ */
|
|
40
|
+
|
|
41
|
+
.stuic-slider {
|
|
42
|
+
/* Internal vars (intent may override the fill) */
|
|
43
|
+
--_track: var(--stuic-slider-track);
|
|
44
|
+
--_fill: var(--stuic-slider-fill);
|
|
45
|
+
--_thumb-bg: var(--stuic-slider-thumb);
|
|
46
|
+
--_thumb-fg: var(--stuic-slider-thumb-foreground);
|
|
47
|
+
--_tick: var(--stuic-slider-tick);
|
|
48
|
+
--_tick-on-fill: var(--stuic-slider-tick-on-fill);
|
|
49
|
+
--_tick-size: var(--stuic-slider-tick-size, 4px);
|
|
50
|
+
|
|
51
|
+
/* Structural (fallbacks resolved here so scoped overrides work) */
|
|
52
|
+
--_thickness: var(--stuic-slider-thickness, 2rem);
|
|
53
|
+
--_length: var(--stuic-slider-length, 10rem);
|
|
54
|
+
--_thumb-inset: var(--stuic-slider-thumb-inset, 3px);
|
|
55
|
+
/* Main-axis extent of the fill (percentages resolve against the track,
|
|
56
|
+
which is the containing block of both the fill and the tick layers) */
|
|
57
|
+
--_fill-len: calc(var(--_ratio) * 100%);
|
|
58
|
+
|
|
59
|
+
display: inline-block;
|
|
60
|
+
vertical-align: middle;
|
|
61
|
+
flex-shrink: 0;
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* FUNCTIONAL, not cosmetic — keyed on an attribute the component always emits
|
|
66
|
+
so it survives `unstyled`: `touch-action: none` keeps the browser from
|
|
67
|
+
stealing touch drags for scrolling, `position: relative` anchors the
|
|
68
|
+
absolutely-positioned track/thumb/hidden input. Attribute specificity (0,1,0)
|
|
69
|
+
leaves it overridable by a consumer utility class (Tailwind's utilities layer
|
|
70
|
+
beats this components layer). */
|
|
71
|
+
[data-stuic-slider] {
|
|
72
|
+
position: relative;
|
|
73
|
+
touch-action: none;
|
|
74
|
+
user-select: none;
|
|
75
|
+
-webkit-user-select: none;
|
|
76
|
+
-webkit-tap-highlight-color: transparent;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.stuic-slider[data-size="sm"] {
|
|
80
|
+
--_thickness: var(--stuic-slider-thickness, 1.25rem);
|
|
81
|
+
}
|
|
82
|
+
/* md is the base default (2rem) */
|
|
83
|
+
.stuic-slider[data-size="lg"] {
|
|
84
|
+
--_thickness: var(--stuic-slider-thickness, 3rem);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.stuic-slider[data-orientation="horizontal"] {
|
|
88
|
+
width: var(--_length);
|
|
89
|
+
height: var(--_thickness);
|
|
90
|
+
}
|
|
91
|
+
.stuic-slider[data-orientation="vertical"] {
|
|
92
|
+
width: var(--_thickness);
|
|
93
|
+
height: var(--_length);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Disabled */
|
|
97
|
+
.stuic-slider[data-disabled="true"] {
|
|
98
|
+
cursor: not-allowed;
|
|
99
|
+
opacity: 0.6;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ============================================================================
|
|
103
|
+
TRACK + FILL
|
|
104
|
+
============================================================================ */
|
|
105
|
+
|
|
106
|
+
.stuic-slider > .track {
|
|
107
|
+
position: absolute;
|
|
108
|
+
inset: 0;
|
|
109
|
+
overflow: hidden;
|
|
110
|
+
border-radius: var(--stuic-slider-radius, 9999px);
|
|
111
|
+
background: var(--_track);
|
|
112
|
+
transition: box-shadow var(--stuic-slider-transition, var(--stuic-transition));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.stuic-slider .fill {
|
|
116
|
+
position: absolute;
|
|
117
|
+
background: var(--_fill);
|
|
118
|
+
}
|
|
119
|
+
/* With a value-tracking thumb, the fill reaches the far edge of the thumb */
|
|
120
|
+
.stuic-slider[data-thumb-travels="true"] {
|
|
121
|
+
--_fill-len: calc(var(--_ratio) * (100% - var(--_thickness)) + var(--_thickness));
|
|
122
|
+
}
|
|
123
|
+
.stuic-slider[data-orientation="horizontal"] .fill {
|
|
124
|
+
inset-block: 0;
|
|
125
|
+
inset-inline-start: 0;
|
|
126
|
+
width: var(--_fill-len);
|
|
127
|
+
}
|
|
128
|
+
.stuic-slider[data-orientation="vertical"] .fill {
|
|
129
|
+
inset-inline: 0;
|
|
130
|
+
bottom: 0;
|
|
131
|
+
height: var(--_fill-len);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Rounded leading edge ("pill inside a pill") */
|
|
135
|
+
.stuic-slider[data-fill-rounded="true"] .fill {
|
|
136
|
+
border-radius: var(--stuic-slider-fill-radius, var(--stuic-slider-radius, 9999px));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* ============================================================================
|
|
140
|
+
THUMB
|
|
141
|
+
============================================================================ */
|
|
142
|
+
|
|
143
|
+
.stuic-slider > .thumb {
|
|
144
|
+
position: absolute;
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
148
|
+
width: calc(var(--_thickness) - 2 * var(--_thumb-inset));
|
|
149
|
+
height: calc(var(--_thickness) - 2 * var(--_thumb-inset));
|
|
150
|
+
border-radius: var(--stuic-slider-thumb-radius, 9999px);
|
|
151
|
+
background: var(--_thumb-bg);
|
|
152
|
+
color: var(--_thumb-fg);
|
|
153
|
+
box-shadow: var(--stuic-slider-thumb-shadow, var(--stuic-shadow));
|
|
154
|
+
/* No pointer-events rules needed: the full-size hidden input is the last
|
|
155
|
+
child and always the topmost hit target, so the thumb never receives
|
|
156
|
+
pointer events. Grab detection is geometric, in _onpointerdown. */
|
|
157
|
+
}
|
|
158
|
+
.stuic-slider[data-orientation="horizontal"] > .thumb {
|
|
159
|
+
inset-block-start: var(--_thumb-inset);
|
|
160
|
+
inset-inline-start: var(--_thumb-inset);
|
|
161
|
+
}
|
|
162
|
+
.stuic-slider[data-orientation="vertical"] > .thumb {
|
|
163
|
+
inset-inline-start: var(--_thumb-inset);
|
|
164
|
+
bottom: var(--_thumb-inset);
|
|
165
|
+
}
|
|
166
|
+
/* thumbPosition="value": ride the fill edge (default). thumbPosition="start"
|
|
167
|
+
keeps the offsets above, so only the bar moves (iOS volume look). */
|
|
168
|
+
.stuic-slider[data-thumb-travels="true"][data-orientation="horizontal"] > .thumb {
|
|
169
|
+
inset-inline-start: calc(
|
|
170
|
+
var(--_thumb-inset) + (100% - var(--_thickness)) * var(--_ratio)
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
.stuic-slider[data-thumb-travels="true"][data-orientation="vertical"] > .thumb {
|
|
174
|
+
bottom: calc(var(--_thumb-inset) + (100% - var(--_thickness)) * var(--_ratio));
|
|
175
|
+
}
|
|
176
|
+
/* Smooth value movement (keyboard steps); instant while dragging */
|
|
177
|
+
.stuic-slider .fill,
|
|
178
|
+
.stuic-slider > .thumb,
|
|
179
|
+
.stuic-slider > .value {
|
|
180
|
+
transition:
|
|
181
|
+
width var(--stuic-slider-transition, var(--stuic-transition)),
|
|
182
|
+
height var(--stuic-slider-transition, var(--stuic-transition)),
|
|
183
|
+
inset-inline-start var(--stuic-slider-transition, var(--stuic-transition)),
|
|
184
|
+
bottom var(--stuic-slider-transition, var(--stuic-transition));
|
|
185
|
+
}
|
|
186
|
+
.stuic-slider[data-dragging="true"] .fill,
|
|
187
|
+
.stuic-slider[data-dragging="true"] > .thumb,
|
|
188
|
+
.stuic-slider[data-dragging="true"] > .value {
|
|
189
|
+
transition: none;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* ============================================================================
|
|
193
|
+
TICKS
|
|
194
|
+
============================================================================ */
|
|
195
|
+
|
|
196
|
+
/* Two identical tick layers clipped to complementary halves of the track: the
|
|
197
|
+
base one covers the unfilled part, the `on-fill` one the filled part, each
|
|
198
|
+
with its own color. A single layer would vanish into whichever it overlaps,
|
|
199
|
+
and overlapping layers would composite (both tick colors are translucent). */
|
|
200
|
+
.stuic-slider .ticks {
|
|
201
|
+
position: absolute;
|
|
202
|
+
inset: 0;
|
|
203
|
+
transition: clip-path var(--stuic-slider-transition, var(--stuic-transition));
|
|
204
|
+
}
|
|
205
|
+
.stuic-slider[data-dragging="true"] .ticks {
|
|
206
|
+
transition: none;
|
|
207
|
+
}
|
|
208
|
+
.stuic-slider .ticks[data-on-fill="true"] .tick {
|
|
209
|
+
background: var(--_tick-on-fill);
|
|
210
|
+
}
|
|
211
|
+
.stuic-slider[data-orientation="horizontal"] .ticks {
|
|
212
|
+
clip-path: inset(0 0 0 var(--_fill-len));
|
|
213
|
+
}
|
|
214
|
+
.stuic-slider[data-orientation="horizontal"] .ticks[data-on-fill="true"] {
|
|
215
|
+
clip-path: inset(0 calc(100% - var(--_fill-len)) 0 0);
|
|
216
|
+
}
|
|
217
|
+
.stuic-slider[data-orientation="horizontal"]:dir(rtl) .ticks {
|
|
218
|
+
clip-path: inset(0 var(--_fill-len) 0 0);
|
|
219
|
+
}
|
|
220
|
+
.stuic-slider[data-orientation="horizontal"]:dir(rtl) .ticks[data-on-fill="true"] {
|
|
221
|
+
clip-path: inset(0 0 0 calc(100% - var(--_fill-len)));
|
|
222
|
+
}
|
|
223
|
+
.stuic-slider[data-orientation="vertical"] .ticks {
|
|
224
|
+
clip-path: inset(0 0 var(--_fill-len) 0);
|
|
225
|
+
}
|
|
226
|
+
.stuic-slider[data-orientation="vertical"] .ticks[data-on-fill="true"] {
|
|
227
|
+
clip-path: inset(calc(100% - var(--_fill-len)) 0 0 0);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.stuic-slider .tick {
|
|
231
|
+
position: absolute;
|
|
232
|
+
width: var(--_tick-size);
|
|
233
|
+
height: var(--_tick-size);
|
|
234
|
+
border-radius: 9999px;
|
|
235
|
+
background: var(--_tick);
|
|
236
|
+
}
|
|
237
|
+
/* Without a traveling thumb there is no thickness inset to hide behind, so the
|
|
238
|
+
travel is inset by half a tick — otherwise the ticks at ratio 0 and 1 sit
|
|
239
|
+
centered on the track edges and the overflow:hidden track halves them. */
|
|
240
|
+
.stuic-slider[data-orientation="horizontal"] .tick {
|
|
241
|
+
top: 50%;
|
|
242
|
+
inset-inline-start: calc(
|
|
243
|
+
var(--_tick-size) / 2 + (100% - var(--_tick-size)) * var(--_tick-ratio)
|
|
244
|
+
);
|
|
245
|
+
transform: translate(-50%, -50%);
|
|
246
|
+
}
|
|
247
|
+
.stuic-slider[data-orientation="vertical"] .tick {
|
|
248
|
+
left: 50%;
|
|
249
|
+
bottom: calc(var(--_tick-size) / 2 + (100% - var(--_tick-size)) * var(--_tick-ratio));
|
|
250
|
+
transform: translate(-50%, 50%);
|
|
251
|
+
}
|
|
252
|
+
/* With a value-tracking thumb, ticks align to the thumb-center travel */
|
|
253
|
+
.stuic-slider[data-thumb-travels="true"][data-orientation="horizontal"] .tick {
|
|
254
|
+
inset-inline-start: calc(
|
|
255
|
+
var(--_thickness) / 2 + (100% - var(--_thickness)) * var(--_tick-ratio)
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
.stuic-slider[data-thumb-travels="true"][data-orientation="vertical"] .tick {
|
|
259
|
+
bottom: calc(var(--_thickness) / 2 + (100% - var(--_thickness)) * var(--_tick-ratio));
|
|
260
|
+
}
|
|
261
|
+
/* Logical-property positioning + physical translate: flip centering in RTL */
|
|
262
|
+
.stuic-slider[data-orientation="horizontal"]:dir(rtl) .tick {
|
|
263
|
+
transform: translate(50%, -50%);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* ============================================================================
|
|
267
|
+
VALUE LABEL
|
|
268
|
+
============================================================================ */
|
|
269
|
+
|
|
270
|
+
.stuic-slider > .value {
|
|
271
|
+
position: absolute;
|
|
272
|
+
pointer-events: none;
|
|
273
|
+
white-space: nowrap;
|
|
274
|
+
font-size: var(--text-xs);
|
|
275
|
+
color: var(--stuic-color-muted-foreground);
|
|
276
|
+
}
|
|
277
|
+
.stuic-slider[data-orientation="horizontal"] > .value {
|
|
278
|
+
bottom: calc(100% + var(--stuic-slider-value-gap, 0.375rem));
|
|
279
|
+
inset-inline-start: calc(var(--_ratio) * 100%);
|
|
280
|
+
transform: translateX(-50%);
|
|
281
|
+
}
|
|
282
|
+
.stuic-slider[data-orientation="vertical"] > .value {
|
|
283
|
+
inset-inline-start: calc(100% + var(--stuic-slider-value-gap, 0.375rem));
|
|
284
|
+
bottom: calc(var(--_ratio) * 100%);
|
|
285
|
+
transform: translateY(50%);
|
|
286
|
+
}
|
|
287
|
+
.stuic-slider[data-thumb-travels="true"][data-orientation="horizontal"] > .value {
|
|
288
|
+
inset-inline-start: calc(
|
|
289
|
+
var(--_thickness) / 2 + (100% - var(--_thickness)) * var(--_ratio)
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
.stuic-slider[data-thumb-travels="true"][data-orientation="vertical"] > .value {
|
|
293
|
+
bottom: calc(var(--_thickness) / 2 + (100% - var(--_thickness)) * var(--_ratio));
|
|
294
|
+
}
|
|
295
|
+
.stuic-slider[data-orientation="horizontal"]:dir(rtl) > .value {
|
|
296
|
+
transform: translateX(50%);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* The full-size hidden input is the topmost hit target */
|
|
300
|
+
.stuic-slider > input {
|
|
301
|
+
cursor: inherit;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/* ============================================================================
|
|
305
|
+
FOCUS RING (keyboard-initiated focus; data-ring is set by the component —
|
|
306
|
+
:focus-visible is unreliable for pointer-initiated programmatic focus)
|
|
307
|
+
============================================================================ */
|
|
308
|
+
|
|
309
|
+
.stuic-slider[data-ring="true"] > .track {
|
|
310
|
+
box-shadow: 0 0 0 var(--stuic-slider-ring-width)
|
|
311
|
+
color-mix(
|
|
312
|
+
in srgb,
|
|
313
|
+
var(--stuic-slider-ring-color) 30%,
|
|
314
|
+
var(--stuic-color-background)
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* ============================================================================
|
|
319
|
+
INTENT COLOR MAPPING
|
|
320
|
+
============================================================================ */
|
|
321
|
+
|
|
322
|
+
.stuic-slider[data-intent="primary"] {
|
|
323
|
+
--_fill: var(--stuic-color-primary);
|
|
324
|
+
}
|
|
325
|
+
.stuic-slider[data-intent="accent"] {
|
|
326
|
+
--_fill: var(--stuic-color-accent);
|
|
327
|
+
}
|
|
328
|
+
.stuic-slider[data-intent="success"] {
|
|
329
|
+
--_fill: var(--stuic-color-success);
|
|
330
|
+
}
|
|
331
|
+
.stuic-slider[data-intent="warning"] {
|
|
332
|
+
--_fill: var(--stuic-color-warning);
|
|
333
|
+
}
|
|
334
|
+
.stuic-slider[data-intent="destructive"] {
|
|
335
|
+
--_fill: var(--stuic-color-destructive);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Slider, type Props as SliderProps, type SliderIntent, type SliderOrientation, type SliderThumbPosition, type SliderRenderCtx, } from "./Slider.svelte";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Slider, } from "./Slider.svelte";
|
package/dist/index.css
CHANGED
|
@@ -98,6 +98,7 @@ In practice:
|
|
|
98
98
|
@import "./components/Progress/index.css";
|
|
99
99
|
@import "./components/Separator/index.css";
|
|
100
100
|
@import "./components/Skeleton/index.css";
|
|
101
|
+
@import "./components/Slider/index.css";
|
|
101
102
|
@import "./components/Spinner/index.css";
|
|
102
103
|
@import "./components/Switch/index.css";
|
|
103
104
|
@import "./components/TabbedMenu/index.css";
|
package/dist/index.d.ts
CHANGED
|
@@ -67,6 +67,7 @@ export * from "./components/PricingTable/index.js";
|
|
|
67
67
|
export * from "./components/Progress/index.js";
|
|
68
68
|
export * from "./components/Separator/index.js";
|
|
69
69
|
export * from "./components/Skeleton/index.js";
|
|
70
|
+
export * from "./components/Slider/index.js";
|
|
70
71
|
export * from "./components/SlidingPanels/index.js";
|
|
71
72
|
export * from "./components/Spinner/index.js";
|
|
72
73
|
export * from "./components/Switch/index.js";
|
package/dist/index.js
CHANGED
|
@@ -73,6 +73,7 @@ export * from "./components/PricingTable/index.js";
|
|
|
73
73
|
export * from "./components/Progress/index.js";
|
|
74
74
|
export * from "./components/Separator/index.js";
|
|
75
75
|
export * from "./components/Skeleton/index.js";
|
|
76
|
+
export * from "./components/Slider/index.js";
|
|
76
77
|
export * from "./components/SlidingPanels/index.js";
|
|
77
78
|
export * from "./components/Spinner/index.js";
|
|
78
79
|
export * from "./components/Switch/index.js";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
63 Svelte 5 component directories with consistent API patterns. All use runes-based reactivity.
|
|
6
6
|
|
|
7
7
|
## Component Categories
|
|
8
8
|
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
| Button | Actions with intent/variant/size |
|
|
30
30
|
| ButtonGroupRadio | Toggle group (single selection) |
|
|
31
31
|
| Switch | Boolean toggle |
|
|
32
|
+
| Slider | Fancy range input (fill + optional thumb) |
|
|
32
33
|
| TwCheck | Styled checkbox/radio |
|
|
33
34
|
| DropdownMenu | Popover menu |
|
|
34
35
|
| CommandMenu | Command palette (keyboard-driven) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/stuic",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.145.0",
|
|
4
4
|
"packageManager": "pnpm@11.5.0",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite dev",
|
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
"test:ui": "vitest --ui",
|
|
20
20
|
"svelte-check": "svelte-check",
|
|
21
21
|
"svelte-package": "svelte-package",
|
|
22
|
-
"rp": "pnpm run build &&
|
|
23
|
-
"rpm": "pnpm run build &&
|
|
22
|
+
"rp": "pnpm run build && deno run -A jsr:@marianmeres/release patch --no-push && npm publish --access public && git push --follow-tags",
|
|
23
|
+
"rpm": "pnpm run build && deno run -A jsr:@marianmeres/release minor --no-push && npm publish --access public && git push --follow-tags",
|
|
24
|
+
"release": "deno run -A jsr:@marianmeres/release"
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
26
27
|
"dist",
|