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.
@@ -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
  }
@@ -32,6 +32,14 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
32
32
  * and closes with its `open` prop, folding under the card, so a band can come
33
33
  * and go without the page jumping by more than its own height.
34
34
  *
35
+ * ## Modes
36
+ *
37
+ * A prompt that changes with what the app is doing names its states with
38
+ * `mode`, and each band says which of them it belongs to with its own
39
+ * `mode`. The bands that match open and the others fold, in one movement,
40
+ * so the card moves once, by the difference between them. A band's own
41
+ * `open` still wins over its mode.
42
+ *
35
43
  * ## Sending
36
44
  *
37
45
  * Enter sends and Shift+Enter starts a new line; Enter while a character is
@@ -120,8 +128,10 @@ const promptVariants = tv({
120
128
  submitRing: 'pointer-events-none absolute -inset-[3px] size-[calc(100%+6px)] text-primary transition-opacity duration-200',
121
129
  statusText: 'min-w-0 truncate bg-[linear-gradient(90deg,var(--muted-foreground)_35%,var(--foreground)_50%,var(--muted-foreground)_65%)] bg-[length:250%_100%] bg-clip-text text-transparent',
122
130
  statusSpinner: 'ms-auto size-4 animate-spin text-muted-foreground motion-reduce:animate-none',
131
+ // The strip folds like a band: its tuck under the card moves with it.
132
+ contextFold: 'mx-4 grid transition-[grid-template-rows,margin,opacity] duration-300 ease-[cubic-bezier(0.2,0,0,1)] motion-reduce:transition-none',
123
133
  context: [
124
- 'relative mx-4 -mt-5 flex items-center gap-x-4 gap-y-1 rounded-b-[1rem] border bg-muted px-3.5 pb-2 pt-6.5 text-xs text-muted-foreground',
134
+ 'relative flex items-center gap-x-4 gap-y-1 rounded-b-[1rem] border bg-muted px-3.5 pb-2 pt-6.5 text-xs text-muted-foreground',
125
135
  'flex-wrap [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-3.5',
126
136
  ],
127
137
  contextItem: 'inline-flex min-w-0 items-center gap-1.5 whitespace-nowrap',
@@ -209,7 +219,7 @@ function attach(file) {
209
219
  preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined,
210
220
  };
211
221
  }
212
- function PromptRoot({ value: controlledValue, defaultValue = '', onValueChange, onSubmit, status = 'idle', onStop, disabled = false, defaultFiles, accept, maxFiles, onReject, className, children, ...props }) {
222
+ function PromptRoot({ value: controlledValue, defaultValue = '', onValueChange, onSubmit, status = 'idle', onStop, disabled = false, defaultFiles, accept, maxFiles, onReject, mode, className, children, ...props }) {
213
223
  const [internalValue, setInternalValue] = useState(defaultValue);
214
224
  const value = controlledValue ?? internalValue;
215
225
  const [attachments, setAttachments] = useState(() => (defaultFiles ?? []).map(attach));
@@ -274,6 +284,7 @@ function PromptRoot({ value: controlledValue, defaultValue = '', onValueChange,
274
284
  removeAttachment,
275
285
  openFileDialog,
276
286
  status,
287
+ mode,
277
288
  busy,
278
289
  disabled,
279
290
  empty,
@@ -287,12 +298,12 @@ function PromptRoot({ value: controlledValue, defaultValue = '', onValueChange,
287
298
  keyHandler,
288
299
  listbox,
289
300
  setListbox,
290
- }), [value, setValue, attachments, addFiles, removeAttachment, openFileDialog, status, busy, disabled, empty, submit, stop, onStop, dragging, fieldId, listbox]);
301
+ }), [value, setValue, attachments, addFiles, removeAttachment, openFileDialog, status, mode, busy, disabled, empty, submit, stop, onStop, dragging, fieldId, listbox]);
291
302
  const send = (event) => {
292
303
  event.preventDefault();
293
304
  submit();
294
305
  };
295
- return (_jsx(PromptStore.Provider, { value: context, children: _jsxs("form", { "data-slot": "prompt", "data-status": status, "aria-busy": busy || undefined, className: promptVariants().root({ className }), onSubmit: send, ...props, children: [children, _jsx("input", { ref: fileInput, type: "file", multiple: maxFiles !== 1, accept: accept, tabIndex: -1, "aria-hidden": "true", className: "hidden", onChange: (event) => {
306
+ return (_jsx(PromptStore.Provider, { value: context, children: _jsxs("form", { "data-slot": "prompt", "data-status": status, "data-mode": mode, "aria-busy": busy || undefined, className: promptVariants().root({ className }), onSubmit: send, ...props, children: [children, _jsx("input", { ref: fileInput, type: "file", multiple: maxFiles !== 1, accept: accept, tabIndex: -1, "aria-hidden": "true", className: "hidden", onChange: (event) => {
296
307
  if (event.target.files)
297
308
  addFiles(event.target.files);
298
309
  // Cleared, so choosing the same file again still counts as a change.
@@ -311,9 +322,22 @@ function Band({ edge, open, slot, className, children, ...props }) {
311
322
  }, [open]);
312
323
  return (_jsx("div", { "data-slot": slot, "data-open": open ? '' : undefined, className: slots.band(), children: _jsx("div", { ref: clip, className: slots.bandClip(), children: _jsx("div", { className: slots.bandInner({ className }), ...props, children: children }) }) }));
313
324
  }
325
+ /**
326
+ * Whether a band is open: its own `open` if given, else whether the
327
+ * prompt's mode is one of the band's, else what the band does by default.
328
+ */
329
+ function useBandOpen(part, open, mode, fallback) {
330
+ const prompt = usePrompt(part);
331
+ if (open !== undefined)
332
+ return open;
333
+ if (mode !== undefined)
334
+ return prompt.mode !== undefined && (Array.isArray(mode) ? mode : [mode]).includes(prompt.mode);
335
+ return fallback(prompt);
336
+ }
314
337
  /** A band above the field, for something to do before or while writing: connect an account, pick a project. */
315
- function PromptBanner({ open = true, ...props }) {
316
- return _jsx(Band, { edge: "top", open: open, slot: "prompt-banner", ...props });
338
+ function PromptBanner({ open, mode, ...props }) {
339
+ const shown = useBandOpen('Prompt.Banner', open, mode, () => true);
340
+ return _jsx(Band, { edge: "top", open: shown, slot: "prompt-banner", ...props });
317
341
  }
318
342
  PromptBanner.displayName = 'Prompt.Banner';
319
343
  /** The banner's button, at its end. */
@@ -340,17 +364,22 @@ function useShimmer(active) {
340
364
  return ref;
341
365
  }
342
366
  /** A band under the field saying what the answer is doing, with the words lit by a moving sweep. */
343
- function PromptStatus({ open, icon, spinner = true, children, ...props }) {
344
- const prompt = usePrompt('Prompt.Status');
345
- const shown = open ?? prompt.busy;
367
+ function PromptStatus({ open, mode, icon, spinner = true, children, ...props }) {
368
+ const shown = useBandOpen('Prompt.Status', open, mode, (prompt) => prompt.busy);
346
369
  const slots = promptVariants();
347
370
  const text = useShimmer(shown);
348
371
  return (_jsxs(Band, { edge: "bottom", open: shown, slot: "prompt-status", role: "status", ...props, children: [icon, _jsx("span", { ref: text, className: slots.statusText(), children: children }), spinner ? (_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", className: slots.statusSpinner(), children: _jsx("path", { d: "M21 12a9 9 0 1 1-6.22-8.56" }) })) : null] }));
349
372
  }
350
373
  PromptStatus.displayName = 'Prompt.Status';
351
374
  /** A strip under the field, inset from its edges, for where the message is going: a branch, a folder, a ticket. */
352
- function PromptContext({ className, ...props }) {
353
- return _jsx("div", { "data-slot": "prompt-context", className: promptVariants().context({ className }), ...props });
375
+ function PromptContext({ open, mode, className, ...props }) {
376
+ const shown = useBandOpen('Prompt.Context', open, mode, () => true);
377
+ const slots = promptVariants();
378
+ const clip = useRef(null);
379
+ useEffect(() => {
380
+ clip.current?.toggleAttribute('inert', !shown);
381
+ }, [shown]);
382
+ return (_jsx("div", { "data-slot": "prompt-context", "data-open": shown ? '' : undefined, className: slots.contextFold({ className: shown ? '-mt-5 grid-rows-[1fr] opacity-100' : 'mt-0 grid-rows-[0fr] opacity-0' }), children: _jsx("div", { ref: clip, className: "min-h-0 overflow-hidden", children: _jsx("div", { className: slots.context({ className }), ...props }) }) }));
354
383
  }
355
384
  PromptContext.displayName = 'Prompt.Context';
356
385
  /** One fact in the context strip, with an icon before it. */
@@ -567,7 +596,8 @@ function ProgressRing({ done, total }) {
567
596
  * fills as they are done, opening into the list. The list scrolls past
568
597
  * `maxHeight`, and keeps the task in progress in view.
569
598
  */
570
- function PromptTasks({ open = true, title, expanded, defaultExpanded = false, onExpandedChange, maxHeight = '10rem', className, children, ...props }) {
599
+ function PromptTasks({ open, mode, title, expanded, defaultExpanded = false, onExpandedChange, maxHeight = '10rem', className, children, ...props }) {
600
+ const shown = useBandOpen('Prompt.Tasks', open, mode, () => true);
571
601
  const slots = promptVariants();
572
602
  const [inner, setInner] = useState(defaultExpanded);
573
603
  const isExpanded = expanded ?? inner;
@@ -593,7 +623,7 @@ function PromptTasks({ open = true, title, expanded, defaultExpanded = false, on
593
623
  setInner(!isExpanded);
594
624
  onExpandedChange?.(!isExpanded);
595
625
  };
596
- return (_jsxs(Band, { edge: "top", open: open, slot: "prompt-tasks", className: ['flex-col items-stretch gap-0 px-0 pt-0.5', className].filter(Boolean).join(' '), ...props, children: [_jsxs("button", { type: "button", "aria-expanded": isExpanded, "aria-controls": listId, onClick: toggle, className: slots.tasksHeader(), children: [_jsx(ProgressRing, { done: done, total: tasks.length }), _jsx("span", { className: slots.tasksTitle(), children: title }), _jsxs("span", { className: slots.tasksCount(), children: [done, "/", tasks.length] }), _jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: slots.tasksChevron({ className: isExpanded ? 'rotate-180' : undefined }), children: _jsx("path", { d: "m6 9 6 6 6-6" }) })] }), _jsx("div", { className: slots.tasksBody({ className: isExpanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]' }), children: _jsx("div", { className: "min-h-0 overflow-hidden", children: _jsx("ol", { ref: list, id: listId, "aria-label": "Tasks", className: slots.tasksList(), style: { maxHeight }, children: children }) }) })] }));
626
+ return (_jsxs(Band, { edge: "top", open: shown, slot: "prompt-tasks", className: ['flex-col items-stretch gap-0 px-0 pt-0.5', className].filter(Boolean).join(' '), ...props, children: [_jsxs("button", { type: "button", "aria-expanded": isExpanded, "aria-controls": listId, onClick: toggle, className: slots.tasksHeader(), children: [_jsx(ProgressRing, { done: done, total: tasks.length }), _jsx("span", { className: slots.tasksTitle(), children: title }), _jsxs("span", { className: slots.tasksCount(), children: [done, "/", tasks.length] }), _jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: slots.tasksChevron({ className: isExpanded ? 'rotate-180' : undefined }), children: _jsx("path", { d: "m6 9 6 6 6-6" }) })] }), _jsx("div", { className: slots.tasksBody({ className: isExpanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]' }), children: _jsx("div", { className: "min-h-0 overflow-hidden", children: _jsx("ol", { ref: list, id: listId, "aria-label": "Tasks", className: slots.tasksList(), style: { maxHeight }, children: children }) }) })] }));
597
627
  }
598
628
  PromptTasks.displayName = 'Prompt.Tasks';
599
629
  /** One task in `Prompt.Tasks`. */
@@ -161,6 +161,50 @@ declare function SheetClose({ render, icon, children, ...props }: SheetCloseProp
161
161
  declare namespace SheetClose {
162
162
  var displayName: string;
163
163
  }
164
+ export interface SheetSlidesProps extends Omit<ComponentProps<'div'>, 'onChange'> {
165
+ /**
166
+ * `true` makes it a deck: each slide fills the height, and a scroll
167
+ * glides to the next one. `false` makes it a numbered column of smaller
168
+ * slides that scrolls freely. Default `true`.
169
+ */
170
+ snap?: boolean;
171
+ /** The current slide, counted from 0, when the caller holds it. Setting it scrolls that slide into view. */
172
+ index?: number;
173
+ /** The slide to start on, when the caller does not hold it. Default `0`. */
174
+ defaultIndex?: number;
175
+ /**
176
+ * Called with the new index when the current slide changes: scrolled to
177
+ * in a deck, pressed in a column. In a column, passing it makes each
178
+ * slide a button.
179
+ */
180
+ onIndexChange?: (index: number) => void;
181
+ /** Show "3 / 8" under the current slide. Default: on in a deck. */
182
+ counter?: boolean;
183
+ /** The shape of a slide in a column, as a CSS aspect ratio. Default `16 / 9`. */
184
+ aspect?: string;
185
+ }
186
+ /**
187
+ * A column of rounded slides in place of `Sheet.Body`: a deck that moves one
188
+ * slide at a time, or with `snap={false}`, a numbered column to choose from.
189
+ */
190
+ declare function SheetSlides({ snap, index, defaultIndex, onIndexChange, counter, aspect, className, style, children, 'aria-label': label, ...props }: SheetSlidesProps): import("react").JSX.Element;
191
+ declare namespace SheetSlides {
192
+ var displayName: string;
193
+ }
194
+ export interface SheetSlideProps extends ComponentProps<'div'> {
195
+ /**
196
+ * What a screen reader calls it, after its place: "3 of 8, Harbour at
197
+ * dawn". An image's `alt` usually says it already.
198
+ */
199
+ label?: string;
200
+ /** Its shape in a column, overriding `Sheet.Slides`' `aspect`. */
201
+ aspect?: string;
202
+ }
203
+ /** One slide: a rounded frame that fills with an image, a video or anything drawn. */
204
+ declare function SheetSlide({ label, aspect, className, style, children, ...props }: SheetSlideProps): import("react").JSX.Element;
205
+ declare namespace SheetSlide {
206
+ var displayName: string;
207
+ }
164
208
  export interface SheetStageProps extends ComponentProps<'div'> {
165
209
  }
166
210
  /**
@@ -183,5 +227,7 @@ export declare const Sheet: typeof SheetRoot & {
183
227
  Footer: typeof SheetFooter;
184
228
  Close: typeof SheetClose;
185
229
  Stage: typeof SheetStage;
230
+ Slides: typeof SheetSlides;
231
+ Slide: typeof SheetSlide;
186
232
  };
187
- export { SheetTrigger, SheetNext, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, SheetClose, SheetStage, };
233
+ export { SheetTrigger, SheetNext, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, SheetClose, SheetStage, SheetSlides, SheetSlide, };