@ixfx/components 0.4.4 → 0.5.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 (47) hide show
  1. package/bundle/index.d.ts +126 -3
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +560 -24
  4. package/bundle/index.js.map +1 -1
  5. package/bundle/style.css +1 -1
  6. package/dist/{colour-picker-BacNRabo.js → colour-picker-C3Ag4uh-.js} +2 -2
  7. package/dist/{colour-picker-BacNRabo.js.map → colour-picker-C3Ag4uh-.js.map} +1 -1
  8. package/dist/colour-picker.js +1 -1
  9. package/dist/{defaults-C3b9OWJD.js → defaults-DcE8RQJc.js} +19 -1
  10. package/dist/defaults-DcE8RQJc.js.map +1 -0
  11. package/dist/icons.js +1 -1
  12. package/dist/index.d.ts +127 -4
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +540 -27
  15. package/dist/index.js.map +1 -1
  16. package/dist/labelled-radial-input.d.ts +1 -1
  17. package/dist/polar-pad.d.ts +1 -1
  18. package/dist/{radial-input-DXMN55Nz.d.ts → radial-input-BhZeyyP-.d.ts} +2 -2
  19. package/dist/{radial-input-DXMN55Nz.d.ts.map → radial-input-BhZeyyP-.d.ts.map} +1 -1
  20. package/dist/radial-input.d.ts +1 -1
  21. package/dist/range-input.d.ts +1 -1
  22. package/dist/range.d.ts +1 -1
  23. package/dist/{slider-input-XppH3vcy.d.ts → slider-input-TidGhvXt.d.ts} +2 -2
  24. package/dist/{slider-input-XppH3vcy.d.ts.map → slider-input-TidGhvXt.d.ts.map} +1 -1
  25. package/dist/slider-input.d.ts +1 -1
  26. package/dist/style.css +1 -1
  27. package/dist/swipe.d.ts +1 -1
  28. package/dist/tab-list-BohmbvqF.d.ts.map +1 -1
  29. package/dist/{tab-list-DzknpdSo.js → tab-list-DhkVhRQc.js} +7 -2
  30. package/dist/{tab-list-DzknpdSo.js.map → tab-list-DhkVhRQc.js.map} +1 -1
  31. package/dist/tabs.js +1 -1
  32. package/dist/wheel-nudge-BlSCe_hD.js.map +1 -1
  33. package/dist/{wheel-nudge-DGrNzWuE.d.ts → wheel-nudge-DgMTRYlh.d.ts} +3 -3
  34. package/dist/wheel-nudge-DgMTRYlh.d.ts.map +1 -0
  35. package/dist/xy-pad.d.ts +1 -1
  36. package/docs-user/README.md +2 -0
  37. package/docs-user/button.md +48 -0
  38. package/docs-user/narrowed-text.md +33 -1
  39. package/docs-user/notifier.md +131 -0
  40. package/docs-user/plots.md +61 -1
  41. package/docs-user/slider-input.md +673 -0
  42. package/docs-user/tabs.md +12 -14
  43. package/docs-user/user-catalog.md +69 -4
  44. package/docs-user/vertical-list.md +75 -7
  45. package/package.json +3 -1
  46. package/dist/defaults-C3b9OWJD.js.map +0 -1
  47. package/dist/wheel-nudge-DGrNzWuE.d.ts.map +0 -1
