@marianmeres/stuic 3.172.0 → 3.174.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/AGENTS.md +19 -4
  2. package/API.md +56 -2
  3. package/README.md +21 -5
  4. package/dist/actions/resizable-width.svelte.d.ts +14 -28
  5. package/dist/actions/resizable-width.svelte.js +16 -171
  6. package/dist/attachments/index.d.ts +1 -0
  7. package/dist/attachments/index.js +1 -0
  8. package/dist/attachments/resizable.d.ts +113 -0
  9. package/dist/attachments/resizable.fixture.svelte +53 -0
  10. package/dist/attachments/resizable.fixture.svelte.d.ts +10 -0
  11. package/dist/attachments/resizable.js +295 -0
  12. package/dist/components/Button/README.md +11 -10
  13. package/dist/components/ButtonGroupRadio/README.md +52 -23
  14. package/dist/components/ButtonGroupRadio/index.css +6 -2
  15. package/dist/components/PricingTable/PricingTable.svelte +8 -14
  16. package/dist/components/PricingTable/README.md +29 -6
  17. package/dist/components/PricingTable/index.css +54 -0
  18. package/dist/components/RangeSlider/README.md +291 -0
  19. package/dist/components/RangeSlider/RangeSlider.svelte +763 -0
  20. package/dist/components/RangeSlider/RangeSlider.svelte.d.ts +130 -0
  21. package/dist/components/RangeSlider/i18n-sk.d.ts +17 -0
  22. package/dist/components/RangeSlider/i18n-sk.js +19 -0
  23. package/dist/components/RangeSlider/i18n.d.ts +33 -0
  24. package/dist/components/RangeSlider/i18n.js +41 -0
  25. package/dist/components/RangeSlider/index.css +430 -0
  26. package/dist/components/RangeSlider/index.d.ts +3 -0
  27. package/dist/components/RangeSlider/index.js +3 -0
  28. package/dist/components/Rating/README.md +206 -0
  29. package/dist/components/Rating/Rating.svelte +355 -0
  30. package/dist/components/Rating/Rating.svelte.d.ts +82 -0
  31. package/dist/components/Rating/i18n-sk.d.ts +17 -0
  32. package/dist/components/Rating/i18n-sk.js +21 -0
  33. package/dist/components/Rating/i18n.d.ts +34 -0
  34. package/dist/components/Rating/i18n.js +42 -0
  35. package/dist/components/Rating/index.css +170 -0
  36. package/dist/components/Rating/index.d.ts +3 -0
  37. package/dist/components/Rating/index.js +3 -0
  38. package/dist/components/SplitPane/README.md +169 -0
  39. package/dist/components/SplitPane/SplitPane.svelte +202 -0
  40. package/dist/components/SplitPane/SplitPane.svelte.d.ts +67 -0
  41. package/dist/components/SplitPane/i18n-sk.d.ts +17 -0
  42. package/dist/components/SplitPane/i18n-sk.js +18 -0
  43. package/dist/components/SplitPane/i18n.d.ts +31 -0
  44. package/dist/components/SplitPane/i18n.js +39 -0
  45. package/dist/components/SplitPane/index.css +153 -0
  46. package/dist/components/SplitPane/index.d.ts +3 -0
  47. package/dist/components/SplitPane/index.js +3 -0
  48. package/dist/components/WithSidePanel/README.md +19 -16
  49. package/dist/icons/index.d.ts +2 -0
  50. package/dist/icons/index.js +3 -0
  51. package/dist/index.css +17 -0
  52. package/dist/index.d.ts +3 -0
  53. package/dist/index.js +3 -0
  54. package/docs/{maybe-todo.md → _archive/maybe-todo.md} +18 -7
  55. package/docs/architecture.md +1 -1
  56. package/docs/conventions.md +27 -7
  57. package/docs/domains/actions.md +18 -18
  58. package/docs/domains/attachments.md +72 -9
  59. package/docs/domains/components.md +175 -32
  60. package/docs/domains/theming.md +57 -13
  61. package/package.json +5 -5
