@tremolo-ui/react 0.6.0 → 0.8.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 (57) hide show
  1. package/dist/index.cjs +522 -997
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +601 -329
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +601 -329
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +487 -953
  8. package/dist/index.js.map +1 -1
  9. package/package.json +4 -4
  10. package/src/components/DropZone/index.tsx +101 -0
  11. package/src/components/FileInput/Trigger.tsx +39 -0
  12. package/src/components/FileInput/context.tsx +24 -0
  13. package/src/components/FileInput/index.tsx +139 -0
  14. package/src/components/Knob/ActiveLine.tsx +5 -3
  15. package/src/components/Knob/InactiveLine.tsx +6 -4
  16. package/src/components/Knob/SVGRoot.tsx +3 -3
  17. package/src/components/Knob/Thumb.tsx +3 -2
  18. package/src/components/Knob/context.tsx +3 -74
  19. package/src/components/Knob/index.tsx +27 -21
  20. package/src/components/NumberInput/DecrementStepper.tsx +2 -13
  21. package/src/components/NumberInput/IncrementStepper.tsx +2 -14
  22. package/src/components/NumberInput/InputField.tsx +17 -38
  23. package/src/components/NumberInput/Stepper.tsx +44 -107
  24. package/src/components/NumberInput/StepperButton.tsx +16 -12
  25. package/src/components/NumberInput/context.tsx +8 -8
  26. package/src/components/NumberInput/index.tsx +41 -81
  27. package/src/components/Piano/index.tsx +39 -149
  28. package/src/components/PointsEditor/Background.tsx +2 -8
  29. package/src/components/PointsEditor/Container.tsx +17 -34
  30. package/src/components/PointsEditor/Point.tsx +41 -86
  31. package/src/components/PointsEditor/SelectionBox.tsx +2 -8
  32. package/src/components/PointsEditor/context.tsx +10 -61
  33. package/src/components/PointsEditor/index.tsx +31 -315
  34. package/src/components/Slider/Marks.tsx +8 -13
  35. package/src/components/Slider/MarksOption.tsx +11 -17
  36. package/src/components/Slider/Thumb.tsx +9 -15
  37. package/src/components/Slider/Track.tsx +7 -13
  38. package/src/components/Slider/index.tsx +33 -39
  39. package/src/components/XYPad/Area.tsx +26 -53
  40. package/src/components/XYPad/Thumb.tsx +8 -12
  41. package/src/components/XYPad/context.tsx +1 -3
  42. package/src/components/XYPad/index.tsx +35 -50
  43. package/src/components/_util/VisuallyHiddenRangeInput.tsx +3 -15
  44. package/src/css-variables.ts +23 -0
  45. package/src/hooks/_internal/useCheckSteps.ts +9 -147
  46. package/src/hooks/useDrag.ts +13 -4
  47. package/src/hooks/useDragValue.ts +17 -2
  48. package/src/hooks/useDropZone.ts +78 -0
  49. package/src/hooks/useLongPress.ts +35 -51
  50. package/src/hooks/useMIDIAccess.ts +0 -8
  51. package/src/hooks/useMIDIInput.ts +0 -3
  52. package/src/hooks/useWheel.ts +4 -1
  53. package/src/index.ts +14 -36
  54. package/src/components/Piano/keyboard-shortcuts.ts +0 -61
  55. package/src/components/Slider/type.ts +0 -44
  56. package/src/components/_util/css-length.ts +0 -13
  57. package/src/input-event.ts +0 -32
@@ -1,32 +1,11 @@
1
1
  import { createContext, CSSProperties, RefObject, useContext } from 'react'
2
2
 
3
- import type { SelectionBoxRect } from '@tremolo-ui/dom'
4
- import type {
5
- InputEventOption,
6
- ModifierState,
7
- ModifierValue,
8
- } from '@tremolo-ui/functions'
9
-
10
- import type { PointBaseType } from './Point'
11
-
12
- /**
13
- * What a `Point` tells the editor about itself, so that a selection can be
14
- * moved without the editor knowing how the points are stored.
15
- *
16
- * Held behind a ref and rewritten on every render: the value changes on every
17
- * frame of a drag, and a registry keyed on it would be rebuilt just as often.
18
- */
19
- export interface PointRegistration {
20
- value: PointBaseType
21
- min?: Partial<PointBaseType>
22
- max?: Partial<PointBaseType>
23
- readonly: boolean
24
- onChange?: (value: PointBaseType) => void
25
- /** The point's own element, to match the focus against. */
26
- element: HTMLElement | null
27
- /** The wheel option this point resolved, root inheritance applied. */
28
- wheel: ModifierValue<InputEventOption> | null
29
- }
3
+ import {
4
+ type InputEventOption,
5
+ type ModifierValue,
6
+ type PointsEditorInstance,
7
+ type SelectionBoxRect,
8
+ } from '@tremolo-ui/dom'
30
9
 
