panelui-web 0.10.0 → 0.11.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/alert/index.d.ts +15 -8
- package/dist/components/alert/index.js +65 -18
- package/dist/components/alert-dialog/index.d.ts +9 -3
- package/dist/components/alert-dialog/index.js +23 -7
- package/dist/components/avatar/index.d.ts +9 -3
- package/dist/components/avatar/index.js +34 -10
- package/dist/components/badge/index.d.ts +57 -0
- package/dist/components/badge/index.js +61 -0
- package/dist/components/bar-picker/index.d.ts +8 -1
- package/dist/components/bar-picker/index.js +35 -26
- package/dist/components/button/index.d.ts +68 -46
- package/dist/components/button/index.js +38 -2
- package/dist/components/button-group/index.d.ts +11 -5
- package/dist/components/button-group/index.js +31 -7
- package/dist/components/canvas/index.d.ts +9 -3
- package/dist/components/canvas/index.js +53 -25
- package/dist/components/card/index.d.ts +13 -7
- package/dist/components/card/index.js +45 -17
- package/dist/components/checkbox/index.d.ts +73 -0
- package/dist/components/checkbox/index.js +100 -0
- package/dist/components/chip/index.d.ts +79 -0
- package/dist/components/chip/index.js +112 -0
- package/dist/components/choice-cards/index.d.ts +11 -5
- package/dist/components/choice-cards/index.js +25 -6
- package/dist/components/command-dock/index.d.ts +16 -9
- package/dist/components/command-dock/index.js +76 -19
- package/dist/components/day-grid/index.d.ts +12 -6
- package/dist/components/day-grid/index.js +71 -27
- package/dist/components/dialog/index.d.ts +19 -12
- package/dist/components/dialog/index.js +84 -25
- package/dist/components/feedback/index.d.ts +20 -13
- package/dist/components/feedback/index.js +83 -27
- package/dist/components/field/index.d.ts +85 -0
- package/dist/components/field/index.js +90 -0
- package/dist/components/flow/index.d.ts +21 -14
- package/dist/components/flow/index.js +56 -20
- package/dist/components/frame/index.d.ts +17 -11
- package/dist/components/frame/index.js +73 -22
- package/dist/components/input/index.d.ts +9 -24
- package/dist/components/input/index.js +53 -6
- package/dist/components/inspector/index.d.ts +69 -63
- package/dist/components/inspector/index.js +31 -9
- package/dist/components/kpi/index.d.ts +20 -14
- package/dist/components/kpi/index.js +111 -36
- package/dist/components/label/index.d.ts +10 -4
- package/dist/components/label/index.js +39 -9
- package/dist/components/lane-board/index.d.ts +15 -9
- package/dist/components/lane-board/index.js +97 -57
- package/dist/components/menu/index.d.ts +10 -4
- package/dist/components/menu/index.js +28 -10
- package/dist/components/message/index.d.ts +15 -9
- package/dist/components/message/index.js +71 -19
- package/dist/components/not-found/index.d.ts +10 -4
- package/dist/components/not-found/index.js +48 -12
- package/dist/components/planet/index.d.ts +11 -5
- package/dist/components/planet/index.js +73 -20
- package/dist/components/prompt/index.d.ts +30 -23
- package/dist/components/prompt/index.js +148 -52
- package/dist/components/radio-group/index.d.ts +124 -0
- package/dist/components/radio-group/index.js +216 -0
- package/dist/components/range-meter/index.d.ts +9 -3
- package/dist/components/range-meter/index.js +61 -27
- package/dist/components/rolling-number/index.d.ts +8 -1
- package/dist/components/rolling-number/index.js +15 -6
- package/dist/components/select/index.d.ts +7 -1
- package/dist/components/select/index.js +13 -4
- package/dist/components/sheet/index.d.ts +8 -2
- package/dist/components/sheet/index.js +27 -15
- package/dist/components/slider/index.d.ts +11 -4
- package/dist/components/slider/index.js +30 -10
- package/dist/components/soundwave/index.d.ts +8 -2
- package/dist/components/soundwave/index.js +26 -6
- package/dist/components/stat/index.d.ts +13 -7
- package/dist/components/stat/index.js +45 -15
- package/dist/components/table/index.d.ts +128 -0
- package/dist/components/table/index.js +145 -0
- package/dist/components/tabs/index.d.ts +7 -1
- package/dist/components/tabs/index.js +8 -2
- package/dist/components/textarea/index.d.ts +0 -23
- package/dist/components/textarea/index.js +22 -5
- package/dist/components/tilt-card/index.d.ts +9 -3
- package/dist/components/tilt-card/index.js +28 -8
- package/dist/components/timeline/index.d.ts +20 -13
- package/dist/components/timeline/index.js +166 -59
- package/dist/components/toast/dither-text.d.ts +13 -0
- package/dist/components/toast/dither-text.js +143 -0
- package/dist/components/toast/dither.d.ts +29 -0
- package/dist/components/toast/dither.js +153 -0
- package/dist/components/toast/index.d.ts +108 -0
- package/dist/components/toast/index.js +225 -0
- package/dist/components/toolbar/index.d.ts +9 -3
- package/dist/components/toolbar/index.js +23 -6
- package/dist/components/workbench/index.d.ts +50 -43
- package/dist/components/workbench/index.js +77 -20
- package/dist/components/xy-pad/index.d.ts +9 -3
- package/dist/components/xy-pad/index.js +13 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
/**
|
|
4
4
|
* Timeline — time laid out left to right: tracks of clips under a ruler,
|
|
5
5
|
* with a playhead, for editing a recording, an animation or a schedule.
|
|
@@ -46,6 +46,8 @@ import { ScrollArea } from '@base-ui/react/scroll-area';
|
|
|
46
46
|
import { Children, createContext, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
47
47
|
import { tv } from 'tailwind-variants';
|
|
48
48
|
import { Slider } from '../slider/index.js';
|
|
49
|
+
import { mergeProps } from '@base-ui/react/merge-props';
|
|
50
|
+
import { useRender } from '@base-ui/react/use-render';
|
|
49
51
|
/** Space before the first second and after the last, in pixels. */
|
|
50
52
|
const GUTTER = 16;
|
|
51
53
|
/** How close, in pixels, an edge has to come to something before it snaps there. */
|
|
@@ -187,8 +189,30 @@ function TimelineRoot({ duration, time: timeProp, defaultTime = 0, onTimeChange,
|
|
|
187
189
|
const playing = playingProp ?? innerPlaying;
|
|
188
190
|
const spans = useRef(new Map()).current;
|
|
189
191
|
const viewport = useRef(null);
|
|
190
|
-
const live = useRef({
|
|
191
|
-
|
|
192
|
+
const live = useRef({
|
|
193
|
+
time,
|
|
194
|
+
zoom,
|
|
195
|
+
duration,
|
|
196
|
+
loop,
|
|
197
|
+
onTimeChange,
|
|
198
|
+
onZoomChange,
|
|
199
|
+
onPlayingChange,
|
|
200
|
+
timeProp,
|
|
201
|
+
zoomProp,
|
|
202
|
+
playingProp,
|
|
203
|
+
});
|
|
204
|
+
live.current = {
|
|
205
|
+
time,
|
|
206
|
+
zoom,
|
|
207
|
+
duration,
|
|
208
|
+
loop,
|
|
209
|
+
onTimeChange,
|
|
210
|
+
onZoomChange,
|
|
211
|
+
onPlayingChange,
|
|
212
|
+
timeProp,
|
|
213
|
+
zoomProp,
|
|
214
|
+
playingProp,
|
|
215
|
+
};
|
|
192
216
|
const setTime = useCallback((next) => {
|
|
193
217
|
const value = clamp(next, 0, live.current.duration);
|
|
194
218
|
// Seen by the playback loop at once, so a jump back to the start is not
|
|
@@ -212,7 +236,10 @@ function TimelineRoot({ duration, time: timeProp, defaultTime = 0, onTimeChange,
|
|
|
212
236
|
const box = viewport.current;
|
|
213
237
|
if (!box)
|
|
214
238
|
return;
|
|
215
|
-
const at = anchor ?? {
|
|
239
|
+
const at = anchor ?? {
|
|
240
|
+
time: live.current.time,
|
|
241
|
+
x: GUTTER + live.current.time * live.current.zoom - box.scrollLeft,
|
|
242
|
+
};
|
|
216
243
|
requestAnimationFrame(() => {
|
|
217
244
|
box.scrollLeft = GUTTER + at.time * value - at.x;
|
|
218
245
|
});
|
|
@@ -263,27 +290,47 @@ function TimelineRoot({ duration, time: timeProp, defaultTime = 0, onTimeChange,
|
|
|
263
290
|
TimelineRoot.displayName = 'Timeline';
|
|
264
291
|
const GuideContext = createContext(null);
|
|
265
292
|
/** A row above the tracks, for a toolbar, the time and the zoom. */
|
|
266
|
-
function TimelineHeader({ className, ...props }) {
|
|
293
|
+
function TimelineHeader({ className, render, ...props }) {
|
|
267
294
|
const slots = timelineVariants();
|
|
268
|
-
return
|
|
295
|
+
return useRender({
|
|
296
|
+
defaultTagName: 'div',
|
|
297
|
+
render,
|
|
298
|
+
props: mergeProps({ 'data-slot': 'timeline-header', className: slots.header({ className }) }, props),
|
|
299
|
+
});
|
|
269
300
|
}
|
|
270
301
|
TimelineHeader.displayName = 'Timeline.Header';
|
|
271
302
|
/** Where the playhead is, written out, and how long the whole is. */
|
|
272
|
-
function TimelineTime({ showDuration = true, className, ...props }) {
|
|
303
|
+
function TimelineTime({ showDuration = true, className, render, ...props }) {
|
|
273
304
|
const { time, duration } = useTimelineContext('Timeline.Time');
|
|
274
305
|
const slots = timelineVariants();
|
|
275
|
-
return (
|
|
306
|
+
return useRender({
|
|
307
|
+
defaultTagName: 'span',
|
|
308
|
+
render,
|
|
309
|
+
props: mergeProps({
|
|
310
|
+
'data-slot': 'timeline-time',
|
|
311
|
+
className: slots.time({ className }),
|
|
312
|
+
children: (_jsxs(_Fragment, { children: [_jsx("b", { children: formatTime(time, true, isShort(duration)) }), showDuration ? ` / ${formatTime(duration, isShort(duration), isShort(duration))}` : null] })),
|
|
313
|
+
}, props),
|
|
314
|
+
});
|
|
276
315
|
}
|
|
277
316
|
TimelineTime.displayName = 'Timeline.Time';
|
|
278
317
|
/** A line slider that zooms the timeline, with zoom out and zoom in either side. */
|
|
279
|
-
function TimelineZoom({ className, ...props }) {
|
|
318
|
+
function TimelineZoom({ className, render, ...props }) {
|
|
280
319
|
const { zoom, setZoom, minZoom, maxZoom } = useTimelineContext('Timeline.Zoom');
|
|
281
320
|
const slots = timelineVariants();
|
|
282
321
|
// The slider runs on a log scale, so each stretch of it zooms by the same factor.
|
|
283
322
|
const toSlider = (value) => Math.log(value / minZoom) / Math.log(maxZoom / minZoom);
|
|
284
323
|
const fromSlider = (value) => minZoom * Math.pow(maxZoom / minZoom, value);
|
|
285
324
|
const step = 0.125;
|
|
286
|
-
return (
|
|
325
|
+
return useRender({
|
|
326
|
+
defaultTagName: 'div',
|
|
327
|
+
render,
|
|
328
|
+
props: mergeProps({
|
|
329
|
+
'data-slot': 'timeline-zoom',
|
|
330
|
+
className: slots.zoom({ className }),
|
|
331
|
+
children: (_jsx(_Fragment, { children: _jsxs(Slider, { variant: "line", label: "Zoom", min: 0, max: 1, step: 0.001, value: toSlider(zoom), onValueChange: (value) => setZoom(fromSlider(value)), format: () => `${Math.round(zoom)} pixels per second`, children: [_jsx(Slider.Step, { by: -step, "aria-label": "Zoom out", children: _jsxs("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", children: [_jsx("circle", { cx: "7", cy: "7", r: "4.5" }), _jsx("path", { d: "M5 7h4M10.5 10.5 13.5 13.5" })] }) }), _jsx(Slider.Step, { by: step, "aria-label": "Zoom in", children: _jsxs("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", children: [_jsx("circle", { cx: "7", cy: "7", r: "4.5" }), _jsx("path", { d: "M5 7h4M7 5v4M10.5 10.5 13.5 13.5" })] }) })] }) })),
|
|
332
|
+
}, props),
|
|
333
|
+
});
|
|
287
334
|
}
|
|
288
335
|
TimelineZoom.displayName = 'Timeline.Zoom';
|
|
289
336
|
/**
|
|
@@ -313,7 +360,7 @@ function TimelineViewport({ className, style, children, ...props }) {
|
|
|
313
360
|
}, [viewport, setZoom]);
|
|
314
361
|
const live = useRef(zoom);
|
|
315
362
|
live.current = zoom;
|
|
316
|
-
return (_jsxs(ScrollArea.Root, { "data-slot": "timeline-viewport", className: slots.viewport({ className }), children: [_jsx(ScrollArea.Viewport, { ref: viewport, className: "outline-none", children: _jsxs(ScrollArea.Content, { dir: "ltr", className: slots.content(), style: { ...style, width, minWidth: '100%', ['--timeline-zoom']: zoom }, ...props, children: [_jsx("span", { "aria-hidden": true, className: slots.end(), style: { left: GUTTER + duration * zoom, right: 0 } }), children, guide !== null ? _jsx("span", { "aria-hidden": true, "data-slot": "timeline-guide", className: slots.guide(), style: { left: GUTTER + guide * zoom } }) : null] }) }), _jsx(ScrollArea.Scrollbar, { orientation: "horizontal", className: slots.scrollbar(), children: _jsx(ScrollArea.Thumb, { className: slots.thumb() }) })] }));
|
|
363
|
+
return (_jsxs(ScrollArea.Root, { "data-slot": "timeline-viewport", className: slots.viewport({ className }), children: [_jsx(ScrollArea.Viewport, { ref: viewport, className: "outline-none", children: _jsxs(ScrollArea.Content, { dir: "ltr", className: slots.content(), style: { ...style, width, minWidth: '100%', ['--timeline-zoom']: zoom }, ...props, children: [_jsx("span", { "aria-hidden": true, className: slots.end(), style: { left: GUTTER + duration * zoom, right: 0 } }), children, guide !== null ? (_jsx("span", { "aria-hidden": true, "data-slot": "timeline-guide", className: slots.guide(), style: { left: GUTTER + guide * zoom } })) : null] }) }), _jsx(ScrollArea.Scrollbar, { orientation: "horizontal", className: slots.scrollbar(), children: _jsx(ScrollArea.Thumb, { className: slots.thumb() }) })] }));
|
|
317
364
|
}
|
|
318
365
|
TimelineViewport.displayName = 'Timeline.Viewport';
|
|
319
366
|
/** Turns a pointer's position into a time on the timeline. */
|
|
@@ -328,7 +375,7 @@ function useTimeAt() {
|
|
|
328
375
|
}, [viewport, zoom, duration]);
|
|
329
376
|
}
|
|
330
377
|
/** Ticks and times along the top. Pressing or dragging on it moves the playhead there. */
|
|
331
|
-
function TimelineRuler({ className, ...props }) {
|
|
378
|
+
function TimelineRuler({ className, render, ...props }) {
|
|
332
379
|
const { duration, zoom, setTime } = useTimelineContext('Timeline.Ruler');
|
|
333
380
|
const slots = timelineVariants();
|
|
334
381
|
const timeAt = useTimeAt();
|
|
@@ -352,10 +399,21 @@ function TimelineRuler({ className, ...props }) {
|
|
|
352
399
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
353
400
|
setTime(timeAt(event.clientX));
|
|
354
401
|
};
|
|
355
|
-
return (
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
402
|
+
return useRender({
|
|
403
|
+
defaultTagName: 'div',
|
|
404
|
+
render,
|
|
405
|
+
props: mergeProps({
|
|
406
|
+
'aria-hidden': true,
|
|
407
|
+
'data-slot': 'timeline-ruler',
|
|
408
|
+
className: slots.ruler({ className }),
|
|
409
|
+
onPointerDown: scrub,
|
|
410
|
+
onPointerMove: (event) => {
|
|
411
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId))
|
|
412
|
+
setTime(timeAt(event.clientX));
|
|
413
|
+
},
|
|
414
|
+
children: _jsx(_Fragment, { children: ticks }),
|
|
415
|
+
}, props),
|
|
416
|
+
});
|
|
359
417
|
}
|
|
360
418
|
TimelineRuler.displayName = 'Timeline.Ruler';
|
|
361
419
|
const TrackContext = createContext(null);
|
|
@@ -369,7 +427,7 @@ function TimelineTrack({ size = 'md', label, className, ...props }) {
|
|
|
369
427
|
}
|
|
370
428
|
TimelineTrack.displayName = 'Timeline.Track';
|
|
371
429
|
/** One clip. With `onChange` it moves by its middle and trims by its ends. */
|
|
372
|
-
function TimelineClip({ start, end, onChange, onChangeEnd, onRemove, variant: variantProp, color = 'primary', label, className, style, children, onKeyDown, ...props }) {
|
|
430
|
+
function TimelineClip({ start, end, onChange, onChangeEnd, onRemove, variant: variantProp, color = 'primary', label, className, style, children, onKeyDown, render, ...props }) {
|
|
373
431
|
const timeline = useTimelineContext('Timeline.Clip');
|
|
374
432
|
const track = useContext(TrackContext);
|
|
375
433
|
if (!track)
|
|
@@ -425,7 +483,9 @@ function TimelineClip({ start, end, onChange, onChangeEnd, onRemove, variant: va
|
|
|
425
483
|
return;
|
|
426
484
|
event.preventDefault();
|
|
427
485
|
event.stopPropagation();
|
|
428
|
-
event.currentTarget
|
|
486
|
+
event.currentTarget
|
|
487
|
+
.closest('[data-slot=timeline-clip]')
|
|
488
|
+
?.focus({ preventScroll: true, focusVisible: false });
|
|
429
489
|
const originX = event.clientX;
|
|
430
490
|
const origin = { start: live.current.start, end: live.current.end };
|
|
431
491
|
const { lower, upper } = room();
|
|
@@ -504,14 +564,32 @@ function TimelineClip({ start, end, onChange, onChangeEnd, onRemove, variant: va
|
|
|
504
564
|
const width = Math.max((end - start) * zoom, 2);
|
|
505
565
|
const name = `${label ? `${label}, ` : ''}${formatTime(start, true, isShort(duration))} to ${formatTime(end, true, isShort(duration))}`;
|
|
506
566
|
const roomy = width >= 40;
|
|
507
|
-
return (
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
'
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
567
|
+
return useRender({
|
|
568
|
+
defaultTagName: 'div',
|
|
569
|
+
render,
|
|
570
|
+
props: mergeProps({
|
|
571
|
+
role: 'button',
|
|
572
|
+
tabIndex: 0,
|
|
573
|
+
'aria-roledescription': 'clip',
|
|
574
|
+
'aria-label': name,
|
|
575
|
+
'data-slot': 'timeline-clip',
|
|
576
|
+
'data-variant': variant,
|
|
577
|
+
'data-editable': editable ? '' : undefined,
|
|
578
|
+
'data-dragging': dragging ? '' : undefined,
|
|
579
|
+
className: slots.clip({ className }),
|
|
580
|
+
style: {
|
|
581
|
+
...style,
|
|
582
|
+
left,
|
|
583
|
+
width,
|
|
584
|
+
'--clip': COLORS[color],
|
|
585
|
+
// Text on a warning-coloured clip is dark; on the others it is white.
|
|
586
|
+
...(color === 'warning' ? { '--clip-foreground': 'rgb(0 0 0 / 0.8)' } : null),
|
|
587
|
+
},
|
|
588
|
+
onPointerDown: (event) => press(event, 'move'),
|
|
589
|
+
onKeyDown: keys,
|
|
590
|
+
children: (_jsxs(_Fragment, { children: [variant === 'block' && roomy && !hasLabel(children) ? (_jsxs("span", { "aria-hidden": true, className: slots.grip(), children: [_jsx("span", {}), _jsx("span", {})] })) : null, _jsx(ClipWidthContext.Provider, { value: width, children: children }), editable && width >= 28 ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, "data-slot": "timeline-clip-handle", className: slots.handle({ className: 'start-0 after:start-1' }), onPointerDown: (event) => press(event, 'start') }), _jsx("span", { "aria-hidden": true, "data-slot": "timeline-clip-handle", className: slots.handle({ className: 'end-0 after:end-1' }), onPointerDown: (event) => press(event, 'end') })] })) : null] })),
|
|
591
|
+
}, props),
|
|
592
|
+
});
|
|
515
593
|
}
|
|
516
594
|
TimelineClip.displayName = 'Timeline.Clip';
|
|
517
595
|
function hasLabel(children) {
|
|
@@ -523,13 +601,17 @@ function hasLabel(children) {
|
|
|
523
601
|
return found;
|
|
524
602
|
}
|
|
525
603
|
/** Words on a clip, with an icon: its length, its speed, what it is. */
|
|
526
|
-
function TimelineClipLabel({ className, ...props }) {
|
|
604
|
+
function TimelineClipLabel({ className, render, ...props }) {
|
|
527
605
|
const slots = timelineVariants();
|
|
528
|
-
return
|
|
606
|
+
return useRender({
|
|
607
|
+
defaultTagName: 'span',
|
|
608
|
+
render,
|
|
609
|
+
props: mergeProps({ 'data-slot': 'timeline-clip-label', className: slots.clipLabel({ className }) }, props),
|
|
610
|
+
});
|
|
529
611
|
}
|
|
530
612
|
TimelineClipLabel.displayName = 'Timeline.ClipLabel';
|
|
531
613
|
/** The sound of a media clip, drawn as bars across it. */
|
|
532
|
-
function TimelineWaveform({ peaks, className, ...props }) {
|
|
614
|
+
function TimelineWaveform({ peaks, className, render, ...props }) {
|
|
533
615
|
const slots = timelineVariants();
|
|
534
616
|
const width = useContext(ClipWidthContext);
|
|
535
617
|
// One bar every 4 pixels at any zoom: the peaks are resampled to the room
|
|
@@ -554,7 +636,18 @@ function TimelineWaveform({ peaks, className, ...props }) {
|
|
|
554
636
|
// Rounded, so the server and the browser write the same numbers.
|
|
555
637
|
return Math.round(Math.max(clamp(loudest, 0, 1) * 100, 3) * 100) / 100;
|
|
556
638
|
});
|
|
557
|
-
return (
|
|
639
|
+
return useRender({
|
|
640
|
+
defaultTagName: 'svg',
|
|
641
|
+
render,
|
|
642
|
+
props: mergeProps({
|
|
643
|
+
'aria-hidden': true,
|
|
644
|
+
'data-slot': 'timeline-waveform',
|
|
645
|
+
viewBox: `0 0 ${bars * 4} 100`,
|
|
646
|
+
preserveAspectRatio: 'none',
|
|
647
|
+
className: slots.waveform({ className }),
|
|
648
|
+
children: (_jsx(_Fragment, { children: heights.map((height, at) => (_jsx("rect", { x: at * 4 + 1, y: Math.round((50 - height / 2) * 100) / 100, width: 2, height: height, rx: 1, fill: "currentColor" }, at))) })),
|
|
649
|
+
}, props),
|
|
650
|
+
});
|
|
558
651
|
}
|
|
559
652
|
TimelineWaveform.displayName = 'Timeline.Waveform';
|
|
560
653
|
/**
|
|
@@ -562,40 +655,54 @@ TimelineWaveform.displayName = 'Timeline.Waveform';
|
|
|
562
655
|
* be dragged, and with focus the arrow keys step it: a tenth of a second,
|
|
563
656
|
* or a second with Shift.
|
|
564
657
|
*/
|
|
565
|
-
function TimelinePlayhead({ color = 'primary', showTime = true, className, style, ...props }) {
|
|
658
|
+
function TimelinePlayhead({ color = 'primary', showTime = true, className, style, render, ...props }) {
|
|
566
659
|
const { time, setTime, duration, zoom, playing, setPlaying } = useTimelineContext('Timeline.Playhead');
|
|
567
660
|
const slots = timelineVariants();
|
|
568
661
|
const timeAt = useTimeAt();
|
|
569
662
|
const left = GUTTER + time * zoom;
|
|
570
663
|
const foreground = color === 'primary' ? 'var(--primary-foreground)' : color === 'warning' ? 'black' : 'white';
|
|
571
|
-
return (
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
event
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
664
|
+
return useRender({
|
|
665
|
+
defaultTagName: 'div',
|
|
666
|
+
render,
|
|
667
|
+
props: mergeProps({
|
|
668
|
+
'data-slot': 'timeline-playhead',
|
|
669
|
+
className: slots.playhead({ className }),
|
|
670
|
+
style: {
|
|
671
|
+
...style,
|
|
672
|
+
left,
|
|
673
|
+
['--playhead']: COLORS[color],
|
|
674
|
+
['--playhead-foreground']: foreground,
|
|
675
|
+
},
|
|
676
|
+
children: (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, className: slots.playheadLine() }), _jsx("div", { role: "slider", tabIndex: 0, "aria-label": "Playhead", "aria-valuemin": 0, "aria-valuemax": duration, "aria-valuenow": Math.round(time * 10) / 10, "aria-valuetext": formatTime(time, true, isShort(duration)), "data-slot": "timeline-playhead-head", className: slots.playheadHead(), onPointerDown: (event) => {
|
|
677
|
+
if (event.button !== 0)
|
|
678
|
+
return;
|
|
679
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
680
|
+
if (playing)
|
|
681
|
+
setPlaying(false);
|
|
682
|
+
}, onPointerMove: (event) => {
|
|
683
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId))
|
|
684
|
+
setTime(timeAt(event.clientX));
|
|
685
|
+
}, onKeyDown: (event) => {
|
|
686
|
+
const by = event.shiftKey ? 1 : 0.1;
|
|
687
|
+
const moves = {
|
|
688
|
+
ArrowLeft: time - by,
|
|
689
|
+
ArrowRight: time + by,
|
|
690
|
+
Home: 0,
|
|
691
|
+
End: duration,
|
|
692
|
+
};
|
|
693
|
+
if (event.key === ' ') {
|
|
694
|
+
event.preventDefault();
|
|
695
|
+
setPlaying(!playing);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const next = moves[event.key];
|
|
699
|
+
if (next === undefined)
|
|
700
|
+
return;
|
|
701
|
+
event.preventDefault();
|
|
702
|
+
setTime(next);
|
|
703
|
+
}, children: showTime ? formatTime(time, isShort(duration), isShort(duration)) : null })] })),
|
|
704
|
+
}, props),
|
|
705
|
+
});
|
|
599
706
|
}
|
|
600
707
|
TimelinePlayhead.displayName = 'Timeline.Playhead';
|
|
601
708
|
export const Timeline = Object.assign(TimelineRoot, {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface DitherTextProps {
|
|
2
|
+
/** The text to show. A change dissolves the old text into the new. */
|
|
3
|
+
text: string;
|
|
4
|
+
/** Whether the first text arrives through dither, as a change from nothing does. Default `true`. */
|
|
5
|
+
animateOnMount?: boolean;
|
|
6
|
+
/** Draws a caret at the arriving edge while the text changes. Default `true`. */
|
|
7
|
+
caret?: boolean;
|
|
8
|
+
/** The colour the dither cells and the caret are drawn in. Settled letters keep the text colour. */
|
|
9
|
+
accent?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
/** A line of text whose changes dissolve through ordered dither. */
|
|
13
|
+
export declare function DitherText({ text, animateOnMount, caret, accent, className }: DitherTextProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { prefersReducedMotion } from './dither.js';
|
|
5
|
+
/*
|
|
6
|
+
* Text that changes by dissolving through dither.
|
|
7
|
+
*
|
|
8
|
+
* Every character of the old text goes dim and monospace, flickers through a
|
|
9
|
+
* few stand-in glyphs and is eaten by an ordered dither, densest first, until
|
|
10
|
+
* it is a solid cell. The new text then resolves from the start: a head moves
|
|
11
|
+
* along it, each cell behind the head thins out through the same dither and
|
|
12
|
+
* settles into its letter, and a caret rides the head. Characters are drawn as
|
|
13
|
+
* spans rather than on a canvas, so the type is the page's own, crisp at any
|
|
14
|
+
* zoom, and the cells take `currentColor`.
|
|
15
|
+
*
|
|
16
|
+
* The dither cells are CSS: four Bayer-like densities built from conic and
|
|
17
|
+
* radial gradients on a 2–3px tile, plus a solid block. No image, no canvas.
|
|
18
|
+
*/
|
|
19
|
+
/** Ordered densities, from the sparsest dust to a solid cell. */
|
|
20
|
+
const CELL = [
|
|
21
|
+
{ backgroundImage: 'radial-gradient(currentColor 0.55px, transparent 0.75px)', backgroundSize: '3px 3px' },
|
|
22
|
+
{ backgroundImage: 'conic-gradient(currentColor 0 25%, transparent 0)', backgroundSize: '2px 2px' },
|
|
23
|
+
{ backgroundImage: 'repeating-conic-gradient(currentColor 0 25%, transparent 0 50%)', backgroundSize: '2px 2px' },
|
|
24
|
+
{ backgroundImage: 'conic-gradient(transparent 0 25%, currentColor 0)', backgroundSize: '2px 2px' },
|
|
25
|
+
{ backgroundColor: 'currentColor' },
|
|
26
|
+
];
|
|
27
|
+
const SCRAMBLE = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
28
|
+
/** Milliseconds between one character starting to go and the next. */
|
|
29
|
+
const OUT_STAGGER = 14;
|
|
30
|
+
/** How long one character takes from letter to solid cell. */
|
|
31
|
+
const OUT_SPAN = 240;
|
|
32
|
+
/** When the new text starts arriving, after the old started going. */
|
|
33
|
+
const IN_DELAY = 150;
|
|
34
|
+
/** Milliseconds between one new character starting to arrive and the next, for a short line. */
|
|
35
|
+
const IN_STAGGER = 30;
|
|
36
|
+
/** The longest a whole line takes to start arriving; a long line's characters come closer together. */
|
|
37
|
+
const IN_SWEEP = 560;
|
|
38
|
+
/** The gap between characters arriving, so that a long line takes no longer than `IN_SWEEP`. */
|
|
39
|
+
function staggerFor(length) {
|
|
40
|
+
return length > 1 ? Math.min(IN_STAGGER, IN_SWEEP / (length - 1)) : IN_STAGGER;
|
|
41
|
+
}
|
|
42
|
+
/** How long one new character takes from cell to letter. */
|
|
43
|
+
const IN_SPAN = 210;
|
|
44
|
+
/** How long the caret stays once the text has arrived. */
|
|
45
|
+
const CARET_HOLD = 520;
|
|
46
|
+
function scramble(seed) {
|
|
47
|
+
return SCRAMBLE[Math.abs(Math.floor(seed)) % SCRAMBLE.length];
|
|
48
|
+
}
|
|
49
|
+
/** What the character at `index` looks like `elapsed` ms into a change from `from` to `to`. */
|
|
50
|
+
function glyphAt(from, to, index, elapsed) {
|
|
51
|
+
const incoming = elapsed - IN_DELAY - index * staggerFor(to.length);
|
|
52
|
+
const target = to[index];
|
|
53
|
+
if (target !== undefined && incoming >= 0) {
|
|
54
|
+
if (target === ' ')
|
|
55
|
+
return { kind: 'gap' };
|
|
56
|
+
const p = incoming / IN_SPAN;
|
|
57
|
+
if (p >= 1)
|
|
58
|
+
return { kind: 'char', char: target };
|
|
59
|
+
// Arriving: a solid cell thins out, flickers as a letter, then settles.
|
|
60
|
+
if (p < 0.72)
|
|
61
|
+
return { kind: 'cell', level: CELL.length - 1 - Math.floor((p / 0.72) * (CELL.length - 1)) };
|
|
62
|
+
return { kind: 'mono', char: p < 0.86 ? scramble(index * 7 + elapsed / 40) : target };
|
|
63
|
+
}
|
|
64
|
+
const source = from[index];
|
|
65
|
+
if (source === undefined) {
|
|
66
|
+
// A position only the new text has, which the head has not reached.
|
|
67
|
+
return target === undefined ? { kind: 'gap' } : { kind: 'cell', level: 0 };
|
|
68
|
+
}
|
|
69
|
+
if (source === ' ')
|
|
70
|
+
return { kind: 'gap' };
|
|
71
|
+
const p = (elapsed - index * Math.min(OUT_STAGGER, IN_SWEEP / Math.max(1, from.length - 1))) / OUT_SPAN;
|
|
72
|
+
if (p < 0)
|
|
73
|
+
return { kind: 'char', char: source };
|
|
74
|
+
// Going: dim and monospace, a few stand-in letters, then dither fills it in.
|
|
75
|
+
if (p < 0.3)
|
|
76
|
+
return { kind: 'mono', char: p < 0.12 ? source : scramble(index * 13 + elapsed / 45) };
|
|
77
|
+
const level = Math.min(CELL.length - 1, Math.floor(((p - 0.3) / 0.7) * CELL.length));
|
|
78
|
+
// A position the new text does not reach thins back to nothing once solid.
|
|
79
|
+
if (target === undefined && p >= 1) {
|
|
80
|
+
const fade = (p - 1) * CELL.length;
|
|
81
|
+
return fade >= CELL.length ? { kind: 'gap' } : { kind: 'cell', level: Math.max(0, CELL.length - 1 - Math.floor(fade)) };
|
|
82
|
+
}
|
|
83
|
+
return { kind: 'cell', level };
|
|
84
|
+
}
|
|
85
|
+
function durationOf(from, to) {
|
|
86
|
+
const arrive = to.length > 0 ? IN_DELAY + (to.length - 1) * staggerFor(to.length) + IN_SPAN : 0;
|
|
87
|
+
// A tail of the old text that the new one does not cover goes solid and then thins away.
|
|
88
|
+
const leave = from.length > to.length ? (from.length - 1) * Math.min(OUT_STAGGER, IN_SWEEP / Math.max(1, from.length - 1)) + OUT_SPAN * 2 : 0;
|
|
89
|
+
return Math.max(arrive, leave);
|
|
90
|
+
}
|
|
91
|
+
/** A line of text whose changes dissolve through ordered dither. */
|
|
92
|
+
export function DitherText({ text, animateOnMount = true, caret = true, accent, className }) {
|
|
93
|
+
const [shown, setShown] = useState(animateOnMount ? '' : text);
|
|
94
|
+
const transition = useRef(null);
|
|
95
|
+
const [now, setNow] = useState(0);
|
|
96
|
+
const frame = useRef(0);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (text === shown && !transition.current)
|
|
99
|
+
return;
|
|
100
|
+
if (prefersReducedMotion()) {
|
|
101
|
+
transition.current = null;
|
|
102
|
+
setShown(text);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// An effect run again for the same text (as React does in development)
|
|
106
|
+
// carries on with the change already under way, rather than restarting it.
|
|
107
|
+
if (transition.current?.to !== text) {
|
|
108
|
+
// A change mid-change starts from what is on screen now, as near as text allows.
|
|
109
|
+
const from = transition.current ? transition.current.to : shown;
|
|
110
|
+
transition.current = { from, to: text, start: performance.now() };
|
|
111
|
+
}
|
|
112
|
+
const { from } = transition.current;
|
|
113
|
+
const total = durationOf(from, text) + (caret ? CARET_HOLD : 0);
|
|
114
|
+
const tick = (time) => {
|
|
115
|
+
const current = transition.current;
|
|
116
|
+
if (!current)
|
|
117
|
+
return;
|
|
118
|
+
if (time - current.start >= total) {
|
|
119
|
+
transition.current = null;
|
|
120
|
+
setShown(current.to);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
setNow(time);
|
|
124
|
+
frame.current = requestAnimationFrame(tick);
|
|
125
|
+
};
|
|
126
|
+
cancelAnimationFrame(frame.current);
|
|
127
|
+
frame.current = requestAnimationFrame(tick);
|
|
128
|
+
return () => cancelAnimationFrame(frame.current);
|
|
129
|
+
// `shown` is read as the starting point only.
|
|
130
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
131
|
+
}, [text, caret]);
|
|
132
|
+
const active = transition.current;
|
|
133
|
+
const label = _jsx("span", { className: "sr-only", children: text });
|
|
134
|
+
if (!active) {
|
|
135
|
+
return (_jsx("span", { "data-slot": "dither-text", className: className, children: shown }));
|
|
136
|
+
}
|
|
137
|
+
const elapsed = Math.max(0, now - active.start);
|
|
138
|
+
const length = Math.max(active.from.length, active.to.length);
|
|
139
|
+
const head = Math.min(active.to.length, Math.max(0, Math.floor((elapsed - IN_DELAY) / staggerFor(active.to.length)) + 1));
|
|
140
|
+
const arriving = elapsed < durationOf(active.from, active.to);
|
|
141
|
+
const glyphs = Array.from({ length }, (_, index) => glyphAt(active.from, active.to, index, elapsed));
|
|
142
|
+
return (_jsxs("span", { "data-slot": "dither-text", "data-changing": "", className: className, children: [label, _jsxs("span", { "aria-hidden": "true", className: "whitespace-pre", style: accent ? { '--dither-accent': accent } : undefined, children: [glyphs.map((glyph, index) => (_jsxs("span", { className: "relative", children: [glyph.kind === 'char' ? glyph.char : null, glyph.kind === 'gap' ? ' ' : null, glyph.kind === 'mono' ? (_jsx("span", { className: "font-mono text-[0.92em] text-(--dither-accent,currentColor) opacity-55", children: glyph.char })) : null, glyph.kind === 'cell' ? (_jsx("span", { className: "inline-block h-[0.82em] w-[0.6em] align-[-0.06em] text-(--dither-accent,currentColor)", style: CELL[glyph.level] })) : null, caret && index === head - 1 && (arriving || Math.floor(elapsed / 260) % 2 === 0) ? (_jsx("span", { className: "absolute inset-y-[0.08em] -end-[0.08em] w-[0.09em] bg-(--dither-accent,currentColor)" })) : null] }, index))), caret && head === 0 ? (_jsx("span", { className: "inline-block h-[0.84em] w-[0.09em] align-[-0.06em] bg-(--dither-accent,currentColor)" })) : null] })] }));
|
|
143
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type CSSProperties } from 'react';
|
|
2
|
+
/** Levels a mask can take: 0 hides everything, 16 shows everything. */
|
|
3
|
+
export declare const LEVELS = 16;
|
|
4
|
+
/** Masks an element to an even dither at `level` (0–16), in cells of `cell` px. Full or empty need no mask. */
|
|
5
|
+
export declare function maskStyle(level: number, cell?: number): CSSProperties;
|
|
6
|
+
export declare function prefersReducedMotion(): boolean;
|
|
7
|
+
export type SweepPhase = 'in' | 'out' | 'done';
|
|
8
|
+
/**
|
|
9
|
+
* Builds a card out of dither cells the way a line of text is built: a head
|
|
10
|
+
* moves across it, the columns it has just passed arrive as solid cells in
|
|
11
|
+
* the accent, and they thin out through the dither to show the card. On the
|
|
12
|
+
* way out the columns thicken into solid cells and go, from the start edge.
|
|
13
|
+
*
|
|
14
|
+
* Returns a mask for the card, which shows only what the head has passed (or
|
|
15
|
+
* what the tail has not), and a canvas of the cells, to sit over the card.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useCardSweep(phase: SweepPhase, duration: number): {
|
|
18
|
+
mask: CSSProperties;
|
|
19
|
+
progress: number;
|
|
20
|
+
edge: number;
|
|
21
|
+
sweeping: boolean;
|
|
22
|
+
};
|
|
23
|
+
/** The cells of a card sweep, drawn over the card in `color`. */
|
|
24
|
+
export declare function SweepCells({ phase, edge, color, className, }: {
|
|
25
|
+
phase: SweepPhase;
|
|
26
|
+
edge: number;
|
|
27
|
+
color: string;
|
|
28
|
+
className?: string;
|
|
29
|
+
}): import("react").JSX.Element;
|