panelui-web 0.7.0 → 0.9.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 +138 -0
- package/dist/components/alert/index.js +279 -0
- package/dist/components/alert-dialog/index.d.ts +128 -0
- package/dist/components/alert-dialog/index.js +327 -0
- package/dist/components/button-group/index.d.ts +156 -0
- package/dist/components/button-group/index.js +78 -0
- package/dist/components/canvas/index.d.ts +114 -0
- package/dist/components/canvas/index.js +210 -0
- package/dist/components/dialog/index.d.ts +162 -0
- package/dist/components/dialog/index.js +299 -0
- package/dist/components/prompt/index.d.ts +34 -8
- package/dist/components/prompt/index.js +43 -13
- package/dist/components/sheet/index.d.ts +47 -1
- package/dist/components/sheet/index.js +251 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +6 -1
- package/package.json +1 -1
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* AlertDialog — a question that has to be answered before anything else on
|
|
5
|
+
* the page can be done: delete this, discard that, sign out everywhere.
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <AlertDialog>
|
|
9
|
+
* <AlertDialog.Trigger render={<Button variant="destructive" />}>Delete project</AlertDialog.Trigger>
|
|
10
|
+
* <AlertDialog.Content tone="destructive">
|
|
11
|
+
* <AlertDialog.Title>Delete Orchard?</AlertDialog.Title>
|
|
12
|
+
* <AlertDialog.Description>Its 14 boards and their history go with it.</AlertDialog.Description>
|
|
13
|
+
* <AlertDialog.Footer>
|
|
14
|
+
* <AlertDialog.Cancel />
|
|
15
|
+
* <AlertDialog.Action onClick={remove}>Delete project</AlertDialog.Action>
|
|
16
|
+
* </AlertDialog.Footer>
|
|
17
|
+
* </AlertDialog.Content>
|
|
18
|
+
* </AlertDialog>
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* ## A press outside is Cancel
|
|
22
|
+
*
|
|
23
|
+
* A press anywhere outside the dialog, Escape and `AlertDialog.Cancel` all
|
|
24
|
+
* close it without doing anything; an action closes it once it has done its
|
|
25
|
+
* work. `dismissible={false}` takes the outside press away, for a question
|
|
26
|
+
* that should not be waved off by a stray click. While an action is running,
|
|
27
|
+
* nothing closes it.
|
|
28
|
+
*
|
|
29
|
+
* ## The question, then the answers
|
|
30
|
+
*
|
|
31
|
+
* It is drawn as a panel in a darker band, the way Feedback is: the panel
|
|
32
|
+
* holds the question, and `AlertDialog.Footer` moves out of it into the band
|
|
33
|
+
* underneath, where the answers sit side by side at equal widths. Neither
|
|
34
|
+
* answer is made to look like the default by being bigger.
|
|
35
|
+
*
|
|
36
|
+
* ## Actions that take time, or nerve
|
|
37
|
+
*
|
|
38
|
+
* An action whose `onClick` returns a promise shows a spinner and keeps the
|
|
39
|
+
* dialog open, and unclosable, until the promise settles: closed if it
|
|
40
|
+
* resolved, still open if it failed, so the failure can be shown in it.
|
|
41
|
+
* `hold` makes an action one that must be pressed and held while a fill
|
|
42
|
+
* runs across it, for a step that cannot be undone.
|
|
43
|
+
*/
|
|
44
|
+
// The dialog primitive rather than the alert-dialog one, which refuses every
|
|
45
|
+
// press outside; the role is still `alertdialog`, set on the popup.
|
|
46
|
+
import { Dialog as Primitive } from '@base-ui/react/dialog';
|
|
47
|
+
import { Children, createContext, isValidElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, } from 'react';
|
|
48
|
+
import { tv } from 'tailwind-variants';
|
|
49
|
+
import { Button } from '../button/index.js';
|
|
50
|
+
const alertDialogVariants = tv({
|
|
51
|
+
slots: {
|
|
52
|
+
backdrop: 'fixed inset-0 z-50 bg-black/40 transition-opacity duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0 motion-reduce:transition-none',
|
|
53
|
+
viewport: 'fixed inset-0 z-50 grid place-items-center overflow-y-auto p-4',
|
|
54
|
+
/*
|
|
55
|
+
* The band is the popover surface with black mixed in, so it is the
|
|
56
|
+
* darker of the two in a light theme and a dark one alike. 38 and 30 are
|
|
57
|
+
* one corner: the panel's radius is the band's less the 8px between them.
|
|
58
|
+
*/
|
|
59
|
+
shell: 'relative flex w-full flex-col rounded-[38px] bg-[color-mix(in_oklab,var(--popover),black_5%)] p-2 text-popover-foreground shadow-xl outline-none ring-1 ring-foreground/[0.06] dark:bg-[color-mix(in_oklab,var(--popover),black_14%)]',
|
|
60
|
+
popup: 'transition-[opacity,scale,translate] duration-200 ease-[cubic-bezier(0.2,0,0,1)] data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:translate-y-2 data-starting-style:scale-95 data-starting-style:opacity-0 motion-reduce:transition-none',
|
|
61
|
+
panel: 'flex flex-col gap-2 rounded-[30px] bg-popover p-5 shadow-xs',
|
|
62
|
+
media: [
|
|
63
|
+
'mb-2 flex size-12 shrink-0 items-center justify-center rounded-full',
|
|
64
|
+
'bg-[color-mix(in_oklab,var(--pu-tone)_14%,transparent)] text-[var(--pu-tone)] [&_svg:not([class*=size-])]:size-6',
|
|
65
|
+
],
|
|
66
|
+
title: 'text-[19px] font-semibold leading-6 tracking-[-0.01em]',
|
|
67
|
+
description: 'text-[15px] leading-[22px] text-muted-foreground',
|
|
68
|
+
// Held in from the band's edge further than the panel is, so the row of
|
|
69
|
+
// answers does not read as a third edge of the dialog.
|
|
70
|
+
footer: 'flex items-center gap-3 px-[18px] pb-2.5 pt-4',
|
|
71
|
+
action: 'relative isolate h-11 min-w-0 flex-1 overflow-hidden rounded-full px-4 text-[15px]',
|
|
72
|
+
// Drawn under the label and over the button's own fill, from the start edge.
|
|
73
|
+
fill: 'pointer-events-none absolute inset-0 -z-10 origin-left scale-x-0 bg-[color-mix(in_oklab,currentColor_24%,transparent)] rtl:origin-right',
|
|
74
|
+
},
|
|
75
|
+
variants: {
|
|
76
|
+
palette: {
|
|
77
|
+
neutral: { shell: '[--pu-tone:var(--muted-foreground)]' },
|
|
78
|
+
info: { shell: '[--pu-tone:var(--info)]' },
|
|
79
|
+
success: { shell: '[--pu-tone:var(--success)]' },
|
|
80
|
+
warning: { shell: '[--pu-tone:var(--warning)]' },
|
|
81
|
+
destructive: { shell: '[--pu-tone:var(--destructive)]' },
|
|
82
|
+
},
|
|
83
|
+
size: {
|
|
84
|
+
default: { shell: 'max-w-md' },
|
|
85
|
+
// Narrow, so the question is centred over the two answers.
|
|
86
|
+
sm: { shell: 'max-w-xs', panel: 'items-center text-center' },
|
|
87
|
+
},
|
|
88
|
+
blur: {
|
|
89
|
+
true: { backdrop: 'bg-black/20 backdrop-blur-md' },
|
|
90
|
+
},
|
|
91
|
+
/** How much an answer asks to be pressed: filled, or a tint of its colour. */
|
|
92
|
+
weight: {
|
|
93
|
+
solid: {},
|
|
94
|
+
soft: {},
|
|
95
|
+
},
|
|
96
|
+
actionTone: {
|
|
97
|
+
primary: {},
|
|
98
|
+
destructive: {},
|
|
99
|
+
neutral: {},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
compoundVariants: [
|
|
103
|
+
{
|
|
104
|
+
weight: 'soft',
|
|
105
|
+
actionTone: 'primary',
|
|
106
|
+
className: { action: 'bg-foreground/10 text-foreground hover:not-disabled:bg-foreground/15 hover:not-disabled:text-foreground' },
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
weight: 'soft',
|
|
110
|
+
actionTone: 'neutral',
|
|
111
|
+
className: {
|
|
112
|
+
action: 'bg-foreground/10 text-muted-foreground hover:not-disabled:bg-foreground/15 hover:not-disabled:text-foreground',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
weight: 'soft',
|
|
117
|
+
actionTone: 'destructive',
|
|
118
|
+
className: {
|
|
119
|
+
action: 'bg-[color-mix(in_oklab,var(--destructive)_12%,transparent)] text-destructive hover:not-disabled:bg-[color-mix(in_oklab,var(--destructive)_18%,transparent)] hover:not-disabled:text-destructive',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
defaultVariants: { palette: 'neutral', size: 'default' },
|
|
124
|
+
});
|
|
125
|
+
const AlertDialogContext = createContext(null);
|
|
126
|
+
function useAlertDialog(part) {
|
|
127
|
+
const context = useContext(AlertDialogContext);
|
|
128
|
+
if (!context)
|
|
129
|
+
throw new Error(`${part} must be used inside an <AlertDialog>.`);
|
|
130
|
+
return context;
|
|
131
|
+
}
|
|
132
|
+
function AlertDialogRoot({ children, open, defaultOpen = false, onOpenChange, dismissible = true, }) {
|
|
133
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
134
|
+
const resolvedOpen = open ?? internalOpen;
|
|
135
|
+
const [busy, setBusy] = useState(false);
|
|
136
|
+
const setOpen = useCallback((next) => {
|
|
137
|
+
if (open === undefined)
|
|
138
|
+
setInternalOpen(next);
|
|
139
|
+
onOpenChange?.(next);
|
|
140
|
+
}, [open, onOpenChange]);
|
|
141
|
+
const context = useMemo(() => ({ open: resolvedOpen, setOpen, busy, setBusy, tone: 'neutral', inline: false }), [resolvedOpen, setOpen, busy]);
|
|
142
|
+
return (_jsx(AlertDialogContext.Provider, { value: context, children: _jsx(Primitive.Root, { open: resolvedOpen, disablePointerDismissal: !dismissible, onOpenChange: (next) => {
|
|
143
|
+
// While an action runs, the answer is already being acted on.
|
|
144
|
+
if (!next && busy)
|
|
145
|
+
return;
|
|
146
|
+
setOpen(next);
|
|
147
|
+
}, children: children }) }));
|
|
148
|
+
}
|
|
149
|
+
AlertDialogRoot.displayName = 'AlertDialog';
|
|
150
|
+
/** What opens the dialog. A plain button unless `render` supplies one. */
|
|
151
|
+
function AlertDialogTrigger({ render, children, ...props }) {
|
|
152
|
+
return (_jsx(Primitive.Trigger, { render: render, ...props, children: children }));
|
|
153
|
+
}
|
|
154
|
+
AlertDialogTrigger.displayName = 'AlertDialog.Trigger';
|
|
155
|
+
const isFooter = (child) => isValidElement(child) && child.type === AlertDialogFooter;
|
|
156
|
+
/** The dialog: the panel with the question, in a band that holds the answers. */
|
|
157
|
+
function AlertDialogContent({ tone = 'neutral', size = 'default', blur = false, inline = false, className, children, ...props }) {
|
|
158
|
+
const dialog = useAlertDialog('AlertDialog.Content');
|
|
159
|
+
const slots = alertDialogVariants({ palette: tone, size, blur });
|
|
160
|
+
const context = useMemo(() => ({ ...dialog, tone, inline }), [dialog, tone, inline]);
|
|
161
|
+
// The footer leaves the panel for the band under it.
|
|
162
|
+
const all = Children.toArray(children);
|
|
163
|
+
const footer = all.filter(isFooter);
|
|
164
|
+
const inside = (_jsxs(AlertDialogContext.Provider, { value: context, children: [_jsx("div", { "data-slot": "alert-dialog-panel", className: slots.panel(), children: all.filter((child) => !isFooter(child)) }), footer] }));
|
|
165
|
+
if (inline) {
|
|
166
|
+
return (_jsx("div", { "data-slot": "alert-dialog-content", "data-tone": tone, className: slots.shell({ className }), ...props, children: inside }));
|
|
167
|
+
}
|
|
168
|
+
return (_jsxs(Primitive.Portal, { children: [_jsx(Primitive.Backdrop, { className: slots.backdrop() }), _jsx(Primitive.Viewport, { className: slots.viewport(), children: _jsx(Primitive.Popup, { role: "alertdialog", "data-slot": "alert-dialog-content", "data-tone": tone, className: slots.shell({ className: [slots.popup(), className] }), ...props, children: inside }) })] }));
|
|
169
|
+
}
|
|
170
|
+
AlertDialogContent.displayName = 'AlertDialog.Content';
|
|
171
|
+
/** A glyph in a disc of the dialog's tone, above the title. */
|
|
172
|
+
function AlertDialogMedia({ className, ...props }) {
|
|
173
|
+
return _jsx("div", { "data-slot": "alert-dialog-media", "aria-hidden": "true", className: alertDialogVariants().media({ className }), ...props });
|
|
174
|
+
}
|
|
175
|
+
AlertDialogMedia.displayName = 'AlertDialog.Media';
|
|
176
|
+
/** The question. It is what the dialog is announced as. */
|
|
177
|
+
function AlertDialogTitle({ className, ...props }) {
|
|
178
|
+
const { inline } = useAlertDialog('AlertDialog.Title');
|
|
179
|
+
const slots = alertDialogVariants();
|
|
180
|
+
if (inline)
|
|
181
|
+
return _jsx("h2", { "data-slot": "alert-dialog-title", className: slots.title({ className }), ...props });
|
|
182
|
+
return _jsx(Primitive.Title, { "data-slot": "alert-dialog-title", className: slots.title({ className }), ...props });
|
|
183
|
+
}
|
|
184
|
+
AlertDialogTitle.displayName = 'AlertDialog.Title';
|
|
185
|
+
/** What follows from each answer, read out after the title. */
|
|
186
|
+
function AlertDialogDescription({ className, ...props }) {
|
|
187
|
+
const { inline } = useAlertDialog('AlertDialog.Description');
|
|
188
|
+
const slots = alertDialogVariants();
|
|
189
|
+
if (inline)
|
|
190
|
+
return _jsx("p", { "data-slot": "alert-dialog-description", className: slots.description({ className }), ...props });
|
|
191
|
+
return _jsx(Primitive.Description, { "data-slot": "alert-dialog-description", className: slots.description({ className }), ...props });
|
|
192
|
+
}
|
|
193
|
+
AlertDialogDescription.displayName = 'AlertDialog.Description';
|
|
194
|
+
/**
|
|
195
|
+
* The answers, side by side at equal widths in the band under the panel.
|
|
196
|
+
* Keep it a direct child of `AlertDialog.Content`, so the band can find it.
|
|
197
|
+
*/
|
|
198
|
+
function AlertDialogFooter({ className, ...props }) {
|
|
199
|
+
return _jsx("div", { "data-slot": "alert-dialog-footer", className: alertDialogVariants().footer({ className }), ...props });
|
|
200
|
+
}
|
|
201
|
+
AlertDialogFooter.displayName = 'AlertDialog.Footer';
|
|
202
|
+
/** Closes the dialog without doing anything. It reads "Cancel" unless given children. */
|
|
203
|
+
function AlertDialogCancel({ className, children = 'Cancel', disabled, ...props }) {
|
|
204
|
+
const dialog = useAlertDialog('AlertDialog.Cancel');
|
|
205
|
+
const slots = alertDialogVariants({ weight: 'soft', actionTone: 'neutral' });
|
|
206
|
+
const button = (_jsx(Button, { variant: "ghost", "data-slot": "alert-dialog-cancel", disabled: disabled || dialog.busy, className: slots.action({ className }), ...props }));
|
|
207
|
+
if (dialog.inline)
|
|
208
|
+
return _jsx(Button, { ...button.props, children: children });
|
|
209
|
+
return _jsx(Primitive.Close, { render: button, children: children });
|
|
210
|
+
}
|
|
211
|
+
AlertDialogCancel.displayName = 'AlertDialog.Cancel';
|
|
212
|
+
/** Holding starts on a press and ends on a release; a click on its own does nothing. */
|
|
213
|
+
function useHold(duration, onDone) {
|
|
214
|
+
const [holding, setHolding] = useState(false);
|
|
215
|
+
const timer = useRef(undefined);
|
|
216
|
+
// When the last press or key went down, so a click that no press preceded,
|
|
217
|
+
// as a screen reader sends, can be told apart.
|
|
218
|
+
const pressedAt = useRef(0);
|
|
219
|
+
const done = useRef(onDone);
|
|
220
|
+
done.current = onDone;
|
|
221
|
+
const stop = useCallback(() => {
|
|
222
|
+
window.clearTimeout(timer.current);
|
|
223
|
+
setHolding(false);
|
|
224
|
+
}, []);
|
|
225
|
+
const start = useCallback(() => {
|
|
226
|
+
if (duration === undefined)
|
|
227
|
+
return;
|
|
228
|
+
pressedAt.current = Date.now();
|
|
229
|
+
window.clearTimeout(timer.current);
|
|
230
|
+
setHolding(true);
|
|
231
|
+
timer.current = window.setTimeout(() => {
|
|
232
|
+
setHolding(false);
|
|
233
|
+
done.current();
|
|
234
|
+
}, duration);
|
|
235
|
+
}, [duration]);
|
|
236
|
+
useEffect(() => () => window.clearTimeout(timer.current), []);
|
|
237
|
+
return {
|
|
238
|
+
holding,
|
|
239
|
+
pressed: () => Date.now() - pressedAt.current < (duration ?? 0) + 1000,
|
|
240
|
+
handlers: duration === undefined
|
|
241
|
+
? {}
|
|
242
|
+
: {
|
|
243
|
+
onPointerDown: (event) => {
|
|
244
|
+
if (event.button === 0)
|
|
245
|
+
start();
|
|
246
|
+
},
|
|
247
|
+
onPointerUp: stop,
|
|
248
|
+
onPointerLeave: stop,
|
|
249
|
+
onPointerCancel: stop,
|
|
250
|
+
onKeyDown: (event) => {
|
|
251
|
+
if ((event.key === ' ' || event.key === 'Enter') && !event.repeat) {
|
|
252
|
+
// Enter would click at once; the hold decides instead.
|
|
253
|
+
event.preventDefault();
|
|
254
|
+
start();
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
onKeyUp: (event) => {
|
|
258
|
+
if (event.key === ' ' || event.key === 'Enter') {
|
|
259
|
+
event.preventDefault();
|
|
260
|
+
stop();
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
onContextMenu: (event) => event.preventDefault(),
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/** Does what the dialog asks about, then closes it. */
|
|
268
|
+
function AlertDialogAction({ onClick, tone, variant = 'solid', hold, loading, disabled, className, children, ...props }) {
|
|
269
|
+
const dialog = useAlertDialog('AlertDialog.Action');
|
|
270
|
+
const resolvedTone = tone ?? (dialog.tone === 'destructive' ? 'destructive' : 'primary');
|
|
271
|
+
const slots = alertDialogVariants({ weight: variant, actionTone: resolvedTone });
|
|
272
|
+
const [pending, setPending] = useState(false);
|
|
273
|
+
const hintId = useId();
|
|
274
|
+
const run = (event) => {
|
|
275
|
+
const result = onClick?.(event);
|
|
276
|
+
if (event.defaultPrevented)
|
|
277
|
+
return;
|
|
278
|
+
if (result && typeof result.then === 'function') {
|
|
279
|
+
setPending(true);
|
|
280
|
+
dialog.setBusy(true);
|
|
281
|
+
result.then(() => {
|
|
282
|
+
setPending(false);
|
|
283
|
+
dialog.setBusy(false);
|
|
284
|
+
dialog.setOpen(false);
|
|
285
|
+
}, () => {
|
|
286
|
+
setPending(false);
|
|
287
|
+
dialog.setBusy(false);
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
dialog.setOpen(false);
|
|
292
|
+
};
|
|
293
|
+
// A hold that completes acts as a click on the button would.
|
|
294
|
+
const button = useRef(null);
|
|
295
|
+
const confirmed = useRef(false);
|
|
296
|
+
const { holding, pressed, handlers } = useHold(hold, () => {
|
|
297
|
+
confirmed.current = true;
|
|
298
|
+
button.current?.click();
|
|
299
|
+
});
|
|
300
|
+
return (_jsxs(Button, { ref: button, "data-slot": "alert-dialog-action", "data-holding": holding ? '' : undefined, variant: variant === 'solid' ? (resolvedTone === 'destructive' ? 'destructive' : 'default') : 'ghost', loading: loading || pending, disabled: disabled || (dialog.busy && !pending), "aria-describedby": hold !== undefined ? hintId : undefined, className: slots.action({ className }), ...handlers, onClick: (event) => {
|
|
301
|
+
if (hold !== undefined) {
|
|
302
|
+
const wasHeld = confirmed.current;
|
|
303
|
+
confirmed.current = false;
|
|
304
|
+
// A click no press or key preceded comes from assistive technology,
|
|
305
|
+
// which cannot hold; it confirms. Any other click must be a hold.
|
|
306
|
+
if (!wasHeld && pressed())
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
run(event);
|
|
310
|
+
}, ...props, children: [hold !== undefined ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", "data-slot": "alert-dialog-action-fill", className: slots.fill({ className: holding ? 'scale-x-100' : undefined }), style: {
|
|
311
|
+
transitionProperty: 'scale, transform',
|
|
312
|
+
transitionDuration: holding ? `${hold}ms` : '250ms',
|
|
313
|
+
transitionTimingFunction: holding ? 'linear' : 'cubic-bezier(0.2,0,0,1)',
|
|
314
|
+
} }), _jsx("span", { id: hintId, className: "sr-only", children: "Press and hold to confirm" })] })) : null, children] }));
|
|
315
|
+
}
|
|
316
|
+
AlertDialogAction.displayName = 'AlertDialog.Action';
|
|
317
|
+
export const AlertDialog = Object.assign(AlertDialogRoot, {
|
|
318
|
+
Trigger: AlertDialogTrigger,
|
|
319
|
+
Content: AlertDialogContent,
|
|
320
|
+
Media: AlertDialogMedia,
|
|
321
|
+
Title: AlertDialogTitle,
|
|
322
|
+
Description: AlertDialogDescription,
|
|
323
|
+
Footer: AlertDialogFooter,
|
|
324
|
+
Cancel: AlertDialogCancel,
|
|
325
|
+
Action: AlertDialogAction,
|
|
326
|
+
});
|
|
327
|
+
export { AlertDialogTrigger, AlertDialogContent, AlertDialogMedia, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ButtonGroup — related actions joined into one control.
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* <ButtonGroup aria-label="History">
|
|
6
|
+
* <Button variant="outline">Undo</Button>
|
|
7
|
+
* <Button variant="outline">Redo</Button>
|
|
8
|
+
* </ButtonGroup>
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* ## Joining
|
|
12
|
+
*
|
|
13
|
+
* The group does not draw anything of its own. It squares the corners where
|
|
14
|
+
* two children meet and drops the border one of them would otherwise double,
|
|
15
|
+
* so outline buttons read as one strip with dividers. It does that with child
|
|
16
|
+
* selectors rather than props, so any `Button`, `Input`, `Menu.Trigger` or
|
|
17
|
+
* `Select.Trigger` placed directly inside joins without being told to.
|
|
18
|
+
*
|
|
19
|
+
* Filled buttons have no border to share, so put a `ButtonGroup.Separator`
|
|
20
|
+
* between them; without one, two primary buttons become one long button.
|
|
21
|
+
*
|
|
22
|
+
* ## Groups of groups
|
|
23
|
+
*
|
|
24
|
+
* A group whose children are groups spaces them apart instead of joining
|
|
25
|
+
* them, which is how a toolbar is built: one group per kind of action.
|
|
26
|
+
*/
|
|
27
|
+
import type { ComponentProps } from 'react';
|
|
28
|
+
import { type VariantProps } from 'tailwind-variants';
|
|
29
|
+
export declare const buttonGroupVariants: import("tailwind-variants").TVReturnType<{
|
|
30
|
+
orientation: {
|
|
31
|
+
horizontal: {
|
|
32
|
+
root: string[];
|
|
33
|
+
separator: string;
|
|
34
|
+
};
|
|
35
|
+
vertical: {
|
|
36
|
+
root: string[];
|
|
37
|
+
separator: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/** Matches the heights of `Button` and `Input` at the same size. */
|
|
41
|
+
size: {
|
|
42
|
+
sm: {
|
|
43
|
+
text: string;
|
|
44
|
+
};
|
|
45
|
+
md: {
|
|
46
|
+
text: string;
|
|
47
|
+
};
|
|
48
|
+
lg: {
|
|
49
|
+
text: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
}, {
|
|
53
|
+
root: string[];
|
|
54
|
+
separator: string;
|
|
55
|
+
text: string[];
|
|
56
|
+
}, undefined, {
|
|
57
|
+
orientation: {
|
|
58
|
+
horizontal: {
|
|
59
|
+
root: string[];
|
|
60
|
+
separator: string;
|
|
61
|
+
};
|
|
62
|
+
vertical: {
|
|
63
|
+
root: string[];
|
|
64
|
+
separator: string;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/** Matches the heights of `Button` and `Input` at the same size. */
|
|
68
|
+
size: {
|
|
69
|
+
sm: {
|
|
70
|
+
text: string;
|
|
71
|
+
};
|
|
72
|
+
md: {
|
|
73
|
+
text: string;
|
|
74
|
+
};
|
|
75
|
+
lg: {
|
|
76
|
+
text: string;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
}, {
|
|
80
|
+
root: string[];
|
|
81
|
+
separator: string;
|
|
82
|
+
text: string[];
|
|
83
|
+
}, import("tailwind-variants").TVReturnType<{
|
|
84
|
+
orientation: {
|
|
85
|
+
horizontal: {
|
|
86
|
+
root: string[];
|
|
87
|
+
separator: string;
|
|
88
|
+
};
|
|
89
|
+
vertical: {
|
|
90
|
+
root: string[];
|
|
91
|
+
separator: string;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
/** Matches the heights of `Button` and `Input` at the same size. */
|
|
95
|
+
size: {
|
|
96
|
+
sm: {
|
|
97
|
+
text: string;
|
|
98
|
+
};
|
|
99
|
+
md: {
|
|
100
|
+
text: string;
|
|
101
|
+
};
|
|
102
|
+
lg: {
|
|
103
|
+
text: string;
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
}, {
|
|
107
|
+
root: string[];
|
|
108
|
+
separator: string;
|
|
109
|
+
text: string[];
|
|
110
|
+
}, undefined, unknown, unknown, undefined>>;
|
|
111
|
+
export type ButtonGroupOrientation = NonNullable<VariantProps<typeof buttonGroupVariants>['orientation']>;
|
|
112
|
+
export type ButtonGroupTextSize = NonNullable<VariantProps<typeof buttonGroupVariants>['size']>;
|
|
113
|
+
export interface ButtonGroupProps extends ComponentProps<'div'> {
|
|
114
|
+
/** Lay the children out in a row or a column. Default `horizontal`. */
|
|
115
|
+
orientation?: ButtonGroupOrientation;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The group. It is a `role="group"`, so give it an `aria-label` that names
|
|
119
|
+
* what its actions have in common.
|
|
120
|
+
*/
|
|
121
|
+
declare function ButtonGroupRoot({ orientation, className, ...props }: ButtonGroupProps): import("react").JSX.Element;
|
|
122
|
+
declare namespace ButtonGroupRoot {
|
|
123
|
+
var displayName: string;
|
|
124
|
+
}
|
|
125
|
+
export interface ButtonGroupSeparatorProps extends ComponentProps<'span'> {
|
|
126
|
+
/**
|
|
127
|
+
* Which way the line runs. It follows the group: a row of buttons is divided
|
|
128
|
+
* by upright lines, a column by flat ones. Default `vertical`, for a row.
|
|
129
|
+
*/
|
|
130
|
+
orientation?: 'vertical' | 'horizontal';
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* A line between two children that have no border to share, such as filled
|
|
134
|
+
* buttons. It is decorative and hidden from assistive technology.
|
|
135
|
+
*/
|
|
136
|
+
declare function ButtonGroupSeparator({ orientation, className, ...props }: ButtonGroupSeparatorProps): import("react").JSX.Element;
|
|
137
|
+
declare namespace ButtonGroupSeparator {
|
|
138
|
+
var displayName: string;
|
|
139
|
+
}
|
|
140
|
+
export interface ButtonGroupTextProps extends ComponentProps<'div'> {
|
|
141
|
+
/** The height, matching the buttons and fields beside it. Default `md`. */
|
|
142
|
+
size?: ButtonGroupTextSize;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* A cell that is read rather than pressed: a prefix such as `https://`, a unit,
|
|
146
|
+
* or the value a pair of buttons changes.
|
|
147
|
+
*/
|
|
148
|
+
declare function ButtonGroupText({ size, className, ...props }: ButtonGroupTextProps): import("react").JSX.Element;
|
|
149
|
+
declare namespace ButtonGroupText {
|
|
150
|
+
var displayName: string;
|
|
151
|
+
}
|
|
152
|
+
export declare const ButtonGroup: typeof ButtonGroupRoot & {
|
|
153
|
+
Separator: typeof ButtonGroupSeparator;
|
|
154
|
+
Text: typeof ButtonGroupText;
|
|
155
|
+
};
|
|
156
|
+
export { ButtonGroupSeparator, ButtonGroupText };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { tv } from 'tailwind-variants';
|
|
3
|
+
export const buttonGroupVariants = tv({
|
|
4
|
+
slots: {
|
|
5
|
+
root: [
|
|
6
|
+
'flex w-fit items-stretch',
|
|
7
|
+
// A focused child draws its ring over its neighbours, not under them.
|
|
8
|
+
'*:focus-visible:relative *:focus-visible:z-10',
|
|
9
|
+
// Groups inside a group are spaced, not joined.
|
|
10
|
+
'has-[>[data-slot=button-group]]:gap-2',
|
|
11
|
+
// A field takes the room the buttons leave.
|
|
12
|
+
'[&>[data-slot=input]]:min-w-0 [&>[data-slot=input]]:flex-1',
|
|
13
|
+
],
|
|
14
|
+
separator: 'shrink-0 self-stretch bg-border',
|
|
15
|
+
text: [
|
|
16
|
+
'flex shrink-0 items-center gap-2 whitespace-nowrap border bg-muted font-medium text-muted-foreground shadow-xs dark:bg-input/30',
|
|
17
|
+
'[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
variants: {
|
|
21
|
+
orientation: {
|
|
22
|
+
horizontal: {
|
|
23
|
+
root: [
|
|
24
|
+
'[&>*:not(:first-child)]:rounded-l-none [&>*:not(:last-child)]:rounded-r-none',
|
|
25
|
+
'[&>*:not(:first-child)]:border-l-0',
|
|
26
|
+
],
|
|
27
|
+
separator: 'w-px',
|
|
28
|
+
},
|
|
29
|
+
vertical: {
|
|
30
|
+
root: [
|
|
31
|
+
'flex-col',
|
|
32
|
+
'[&>*:not(:first-child)]:rounded-t-none [&>*:not(:last-child)]:rounded-b-none',
|
|
33
|
+
'[&>*:not(:first-child)]:border-t-0',
|
|
34
|
+
],
|
|
35
|
+
separator: 'h-px',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
/** Matches the heights of `Button` and `Input` at the same size. */
|
|
39
|
+
size: {
|
|
40
|
+
sm: { text: 'h-8 rounded-md px-2.5 text-[0.8125rem]' },
|
|
41
|
+
md: { text: 'h-9 rounded-lg px-3 text-sm' },
|
|
42
|
+
lg: { text: 'h-11 rounded-xl px-3.5 text-[0.9375rem]' },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
defaultVariants: { orientation: 'horizontal', size: 'md' },
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* The group. It is a `role="group"`, so give it an `aria-label` that names
|
|
49
|
+
* what its actions have in common.
|
|
50
|
+
*/
|
|
51
|
+
function ButtonGroupRoot({ orientation = 'horizontal', className, ...props }) {
|
|
52
|
+
return (_jsx("div", { role: "group", "data-slot": "button-group", "data-orientation": orientation, className: buttonGroupVariants({ orientation }).root({ className }), ...props }));
|
|
53
|
+
}
|
|
54
|
+
ButtonGroupRoot.displayName = 'ButtonGroup';
|
|
55
|
+
/**
|
|
56
|
+
* A line between two children that have no border to share, such as filled
|
|
57
|
+
* buttons. It is decorative and hidden from assistive technology.
|
|
58
|
+
*/
|
|
59
|
+
function ButtonGroupSeparator({ orientation = 'vertical', className, ...props }) {
|
|
60
|
+
const slots = buttonGroupVariants({
|
|
61
|
+
orientation: orientation === 'vertical' ? 'horizontal' : 'vertical',
|
|
62
|
+
});
|
|
63
|
+
return (_jsx("span", { "aria-hidden": "true", "data-slot": "button-group-separator", "data-orientation": orientation, className: slots.separator({ className }), ...props }));
|
|
64
|
+
}
|
|
65
|
+
ButtonGroupSeparator.displayName = 'ButtonGroup.Separator';
|
|
66
|
+
/**
|
|
67
|
+
* A cell that is read rather than pressed: a prefix such as `https://`, a unit,
|
|
68
|
+
* or the value a pair of buttons changes.
|
|
69
|
+
*/
|
|
70
|
+
function ButtonGroupText({ size, className, ...props }) {
|
|
71
|
+
return (_jsx("div", { "data-slot": "button-group-text", className: buttonGroupVariants({ size }).text({ className }), ...props }));
|
|
72
|
+
}
|
|
73
|
+
ButtonGroupText.displayName = 'ButtonGroup.Text';
|
|
74
|
+
export const ButtonGroup = Object.assign(ButtonGroupRoot, {
|
|
75
|
+
Separator: ButtonGroupSeparator,
|
|
76
|
+
Text: ButtonGroupText,
|
|
77
|
+
});
|
|
78
|
+
export { ButtonGroupSeparator, ButtonGroupText };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas — a surface for other components to sit on: a field of dots, a
|
|
3
|
+
* grid or crosses, with a light that can follow the pointer across it.
|
|
4
|
+
*
|
|
5
|
+
* ```tsx
|
|
6
|
+
* <Canvas className="h-96">
|
|
7
|
+
* <Canvas.Pattern variant="dots" />
|
|
8
|
+
* <Canvas.Spotlight />
|
|
9
|
+
* <Canvas.Content>…</Canvas.Content>
|
|
10
|
+
* </Canvas>
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* ## Layers on one surface
|
|
14
|
+
*
|
|
15
|
+
* A canvas is a stack of layers. `Canvas.Pattern` draws marks across the
|
|
16
|
+
* whole surface, several patterns stack, `Canvas.Spotlight` lights them near
|
|
17
|
+
* the pointer, and `Canvas.Content` is what sits on top. Every layer is
|
|
18
|
+
* drawn from the same `viewport`, so a layer added later, such as ink drawn
|
|
19
|
+
* with the pointer, lines up with the pattern without measuring it.
|
|
20
|
+
*
|
|
21
|
+
* ## It moves with what is on it
|
|
22
|
+
*
|
|
23
|
+
* `viewport` takes the `{ x, y, zoom }` a Flow reports from
|
|
24
|
+
* `onViewportChange`, so the pattern pans and scales with the graph on top
|
|
25
|
+
* of it instead of staying behind as the graph slides over it.
|
|
26
|
+
*
|
|
27
|
+
* ## The light follows the hand
|
|
28
|
+
*
|
|
29
|
+
* The spotlight is a second copy of the pattern in the foreground colour,
|
|
30
|
+
* shown through a round window centred on the pointer that fades from the
|
|
31
|
+
* middle to its edge. It follows the pointer without easing, from events on
|
|
32
|
+
* the canvas itself, so it works over whatever is in the content. It is
|
|
33
|
+
* written straight to the element, once a frame, without rendering, and it
|
|
34
|
+
* fades out when the pointer leaves. On a touch screen it shows while a
|
|
35
|
+
* finger is down.
|
|
36
|
+
*/
|
|
37
|
+
import { type ComponentProps } from 'react';
|
|
38
|
+
/** Where the canvas is looking: the same shape a Flow reports. */
|
|
39
|
+
export interface CanvasViewport {
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
zoom: number;
|
|
43
|
+
}
|
|
44
|
+
/** The marks a pattern draws. */
|
|
45
|
+
export type CanvasPatternVariant = 'dots' | 'grid' | 'cross';
|
|
46
|
+
/** How a pattern fades out: not at all, towards every edge, or towards the bottom. */
|
|
47
|
+
export type CanvasFade = 'none' | 'edges' | 'bottom';
|
|
48
|
+
export interface CanvasProps extends ComponentProps<'div'> {
|
|
49
|
+
/**
|
|
50
|
+
* Where the canvas is looking, as `{ x, y, zoom }`. Pass what a Flow reports
|
|
51
|
+
* from `onViewportChange`, and the pattern pans and scales with the graph.
|
|
52
|
+
* Default `{ x: 0, y: 0, zoom: 1 }`.
|
|
53
|
+
*/
|
|
54
|
+
viewport?: CanvasViewport;
|
|
55
|
+
}
|
|
56
|
+
declare function CanvasRoot({ viewport, className, children, ...props }: CanvasProps): import("react").JSX.Element;
|
|
57
|
+
declare namespace CanvasRoot {
|
|
58
|
+
var displayName: string;
|
|
59
|
+
}
|
|
60
|
+
export interface CanvasPatternProps extends Omit<ComponentProps<'svg'>, 'children'> {
|
|
61
|
+
/** Dots, a grid of lines, or small crosses. Default `dots`. */
|
|
62
|
+
variant?: CanvasPatternVariant;
|
|
63
|
+
/** The distance from one mark to the next, in pixels at a zoom of 1. Default `12`, or `24` for a grid. */
|
|
64
|
+
gap?: number;
|
|
65
|
+
/** How big a mark is: a dot's radius, a line's width, half a cross's arm. Default `0.75`, `1` or `3`. */
|
|
66
|
+
size?: number;
|
|
67
|
+
/** Draws every this-many-th line or mark stronger, as on graph paper. */
|
|
68
|
+
major?: number;
|
|
69
|
+
/** Fades the pattern out towards every edge or towards the bottom. Default `none`. */
|
|
70
|
+
fade?: CanvasFade;
|
|
71
|
+
/**
|
|
72
|
+
* How strongly the marks show at rest, as a share of the foreground
|
|
73
|
+
* colour. Default `28%` for dots and crosses, `10%` for a grid.
|
|
74
|
+
*/
|
|
75
|
+
strength?: string;
|
|
76
|
+
}
|
|
77
|
+
/** A pattern across the whole canvas, which pans and scales with its viewport. */
|
|
78
|
+
declare function CanvasPattern({ variant, gap, size, major, fade, strength, className, style, ...props }: CanvasPatternProps): import("react").JSX.Element;
|
|
79
|
+
declare namespace CanvasPattern {
|
|
80
|
+
var displayName: string;
|
|
81
|
+
}
|
|
82
|
+
export interface CanvasSpotlightProps extends Omit<ComponentProps<'div'>, 'children'> {
|
|
83
|
+
/** How far the light reaches from its centre, in pixels. Default `100`. */
|
|
84
|
+
radius?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Holds the light at a point, in pixels from the canvas's top left corner,
|
|
87
|
+
* instead of following the pointer.
|
|
88
|
+
*/
|
|
89
|
+
position?: {
|
|
90
|
+
x: number;
|
|
91
|
+
y: number;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Lights the patterns near the pointer: the marks inside the circle brighten
|
|
96
|
+
* towards the foreground colour, most at the centre.
|
|
97
|
+
*/
|
|
98
|
+
declare function CanvasSpotlight({ radius, position, className, style, ...props }: CanvasSpotlightProps): import("react").JSX.Element;
|
|
99
|
+
declare namespace CanvasSpotlight {
|
|
100
|
+
var displayName: string;
|
|
101
|
+
}
|
|
102
|
+
export interface CanvasContentProps extends ComponentProps<'div'> {
|
|
103
|
+
}
|
|
104
|
+
/** What sits on the canvas, above its patterns and its light. */
|
|
105
|
+
declare function CanvasContent({ className, ...props }: CanvasContentProps): import("react").JSX.Element;
|
|
106
|
+
declare namespace CanvasContent {
|
|
107
|
+
var displayName: string;
|
|
108
|
+
}
|
|
109
|
+
export declare const Canvas: typeof CanvasRoot & {
|
|
110
|
+
Pattern: typeof CanvasPattern;
|
|
111
|
+
Spotlight: typeof CanvasSpotlight;
|
|
112
|
+
Content: typeof CanvasContent;
|
|
113
|
+
};
|
|
114
|
+
export { CanvasPattern, CanvasSpotlight, CanvasContent };
|