panelui-web 0.6.0 → 0.8.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.
@@ -0,0 +1,162 @@
1
+ import { type ComponentProps, type ReactElement, type ReactNode } from 'react';
2
+ export interface DialogProps {
3
+ children?: ReactNode;
4
+ /** Whether it is open, when the caller holds that. */
5
+ open?: boolean;
6
+ /** Whether it starts open, when the caller does not hold it. */
7
+ defaultOpen?: boolean;
8
+ /** Called when it opens or closes. */
9
+ onOpenChange?: (open: boolean) => void;
10
+ /**
11
+ * Whether a press outside it closes it. Turn it off while a form in it has
12
+ * changes that would be lost; Escape and the close button still work.
13
+ * Default `true`.
14
+ */
15
+ dismissible?: boolean;
16
+ }
17
+ declare function DialogRoot({ children, open, defaultOpen, onOpenChange, dismissible, }: DialogProps): import("react").JSX.Element;
18
+ declare namespace DialogRoot {
19
+ var displayName: string;
20
+ }
21
+ export interface DialogTriggerProps extends Omit<ComponentProps<'button'>, 'children'> {
22
+ children?: ReactNode;
23
+ /**
24
+ * Your own element to be the trigger, such as a styled button. It is given
25
+ * the click handler and the attributes that tie it to the dialog.
26
+ */
27
+ render?: ReactElement;
28
+ }
29
+ /** What opens the dialog. A plain button unless `render` supplies one. */
30
+ declare function DialogTrigger({ render, children, ...props }: DialogTriggerProps): import("react").JSX.Element;
31
+ declare namespace DialogTrigger {
32
+ var displayName: string;
33
+ }
34
+ export interface DialogContentProps extends Omit<ComponentProps<'div'>, 'children'> {
35
+ children?: ReactNode;
36
+ /**
37
+ * `inset` sets the task in a panel inside a darker band that holds the
38
+ * footer. `plain` is one surface with the footer ruled off. Default `inset`.
39
+ */
40
+ variant?: 'inset' | 'plain';
41
+ /** How wide it may grow: `sm` 24rem, `md` 28rem, `lg` 36rem. Default `md`. */
42
+ size?: 'sm' | 'md' | 'lg';
43
+ /** Show the round close button in the top trailing corner. Default `true`. */
44
+ showClose?: boolean;
45
+ /** Frost the page behind it instead of only dimming it. */
46
+ blur?: boolean;
47
+ /**
48
+ * Draw it in the page instead of over it: no backdrop, no trigger, always
49
+ * there. For previews; `open` is ignored.
50
+ */
51
+ inline?: boolean;
52
+ }
53
+ /** The dialog itself, with the dimmed page behind it. */
54
+ declare function DialogContent({ variant, size, showClose, blur, inline, className, children, ...props }: DialogContentProps): import("react").JSX.Element;
55
+ declare namespace DialogContent {
56
+ var displayName: string;
57
+ }
58
+ export interface DialogHeaderProps extends Omit<ComponentProps<'div'>, 'title'> {
59
+ /** The heading. It is also what the dialog is announced as. */
60
+ title?: ReactNode;
61
+ /** A line under the heading. */
62
+ description?: ReactNode;
63
+ }
64
+ /** The heading block. It leaves room for the close button beside the title. */
65
+ declare function DialogHeader({ title, description, className, children, ...props }: DialogHeaderProps): import("react").JSX.Element;
66
+ declare namespace DialogHeader {
67
+ var displayName: string;
68
+ }
69
+ export interface DialogTitleProps extends ComponentProps<'h2'> {
70
+ }
71
+ /** The dialog's name, announced when it opens. */
72
+ declare function DialogTitle({ className, ...props }: DialogTitleProps): import("react").JSX.Element;
73
+ declare namespace DialogTitle {
74
+ var displayName: string;
75
+ }
76
+ export interface DialogDescriptionProps extends ComponentProps<'p'> {
77
+ }
78
+ /** A sentence under the title, read out after it. */
79
+ declare function DialogDescription({ className, ...props }: DialogDescriptionProps): import("react").JSX.Element;
80
+ declare namespace DialogDescription {
81
+ var displayName: string;
82
+ }
83
+ export interface DialogBodyProps extends ComponentProps<'div'> {
84
+ }
85
+ /**
86
+ * The part that scrolls, between the header and the footer. Its children are
87
+ * laid out in a column with a gap, so a form's fields need no wrapper.
88
+ */
89
+ declare function DialogBody({ className, children, ...props }: DialogBodyProps): import("react").JSX.Element;
90
+ declare namespace DialogBody {
91
+ var displayName: string;
92
+ }
93
+ export interface DialogFooterProps extends ComponentProps<'div'> {
94
+ }
95
+ /**
96
+ * The buttons. In the inset look they sit in the band under the panel as
97
+ * equal pills; in `plain` they sit at the end of a ruled strip. Keep it a
98
+ * direct child of `Dialog.Content`, so the band can find it.
99
+ */
100
+ declare function DialogFooter({ className, ...props }: DialogFooterProps): import("react").JSX.Element;
101
+ declare namespace DialogFooter {
102
+ var displayName: string;
103
+ }
104
+ export interface DialogCloseProps extends ComponentProps<'button'> {
105
+ /** Your own element to close the dialog with, such as a Cancel button. */
106
+ render?: ReactElement;
107
+ /** Draw the cross glyph. Set for you on the built-in close button. */
108
+ icon?: boolean;
109
+ }
110
+ /** Closes the dialog. */
111
+ declare function DialogClose({ render, icon, children, ...props }: DialogCloseProps): import("react").JSX.Element;
112
+ declare namespace DialogClose {
113
+ var displayName: string;
114
+ }
115
+ export interface DialogListProps extends ComponentProps<'div'> {
116
+ }
117
+ /**
118
+ * A list of one-line choices in the body. The arrow keys, Home and End move
119
+ * between its items, and it opens scrolled to the selected one.
120
+ */
121
+ declare function DialogList({ className, onKeyDown, children, ...props }: DialogListProps): import("react").JSX.Element;
122
+ declare namespace DialogList {
123
+ var displayName: string;
124
+ }
125
+ export interface DialogListGroupProps extends Omit<ComponentProps<'div'>, 'title'> {
126
+ /** The heading over the group, which stays pinned while its items scroll under it. */
127
+ label: ReactNode;
128
+ }
129
+ /** A run of items in `Dialog.List` under a heading of their own. */
130
+ declare function DialogListGroup({ label, className, children, ...props }: DialogListGroupProps): import("react").JSX.Element;
131
+ declare namespace DialogListGroup {
132
+ var displayName: string;
133
+ }
134
+ export interface DialogListItemProps extends Omit<ComponentProps<'div'>, 'onSelect'> {
135
+ /** Whether it is the chosen one: it is tinted and checked. */
136
+ selected?: boolean;
137
+ /** Called when it is pressed, or chosen with Enter or Space. */
138
+ onSelect?: () => void;
139
+ /** Quieter text at its end, in figures that line up: a time, a count. */
140
+ meta?: ReactNode;
141
+ /** Shown, but cannot be chosen. */
142
+ disabled?: boolean;
143
+ }
144
+ /** One choice in `Dialog.List`, on one line. */
145
+ declare function DialogListItem({ selected, onSelect, meta, disabled, className, onClick, onKeyDown, children, ...props }: DialogListItemProps): import("react").JSX.Element;
146
+ declare namespace DialogListItem {
147
+ var displayName: string;
148
+ }
149
+ export declare const Dialog: typeof DialogRoot & {
150
+ Trigger: typeof DialogTrigger;
151
+ Content: typeof DialogContent;
152
+ Header: typeof DialogHeader;
153
+ Title: typeof DialogTitle;
154
+ Description: typeof DialogDescription;
155
+ Body: typeof DialogBody;
156
+ Footer: typeof DialogFooter;
157
+ Close: typeof DialogClose;
158
+ List: typeof DialogList;
159
+ ListGroup: typeof DialogListGroup;
160
+ ListItem: typeof DialogListItem;
161
+ };
162
+ export { DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter, DialogClose, DialogList, DialogListGroup, DialogListItem, };
@@ -0,0 +1,299 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * Dialog — a task taken out of the page and done in a box over it: renaming
5
+ * something, inviting people, filling in a short form.
6
+ *
7
+ * ```tsx
8
+ * <Dialog>
9
+ * <Dialog.Trigger render={<Button />}>Rename</Dialog.Trigger>
10
+ * <Dialog.Content>
11
+ * <Dialog.Header title="Rename board" description="Everyone on the board sees the new name." />
12
+ * <Dialog.Body>
13
+ * <Input defaultValue="Spring launch" aria-label="Name" />
14
+ * </Dialog.Body>
15
+ * <Dialog.Footer>
16
+ * <Dialog.Close render={<Button variant="secondary" />}>Cancel</Dialog.Close>
17
+ * <Button>Save</Button>
18
+ * </Dialog.Footer>
19
+ * </Dialog.Content>
20
+ * </Dialog>
21
+ * ```
22
+ *
23
+ * ## A panel in a band
24
+ *
25
+ * The task sits in a panel, and `Dialog.Footer` moves out of it into the
26
+ * darker band underneath, where the buttons are equal pills: the same shape
27
+ * as Feedback and Alert Dialog, so the three read as one family.
28
+ * `variant="plain"` is a single surface with the footer ruled off at its
29
+ * foot, for a dense form where the band would be one more edge.
30
+ *
31
+ * ## The middle scrolls
32
+ *
33
+ * The dialog is never taller than the screen. The header and footer stay
34
+ * put and `Dialog.Body` scrolls between them, with a thin drawn thumb that
35
+ * shows while it moves.
36
+ *
37
+ * ## Focus goes to the task
38
+ *
39
+ * The close button is drawn in the corner but comes last in the order, so
40
+ * opening the dialog puts focus on its first field rather than on the way
41
+ * out. A press outside, Escape or the close button dismiss it.
42
+ *
43
+ * ## Lists to choose from
44
+ *
45
+ * `Dialog.List` is a list of one-line choices for the body, such as a time
46
+ * zone or a project to move something to: grouped under headings that stay
47
+ * pinned while it scrolls, moved through with the arrow keys, with a check
48
+ * on the chosen one. It opens scrolled to the chosen one.
49
+ *
50
+ * ## On a phone it sits low
51
+ *
52
+ * Under 640px wide it rests at the bottom of the screen, a few pixels clear
53
+ * of the edges, where a thumb reaches its buttons.
54
+ */
55
+ import { Dialog as Primitive } from '@base-ui/react/dialog';
56
+ import { ScrollArea } from '@base-ui/react/scroll-area';
57
+ import { Children, createContext, isValidElement, useContext, useEffect, useId, useMemo, useRef, } from 'react';
58
+ import { tv } from 'tailwind-variants';
59
+ const dialogVariants = tv({
60
+ slots: {
61
+ 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',
62
+ viewport: 'fixed inset-0 z-50 grid place-items-center overflow-hidden p-4 max-sm:items-end max-sm:p-2',
63
+ shell: [
64
+ 'relative flex max-h-[calc(100dvh-2rem)] w-full min-h-0 flex-col text-popover-foreground outline-none max-sm:max-h-[calc(100dvh-1rem)]',
65
+ // A dialog opened from this one steps it back, smaller and veiled, so
66
+ // the depth can be counted.
67
+ 'origin-top scale-[calc(1-0.05*var(--nested-dialogs,0))] translate-y-[calc(-0.75rem*var(--nested-dialogs,0))]',
68
+ "after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:bg-black/0 after:transition-colors after:duration-200 after:content-[''] data-nested-dialog-open:after:bg-black/10 dark:data-nested-dialog-open:after:bg-black/30",
69
+ ],
70
+ 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',
71
+ panel: 'relative flex min-h-0 flex-1 flex-col',
72
+ header: 'flex shrink-0 flex-col gap-1',
73
+ title: 'text-[19px] font-semibold leading-6 tracking-[-0.01em]',
74
+ description: 'text-[15px] leading-[22px] text-muted-foreground',
75
+ body: [
76
+ 'relative flex min-h-0 flex-1 flex-col',
77
+ // A hairline at an edge the content has been scrolled past, so what is
78
+ // cut off reads as going under the header, not as stopping there.
79
+ "before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-20 before:h-px before:bg-border before:opacity-0 before:transition-opacity before:content-[''] data-overflow-y-start:before:opacity-100",
80
+ ],
81
+ bodyViewport: 'min-h-0 flex-1 overscroll-contain rounded-[inherit] outline-none',
82
+ bodyContent: 'flex flex-col gap-4',
83
+ scrollbar: 'my-2 me-1 flex w-1.5 opacity-0 transition-opacity delay-300 data-hovering:opacity-100 data-hovering:delay-0 data-scrolling:opacity-100 data-scrolling:delay-0',
84
+ thumb: 'relative flex-1 rounded-full bg-foreground/20',
85
+ footer: 'flex shrink-0 items-center',
86
+ close: [
87
+ 'absolute end-4 top-4 z-10 inline-flex size-8 cursor-pointer items-center justify-center rounded-full',
88
+ 'bg-muted text-muted-foreground outline-none transition-[background-color,color,scale] duration-150',
89
+ 'after:absolute after:-inset-1.5 hover:bg-foreground/10 hover:text-foreground active:scale-95',
90
+ 'focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none [&_svg]:size-4',
91
+ ],
92
+ // Pulled out by the rows' own padding, so a row's text lines up with the
93
+ // header's, and no further: the scrollbar runs in the 12px left at each
94
+ // side, and a heading or a row reaching into it shows through the thumb.
95
+ list: '-mx-2 flex flex-col outline-none',
96
+ listGroup: 'flex flex-col [&+&]:mt-2',
97
+ listGroupLabel: 'sticky top-0 z-10 bg-popover px-2 pb-1.5 pt-2 text-xs font-medium text-muted-foreground',
98
+ listItem: [
99
+ 'flex h-10 shrink-0 cursor-pointer select-none items-center gap-3 rounded-xl px-2 text-sm outline-none',
100
+ // Room for the pinned heading above it when the arrow keys scroll it into view.
101
+ 'scroll-mb-2 scroll-mt-10 transition-colors duration-100 hover:bg-foreground/[0.05]',
102
+ 'focus-visible:bg-foreground/[0.06] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/60',
103
+ 'data-selected:bg-[color-mix(in_oklab,var(--primary)_9%,transparent)] data-selected:font-medium',
104
+ 'aria-disabled:cursor-default aria-disabled:opacity-50 aria-disabled:hover:bg-transparent motion-reduce:transition-none',
105
+ ],
106
+ listItemLabel: 'min-w-0 flex-1 truncate',
107
+ listItemMeta: 'shrink-0 text-xs font-normal tabular-nums text-muted-foreground',
108
+ listItemCheck: 'flex size-4 shrink-0 items-center justify-center text-primary [&_svg]:size-4',
109
+ },
110
+ variants: {
111
+ variant: {
112
+ /*
113
+ * The band is the popover surface with black mixed in, so it is the
114
+ * darker of the two in either theme. 38 and 30 are one corner: the
115
+ * panel's radius is the band's less the 8px between them.
116
+ */
117
+ inset: {
118
+ shell: 'rounded-[38px] bg-[color-mix(in_oklab,var(--popover),black_5%)] p-2 shadow-xl ring-1 ring-foreground/[0.06] dark:bg-[color-mix(in_oklab,var(--popover),black_14%)]',
119
+ panel: 'overflow-hidden rounded-[30px] bg-popover shadow-xs',
120
+ header: 'px-5 pb-3 pt-5',
121
+ body: 'first:pt-5',
122
+ bodyContent: 'px-5 pb-5',
123
+ // Held in from the band's edge further than the panel is, with the
124
+ // buttons as equal pills: a row of decisions, not a third edge.
125
+ footer: 'gap-3 px-[10px] pb-0.5 pt-2.5 *:h-11 *:min-w-0 *:flex-1 *:rounded-full',
126
+ },
127
+ plain: {
128
+ shell: 'overflow-hidden rounded-[28px] border bg-popover shadow-xl',
129
+ header: 'px-6 pb-4 pt-6',
130
+ body: 'first:pt-6',
131
+ bodyContent: 'px-6 pb-6',
132
+ footer: 'justify-end gap-2 border-t bg-muted/50 px-6 py-3.5',
133
+ },
134
+ },
135
+ size: {
136
+ sm: { shell: 'max-w-sm' },
137
+ md: { shell: 'max-w-md' },
138
+ lg: { shell: 'max-w-xl' },
139
+ },
140
+ blur: {
141
+ true: { backdrop: 'bg-black/20 backdrop-blur-md' },
142
+ },
143
+ },
144
+ defaultVariants: { variant: 'inset', size: 'md' },
145
+ });
146
+ const DialogContext = createContext({ inline: false, showClose: true, variant: 'inset' });
147
+ function DialogRoot({ children, open, defaultOpen, onOpenChange, dismissible = true, }) {
148
+ return (_jsx(Primitive.Root, { open: open, defaultOpen: defaultOpen, onOpenChange: onOpenChange ? (next) => onOpenChange(next) : undefined, disablePointerDismissal: !dismissible, children: children }));
149
+ }
150
+ DialogRoot.displayName = 'Dialog';
151
+ /** What opens the dialog. A plain button unless `render` supplies one. */
152
+ function DialogTrigger({ render, children, ...props }) {
153
+ return (_jsx(Primitive.Trigger, { render: render, ...props, children: children }));
154
+ }
155
+ DialogTrigger.displayName = 'Dialog.Trigger';
156
+ const isFooter = (child) => isValidElement(child) && child.type === DialogFooter;
157
+ /** The dialog itself, with the dimmed page behind it. */
158
+ function DialogContent({ variant = 'inset', size = 'md', showClose = true, blur = false, inline = false, className, children, ...props }) {
159
+ const slots = dialogVariants({ variant, size, blur });
160
+ const context = useMemo(() => ({ inline, showClose, variant }), [inline, showClose, variant]);
161
+ // In the inset look the footer leaves the panel for the band under it.
162
+ const all = Children.toArray(children);
163
+ const footer = variant === 'inset' ? all.filter(isFooter) : [];
164
+ const rest = variant === 'inset' ? all.filter((child) => !isFooter(child)) : all;
165
+ const inside = (_jsxs(DialogContext.Provider, { value: context, children: [_jsxs("div", { "data-slot": "dialog-panel", className: slots.panel(), children: [rest, showClose ? _jsx(DialogClose, { "aria-label": "Close", className: slots.close(), icon: true }) : null] }), footer] }));
166
+ if (inline) {
167
+ return (_jsx("div", { "data-slot": "dialog-content", "data-variant": variant, className: slots.shell({ className }), ...props, children: inside }));
168
+ }
169
+ return (_jsxs(Primitive.Portal, { children: [_jsx(Primitive.Backdrop, { className: slots.backdrop() }), _jsx(Primitive.Viewport, { className: slots.viewport(), children: _jsx(Primitive.Popup, { "data-slot": "dialog-content", "data-variant": variant, className: slots.shell({ className: [slots.popup(), className] }), ...props, children: inside }) })] }));
170
+ }
171
+ DialogContent.displayName = 'Dialog.Content';
172
+ /** The heading block. It leaves room for the close button beside the title. */
173
+ function DialogHeader({ title, description, className, children, ...props }) {
174
+ const { showClose, variant } = useContext(DialogContext);
175
+ const slots = dialogVariants({ variant });
176
+ return (_jsxs("div", { "data-slot": "dialog-header", className: slots.header({ className: [showClose && 'pe-14', className] }), ...props, children: [title !== undefined ? _jsx(DialogTitle, { children: title }) : null, description !== undefined ? _jsx(DialogDescription, { children: description }) : null, children] }));
177
+ }
178
+ DialogHeader.displayName = 'Dialog.Header';
179
+ /** The dialog's name, announced when it opens. */
180
+ function DialogTitle({ className, ...props }) {
181
+ const { inline } = useContext(DialogContext);
182
+ const slots = dialogVariants();
183
+ if (inline)
184
+ return _jsx("h2", { "data-slot": "dialog-title", className: slots.title({ className }), ...props });
185
+ return _jsx(Primitive.Title, { "data-slot": "dialog-title", className: slots.title({ className }), ...props });
186
+ }
187
+ DialogTitle.displayName = 'Dialog.Title';
188
+ /** A sentence under the title, read out after it. */
189
+ function DialogDescription({ className, ...props }) {
190
+ const { inline } = useContext(DialogContext);
191
+ const slots = dialogVariants();
192
+ if (inline)
193
+ return _jsx("p", { "data-slot": "dialog-description", className: slots.description({ className }), ...props });
194
+ return _jsx(Primitive.Description, { "data-slot": "dialog-description", className: slots.description({ className }), ...props });
195
+ }
196
+ DialogDescription.displayName = 'Dialog.Description';
197
+ /**
198
+ * The part that scrolls, between the header and the footer. Its children are
199
+ * laid out in a column with a gap, so a form's fields need no wrapper.
200
+ */
201
+ function DialogBody({ className, children, ...props }) {
202
+ const { variant } = useContext(DialogContext);
203
+ const slots = dialogVariants({ variant });
204
+ return (_jsxs(ScrollArea.Root, { "data-slot": "dialog-body", className: slots.body(), children: [_jsx(ScrollArea.Viewport, { className: slots.bodyViewport(), children: _jsx(ScrollArea.Content, { className: slots.bodyContent({ className }), ...props, children: children }) }), _jsx(ScrollArea.Scrollbar, { orientation: "vertical", className: slots.scrollbar(), children: _jsx(ScrollArea.Thumb, { className: slots.thumb() }) })] }));
205
+ }
206
+ DialogBody.displayName = 'Dialog.Body';
207
+ /**
208
+ * The buttons. In the inset look they sit in the band under the panel as
209
+ * equal pills; in `plain` they sit at the end of a ruled strip. Keep it a
210
+ * direct child of `Dialog.Content`, so the band can find it.
211
+ */
212
+ function DialogFooter({ className, ...props }) {
213
+ const { variant } = useContext(DialogContext);
214
+ return _jsx("div", { "data-slot": "dialog-footer", className: dialogVariants({ variant }).footer({ className }), ...props });
215
+ }
216
+ DialogFooter.displayName = 'Dialog.Footer';
217
+ /** Closes the dialog. */
218
+ function DialogClose({ render, icon = false, children, ...props }) {
219
+ const { inline } = useContext(DialogContext);
220
+ const content = icon ? (_jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", fill: "none", stroke: "currentColor", strokeWidth: 2.4, strokeLinecap: "round", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) })) : (children);
221
+ if (inline) {
222
+ return (_jsx("button", { type: "button", "data-slot": "dialog-close", ...props, children: content }));
223
+ }
224
+ return (_jsx(Primitive.Close, { "data-slot": "dialog-close", render: render, ...props, children: content }));
225
+ }
226
+ DialogClose.displayName = 'Dialog.Close';
227
+ /**
228
+ * A list of one-line choices in the body. The arrow keys, Home and End move
229
+ * between its items, and it opens scrolled to the selected one.
230
+ */
231
+ function DialogList({ className, onKeyDown, children, ...props }) {
232
+ const list = useRef(null);
233
+ const items = () => [...(list.current?.querySelectorAll('[data-slot=dialog-list-item]:not([aria-disabled=true])') ?? [])];
234
+ useEffect(() => {
235
+ const all = items();
236
+ const selected = all.find((item) => item.dataset.selected !== undefined);
237
+ // One item takes Tab: the chosen one, or the first when none is.
238
+ if (!selected && all[0])
239
+ all[0].tabIndex = 0;
240
+ selected?.scrollIntoView({ block: 'center' });
241
+ }, []);
242
+ const move = (event) => {
243
+ onKeyDown?.(event);
244
+ const all = items();
245
+ const index = all.indexOf(document.activeElement);
246
+ const next = event.key === 'ArrowDown'
247
+ ? all[index < 0 ? 0 : Math.min(index + 1, all.length - 1)]
248
+ : event.key === 'ArrowUp'
249
+ ? all[Math.max(index - 1, 0)]
250
+ : event.key === 'Home'
251
+ ? all[0]
252
+ : event.key === 'End'
253
+ ? all[all.length - 1]
254
+ : undefined;
255
+ if (!next)
256
+ return;
257
+ event.preventDefault();
258
+ next.focus();
259
+ };
260
+ return (_jsx("div", { ref: list, role: "listbox", "data-slot": "dialog-list", className: dialogVariants().list({ className }), onKeyDown: move, ...props, children: children }));
261
+ }
262
+ DialogList.displayName = 'Dialog.List';
263
+ /** A run of items in `Dialog.List` under a heading of their own. */
264
+ function DialogListGroup({ label, className, children, ...props }) {
265
+ const slots = dialogVariants();
266
+ const id = useId();
267
+ return (_jsxs("div", { role: "group", "aria-labelledby": id, "data-slot": "dialog-list-group", className: slots.listGroup({ className }), ...props, children: [_jsx("div", { id: id, className: slots.listGroupLabel(), children: label }), children] }));
268
+ }
269
+ DialogListGroup.displayName = 'Dialog.ListGroup';
270
+ /** One choice in `Dialog.List`, on one line. */
271
+ function DialogListItem({ selected = false, onSelect, meta, disabled = false, className, onClick, onKeyDown, children, ...props }) {
272
+ const slots = dialogVariants();
273
+ return (_jsxs("div", { role: "option", "aria-selected": selected, "aria-disabled": disabled || undefined, tabIndex: selected ? 0 : -1, "data-slot": "dialog-list-item", "data-selected": selected ? '' : undefined, className: slots.listItem({ className }), onClick: (event) => {
274
+ onClick?.(event);
275
+ if (!disabled)
276
+ onSelect?.();
277
+ }, onKeyDown: (event) => {
278
+ onKeyDown?.(event);
279
+ if ((event.key === 'Enter' || event.key === ' ') && !disabled) {
280
+ event.preventDefault();
281
+ onSelect?.();
282
+ }
283
+ }, ...props, children: [_jsx("span", { className: slots.listItemLabel(), children: children }), meta !== undefined ? _jsx("span", { className: slots.listItemMeta(), children: meta }) : null, _jsx("span", { "aria-hidden": "true", className: slots.listItemCheck(), children: selected ? (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m5 12.5 4.5 4.5L19 7.5" }) })) : null })] }));
284
+ }
285
+ DialogListItem.displayName = 'Dialog.ListItem';
286
+ export const Dialog = Object.assign(DialogRoot, {
287
+ Trigger: DialogTrigger,
288
+ Content: DialogContent,
289
+ Header: DialogHeader,
290
+ Title: DialogTitle,
291
+ Description: DialogDescription,
292
+ Body: DialogBody,
293
+ Footer: DialogFooter,
294
+ Close: DialogClose,
295
+ List: DialogList,
296
+ ListGroup: DialogListGroup,
297
+ ListItem: DialogListItem,
298
+ });
299
+ export { DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter, DialogClose, DialogList, DialogListGroup, DialogListItem, };
@@ -30,6 +30,14 @@
30
30
  * and closes with its `open` prop, folding under the card, so a band can come
31
31
  * and go without the page jumping by more than its own height.
32
32
  *
33
+ * ## Modes
34
+ *
35
+ * A prompt that changes with what the app is doing names its states with
36
+ * `mode`, and each band says which of them it belongs to with its own
37
+ * `mode`. The bands that match open and the others fold, in one movement,
38
+ * so the card moves once, by the difference between them. A band's own
39
+ * `open` still wins over its mode.
40
+ *
33
41
  * ## Sending
34
42
  *
35
43
  * Enter sends and Shift+Enter starts a new line; Enter while a character is
@@ -97,17 +105,27 @@ export interface PromptProps extends Omit<ComponentProps<'form'>, 'onSubmit' | '
97
105
  maxFiles?: number;
98
106
  /** Called with the files that were not taken, and why. */
99
107
  onReject?: (files: File[], reason: PromptRejectReason) => void;
108
+ /**
109
+ * The state the prompt is in, named by you: `"connect"`, `"tasks"`,
110
+ * `"writing"`. Each band given a `mode` of its own opens while this
111
+ * matches it and folds away otherwise.
112
+ */
113
+ mode?: string;
100
114
  }
101
- declare function PromptRoot({ value: controlledValue, defaultValue, onValueChange, onSubmit, status, onStop, disabled, defaultFiles, accept, maxFiles, onReject, className, children, ...props }: PromptProps): import("react").JSX.Element;
115
+ declare function PromptRoot({ value: controlledValue, defaultValue, onValueChange, onSubmit, status, onStop, disabled, defaultFiles, accept, maxFiles, onReject, mode, className, children, ...props }: PromptProps): import("react").JSX.Element;
102
116
  declare namespace PromptRoot {
103
117
  var displayName: string;
104
118
  }
119
+ /** A band's `mode`: the prompt mode, or modes, it is open in. */
120
+ export type PromptBandMode = string | string[];
105
121
  export interface PromptBannerProps extends ComponentProps<'div'> {
106
- /** Whether it is shown. It folds under the field when it is not. Default `true`. */
122
+ /** Whether it is shown. It folds under the field when it is not. Default: shown, or with `mode`, while the prompt is in that mode. */
107
123
  open?: boolean;
124
+ /** The prompt modes it is shown in. It folds away in any other. */
125
+ mode?: PromptBandMode;
108
126
  }
109
127
  /** A band above the field, for something to do before or while writing: connect an account, pick a project. */
110
- declare function PromptBanner({ open, ...props }: PromptBannerProps): import("react").JSX.Element;
128
+ declare function PromptBanner({ open, mode, ...props }: PromptBannerProps): import("react").JSX.Element;
111
129
  declare namespace PromptBanner {
112
130
  var displayName: string;
113
131
  }
@@ -119,22 +137,28 @@ declare namespace PromptBannerAction {
119
137
  var displayName: string;
120
138
  }
121
139
  export interface PromptStatusProps extends ComponentProps<'div'> {
122
- /** Whether it is shown. Default: while `status` is `submitted` or `streaming`. */
140
+ /** Whether it is shown. Default: while `status` is `submitted` or `streaming`, or with `mode`, while the prompt is in that mode. */
123
141
  open?: boolean;
142
+ /** The prompt modes it is shown in. It folds away in any other. */
143
+ mode?: PromptBandMode;
124
144
  /** Something before the words: the model's mark, a tool's icon. */
125
145
  icon?: ReactNode;
126
146
  /** Whether a spinner turns at its end. Default `true`. */
127
147
  spinner?: boolean;
128
148
  }
129
149
  /** A band under the field saying what the answer is doing, with the words lit by a moving sweep. */
130
- declare function PromptStatus({ open, icon, spinner, children, ...props }: PromptStatusProps): import("react").JSX.Element;
150
+ declare function PromptStatus({ open, mode, icon, spinner, children, ...props }: PromptStatusProps): import("react").JSX.Element;
131
151
  declare namespace PromptStatus {
132
152
  var displayName: string;
133
153
  }
134
154
  export interface PromptContextProps extends ComponentProps<'div'> {
155
+ /** Whether it is shown. It folds under the field when it is not. Default: shown, or with `mode`, while the prompt is in that mode. */
156
+ open?: boolean;
157
+ /** The prompt modes it is shown in. It folds away in any other. */
158
+ mode?: PromptBandMode;
135
159
  }
136
160
  /** A strip under the field, inset from its edges, for where the message is going: a branch, a folder, a ticket. */
137
- declare function PromptContext({ className, ...props }: PromptContextProps): import("react").JSX.Element;
161
+ declare function PromptContext({ open, mode, className, ...props }: PromptContextProps): import("react").JSX.Element;
138
162
  declare namespace PromptContext {
139
163
  var displayName: string;
140
164
  }
@@ -296,8 +320,10 @@ declare namespace PromptSubmit {
296
320
  }
297
321
  export type PromptTaskStatus = 'pending' | 'active' | 'done' | 'error';
298
322
  export interface PromptTasksProps extends Omit<ComponentProps<'div'>, 'title'> {
299
- /** Whether the band is shown. It folds under the field when it is not. Default `true`. */
323
+ /** Whether the band is shown. It folds under the field when it is not. Default: shown, or with `mode`, while the prompt is in that mode. */
300
324
  open?: boolean;
325
+ /** The prompt modes it is shown in. It folds away in any other. */
326
+ mode?: PromptBandMode;
301
327
  /** What the agent is doing, on the band's one line: "Preparing the release". */
302
328
  title: ReactNode;
303
329
  /** Whether the list is open under the title, when the caller holds it. */
@@ -314,7 +340,7 @@ export interface PromptTasksProps extends Omit<ComponentProps<'div'>, 'title'> {
314
340
  * fills as they are done, opening into the list. The list scrolls past
315
341
  * `maxHeight`, and keeps the task in progress in view.
316
342
  */
317
- declare function PromptTasks({ open, title, expanded, defaultExpanded, onExpandedChange, maxHeight, className, children, ...props }: PromptTasksProps): import("react").JSX.Element;
343
+ declare function PromptTasks({ open, mode, title, expanded, defaultExpanded, onExpandedChange, maxHeight, className, children, ...props }: PromptTasksProps): import("react").JSX.Element;
318
344
  declare namespace PromptTasks {
319
345
  var displayName: string;
320
346
  }