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.
Files changed (99) hide show
  1. package/dist/components/alert/index.d.ts +15 -8
  2. package/dist/components/alert/index.js +65 -18
  3. package/dist/components/alert-dialog/index.d.ts +9 -3
  4. package/dist/components/alert-dialog/index.js +23 -7
  5. package/dist/components/avatar/index.d.ts +9 -3
  6. package/dist/components/avatar/index.js +34 -10
  7. package/dist/components/badge/index.d.ts +57 -0
  8. package/dist/components/badge/index.js +61 -0
  9. package/dist/components/bar-picker/index.d.ts +8 -1
  10. package/dist/components/bar-picker/index.js +35 -26
  11. package/dist/components/button/index.d.ts +68 -46
  12. package/dist/components/button/index.js +38 -2
  13. package/dist/components/button-group/index.d.ts +11 -5
  14. package/dist/components/button-group/index.js +31 -7
  15. package/dist/components/canvas/index.d.ts +9 -3
  16. package/dist/components/canvas/index.js +53 -25
  17. package/dist/components/card/index.d.ts +13 -7
  18. package/dist/components/card/index.js +45 -17
  19. package/dist/components/checkbox/index.d.ts +73 -0
  20. package/dist/components/checkbox/index.js +100 -0
  21. package/dist/components/chip/index.d.ts +79 -0
  22. package/dist/components/chip/index.js +112 -0
  23. package/dist/components/choice-cards/index.d.ts +11 -5
  24. package/dist/components/choice-cards/index.js +25 -6
  25. package/dist/components/command-dock/index.d.ts +16 -9
  26. package/dist/components/command-dock/index.js +76 -19
  27. package/dist/components/day-grid/index.d.ts +12 -6
  28. package/dist/components/day-grid/index.js +71 -27
  29. package/dist/components/dialog/index.d.ts +19 -12
  30. package/dist/components/dialog/index.js +84 -25
  31. package/dist/components/feedback/index.d.ts +20 -13
  32. package/dist/components/feedback/index.js +83 -27
  33. package/dist/components/field/index.d.ts +85 -0
  34. package/dist/components/field/index.js +90 -0
  35. package/dist/components/flow/index.d.ts +21 -14
  36. package/dist/components/flow/index.js +56 -20
  37. package/dist/components/frame/index.d.ts +17 -11
  38. package/dist/components/frame/index.js +73 -22
  39. package/dist/components/input/index.d.ts +9 -24
  40. package/dist/components/input/index.js +53 -6
  41. package/dist/components/inspector/index.d.ts +69 -63
  42. package/dist/components/inspector/index.js +31 -9
  43. package/dist/components/kpi/index.d.ts +20 -14
  44. package/dist/components/kpi/index.js +111 -36
  45. package/dist/components/label/index.d.ts +10 -4
  46. package/dist/components/label/index.js +39 -9
  47. package/dist/components/lane-board/index.d.ts +15 -9
  48. package/dist/components/lane-board/index.js +97 -57
  49. package/dist/components/menu/index.d.ts +10 -4
  50. package/dist/components/menu/index.js +28 -10
  51. package/dist/components/message/index.d.ts +15 -9
  52. package/dist/components/message/index.js +71 -19
  53. package/dist/components/not-found/index.d.ts +10 -4
  54. package/dist/components/not-found/index.js +48 -12
  55. package/dist/components/planet/index.d.ts +11 -5
  56. package/dist/components/planet/index.js +73 -20
  57. package/dist/components/prompt/index.d.ts +30 -23
  58. package/dist/components/prompt/index.js +148 -52
  59. package/dist/components/radio-group/index.d.ts +124 -0
  60. package/dist/components/radio-group/index.js +216 -0
  61. package/dist/components/range-meter/index.d.ts +9 -3
  62. package/dist/components/range-meter/index.js +61 -27
  63. package/dist/components/rolling-number/index.d.ts +8 -1
  64. package/dist/components/rolling-number/index.js +15 -6
  65. package/dist/components/select/index.d.ts +7 -1
  66. package/dist/components/select/index.js +13 -4
  67. package/dist/components/sheet/index.d.ts +8 -2
  68. package/dist/components/sheet/index.js +27 -15
  69. package/dist/components/slider/index.d.ts +11 -4
  70. package/dist/components/slider/index.js +30 -10
  71. package/dist/components/soundwave/index.d.ts +8 -2
  72. package/dist/components/soundwave/index.js +26 -6
  73. package/dist/components/stat/index.d.ts +13 -7
  74. package/dist/components/stat/index.js +45 -15
  75. package/dist/components/table/index.d.ts +128 -0
  76. package/dist/components/table/index.js +145 -0
  77. package/dist/components/tabs/index.d.ts +7 -1
  78. package/dist/components/tabs/index.js +8 -2
  79. package/dist/components/textarea/index.d.ts +0 -23
  80. package/dist/components/textarea/index.js +22 -5
  81. package/dist/components/tilt-card/index.d.ts +9 -3
  82. package/dist/components/tilt-card/index.js +28 -8
  83. package/dist/components/timeline/index.d.ts +20 -13
  84. package/dist/components/timeline/index.js +166 -59
  85. package/dist/components/toast/dither-text.d.ts +13 -0
  86. package/dist/components/toast/dither-text.js +143 -0
  87. package/dist/components/toast/dither.d.ts +29 -0
  88. package/dist/components/toast/dither.js +153 -0
  89. package/dist/components/toast/index.d.ts +108 -0
  90. package/dist/components/toast/index.js +225 -0
  91. package/dist/components/toolbar/index.d.ts +9 -3
  92. package/dist/components/toolbar/index.js +23 -6
  93. package/dist/components/workbench/index.d.ts +50 -43
  94. package/dist/components/workbench/index.js +77 -20
  95. package/dist/components/xy-pad/index.d.ts +9 -3
  96. package/dist/components/xy-pad/index.js +13 -2
  97. package/dist/index.d.ts +7 -0
  98. package/dist/index.js +7 -0
  99. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
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({ time, zoom, duration, loop, onTimeChange, onZoomChange, onPlayingChange, timeProp, zoomProp, playingProp });
191
- live.current = { time, zoom, duration, loop, onTimeChange, onZoomChange, onPlayingChange, timeProp, zoomProp, playingProp };
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 ?? { time: live.current.time, x: GUTTER + live.current.time * live.current.zoom - box.scrollLeft };
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 _jsx("div", { "data-slot": "timeline-header", className: slots.header({ className }), ...props });
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 (_jsxs("span", { "data-slot": "timeline-time", className: slots.time({ className }), ...props, children: [_jsx("b", { children: formatTime(time, true, isShort(duration)) }), showDuration ? ` / ${formatTime(duration, isShort(duration), isShort(duration))}` : null] }));
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 (_jsx("div", { "data-slot": "timeline-zoom", className: slots.zoom({ className }), ...props, 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" })] }) })] }) }));
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 (_jsx("div", { "aria-hidden": true, "data-slot": "timeline-ruler", className: slots.ruler({ className }), onPointerDown: scrub, onPointerMove: (event) => {
356
- if (event.currentTarget.hasPointerCapture(event.pointerId))
357
- setTime(timeAt(event.clientX));
358
- }, ...props, children: ticks }));
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.closest('[data-slot=timeline-clip]')?.focus({ preventScroll: true, focusVisible: false });
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 (_jsxs("div", { role: "button", tabIndex: 0, "aria-roledescription": "clip", "aria-label": name, "data-slot": "timeline-clip", "data-variant": variant, "data-editable": editable ? '' : undefined, "data-dragging": dragging ? '' : undefined, className: slots.clip({ className }), style: {
508
- ...style,
509
- left,
510
- width,
511
- '--clip': COLORS[color],
512
- // Text on a warning-coloured clip is dark; on the others it is white.
513
- ...(color === 'warning' ? { '--clip-foreground': 'rgb(0 0 0 / 0.8)' } : null),
514
- }, onPointerDown: (event) => press(event, 'move'), onKeyDown: keys, ...props, 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] }));
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 _jsx("span", { "data-slot": "timeline-clip-label", className: slots.clipLabel({ className }), ...props });
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 (_jsx("svg", { "aria-hidden": true, "data-slot": "timeline-waveform", viewBox: `0 0 ${bars * 4} 100`, preserveAspectRatio: "none", className: slots.waveform({ className }), ...props, 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))) }));
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 (_jsxs("div", { "data-slot": "timeline-playhead", className: slots.playhead({ className }), style: { ...style, left, ['--playhead']: COLORS[color], ['--playhead-foreground']: foreground }, ...props, 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) => {
572
- if (event.button !== 0)
573
- return;
574
- event.currentTarget.setPointerCapture(event.pointerId);
575
- if (playing)
576
- setPlaying(false);
577
- }, onPointerMove: (event) => {
578
- if (event.currentTarget.hasPointerCapture(event.pointerId))
579
- setTime(timeAt(event.clientX));
580
- }, onKeyDown: (event) => {
581
- const by = event.shiftKey ? 1 : 0.1;
582
- const moves = {
583
- ArrowLeft: time - by,
584
- ArrowRight: time + by,
585
- Home: 0,
586
- End: duration,
587
- };
588
- if (event.key === ' ') {
589
- event.preventDefault();
590
- setPlaying(!playing);
591
- return;
592
- }
593
- const next = moves[event.key];
594
- if (next === undefined)
595
- return;
596
- event.preventDefault();
597
- setTime(next);
598
- }, children: showTime ? formatTime(time, isShort(duration), isShort(duration)) : null })] }));
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;