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
@@ -0,0 +1,153 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from 'react';
4
+ /*
5
+ * The ordered dither the toast's versions are drawn with.
6
+ *
7
+ * A coverage level picks the cells of the 4×4 Bayer matrix that are on, and
8
+ * one SVG tile of those cells, as a CSS mask, cuts an element down to an even
9
+ * dither, as the timer bar is drawn. The card sweep thresholds a tone per
10
+ * column against the same matrix on a canvas.
11
+ */
12
+ /** The 4×4 Bayer matrix, as thresholds 0–15. */
13
+ const BAYER = [
14
+ [0, 8, 2, 10],
15
+ [12, 4, 14, 6],
16
+ [3, 11, 1, 9],
17
+ [15, 7, 13, 5],
18
+ ];
19
+ /** Levels a mask can take: 0 hides everything, 16 shows everything. */
20
+ export const LEVELS = 16;
21
+ const tiles = new Map();
22
+ /** A CSS `url()` of one tile showing the cells lit at `level` (0–16). */
23
+ function tile(level) {
24
+ const cached = tiles.get(level);
25
+ if (cached)
26
+ return cached;
27
+ let rects = '';
28
+ for (let y = 0; y < 4; y++) {
29
+ for (let x = 0; x < 4; x++) {
30
+ if (BAYER[y][x] < level)
31
+ rects += `<rect x='${x}' y='${y}' width='1' height='1'/>`;
32
+ }
33
+ }
34
+ const url = `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4' shape-rendering='crispEdges'>${rects}</svg>`)}")`;
35
+ tiles.set(level, url);
36
+ return url;
37
+ }
38
+ /** Masks an element to an even dither at `level` (0–16), in cells of `cell` px. Full or empty need no mask. */
39
+ export function maskStyle(level, cell = 2) {
40
+ const clamped = Math.max(0, Math.min(LEVELS, Math.round(level)));
41
+ if (clamped >= LEVELS)
42
+ return {};
43
+ const image = clamped === 0 ? 'linear-gradient(transparent, transparent)' : tile(clamped);
44
+ const size = `${cell * 4}px ${cell * 4}px`;
45
+ return {
46
+ maskImage: image,
47
+ WebkitMaskImage: image,
48
+ maskSize: size,
49
+ WebkitMaskSize: size,
50
+ maskRepeat: 'repeat',
51
+ WebkitMaskRepeat: 'repeat',
52
+ };
53
+ }
54
+ export function prefersReducedMotion() {
55
+ return typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
56
+ }
57
+ /** How wide the band of dither cells behind the head is, as a share of the card's width. */
58
+ const BAND = 0.34;
59
+ /** The width of one column of cells, in px: about a character, so the card is built as the text is. */
60
+ const COLUMN = 8;
61
+ /**
62
+ * Builds a card out of dither cells the way a line of text is built: a head
63
+ * moves across it, the columns it has just passed arrive as solid cells in
64
+ * the accent, and they thin out through the dither to show the card. On the
65
+ * way out the columns thicken into solid cells and go, from the start edge.
66
+ *
67
+ * Returns a mask for the card, which shows only what the head has passed (or
68
+ * what the tail has not), and a canvas of the cells, to sit over the card.
69
+ */
70
+ export function useCardSweep(phase, duration) {
71
+ const [progress, setProgress] = useState(phase === 'done' ? 1 : 0);
72
+ useEffect(() => {
73
+ if (phase === 'done')
74
+ return;
75
+ if (prefersReducedMotion()) {
76
+ setProgress(1);
77
+ return;
78
+ }
79
+ const began = performance.now();
80
+ let frame = 0;
81
+ const tick = (time) => {
82
+ const t = Math.min(1, (time - began) / duration);
83
+ setProgress(t);
84
+ if (t < 1)
85
+ frame = requestAnimationFrame(tick);
86
+ };
87
+ setProgress(0);
88
+ frame = requestAnimationFrame(tick);
89
+ return () => cancelAnimationFrame(frame);
90
+ }, [phase, duration]);
91
+ // The head (arriving) or the tail (leaving), in units of the card's width.
92
+ const edge = progress * (1 + BAND);
93
+ let mask = {};
94
+ if (phase === 'in' && progress < 1) {
95
+ const image = `linear-gradient(to right, #000 ${edge * 100}%, transparent ${edge * 100}%)`;
96
+ mask = { maskImage: image, WebkitMaskImage: image };
97
+ }
98
+ else if (phase === 'out') {
99
+ const tail = Math.max(0, edge - BAND);
100
+ const image = `linear-gradient(to right, transparent ${tail * 100}%, #000 ${tail * 100}%)`;
101
+ mask = { maskImage: image, WebkitMaskImage: image };
102
+ }
103
+ const sweeping = phase !== 'done' && progress > 0 && progress < 1;
104
+ return { mask, progress, edge, sweeping };
105
+ }
106
+ /** The cells of a card sweep, drawn over the card in `color`. */
107
+ export function SweepCells({ phase, edge, color, className, }) {
108
+ const canvas = useRef(null);
109
+ useEffect(() => {
110
+ const node = canvas.current;
111
+ const context = node?.getContext('2d');
112
+ if (!node || !context)
113
+ return;
114
+ node.style.color = color;
115
+ const ink = getComputedStyle(node).color;
116
+ const ratio = window.devicePixelRatio || 1;
117
+ const width = node.clientWidth;
118
+ const height = node.clientHeight;
119
+ if (node.width !== Math.round(width * ratio))
120
+ node.width = Math.round(width * ratio);
121
+ if (node.height !== Math.round(height * ratio))
122
+ node.height = Math.round(height * ratio);
123
+ context.setTransform(ratio, 0, 0, ratio, 0, 0);
124
+ context.clearRect(0, 0, width, height);
125
+ if (phase === 'done' || width === 0)
126
+ return;
127
+ context.fillStyle = ink;
128
+ const dot = 2;
129
+ const head = edge * width;
130
+ const band = BAND * width;
131
+ for (let column = 0; column * COLUMN < width; column++) {
132
+ const x = column * COLUMN;
133
+ // How far through its change this column is: 1 just behind the head, 0 at the band's far end.
134
+ const distance = phase === 'in' ? (head - (x + COLUMN)) / band : (x - (head - band)) / band;
135
+ if (distance < 0 || distance > 1)
136
+ continue;
137
+ // Arriving, a column starts solid and thins; leaving, it thickens to solid and goes.
138
+ const tone = phase === 'in' ? 1 - distance : 1 - distance;
139
+ for (let y = 0; y < height; y += dot) {
140
+ for (let dx = 0; dx < COLUMN; dx += dot) {
141
+ const threshold = BAYER[(y / dot) % 4][((x + dx) / dot) % 4];
142
+ if (tone * LEVELS > threshold + 0.5)
143
+ context.fillRect(x + dx, y, dot, dot);
144
+ }
145
+ }
146
+ }
147
+ // The caret rides the head on the way in, as it does on a line of text.
148
+ if (phase === 'in' && head > 0 && head < width) {
149
+ context.fillRect(Math.round(head) - 1, Math.round(height * 0.18), 2, Math.round(height * 0.64));
150
+ }
151
+ }, [phase, edge, color]);
152
+ return _jsx("canvas", { ref: canvas, "aria-hidden": "true", className: className });
153
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Toast — a short message about something that just happened, which leaves on its own.
3
+ *
4
+ * ```tsx
5
+ * <Toast.Provider position="bottom-right">
6
+ * <App />
7
+ * </Toast.Provider>
8
+ *
9
+ * const toast = useToast();
10
+ * toast.add({ title: 'Invoice sent', description: 'Harbour Bakery will get it in a minute.', type: 'success' });
11
+ * toast.promise(save(), { loading: 'Saving draft', success: 'Draft saved', error: 'Could not save' });
12
+ * ```
13
+ *
14
+ * ## The title changes through dither
15
+ *
16
+ * A toast's title is not swapped. When it arrives, it resolves out of a row of
17
+ * dither cells from the start, with a caret riding the edge, the way a line
18
+ * types itself onto a terminal. When it changes — a save going on to say
19
+ * "Draft saved" where it said "Saving draft" — the old words dim, turn
20
+ * monospace, are eaten by an ordered dither and the new ones resolve through
21
+ * the same cells, so a reader
22
+ * sees that this is the same toast with news, not a second one. When it
23
+ * leaves, it dissolves back into the cells before the card fades.
24
+ *
25
+ * The cells and caret are drawn in the toast's colour; the settled text is the
26
+ * ordinary foreground, so it reads as text and not as a highlight. With
27
+ * reduced motion turned on, the title changes in place.
28
+ *
29
+ * ## Versions
30
+ *
31
+ * `effect` on the provider picks one of three. `type`, the default, does the
32
+ * above to the title alone. `materialize` brings the whole card in through an
33
+ * ordered dither first, and only then resolves its title and description
34
+ * through the dither cells, so the toast builds itself in two steps; leaving,
35
+ * the words dissolve and then the card does. `timer` adds a dithered bar along
36
+ * the bottom that runs down the toast's time and pauses while the stack is open.
37
+ *
38
+ * ## The mark is a dither cell too
39
+ *
40
+ * Beside the title, a small square of ordered dither says what kind of toast
41
+ * it is: filled solid in the success, info, warning or destructive colour, or
42
+ * cycling through its densities while a `loading` toast waits.
43
+ *
44
+ * ## Stacked, then spread
45
+ *
46
+ * Several toasts stack into one, with the ones behind peeking out under it.
47
+ * Hovering or focusing the stack spreads them into a list, and pauses their
48
+ * timers while it is open. A toast can be swiped away towards the nearest edge.
49
+ *
50
+ * ## Announced, not focused
51
+ *
52
+ * Toasts are read out by a screen reader as they arrive, politely by default
53
+ * and at once for `priority: 'high'`, without moving focus. F6 moves focus to
54
+ * the stack, so its buttons can be reached from the keyboard. Anything that
55
+ * needs an answer before the reader moves on belongs in an Alert Dialog.
56
+ */
57
+ import { Toast as BaseToast } from '@base-ui/react/toast';
58
+ import { type ReactNode } from 'react';
59
+ export type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
60
+ export type ToastType = 'default' | 'success' | 'info' | 'warning' | 'destructive' | 'loading';
61
+ export type ToastEffect = 'type' | 'materialize' | 'timer';
62
+ /** What a toast can carry in `data`. */
63
+ export interface ToastData {
64
+ /** Which dither this one toast is drawn with, overriding the provider's `effect`. */
65
+ effect?: ToastEffect;
66
+ }
67
+ export interface ToastProviderProps extends Omit<BaseToast.Provider.Props, 'children'> {
68
+ /** Which corner or edge of the screen the toasts gather at. Default `bottom-right`. */
69
+ position?: ToastPosition;
70
+ /**
71
+ * Draws the stack where the provider is, in the flow of the page, instead
72
+ * of pinned to a corner of the screen. For a preview or a thumbnail.
73
+ */
74
+ inline?: boolean;
75
+ /**
76
+ * How the toasts are dithered. `type` writes the title through dither
77
+ * cells. `materialize` dithers the whole card in, then resolves its title and
78
+ * description through dither inside it, and runs the same backwards when it
79
+ * leaves. `timer` writes the title as `type` does and runs a dithered bar down
80
+ * the toast's time. One toast can choose its own with `data: { effect }`.
81
+ * Default `type`.
82
+ */
83
+ effect?: ToastEffect;
84
+ children?: ReactNode;
85
+ }
86
+ /**
87
+ * Holds the toasts and draws them. Put it once near the root; `useToast()`
88
+ * works anywhere inside it. Takes `timeout` (ms, default 5000, `0` to stay),
89
+ * `limit` (default 3) and a `toastManager` from `createToaster()` for
90
+ * toasts raised outside React.
91
+ */
92
+ declare function ToastProvider({ position, inline, effect, children, ...props }: ToastProviderProps): import("react").JSX.Element;
93
+ declare namespace ToastProvider {
94
+ var displayName: string;
95
+ }
96
+ /**
97
+ * Raises, updates and closes toasts. `add({ title, description, type, timeout,
98
+ * actionProps })` returns an id; `update(id, …)` changes one in place, and its
99
+ * title dissolves into the new one; `promise(p, { loading, success, error })`
100
+ * follows a promise through the three; `close(id)` dismisses one, or all.
101
+ */
102
+ export declare const useToast: typeof BaseToast.useToastManager;
103
+ /** A toast manager for code outside React. Pass it to `Toast.Provider` as `toastManager`. */
104
+ export declare const createToaster: typeof BaseToast.createToastManager;
105
+ export declare const Toast: {
106
+ Provider: typeof ToastProvider;
107
+ };
108
+ export { ToastProvider };
@@ -0,0 +1,225 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * Toast — a short message about something that just happened, which leaves on its own.
5
+ *
6
+ * ```tsx
7
+ * <Toast.Provider position="bottom-right">
8
+ * <App />
9
+ * </Toast.Provider>
10
+ *
11
+ * const toast = useToast();
12
+ * toast.add({ title: 'Invoice sent', description: 'Harbour Bakery will get it in a minute.', type: 'success' });
13
+ * toast.promise(save(), { loading: 'Saving draft', success: 'Draft saved', error: 'Could not save' });
14
+ * ```
15
+ *
16
+ * ## The title changes through dither
17
+ *
18
+ * A toast's title is not swapped. When it arrives, it resolves out of a row of
19
+ * dither cells from the start, with a caret riding the edge, the way a line
20
+ * types itself onto a terminal. When it changes — a save going on to say
21
+ * "Draft saved" where it said "Saving draft" — the old words dim, turn
22
+ * monospace, are eaten by an ordered dither and the new ones resolve through
23
+ * the same cells, so a reader
24
+ * sees that this is the same toast with news, not a second one. When it
25
+ * leaves, it dissolves back into the cells before the card fades.
26
+ *
27
+ * The cells and caret are drawn in the toast's colour; the settled text is the
28
+ * ordinary foreground, so it reads as text and not as a highlight. With
29
+ * reduced motion turned on, the title changes in place.
30
+ *
31
+ * ## Versions
32
+ *
33
+ * `effect` on the provider picks one of three. `type`, the default, does the
34
+ * above to the title alone. `materialize` brings the whole card in through an
35
+ * ordered dither first, and only then resolves its title and description
36
+ * through the dither cells, so the toast builds itself in two steps; leaving,
37
+ * the words dissolve and then the card does. `timer` adds a dithered bar along
38
+ * the bottom that runs down the toast's time and pauses while the stack is open.
39
+ *
40
+ * ## The mark is a dither cell too
41
+ *
42
+ * Beside the title, a small square of ordered dither says what kind of toast
43
+ * it is: filled solid in the success, info, warning or destructive colour, or
44
+ * cycling through its densities while a `loading` toast waits.
45
+ *
46
+ * ## Stacked, then spread
47
+ *
48
+ * Several toasts stack into one, with the ones behind peeking out under it.
49
+ * Hovering or focusing the stack spreads them into a list, and pauses their
50
+ * timers while it is open. A toast can be swiped away towards the nearest edge.
51
+ *
52
+ * ## Announced, not focused
53
+ *
54
+ * Toasts are read out by a screen reader as they arrive, politely by default
55
+ * and at once for `priority: 'high'`, without moving focus. F6 moves focus to
56
+ * the stack, so its buttons can be reached from the keyboard. Anything that
57
+ * needs an answer before the reader moves on belongs in an Alert Dialog.
58
+ */
59
+ import { Toast as BaseToast } from '@base-ui/react/toast';
60
+ import { useEffect, useState } from 'react';
61
+ import { tv } from 'tailwind-variants';
62
+ import { maskStyle, prefersReducedMotion, SweepCells, useCardSweep } from './dither.js';
63
+ import { DitherText } from './dither-text.js';
64
+ const toastVariants = tv({
65
+ slots: {
66
+ viewport: [
67
+ 'fixed z-[60] flex w-[calc(100%-2*var(--toast-inset))] max-w-[22.5rem] outline-none [--toast-inset:1rem] sm:[--toast-inset:1.5rem]',
68
+ 'data-[position^=top]:top-(--toast-inset) data-[position^=bottom]:bottom-(--toast-inset)',
69
+ 'data-[position$=left]:left-(--toast-inset) data-[position$=right]:right-(--toast-inset)',
70
+ 'data-[position$=center]:left-1/2 data-[position$=center]:-translate-x-1/2',
71
+ ],
72
+ // In place, for a preview: as tall as the front toast, in the flow of the page.
73
+ inlineViewport: 'relative w-full max-w-[22.5rem] [--toast-inset:0px] h-(--toast-frontmost-height,5rem) min-h-16',
74
+ root: [
75
+ 'group/toast absolute inset-x-0 z-[calc(100-var(--toast-index))] select-none',
76
+ 'rounded-xl border bg-popover text-popover-foreground shadow-lg shadow-black/5 dark:shadow-black/30',
77
+ '[--gap:0.625rem] [--peek:0.5rem] [--shrink:calc(var(--toast-index)*0.05)]',
78
+ // Collapsed, every toast is as tall as the front one, so the ones behind show only as edges.
79
+ 'h-(--toast-frontmost-height,var(--toast-height)) data-expanded:h-(--toast-height)',
80
+ '[transition:transform_0.45s_cubic-bezier(0.22,1,0.36,1),opacity_0.3s,height_0.2s]',
81
+ 'data-[position^=bottom]:bottom-0 data-[position^=bottom]:origin-bottom',
82
+ 'data-[position^=top]:top-0 data-[position^=top]:origin-top',
83
+ // Collapsed: peeking out behind the front one, a little smaller each.
84
+ 'data-[position^=bottom]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-var(--toast-index)*var(--peek)))_scale(calc(1-var(--shrink)))]',
85
+ 'data-[position^=top]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+var(--toast-index)*var(--peek)))_scale(calc(1-var(--shrink)))]',
86
+ // Spread: each at its own place in the list.
87
+ 'data-[position^=bottom]:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-var(--toast-offset-y)-var(--toast-index)*var(--gap)))]',
88
+ 'data-[position^=top]:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+var(--toast-offset-y)+var(--toast-index)*var(--gap)))]',
89
+ // A bridge over the gap, so moving the pointer between spread toasts does not collapse them.
90
+ "after:absolute after:inset-x-0 after:h-(--gap) after:content-[''] data-[position^=bottom]:after:top-full data-[position^=top]:after:bottom-full",
91
+ // Arriving from the edge it is pinned to.
92
+ 'data-[position^=bottom]:data-starting-style:transform-[translateY(calc(100%+var(--toast-inset)))]',
93
+ 'data-[position^=top]:data-starting-style:transform-[translateY(calc(-100%-var(--toast-inset)))]',
94
+ 'data-starting-style:opacity-0 data-limited:opacity-0',
95
+ // Leaving: the title dissolves first, then the card fades. A swipe goes at once.
96
+ 'data-ending-style:opacity-0 data-ending-style:not-data-swipe-direction:[transition-delay:0s,0.32s,0s]',
97
+ // Materialize holds the card until its text and then the card itself have dithered out.
98
+ 'data-[effect=materialize]:data-ending-style:not-data-swipe-direction:[transition-delay:0s,0.9s,0s]',
99
+ 'data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))]',
100
+ 'data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))]',
101
+ 'data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]',
102
+ 'data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]',
103
+ 'motion-reduce:transition-[opacity]',
104
+ ],
105
+ // A layer under or over the content, clipped to the card's corners.
106
+ surface: 'pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit]',
107
+ timer: 'absolute inset-x-0 bottom-0 h-[3px] origin-left bg-(--toast-color) group-data-expanded/toast:[animation-play-state:paused] motion-reduce:[animation:none!important]',
108
+ content: [
109
+ 'relative flex items-start gap-3 overflow-hidden p-4 transition-opacity duration-200',
110
+ // Behind the front one, collapsed, its content would show through the edge.
111
+ 'not-data-expanded:data-behind:opacity-0',
112
+ ],
113
+ mark: 'mt-[0.3125rem] grid size-2.5 shrink-0 grid-cols-2 grid-rows-2 gap-px text-(--toast-color)',
114
+ body: 'flex min-w-0 flex-1 flex-col gap-1',
115
+ title: 'text-sm font-medium leading-5 text-foreground',
116
+ description: 'text-[0.8125rem] leading-5 text-muted-foreground',
117
+ action: [
118
+ 'inline-flex h-7 shrink-0 cursor-pointer items-center rounded-md border bg-background px-2.5 text-xs font-medium text-foreground shadow-xs outline-none',
119
+ 'hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring dark:bg-input/30',
120
+ ],
121
+ close: [
122
+ 'relative -me-1 -mt-1 inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground outline-none',
123
+ 'opacity-0 transition-opacity group-hover/toast:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring',
124
+ 'hover:bg-accent hover:text-foreground pointer-coarse:opacity-100',
125
+ ],
126
+ },
127
+ });
128
+ /** The colour each type's mark and dither are drawn in. */
129
+ const COLOR = {
130
+ default: 'var(--foreground)',
131
+ success: 'var(--success)',
132
+ info: 'var(--info)',
133
+ warning: 'var(--warning)',
134
+ destructive: 'var(--destructive)',
135
+ loading: 'var(--info)',
136
+ };
137
+ /** The order the mark's four cells light in while loading: a 2×2 Bayer matrix, read as thresholds. */
138
+ const BAYER_2 = [0, 2, 3, 1];
139
+ function Mark({ type }) {
140
+ const slots = toastVariants();
141
+ const loading = type === 'loading';
142
+ return (_jsx("span", { "data-slot": "toast-mark", "aria-hidden": "true", className: slots.mark(), children: BAYER_2.map((threshold, index) => (_jsx("span", { className: loading ? 'rounded-[1px] bg-current motion-safe:animate-pulse' : 'rounded-[1px] bg-current', style: loading ? { animationDelay: `${threshold * 0.25}s`, animationDuration: '1s' } : undefined }, index))) }));
143
+ }
144
+ function swipeOf(position) {
145
+ const vertical = position.startsWith('top') ? 'up' : 'down';
146
+ if (position.endsWith('center'))
147
+ return [vertical];
148
+ return [position.endsWith('left') ? 'left' : 'right', vertical];
149
+ }
150
+ function textOf(node) {
151
+ return typeof node === 'string' || typeof node === 'number' ? String(node) : null;
152
+ }
153
+ const DEFAULT_TIMEOUT = 5000;
154
+ /** The keyframes the timer bar runs on, written once per provider. */
155
+ const KEYFRAMES = '@keyframes panelui-toast-timer{from{transform:scaleX(1)}to{transform:scaleX(0)}}' +
156
+ '@keyframes panelui-toast-sweep{from{transform:translateX(-100%)}to{transform:translateX(250%)}}';
157
+ /** How long the card takes to dither in or out, in `materialize`. */
158
+ const CARD_MS = 520;
159
+ /** How long the text takes to dissolve before the card follows it out. */
160
+ const TEXT_OUT_MS = 380;
161
+ /**
162
+ * A line of text that is held at its full size while it dithers in and out,
163
+ * so the toast keeps its height and the stack does not jump as letters arrive.
164
+ */
165
+ function Dithered({ text, show, accent, caret }) {
166
+ return (_jsxs("span", { className: "grid", children: [_jsx("span", { "aria-hidden": "true", className: "invisible col-start-1 row-start-1", children: text }), _jsx("span", { className: "col-start-1 row-start-1", children: _jsx(DitherText, { text: show ? text : '', accent: accent, caret: caret }) })] }));
167
+ }
168
+ function ToastItem({ toast, index, position, swipe, effect: fallback, timeout: defaultTimeout, }) {
169
+ const slots = toastVariants();
170
+ const type = toast.type && toast.type in COLOR ? toast.type : 'default';
171
+ const effect = toast.data?.effect ?? fallback;
172
+ const title = textOf(toast.title);
173
+ const leaving = toast.transitionStatus === 'ending';
174
+ const timeout = toast.timeout ?? defaultTimeout;
175
+ const description = textOf(toast.description);
176
+ const materialize = effect === 'materialize';
177
+ // Materialize runs in order: the card dithers in, then its text resolves
178
+ // through dither; leaving, the text dissolves first and the card follows.
179
+ const [textIn, setTextIn] = useState(!materialize || prefersReducedMotion());
180
+ const [cardOut, setCardOut] = useState(false);
181
+ useEffect(() => {
182
+ if (textIn)
183
+ return;
184
+ const timer = setTimeout(() => setTextIn(true), CARD_MS);
185
+ return () => clearTimeout(timer);
186
+ }, [textIn]);
187
+ useEffect(() => {
188
+ if (!materialize || !leaving)
189
+ return;
190
+ const timer = setTimeout(() => setCardOut(true), prefersReducedMotion() ? 0 : TEXT_OUT_MS);
191
+ return () => clearTimeout(timer);
192
+ }, [materialize, leaving]);
193
+ const phase = !materialize ? 'done' : cardOut ? 'out' : textIn ? 'done' : 'in';
194
+ const sweep = useCardSweep(phase, CARD_MS);
195
+ const showText = textIn && !leaving;
196
+ return (_jsxs(BaseToast.Root, { toast: toast, swipeDirection: swipe, "data-slot": "toast", "data-type": type, "data-effect": effect, "data-position": position, "data-behind": index > 0 ? '' : undefined, className: slots.root(), style: { '--toast-color': COLOR[type], ...sweep.mask }, children: [materialize && phase !== 'done' ? (_jsx("span", { "aria-hidden": "true", className: slots.surface({ className: 'z-20' }), children: _jsx(SweepCells, { phase: phase, edge: sweep.edge, color: COLOR[type], className: "absolute inset-0 size-full" }) })) : null, _jsxs(BaseToast.Content, { className: slots.content(), children: [_jsx(Mark, { type: type }), _jsxs("div", { className: slots.body(), children: [_jsx(BaseToast.Title, { "data-slot": "toast-title", className: slots.title(), children: title !== null ? _jsx(Dithered, { text: title, show: showText, accent: COLOR[type] }) : toast.title }), toast.description ? (_jsx(BaseToast.Description, { "data-slot": "toast-description", className: slots.description(), children: materialize && description !== null ? (_jsx(Dithered, { text: description, show: showText, accent: COLOR[type], caret: false })) : (toast.description) })) : null] }), toast.actionProps ? _jsx(BaseToast.Action, { "data-slot": "toast-action", className: slots.action() }) : null, _jsx(BaseToast.Close, { "data-slot": "toast-close", "aria-label": "Dismiss", className: slots.close(), children: _jsx("svg", { viewBox: "0 0 16 16", "aria-hidden": "true", fill: "none", className: "size-3.5", children: _jsx("path", { d: "M4.5 4.5l7 7m0-7l-7 7", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }) }) })] }), effect === 'timer' ? (_jsx("span", { "aria-hidden": "true", className: slots.surface(), children: timeout > 0 ? (_jsx("span", { "data-slot": "toast-timer", className: slots.timer(), style: { ...maskStyle(10, 1), animation: `panelui-toast-timer ${timeout}ms linear forwards` } }, toast.updateKey ?? 0)) : (_jsx("span", { "data-slot": "toast-timer", className: slots.timer({ className: 'w-2/5 origin-center' }), style: { ...maskStyle(10, 1), animation: 'panelui-toast-sweep 1.4s cubic-bezier(0.45,0,0.55,1) infinite' } })) })) : null] }));
197
+ }
198
+ function ToastList({ position, inline, effect, timeout, }) {
199
+ const { toasts } = BaseToast.useToastManager();
200
+ const slots = toastVariants();
201
+ const swipe = swipeOf(position);
202
+ const viewport = (_jsxs(BaseToast.Viewport, { "data-slot": "toast-viewport", "data-position": position, "data-inline": inline ? '' : undefined, className: inline ? slots.inlineViewport() : slots.viewport(), children: [_jsx("style", { children: KEYFRAMES }), toasts.map((toast, index) => (_jsx(ToastItem, { toast: toast, index: index, position: position, swipe: swipe, effect: effect, timeout: timeout }, toast.id)))] }));
203
+ return inline ? viewport : _jsx(BaseToast.Portal, { children: viewport });
204
+ }
205
+ /**
206
+ * Holds the toasts and draws them. Put it once near the root; `useToast()`
207
+ * works anywhere inside it. Takes `timeout` (ms, default 5000, `0` to stay),
208
+ * `limit` (default 3) and a `toastManager` from `createToaster()` for
209
+ * toasts raised outside React.
210
+ */
211
+ function ToastProvider({ position = 'bottom-right', inline = false, effect = 'type', children, ...props }) {
212
+ return (_jsxs(BaseToast.Provider, { ...props, children: [children, _jsx(ToastList, { position: position, inline: inline, effect: effect, timeout: props.timeout ?? DEFAULT_TIMEOUT })] }));
213
+ }
214
+ ToastProvider.displayName = 'Toast.Provider';
215
+ /**
216
+ * Raises, updates and closes toasts. `add({ title, description, type, timeout,
217
+ * actionProps })` returns an id; `update(id, …)` changes one in place, and its
218
+ * title dissolves into the new one; `promise(p, { loading, success, error })`
219
+ * follows a promise through the three; `close(id)` dismisses one, or all.
220
+ */
221
+ export const useToast = BaseToast.useToastManager;
222
+ /** A toast manager for code outside React. Pass it to `Toast.Provider` as `toastManager`. */
223
+ export const createToaster = BaseToast.createToastManager;
224
+ export const Toast = { Provider: ToastProvider };
225
+ export { ToastProvider };
@@ -1,5 +1,11 @@
1
1
  import { Toolbar as BaseToolbar } from '@base-ui/react/toolbar';
2
2
  import { type ComponentProps, type CSSProperties, type ReactElement, type ReactNode } from 'react';
3
+ import { useRender } from '@base-ui/react/use-render';
4
+ import type { ElementType as RenderElementType } from 'react';
5
+ /** Draws a part as another element, or composes it with a component: `render={<a href="/" />}`. */
6
+ type RenderProp<Tag extends RenderElementType> = {
7
+ render?: useRender.ComponentProps<Tag>['render'];
8
+ };
3
9
  export type ToolbarVariant = 'floating' | 'inline';
4
10
  export type ToolbarSize = 'sm' | 'md' | 'lg';
5
11
  declare const toolbarVariants: import("tailwind-variants").TVReturnType<{
@@ -58,7 +64,7 @@ declare const toolbarVariants: import("tailwind-variants").TVReturnType<{
58
64
  caret: string[];
59
65
  tooltip: string[];
60
66
  kbd: string;
61
- }, import("tailwind-variants").TVReturnType<{
67
+ }, import("tailwind-variants").TVReturnTypeLike<{
62
68
  variant: {
63
69
  floating: {
64
70
  root: string[];
@@ -86,7 +92,7 @@ declare const toolbarVariants: import("tailwind-variants").TVReturnType<{
86
92
  caret: string[];
87
93
  tooltip: string[];
88
94
  kbd: string;
89
- }, undefined, unknown, unknown, undefined>>;
95
+ }>>;
90
96
  export interface ToolbarProps extends Omit<BaseToolbar.Root.Props, 'className' | 'style'> {
91
97
  /** `floating` sits on a raised surface of its own; `inline` draws only the buttons. Default `floating`. */
92
98
  variant?: ToolbarVariant;
@@ -171,7 +177,7 @@ declare namespace ToolbarSeparator {
171
177
  export interface ToolbarCaretProps extends ComponentProps<'span'> {
172
178
  }
173
179
  /** A small chevron after a tool's icon, saying it opens more choices. */
174
- declare function ToolbarCaret({ className, children, ...props }: ToolbarCaretProps): import("react").JSX.Element;
180
+ declare function ToolbarCaret({ className, children, render, ...props }: ToolbarCaretProps & RenderProp<'span'>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
175
181
  declare namespace ToolbarCaret {
176
182
  var displayName: string;
177
183
  }
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  /**
4
4
  * Toolbar — a strip of tools and actions for an editor: one tool chosen at a
5
5
  * time, and buttons that act at once.
@@ -42,6 +42,8 @@ import { Toolbar as BaseToolbar } from '@base-ui/react/toolbar';
42
42
  import { Tooltip } from '@base-ui/react/tooltip';
43
43
  import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
44
44
  import { tv } from 'tailwind-variants';
45
+ import { mergeProps } from '@base-ui/react/merge-props';
46
+ import { useRender } from '@base-ui/react/use-render';
45
47
  const toolbarVariants = tv({
46
48
  slots: {
47
49
  root: [
@@ -101,9 +103,15 @@ const toolbarVariants = tv({
101
103
  inline: {},
102
104
  },
103
105
  size: {
104
- sm: { root: '[--toolbar-item:1.75rem] [--toolbar-icon:0.875rem] [--toolbar-radius:0.5rem] [--toolbar-pad:0.25rem]' },
105
- md: { root: '[--toolbar-item:2.25rem] [--toolbar-icon:1.125rem] [--toolbar-radius:0.75rem] [--toolbar-pad:0.3125rem]' },
106
- lg: { root: '[--toolbar-item:2.75rem] [--toolbar-icon:1.25rem] [--toolbar-radius:0.875rem] [--toolbar-pad:0.375rem]' },
106
+ sm: {
107
+ root: '[--toolbar-item:1.75rem] [--toolbar-icon:0.875rem] [--toolbar-radius:0.5rem] [--toolbar-pad:0.25rem]',
108
+ },
109
+ md: {
110
+ root: '[--toolbar-item:2.25rem] [--toolbar-icon:1.125rem] [--toolbar-radius:0.75rem] [--toolbar-pad:0.3125rem]',
111
+ },
112
+ lg: {
113
+ root: '[--toolbar-item:2.75rem] [--toolbar-icon:1.25rem] [--toolbar-radius:0.875rem] [--toolbar-pad:0.375rem]',
114
+ },
107
115
  },
108
116
  },
109
117
  defaultVariants: { variant: 'floating', size: 'md' },
@@ -255,9 +263,18 @@ function ToolbarSeparator({ className, ...props }) {
255
263
  }
256
264
  ToolbarSeparator.displayName = 'Toolbar.Separator';
257
265
  /** A small chevron after a tool's icon, saying it opens more choices. */
258
- function ToolbarCaret({ className, children, ...props }) {
266
+ function ToolbarCaret({ className, children, render, ...props }) {
259
267
  const slots = toolbarVariants();
260
- return (_jsx("span", { "aria-hidden": true, "data-slot": "toolbar-caret", className: slots.caret({ className }), ...props, children: children ?? (_jsx("svg", { viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M3.5 4.75 6 7.25l2.5-2.5" }) })) }));
268
+ return useRender({
269
+ defaultTagName: 'span',
270
+ render,
271
+ props: mergeProps({
272
+ 'aria-hidden': true,
273
+ 'data-slot': 'toolbar-caret',
274
+ className: slots.caret({ className }),
275
+ children: (_jsx(_Fragment, { children: children ?? (_jsx("svg", { viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M3.5 4.75 6 7.25l2.5-2.5" }) })) })),
276
+ }, props),
277
+ });
261
278
  }
262
279
  ToolbarCaret.displayName = 'Toolbar.Caret';
263
280
  export const Toolbar = Object.assign(ToolbarRoot, {