@@ -0,0 +1,673 @@
1
+ # ixfx-slider-input
2
+
3
+ A linear SVG-based slider. Supports horizontal and vertical orientations, three value modes (single/dual/triple), three single-mode styles (range/bipolar/unipolar), triangle/needle/dot markers, and `value-drag` length-drag gestures for multi-handle modes.
4
+
5
+ ---
6
+
7
+ ## Basic Usage
8
+
9
+ ```html
10
+ <ixfx-slider-input min="0" max="100" value-a="50"></ixfx-slider-input>
11
+ ```
12
+
13
+ The component defaults to `200px` wide and `24px` tall (the horizontal cross axis). Set width and height via CSS:
14
+
15
+ ```html
16
+ <ixfx-slider-input min="0" max="100" value-a="50" style="width: 320px;"></ixfx-slider-input>
17
+ ```
18
+
19
+ Vertical orientation swaps the dimensions and rotates interaction to up/down:
20
+
21
+ ```html
22
+ <ixfx-slider-input
23
+ orientation="vertical"
24
+ min="0" max="100" value-a="30"
25
+ style="width: 24px; height: 200px;"
26
+ ></ixfx-slider-input>
27
+ ```
28
+
29
+ To fill a container, set both `width: 100%` and `height: 100%`:
30
+
31
+ ```html
32
+ <div style="width: 100%; height: 24px;">
33
+ <ixfx-slider-input min="0" max="100" value-a="50" style="width: 100%; height: 100%;"></ixfx-slider-input>
34
+ </div>
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Attributes & Properties
40
+
41
+ | Attribute / Property | Type | Default | Description |
42
+ |---|---|---|---|
43
+ | `value-a` | `number` | `50` | Primary handle value (single mode), or lower pole (dual/triple) |
44
+ | `value-b` | `number` | `0` | Upper pole (dual/triple modes only) |
45
+ | `value-c` | `number` | midpoint of A/B | Middle handle (triple mode only) |
46
+ | `min` | `number` | `0` | Minimum value |
47
+ | `max` | `number` | `100` | Maximum value |
48
+ | `step` | `number` | `undefined` | Keyboard/wheel/pointer step size. If unset, defaults to 10% of range |
49
+ | `stops` | `number` | `undefined` | Quantise to N discrete positions. Also sets step size |
50
+ | `value-mode` | `"single"` \| `"dual"` \| `"triple"` | `"single"` | Number of handles |
51
+ | `single-style` | `"range"` \| `"bipolar"` \| `"unipolar"` | `"range"` | How the value bar is drawn (single mode only) |
52
+ | `orientation` | `"horizontal"` \| `"vertical"` | `"horizontal"` | Track orientation |
53
+ | `marker-style` | `"needle"` \| `"dot"` \| `"triangle"` \| `"none"` | `"needle"` | Marker shape (applies to all handles) |
54
+ | `marker-style-c` | as `marker-style` | `undefined` | Optional per-handle marker override for the C handle in triple mode |
55
+ | `crossover` | `"none"` \| `"allow"` | `"allow"` | Whether handles can pass through each other (dual/triple) |
56
+ | `value-drag` | `"none"` \| `"stop"` \| `"flex"` | `"none"` | Whether the segment between A and B is draggable (dual/triple) |
57
+ | `value-min` | `number` | `1` | Minimum gap between A and B in value units (dual/triple) |
58
+ | `value-c-follow` | `"none"` \| `"proportional"` | `"none"` | How C follows when A/B move the band (triple mode) |
59
+ | `disabled` | `boolean` | `false` | Disables all interaction. Reflected as attribute |
60
+
61
+ ### Sizing
62
+
63
+ Size via CSS `width`/`height`. The horizontal default is 200×24, vertical 24×200. Both can be overridden freely.
64
+
65
+ ```html
66
+ <!-- Thin (16px tall) -->
67
+ <ixfx-slider-input style="height: 16px;" min="0" max="100" value-a="60"></ixfx-slider-input>
68
+
69
+ <!-- Large (32px tall) -->
70
+ <ixfx-slider-input style="height: 32px;" min="0" max="100" value-a="60"></ixfx-slider-input>
71
+ ```
72
+
73
+ ### Disabled state
74
+
75
+ ```html
76
+ <ixfx-slider-input disabled min="0" max="100" value-a="70"></ixfx-slider-input>
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Interaction
82
+
83
+ **Mouse / touch:** click or drag on the track. Clicking sets the value to that position; dragging continuously updates it. In dual/triple modes, clicking a handle drags it; clicking the segment between handles (when `value-drag` is enabled) drags the whole band. Full min-to-max range spans `2×` the component's rendered long-axis size.
84
+
85
+ **Cursor feedback:**
86
+ - `grab` over a marker handle (a/b/c)
87
+ - `move` over a draggable segment (between A and B when `value-drag` is on)
88
+ - `pointer` over empty track where clicking sets a value (single mode; or the dead zones outside A/B in multi mode)
89
+ - `grabbing` while actively dragging
90
+ - `default` over a no-op segment (dual mode with `value-drag="none"`, or between A-C/C-B in triple with `value-drag="none"`)
91
+ - `not-allowed` on a `disabled` slider
92
+
93
+ **Shift + drag:** fine control — sensitivity reduced to 1/10th (`KEY_STEP_DIVISOR = 10`).
94
+
95
+ **Keyboard** (when focused):
96
+
97
+ | Key | Action |
98
+ |---|---|
99
+ | `ArrowRight` / `ArrowUp` | Increase by step (single mode), or move last-interacted handle |
100
+ | `ArrowLeft` / `ArrowDown` | Decrease by step (single mode), or move last-interacted handle |
101
+ | `Home` | Set to `min` (or move handle to low in multi mode) |
102
+ | `End` | Set to `max` (or move handle to high in multi mode) |
103
+ | `Shift` + arrows | Fine step (÷10) |
104
+
105
+ **Wheel** (when focused or `lighttouch` is on):
106
+
107
+ Wheel forward increases the value, wheel backward decreases it (sign is platform-agnostic — both `deltaY` and `deltaX` are considered so Mac shift-scrolls are handled correctly). Shift narrows the step to 1/10th.
108
+
109
+ Step size priority: `stops` > explicit `step` > 10% of range.
110
+
111
+ ---
112
+
113
+ ## Value Modes
114
+
115
+ ### `single` (default)
116
+
117
+ One handle. The `value-a` property holds the current value.
118
+
119
+ ```html
120
+ <ixfx-slider-input value-mode="single" min="0" max="100" value-a="60"></ixfx-slider-input>
121
+ ```
122
+
123
+ ### `dual`
124
+
125
+ Two handles. The `value-a` / `value-b` properties hold the low/high poles, and the value bar is drawn between them. They are swapped automatically so that `valueMin = min(valueA, valueB)` and `valueMax = max(valueA, valueB)`.
126
+
127
+ ```html
128
+ <ixfx-slider-input
129
+ value-mode="dual"
130
+ min="0" max="100"
131
+ value-a="20" value-b="80"
132
+ ></ixfx-slider-input>
133
+ ```
134
+
135
+ The `crossover` attribute controls whether the handles can pass through each other:
136
+ - `"allow"` (default) — handles can cross; the bar will flip and the high/low swap.
137
+ - `"none"` — handles are pinned to their side and can't cross. `value-min` enforces a minimum gap.
138
+
139
+ The `value-drag` attribute controls whether the segment between A and B is itself draggable:
140
+ - `"none"` (default) — clicking the segment does nothing.
141
+ - `"stop"` — dragging the segment translates it, but length stays fixed at the bounds.
142
+ - `"flex"` — dragging the segment translates it, length shortens at the bounds.
143
+
144
+ ### `triple`
145
+
146
+ Three handles. `value-a` / `value-b` are the outer poles, `value-c` is the middle handle. The `value-c-follow` attribute controls how C behaves when the A/B band changes:
147
+ - `"none"` (default) — C keeps its value unless pushed outside the new band.
148
+ - `"proportional"` — C is re-anchored at the same fractional position it had within the old band, so it moves with A/B.
149
+
150
+ ```html
151
+ <ixfx-slider-input
152
+ value-mode="triple"
153
+ min="0" max="100"
154
+ value-a="20" value-b="80"
155
+ ></ixfx-slider-input>
156
+ ```
157
+
158
+ A/B/C can be styled independently via `--marker-a-color`, `--marker-b-color`, `--marker-c-color`, and the C handle can take a different marker shape via `marker-style-c="dot"`.
159
+
160
+ ---
161
+
162
+ ## Single Styles
163
+
164
+ The `single-style` attribute (single mode only) controls how the value bar is drawn.
165
+
166
+ ### `range` (default)
167
+
168
+ The value bar grows from the start of the track to the value. Classic slider behaviour.
169
+
170
+ ```html
171
+ <ixfx-slider-input single-style="range" min="0" max="100" value-a="60"></ixfx-slider-input>
172
+ ```
173
+
174
+ ### `bipolar`
175
+
176
+ The value bar is centred on the midpoint of `min`…`max`. Values below the midpoint grow leftward, values above grow rightward. A small tick is drawn at the centre. Suited for pan, balance, or any centred parameter.
177
+
178
+ ```html
179
+ <ixfx-slider-input single-style="bipolar" min="-1" max="1" value-a="0.5"></ixfx-slider-input>
180
+ ```
181
+
182
+ ### `unipolar`
183
+
184
+ The value bar is symmetric around the centre, growing in both directions as the value increases. A marker is shown at *both* ends of the bar (low and high). A small tick is drawn at the centre. Click-to-set follows a V-shape: 0 at the centre, 1 at either end (clicking 25% in from the left or right sets value 0.5).
185
+
186
+ ```html
187
+ <ixfx-slider-input single-style="unipolar" min="0" max="1" value-a="0.6"></ixfx-slider-input>
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Stepping & Quantisation
193
+
194
+ `step` controls keyboard, wheel, and pointer step size:
195
+
196
+ ```html
197
+ <ixfx-slider-input min="0" max="1" value-a="0.5" step="0.2"></ixfx-slider-input>
198
+ ```
199
+
200
+ `stops` quantises the value to N equally-spaced positions. Drag, click, keyboard, and wheel input all snap:
201
+
202
+ ```html
203
+ <!-- 5 discrete stops: 0, 0.2, 0.4, 0.6, 0.8, 1.0 -->
204
+ <ixfx-slider-input min="0" max="1" value-a="0.5" stops="5"></ixfx-slider-input>
205
+
206
+ <!-- 10 stops across 0-100: snaps to 0, 10, 20 … 100 -->
207
+ <ixfx-slider-input min="0" max="100" value-a="50" stops="10"></ixfx-slider-input>
208
+ ```
209
+
210
+ When `stops` is set it also governs the keyboard and wheel step (one stop per keypress).
211
+
212
+ ---
213
+
214
+ ## Events
215
+
216
+ Both events bubble and are `composed` (cross shadow-DOM boundaries).
217
+
218
+ ### `input`
219
+
220
+ Fired continuously during user interaction (every pointer/key event that changes the value). Use for live updates.
221
+
222
+ ```ts
223
+ import type { SliderInputInputEvent } from 'ixfx-slider-input/types';
224
+
225
+ slider.addEventListener('input', (e: SliderInputInputEvent) => {
226
+ const d = e.detail;
227
+ // d.value: number in min..max (single mode), or = valueMax in multi modes
228
+ // d.valueRelative: number 0..1
229
+ // d.valueMin, d.valueMax, d.valueMinRelative, d.valueMaxRelative: dual/triple
230
+ // d.valueC, d.valueCRelative: triple only
231
+ // d.changed: 'min' | 'max' | 'c' | 'both' (which pole(s) moved)
232
+ });
233
+ ```
234
+
235
+ ### `change`
236
+
237
+ Fired after the user finishes interacting (debounced 150 ms). Equivalent to the native `<input type="range">` change semantics. Also fires immediately for programmatic value changes.
238
+
239
+ ```ts
240
+ import type { SliderInputChangeEvent } from 'ixfx-slider-input/types';
241
+
242
+ slider.addEventListener('change', (e: SliderInputChangeEvent) => {
243
+ const d = e.detail;
244
+ // Same shape as `input` plus previousValue / previousValueMin / etc.
245
+ });
246
+ ```
247
+
248
+ **Note:** `input` fires only for user-initiated changes. Programmatic `slider.valueA = x` triggers only `change`.
249
+
250
+ ---
251
+
252
+ ## CSS Custom Properties
253
+
254
+ ### Track
255
+
256
+ | Variable | Default | Description |
257
+ |---|---|---|
258
+ | `--track-color` | `var(--surface-4)` | Stroke colour of the central track rect |
259
+ | `--track-width` | `2` | Stroke width of the track (SVG units) |
260
+ | `--track-fill` | `transparent` | Fill colour of the track rect |
261
+
262
+ ```html
263
+ <ixfx-slider-input
264
+ min="0" max="100" value-a="60"
265
+ style="--track-width: 6; --track-color: hsl(280, 30%, 30%);"
266
+ ></ixfx-slider-input>
267
+ ```
268
+
269
+ ### Lanes
270
+
271
+ Lanes are line segments drawn over the track. The built-in lanes are `track` (the grey background) and `value` (the accent-coloured progress bar). Custom lanes can be added at runtime (see [Data Lanes](#data-lanes-programmatic-api)).
272
+
273
+ | Variable | Default | Description |
274
+ |---|---|---|
275
+ | `--lane-track-color` | `var(--surface-4)` | Colour of the `track` lane |
276
+ | `--lane-value-color` | `var(--accent)` | Colour of the `value` lane |
277
+ | `--lane-color` | `var(--accent)` | Fallback colour for custom lanes |
278
+ | `--lane-<key>-color` | — | Per-lane colour override (e.g. `--lane-sweep-color`) |
279
+ | `--lane-<key>-width` | — | Per-lane width override (e.g. `--lane-sweep-width`) |
280
+ | `--lane-dasharray` | `none` | SVG `stroke-dasharray` applied to all lanes |
281
+ | `--lane-dashoffset` | `0` | SVG `stroke-dashoffset` — animate for marching-ants effect |
282
+
283
+ Dashed lanes:
284
+
285
+ ```html
286
+ <!-- 8px dashes, 4px gaps -->
287
+ <ixfx-slider-input style="--lane-dasharray: 8 4;" min="0" max="100" value-a="60"></ixfx-slider-input>
288
+
289
+ <!-- Dotted -->
290
+ <ixfx-slider-input style="--lane-dasharray: 2 4;" min="0" max="100" value-a="60"></ixfx-slider-input>
291
+ ```
292
+
293
+ ### Track + lanes styled together (independent of host dimensions)
294
+
295
+ ```html
296
+ <ixfx-slider-input
297
+ min="0" max="100" value-a="60"
298
+ style="--track-width: 6; --lane-track-width: 0.4; --lane-value-width: 0.4;"
299
+ ></ixfx-slider-input>
300
+ ```
301
+
302
+ ### Marker (value indicator)
303
+
304
+ | Variable | Default | Description |
305
+ |---|---|---|
306
+ | `--marker-color` | `var(--accent)` | Colour of the needle line, dot fill, or triangle fill |
307
+ | `--marker-thickness` | `0.12` | Relative thickness (fraction of cross axis) |
308
+ | `--marker-length` | `0.8` | Needle length as fraction of cross axis (needle) |
309
+ | `--marker-offset` | `0` | Perpendicular offset from track centreline (needle/dot/triangle) |
310
+ | `--marker-border-width` | `0` | Border halo thickness around marker |
311
+ | `--marker-border-color` | `transparent` | Border halo colour |
312
+ | `--marker-dasharray` | `none` | Dashed needle pattern |
313
+ | `--marker-dashoffset` | `0` | Dashed needle offset |
314
+
315
+ Per-handle overrides in dual/triple modes follow the same convention: `--marker-a-color`, `--marker-b-color`, `--marker-c-color`.
316
+
317
+ ### Midpoint marker (bipolar / unipolar modes only)
318
+
319
+ | Variable | Default | Description |
320
+ |---|---|---|
321
+ | `--midpoint-color` | `var(--surface-muted-text)` | Colour of the centre tick / dot |
322
+ | `--midpoint-thickness` | `1px` (needle) / `0.08` (dot) | Tick line width or dot radius fraction |
323
+ | `--midpoint-length` | `0.15` | Tick length as fraction of cross axis (needle) |
324
+ | `--midpoint-border-color` | `transparent` | Dot border colour (dot only) |
325
+ | `--midpoint-border-width` | `0` | Dot border width (dot only) |
326
+
327
+ ### Dynamic value variables
328
+
329
+ The component sets `--value-relative` (a number 0…1) on the host as the value changes. In dual/triple modes it also sets `--value-min-relative`, `--value-max-relative`, and `--value-c-relative`. Use them inside `calc()` to create value-reactive styles entirely in CSS:
330
+
331
+ ```html
332
+ <!-- Value bar hue shifts from purple to pink as value increases -->
333
+ <ixfx-slider-input
334
+ min="0" max="100" value-a="60"
335
+ style="--lane-value-color: hsl(calc(var(--value-relative) * 280), 80%, 55%);"
336
+ ></ixfx-slider-input>
337
+
338
+ <!-- Marker hue follows value -->
339
+ <ixfx-slider-input
340
+ min="0" max="100" value-a="60"
341
+ style="--marker-color: hsl(calc(var(--value-relative) * 280), 80%, 55%);"
342
+ ></ixfx-slider-input>
343
+
344
+ <!-- Marker thickness scales with value -->
345
+ <ixfx-slider-input
346
+ min="0" max="100" value-a="60"
347
+ style="--marker-thickness: calc(0.08 + var(--value-relative) * 0.25);"
348
+ ></ixfx-slider-input>
349
+ ```
350
+
351
+ ### Focus outline
352
+
353
+ | Variable | Default | Description |
354
+ |---|---|---|
355
+ | `--slider-input-svg-focus-outline` | `2px solid var(--accent)` | Outline colour and style applied to the SVG when focused via keyboard |
356
+ | `--slider-input-svg-focus-outline-offset` | `2px` | Outline offset |
357
+
358
+ ---
359
+
360
+ ## Marker Styles
361
+
362
+ ### `needle` (default)
363
+
364
+ A line drawn perpendicular to the track. Spans `--marker-offset` (offset from centreline) to `--marker-length` (length, both as fractions of cross axis).
365
+
366
+ ```html
367
+ <ixfx-slider-input marker-style="needle" min="0" max="100" value-a="60"></ixfx-slider-input>
368
+ ```
369
+
370
+ Thin minimalist look:
371
+
372
+ ```html
373
+ <ixfx-slider-input
374
+ marker-style="needle" min="0" max="100" value-a="60"
375
+ style="--marker-thickness: 0.05; --marker-color: var(--surface-muted-text); --marker-length: 0.9; --marker-offset: 0.5;"
376
+ ></ixfx-slider-input>
377
+ ```
378
+
379
+ With a border halo (useful for contrast against coloured lanes):
380
+
381
+ ```html
382
+ <ixfx-slider-input
383
+ marker-style="needle" min="0" max="100" value-a="60"
384
+ style="--marker-thickness: 0.1; --marker-border-width: 0.02; --marker-border-color: white;"
385
+ ></ixfx-slider-input>
386
+ ```
387
+
388
+ ### `dot`
389
+
390
+ A filled circle placed at the value's track position. `--marker-offset` shifts it perpendicular to the track (0 = on the centreline, positive = away from the centreline in the track's perpendicular direction).
391
+
392
+ ```html
393
+ <!-- Dot on the track -->
394
+ <ixfx-slider-input marker-style="dot" min="0" max="100" value-a="60"></ixfx-slider-input>
395
+
396
+ <!-- Dot offset above the track -->
397
+ <ixfx-slider-input
398
+ marker-style="dot" min="0" max="100" value-a="60"
399
+ style="--marker-offset: 0.5; --marker-thickness: 0.15;"
400
+ ></ixfx-slider-input>
401
+ ```
402
+
403
+ ### `triangle`
404
+
405
+ A downward-pointing triangle. The apex sits on the value-track centreline at the value's track position; the base is offset *outward* by `--marker-length` (away from the centreline), so the shape hangs outside the value bar with its point facing in. Useful as a "data is here" indicator on a stacked-lane layout.
406
+
407
+ ```html
408
+ <ixfx-slider-input
409
+ marker-style="triangle" min="0" max="100" value-a="60"
410
+ style="--marker-offset: 0.7; --marker-thickness: 0.3;"
411
+ ></ixfx-slider-input>
412
+ ```
413
+
414
+ ### `none`
415
+
416
+ No marker rendered. Use when the value bar or lanes carry the visual.
417
+
418
+ ```html
419
+ <ixfx-slider-input marker-style="none" min="0" max="100" value-a="60"></ixfx-slider-input>
420
+ ```
421
+
422
+ ---
423
+
424
+ ## Data Lanes (programmatic API)
425
+
426
+ Lanes are additional line segments drawn over (or offset from) the track. The two built-in lanes are `track` (grey background) and `value` (accent progress bar). You can add custom lanes at runtime.
427
+
428
+ ### Lane types
429
+
430
+ | Type | Behaviour |
431
+ |---|---|
432
+ | `value` | Draws from `lane.valueMin` to `lane.valueMax` along the track. Update with `setLaneRange()` for ranges or `setLaneValue()` for a single position (single mode) |
433
+ | `fixed` | Always shows the full track — useful for static reference lines |
434
+ | `anchored` | Centred on the current value, with `lane.value` controlling its half-width (0..1 of total track) |
435
+
436
+ ### API
437
+
438
+ ```ts
439
+ import type { SliderInputElement } from 'ixfx-slider-input';
440
+
441
+ const slider = document.querySelector('ixfx-slider-input') as SliderInputElement;
442
+
443
+ await customElements.whenDefined('ixfx-slider-input');
444
+ await slider.updateComplete;
445
+
446
+ // Add a fixed reference line above the track
447
+ slider.addLane('target', {
448
+ type: 'fixed',
449
+ offset: 0.2, // 20% above the track centreline
450
+ width: 0.05, // thin line
451
+ });
452
+
453
+ // Add a value-type lane (will default to current value span)
454
+ slider.addLane('average', {
455
+ type: 'value',
456
+ value: 0.4, // initial 0..1 position
457
+ });
458
+
459
+ // Add an anchored tolerance band centred on the current value
460
+ slider.addLane('tolerance', {
461
+ type: 'anchored',
462
+ value: 0.15, // band spans ±15% of total track
463
+ });
464
+
465
+ // Update a value-type lane
466
+ slider.setLaneValue('average', 0.6);
467
+
468
+ // Update a value-type lane's range (dual/triple band)
469
+ slider.setLaneRange('average', 0.2, 0.8);
470
+
471
+ // Remove a lane
472
+ slider.removeLane('target');
473
+
474
+ // Read lane state
475
+ const lane = slider.getLane('average');
476
+ const allLanes = slider.getLanes(); // ReadonlyMap<string, LaneData>
477
+ ```
478
+
479
+ Style lanes via CSS variables using the lane key:
480
+
481
+ ```css
482
+ ixfx-slider-input {
483
+ --lane-average-color: hsl(120, 70%, 50%);
484
+ --lane-tolerance-color: hsl(180, 80%, 50%);
485
+ --lane-sweep-width: 0.06;
486
+ }
487
+ ```
488
+
489
+ ### Live lane animation example
490
+
491
+ From the demo — a sweep lane that oscillates between 0 and the current slider value:
492
+
493
+ ```ts
494
+ let direction = 1;
495
+ setInterval(() => {
496
+ const sweep = slider.getLane('sweep');
497
+ if (!sweep) return;
498
+ const lo = sweep.valueMin ?? 0;
499
+ let nextLo = lo + direction * 0.02;
500
+ if (nextLo >= 0.7 || nextLo <= 0.05) {
501
+ direction *= -1;
502
+ nextLo = Math.max(0.05, Math.min(0.7, nextLo));
503
+ }
504
+ slider.setLaneRange('sweep', nextLo, nextLo + 0.3);
505
+ }, 100);
506
+ ```
507
+
508
+ The lane's `offset` value (in `addLane({ offset })`) positions it perpendicular to the track. Positive values push the lane *away* from the centreline (above for horizontal, right of for vertical).
509
+
510
+ ---
511
+
512
+ ## Tooltip Integration
513
+
514
+ Set `data-tooltip` and `data-tooltip-live` to wire up a live tooltip that updates as the user drags:
515
+
516
+ ```html
517
+ <ixfx-slider-input
518
+ min="0" max="100" value-a="50"
519
+ data-tooltip="50%"
520
+ data-tooltip-anchor="top"
521
+ data-tooltip-live="input"
522
+ ></ixfx-slider-input>
523
+ ```
524
+
525
+ When `data-tooltip-live` is present the component automatically updates `data-tooltip` to the current percentage (e.g. `"63%"`) on every `input` event. In dual/triple modes the tooltip reflects the value at the max pole (parity with native `<input type="range">`).
526
+
527
+ ---
528
+
529
+ ## CSS Parts
530
+
531
+ Lane paths and marker groups are exposed as CSS shadow parts for styling from outside the shadow DOM:
532
+
533
+ ```css
534
+ ixfx-slider-input::part(lane) { … } /* all lanes */
535
+ ixfx-slider-input::part(lane-track) { … } /* track lane */
536
+ ixfx-slider-input::part(lane-value) { … } /* value lane */
537
+ ixfx-slider-input::part(lane-sweep) { … } /* a custom lane */
538
+ ixfx-slider-input::part(value-marker) { … } /* all handles' marker group */
539
+ ixfx-slider-input::part(value-marker-a) { … } /* dual/triple: A handle */
540
+ ixfx-slider-input::part(value-marker-b) { … } /* dual/triple: B handle */
541
+ ixfx-slider-input::part(value-marker-c) { … } /* triple: C handle */
542
+ ixfx-slider-input::part(midpoint-marker) { … } /* bipolar/unipolar centre tick */
543
+ ```
544
+
545
+ ---
546
+
547
+ ## Vertical Orientation
548
+
549
+ Set `orientation="vertical"`. The component swaps width and height (24×200 default) and inverts the click-to-set / drag direction so the **top of the track = high value** and the **bottom = low value**. The host's CSS dimensions need to match the new layout (typically swap width ↔ height and tweak one to match the visual thickness you want):
550
+
551
+ ```html
552
+ <ixfx-slider-input
553
+ orientation="vertical" min="0" max="100" value-a="50"
554
+ style="width: 24px; height: 200px;"
555
+ ></ixfx-slider-input>
556
+ ```
557
+
558
+ The track, value bar, and markers all use the same `CROSS` axis for their perpendicular dimension, so all CSS variables (e.g. `--marker-thickness`, `--lane-track-width`) work identically in both orientations.
559
+
560
+ ---
561
+
562
+ ## Styling Recipes
563
+
564
+ ### Purple theme
565
+
566
+ ```css
567
+ #my-slider {
568
+ --lane-value-color: hsl(280, 100%, 60%);
569
+ --lane-track-color: hsl(280, 30%, 30%);
570
+ --marker-color: hsl(280, 100%, 70%);
571
+ --marker-thickness: 0.3;
572
+ }
573
+ ```
574
+
575
+ ### Animated marching-ants track
576
+
577
+ ```css
578
+ @keyframes march {
579
+ to { --lane-dashoffset: -30; }
580
+ }
581
+ #my-slider {
582
+ --lane-dasharray: 20 10;
583
+ animation: march 1s linear infinite;
584
+ }
585
+ ```
586
+
587
+ ### Value-reactive hue (single mode)
588
+
589
+ ```html
590
+ <ixfx-slider-input
591
+ min="0" max="100" value-a="60"
592
+ style="--marker-color: hsl(calc((1 - var(--value-relative)) * 280), 80%, 55%);"
593
+ ></ixfx-slider-input>
594
+ ```
595
+
596
+ ### SVG gradient on the value bar
597
+
598
+ Define gradients in a hidden `<svg>` at the top of the page using `gradientUnits="userSpaceOnUse"` anchored to the slider's viewBox, then reference them via `--lane-value-color`:
599
+
600
+ ```html
601
+ <svg width="0" height="0" style="position:absolute" aria-hidden="true">
602
+ <defs>
603
+ <linearGradient id="slider-grad-h" gradientUnits="userSpaceOnUse" x1="5" y1="0" x2="95" y2="0">
604
+ <stop offset="0%" stop-color="oklch(70% 0.22 25)" />
605
+ <stop offset="100%" stop-color="oklch(75% 0.22 145)" />
606
+ </linearGradient>
607
+ </defs>
608
+ </svg>
609
+ <ixfx-slider-input
610
+ min="0" max="100" value-a="60"
611
+ style="--lane-value-color: url(#slider-grad-h);"
612
+ ></ixfx-slider-input>
613
+ ```
614
+
615
+ ---
616
+
617
+ ## Implementation Notes
618
+
619
+ These are lessons learned from building the component — useful context when building a similar thing from scratch.
620
+
621
+ ### SVG in a fixed `100×24` viewBox
622
+
623
+ All geometry is computed in a fixed `100×24` viewBox coordinate space for horizontal, or `24×100` for vertical, with the track running along the long axis. The `CROSS = 24` constant sets the perpendicular dimension, and `LONG = 100` sets the long axis. The SVG `viewBox` attribute handles scaling to actual DOM pixels, so every calculation stays coordinate-system-agnostic. The path is always `(viewBox-relative) × (host size / viewBox size)` regardless of the host's rendered size.
624
+
625
+ ### Lane offset needs a CSS unit
626
+
627
+ `transform: translateY(${offset})` (unitless) is **invalid** CSS — a `<length>` is required, otherwise the whole `transform` declaration is dropped and the lane renders on the centreline. The code uses `translateY(${offset * CROSS / 2}px)` so the offset is expressed in viewBox units (with `px` suffix for CSS), making the offset stable across host sizes.
628
+
629
+ ### Marker rendering uses `part` + per-handle CSS variable patterns
630
+
631
+ Each marker group has `part="value-marker"` (or `value-marker-<handle>` in multi modes) and reads its colour from a `var(--marker-<handle>-color, var(--marker-color, var(--accent)))` chain. The two-tier fallback means `--marker-color` styles all markers globally, while `--marker-a-color` / `--marker-b-color` / `--marker-c-color` can override per handle. The same pattern is used for the marker-style override (`--marker-style-c`) and for lane widths (`--lane-<key>-width`).
632
+
633
+ The `<defs>` + `<use>` pattern with `markerBorderWidth > 0` produces a halo: the shape is pushed into a single root `<defs>` (so it's not rendered twice) and referenced twice via `<use>` — once as `class="marker-border"` (slightly thicker stroke) and once as `class="marker-fill"` (the actual colour). The same approach handles both needle (`<path>`) and dot/triangle (`<circle>` / `<polygon>`) shapes.
634
+
635
+ ### `#quantise` runs at the set-value boundary
636
+
637
+ `#quantise(value)` is the single internal entry point for snapping: it applies `stops` first (priority), then `step` (if `stops` not set), and returns the raw value otherwise. Because every value-setting path (`#setSingleValueFromRelative`, `#setDualOrTripleValueFromRelative`, `#stepHandleByRelativeDelta`, `#applyLengthDragFromRelative`) funnels through it, snapping is universal — drag, click, keyboard, wheel, and length-drag all quantise identically.
638
+
639
+ ### WillUpdate initialises the value lane before the first render
640
+
641
+ In multi-handle modes, the internal `#poleA` / `#poleB` / `#poleC` must be in sync before the first paint. This is done in `willUpdate()` (not the constructor) because `@property` values set from HTML attributes are not yet parsed at construction time. The triple-mode `valueC` defaults to the midpoint of A and B, matching `radial-input`'s deferred lane-correction pattern.
642
+
643
+ ### `value-c-follow="proportional"` keeps C anchored to the band
644
+
645
+ When A or B moves, the C handle's fractional position within the *old* band is recomputed against the *new* band, so C tracks the band automatically across continuous drags. Because `#resolvePoleChange` and `#applyLengthDragFromRelative` both use the same `oldLo/oldHi → newLo/newHi` formula, the behaviour is consistent across pointer-drag, keyboard nudges, wheel, and length-drag.
646
+
647
+ ### Length-drag preserves the grab offset
648
+
649
+ The bar between A and B in dual/triple modes is draggable when `value-drag !== "none"`. `#applyLengthDragFromRelative` computes the delta against `#lengthDragStartRelative` (the host-relative position at `pointerdown`), not against the low edge. Without this, the low edge would jump under the cursor on grab. The same `#lengthDragStartRelative` is captured in `#onPointerDown` when `chosenHandle === 'length'`.
650
+
651
+ ### Dead-zone clicks are no-ops when `value-drag` is enabled
652
+
653
+ In `#onPointerDown`'s multi-handle hit-testing, the "click outside the band jumps the nearest handle" branches are gated on `valueDrag === "none"`. When `value-drag` is on, the only interactions that change values are grabbing a handle directly or grabbing the bar itself. This makes the click target clear: outside the band does nothing.
654
+
655
+ ### Midpoint marker only renders in bipolar / unipolar single mode
656
+
657
+ `#renderMidpointMarker()` is a no-op unless `valueMode === "single"` and `singleStyle` is `"bipolar"` or `"unipolar"`. In those modes a small tick is drawn at the track centre to anchor the symmetric value bar.
658
+
659
+ ### Unipolar shows two markers (low + high end)
660
+
661
+ For unipolar single mode, `#renderValueMarker` computes a `lowCenter` reflection of the high end across `bounds.centre` and emits a second marker shape at that position. The low-end marker uses the same `--marker-color` / `--marker-thickness` / `--marker-length` as the high end, so the existing CSS customisation patterns work unchanged.
662
+
663
+ ### Unipolar click-to-set uses a V-shape mapping
664
+
665
+ `#onPointerDown` and `#onPointerMove` remap the host-relative click position to the value via `Math.abs(2 * relativeValue - 1)` when in unipolar single mode: 0 at the centre, 1 at either end. So clicking 25% in from the left or right sets value 0.5, and clicking at the centre sets value 0. The mapping is *not* applied to cursor hit-testing (the cursor logic uses the raw host-relative position so it can find the low/high markers at `0.5 ± value/2`).
666
+
667
+ ### `disconnectedCallback` cleans up timers and global listeners
668
+
669
+ The debounce timer and the `pointermove` / `pointerup` / `pointercancel` window listeners are both cleaned up in `disconnectedCallback`. The pointer cleanup is achieved by calling `#onPointerUp()` directly, which resets `#dragging`, releases pointer capture, and removes the window listeners. Forgetting this causes a memory leak and a stuck-dragging bug if an element is removed mid-drag.
670
+
671
+ ### Wheel handling is platform-agnostic
672
+
673
+ The `WheelNudgeMixin` consults `event.deltaY` first, falling back to `event.deltaX` when `deltaY` is 0. This matters on Mac, where shift+scroll is interpreted as horizontal scrolling and the browser sends `deltaX` with `deltaY=0`; without the fallback, shift+wheel on Mac would always read direction as `-1` (because `0 < 0` is false). The slider's `_applyWheelDelta` then inverts the sign so that "wheel forward = value up" works on both Windows and Mac.