@@ -0,0 +1,291 @@
1
+ # RangeSlider
2
+
3
+ The dual-thumb sibling of [`Slider`](../Slider/README.md): two values (`start` ≤ `end`) on
4
+ one pill-shaped track, with the fill spanning the selected range — price filters,
5
+ "between" queries, min/max limits. Same construction and look as `Slider`: horizontal
6
+ and vertical orientation, pointer dragging with step snapping (the thumbs never cross),
7
+ native keyboard interaction per thumb, tick marks, floating value labels, form
8
+ participation via two hidden range inputs, and validation.
9
+
10
+ Not a replacement for two `FieldInput type="range"`s — this is the "fancy" custom-UI
11
+ variant. The bound ends are two plain numbers (`bind:start` / `bind:end`), the same
12
+ shape `FieldDateRange` uses.
13
+
14
+ ## Props
15
+
16
+ | Prop | Type | Default | Description |
17
+ | -------------- | ------------------------------------------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
18
+ | `start` | `number` | `min` | Lower value (bindable; non-finite / out-of-range / off-grid writes are normalized back, a reversed pair is reordered, `minRange` enforced) |
19
+ | `end` | `number` | `max` | Upper value (bindable; same normalization). Defaults to `max`, or the last step-grid point below it |
20
+ | `min` | `number` | `0` | Minimum value |
21
+ | `max` | `number` | `100` | Maximum value |
22
+ | `step` | `number \| "any"` | `1` | Snap increment (`"any"` or non-positive = continuous) |
23
+ | `minRange` | `number` | `0` | Minimum distance between the two values; rounded up onto the step grid, capped at the span. `0` lets the thumbs coincide |
24
+ | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Slider direction (vertical fills bottom-up) |
25
+ | `size` | `"sm" \| "md" \| "lg" \| string` | `"md"` | Cross-axis thickness preset |
26
+ | `intent` | `"primary" \| "accent" \| "success" \| "warning" \| "destructive"` | - | Semantic fill color |
27
+ | `thumb` | `boolean \| Snippet<[RangeSliderRenderCtx]>` | `true` | `false` hides both thumbs (fill-only look), a snippet renders inside each thumb (its context says which one) |
28
+ | `fillRounded` | `boolean` | `false` | Round the fill's edges ("pill inside a pill") |
29
+ | `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 |
30
+ | `valueLabel` | `Snippet<[RangeSliderRenderCtx]>` | - | Floating label per thumb, at its value along the track |
31
+ | `disabled` | `boolean` | `false` | Disable interaction |
32
+ | `label` | `string` | - | Accessible name of the whole control (`aria-label` on the `role="group"` root) |
33
+ | `labelStart` | `string` | `t("minimum")` | Accessible name of the start thumb's input ("Minimum") |
34
+ | `labelEnd` | `string` | `t("maximum")` | Accessible name of the end thumb's input ("Maximum") |
35
+ | `nameStart` | `string` | - | Form field name of the hidden range input carrying `start` |
36
+ | `nameEnd` | `string` | - | Form field name of the hidden range input carrying `end` |
37
+ | `oninput` | `(value: RangeSliderValue, thumb: RangeSliderThumb) => void` | - | Fires on every value change (drag, keyboard) with the pair and the thumb that moved |
38
+ | `onchange` | `(value: RangeSliderValue, thumb: RangeSliderThumb) => void` | - | Fires when a change is committed (drag release, keyboard) |
39
+ | `validate` | `boolean \| ValidateOptions` | - | Enable validation (stuic validate action). **`customValidator` receives the `{ start, end }` pair** as its value |
40
+ | `t` | `TranslateFn` | English | i18n of the default thumb names (`createRangeSliderT`) |
41
+ | `unstyled` | `boolean` | `false` | Skip all default styling |
42
+ | `class` | `string` | - | Classes for the root element |
43
+ | `trackClass` | `string` | - | Classes for the track (pill background) |
44
+ | `fillClass` | `string` | - | Classes for the fill (selected range) |
45
+ | `thumbClass` | `string` | - | Classes for both thumbs |
46
+ | `tickClass` | `string` | - | Classes for each tick mark |
47
+ | `valueClass` | `string` | - | Classes for both value label wrappers |
48
+ | `el` | `HTMLDivElement` | - | Root element reference (bindable) |
49
+ | `inputStartEl` | `HTMLInputElement` | - | Hidden range input carrying `start` (bindable) |
50
+ | `inputEndEl` | `HTMLInputElement` | - | Hidden range input carrying `end` (bindable) |
51
+
52
+ `RangeSliderValue` (the callback / validator payload): `{ start: number; end: number }`.
53
+
54
+ `RangeSliderThumb`: `"start" | "end"`.
55
+
56
+ `RangeSliderRenderCtx` (passed to the `thumb` and `valueLabel` snippets, once per thumb):
57
+ `{ thumb: RangeSliderThumb; value: number; ratio: number /* 0..1 */; percent: number /* 0..100 */; dragging: boolean; start: number; end: number }`
58
+
59
+ Remaining props are spread onto the root `<div>`.
60
+
61
+ ### Exported methods (via component instance binding)
62
+
63
+ | Method | Description |
64
+ | ----------------------- | ------------------------------------------------------------------- |
65
+ | `validate()` | Trigger validation now |
66
+ | `clearValidation()` | Clear the current validation result |
67
+ | `getValidation()` | Read the current validation result |
68
+ | `focus()` | Focus the start thumb's range input (Tab moves on to the end thumb) |
69
+ | `scrollIntoView(opts?)` | Scroll the slider into view |
70
+
71
+ ### Other exports
72
+
73
+ | Export | Description |
74
+ | -------------------------- | ---------------------------------------------------- |
75
+ | `createRangeSliderT` | Builds the `t` prop from a (partial) message catalog |
76
+ | `RANGE_SLIDER_MESSAGES_EN` | Built-in English catalog (also the fallback) |
77
+ | `RANGE_SLIDER_MESSAGES_SK` | Bundled Slovak catalog (opt-in) |
78
+ | `RangeSliderMessageKey` | `"minimum" \| "maximum"` |
79
+ | `RangeSliderMessages` | One locale's catalog |
80
+
81
+ ## Usage
82
+
83
+ ### Basic
84
+
85
+ ```svelte
86
+ <script lang="ts">
87
+ import { RangeSlider } from "@marianmeres/stuic";
88
+
89
+ let priceMin = $state(150);
90
+ let priceMax = $state(600);
91
+ </script>
92
+
93
+ <RangeSlider bind:start={priceMin} bind:end={priceMax} min={0} max={1000} step={10} />
94
+ ```
95
+
96
+ ### Price filter with value labels
97
+
98
+ ```svelte
99
+ <RangeSlider
100
+ bind:start={priceMin}
101
+ bind:end={priceMax}
102
+ min={0}
103
+ max={1000}
104
+ step={10}
105
+ label="Price"
106
+ labelStart="Minimum price"
107
+ labelEnd="Maximum price"
108
+ >
109
+ {#snippet valueLabel({ value })}
110
+ {eur.format(value)}
111
+ {/snippet}
112
+ </RangeSlider>
113
+ ```
114
+
115
+ Labels are rendered per thumb and will overlap when the thumbs are close; for a single
116
+ combined readout ("€150 – €600") render it yourself next to the slider from the bound
117
+ values.
118
+
119
+ ### Minimum distance
120
+
121
+ ```svelte
122
+ <!-- the thumbs can never get closer than 20 -->
123
+ <RangeSlider bind:start bind:end minRange={20} />
124
+ ```
125
+
126
+ ### Steps and ticks
127
+
128
+ ```svelte
129
+ <RangeSlider min={0} max={24} step={1} ticks bind:start={from} bind:end={to} />
130
+ <RangeSlider min={0} max={100} step={0.5} ticks={[0, 25, 50, 75, 100]} />
131
+ ```
132
+
133
+ ### Fill-only (no thumbs)
134
+
135
+ ```svelte
136
+ <RangeSlider thumb={false} start={30} end={70} />
137
+ ```
138
+
139
+ With no thumbs there is nothing to reserve: the values map linearly across the whole
140
+ track and the fill collapses to zero when they coincide.
141
+
142
+ ### Custom thumb content
143
+
144
+ The snippet renders once per thumb; use `thumb` from its context to tell them apart.
145
+
146
+ ```svelte
147
+ <RangeSlider bind:start bind:end size="lg">
148
+ {#snippet thumb({ thumb })}
149
+ {@html (thumb === "start" ? iconChevronLeft : iconChevronRight)({ size: 18 })}
150
+ {/snippet}
151
+ </RangeSlider>
152
+ ```
153
+
154
+ ### Vertical
155
+
156
+ ```svelte
157
+ <RangeSlider orientation="vertical" class="h-40" bind:start bind:end />
158
+ ```
159
+
160
+ ### In a form
161
+
162
+ Two hidden `input[type=range]` carry the values:
163
+
164
+ ```svelte
165
+ <form onsubmit={...}>
166
+ <RangeSlider nameStart="price_min" nameEnd="price_max" bind:start bind:end />
167
+ </form>
168
+ ```
169
+
170
+ ### Validation
171
+
172
+ The stuic `validate` action is attached to the start input and re-run whenever either
173
+ thumb commits. Because a single input's DOM string is useless for a range rule,
174
+ `customValidator` receives the `{ start, end }` pair as its value (the start input is
175
+ still passed as the third argument):
176
+
177
+ ```svelte
178
+ <RangeSlider
179
+ bind:start
180
+ bind:end
181
+ validate={{
182
+ customValidator: (v) => {
183
+ const { start, end } = v as RangeSliderValue;
184
+ return end - start < 25 ? "Span at least 25" : "";
185
+ },
186
+ }}
187
+ setValidationResult={(res) => (validation = res)}
188
+ />
189
+ ```
190
+
191
+ ### i18n
192
+
193
+ Only the default thumb names ("Minimum" / "Maximum") are translatable — pass explicit
194
+ `labelStart` / `labelEnd` for context-specific names.
195
+
196
+ ```svelte
197
+ <script>
198
+ import {
199
+ RangeSlider,
200
+ createRangeSliderT,
201
+ RANGE_SLIDER_MESSAGES_SK,
202
+ } from "@marianmeres/stuic";
203
+ const t = createRangeSliderT(RANGE_SLIDER_MESSAGES_SK);
204
+ </script>
205
+
206
+ <RangeSlider bind:start bind:end {t} />
207
+ ```
208
+
209
+ ## Interaction
210
+
211
+ - **Pointer**: press anywhere on the track and the _nearest_ thumb jumps there, then
212
+ drags. Grabbing a thumb itself does not jump (the drag continues from the grab point).
213
+ When both thumbs sit on top of each other, a press beside them moves the thumb on that
214
+ side (towards `max` the end thumb, towards `min` the start thumb), and a press _on_
215
+ them is resolved by the first move's direction — drag right/up and the end thumb
216
+ comes along, left/down the start thumb.
217
+ - **No crossing**: a thumb dragged (or stepped) past the other one stops at it —
218
+ `minRange` apart when set. Thumbs never swap roles.
219
+ - **Keyboard**: Tab focuses the start thumb, then the end thumb; Arrow keys / PageUp /
220
+ PageDown / Home / End step the focused one — native `input[type=range]` behavior, with
221
+ the other thumb as the limit (End on the start thumb jumps up to the end thumb).
222
+ - **Thumb reserve**: with thumbs rendered the values map onto the thumb-center travel —
223
+ the outer half-thumb at each end of the track resolves to `min` / `max`, and the fill
224
+ never shrinks below one thumb (coinciding thumbs leave a thumb-sized nub).
225
+ `thumb={false}` maps linearly across the whole track instead.
226
+ - **Vertical**: bottom is `min`, top is `max`; ArrowUp increases.
227
+ - **RTL**: horizontal sliders flip automatically (logical CSS properties + pointer math).
228
+ - **Commit semantics**: `oninput` fires only on actual value changes; `onchange` only
229
+ when a drag / keypress committed a _different_ value (native-faithful — a no-move tap
230
+ fires neither). Both receive the whole pair plus the thumb that moved.
231
+ - **Off-grid max**: when `max` is not on the step grid (e.g. `min=0 max=95 step=10`),
232
+ the largest reachable value is the last grid point (`90`), matching native range
233
+ sanitization — that is also the default `end`.
234
+
235
+ ## Accessibility
236
+
237
+ The root is a `role="group"` named by `label`; inside it, each thumb is a real
238
+ `<input type="range">` (visually hidden, full thickness, covering the track from its end
239
+ up to the midpoint between the thumbs), so screen readers see two sliders named
240
+ `labelStart` / `labelEnd` — "Minimum" / "Maximum" by default (`t`) — with the native
241
+ value, min and max. Explore-by-touch on VoiceOver / TalkBack lands on the slider of the
242
+ side being touched. The focus ring is drawn around the focused thumb (around the track
243
+ when `thumb={false}`).
244
+
245
+ ## Caveats
246
+
247
+ - **Cross-axis sizing**: size the thickness via `size` presets or
248
+ `--stuic-range-slider-thickness` — not via `h-*`/`w-*` utility classes. The pointer math
249
+ and the CSS thumb positioning both derive from the thickness; a utility class
250
+ resizes the box without updating `--_thickness`, misaligning them. (Main-axis length
251
+ via a class — e.g. `class="w-72"` — is fine.)
252
+ - **Root pointer handlers are reserved**: `onpointerdown/move/up/cancel` are excluded
253
+ from `Props` (the drag machinery owns them). Wrap the slider if you need them.
254
+ - **Touch**: the slider claims the whole touch gesture (`touch-action: none`) — a touch
255
+ starting on it adjusts a value and never scrolls the page.
256
+ - **The wrapper is not the control**: props spread onto the root `<div>` — including
257
+ `onfocus` / `onblur` — never reach the hidden inputs. Use `label`, `labelStart`,
258
+ `labelEnd`; for anything else bind `inputStartEl` / `inputEndEl` and wire it
259
+ imperatively.
260
+ - **Independent tokens**: the look is deliberately not derived from `--stuic-slider-*`.
261
+ A theme that restyles `Slider` restyles `RangeSlider` by setting the
262
+ `--stuic-range-slider-*` twins.
263
+
264
+ ## CSS Variables
265
+
266
+ | Variable | Default | Description |
267
+ | --------------------------------------- | ----------------------------- | ---------------------------------- |
268
+ | `--stuic-range-slider-track` | `--stuic-color-muted` | Track (pill background) color |
269
+ | `--stuic-range-slider-fill` | `--stuic-color-primary` | Fill (selected range) color |
270
+ | `--stuic-range-slider-thumb` | `--color-white` | Thumb background |
271
+ | `--stuic-range-slider-thumb-foreground` | `--stuic-color-foreground` | Thumb content color |
272
+ | `--stuic-range-slider-tick` | foreground 25% mix | Tick mark color (over the track) |
273
+ | `--stuic-range-slider-tick-on-fill` | background 55% mix | Tick mark color (over the fill) |
274
+ | `--stuic-range-slider-ring-width` | `4px` | Focus ring width |
275
+ | `--stuic-range-slider-ring-color` | `--stuic-color-ring` | Focus ring color |
276
+ | `--stuic-range-slider-thickness` | `2rem` (`sm` 1.25, `lg` 3) | Cross-axis size |
277
+ | `--stuic-range-slider-length` | `10rem` | Main-axis size |
278
+ | `--stuic-range-slider-thumb-inset` | `3px` | Gap between thumb and track edge |
279
+ | `--stuic-range-slider-radius` | `9999px` | Track corner radius |
280
+ | `--stuic-range-slider-fill-radius` | `--stuic-range-slider-radius` | Fill radius (when `fillRounded`) |
281
+ | `--stuic-range-slider-thumb-radius` | `9999px` | Thumb corner radius |
282
+ | `--stuic-range-slider-thumb-shadow` | `--stuic-shadow` | Thumb shadow |
283
+ | `--stuic-range-slider-tick-size` | `4px` | Tick mark diameter |
284
+ | `--stuic-range-slider-value-gap` | `0.375rem` | Gap between track and value labels |
285
+ | `--stuic-range-slider-transition` | `--stuic-transition` | Fill/thumb movement transition |
286
+
287
+ Data attributes on the root, for custom CSS: `data-orientation`, `data-thumbs`
288
+ (`"true"` / `"false"`), `data-fill-rounded`, `data-size`, `data-intent`,
289
+ `data-disabled`, `data-dragging` / `data-ring` / `data-active-thumb` (each naming a
290
+ thumb: `"start"` / `"end"`). Thumbs, value labels and the hidden inputs carry
291
+ `data-thumb="start|end"`; tick layers `data-layer="before|on-fill|after"`.