@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.
- package/dist/index.cjs +522 -997
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +601 -329
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +601 -329
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +487 -953
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/components/DropZone/index.tsx +101 -0
- package/src/components/FileInput/Trigger.tsx +39 -0
- package/src/components/FileInput/context.tsx +24 -0
- package/src/components/FileInput/index.tsx +139 -0
- package/src/components/Knob/ActiveLine.tsx +5 -3
- package/src/components/Knob/InactiveLine.tsx +6 -4
- package/src/components/Knob/SVGRoot.tsx +3 -3
- package/src/components/Knob/Thumb.tsx +3 -2
- package/src/components/Knob/context.tsx +3 -74
- package/src/components/Knob/index.tsx +27 -21
- package/src/components/NumberInput/DecrementStepper.tsx +2 -13
- package/src/components/NumberInput/IncrementStepper.tsx +2 -14
- package/src/components/NumberInput/InputField.tsx +17 -38
- package/src/components/NumberInput/Stepper.tsx +44 -107
- package/src/components/NumberInput/StepperButton.tsx +16 -12
- package/src/components/NumberInput/context.tsx +8 -8
- package/src/components/NumberInput/index.tsx +41 -81
- package/src/components/Piano/index.tsx +39 -149
- package/src/components/PointsEditor/Background.tsx +2 -8
- package/src/components/PointsEditor/Container.tsx +17 -34
- package/src/components/PointsEditor/Point.tsx +41 -86
- package/src/components/PointsEditor/SelectionBox.tsx +2 -8
- package/src/components/PointsEditor/context.tsx +10 -61
- package/src/components/PointsEditor/index.tsx +31 -315
- package/src/components/Slider/Marks.tsx +8 -13
- package/src/components/Slider/MarksOption.tsx +11 -17
- package/src/components/Slider/Thumb.tsx +9 -15
- package/src/components/Slider/Track.tsx +7 -13
- package/src/components/Slider/index.tsx +33 -39
- package/src/components/XYPad/Area.tsx +26 -53
- package/src/components/XYPad/Thumb.tsx +8 -12
- package/src/components/XYPad/context.tsx +1 -3
- package/src/components/XYPad/index.tsx +35 -50
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +3 -15
- package/src/css-variables.ts +23 -0
- package/src/hooks/_internal/useCheckSteps.ts +9 -147
- package/src/hooks/useDrag.ts +13 -4
- package/src/hooks/useDragValue.ts +17 -2
- package/src/hooks/useDropZone.ts +78 -0
- package/src/hooks/useLongPress.ts +35 -51
- package/src/hooks/useMIDIAccess.ts +0 -8
- package/src/hooks/useMIDIInput.ts +0 -3
- package/src/hooks/useWheel.ts +4 -1
- package/src/index.ts +14 -36
- package/src/components/Piano/keyboard-shortcuts.ts +0 -61
- package/src/components/Slider/type.ts +0 -44
- package/src/components/_util/css-length.ts +0 -13
- package/src/input-event.ts +0 -32
|
@@ -1,32 +1,11 @@
|
|
|
1
1
|
import { createContext, CSSProperties, RefObject, useContext } from 'react'
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from '@tremolo-ui/
|
|
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
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 {
|
|
35
|
-
import {
|
|
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 =
|
|
229
|
-
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
|
|
446
|
-
(
|
|
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
|
-
|
|
459
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1
|
+
import { ComponentPropsWithoutRef, CSSProperties } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1
|
+
import { ComponentPropsWithoutRef, CSSProperties } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
|
|
68
|
-
|
|
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 ? `${
|
|
95
|
-
top: vertical ? `${
|
|
88
|
+
left: !vertical ? `${percent}%` : undefined,
|
|
89
|
+
top: vertical ? `${percent}%` : undefined,
|
|
96
90
|
} as CSSProperties
|
|
97
91
|
}
|
|
98
92
|
data-orientation={vertical ? 'vertical' : 'horizontal'}
|