panelui-web 0.9.0 → 0.10.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/components/button-group/index.d.ts +28 -16
- package/dist/components/button-group/index.js +53 -15
- package/dist/components/choice-cards/index.d.ts +167 -0
- package/dist/components/choice-cards/index.js +132 -0
- package/dist/components/inspector/index.d.ts +242 -0
- package/dist/components/inspector/index.js +230 -0
- package/dist/components/sheet/index.d.ts +16 -5
- package/dist/components/sheet/index.js +72 -15
- package/dist/components/slider/index.d.ts +293 -0
- package/dist/components/slider/index.js +342 -0
- package/dist/components/timeline/index.d.ts +328 -0
- package/dist/components/timeline/index.js +613 -0
- package/dist/components/toolbar/index.d.ts +185 -0
- package/dist/components/toolbar/index.js +270 -0
- package/dist/components/workbench/index.d.ts +224 -0
- package/dist/components/workbench/index.js +306 -0
- package/dist/components/xy-pad/index.d.ts +180 -0
- package/dist/components/xy-pad/index.js +278 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/package.json +1 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Slider — a number chosen by dragging along a track, with its name and its
|
|
5
|
+
* value written inside the track.
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <Slider label="Opacity" defaultValue={64} format={{ style: 'unit', unit: 'percent' }} />
|
|
9
|
+
*
|
|
10
|
+
* <Slider variant="line" label="Zoom" min={10} max={400} defaultValue={100}>
|
|
11
|
+
* <Slider.Step by={-10} aria-label="Zoom out"><MinusIcon /></Slider.Step>
|
|
12
|
+
* <Slider.Step by={10} aria-label="Zoom in"><PlusIcon /></Slider.Step>
|
|
13
|
+
* </Slider>
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* ## The track is the label
|
|
17
|
+
*
|
|
18
|
+
* In a panel of settings, a column of sliders each with a label above and a
|
|
19
|
+
* number beside is three things to line up per row. The `fill` slider puts
|
|
20
|
+
* the name at the start of the track and the value at its end, so a row is
|
|
21
|
+
* one bar the height of a button, and a stack of them reads as a table.
|
|
22
|
+
*
|
|
23
|
+
* ## It moves the way the hand does
|
|
24
|
+
*
|
|
25
|
+
* A press away from the handle glides the fill there on a curve with a
|
|
26
|
+
* little overshoot; a drag follows the pointer exactly, with nothing
|
|
27
|
+
* easing behind it. Pulled past either end, the track stretches a few
|
|
28
|
+
* pixels towards the pointer and springs back on release, so the end of
|
|
29
|
+
* the range is felt rather than hit.
|
|
30
|
+
*
|
|
31
|
+
* ## Two weights
|
|
32
|
+
*
|
|
33
|
+
* `fill` is the slider for a settings panel. `line` is a thin track with a
|
|
34
|
+
* round knob, for a toolbar, where a zoom or a scrubber sits between two
|
|
35
|
+
* buttons: `Slider.Step` is that button, and it moves the value by `by`.
|
|
36
|
+
*
|
|
37
|
+
* `origin="center"` fills from the middle out, for a value that can go
|
|
38
|
+
* either way from nothing: exposure, a balance, a pitch shift.
|
|
39
|
+
*/
|
|
40
|
+
import { Slider as BaseSlider } from '@base-ui/react/slider';
|
|
41
|
+
import { Children, createContext, isValidElement, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
|
|
42
|
+
import { tv } from 'tailwind-variants';
|
|
43
|
+
/** A glide with a little overshoot, for a press. A drag turns it off. */
|
|
44
|
+
const GLIDE = 'duration-[420ms] ease-[cubic-bezier(0.3,1.3,0.45,1)]';
|
|
45
|
+
/** How far the track gives past an end, at most, in pixels. */
|
|
46
|
+
const STRETCH = 8;
|
|
47
|
+
/** How far the pointer has to travel before a press counts as a drag. */
|
|
48
|
+
const DRAG_SLOP = 3;
|
|
49
|
+
/** Steps are marked when there are this many or fewer. */
|
|
50
|
+
const MAX_TICKS = 12;
|
|
51
|
+
const sliderVariants = tv({
|
|
52
|
+
slots: {
|
|
53
|
+
root: 'group/slider relative flex w-full min-w-0 items-center gap-1.5 data-disabled:opacity-50',
|
|
54
|
+
control: [
|
|
55
|
+
'relative min-w-0 flex-1 cursor-pointer touch-none select-none outline-none',
|
|
56
|
+
'transition-[margin] duration-500 ease-[cubic-bezier(0.3,1.4,0.4,1)] data-stretching:duration-0',
|
|
57
|
+
'data-disabled:cursor-not-allowed motion-reduce:transition-none',
|
|
58
|
+
],
|
|
59
|
+
track: 'absolute overflow-hidden',
|
|
60
|
+
fill: [
|
|
61
|
+
'absolute inset-y-0 start-(--slider-start) w-(--slider-span)',
|
|
62
|
+
`transition-[inset-inline-start,width] ${GLIDE} group-data-moving/slider:duration-0 motion-reduce:transition-none`,
|
|
63
|
+
],
|
|
64
|
+
tick: 'pointer-events-none absolute -translate-x-1/2 rtl:translate-x-1/2',
|
|
65
|
+
center: 'pointer-events-none absolute inset-y-1.5 start-1/2 w-px -translate-x-1/2 bg-foreground/20 rtl:translate-x-1/2',
|
|
66
|
+
handle: [
|
|
67
|
+
'pointer-events-none absolute top-1/2 start-(--slider-at) -translate-y-1/2',
|
|
68
|
+
`transition-[inset-inline-start,height,width,background-color,scale] ${GLIDE} group-data-moving/slider:duration-0 motion-reduce:transition-none`,
|
|
69
|
+
],
|
|
70
|
+
// Base UI's thumb carries the keyboard and the screen reader. It is
|
|
71
|
+
// invisible: the handle above is what is seen.
|
|
72
|
+
thumb: 'pointer-events-none size-px opacity-0',
|
|
73
|
+
text: 'pointer-events-none absolute inset-0 flex items-center justify-between gap-3 px-3',
|
|
74
|
+
// Lighter than muted, so the name reads on a coloured fill as well as on the track.
|
|
75
|
+
label: 'truncate text-foreground/70',
|
|
76
|
+
value: 'shrink-0 tabular-nums text-foreground',
|
|
77
|
+
step: [
|
|
78
|
+
'inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-lg text-muted-foreground outline-none',
|
|
79
|
+
'transition-[color,background-color,scale] duration-150 hover:bg-foreground/[0.06] hover:text-foreground active:scale-90',
|
|
80
|
+
'focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40',
|
|
81
|
+
'[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
variants: {
|
|
85
|
+
variant: {
|
|
86
|
+
fill: {
|
|
87
|
+
control: 'h-(--slider-h) rounded-[10px] has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring',
|
|
88
|
+
track: 'inset-0 rounded-[inherit] bg-muted',
|
|
89
|
+
fill: 'bg-(--slider-fill)',
|
|
90
|
+
// A notch on the top and bottom edges at every step, like the marks
|
|
91
|
+
// on a rule: the text runs between them and never crosses one.
|
|
92
|
+
tick: [
|
|
93
|
+
'inset-y-0 w-px',
|
|
94
|
+
'bg-[linear-gradient(to_bottom,var(--slider-notch)_0_5px,transparent_5px_calc(100%-5px),var(--slider-notch)_calc(100%-5px))]',
|
|
95
|
+
'[--slider-notch:color-mix(in_oklab,var(--foreground)_22%,transparent)]',
|
|
96
|
+
],
|
|
97
|
+
handle: [
|
|
98
|
+
// Centred on the value, so it stands on the same line as the fill's
|
|
99
|
+
// edge and the notch of the step it is on.
|
|
100
|
+
'h-[calc(var(--slider-h)*0.44)] w-[3px] -translate-x-1/2 rounded-full bg-(--slider-mark) opacity-60 rtl:translate-x-1/2',
|
|
101
|
+
'group-hover/slider:h-[calc(var(--slider-h)*0.56)] group-hover/slider:opacity-100 group-data-moving/slider:h-[calc(var(--slider-h)*0.56)] group-data-moving/slider:opacity-100',
|
|
102
|
+
'group-data-disabled/slider:opacity-40',
|
|
103
|
+
// Where it would cross the name or the value, it steps out of the way:
|
|
104
|
+
// the edge of the fill still says where the value is.
|
|
105
|
+
'data-crowded:opacity-0 group-hover/slider:data-crowded:opacity-0 group-data-moving/slider:data-crowded:opacity-0',
|
|
106
|
+
],
|
|
107
|
+
text: 'text-(length:--slider-text) font-medium',
|
|
108
|
+
},
|
|
109
|
+
line: {
|
|
110
|
+
control: 'h-6 rounded-full',
|
|
111
|
+
track: 'inset-x-0 top-1/2 h-1 -translate-y-1/2 rounded-full bg-foreground/15',
|
|
112
|
+
fill: 'rounded-full bg-(--slider-mark)',
|
|
113
|
+
tick: 'top-1/2 size-[3px] -translate-y-1/2 rounded-full bg-background/70',
|
|
114
|
+
handle: [
|
|
115
|
+
'size-4 -translate-x-1/2 rounded-full border-2 border-(--slider-mark) bg-background shadow-[0_1px_3px_rgb(0_0_0/0.2)] rtl:translate-x-1/2',
|
|
116
|
+
'group-hover/slider:scale-110 group-data-moving/slider:scale-125',
|
|
117
|
+
'group-has-[:focus-visible]/slider:ring-2 group-has-[:focus-visible]/slider:ring-ring group-has-[:focus-visible]/slider:ring-offset-2 group-has-[:focus-visible]/slider:ring-offset-background',
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
color: {
|
|
122
|
+
neutral: { root: '[--slider-fill:color-mix(in_oklab,var(--foreground)_11%,transparent)] [--slider-mark:var(--foreground)]' },
|
|
123
|
+
primary: { root: '[--slider-fill:color-mix(in_oklab,var(--primary)_24%,transparent)] [--slider-mark:var(--primary)]' },
|
|
124
|
+
info: { root: '[--slider-fill:color-mix(in_oklab,var(--info)_26%,transparent)] [--slider-mark:var(--info)]' },
|
|
125
|
+
success: { root: '[--slider-fill:color-mix(in_oklab,var(--success)_26%,transparent)] [--slider-mark:var(--success)]' },
|
|
126
|
+
warning: { root: '[--slider-fill:color-mix(in_oklab,var(--warning)_28%,transparent)] [--slider-mark:var(--warning)]' },
|
|
127
|
+
destructive: {
|
|
128
|
+
root: '[--slider-fill:color-mix(in_oklab,var(--destructive)_24%,transparent)] [--slider-mark:var(--destructive)]',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
size: {
|
|
132
|
+
sm: { root: '[--slider-h:1.75rem] [--slider-text:12px]' },
|
|
133
|
+
md: { root: '[--slider-h:2rem] [--slider-text:13px]' },
|
|
134
|
+
lg: { root: '[--slider-h:2.5rem] [--slider-text:14px]' },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
compoundVariants: [
|
|
138
|
+
// The line's fill is drawn solid, so a neutral one is the foreground itself.
|
|
139
|
+
{ variant: 'line', color: 'neutral', className: { fill: 'bg-foreground' } },
|
|
140
|
+
],
|
|
141
|
+
defaultVariants: { variant: 'fill', color: 'neutral', size: 'md' },
|
|
142
|
+
});
|
|
143
|
+
const SliderContext = createContext(null);
|
|
144
|
+
const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
145
|
+
const noop = () => () => { };
|
|
146
|
+
const yes = () => true;
|
|
147
|
+
const no = () => false;
|
|
148
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
149
|
+
const toPercent = (value, min, max) => ((clamp(value, min, max) - min) / (max - min)) * 100;
|
|
150
|
+
/** Rounds away the float noise that repeated steps leave, such as 0.30000000000000004. */
|
|
151
|
+
const tidy = (value, step) => {
|
|
152
|
+
const decimals = (String(step).split('.')[1] ?? '').length;
|
|
153
|
+
return Number(value.toFixed(Math.min(decimals + 2, 10)));
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* A slider. `defaultValue`, or `value` with `onValueChange`; `min`, `max`
|
|
157
|
+
* and `step` as for any range.
|
|
158
|
+
*/
|
|
159
|
+
function SliderRoot({ value: valueProp, defaultValue, onValueChange, variant = 'fill', label, format, origin = 'start', color, size, showValue = true, ticks, min = 0, max = 100, step = 1, disabled = false, locale, children, className, style, ...props }) {
|
|
160
|
+
const slots = sliderVariants({ variant, color, size });
|
|
161
|
+
const [inner, setInner] = useState(() => defaultValue ?? (origin === 'center' ? (min + max) / 2 : min));
|
|
162
|
+
const value = (valueProp ?? inner);
|
|
163
|
+
const values = Array.isArray(value) ? value : [value];
|
|
164
|
+
const range = values.length > 1;
|
|
165
|
+
const live = useRef({ value, onValueChange, controlled: valueProp !== undefined });
|
|
166
|
+
live.current = { value, onValueChange, controlled: valueProp !== undefined };
|
|
167
|
+
const change = useCallback((next) => {
|
|
168
|
+
if (!live.current.controlled)
|
|
169
|
+
setInner(next);
|
|
170
|
+
live.current.onValueChange?.(next);
|
|
171
|
+
}, []);
|
|
172
|
+
// The server cannot know the reader's locale, so the page is first written
|
|
173
|
+
// in a fixed one and switches to theirs once it is running in the browser.
|
|
174
|
+
const hydrated = useSyncExternalStore(noop, yes, no);
|
|
175
|
+
const write = useMemo(() => {
|
|
176
|
+
if (typeof format === 'function')
|
|
177
|
+
return format;
|
|
178
|
+
const formatter = new Intl.NumberFormat(locale ?? (hydrated ? undefined : 'en-US'), format);
|
|
179
|
+
return (number) => formatter.format(number);
|
|
180
|
+
}, [format, locale, hydrated]);
|
|
181
|
+
// The fill: from the start, from the middle, or between two handles.
|
|
182
|
+
const percents = values.map((each) => toPercent(each, min, max));
|
|
183
|
+
const [low, high] = range
|
|
184
|
+
? [Math.min(...percents), Math.max(...percents)]
|
|
185
|
+
: origin === 'center'
|
|
186
|
+
? [Math.min(50, percents[0]), Math.max(50, percents[0])]
|
|
187
|
+
: [0, percents[0]];
|
|
188
|
+
const count = (max - min) / step;
|
|
189
|
+
const showTicks = ticks ?? (Number.isInteger(Math.round(count * 1e6) / 1e6) && count >= 2 && count <= MAX_TICKS);
|
|
190
|
+
/*
|
|
191
|
+
* Press, drag and stretch. Base UI moves the value; this watches the same
|
|
192
|
+
* pointer to tell a press from a drag, which decides whether the fill
|
|
193
|
+
* glides, and to measure how far past an end the pointer has gone.
|
|
194
|
+
*/
|
|
195
|
+
const control = useRef(null);
|
|
196
|
+
// Held in state as well, because the element can be replaced after
|
|
197
|
+
// hydration, and the measuring below has to follow it to the new one.
|
|
198
|
+
const [controlElement, setControlElement] = useState(null);
|
|
199
|
+
const controlRef = useCallback((element) => {
|
|
200
|
+
control.current = element;
|
|
201
|
+
setControlElement(element);
|
|
202
|
+
}, []);
|
|
203
|
+
const root = useRef(null);
|
|
204
|
+
const [stretch, setStretch] = useState(0);
|
|
205
|
+
const press = (event) => {
|
|
206
|
+
if (disabled || event.button !== 0)
|
|
207
|
+
return;
|
|
208
|
+
const box = control.current?.getBoundingClientRect();
|
|
209
|
+
if (!box)
|
|
210
|
+
return;
|
|
211
|
+
const startX = event.clientX;
|
|
212
|
+
const startY = event.clientY;
|
|
213
|
+
const rtl = getComputedStyle(control.current).direction === 'rtl';
|
|
214
|
+
const move = (next) => {
|
|
215
|
+
if (Math.hypot(next.clientX - startX, next.clientY - startY) > DRAG_SLOP) {
|
|
216
|
+
root.current?.setAttribute('data-moving', '');
|
|
217
|
+
}
|
|
218
|
+
const past = next.clientX < box.left ? next.clientX - box.left : next.clientX > box.right ? next.clientX - box.right : 0;
|
|
219
|
+
// Eases off, so the further the pointer goes the less the track follows.
|
|
220
|
+
const give = Math.sign(past) * STRETCH * Math.sqrt(Math.min(Math.max(Math.abs(past) - 12, 0) / 200, 1));
|
|
221
|
+
setStretch(rtl ? -give : give);
|
|
222
|
+
};
|
|
223
|
+
const release = () => {
|
|
224
|
+
root.current?.removeAttribute('data-moving');
|
|
225
|
+
setStretch(0);
|
|
226
|
+
window.removeEventListener('pointermove', move);
|
|
227
|
+
window.removeEventListener('pointerup', release);
|
|
228
|
+
window.removeEventListener('pointercancel', release);
|
|
229
|
+
};
|
|
230
|
+
window.addEventListener('pointermove', move);
|
|
231
|
+
window.addEventListener('pointerup', release);
|
|
232
|
+
window.addEventListener('pointercancel', release);
|
|
233
|
+
};
|
|
234
|
+
useEffect(() => () => root.current?.removeAttribute('data-moving'), []);
|
|
235
|
+
// Which handles would land on the text, measured where they are going
|
|
236
|
+
// rather than where the glide has them now.
|
|
237
|
+
const labelRef = useRef(null);
|
|
238
|
+
const valueRef = useRef(null);
|
|
239
|
+
const [crowded, setCrowded] = useState([]);
|
|
240
|
+
const percentKey = percents.join(',');
|
|
241
|
+
useIsomorphicLayoutEffect(() => {
|
|
242
|
+
const box = controlElement;
|
|
243
|
+
if (variant !== 'fill' || !box)
|
|
244
|
+
return;
|
|
245
|
+
const measure = () => {
|
|
246
|
+
const width = box.clientWidth;
|
|
247
|
+
const rtl = getComputedStyle(box).direction === 'rtl';
|
|
248
|
+
// Distances from the start edge, in the direction the text runs.
|
|
249
|
+
const span = (element) => {
|
|
250
|
+
if (!element || !element.textContent)
|
|
251
|
+
return null;
|
|
252
|
+
const start = rtl ? width - element.offsetLeft - element.offsetWidth : element.offsetLeft;
|
|
253
|
+
return [start - 6, start + element.offsetWidth + 6];
|
|
254
|
+
};
|
|
255
|
+
const zones = [span(labelRef.current), span(valueRef.current)].filter((zone) => zone !== null);
|
|
256
|
+
const next = percentKey.split(',').map((percent) => {
|
|
257
|
+
const x = (Number(percent) / 100) * width;
|
|
258
|
+
const at = Math.min(Math.max(x, 8), width - 8) - 1.5;
|
|
259
|
+
return zones.some(([left, right]) => at + 3 > left && at < right);
|
|
260
|
+
});
|
|
261
|
+
setCrowded((last) => (last.length === next.length && last.every((each, i) => each === next[i]) ? last : next));
|
|
262
|
+
};
|
|
263
|
+
measure();
|
|
264
|
+
// The text can change size after the first paint, when a web font or a
|
|
265
|
+
// stylesheet arrives, without the track changing at all.
|
|
266
|
+
const observer = new ResizeObserver(measure);
|
|
267
|
+
for (const element of [box, labelRef.current, valueRef.current])
|
|
268
|
+
if (element)
|
|
269
|
+
observer.observe(element);
|
|
270
|
+
let live = true;
|
|
271
|
+
document.fonts?.ready.then(() => live && measure());
|
|
272
|
+
return () => {
|
|
273
|
+
live = false;
|
|
274
|
+
observer.disconnect();
|
|
275
|
+
};
|
|
276
|
+
}, [variant, percentKey, controlElement]);
|
|
277
|
+
const single = values[0];
|
|
278
|
+
const context = useMemo(() => ({
|
|
279
|
+
nudge: (by) => {
|
|
280
|
+
if (range)
|
|
281
|
+
return;
|
|
282
|
+
change(tidy(clamp(single + by, min, max), step));
|
|
283
|
+
},
|
|
284
|
+
atMin: !range && single <= min,
|
|
285
|
+
atMax: !range && single >= max,
|
|
286
|
+
disabled,
|
|
287
|
+
}), [range, single, min, max, step, change, disabled]);
|
|
288
|
+
const text = range ? values.map(write).join(' – ') : write(single);
|
|
289
|
+
const name = label ?? props['aria-label'];
|
|
290
|
+
return (_jsx(SliderContext.Provider, { value: context, children: _jsxs(BaseSlider.Root, { ref: root, "data-slot": "slider", "data-variant": variant, value: value, onValueChange: (next) => change(next), min: min, max: max, step: step, disabled: disabled, locale: locale, className: slots.root({ className }), style: {
|
|
291
|
+
...style,
|
|
292
|
+
'--slider-start': `${low}%`,
|
|
293
|
+
'--slider-span': `${high - low}%`,
|
|
294
|
+
}, ...props, children: [_jsxs(BaseSlider.Control, { ref: controlRef, "data-slot": "slider-control", "data-stretching": stretch !== 0 ? '' : undefined, onPointerDown: press, className: slots.control(), style: {
|
|
295
|
+
marginInlineStart: stretch < 0 ? stretch : 0,
|
|
296
|
+
marginInlineEnd: stretch > 0 ? -stretch : 0,
|
|
297
|
+
}, children: [_jsxs("div", { "data-slot": "slider-track", className: slots.track(), children: [_jsx("div", { "data-slot": "slider-fill", className: slots.fill() }), showTicks
|
|
298
|
+
? Array.from({ length: Math.round(count) - 1 }, (_, at) => (_jsx("span", { "aria-hidden": true, className: slots.tick(), style: { insetInlineStart: `${((at + 1) / Math.round(count)) * 100}%` } }, at)))
|
|
299
|
+
: null, origin === 'center' && !range ? _jsx("span", { "aria-hidden": true, className: slots.center() }) : null] }), values.map((each, at) => {
|
|
300
|
+
const percent = toPercent(each, min, max);
|
|
301
|
+
// At either end the fill's handle stays clear of the rounded corner.
|
|
302
|
+
const place = variant !== 'fill' ? `${percent}%` : `clamp(8px, ${percent}%, calc(100% - 8px))`;
|
|
303
|
+
return (_jsx("span", { "aria-hidden": true, "data-slot": "slider-handle", "data-crowded": crowded[at] ? '' : undefined, className: slots.handle(), style: { ['--slider-at']: place } }, at));
|
|
304
|
+
}), variant === 'fill' && (label || showValue) ? (_jsxs("span", { "aria-hidden": true, "data-slot": "slider-text", className: slots.text(), children: [_jsx("span", { ref: labelRef, className: slots.label(), children: label }), showValue ? (_jsx("span", { ref: valueRef, className: slots.value(), children: text })) : null] })) : null, values.map((_, at) => (_jsx(BaseSlider.Thumb, { index: range ? at : undefined, "aria-label": range ? `${name ?? 'Value'}, ${at === 0 ? 'start' : 'end'}` : name, getAriaValueText: (_formatted, number) => write(number), className: slots.thumb() }, at)))] }), _jsx(SliderSteps, { children: children })] }) }));
|
|
305
|
+
}
|
|
306
|
+
SliderRoot.displayName = 'Slider';
|
|
307
|
+
/** Puts each `Slider.Step` on its side of the track: a step down before, a step up after. */
|
|
308
|
+
function SliderSteps({ children }) {
|
|
309
|
+
const before = [];
|
|
310
|
+
const after = [];
|
|
311
|
+
Children.forEach(children, (child) => {
|
|
312
|
+
if (isValidElement(child) && typeof child.props.by === 'number' && child.props.by < 0) {
|
|
313
|
+
before.push(child);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
after.push(child);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
return (_jsxs(_Fragment, { children: [before.length ? _jsx("div", { className: "order-first flex shrink-0 items-center", children: before }) : null, after.length ? _jsx("div", { className: "order-last flex shrink-0 items-center", children: after }) : null] }));
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* A button beside the track that moves the value by `by`: zoom out and zoom
|
|
323
|
+
* in on either side of a zoom slider. It is disabled at the end it moves
|
|
324
|
+
* towards.
|
|
325
|
+
*/
|
|
326
|
+
function SliderStep({ by, className, disabled, onClick, ...props }) {
|
|
327
|
+
const slider = useContext(SliderContext);
|
|
328
|
+
if (!slider)
|
|
329
|
+
throw new Error('Slider.Step must be used inside a <Slider>.');
|
|
330
|
+
const slots = sliderVariants();
|
|
331
|
+
const stuck = by < 0 ? slider.atMin : slider.atMax;
|
|
332
|
+
return (_jsx("button", { type: "button", "data-slot": "slider-step", disabled: disabled || slider.disabled || stuck, className: slots.step({ className }), onClick: (event) => {
|
|
333
|
+
onClick?.(event);
|
|
334
|
+
if (!event.defaultPrevented)
|
|
335
|
+
slider.nudge(by);
|
|
336
|
+
}, ...props }));
|
|
337
|
+
}
|
|
338
|
+
SliderStep.displayName = 'Slider.Step';
|
|
339
|
+
export const Slider = Object.assign(SliderRoot, {
|
|
340
|
+
Step: SliderStep,
|
|
341
|
+
});
|
|
342
|
+
export { SliderStep, sliderVariants };
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { type ComponentProps } from 'react';
|
|
2
|
+
export type TimelineColor = 'primary' | 'info' | 'success' | 'warning' | 'destructive' | 'chart-1' | 'chart-2' | 'chart-3' | 'chart-4' | 'chart-5';
|
|
3
|
+
export type TimelineClipVariant = 'bar' | 'block' | 'media';
|
|
4
|
+
export type TimelineTrackSize = 'sm' | 'md' | 'lg';
|
|
5
|
+
/**
|
|
6
|
+
* A time as a reader expects it. On a timeline under a minute long, seconds
|
|
7
|
+
* with a unit: `0.5s`, `12s`. Longer, `m:ss`, and `h:mm:ss` past an hour.
|
|
8
|
+
* `tenths` adds a tenth of a second either way.
|
|
9
|
+
*/
|
|
10
|
+
export declare function formatTime(seconds: number, tenths?: boolean, short?: boolean): string;
|
|
11
|
+
/** The ruler's step at a zoom: the smallest that keeps labels `LABEL_GAP` apart. */
|
|
12
|
+
export declare function rulerStep(zoom: number): number;
|
|
13
|
+
declare const timelineVariants: import("tailwind-variants").TVReturnType<{
|
|
14
|
+
variant: {
|
|
15
|
+
bar: {
|
|
16
|
+
clip: string;
|
|
17
|
+
};
|
|
18
|
+
block: {
|
|
19
|
+
clip: string;
|
|
20
|
+
};
|
|
21
|
+
media: {
|
|
22
|
+
clip: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
size: {
|
|
26
|
+
sm: {
|
|
27
|
+
track: string;
|
|
28
|
+
};
|
|
29
|
+
md: {
|
|
30
|
+
track: string;
|
|
31
|
+
};
|
|
32
|
+
lg: {
|
|
33
|
+
track: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
}, {
|
|
37
|
+
root: string[];
|
|
38
|
+
header: string;
|
|
39
|
+
time: string;
|
|
40
|
+
zoom: string;
|
|
41
|
+
viewport: string;
|
|
42
|
+
content: string;
|
|
43
|
+
scrollbar: string;
|
|
44
|
+
thumb: string;
|
|
45
|
+
end: string[];
|
|
46
|
+
ruler: string;
|
|
47
|
+
tick: string;
|
|
48
|
+
label: string;
|
|
49
|
+
track: string;
|
|
50
|
+
clip: string[];
|
|
51
|
+
handle: string[];
|
|
52
|
+
grip: string;
|
|
53
|
+
clipLabel: string[];
|
|
54
|
+
waveform: string;
|
|
55
|
+
guide: string;
|
|
56
|
+
playhead: string;
|
|
57
|
+
playheadLine: string;
|
|
58
|
+
playheadHead: string[];
|
|
59
|
+
}, undefined, {
|
|
60
|
+
variant: {
|
|
61
|
+
bar: {
|
|
62
|
+
clip: string;
|
|
63
|
+
};
|
|
64
|
+
block: {
|
|
65
|
+
clip: string;
|
|
66
|
+
};
|
|
67
|
+
media: {
|
|
68
|
+
clip: string;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
size: {
|
|
72
|
+
sm: {
|
|
73
|
+
track: string;
|
|
74
|
+
};
|
|
75
|
+
md: {
|
|
76
|
+
track: string;
|
|
77
|
+
};
|
|
78
|
+
lg: {
|
|
79
|
+
track: string;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
}, {
|
|
83
|
+
root: string[];
|
|
84
|
+
header: string;
|
|
85
|
+
time: string;
|
|
86
|
+
zoom: string;
|
|
87
|
+
viewport: string;
|
|
88
|
+
content: string;
|
|
89
|
+
scrollbar: string;
|
|
90
|
+
thumb: string;
|
|
91
|
+
end: string[];
|
|
92
|
+
ruler: string;
|
|
93
|
+
tick: string;
|
|
94
|
+
label: string;
|
|
95
|
+
track: string;
|
|
96
|
+
clip: string[];
|
|
97
|
+
handle: string[];
|
|
98
|
+
grip: string;
|
|
99
|
+
clipLabel: string[];
|
|
100
|
+
waveform: string;
|
|
101
|
+
guide: string;
|
|
102
|
+
playhead: string;
|
|
103
|
+
playheadLine: string;
|
|
104
|
+
playheadHead: string[];
|
|
105
|
+
}, import("tailwind-variants").TVReturnType<{
|
|
106
|
+
variant: {
|
|
107
|
+
bar: {
|
|
108
|
+
clip: string;
|
|
109
|
+
};
|
|
110
|
+
block: {
|
|
111
|
+
clip: string;
|
|
112
|
+
};
|
|
113
|
+
media: {
|
|
114
|
+
clip: string;
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
size: {
|
|
118
|
+
sm: {
|
|
119
|
+
track: string;
|
|
120
|
+
};
|
|
121
|
+
md: {
|
|
122
|
+
track: string;
|
|
123
|
+
};
|
|
124
|
+
lg: {
|
|
125
|
+
track: string;
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
}, {
|
|
129
|
+
root: string[];
|
|
130
|
+
header: string;
|
|
131
|
+
time: string;
|
|
132
|
+
zoom: string;
|
|
133
|
+
viewport: string;
|
|
134
|
+
content: string;
|
|
135
|
+
scrollbar: string;
|
|
136
|
+
thumb: string;
|
|
137
|
+
end: string[];
|
|
138
|
+
ruler: string;
|
|
139
|
+
tick: string;
|
|
140
|
+
label: string;
|
|
141
|
+
track: string;
|
|
142
|
+
clip: string[];
|
|
143
|
+
handle: string[];
|
|
144
|
+
grip: string;
|
|
145
|
+
clipLabel: string[];
|
|
146
|
+
waveform: string;
|
|
147
|
+
guide: string;
|
|
148
|
+
playhead: string;
|
|
149
|
+
playheadLine: string;
|
|
150
|
+
playheadHead: string[];
|
|
151
|
+
}, undefined, unknown, unknown, undefined>>;
|
|
152
|
+
/**
|
|
153
|
+
* The timeline's state, for controls of your own: a play button in a
|
|
154
|
+
* toolbar, a readout, a button that zooms to fit. Use it inside `<Timeline>`.
|
|
155
|
+
*/
|
|
156
|
+
export declare function useTimeline(): {
|
|
157
|
+
duration: number;
|
|
158
|
+
time: number;
|
|
159
|
+
setTime: (time: number) => void;
|
|
160
|
+
playing: boolean;
|
|
161
|
+
setPlaying: (playing: boolean) => void;
|
|
162
|
+
zoom: number;
|
|
163
|
+
setZoom: (zoom: number, anchor?: {
|
|
164
|
+
time: number;
|
|
165
|
+
x: number;
|
|
166
|
+
}) => void;
|
|
167
|
+
minZoom: number;
|
|
168
|
+
maxZoom: number;
|
|
169
|
+
};
|
|
170
|
+
export interface TimelineProps extends Omit<ComponentProps<'div'>, 'onChange'> {
|
|
171
|
+
/** How long the timeline is, in seconds. */
|
|
172
|
+
duration: number;
|
|
173
|
+
/** Where the playhead is, in seconds, when the caller holds it. */
|
|
174
|
+
time?: number;
|
|
175
|
+
/** Where the playhead starts, when the caller does not hold it. Default 0. */
|
|
176
|
+
defaultTime?: number;
|
|
177
|
+
/** Called with the new time when the playhead moves: scrubbed, stepped or playing. */
|
|
178
|
+
onTimeChange?: (time: number) => void;
|
|
179
|
+
/** Pixels per second, when the caller holds it. */
|
|
180
|
+
zoom?: number;
|
|
181
|
+
/** Pixels per second to start at. Default 12. */
|
|
182
|
+
defaultZoom?: number;
|
|
183
|
+
/** Called with the new zoom. */
|
|
184
|
+
onZoomChange?: (zoom: number) => void;
|
|
185
|
+
/** The furthest out it zooms, in pixels per second. Default 2. */
|
|
186
|
+
minZoom?: number;
|
|
187
|
+
/** The furthest in it zooms, in pixels per second. Default 120. */
|
|
188
|
+
maxZoom?: number;
|
|
189
|
+
/** Whether time is running, when the caller holds it. */
|
|
190
|
+
playing?: boolean;
|
|
191
|
+
/** Called when playback starts or stops, including when it reaches the end. */
|
|
192
|
+
onPlayingChange?: (playing: boolean) => void;
|
|
193
|
+
/** Start again from the beginning on reaching the end, instead of stopping. */
|
|
194
|
+
loop?: boolean;
|
|
195
|
+
/** Snap clip edges to other edges, the playhead and the ends. Default true. */
|
|
196
|
+
snap?: boolean;
|
|
197
|
+
/** Show it without letting clips be moved or trimmed. The playhead still moves. */
|
|
198
|
+
readOnly?: boolean;
|
|
199
|
+
}
|
|
200
|
+
/** Holds the time, the zoom and playback for the parts inside it. */
|
|
201
|
+
declare function TimelineRoot({ duration, time: timeProp, defaultTime, onTimeChange, zoom: zoomProp, defaultZoom, onZoomChange, minZoom, maxZoom, playing: playingProp, onPlayingChange, loop, snap, readOnly, className, children, ...props }: TimelineProps): import("react").JSX.Element;
|
|
202
|
+
declare namespace TimelineRoot {
|
|
203
|
+
var displayName: string;
|
|
204
|
+
}
|
|
205
|
+
export interface TimelineHeaderProps extends ComponentProps<'div'> {
|
|
206
|
+
}
|
|
207
|
+
/** A row above the tracks, for a toolbar, the time and the zoom. */
|
|
208
|
+
declare function TimelineHeader({ className, ...props }: TimelineHeaderProps): import("react").JSX.Element;
|
|
209
|
+
declare namespace TimelineHeader {
|
|
210
|
+
var displayName: string;
|
|
211
|
+
}
|
|
212
|
+
export interface TimelineTimeProps extends ComponentProps<'span'> {
|
|
213
|
+
/** Show the length after the time, as `1:32 / 2:30`. Default true. */
|
|
214
|
+
showDuration?: boolean;
|
|
215
|
+
}
|
|
216
|
+
/** Where the playhead is, written out, and how long the whole is. */
|
|
217
|
+
declare function TimelineTime({ showDuration, className, ...props }: TimelineTimeProps): import("react").JSX.Element;
|
|
218
|
+
declare namespace TimelineTime {
|
|
219
|
+
var displayName: string;
|
|
220
|
+
}
|
|
221
|
+
export interface TimelineZoomProps extends Omit<ComponentProps<'div'>, 'children'> {
|
|
222
|
+
}
|
|
223
|
+
/** A line slider that zooms the timeline, with zoom out and zoom in either side. */
|
|
224
|
+
declare function TimelineZoom({ className, ...props }: TimelineZoomProps): import("react").JSX.Element;
|
|
225
|
+
declare namespace TimelineZoom {
|
|
226
|
+
var displayName: string;
|
|
227
|
+
}
|
|
228
|
+
export interface TimelineViewportProps extends ComponentProps<'div'> {
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* The part that scrolls sideways: the ruler, the tracks and the playhead go
|
|
232
|
+
* in here. Pinch, or ⌘ or Ctrl with the wheel, zooms about the pointer.
|
|
233
|
+
*/
|
|
234
|
+
declare function TimelineViewport({ className, style, children, ...props }: TimelineViewportProps): import("react").JSX.Element;
|
|
235
|
+
declare namespace TimelineViewport {
|
|
236
|
+
var displayName: string;
|
|
237
|
+
}
|
|
238
|
+
export interface TimelineRulerProps extends ComponentProps<'div'> {
|
|
239
|
+
}
|
|
240
|
+
/** Ticks and times along the top. Pressing or dragging on it moves the playhead there. */
|
|
241
|
+
declare function TimelineRuler({ className, ...props }: TimelineRulerProps): import("react").JSX.Element;
|
|
242
|
+
declare namespace TimelineRuler {
|
|
243
|
+
var displayName: string;
|
|
244
|
+
}
|
|
245
|
+
export interface TimelineTrackProps extends ComponentProps<'div'> {
|
|
246
|
+
/** The height of the row: `sm` for bars, `md` for blocks, `lg` for media. Default `md`. */
|
|
247
|
+
size?: TimelineTrackSize;
|
|
248
|
+
/** What the track holds, for a screen reader: "Zooms", "Voice". */
|
|
249
|
+
label?: string;
|
|
250
|
+
}
|
|
251
|
+
/** A row of clips. Clips on one track cannot overlap. */
|
|
252
|
+
declare function TimelineTrack({ size, label, className, ...props }: TimelineTrackProps): import("react").JSX.Element;
|
|
253
|
+
declare namespace TimelineTrack {
|
|
254
|
+
var displayName: string;
|
|
255
|
+
}
|
|
256
|
+
export interface TimelineClipProps extends Omit<ComponentProps<'div'>, 'onChange'> {
|
|
257
|
+
/** Where it starts, in seconds. */
|
|
258
|
+
start: number;
|
|
259
|
+
/** Where it ends, in seconds. */
|
|
260
|
+
end: number;
|
|
261
|
+
/** Called with the new span while it is moved or trimmed. Without it the clip stays put. */
|
|
262
|
+
onChange?: (span: {
|
|
263
|
+
start: number;
|
|
264
|
+
end: number;
|
|
265
|
+
}) => void;
|
|
266
|
+
/** Called once a move or a trim ends. */
|
|
267
|
+
onChangeEnd?: (span: {
|
|
268
|
+
start: number;
|
|
269
|
+
end: number;
|
|
270
|
+
}) => void;
|
|
271
|
+
/** Called on Delete or Backspace while the clip has focus. */
|
|
272
|
+
onRemove?: () => void;
|
|
273
|
+
/** `bar`, `block` or `media`. Default: `bar` on a small track, `media` on a large one, `block` otherwise. */
|
|
274
|
+
variant?: TimelineClipVariant;
|
|
275
|
+
/** Its colour, from the theme. Default `primary`. */
|
|
276
|
+
color?: TimelineColor;
|
|
277
|
+
/** What a screen reader calls it, before its times. */
|
|
278
|
+
label?: string;
|
|
279
|
+
}
|
|
280
|
+
/** One clip. With `onChange` it moves by its middle and trims by its ends. */
|
|
281
|
+
declare function TimelineClip({ start, end, onChange, onChangeEnd, onRemove, variant: variantProp, color, label, className, style, children, onKeyDown, ...props }: TimelineClipProps): import("react").JSX.Element;
|
|
282
|
+
declare namespace TimelineClip {
|
|
283
|
+
var displayName: string;
|
|
284
|
+
}
|
|
285
|
+
export interface TimelineClipLabelProps extends ComponentProps<'span'> {
|
|
286
|
+
}
|
|
287
|
+
/** Words on a clip, with an icon: its length, its speed, what it is. */
|
|
288
|
+
declare function TimelineClipLabel({ className, ...props }: TimelineClipLabelProps): import("react").JSX.Element;
|
|
289
|
+
declare namespace TimelineClipLabel {
|
|
290
|
+
var displayName: string;
|
|
291
|
+
}
|
|
292
|
+
export interface TimelineWaveformProps extends Omit<ComponentProps<'svg'>, 'children'> {
|
|
293
|
+
/** Loudness along the clip, each from 0 to 1. Drawn mirrored about the middle. */
|
|
294
|
+
peaks: number[];
|
|
295
|
+
}
|
|
296
|
+
/** The sound of a media clip, drawn as bars across it. */
|
|
297
|
+
declare function TimelineWaveform({ peaks, className, ...props }: TimelineWaveformProps): import("react").JSX.Element;
|
|
298
|
+
declare namespace TimelineWaveform {
|
|
299
|
+
var displayName: string;
|
|
300
|
+
}
|
|
301
|
+
export interface TimelinePlayheadProps extends Omit<ComponentProps<'div'>, 'color'> {
|
|
302
|
+
/** The colour of the line and its head. Default `primary`. */
|
|
303
|
+
color?: TimelineColor;
|
|
304
|
+
/** Write the time in the head. Default true. */
|
|
305
|
+
showTime?: boolean;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* The line at the current time, from the ruler to the bottom. Its head can
|
|
309
|
+
* be dragged, and with focus the arrow keys step it: a tenth of a second,
|
|
310
|
+
* or a second with Shift.
|
|
311
|
+
*/
|
|
312
|
+
declare function TimelinePlayhead({ color, showTime, className, style, ...props }: TimelinePlayheadProps): import("react").JSX.Element;
|
|
313
|
+
declare namespace TimelinePlayhead {
|
|
314
|
+
var displayName: string;
|
|
315
|
+
}
|
|
316
|
+
export declare const Timeline: typeof TimelineRoot & {
|
|
317
|
+
Header: typeof TimelineHeader;
|
|
318
|
+
Time: typeof TimelineTime;
|
|
319
|
+
Zoom: typeof TimelineZoom;
|
|
320
|
+
Viewport: typeof TimelineViewport;
|
|
321
|
+
Ruler: typeof TimelineRuler;
|
|
322
|
+
Track: typeof TimelineTrack;
|
|
323
|
+
Clip: typeof TimelineClip;
|
|
324
|
+
ClipLabel: typeof TimelineClipLabel;
|
|
325
|
+
Waveform: typeof TimelineWaveform;
|
|
326
|
+
Playhead: typeof TimelinePlayhead;
|
|
327
|
+
};
|
|
328
|
+
export { TimelineHeader, TimelineTime, TimelineZoom, TimelineViewport, TimelineRuler, TimelineTrack, TimelineClip, TimelineClipLabel, TimelineWaveform, TimelinePlayhead, timelineVariants, };
|