@agencecinq/slider 6.0.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/README.md ADDED
@@ -0,0 +1,473 @@
1
+ [![](https://img.shields.io/npm/v/@agencecinq/slider)](https://www.npmjs.com/package/@agencecinq/slider)
2
+ [![](https://img.shields.io/npm/dm/@agencecinq/slider)](https://www.npmjs.com/package/@agencecinq/slider)
3
+
4
+ # @agencecinq/slider
5
+
6
+ > Accessible, WAI-ARIA slider as a lightweight Web Component.
7
+
8
+ A slider lets users pick a value within a range by moving a thumb along a rail.
9
+ `<cinq-slider>` is the **rail** (geometry + CSS variables). A nested
10
+ `[role="slider"]` thumb is the focusable control: ARIA bounds, labels, and
11
+ keyboard focus live there.
12
+
13
+ Implementation follows the
14
+ [WAI-ARIA Authoring Practices slider pattern](https://www.w3.org/WAI/ARIA/apg/patterns/slider/).
15
+ Inspired by [`@19h47/slider`](https://github.com/19h47/19h47-slider).
16
+
17
+ Two custom elements in one package:
18
+
19
+ | Tag | Pattern | Class |
20
+ | --- | ------- | ----- |
21
+ | `<cinq-slider>` | [Single-thumb slider](https://www.w3.org/WAI/ARIA/apg/patterns/slider/) | `Slider` |
22
+ | `<cinq-slider-range>` | [Multi-thumb range](https://www.w3.org/WAI/ARIA/apg/patterns/slider-multithumb/) | `SliderRange` |
23
+
24
+ Both support **horizontal** and **vertical** layout (`aria-orientation` on the thumb). Horizontal sliders mirror under RTL; vertical axis is unchanged by `dir`.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pnpm add @agencecinq/slider
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```html
35
+ <label id="volume-label" for="volume-slider">Volume</label>
36
+ <cinq-slider class="volume-rail">
37
+ <button
38
+ type="button"
39
+ id="volume-slider"
40
+ role="slider"
41
+ tabindex="0"
42
+ aria-labelledby="volume-label"
43
+ aria-orientation="horizontal"
44
+ aria-valuemin="0"
45
+ aria-valuemax="100"
46
+ aria-valuenow="50"
47
+ aria-valuetext="50%"
48
+ ></button>
49
+ </cinq-slider>
50
+ ```
51
+
52
+ ```js
53
+ import "@agencecinq/slider";
54
+ ```
55
+
56
+ Importing `@agencecinq/slider` registers **both** custom elements automatically.
57
+ No manual `init()` call required.
58
+
59
+ > **HTML is the source of truth.** The component will not auto-set `role`,
60
+ > auto-migrate attributes, or warn about missing labels. Use an a11y linter
61
+ > (axe-core, Lighthouse) to catch invalid markup.
62
+
63
+ The package implements APG behaviour (keyboard, pointer, ARIA sync, events).
64
+ **Layout, hit area, contrast, and focus rings are your CSS.** See
65
+ [Consumer styling](#consumer-styling) below.
66
+
67
+ ### Required markup, single (`<cinq-slider>`)
68
+
69
+ Author contract (not enforced in JS, use an a11y linter). The component **does** throw if thumb count ≠ 1.
70
+
71
+ | Attribute / element | Expected | Role |
72
+ | ------------------- | -------- | ---- |
73
+ | `<cinq-slider>` | **Yes** | Rail / bounds container. |
74
+ | One `[role="slider"]` | **Yes** | Focusable thumb (`$thumb`). |
75
+ | `tabindex="0"` | **Yes** | On the thumb. |
76
+ | `aria-orientation="horizontal"` or `"vertical"` | **Yes** | On the thumb. Geometry follows the declared axis. |
77
+ | `aria-valuemin` / `aria-valuemax` / `aria-valuenow` | **Yes** | Read with fallbacks (`0` / `100`) if omitted. |
78
+ | `aria-label` or `aria-labelledby` | **Yes** | On the thumb, per APG. |
79
+ | `aria-valuetext` | Recommended | Updated by the component when `formatValue` is set. |
80
+
81
+ Prefer `<button type="button">` for a clear focus target.
82
+
83
+ ### Range (`<cinq-slider-range>`)
84
+
85
+ APG order: **first thumb in DOM = max handle**, **second = min handle**.
86
+
87
+ ```html
88
+ <label id="price-label">Price range</label>
89
+ <cinq-slider-range class="price-rail" aria-labelledby="price-label">
90
+ <button
91
+ type="button"
92
+ role="slider"
93
+ tabindex="0"
94
+ aria-label="Maximum price"
95
+ aria-orientation="horizontal"
96
+ aria-valuemin="100"
97
+ aria-valuemax="1000"
98
+ aria-valuenow="800"
99
+ ></button>
100
+ <button
101
+ type="button"
102
+ role="slider"
103
+ tabindex="0"
104
+ aria-label="Minimum price"
105
+ aria-orientation="horizontal"
106
+ aria-valuemin="100"
107
+ aria-valuemax="1000"
108
+ aria-valuenow="200"
109
+ ></button>
110
+ </cinq-slider-range>
111
+ ```
112
+
113
+ Range track fill between thumbs:
114
+
115
+ ```css
116
+ cinq-slider-range::after {
117
+ inset-inline-start: calc(
118
+ var(--thumb-size) / 2 + var(--min-ratio) * (100% - var(--thumb-size))
119
+ );
120
+ width: calc(
121
+ (var(--max-ratio) - var(--min-ratio)) * (100% - var(--thumb-size))
122
+ );
123
+ border-radius: 0; /* square ends between two handles */
124
+ }
125
+ ```
126
+
127
+ See [Positioning with CSS variables](#positioning-with-css-variables) for the full pattern (rail, fill, thumbs).
128
+
129
+ ### Options
130
+
131
+ | Attribute | Type | Default | Description |
132
+ | --------- | ---- | ------- | ----------- |
133
+ | `data-step` | number | `1` | Arrow key increment; values snap to this step (pointer + keyboard). |
134
+ | `data-page` | number | `10` | Page Up / Page Down increment. |
135
+
136
+ Thumb and track layout are **consumer CSS**. The component only updates CSS custom properties on the host (see [Styling hooks](#styling-hooks)).
137
+
138
+ ### API, `<cinq-slider>` (`Slider`)
139
+
140
+ | Method | Description |
141
+ | ------ | ----------- |
142
+ | `setValue(value, options?)` | Clamp, sync, dispatch unless `emit: false`. |
143
+ | `sync()` | Re-read ARIA and re-apply geometry. |
144
+ | `rect()` | Host `DOMRect` (rail). |
145
+ | `init()` / `destroy()` | Bind / unbind for DOM mutation. |
146
+
147
+ | Property | Description |
148
+ | -------- | ----------- |
149
+ | `$thumb` | The `[role="slider"]` element. |
150
+ | `value` / `min` / `max` / `ratio` | From thumb ARIA. |
151
+ | `step` / `page` | From host `data-step` / `data-page`. |
152
+ | `formatValue` | `(value) => string` for `aria-valuetext`. |
153
+
154
+ ### API, `<cinq-slider-range>` (`SliderRange`)
155
+
156
+ | Method | Description |
157
+ | ------ | ----------- |
158
+ | `setValues(min, max, options?)` | Anti-cross clamp, sync, dispatch unless `emit: false`. |
159
+ | `sync()` | Re-read ARIA and re-apply geometry. |
160
+ | `rect()` | Host `DOMRect` (rail). |
161
+ | `init()` / `destroy()` | Bind / unbind for DOM mutation. |
162
+
163
+ | Property | Description |
164
+ | -------- | ----------- |
165
+ | `$max` / `$min` | Thumbs (first / second in DOM). |
166
+ | `min` / `max` | Current selected values. |
167
+ | `boundsMin` / `boundsMax` | Global bounds from initial markup. |
168
+ | `step` / `page` | From host `data-step` / `data-page`. |
169
+ | `formatValue` | `(value) => string` for both thumbs. |
170
+
171
+ ### Keyboard
172
+
173
+ Matches [APG slider keyboard interaction](https://www.w3.org/WAI/ARIA/apg/patterns/slider/#keyboard-interaction).
174
+ Focus must be on the thumb.
175
+
176
+ **Range (`<cinq-slider-range>`):** both thumbs are in the tab sequence (APG
177
+ multi-thumb). Use **Tab** / **Shift+Tab** to move focus between the max handle
178
+ (first in DOM) and the min handle (second). Arrow keys adjust the **focused**
179
+ thumb only. Tab order follows DOM order, not visual position when handles
180
+ cross. Style `:focus-visible` and raise `z-index` on the focused thumb so the
181
+ ring stays visible (see playground `[data-active]` pattern).
182
+
183
+ **Vertical:** Up Arrow increases, Down Arrow decreases. **RTL does not apply**
184
+ to the vertical axis (same as `@19h47/slider`).
185
+
186
+ | Key | Horizontal LTR | Horizontal RTL | Vertical |
187
+ | --- | -------------- | -------------- | -------- |
188
+ | Right Arrow | + `data-step` | − `data-step` | + `data-step` |
189
+ | Left Arrow | − `data-step` | + `data-step` | − `data-step` |
190
+ | Up Arrow | + `data-step` | + `data-step` | + `data-step` |
191
+ | Down Arrow | − `data-step` | − `data-step` | − `data-step` |
192
+ | Home / End | min / max (single); APG multi-thumb rules (range) | same | same |
193
+ | Page Up / Down | + / − `data-page` | same | same |
194
+
195
+ ### RTL
196
+
197
+ Set `dir="rtl"` on the host or an ancestor for **horizontal** sliders. Thumb
198
+ position, pointer mapping, and horizontal arrow keys mirror (same as
199
+ `@agencecinq/tabs`). Vertical sliders ignore `direction` on the value axis.
200
+
201
+ ### Events
202
+
203
+ Both hosts fire `slider:change` (constant `EVENTS.SLIDER_CHANGE`) after ARIA sync.
204
+
205
+ Detail: `{ min, max, $thumb }`. Single thumb: `min === max`. Range: current
206
+ endpoints. `$thumb` is always the handle that moved or has focus.
207
+
208
+ ```js
209
+ import { EVENTS } from "@agencecinq/utils";
210
+
211
+ document.querySelector("cinq-slider")?.addEventListener(
212
+ EVENTS.SLIDER_CHANGE,
213
+ (event) => {
214
+ const { min, max, $thumb } = event.detail;
215
+ console.log(min, max, $thumb);
216
+ },
217
+ );
218
+
219
+ document.querySelector("cinq-slider-range")?.addEventListener(
220
+ EVENTS.SLIDER_CHANGE,
221
+ (event) => {
222
+ const { min, max, $thumb } = event.detail;
223
+ const range = event.currentTarget;
224
+ const which =
225
+ $thumb === range.$min ? "min" : $thumb === range.$max ? "max" : null;
226
+ console.log(min, max, which);
227
+ },
228
+ );
229
+ ```
230
+
231
+ ### Styling hooks
232
+
233
+ | Hook | Host | Description |
234
+ | ---- | ---- | ----------- |
235
+ | `[dragging]` | Both | Pointer drag in progress |
236
+ | `--value` / `--ratio` | `<cinq-slider>` | Current value and semantic ratio `0` to `1` |
237
+ | `--min` / `--max` | `<cinq-slider-range>` | Current range values |
238
+ | `--min-ratio` / `--max-ratio` | `<cinq-slider-range>` | Semantic ratios `0` to `1` within global bounds |
239
+ | `[data-active="min"\|"max"]` | `<cinq-slider-range>` | Last focused / dragged thumb |
240
+ | `[data-collapsed]` | `<cinq-slider-range>` | Set when `min === max` (side-by-side thumb CSS) |
241
+
242
+ Ratios are **semantic**: `0` = global minimum, `1` = global maximum, unchanged in RTL. Use **logical properties** (`inset-inline-start`, etc.) so layout mirrors with `dir="rtl"`.
243
+
244
+ ### Positioning with CSS variables
245
+
246
+ The package never positions thumbs or track in JS. It updates ARIA and sets the variables above on the **host**. You style the rail, fill, and thumbs in your stylesheet.
247
+
248
+ Define a thumb size on the host (name is yours, the lib does not set it):
249
+
250
+ ```css
251
+ cinq-slider,
252
+ cinq-slider-range {
253
+ position: relative;
254
+ --thumb-size: 2.75rem; /* hit area width/height of [role="slider"] */
255
+ }
256
+ ```
257
+
258
+ The usable track is shorter than the host by one thumb (single) or two thumbs (range). Inset the rail and map ratios over `(100% - var(--thumb-size))` (or `2 * var(--thumb-size)` for range).
259
+
260
+ #### Single thumb (`<cinq-slider>`)
261
+
262
+ **Variables:** `--value` (number), `--ratio` (`0` to `1`).
263
+
264
+ **Track rail** (full width, gray):
265
+
266
+ ```css
267
+ cinq-slider::before {
268
+ content: "";
269
+ position: absolute;
270
+ inset-inline: calc(var(--thumb-size) / 2);
271
+ top: 50%;
272
+ block-size: 0.375rem;
273
+ border-radius: 9999px;
274
+ background: #e5e7eb;
275
+ transform: translateY(-50%);
276
+ pointer-events: none;
277
+ }
278
+ ```
279
+
280
+ **Track fill** (from start to current value):
281
+
282
+ ```css
283
+ cinq-slider::after {
284
+ content: "";
285
+ position: absolute;
286
+ inset-inline-start: calc(var(--thumb-size) / 2);
287
+ top: 50%;
288
+ width: calc(var(--ratio) * (100% - var(--thumb-size)));
289
+ block-size: 0.375rem;
290
+ border-radius: 9999px;
291
+ background: #111827;
292
+ transform: translateY(-50%);
293
+ pointer-events: none;
294
+ }
295
+ ```
296
+
297
+ **Thumb** (centered on ratio):
298
+
299
+ ```css
300
+ cinq-slider [role="slider"] {
301
+ position: absolute;
302
+ top: calc(50% - var(--thumb-size) / 2);
303
+ width: var(--thumb-size);
304
+ height: var(--thumb-size);
305
+ inset-inline-start: calc(var(--ratio) * (100% - var(--thumb-size)));
306
+ }
307
+ ```
308
+
309
+ Alternative: keep `inset-inline-start: 0` and use `translate: calc(var(--ratio) * (100% - var(--thumb-size))) 0`.
310
+
311
+ #### Range (`<cinq-slider-range>`)
312
+
313
+ **Variables:** `--min`, `--max`, `--min-ratio`, `--max-ratio`.
314
+
315
+ APG DOM order: **`:first-child` = max handle**, **`:last-child` = min handle**.
316
+
317
+ **Track fill** (segment between the two values):
318
+
319
+ ```css
320
+ cinq-slider-range::after {
321
+ content: "";
322
+ position: absolute;
323
+ inset-inline-start: calc(
324
+ var(--thumb-size) / 2 + var(--min-ratio) * (100% - var(--thumb-size))
325
+ );
326
+ top: 50%;
327
+ width: calc(
328
+ (var(--max-ratio) - var(--min-ratio)) * (100% - var(--thumb-size))
329
+ );
330
+ block-size: 0.375rem;
331
+ border-radius: 0;
332
+ background: #111827;
333
+ transform: translateY(-50%);
334
+ pointer-events: none;
335
+ }
336
+ ```
337
+
338
+ **Thumbs:**
339
+
340
+ ```css
341
+ cinq-slider-range [role="slider"]:first-child {
342
+ inset-inline-start: calc(var(--max-ratio) * (100% - var(--thumb-size)));
343
+ z-index: 2;
344
+ }
345
+
346
+ cinq-slider-range [role="slider"]:last-child {
347
+ inset-inline-start: calc(var(--min-ratio) * (100% - var(--thumb-size)));
348
+ }
349
+ ```
350
+
351
+ **Collapsed range** (`min === max`): the host gets `[data-collapsed]`. Offset the min
352
+ handle by one thumb width on the low side so both handles stay visible and
353
+ selectable (consumer CSS):
354
+
355
+ ```css
356
+ cinq-slider-range[data-collapsed] [role="slider"]:last-child {
357
+ inset-inline-start: calc(
358
+ var(--min-ratio) * (100% - var(--thumb-size)) - var(--thumb-size)
359
+ );
360
+ }
361
+
362
+ [dir="rtl"] cinq-slider-range[data-collapsed] [role="slider"]:last-child {
363
+ inset-inline-start: calc(
364
+ var(--min-ratio) * (100% - var(--thumb-size)) + var(--thumb-size)
365
+ );
366
+ }
367
+
368
+ cinq-slider-range:has([aria-orientation="vertical"])[data-collapsed] [role="slider"]:last-child {
369
+ inset-block-end: calc(
370
+ var(--min-ratio) * (100% - var(--thumb-size)) - var(--thumb-size)
371
+ );
372
+ }
373
+ ```
374
+
375
+ When handles differ, raise the active thumb with `[data-active]`:
376
+
377
+ ```css
378
+ cinq-slider-range[data-active="min"] [role="slider"]:last-child,
379
+ cinq-slider-range[data-active="max"] [role="slider"]:first-child {
380
+ z-index: 3;
381
+ }
382
+ ```
383
+
384
+ #### Vertical, `<cinq-slider>`
385
+
386
+ Semantic `--ratio` is unchanged (`0` = min, `1` = max). Map along the block
387
+ axis with min at the bottom and max at the top (see **Potion potency** in the playground):
388
+
389
+ ```css
390
+ cinq-slider:has([aria-orientation="vertical"]) {
391
+ width: var(--thumb-size);
392
+ height: 12rem;
393
+ }
394
+
395
+ cinq-slider:has([aria-orientation="vertical"])::before {
396
+ inset-block: calc(var(--thumb-size) / 2);
397
+ inset-inline-start: 50%;
398
+ inline-size: 0.375rem;
399
+ transform: translateX(-50%);
400
+ }
401
+
402
+ cinq-slider:has([aria-orientation="vertical"])::after {
403
+ inset-inline-start: 50%;
404
+ inset-block-end: calc(var(--thumb-size) / 2);
405
+ block-size: calc(var(--ratio) * (100% - var(--thumb-size)));
406
+ inline-size: 0.375rem;
407
+ transform: translateX(-50%);
408
+ }
409
+
410
+ cinq-slider [aria-orientation="vertical"] {
411
+ inset-inline-start: calc(50% - var(--thumb-size) / 2);
412
+ inset-block-end: calc(var(--ratio) * (100% - var(--thumb-size)));
413
+ }
414
+ ```
415
+
416
+ Range vertical thumbs and fill use `--min-ratio` / `--max-ratio` on
417
+ `inset-block-end` the same way. See the docs playground **HP comfort band** demo.
418
+
419
+ #### Interaction hooks
420
+
421
+ ```css
422
+ cinq-slider[dragging] [role="slider"],
423
+ cinq-slider-range[dragging] [role="slider"] {
424
+ cursor: grabbing;
425
+ }
426
+
427
+ cinq-slider [role="slider"]:focus-visible,
428
+ cinq-slider-range [role="slider"]:focus-visible {
429
+ outline: 2px solid currentColor;
430
+ outline-offset: 3px;
431
+ }
432
+ ```
433
+
434
+ Lead offsets for multi-thumb pointer mapping follow `@19h47/slider` (horizontal:
435
+ max thumb; vertical: min thumb). The [interactive docs](https://agencecinq.github.io/ui/components/slider/) playground is the reference implementation.
436
+
437
+ ### Consumer styling
438
+
439
+ The component does **not** enforce thumb size, contrast, or touch targets.
440
+
441
+ - **Touch targets:** WCAG recommends at least **44×44 CSS px** for pointer
442
+ targets. You can use a large hit area with a smaller visual knob (`::before`).
443
+ - **Focus:** use `:focus-visible` on the thumb.
444
+ - **Touch AT:** the APG [warns](https://www.w3.org/WAI/ARIA/apg/patterns/slider/)
445
+ that some touch-based assistive technologies may not yet synthesize the key
446
+ events custom sliders need. Test with VoiceOver / TalkBack on real devices.
447
+
448
+ ### `formatValue`
449
+
450
+ ```js
451
+ const host = document.querySelector("cinq-slider");
452
+ host.formatValue = (value) => `${value}%`;
453
+ ```
454
+
455
+ ### Runtime DOM mutation
456
+
457
+ ```js
458
+ host.destroy();
459
+ // mutate light DOM...
460
+ host.init();
461
+ ```
462
+
463
+ ## Build setup
464
+
465
+ ```bash
466
+ pnpm -C packages/slider build
467
+ ```
468
+
469
+ ## Acknowledgments
470
+
471
+ - [WAI-ARIA Slider Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/slider/)
472
+ - [`@19h47/slider`](https://github.com/19h47/19h47-slider)
473
+ - [Interactive docs](https://agencecinq.github.io/ui/components/slider/)
@@ -0,0 +1,19 @@
1
+ import { ActiveThumb } from './types.js';
2
+ /** Semantic ratio in `[0, 1]` from value and bounds. */
3
+ export declare const ratioFromValue: (value: number, min: number, max: number) => number;
4
+ /** Value from semantic ratio. */
5
+ export declare const valueFromRatio: (ratio: number, min: number, max: number) => number;
6
+ /** Snap a value to the nearest step within `[min, max]`. */
7
+ export declare const snapToStep: (value: number, min: number, max: number, step: number) => number;
8
+ /** Single-thumb track length. */
9
+ export declare const singleTrackLength: (railLength: number, thumbSize: number) => number;
10
+ /** Multi-thumb track length (two thumbs). */
11
+ export declare const multiTrackLength: (railLength: number, thumbSize: number) => number;
12
+ /** Map pointer position to semantic ratio (single thumb). */
13
+ export declare const ratioFromPointer: (clientCoord: number, rect: DOMRect, thumbSize: number, rtl: boolean, vertical: boolean) => number;
14
+ /**
15
+ * Map pointer position to value (multi-thumb, horizontal or vertical).
16
+ * `clientCoord` is `clientX` or `clientY`. RTL mirrors the horizontal axis.
17
+ */
18
+ export declare const multiValueFromPointer: (clientCoord: number, rect: DOMRect, which: ActiveThumb, gmin: number, gmax: number, track: number, thumbSize: number, rtl: boolean, vertical: boolean) => number;
19
+ //# sourceMappingURL=geometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.d.ts","sourceRoot":"","sources":["../src/geometry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,wDAAwD;AACxD,eAAO,MAAM,cAAc,GACzB,OAAO,MAAM,EACb,KAAK,MAAM,EACX,KAAK,MAAM,KACV,MAGF,CAAC;AAEF,iCAAiC;AACjC,eAAO,MAAM,cAAc,GACzB,OAAO,MAAM,EACb,KAAK,MAAM,EACX,KAAK,MAAM,KACV,MAAmC,CAAC;AAEvC,4DAA4D;AAC5D,eAAO,MAAM,UAAU,GACrB,OAAO,MAAM,EACb,KAAK,MAAM,EACX,KAAK,MAAM,EACX,MAAM,MAAM,KACX,MAOF,CAAC;AAEF,iCAAiC;AACjC,eAAO,MAAM,iBAAiB,GAC5B,YAAY,MAAM,EAClB,WAAW,MAAM,KAChB,MAA6C,CAAC;AAEjD,6CAA6C;AAC7C,eAAO,MAAM,gBAAgB,GAC3B,YAAY,MAAM,EAClB,WAAW,MAAM,KAChB,MAAiD,CAAC;AAoBrD,6DAA6D;AAC7D,eAAO,MAAM,gBAAgB,GAC3B,aAAa,MAAM,EACnB,MAAM,OAAO,EACb,WAAW,MAAM,EACjB,KAAK,OAAO,EACZ,UAAU,OAAO,KAChB,MAaF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,GAChC,aAAa,MAAM,EACnB,MAAM,OAAO,EACb,OAAO,WAAW,EAClB,MAAM,MAAM,EACZ,MAAM,MAAM,EACZ,OAAO,MAAM,EACb,WAAW,MAAM,EACjB,KAAK,OAAO,EACZ,UAAU,OAAO,KAChB,MAaF,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { Slider } from './slider.js';
2
+ export { SliderRange } from './slider-range.js';
3
+ export type { ActiveThumb, Detail, FormatValue, SetValueOptions, SetValuesOptions, SliderChangeDetail, } from './types.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AACrB,OAAO,mBAAmB,CAAC;AAE3B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EACV,WAAW,EACX,MAAM,EACN,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,387 @@
1
+ //#region ../utils/dist/index.js
2
+ var e = {
3
+ DRAWER_BEFORE_CLOSE: "drawer:before-close",
4
+ DRAWER_BEFORE_OPEN: "drawer:before-open",
5
+ DRAWER_CLOSE: "drawer:close",
6
+ DRAWER_OPEN: "drawer:open",
7
+ DRAWER_TOGGLE: "drawer:toggle",
8
+ MODAL_BEFORE_CLOSE: "modal:before-close",
9
+ MODAL_BEFORE_OPEN: "modal:before-open",
10
+ MODAL_CLOSE: "modal:close",
11
+ MODAL_OPEN: "modal:open",
12
+ MODAL_TOGGLE: "modal:toggle",
13
+ SPINBUTTON_CHANGE: "spinbutton:change",
14
+ DISCLOSURE_BUTTON_OPEN: "disclosure-button:open",
15
+ DISCLOSURE_BUTTON_CLOSE: "disclosure-button:close",
16
+ SWITCH_ACTIVATE: "switch:activate",
17
+ SWITCH_DEACTIVATE: "switch:deactivate",
18
+ ACCORDION_OPEN: "accordion:open",
19
+ ACCORDION_CLOSE: "accordion:close",
20
+ COMBOBOX_LOADING: "combobox:loading",
21
+ COMBOBOX_LOADED: "combobox:loaded",
22
+ COMBOBOX_UPDATE: "combobox:update",
23
+ COMBOBOX_SUBMIT: "combobox:submit",
24
+ COMBOBOX_EMPTY: "combobox:empty",
25
+ WINDOWSPLITTER_CHANGE: "windowsplitter:change",
26
+ SLIDER_CHANGE: "slider:change",
27
+ CALENDAR_CHANGE: "calendar:change",
28
+ TABS_BEFORE_ACTIVATE: "tabs:before-activate",
29
+ TABS_ACTIVATE: "tabs:activate",
30
+ TABS_DELETE: "tabs:delete",
31
+ CART_BEFORE_ADD: "cart:before-add",
32
+ CART_BEFORE_UPDATE: "cart:before-update",
33
+ CART_UPDATE: "cart:update",
34
+ VARIANT_CHANGE: "variant:change"
35
+ }, t = (e, t, n, r = {}) => {
36
+ let { bubbles: i = !0, cancelable: a = !0 } = r;
37
+ return e.dispatchEvent(new CustomEvent(t, {
38
+ bubbles: i,
39
+ cancelable: a,
40
+ detail: n
41
+ }));
42
+ }, n = (e, t) => {
43
+ if (e == null || e === "") return t;
44
+ let n = Number(e);
45
+ return Number.isFinite(n) ? n : t;
46
+ }, r = (e, t) => {
47
+ let n = null, r = null, i = () => {
48
+ r && e(...r), n = null;
49
+ };
50
+ return (...e) => {
51
+ r = e, n ||= setTimeout(i, t);
52
+ };
53
+ }, i = document.documentElement, { body: a } = document;
54
+ i.hasAttribute("data-debug"), window.addEventListener("pointermove", r(({ x: e, y: t }) => {}, 100), { passive: !0 }), window.matchMedia("(width >= 64rem)"), window.matchMedia("(min-width: 1280px)"), window.matchMedia("(min-width: 1440px)"), window.matchMedia("(min-width: 1920px)");
55
+ var o = (e, t, n) => Math.min(Math.max(e, t), n), s = (e, t, n) => {
56
+ let r = n - t;
57
+ return r > 0 ? (e - t) / r : 0;
58
+ }, c = (e, t, n) => t + e * (n - t), l = (e, t, n, r) => o(r <= 0 ? e : t + Math.round((e - t) / r) * r, t, n), u = (e, t) => Math.max(0, e - t), d = (e, t) => Math.max(0, e - 2 * t), f = (e, t, n, r) => r || n ? e === "min" ? t : 0 : e === "max" ? t : 0, p = (e, t, n, r, i) => {
59
+ let a = i ? t.height : t.width, s = i ? t.top : t.left, c = u(a, n);
60
+ if (c <= 0) return 0;
61
+ let l = o((e - s - n / 2) / c, 0, 1);
62
+ return i || r ? 1 - l : l;
63
+ }, m = (e, t, n, r, i, a, s, c, l) => {
64
+ let u = f(n, s, c, l), d = o(e - (l ? t.top : t.left) - u, 0, a), p = a > 0 ? d / a : 0, m = i - r;
65
+ return Math.round(l || c ? i - m * p : r + m * p);
66
+ }, h = class {
67
+ #e;
68
+ #t;
69
+ #n;
70
+ #r;
71
+ constructor(e, t, n, r = !1) {
72
+ this.#e = e, this.#t = t, this.#n = n, this.#r = r;
73
+ }
74
+ delta(e) {
75
+ let t = this.#e, n = -this.#e, r = this.#t, i = -this.#t;
76
+ if (this.#r) {
77
+ let r = {
78
+ ArrowUp: t,
79
+ ArrowDown: n,
80
+ ArrowRight: t,
81
+ ArrowLeft: n
82
+ };
83
+ if (e in r) return {
84
+ delta: r[e],
85
+ preventDefault: !0
86
+ };
87
+ } else {
88
+ let r = this.#n ? {
89
+ ArrowLeft: t,
90
+ ArrowRight: n,
91
+ ArrowUp: t,
92
+ ArrowDown: n
93
+ } : {
94
+ ArrowRight: t,
95
+ ArrowLeft: n,
96
+ ArrowUp: t,
97
+ ArrowDown: n
98
+ };
99
+ if (e in r) return {
100
+ delta: r[e],
101
+ preventDefault: !0
102
+ };
103
+ }
104
+ return e === "PageUp" ? {
105
+ delta: r,
106
+ preventDefault: !0
107
+ } : e === "PageDown" ? {
108
+ delta: i,
109
+ preventDefault: !0
110
+ } : e === "Home" || e === "End" ? {
111
+ delta: 0,
112
+ preventDefault: !0
113
+ } : null;
114
+ }
115
+ static isHome(e) {
116
+ return e === "Home";
117
+ }
118
+ static isEnd(e) {
119
+ return e === "End";
120
+ }
121
+ }, g = class extends HTMLElement {
122
+ static observedAttributes = ["data-step", "data-page"];
123
+ step = 1;
124
+ page = 10;
125
+ #e;
126
+ #t = null;
127
+ #n = null;
128
+ onThumbFocus(e) {}
129
+ get formatValue() {
130
+ return this.#e;
131
+ }
132
+ set formatValue(e) {
133
+ this.#e = e, this.syncValuetext();
134
+ }
135
+ get isRtl() {
136
+ return getComputedStyle(this).direction === "rtl";
137
+ }
138
+ get isVertical() {
139
+ return this.orientation === "vertical";
140
+ }
141
+ connectedCallback() {
142
+ this.init();
143
+ }
144
+ disconnectedCallback() {
145
+ this.destroy();
146
+ }
147
+ attributeChangedCallback(e, t, r) {
148
+ if (e === "data-step") {
149
+ this.step = n(r, 1);
150
+ return;
151
+ }
152
+ e === "data-page" && (this.page = n(r, 10));
153
+ }
154
+ rect() {
155
+ return this.getBoundingClientRect();
156
+ }
157
+ observeResize() {
158
+ this.#t?.disconnect(), this.#t = new ResizeObserver(() => {
159
+ this.sync();
160
+ }), this.#t.observe(this);
161
+ }
162
+ disconnectResize() {
163
+ this.#t?.disconnect(), this.#t = null;
164
+ }
165
+ setValuetext(e, t) {
166
+ let n = this.formatValue ? this.formatValue(t) : String(t);
167
+ e.setAttribute("aria-valuetext", n);
168
+ }
169
+ bindThumb(e) {
170
+ e.tabIndex < 0 && !e.hasAttribute("tabindex") && (e.tabIndex = 0), e.style.touchAction = "none", e.addEventListener("keydown", this.#r), e.addEventListener("pointerdown", this.#a), e.addEventListener("pointermove", this.#o), e.addEventListener("pointerup", this.#s), e.addEventListener("pointercancel", this.#s), e.addEventListener("lostpointercapture", this.#s), e.addEventListener("focus", this.#i);
171
+ }
172
+ unbindThumb(e) {
173
+ e.removeEventListener("keydown", this.#r), e.removeEventListener("pointerdown", this.#a), e.removeEventListener("pointermove", this.#o), e.removeEventListener("pointerup", this.#s), e.removeEventListener("pointercancel", this.#s), e.removeEventListener("lostpointercapture", this.#s), e.removeEventListener("focus", this.#i), e.style.removeProperty("touch-action");
174
+ }
175
+ clearDragState() {
176
+ this.#n = null, this.removeAttribute("dragging");
177
+ }
178
+ #r = (e) => {
179
+ this.onKeydown(e);
180
+ };
181
+ #i = (e) => {
182
+ this.onThumbFocus(e);
183
+ };
184
+ #a = (e) => {
185
+ let t = e.currentTarget, { button: n, clientY: r, clientX: i, pointerId: a } = e;
186
+ if (n !== 0) return;
187
+ t.focus({ preventScroll: !0 }), e.preventDefault();
188
+ let { top: o, left: s, width: c, height: l } = t.getBoundingClientRect(), u = s + c / 2 - i;
189
+ this.isVertical && (u = o + l / 2 - r), this.#n = {
190
+ id: a,
191
+ offset: u,
192
+ $thumb: t
193
+ }, this.setAttribute("dragging", ""), t.setPointerCapture(a), this.commitPointerValue(e, t);
194
+ };
195
+ #o = (e) => {
196
+ if (!this.#n) return;
197
+ let { id: t, $thumb: n } = this.#n, { pointerId: r } = e;
198
+ r === t && (this.commitPointerValue(e, n), e.preventDefault());
199
+ };
200
+ #s = (e) => {
201
+ if (!this.#n) return;
202
+ let { id: t, $thumb: n } = this.#n, { pointerId: r } = e;
203
+ r === t && (this.#n = null, this.removeAttribute("dragging"), n.hasPointerCapture?.(t) && n.releasePointerCapture(t));
204
+ };
205
+ get offset() {
206
+ return this.#n?.offset ?? 0;
207
+ }
208
+ }, _ = class extends g {
209
+ $thumb = null;
210
+ get orientation() {
211
+ return this.$thumb?.getAttribute("aria-orientation") ?? null;
212
+ }
213
+ get min() {
214
+ return n(this.$thumb?.getAttribute("aria-valuemin"), 0);
215
+ }
216
+ get max() {
217
+ return n(this.$thumb?.getAttribute("aria-valuemax"), 100);
218
+ }
219
+ get value() {
220
+ return n(this.$thumb?.getAttribute("aria-valuenow"), this.min);
221
+ }
222
+ get ratio() {
223
+ return s(this.value, this.min, this.max);
224
+ }
225
+ init() {
226
+ let e = this.querySelectorAll("[role=\"slider\"]");
227
+ if (e.length !== 1) throw Error("cinq-slider: exactly one [role=\"slider\"] thumb is required");
228
+ this.$thumb = e[0], this.step = n(this.getAttribute("data-step"), 1), this.page = n(this.getAttribute("data-page"), 10), this.bindThumb(this.$thumb), this.observeResize(), this.sync();
229
+ }
230
+ destroy() {
231
+ this.$thumb && this.unbindThumb(this.$thumb), this.disconnectResize(), this.clearDragState(), this.$thumb = null;
232
+ }
233
+ syncValuetext() {
234
+ this.$thumb && this.setValuetext(this.$thumb, this.value);
235
+ }
236
+ sync() {
237
+ this.#e(this.value, !1);
238
+ }
239
+ setValue(e, t = {}) {
240
+ let { emit: n = !0 } = t, r = l(e, this.min, this.max, this.step);
241
+ this.#e(r, n);
242
+ }
243
+ valueFromPointer(e, t) {
244
+ let { clientY: n, clientX: r } = e, i = this.getBoundingClientRect(), { min: a, max: o } = this, { offsetHeight: s, offsetWidth: l } = t, u = this.isVertical, d = p((u ? n : r) + this.offset, i, u ? s : l, this.isRtl, u);
245
+ return Math.round(c(d, a, o));
246
+ }
247
+ commitPointerValue(e, t) {
248
+ this.setValue(this.valueFromPointer(e, t));
249
+ }
250
+ onKeydown(e) {
251
+ if (!this.$thumb) return;
252
+ let { key: t } = e;
253
+ if (h.isHome(t)) {
254
+ e.preventDefault(), this.setValue(this.min);
255
+ return;
256
+ }
257
+ if (h.isEnd(t)) {
258
+ e.preventDefault(), this.setValue(this.max);
259
+ return;
260
+ }
261
+ let n = new h(this.step, this.page, this.isRtl, this.isVertical).delta(t);
262
+ if (!n) return;
263
+ let { preventDefault: r, delta: i } = n;
264
+ r && e.preventDefault(), i !== 0 && this.setValue(this.value + i);
265
+ }
266
+ #e(n, r) {
267
+ if (!this.$thumb) return;
268
+ let { min: i, max: a } = this, o = s(n, i, a);
269
+ this.$thumb.setAttribute("aria-valuenow", String(n)), this.setValuetext(this.$thumb, n), this.style.setProperty("--value", String(n)), this.style.setProperty("--ratio", String(o)), r && t(this, e.SLIDER_CHANGE, {
270
+ min: n,
271
+ max: n,
272
+ $thumb: this.$thumb
273
+ }, { cancelable: !1 });
274
+ }
275
+ };
276
+ customElements.get("cinq-slider") || customElements.define("cinq-slider", _);
277
+ //#endregion
278
+ //#region src/slider-range.ts
279
+ var v = class extends g {
280
+ $max = null;
281
+ $min = null;
282
+ #e = "min";
283
+ #t = 0;
284
+ #n = 100;
285
+ get boundsMin() {
286
+ return this.#t;
287
+ }
288
+ get boundsMax() {
289
+ return this.#n;
290
+ }
291
+ get min() {
292
+ return n(this.$min?.getAttribute("aria-valuenow"), this.#t);
293
+ }
294
+ get max() {
295
+ return n(this.$max?.getAttribute("aria-valuenow"), this.#n);
296
+ }
297
+ get orientation() {
298
+ return this.$max?.getAttribute("aria-orientation") ?? null;
299
+ }
300
+ init() {
301
+ let e = [...this.querySelectorAll("[role=\"slider\"]")];
302
+ if (e.length !== 2) throw Error("cinq-slider-range: exactly two [role=\"slider\"] thumbs are required");
303
+ let [t, r] = e;
304
+ this.$max = t, this.$min = r, this.step = n(this.getAttribute("data-step"), 1), this.page = n(this.getAttribute("data-page"), 10), this.#t = n(this.$max.getAttribute("aria-valuemin"), 0), this.#n = n(this.$max.getAttribute("aria-valuemax"), 100), this.bindThumb(this.$max), this.bindThumb(this.$min), this.observeResize(), this.sync();
305
+ }
306
+ destroy() {
307
+ for (let e of [this.$max, this.$min]) e && this.unbindThumb(e);
308
+ this.disconnectResize(), this.clearDragState(), this.removeAttribute("data-active"), this.removeAttribute("data-collapsed"), this.$max = null, this.$min = null;
309
+ }
310
+ syncValuetext() {
311
+ this.$min && this.setValuetext(this.$min, this.min), this.$max && this.setValuetext(this.$max, this.max);
312
+ }
313
+ sync() {
314
+ this.#a(this.min, this.max, this.#e, !1);
315
+ }
316
+ setValues(e, t, n = {}) {
317
+ let { active: r = this.#e, emit: i = !0 } = n;
318
+ this.#a(e, t, r, i);
319
+ }
320
+ onThumbFocus(e) {
321
+ let t = this.#r(e.currentTarget);
322
+ t && (this.#e = t, this.setAttribute("data-active", t));
323
+ }
324
+ valueFromPointer(e, t) {
325
+ let { clientY: n, clientX: r } = e, { offsetHeight: i, offsetWidth: a } = t, o = t === this.$min ? "min" : "max", s = this.isVertical ? i : a;
326
+ return m(this.isVertical ? n + this.offset : r + this.offset, this.getBoundingClientRect(), o, this.#t, this.#n, this.#i(s), s, this.isRtl, this.isVertical);
327
+ }
328
+ commitPointerValue(e, t) {
329
+ let n = this.valueFromPointer(e, t);
330
+ if ((t === this.$min ? "min" : "max") == "min") {
331
+ this.setValues(n, this.max, { active: "min" });
332
+ return;
333
+ }
334
+ this.setValues(this.min, n, { active: "max" });
335
+ }
336
+ onKeydown(e) {
337
+ let t = this.#r(e.currentTarget);
338
+ if (!t) return;
339
+ this.#e = t;
340
+ let { key: n } = e, r = t === "min" ? this.min : this.max;
341
+ if (h.isHome(n)) {
342
+ e.preventDefault();
343
+ let n = t === "min" ? this.#t : this.min;
344
+ t === "min" ? this.setValues(n, this.max, { active: "min" }) : this.setValues(this.min, n, { active: "max" });
345
+ return;
346
+ }
347
+ if (h.isEnd(n)) {
348
+ e.preventDefault();
349
+ let n = t === "max" ? this.#n : this.max;
350
+ t === "min" ? this.setValues(n, this.max, { active: "min" }) : this.setValues(this.min, n, { active: "max" });
351
+ return;
352
+ }
353
+ let i = new h(this.step, this.page, this.isRtl, this.isVertical).delta(n);
354
+ if (!i) return;
355
+ let { delta: a } = i;
356
+ if (a !== 0) {
357
+ if (e.preventDefault(), t === "min") {
358
+ this.setValues(r + a, this.max, { active: "min" });
359
+ return;
360
+ }
361
+ this.setValues(this.min, r + a, { active: "max" });
362
+ }
363
+ }
364
+ #r(e) {
365
+ return e === this.$min ? "min" : e === this.$max ? "max" : null;
366
+ }
367
+ #i(e) {
368
+ return d(this.isVertical ? this.clientHeight : this.clientWidth, e);
369
+ }
370
+ #a(n, r, i, a) {
371
+ if (!this.$min || !this.$max) return;
372
+ let o = this.#t, c = this.#n, u = l(n, o, c, this.step), d = l(r, o, c, this.step);
373
+ i === "min" ? u = Math.min(u, d) : d = Math.max(d, u), this.#e = i, this.setAttribute("data-active", i), this.$min.setAttribute("aria-valuenow", String(u)), this.$max.setAttribute("aria-valuenow", String(d)), this.$min.setAttribute("aria-valuemax", String(d)), this.$max.setAttribute("aria-valuemin", String(u)), this.setValuetext(this.$min, u), this.setValuetext(this.$max, d);
374
+ let f = s(u, o, c), p = s(d, o, c);
375
+ if (this.style.setProperty("--min", String(u)), this.style.setProperty("--max", String(d)), this.style.setProperty("--min-ratio", String(f)), this.style.setProperty("--max-ratio", String(p)), u === d ? this.setAttribute("data-collapsed", "") : this.removeAttribute("data-collapsed"), a) {
376
+ let n = i === "min" ? this.$min : this.$max;
377
+ t(this, e.SLIDER_CHANGE, {
378
+ min: u,
379
+ max: d,
380
+ $thumb: n
381
+ }, { cancelable: !1 });
382
+ }
383
+ }
384
+ };
385
+ customElements.get("cinq-slider-range") || customElements.define("cinq-slider-range", v);
386
+ //#endregion
387
+ export { _ as Slider, v as SliderRange };
@@ -0,0 +1,20 @@
1
+ export type KeyboardDelta = {
2
+ delta: number;
3
+ preventDefault: boolean;
4
+ };
5
+ /**
6
+ * APG slider keyboard mapping for arrow and page keys.
7
+ *
8
+ * Horizontal: Left/Right swap in RTL (same convention as `@agencecinq/tabs`).
9
+ * Vertical: Up increases, Down decreases. RTL has no effect on the axis.
10
+ * Home/End are handled by the host (`Keyboard.isHome` / `Keyboard.isEnd`).
11
+ */
12
+ export default class Keyboard {
13
+ #private;
14
+ constructor(step: number, page: number, rtl: boolean, vertical?: boolean);
15
+ /** Step/page delta for `key`, or `null` if unhandled. */
16
+ delta(key: string): KeyboardDelta | null;
17
+ static isHome(key: string): boolean;
18
+ static isEnd(key: string): boolean;
19
+ }
20
+ //# sourceMappingURL=keyboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyboard.d.ts","sourceRoot":"","sources":["../src/keyboard.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,OAAO,QAAQ;;gBAMf,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,UAAQ;IAOtE,yDAAyD;IACzD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI;IAoDxC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAInC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;CAGnC"}
@@ -0,0 +1,47 @@
1
+ import { FormatValue } from './types.js';
2
+ /**
3
+ * Shared slider host: lifecycle, `data-step` / `data-page`, pointer capture,
4
+ * resize sync, and thumb event wiring.
5
+ *
6
+ * Subclasses implement value mapping (`valueFromPointer`, `commitPointerValue`)
7
+ * and keyboard handling. Orientation comes from thumb `aria-orientation`.
8
+ */
9
+ export declare abstract class SliderBase extends HTMLElement {
10
+ #private;
11
+ static observedAttributes: string[];
12
+ step: number;
13
+ page: number;
14
+ protected abstract syncValuetext(): void;
15
+ /** Sync CSS custom properties from current ARIA values (no event). */
16
+ protected abstract sync(): void;
17
+ /** Raw value from pointer position before step snapping. */
18
+ protected abstract valueFromPointer(event: PointerEvent, $thumb: HTMLElement): number;
19
+ /** Apply pointer-derived value (called on pointerdown and pointermove). */
20
+ protected abstract commitPointerValue(event: PointerEvent, $thumb: HTMLElement): void;
21
+ protected abstract onKeydown(event: KeyboardEvent): void;
22
+ protected onThumbFocus(_event: FocusEvent): void;
23
+ /** Optional formatter for thumb `aria-valuetext`. */
24
+ get formatValue(): FormatValue | undefined;
25
+ set formatValue(fn: FormatValue | undefined);
26
+ protected get isRtl(): boolean;
27
+ /** `aria-orientation` on the thumb markup (`null` if unset). */
28
+ protected abstract get orientation(): string | null;
29
+ /** `true` when `orientation === "vertical"`. */
30
+ protected get isVertical(): boolean;
31
+ connectedCallback(): void;
32
+ disconnectedCallback(): void;
33
+ abstract init(): void;
34
+ abstract destroy(): void;
35
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void;
36
+ /** Host rail bounding rect (same as `getBoundingClientRect()`). */
37
+ rect(): DOMRect;
38
+ protected observeResize(): void;
39
+ protected disconnectResize(): void;
40
+ protected setValuetext($el: HTMLElement, value: number): void;
41
+ protected bindThumb($thumb: HTMLElement): void;
42
+ protected unbindThumb($thumb: HTMLElement): void;
43
+ protected clearDragState(): void;
44
+ /** Pointer-to-thumb-center offset while dragging (`0` when idle). */
45
+ protected get offset(): number;
46
+ }
47
+ //# sourceMappingURL=slider-base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slider-base.d.ts","sourceRoot":"","sources":["../src/slider-base.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;GAMG;AACH,8BAAsB,UAAW,SAAQ,WAAW;;IAClD,MAAM,CAAC,kBAAkB,WAA8B;IAEvD,IAAI,SAAK;IACT,IAAI,SAAM;IAMV,SAAS,CAAC,QAAQ,CAAC,aAAa,IAAI,IAAI;IAExC,sEAAsE;IACtE,SAAS,CAAC,QAAQ,CAAC,IAAI,IAAI,IAAI;IAE/B,4DAA4D;IAC5D,SAAS,CAAC,QAAQ,CAAC,gBAAgB,CACjC,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,WAAW,GAClB,MAAM;IAET,2EAA2E;IAC3E,SAAS,CAAC,QAAQ,CAAC,kBAAkB,CACnC,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,WAAW,GAClB,IAAI;IAEP,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAExD,SAAS,CAAC,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAIhD,qDAAqD;IACrD,IAAI,WAAW,IAAI,WAAW,GAAG,SAAS,CAEzC;IAED,IAAI,WAAW,CAAC,EAAE,EAAE,WAAW,GAAG,SAAS,EAG1C;IAED,SAAS,KAAK,KAAK,IAAI,OAAO,CAE7B;IAED,gEAAgE;IAChE,SAAS,CAAC,QAAQ,KAAK,WAAW,IAAI,MAAM,GAAG,IAAI,CAAC;IAEpD,gDAAgD;IAChD,SAAS,KAAK,UAAU,IAAI,OAAO,CAElC;IAED,iBAAiB,IAAI,IAAI;IAIzB,oBAAoB,IAAI,IAAI;IAI5B,QAAQ,CAAC,IAAI,IAAI,IAAI;IAErB,QAAQ,CAAC,OAAO,IAAI,IAAI;IAExB,wBAAwB,CACtB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,IAAI;IAWP,mEAAmE;IACnE,IAAI,IAAI,OAAO;IAIf,SAAS,CAAC,aAAa,IAAI,IAAI;IAQ/B,SAAS,CAAC,gBAAgB,IAAI,IAAI;IAKlC,SAAS,CAAC,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAQ7D,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAgB9C,SAAS,CAAC,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAWhD,SAAS,CAAC,cAAc,IAAI,IAAI;IA+EhC,qEAAqE;IACrE,SAAS,KAAK,MAAM,IAAI,MAAM,CAE7B;CACF"}
@@ -0,0 +1,47 @@
1
+ import { SliderBase } from './slider-base.js';
2
+ import { SetValuesOptions } from './types.js';
3
+ /**
4
+ * Two-thumb range slider (WAI-ARIA multi-thumb pattern).
5
+ *
6
+ * Horizontal (default) or vertical via `aria-orientation` on a thumb.
7
+ * Horizontal axis mirrors for RTL. Vertical ignores RTL on the axis.
8
+ *
9
+ * APG DOM order: first thumb = max handle, second = min handle.
10
+ *
11
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/slider-multithumb/
12
+ * @see https://github.com/19h47/19h47-slider
13
+ */
14
+ export declare class SliderRange extends SliderBase {
15
+ #private;
16
+ /** First `[role="slider"]` in DOM (max handle). */
17
+ $max: HTMLElement | null;
18
+ /** Second `[role="slider"]` in DOM (min handle). */
19
+ $min: HTMLElement | null;
20
+ /** Global lower bound from `$max` `aria-valuemin`. */
21
+ get boundsMin(): number;
22
+ /** Global upper bound from `$max` `aria-valuemax`. */
23
+ get boundsMax(): number;
24
+ /** Current min thumb value (`aria-valuenow` on `$min`). */
25
+ get min(): number;
26
+ /** Current max thumb value (`aria-valuenow` on `$max`). */
27
+ get max(): number;
28
+ /** `aria-orientation` on `$max` (`null` if unset). */
29
+ get orientation(): string | null;
30
+ /** Bind thumbs, read bounds and options, initial sync. */
31
+ init(): void;
32
+ /** Unbind thumbs, disconnect observers, clear drag state. */
33
+ destroy(): void;
34
+ protected syncValuetext(): void;
35
+ /** Re-read thumbs from markup, sync CSS vars and ARIA (no event). */
36
+ protected sync(): void;
37
+ /**
38
+ * Clamp both endpoints to bounds and step, enforce anti-crossing for
39
+ * `active`, sync, dispatch `slider:change` unless `emit: false`.
40
+ */
41
+ setValues(minimum: number, maximum: number, options?: SetValuesOptions): void;
42
+ protected onThumbFocus(event: FocusEvent): void;
43
+ protected valueFromPointer(event: PointerEvent, $thumb: HTMLElement): number;
44
+ protected commitPointerValue(event: PointerEvent, $thumb: HTMLElement): void;
45
+ protected onKeydown(event: KeyboardEvent): void;
46
+ }
47
+ //# sourceMappingURL=slider-range.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slider-range.d.ts","sourceRoot":"","sources":["../src/slider-range.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEhE;;;;;;;;;;GAUG;AACH,qBAAa,WAAY,SAAQ,UAAU;;IACzC,mDAAmD;IACnD,IAAI,EAAE,WAAW,GAAG,IAAI,CAAQ;IAEhC,oDAAoD;IACpD,IAAI,EAAE,WAAW,GAAG,IAAI,CAAQ;IAMhC,sDAAsD;IACtD,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,sDAAsD;IACtD,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,2DAA2D;IAC3D,IAAI,GAAG,IAAI,MAAM,CAKhB;IAED,2DAA2D;IAC3D,IAAI,GAAG,IAAI,MAAM,CAKhB;IAED,sDAAsD;IACtD,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAE/B;IAED,0DAA0D;IAC1D,IAAI,IAAI,IAAI;IAgCZ,6DAA6D;IAC7D,OAAO,IAAI,IAAI;IAef,SAAS,CAAC,aAAa,IAAI,IAAI;IAU/B,qEAAqE;IACrE,SAAS,CAAC,IAAI,IAAI,IAAI;IAItB;;;OAGG;IACH,SAAS,CACP,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,gBAAqB,GAC7B,IAAI;IAKP,SAAS,CAAC,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAS/C,SAAS,CAAC,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM;IAsB5E,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI;IAY5E,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;CA2IhD"}
@@ -0,0 +1,38 @@
1
+ import { SliderBase } from './slider-base.js';
2
+ import { SetValueOptions } from './types.js';
3
+ /**
4
+ * Single-thumb slider (WAI-ARIA slider pattern).
5
+ *
6
+ * Horizontal (default) or vertical via `aria-orientation` on the thumb.
7
+ * Horizontal axis mirrors for RTL (`dir="rtl"` on host or ancestor).
8
+ *
9
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/slider/
10
+ */
11
+ export declare class Slider extends SliderBase {
12
+ #private;
13
+ /** Cached `[role="slider"]` thumb. */
14
+ $thumb: HTMLElement | null;
15
+ /** `aria-orientation` on `$thumb` (`null` if unset). */
16
+ get orientation(): string | null;
17
+ /** Lower bound from `$thumb` `aria-valuemin`. */
18
+ get min(): number;
19
+ /** Upper bound from `$thumb` `aria-valuemax`. */
20
+ get max(): number;
21
+ /** Current value from `$thumb` `aria-valuenow`. */
22
+ get value(): number;
23
+ /** Semantic position in `[0, 1]` within `min`..`max`. */
24
+ get ratio(): number;
25
+ /** Bind thumb, read options, initial sync. */
26
+ init(): void;
27
+ /** Unbind thumb, disconnect observers, clear drag state. */
28
+ destroy(): void;
29
+ protected syncValuetext(): void;
30
+ /** Re-read thumb from markup, sync CSS vars and ARIA (no event). */
31
+ protected sync(): void;
32
+ /** Clamp to bounds and step, sync, dispatch `slider:change` unless `emit: false`. */
33
+ setValue(value: number, options?: SetValueOptions): void;
34
+ protected valueFromPointer(event: PointerEvent, $thumb: HTMLElement): number;
35
+ protected commitPointerValue(event: PointerEvent, $thumb: HTMLElement): void;
36
+ protected onKeydown(event: KeyboardEvent): void;
37
+ }
38
+ //# sourceMappingURL=slider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slider.d.ts","sourceRoot":"","sources":["../src/slider.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;;;GAOG;AACH,qBAAa,MAAO,SAAQ,UAAU;;IACpC,sCAAsC;IACtC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAQ;IAElC,wDAAwD;IACxD,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAE/B;IAED,iDAAiD;IACjD,IAAI,GAAG,IAAI,MAAM,CAEhB;IAED,iDAAiD;IACjD,IAAI,GAAG,IAAI,MAAM,CAEhB;IAED,mDAAmD;IACnD,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,yDAAyD;IACzD,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,8CAA8C;IAC9C,IAAI,IAAI,IAAI;IAiBZ,4DAA4D;IAC5D,OAAO,IAAI,IAAI;IAUf,SAAS,CAAC,aAAa,IAAI,IAAI;IAM/B,oEAAoE;IACpE,SAAS,CAAC,IAAI,IAAI,IAAI;IAItB,qFAAqF;IACrF,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,IAAI;IAM5D,SAAS,CAAC,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM;IAmB5E,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI;IAI5E,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;CAgEhD"}
@@ -0,0 +1,25 @@
1
+ export type FormatValue = (value: number) => string;
2
+ /** Which range thumb is active (`data-active` on the host). */
3
+ export type ActiveThumb = "min" | "max";
4
+ /** Unified `slider:change` detail for `<cinq-slider>` and `<cinq-slider-range>`. */
5
+ export interface SliderChangeDetail {
6
+ /** Current low end of the selection (single: equals `max`). */
7
+ min: number;
8
+ /** Current high end of the selection (single: equals `min`). */
9
+ max: number;
10
+ /** Thumb that moved or has focus (always set). */
11
+ $thumb: HTMLElement;
12
+ }
13
+ /** @deprecated Use `SliderChangeDetail`. */
14
+ export type Detail = SliderChangeDetail;
15
+ export interface SetValueOptions {
16
+ /** Dispatch `slider:change` after sync (default `true`). */
17
+ emit?: boolean;
18
+ }
19
+ export interface SetValuesOptions {
20
+ /** Thumb that drives anti-crossing clamp (default: last active). */
21
+ active?: ActiveThumb;
22
+ /** Dispatch `slider:change` after sync (default `true`). */
23
+ emit?: boolean;
24
+ }
25
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;AAEpD,+DAA+D;AAC/D,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,KAAK,CAAC;AAExC,oFAAoF;AACpF,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,GAAG,EAAE,MAAM,CAAC;IACZ,gEAAgE;IAChE,GAAG,EAAE,MAAM,CAAC;IACZ,kDAAkD;IAClD,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,4CAA4C;AAC5C,MAAM,MAAM,MAAM,GAAG,kBAAkB,CAAC;AAExC,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@agencecinq/slider",
3
+ "version": "6.0.0",
4
+ "description": "Accessible, WAI-ARIA slider as a lightweight Web Component.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Jérémy Levron <jeremy@agencecinq.com> (https://agencecinq.com)",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/agencecinq/shopify.git",
11
+ "directory": "packages/slider"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "main": "./dist/index.js",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "peerDependencies": {
26
+ "@agencecinq/utils": "*"
27
+ },
28
+ "devDependencies": {
29
+ "vite": "^8.2.0",
30
+ "vite-plugin-dts": "^5.0.3",
31
+ "vitest": "^3.2.4",
32
+ "@agencecinq/utils": "7.2.0"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "scripts": {
38
+ "build": "vite build",
39
+ "dev": "vite build --watch",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest"
42
+ }
43
+ }