31
10
  export type PointsEditorContextValue = {
32
11
  disabled: boolean
@@ -40,53 +19,23 @@ export type PointsEditorContextValue = {
40
19
  externalStyles: {
41
20
  cursor?: CSSProperties['cursor']
42
21
  }
43
-
44
22
  /**
45
23
  * `Container` registers its element here; `Point` normalizes the pointer
46
24
  * against it, so a point is placed by its position within the container.
47
25
  */
48
26
  containerRef: RefObject<HTMLDivElement | null>
49
-
50
27
  /** Whether points can be selected at all. See `PointsEditorProps.selectable`. */
51
28
  selectable: boolean
52
29
  /** Ids of the points currently selected. Always empty while `selectable` is off. */
53
30
  selection: readonly string[]
54
- /** Register a point so that a selection can move it with the rest. */
55
- registerPoint: (id: string, entry: RefObject<PointRegistration>) => () => void
56
- /**
57
- * Whether the element is a point, or inside one. The registry knows which
58
- * elements the points are, so nothing has to be recognised by a class name.
59
- */
60
- isPointElement: (element: Element | null | undefined) => boolean
61
- /**
62
- * A pointer went down on a point: works out the new selection and takes the
63
- * snapshot the move will be measured against.
64
- */
65
- beginPointDrag: (id: string, modifiers: ModifierState) => void
66
- /** Move everything the drag picked up, by one amount, clamped as one. */
67
- movePointDrag: (delta: PointBaseType) => void
68
- /**
69
- * Move the selection by an amount that did not come from a drag — an arrow
70
- * key or a wheel notch. The current values are the starting point.
71
- */
72
- nudgeSelection: (id: string, delta: PointBaseType) => void
73
31
  /**
74
- * Move the point that currently holds focus by one notch. The wheel listener
75
- * belongs to `Container` rather than to every `Point`, so the lookup of who
76
- * has focus happens here, where the registry is. Returns whether a point
77
- * took it, so the caller knows whether to consume the event.
32
+ * The selection and the moves, from `createPointsEditor` in
33
+ * `@tremolo-ui/dom`: every `Point` registers with it, and a press, a drag,
34
+ * a key or the wheel goes through it so that a selection moves as one.
78
35
  */
79
- nudgeFocusedPoint: (
80
- axis: 'x' | 'y',
81
- direction: number,
82
- modifiers: ModifierState,
83
- ) => boolean
84
-
36
+ editor: PointsEditorInstance
85
37
  /** The selection box, while one is being dragged. */
86
38
  selectionBox: SelectionBoxRect | null
87
- beginSelectionBox: (at: PointBaseType, modifiers: ModifierState) => void
88
- moveSelectionBox: (to: PointBaseType) => void
89
- endSelectionBox: () => void
90
39
  }
91
40
 
92
41
  const PointsEditorContext =
@@ -3,8 +3,6 @@ import {
3
3
  CSSProperties,
4
4
  forwardRef,
5
5
  ReactNode,
6
- RefObject,
7
- useCallback,
8
6
  useEffect,
9
7
  useMemo,
10
8
  useRef,
@@ -12,90 +10,25 @@ import {
12
10
  } from 'react'
13
11
 
14
12
  import {
15
- createSelectionBox,
16
- type SelectionBoxInstance,
17
- type SelectionBoxRect,
18
- type XY,
19
- } from '@tremolo-ui/dom'
20
- import {
21
- applyDelta,
22
- clamp,
13
+ createPointsEditor,
14
+ DEFAULT_DRAG_SENSITIVITY,
23
15
  type InputEventOption,
24
- type ModifierState,
25
16
  type ModifierValue,
26
- toPrecision,
27
- } from '@tremolo-ui/functions'
28
-
29
- import { DEFAULT_DRAG_SENSITIVITY } from '../../input-event'
30
- import { cssLength } from '../_util/css-length'
17
+ POINTS_EDITOR_DEFAULT_KEYBOARD,
18
+ POINTS_EDITOR_DEFAULT_WHEEL,
19
+ type SelectionBoxRect,
20
+ } from '@tremolo-ui/dom'
31
21
 
32
22
  import { Background } from './Background'
33
23
  import { Container } from './Container'
34
- import { type PointRegistration, PointsEditorProvider } from './context'
35
- import { AXIS, Point, type PointBaseType } from './Point'
24
+ import { PointsEditorProvider } from './context'
25
+ import { Point } from './Point'
36
26
  import { SelectionBox } from './SelectionBox'
37
27
 
38
28
  /** One array for every editor with selection turned off, so memos hold still. */
39
29
  const EMPTY: readonly string[] = []
40
30
 
41
- /**
42
- * How far a point may move before something in the selection leaves its range.
43
- *
44
- * Clamping each point on its own would break the shape of the selection: the
45
- * one that reached the edge would stop while the rest carried on. One amount
46
- * for all of them means the whole selection stops together.
47
- */
48
- function allowedDelta(
49
- delta: PointBaseType,
50
- entries: { start: PointBaseType; registration: PointRegistration }[],
51
- ): PointBaseType {
52
- let loX = -Infinity
53
- let hiX = Infinity
54
- let loY = -Infinity
55
- let hiY = Infinity
56
-
57
- for (const { start, registration } of entries) {
58
- loX = Math.max(loX, (registration.min?.x ?? 0) - start.x)
59
- hiX = Math.min(hiX, (registration.max?.x ?? 1) - start.x)
60
- loY = Math.max(loY, (registration.min?.y ?? 0) - start.y)
61
- hiY = Math.min(hiY, (registration.max?.y ?? 1) - start.y)
62
- }
63
-
64
- // A point that started outside its own range leaves nothing to move within.
65
- return {
66
- x: hiX < loX ? 0 : clamp(delta.x, loX, hiX),
67
- y: hiY < loY ? 0 : clamp(delta.y, loY, hiY),
68
- }
69
- }
70
-
71
- /*
72
- TODO:
73
-
74
- - grid
75
- */
76
-
77
- /**
78
- * A point moves over 0..1 in both axes, so a nudge of 0.01 crosses the editor
79
- * in a hundred steps whatever its pixel size.
80
- */
81
- const DEFAULT_WHEEL: ModifierValue<InputEventOption> = ['normalized', 0.01]
82
-
83
- /**
84
- * Shift is the fine-adjustment key everywhere else, so it is bound here too
85
- * — but only on the keyboard. On the wheel it already means the x axis, and
86
- * browsers hand shift+wheel over as horizontal scrolling anyway.
87
- */
88
- const DEFAULT_KEYBOARD: ModifierValue<InputEventOption> = {
89
- default: ['normalized', 0.01],
90
- shift: ['normalized', 0.001],
91
- }
92
-
93
31
  export interface PointsEditorProps {
94
- /** Sets `--width`; the size the theme gives it stands when omitted. */
95
- width?: number | string
96
- /** Sets `--height`; the size the theme gives it stands when omitted. */
97
- height?: number | string
98
-
99
32
  /**
100
33
  * Make the points unchangeable and remove them from the tab order.
101
34
  * The parts carry `data-disabled` while it is set.
@@ -221,12 +154,10 @@ type Props = PointsEditorProps &
221
154
  export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
222
155
  (
223
156
  {
224
- width,
225
- height,
226
157
  disabled = false,
227
158
  readonly = false,
228
- wheel = DEFAULT_WHEEL,
229
- keyboard = DEFAULT_KEYBOARD,
159
+ wheel = POINTS_EDITOR_DEFAULT_WHEEL,
160
+ keyboard = POINTS_EDITOR_DEFAULT_KEYBOARD,
230
161
  dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
231
162
  selectable = false,
232
163
  selection: selectionProp,
@@ -241,12 +172,13 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
241
172
  forwardedRef,
242
173
  ) => {
243
174
  const containerRef = useRef<HTMLDivElement>(null)
244
-
245
175
  // Picked apart so that the memo below depends on values rather than on the
246
176
  // object literal a caller writes inline, which is new on every render.
247
177
  const { cursor = 'grabbing' } = externalStyles ?? {}
248
178
 
249
179
  // --- selection ---
180
+ // The selection lives here, where React state can hold it; which points a
181
+ // press or a box selects, and how a selection moves, is the core's.
250
182
  const controlled = selectionProp !== undefined
251
183
  const [ownSelection, setOwnSelection] = useState<string[]>(
252
184
  defaultSelection ?? [],
@@ -255,220 +187,29 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
255
187
  // point it started on and `data-selected` never turns on.
256
188
  const selection = selectable ? (selectionProp ?? ownSelection) : EMPTY
257
189
 
258
- // Drags read the selection from a native event handler, which runs after
259
- // the commit, so a ref is current by the time it matters.
260
- const selectionRef = useRef(selection)
261
- const changeHandlerRef = useRef(onSelectionChange)
262
- useEffect(() => {
263
- selectionRef.current = selection
264
- changeHandlerRef.current = onSelectionChange
265
- })
266
-
267
- const changeSelection = useCallback(
268
- (next: string[]) => {
269
- selectionRef.current = next
270
- if (!controlled) setOwnSelection(next)
271
- changeHandlerRef.current?.(next)
272
- },
273
- [controlled],
274
- )
275
-
276
- /**
277
- * Every mounted point, by id. A registration is a ref rather than a value:
278
- * the value inside changes on every frame of a drag, and a registry keyed
279
- * on it would be rebuilt just as often.
280
- */
281
- const points = useRef(new Map<string, RefObject<PointRegistration>>())
282
-
283
- const registerPoint = useCallback(
284
- (id: string, entry: RefObject<PointRegistration>) => {
285
- points.current.set(id, entry)
286
- return () => {
287
- points.current.delete(id)
288
- }
289
- },
290
- [],
291
- )
292
-
293
- /** What the current drag picked up, and where those points started. */
294
- const dragRef = useRef<{ id: string; start: PointBaseType }[]>([])
295
-
296
- const snapshot = useCallback((ids: readonly string[]) => {
297
- return ids.flatMap((id) => {
298
- const registration = points.current.get(id)?.current
299
- return registration ? [{ id, start: { ...registration.value } }] : []
300
- })
301
- }, [])
302
-
303
- const applyDeltaTo = useCallback(
304
- (
305
- entries: { id: string; start: PointBaseType }[],
306
- delta: PointBaseType,
307
- ) => {
308
- const withRegistration = entries.flatMap((entry) => {
309
- const registration = points.current.get(entry.id)?.current
310
- return registration ? [{ ...entry, registration }] : []
311
- })
312
- const allowed = allowedDelta(delta, withRegistration)
313
- for (const { start, registration } of withRegistration) {
314
- if (registration.readonly) continue
315
- // Rounded here as well as in the pipeline: a move is a subtraction
316
- // and an addition of its own, and that is enough to put the binary
317
- // artefact back (0.2 + 0.1 lands on 0.30000000000000004).
318
- registration.onChange?.({
319
- x: toPrecision(start.x + allowed.x),
320
- y: toPrecision(start.y + allowed.y),
321
- })
322
- }
323
- },
324
- [],
325
- )
326
-
327
- const beginPointDrag = useCallback(
328
- (id: string, modifiers: ModifierState) => {
329
- if (!selectable) {
330
- dragRef.current = snapshot([id])
331
- return
332
- }
333
- const current = selectionRef.current
334
- // Ctrl / meta rather than shift: shift is the fine-adjustment key on
335
- // every control here, and it cannot be both.
336
- const additive = modifiers.ctrlKey || modifiers.metaKey
337
- let next: readonly string[]
338
- if (additive) {
339
- next = current.includes(id)
340
- ? current.filter((x) => x !== id)
341
- : [...current, id]
342
- } else if (current.includes(id)) {
343
- // Already part of a group: keep it, so the group can be dragged.
344
- next = current
345
- } else {
346
- next = [id]
347
- }
348
- changeSelection([...next])
349
- // A press that took the point out of the selection was a deselect, not
350
- // the start of a move, so there is nothing to drag.
351
- dragRef.current = next.includes(id) ? snapshot(next) : []
352
- },
353
- [selectable, changeSelection, snapshot],
354
- )
355
-
356
- const movePointDrag = useCallback(
357
- (delta: PointBaseType) => applyDeltaTo(dragRef.current, delta),
358
- [applyDeltaTo],
359
- )
360
-
361
- const nudgeSelection = useCallback(
362
- (id: string, delta: PointBaseType) => {
363
- // From wherever the points are now: a key press is not a drag, so
364
- // there is no earlier position to measure against.
365
- const ids = selectionRef.current.includes(id)
366
- ? selectionRef.current
367
- : [id]
368
- applyDeltaTo(snapshot(ids), delta)
369
- },
370
- [applyDeltaTo, snapshot],
371
- )
372
-
373
- const isPointElement = useCallback(
374
- (element: Element | null | undefined) => {
375
- if (!element) return false
376
- for (const [, entry] of points.current) {
377
- if (entry.current.element?.contains(element)) return true
378
- }
379
- return false
380
- },
381
- [],
382
- )
383
-
384
- const nudgeFocusedPoint = useCallback(
385
- (axis: 'x' | 'y', direction: number, modifiers: ModifierState) => {
386
- const container = containerRef.current
387
- const active = container?.ownerDocument.activeElement
388
- if (!container || !active || !container.contains(active)) return false
389
- for (const [id, entry] of points.current) {
390
- const { element, wheel, readonly, onChange, value } = entry.current
391
- // A point answers only for the focus inside its own inputs, so both
392
- // axes stay part of the same interaction.
393
- if (!element?.contains(active)) continue
394
- if (!wheel || readonly || !onChange) return false
395
- const next = applyDelta(
396
- value[axis],
397
- direction,
398
- wheel,
399
- AXIS,
400
- modifiers,
401
- )
402
- // As a move, so that the rest of the selection comes along and the
403
- // whole group stops together at the edge.
404
- nudgeSelection(id, {
405
- x: axis === 'x' ? next - value.x : 0,
406
- y: axis === 'y' ? next - value.y : 0,
407
- })
408
- return true
409
- }
410
- return false
411
- },
412
- [nudgeSelection],
413
- )
414
-
415
- // --- selection box ---
416
- // The box itself lives in the core: which items a rectangle covers, and
417
- // what a press adds to or replaces, are not React's to decide. What is
418
- // left here is the registry it reads and the state the box is drawn from.
419
190
  const [selectionBox, setSelectionBox] = useState<SelectionBoxRect | null>(
420
191
  null,
421
192
  )
422
- const selectionBoxRef = useRef<SelectionBoxInstance<string> | null>(null)
423
- selectionBoxRef.current ??= createSelectionBox<string>({
424
- *items(): Generator<readonly [string, XY<number>]> {
425
- for (const [id, entry] of points.current) {
426
- const { x, y } = entry.current.value
427
- yield [id, [x, y]]
428
- }
429
- },
430
- onBoxChange: setSelectionBox,
431
- })
432
-
433
- // The selection is reported to whoever owns it, and that handler is a new
434
- // function whenever the caller's is, so it is handed over rather than
435
- // closed over.
436
- useEffect(() => {
437
- selectionBoxRef.current?.update({ onSelectionChange: changeSelection })
438
- }, [changeSelection])
439
-
440
- useEffect(() => {
441
- const box = selectionBoxRef.current
442
- return () => box?.destroy()
443
- }, [])
444
193
 
445
- const beginSelectionBox = useCallback(
446
- (at: PointBaseType, modifiers: ModifierState) => {
447
- if (!selectable) return
448
- selectionBoxRef.current?.begin([at.x, at.y], {
449
- // Ctrl / meta rather than shift: shift is the fine-adjustment key on
450
- // every control here, and it cannot be both.
451
- additive: modifiers.ctrlKey || modifiers.metaKey,
452
- selection: selectionRef.current,
453
- })
454
- },
455
- [selectable],
194
+ const [editor] = useState(() =>
195
+ createPointsEditor({ onSelectionBoxChange: setSelectionBox }),
456
196
  )
457
197
 
458
- const moveSelectionBox = useCallback((to: PointBaseType) => {
459
- selectionBoxRef.current?.move([to.x, to.y])
460
- }, [])
198
+ // Drags read the selection from a native event handler, which runs after
199
+ // the commit, so pushing it from an effect is current by the time it
200
+ // matters. The handler goes the same way: it closes over this render.
201
+ useEffect(() => {
202
+ editor.update({
203
+ selectable,
204
+ selection,
205
+ onSelectionChange: (next) => {
206
+ if (!controlled) setOwnSelection(next)
207
+ onSelectionChange?.(next)
208
+ },
209
+ })
210
+ })
461
211
 
462
- const endSelectionBox = useCallback(() => {
463
- if (!selectionBoxRef.current?.end()) return
464
- // A selection box is drawn on the container, which is not a control and
465
- // cannot hold focus, so the press that started it left the focus on
466
- // nothing. The arrow keys and the wheel reach a point only through the
467
- // focus, so it is handed to one of the points the box selected —
468
- // whichever point takes it moves the whole selection.
469
- const [first] = selectionRef.current
470
- if (first) points.current.get(first)?.current.element?.focus()
471
- }, [])
212
+ useEffect(() => () => editor.destroy(), [editor])
472
213
 
473
214
  const context = useMemo(
474
215
  () => ({
@@ -481,16 +222,8 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
481
222
  containerRef,
482
223
  selectable,
483
224
  selection,
484
- registerPoint,
485
- isPointElement,
486
- beginPointDrag,
487
- movePointDrag,
488
- nudgeSelection,
489
- nudgeFocusedPoint,
225
+ editor,
490
226
  selectionBox,
491
- beginSelectionBox,
492
- moveSelectionBox,
493
- endSelectionBox,
494
227
  }),
495
228
  [
496
229
  disabled,
@@ -501,16 +234,8 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
501
234
  cursor,
502
235
  selectable,
503
236
  selection,
504
- registerPoint,
505
- isPointElement,
506
- beginPointDrag,
507
- movePointDrag,
508
- nudgeSelection,
509
- nudgeFocusedPoint,
237
+ editor,
510
238
  selectionBox,
511
- beginSelectionBox,
512
- moveSelectionBox,
513
- endSelectionBox,
514
239
  ],
515
240
  )
516
241
 
@@ -523,8 +248,6 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
523
248
  data-readonly={readonly ? '' : undefined}
524
249
  style={
525
250
  {
526
- '--width': cssLength(width),
527
- '--height': cssLength(height),
528
251
  // The layers inside are placed against this box.
529
252
  position: 'relative',
530
253
  ...style,
@@ -554,11 +277,4 @@ export {
554
277
  usePointsEditorContext,
555
278
  type PointsEditorContextValue,
556
279
  } from './context'
557
- export { type PointsEditorBackgroundProps } from './Background'
558
- export { type PointsEditorContainerProps } from './Container'
559
- export { type PointsEditorSelectionBoxProps } from './SelectionBox'
560
- export {
561
- clampPoint,
562
- type PointBaseType,
563
- type PointsEditorPointProps,
564
- } from './Point'
280
+ export { type PointsEditorPointProps } from './Point'
@@ -1,13 +1,13 @@
1
- import { ComponentPropsWithoutRef, CSSProperties, ReactNode } from 'react'
1
+ import { ComponentPropsWithoutRef, CSSProperties } from 'react'
2
2
 
3
- import { xor } from '@tremolo-ui/functions'
3
+ import { cssLength, type MarksOptions, sliderMarks } from '@tremolo-ui/dom'
4
4
 
5
- import { cssLength } from '../_util/css-length'
6
5
  import { Placement } from '../_util/Placement'
7
6
 
8
7
  import { useSliderContext } from './context'
9
8
  import { MarksOption } from './MarksOption'
10
- import { generateOptionsList, MarksOptions } from './type'
9
+
10
+ import type { CSSVariables } from '../../css-variables'
11
11
 
12
12
  export interface SliderMarksProps {
13
13
  /**
@@ -24,11 +24,8 @@ export interface SliderMarksProps {
24
24
  * `'step'` makes one per step, which is a great many for a fine `step`.
25
25
  */
26
26
  options?: MarksOptions
27
- /**
28
- * `<Slider.MarksOption />` goes here, one per mark. Ignored while `options`
29
- * is set.
30
- */
31
- children?: ReactNode
27
+
28
+ style?: CSSProperties & CSSVariables<'gap'>
32
29
  }
33
30
 
34
31
  export function Marks({
@@ -46,10 +43,8 @@ export function Marks({
46
43
  const vertical = useSliderContext((s) => s.vertical)
47
44
  const reverse = useSliderContext((s) => s.reverse)
48
45
 
49
- const optionsList = options
50
- ? generateOptionsList(options, min, max, step)
51
- : []
52
- if (xor(vertical, reverse)) optionsList.reverse()
46
+ const optionsList = options ? sliderMarks(options, min, max, step) : []
47
+ if (vertical !== reverse) optionsList.reverse()
53
48
 
54
49
  return (
55
50
  <div
@@ -1,12 +1,13 @@
1
- import { ComponentPropsWithoutRef, CSSProperties, useCallback } from 'react'
1
+ import { ComponentPropsWithoutRef, CSSProperties } from 'react'
2
2
 
3
- import { toFixed, xor } from '@tremolo-ui/functions'
3
+ import { cssLength, valuePercent } from '@tremolo-ui/dom'
4
4
 
5
- import { cssLength } from '../_util/css-length'
6
5
  import { useCheckPlacement } from '../_util/Placement'
7
6
 
8
7
  import { useSliderContext } from './context'
9
8
 
9
+ import type { CSSVariables } from '../../css-variables'
10
+
10
11
  export interface SliderMarksOptionProps {
11
12
  /** Where the mark sits, on the same scale as the thumb. */
12
13
  value: number
@@ -27,8 +28,6 @@ export interface SliderMarksOptionProps {
27
28
  length?: number | string
28
29
  /** Space between the mark and the label. Sets `--gap`. */
29
30
  gap?: number | string
30
- /** Width of the label. Sets `--label-width`. */
31
- labelWidth?: number | string
32
31
  /** Classes for the mark and the label inside the option. */
33
32
  classes?: {
34
33
  mark?: string
@@ -39,6 +38,9 @@ export interface SliderMarksOptionProps {
39
38
  mark?: CSSProperties
40
39
  label?: CSSProperties
41
40
  }
41
+
42
+ style?: CSSProperties &
43
+ CSSVariables<'thickness' | 'length' | 'gap' | 'translate'>
42
44
  }
43
45
 
44
46
  export function MarksOption({
@@ -48,7 +50,6 @@ export function MarksOption({
48
50
  thickness,
49
51
  length,
50
52
  gap,
51
- labelWidth,
52
53
  classes,
53
54
  styles,
54
55
  className,
@@ -64,14 +65,8 @@ export function MarksOption({
64
65
  const vertical = useSliderContext((s) => s.vertical)
65
66
  const reverse = useSliderContext((s) => s.reverse)
66
67
 
67
- const calcPercent = useCallback(
68
- (value: number) => {
69
- // The marks have to sit on the same curve the thumb runs along.
70
- const percent = scale.normalize(value, min, max) * 100
71
- return toFixed(xor(vertical, reverse) ? 100 - percent : percent)
72
- },
73
- [vertical, reverse, max, min, scale],
74
- )
68
+ // The marks have to sit on the same curve the thumb runs along.
69
+ const percent = valuePercent(value, { min, max, scale }, vertical !== reverse)
75
70
 
76
71
  return (
77
72
  <div
@@ -82,7 +77,6 @@ export function MarksOption({
82
77
  '--thickness': cssLength(thickness),
83
78
  '--length': cssLength(length),
84
79
  '--gap': cssLength(gap),
85
- '--label-width': cssLength(labelWidth),
86
80
  // The mechanics of the position below, as on the thumb.
87
81
  position: 'absolute',
88
82
  translate: vertical
@@ -91,8 +85,8 @@ export function MarksOption({
91
85
  zIndex: 10,
92
86
  ...style,
93
87
  // Where the mark belongs on the track: the value, not a style.
94
- left: !vertical ? `${calcPercent(value)}%` : undefined,
95
- top: vertical ? `${calcPercent(value)}%` : undefined,
88
+ left: !vertical ? `${percent}%` : undefined,
89
+ top: vertical ? `${percent}%` : undefined,
96
90
  } as CSSProperties
97
91
  }
98
92
  data-orientation={vertical ? 'vertical' : 'horizontal'}