panelui-web 0.4.0 → 0.6.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,134 @@
1
+ /**
2
+ * Select — a button that opens a list of options, one of which is chosen.
3
+ *
4
+ * ```tsx
5
+ * <Select items={plans} defaultValue="team">
6
+ * <Select.Trigger>
7
+ * <Select.Value placeholder="Pick a plan" />
8
+ * </Select.Trigger>
9
+ * <Select.Content>
10
+ * {plans.map((plan) => (
11
+ * <Select.Item key={plan.value} value={plan.value} description={plan.price}>
12
+ * {plan.label}
13
+ * </Select.Item>
14
+ * ))}
15
+ * </Select.Content>
16
+ * </Select>
17
+ * ```
18
+ *
19
+ * ## The list opens over the trigger
20
+ *
21
+ * Opened with a pointer, the list is placed so the chosen option sits exactly
22
+ * over the trigger, and the eye does not have to go looking for it. Where
23
+ * there is no room for that, or `alignItemWithTrigger` is false, it opens
24
+ * below. Opened from the keyboard, the arrow keys move through the options,
25
+ * typing jumps to one by its first letters, and Enter chooses.
26
+ *
27
+ * ## Labels
28
+ *
29
+ * Pass `items` to the root, as `{ value, label }` pairs, and `Select.Value`
30
+ * shows the chosen option's label rather than its value. Without `items`,
31
+ * give `Select.Value` a function that turns a value into what to show.
32
+ *
33
+ * ## Three triggers
34
+ *
35
+ * `default` is a field, for a form. `ghost` has no box until it is hovered,
36
+ * for a choice set into a toolbar or a line of text. `pill` is a rounded chip
37
+ * with an edge, for a choice that sits among other chips, such as the model
38
+ * in a prompt.
39
+ */
40
+ import { Select as BaseSelect } from '@base-ui/react/select';
41
+ import type { ComponentProps, ReactNode } from 'react';
42
+ export type SelectProps<Value = string, Multiple extends boolean | undefined = false> = BaseSelect.Root.Props<Value, Multiple>;
43
+ /** Holds the chosen value. Takes `value`/`onValueChange`, `defaultValue`, `items`, `name`, `disabled` and `multiple`. */
44
+ declare function SelectRoot<Value = string, Multiple extends boolean | undefined = false>(props: SelectProps<Value, Multiple>): import("react").JSX.Element;
45
+ declare namespace SelectRoot {
46
+ var displayName: string;
47
+ }
48
+ export interface SelectTriggerProps extends BaseSelect.Trigger.Props {
49
+ /** `default` is a field, `ghost` has no box until hovered, `pill` is a rounded chip. Default `default`. */
50
+ variant?: 'default' | 'ghost' | 'pill';
51
+ /** Its height and text size. Default `md`. */
52
+ size?: 'sm' | 'md' | 'lg';
53
+ /** Replaces the chevron at the end. Pass `null` for none. */
54
+ icon?: ReactNode;
55
+ className?: string;
56
+ }
57
+ /** The button that opens the list. Put a `Select.Value` in it. */
58
+ declare function SelectTrigger({ variant, size, icon, className, children, ...props }: SelectTriggerProps): import("react").JSX.Element;
59
+ declare namespace SelectTrigger {
60
+ var displayName: string;
61
+ }
62
+ export interface SelectValueProps extends BaseSelect.Value.Props {
63
+ className?: string;
64
+ }
65
+ /** The chosen option's label, or `placeholder` while there is none. */
66
+ declare function SelectValue({ className, ...props }: SelectValueProps): import("react").JSX.Element;
67
+ declare namespace SelectValue {
68
+ var displayName: string;
69
+ }
70
+ export interface SelectContentProps extends BaseSelect.Popup.Props {
71
+ /** Which side of the trigger it opens on, when it is not placed over it. Default `bottom`. */
72
+ side?: BaseSelect.Positioner.Props['side'];
73
+ /** How it lines up with the trigger along that side. Default `start`. */
74
+ align?: BaseSelect.Positioner.Props['align'];
75
+ /** The gap between the trigger and the list, in pixels. Default `6`. */
76
+ sideOffset?: number;
77
+ /** Whether a pointer opens it with the chosen option over the trigger. Default `true`. */
78
+ alignItemWithTrigger?: boolean;
79
+ /** Where in the document the list is put. Default `document.body`. */
80
+ container?: BaseSelect.Portal.Props['container'];
81
+ className?: string;
82
+ }
83
+ /** The list of options, opened in a layer over the page. */
84
+ declare function SelectContent({ side, align, sideOffset, alignItemWithTrigger, container, className, children, ...props }: SelectContentProps): import("react").JSX.Element;
85
+ declare namespace SelectContent {
86
+ var displayName: string;
87
+ }
88
+ export interface SelectItemProps extends Omit<BaseSelect.Item.Props, 'children'> {
89
+ /** The option's label: what the trigger shows once it is chosen. */
90
+ children?: ReactNode;
91
+ /** A second, quieter line under the label. */
92
+ description?: ReactNode;
93
+ /** An icon before the label. */
94
+ icon?: ReactNode;
95
+ className?: string;
96
+ }
97
+ /** One option. A check marks the chosen one. */
98
+ declare function SelectItem({ description, icon, className, children, ...props }: SelectItemProps): import("react").JSX.Element;
99
+ declare namespace SelectItem {
100
+ var displayName: string;
101
+ }
102
+ export interface SelectGroupProps extends BaseSelect.Group.Props {
103
+ className?: string;
104
+ }
105
+ /** Options that belong together, under a `Select.GroupLabel`. */
106
+ declare function SelectGroup({ className, ...props }: SelectGroupProps): import("react").JSX.Element;
107
+ declare namespace SelectGroup {
108
+ var displayName: string;
109
+ }
110
+ export interface SelectGroupLabelProps extends BaseSelect.GroupLabel.Props {
111
+ className?: string;
112
+ }
113
+ /** The heading of a group. Screen readers announce the group by it. */
114
+ declare function SelectGroupLabel({ className, ...props }: SelectGroupLabelProps): import("react").JSX.Element;
115
+ declare namespace SelectGroupLabel {
116
+ var displayName: string;
117
+ }
118
+ export interface SelectSeparatorProps extends ComponentProps<'div'> {
119
+ }
120
+ /** A rule between groups of options. */
121
+ declare function SelectSeparator({ className, ...props }: SelectSeparatorProps): import("react").JSX.Element;
122
+ declare namespace SelectSeparator {
123
+ var displayName: string;
124
+ }
125
+ export declare const Select: typeof SelectRoot & {
126
+ Trigger: typeof SelectTrigger;
127
+ Value: typeof SelectValue;
128
+ Content: typeof SelectContent;
129
+ Item: typeof SelectItem;
130
+ Group: typeof SelectGroup;
131
+ GroupLabel: typeof SelectGroupLabel;
132
+ Separator: typeof SelectSeparator;
133
+ };
134
+ export { SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectGroupLabel, SelectSeparator };
@@ -0,0 +1,164 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Select — a button that opens a list of options, one of which is chosen.
4
+ *
5
+ * ```tsx
6
+ * <Select items={plans} defaultValue="team">
7
+ * <Select.Trigger>
8
+ * <Select.Value placeholder="Pick a plan" />
9
+ * </Select.Trigger>
10
+ * <Select.Content>
11
+ * {plans.map((plan) => (
12
+ * <Select.Item key={plan.value} value={plan.value} description={plan.price}>
13
+ * {plan.label}
14
+ * </Select.Item>
15
+ * ))}
16
+ * </Select.Content>
17
+ * </Select>
18
+ * ```
19
+ *
20
+ * ## The list opens over the trigger
21
+ *
22
+ * Opened with a pointer, the list is placed so the chosen option sits exactly
23
+ * over the trigger, and the eye does not have to go looking for it. Where
24
+ * there is no room for that, or `alignItemWithTrigger` is false, it opens
25
+ * below. Opened from the keyboard, the arrow keys move through the options,
26
+ * typing jumps to one by its first letters, and Enter chooses.
27
+ *
28
+ * ## Labels
29
+ *
30
+ * Pass `items` to the root, as `{ value, label }` pairs, and `Select.Value`
31
+ * shows the chosen option's label rather than its value. Without `items`,
32
+ * give `Select.Value` a function that turns a value into what to show.
33
+ *
34
+ * ## Three triggers
35
+ *
36
+ * `default` is a field, for a form. `ghost` has no box until it is hovered,
37
+ * for a choice set into a toolbar or a line of text. `pill` is a rounded chip
38
+ * with an edge, for a choice that sits among other chips, such as the model
39
+ * in a prompt.
40
+ */
41
+ import { Select as BaseSelect } from '@base-ui/react/select';
42
+ import { tv } from 'tailwind-variants';
43
+ const selectVariants = tv({
44
+ slots: {
45
+ trigger: [
46
+ 'group/select-trigger inline-flex min-w-0 cursor-pointer select-none items-center justify-between gap-2 whitespace-nowrap text-foreground outline-none',
47
+ 'transition-[background-color,border-color,box-shadow,color] duration-150',
48
+ 'focus-visible:ring-[3px] focus-visible:ring-ring/30',
49
+ 'data-disabled:cursor-not-allowed data-disabled:opacity-50',
50
+ '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',
51
+ ],
52
+ icon: 'flex text-muted-foreground transition-transform duration-200 group-data-popup-open/select-trigger:rotate-180 motion-reduce:transition-none',
53
+ value: 'flex min-w-0 flex-1 items-center gap-2 truncate text-start data-placeholder:text-muted-foreground/80',
54
+ positioner: 'z-50 outline-none',
55
+ popup: [
56
+ 'min-w-(--anchor-width) origin-(--transform-origin) overflow-hidden rounded-xl border bg-popover text-popover-foreground shadow-lg outline-none',
57
+ 'transition-[opacity,scale] duration-150 ease-out',
58
+ 'data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:scale-95 data-ending-style:opacity-0',
59
+ // Placed over the trigger, it only fades: growing from a point inside
60
+ // the list would look like it came from nowhere.
61
+ 'data-[side=none]:data-starting-style:scale-100 data-[side=none]:data-ending-style:scale-100',
62
+ 'motion-reduce:transition-none',
63
+ ],
64
+ list: 'max-h-(--available-height) scroll-py-1 overflow-y-auto p-1 [scrollbar-color:var(--border)_transparent] [scrollbar-width:thin]',
65
+ item: [
66
+ 'flex cursor-default items-center gap-2.5 rounded-lg px-2 py-1.5 text-sm outline-none select-none',
67
+ 'data-highlighted:bg-accent data-highlighted:text-accent-foreground',
68
+ 'data-disabled:pointer-events-none data-disabled:opacity-50',
69
+ 'in-data-[side=none]:min-w-[calc(var(--anchor-width)+0.5rem)]',
70
+ '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',
71
+ ],
72
+ itemIcon: 'flex size-4 shrink-0 items-center justify-center text-muted-foreground',
73
+ itemBody: 'flex min-w-0 flex-1 flex-col',
74
+ itemText: 'truncate',
75
+ itemDescription: 'truncate text-xs text-muted-foreground',
76
+ // The box is always there, so an option does not change width as it is chosen.
77
+ indicator: 'flex size-4 shrink-0 items-center justify-center text-foreground',
78
+ group: 'flex flex-col',
79
+ groupLabel: 'px-2 pb-1 pt-2 text-xs font-medium text-muted-foreground',
80
+ separator: 'mx-2 my-1 h-px bg-border',
81
+ },
82
+ variants: {
83
+ variant: {
84
+ default: {
85
+ trigger: 'rounded-lg border border-input bg-background shadow-xs hover:bg-accent/40 data-popup-open:border-ring dark:bg-input/30',
86
+ },
87
+ ghost: {
88
+ trigger: 'rounded-lg text-muted-foreground hover:bg-accent hover:text-foreground data-popup-open:bg-accent data-popup-open:text-foreground',
89
+ },
90
+ pill: {
91
+ trigger: 'rounded-full border bg-background shadow-xs hover:bg-accent data-popup-open:bg-accent dark:bg-input/30',
92
+ },
93
+ },
94
+ size: {
95
+ sm: { trigger: 'h-8 px-2.5 text-[0.8125rem]' },
96
+ md: { trigger: 'h-9 px-3 text-sm' },
97
+ lg: { trigger: 'h-11 px-3.5 text-[0.9375rem]' },
98
+ },
99
+ },
100
+ compoundVariants: [
101
+ // A field is as wide as its column; a ghost or a pill is as wide as its label.
102
+ { variant: 'default', className: { trigger: 'w-full' } },
103
+ { variant: 'ghost', size: 'sm', className: { trigger: 'px-2' } },
104
+ ],
105
+ defaultVariants: { variant: 'default', size: 'md' },
106
+ });
107
+ /** Holds the chosen value. Takes `value`/`onValueChange`, `defaultValue`, `items`, `name`, `disabled` and `multiple`. */
108
+ function SelectRoot(props) {
109
+ return _jsx(BaseSelect.Root, { ...props });
110
+ }
111
+ SelectRoot.displayName = 'Select';
112
+ function Chevron({ variant }) {
113
+ // A field shows both directions, since its list can open either way over it.
114
+ // A chip shows one, and turns over while its list is open.
115
+ return (_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: variant === 'default' || variant === undefined ? _jsx("path", { d: "m7 15 5 5 5-5M7 9l5-5 5 5" }) : _jsx("path", { d: "m6 9 6 6 6-6" }) }));
116
+ }
117
+ /** The button that opens the list. Put a `Select.Value` in it. */
118
+ function SelectTrigger({ variant, size, icon, className, children, ...props }) {
119
+ const slots = selectVariants({ variant, size });
120
+ return (_jsxs(BaseSelect.Trigger, { "data-slot": "select-trigger", className: slots.trigger({ className }), ...props, children: [children, icon === null ? null : (_jsx(BaseSelect.Icon, { "data-slot": "select-icon", className: slots.icon({ className: variant === 'default' || variant === undefined ? 'rotate-0!' : undefined }), children: icon ?? _jsx(Chevron, { variant: variant }) }))] }));
121
+ }
122
+ SelectTrigger.displayName = 'Select.Trigger';
123
+ /** The chosen option's label, or `placeholder` while there is none. */
124
+ function SelectValue({ className, ...props }) {
125
+ return _jsx(BaseSelect.Value, { "data-slot": "select-value", className: selectVariants().value({ className }), ...props });
126
+ }
127
+ SelectValue.displayName = 'Select.Value';
128
+ /** The list of options, opened in a layer over the page. */
129
+ function SelectContent({ side = 'bottom', align = 'start', sideOffset = 6, alignItemWithTrigger = true, container, className, children, ...props }) {
130
+ const slots = selectVariants();
131
+ return (_jsx(BaseSelect.Portal, { container: container, children: _jsx(BaseSelect.Positioner, { side: side, align: align, sideOffset: sideOffset, alignItemWithTrigger: alignItemWithTrigger, className: slots.positioner(), children: _jsx(BaseSelect.Popup, { "data-slot": "select-content", className: slots.popup({ className }), ...props, children: _jsx(BaseSelect.List, { className: slots.list(), children: children }) }) }) }));
132
+ }
133
+ SelectContent.displayName = 'Select.Content';
134
+ /** One option. A check marks the chosen one. */
135
+ function SelectItem({ description, icon, className, children, ...props }) {
136
+ const slots = selectVariants();
137
+ return (_jsxs(BaseSelect.Item, { "data-slot": "select-item", className: slots.item({ className }), ...props, children: [icon ? (_jsx("span", { "data-slot": "select-item-icon", "aria-hidden": "true", className: slots.itemIcon(), children: icon })) : null, _jsxs("span", { className: slots.itemBody(), children: [_jsx(BaseSelect.ItemText, { className: slots.itemText(), children: children }), description ? _jsx("span", { className: slots.itemDescription(), children: description }) : null] }), _jsx("span", { className: slots.indicator(), children: _jsx(BaseSelect.ItemIndicator, { className: "flex", children: _jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.25", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M20 6 9 17l-5-5" }) }) }) })] }));
138
+ }
139
+ SelectItem.displayName = 'Select.Item';
140
+ /** Options that belong together, under a `Select.GroupLabel`. */
141
+ function SelectGroup({ className, ...props }) {
142
+ return _jsx(BaseSelect.Group, { "data-slot": "select-group", className: selectVariants().group({ className }), ...props });
143
+ }
144
+ SelectGroup.displayName = 'Select.Group';
145
+ /** The heading of a group. Screen readers announce the group by it. */
146
+ function SelectGroupLabel({ className, ...props }) {
147
+ return (_jsx(BaseSelect.GroupLabel, { "data-slot": "select-group-label", className: selectVariants().groupLabel({ className }), ...props }));
148
+ }
149
+ SelectGroupLabel.displayName = 'Select.GroupLabel';
150
+ /** A rule between groups of options. */
151
+ function SelectSeparator({ className, ...props }) {
152
+ return _jsx("div", { role: "separator", "data-slot": "select-separator", className: selectVariants().separator({ className }), ...props });
153
+ }
154
+ SelectSeparator.displayName = 'Select.Separator';
155
+ export const Select = Object.assign(SelectRoot, {
156
+ Trigger: SelectTrigger,
157
+ Value: SelectValue,
158
+ Content: SelectContent,
159
+ Item: SelectItem,
160
+ Group: SelectGroup,
161
+ GroupLabel: SelectGroupLabel,
162
+ Separator: SelectSeparator,
163
+ });
164
+ export { SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectGroupLabel, SelectSeparator };
@@ -17,7 +17,7 @@ export interface SheetProps {
17
17
  */
18
18
  dismissible?: boolean;
19
19
  }
20
- declare function SheetRoot({ children, open, onOpenChange, defaultOpen, side, dismissible, }: SheetProps): import("react").JSX.Element;
20
+ declare function SheetRoot(props: SheetProps): import("react").JSX.Element;
21
21
  declare namespace SheetRoot {
22
22
  var displayName: string;
23
23
  }
@@ -28,12 +28,50 @@ export interface SheetTriggerProps extends Omit<ComponentProps<'button'>, 'child
28
28
  * the click handler and the attributes that tie it to the sheet.
29
29
  */
30
30
  render?: ReactElement;
31
+ /**
32
+ * The `name` of a `Sheet.Next` to open instead of the sheet this trigger is
33
+ * in. Any button in a sheet can open the next one this way, wherever it sits.
34
+ */
35
+ to?: string;
36
+ /**
37
+ * Handed to the `Sheet.Next` that `to` names, as the argument of its
38
+ * children function. Use it to open one sheet for every row of a list.
39
+ */
40
+ payload?: unknown;
31
41
  }
32
- /** What opens the sheet. A plain button unless `render` supplies one. */
33
- declare function SheetTrigger({ render, children, ...props }: SheetTriggerProps): import("react").JSX.Element;
42
+ /**
43
+ * What opens a sheet: its own, or with `to`, the `Sheet.Next` of that name.
44
+ * A plain button unless `render` supplies one.
45
+ */
46
+ declare function SheetTrigger({ render, children, to, payload, ...props }: SheetTriggerProps): import("react").JSX.Element;
34
47
  declare namespace SheetTrigger {
35
48
  var displayName: string;
36
49
  }
50
+ export interface SheetNextProps extends Omit<SheetContentProps, 'children' | 'inline'> {
51
+ /** What a `Sheet.Trigger` passes as `to` to open this sheet. Unique within one stack of sheets. */
52
+ name: string;
53
+ /**
54
+ * The sheet's parts. Pass a function to read the `payload` of the trigger
55
+ * that opened it; it is `undefined` when that trigger has none.
56
+ */
57
+ children?: ReactNode | ((payload: any) => ReactNode);
58
+ /** The edge it rises from. Default: the edge of the sheet it is declared in. */
59
+ side?: SheetSide;
60
+ /** Whether a click outside, a swipe or Escape closes it. Default true. */
61
+ dismissible?: boolean;
62
+ /** Called when it opens or closes. */
63
+ onOpenChange?: (open: boolean) => void;
64
+ }
65
+ /**
66
+ * A sheet opened from the one it is declared in, by any `Sheet.Trigger` whose
67
+ * `to` is its `name`. Put it anywhere inside the first sheet's content; it
68
+ * takes `Sheet.Content`'s props and draws its own content, so its children are
69
+ * the header, body and footer.
70
+ */
71
+ declare function SheetNext({ name, children, side, dismissible, onOpenChange, ...content }: SheetNextProps): import("react").JSX.Element;
72
+ declare namespace SheetNext {
73
+ var displayName: string;
74
+ }
37
75
  export interface SheetContentProps extends Omit<ComponentProps<'div'>, 'children'> {
38
76
  children?: ReactNode;
39
77
  /**
@@ -136,6 +174,7 @@ declare namespace SheetStage {
136
174
  }
137
175
  export declare const Sheet: typeof SheetRoot & {
138
176
  Trigger: typeof SheetTrigger;
177
+ Next: typeof SheetNext;
139
178
  Content: typeof SheetContent;
140
179
  Header: typeof SheetHeader;
141
180
  Title: typeof SheetTitle;
@@ -145,4 +184,4 @@ export declare const Sheet: typeof SheetRoot & {
145
184
  Close: typeof SheetClose;
146
185
  Stage: typeof SheetStage;
147
186
  };
148
- export { SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, SheetClose, SheetStage, };
187
+ export { SheetTrigger, SheetNext, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, SheetClose, SheetStage, };
@@ -34,6 +34,13 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
34
34
  * still shows, and dimmed. Only the first sheet darkens the page, so two open
35
35
  * sheets do not stack two shades of black.
36
36
  *
37
+ * The next sheet is declared once, as `Sheet.Next` with a `name`, and opened
38
+ * by any `Sheet.Trigger to="name"` in the sheet: a row in the body, an icon in
39
+ * the header, a button in the footer. The trigger and the sheet it opens are
40
+ * apart in the markup because they are apart on the screen: a list of rows
41
+ * that each open a sheet should not have to wrap every row in one. A trigger
42
+ * can carry a `payload`, so one `Sheet.Next` serves every row of a list.
43
+ *
37
44
  * ## The hand decides
38
45
  *
39
46
  * Every sheet follows a drag and leaves on a flick toward its own edge. The
@@ -41,14 +48,14 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
41
48
  */
42
49
  import { Drawer } from '@base-ui/react/drawer';
43
50
  import { ScrollArea } from '@base-ui/react/scroll-area';
44
- import { createContext, useContext, useMemo, } from 'react';
51
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
45
52
  import { tv } from 'tailwind-variants';
46
53
  /*
47
54
  * One curve for every movement: the sheet arriving, the one behind it stepping
48
55
  * back, the page receding. A fast start that eases into place reads as weight,
49
56
  * where a symmetric ease reads as a slide show.
50
57
  */
51
- const MOVE = '[transition:transform_450ms_cubic-bezier(0.32,0.72,0,1),border-radius_450ms_cubic-bezier(0.32,0.72,0,1),margin_450ms_cubic-bezier(0.32,0.72,0,1)]';
58
+ const MOVE = '[transition:transform_450ms_cubic-bezier(0.32,0.72,0,1),border-radius_450ms_cubic-bezier(0.32,0.72,0,1),margin_450ms_cubic-bezier(0.32,0.72,0,1),height_450ms_cubic-bezier(0.32,0.72,0,1)]';
52
59
  const sheetVariants = tv({
53
60
  slots: {
54
61
  backdrop: [
@@ -83,6 +90,17 @@ const sheetVariants = tv({
83
90
  "after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:bg-popover after:opacity-0 after:transition-opacity after:duration-[450ms] after:content-['']",
84
91
  'data-nested-drawer-open:after:opacity-50',
85
92
  ],
93
+ /*
94
+ * A top or bottom sheet behind another takes the front one's height, so
95
+ * the strip that shows past it is the same whichever of the two is taller.
96
+ * A taller sheet behind would otherwise stand above the stack, and a
97
+ * shorter one would vanish behind the front one entirely.
98
+ */
99
+ stacked: [
100
+ 'data-nested-drawer-open:h-(--drawer-frontmost-height) data-nested-drawer-open:overflow-hidden',
101
+ // Lets the height animate to and from `auto` where the browser can.
102
+ '[interpolate-size:allow-keywords]',
103
+ ],
86
104
  grabber: [
87
105
  'pointer-events-none absolute inset-x-0 z-10 mx-auto flex h-6 w-24 justify-center',
88
106
  'before:h-[5px] before:w-9 before:rounded-full before:bg-muted-foreground/35',
@@ -206,6 +224,19 @@ const SWIPE = {
206
224
  left: 'left',
207
225
  right: 'right',
208
226
  };
227
+ function createStack() {
228
+ const handles = new Map();
229
+ return {
230
+ handle(name) {
231
+ let handle = handles.get(name);
232
+ if (!handle) {
233
+ handle = Drawer.createHandle();
234
+ handles.set(name, handle);
235
+ }
236
+ return handle;
237
+ },
238
+ };
239
+ }
209
240
  const SheetContext = createContext(null);
210
241
  function useSheet(part) {
211
242
  const context = useContext(SheetContext);
@@ -213,11 +244,54 @@ function useSheet(part) {
213
244
  throw new Error(`${part} must be used inside a <Sheet>.`);
214
245
  return context;
215
246
  }
216
- function SheetRoot({ children, open, onOpenChange, defaultOpen = false, side = 'bottom', dismissible = true, }) {
247
+ function SheetRoot(props) {
248
+ return _jsx(SheetFrame, { ...props });
249
+ }
250
+ SheetRoot.displayName = 'Sheet';
251
+ function SheetFrame({ children, open, onOpenChange, defaultOpen = false, side = 'bottom', dismissible = true, handle: drawerHandle, }) {
217
252
  // A sheet inside another sheet's content finds that sheet's context.
218
253
  const parent = useContext(SheetContext);
219
- const context = useMemo(() => ({ side, nested: parent !== null && !parent.inline, inline: false, showClose: true }), [side, parent]);
220
- return (_jsx(SheetContext.Provider, { value: context, children: _jsx(Drawer.Root, { open: open, defaultOpen: defaultOpen, swipeDirection: SWIPE[side], disablePointerDismissal: !dismissible, onOpenChange: (next, details) => {
254
+ const nested = parent !== null && !parent.inline;
255
+ const [ownStack] = useState(createStack);
256
+ const stack = nested && parent ? parent.stack : ownStack;
257
+ // Held here as well as by the primitive, so a sheet opened from this one
258
+ // can close it.
259
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
260
+ const isOpen = open ?? internalOpen;
261
+ const setOpen = useCallback((next) => {
262
+ if (open === undefined)
263
+ setInternalOpen(next);
264
+ onOpenChange?.(next);
265
+ }, [open, onOpenChange]);
266
+ // The sheets opened from this one, so a press on the page behind them all
267
+ // can close the whole stack. The primitive gives that press to no one
268
+ // while a second sheet is open.
269
+ const opened = useRef(new Set());
270
+ const live = useRef({ isOpen, setOpen });
271
+ live.current = { isOpen, setOpen };
272
+ const handle = useMemo(() => ({
273
+ isOpen: () => live.current.isOpen,
274
+ closeTree: () => {
275
+ opened.current.forEach((child) => child.closeTree());
276
+ if (live.current.isOpen)
277
+ live.current.setOpen(false);
278
+ },
279
+ }), []);
280
+ useEffect(() => (nested && parent ? parent.register(handle) : undefined), [nested, parent, handle]);
281
+ const context = useMemo(() => ({
282
+ side,
283
+ stack,
284
+ nested,
285
+ inline: false,
286
+ showClose: true,
287
+ register: (child) => {
288
+ opened.current.add(child);
289
+ return () => opened.current.delete(child);
290
+ },
291
+ closeTree: handle.closeTree,
292
+ hasOpenChild: () => [...opened.current].some((child) => child.isOpen()),
293
+ }), [side, stack, nested, handle]);
294
+ return (_jsx(SheetContext.Provider, { value: context, children: _jsx(Drawer.Root, { open: isOpen, handle: drawerHandle, swipeDirection: SWIPE[side], disablePointerDismissal: !dismissible, onOpenChange: (next, details) => {
221
295
  // The primitive has a switch for a press outside and none for
222
296
  // Escape or a swipe, so those are refused here.
223
297
  if (!next &&
@@ -226,15 +300,44 @@ function SheetRoot({ children, open, onOpenChange, defaultOpen = false, side = '
226
300
  details.reason !== 'imperative-action') {
227
301
  return;
228
302
  }
229
- onOpenChange?.(next);
230
- }, children: children }) }));
303
+ setOpen(next);
304
+ }, children: typeof children === 'function'
305
+ ? ({ payload }) => children(payload)
306
+ : children }) }));
231
307
  }
232
- SheetRoot.displayName = 'Sheet';
233
- /** What opens the sheet. A plain button unless `render` supplies one. */
234
- function SheetTrigger({ render, children, ...props }) {
308
+ /**
309
+ * What opens a sheet: its own, or with `to`, the `Sheet.Next` of that name.
310
+ * A plain button unless `render` supplies one.
311
+ */
312
+ function SheetTrigger({ render, children, to, payload, ...props }) {
313
+ const sheet = useContext(SheetContext);
314
+ if (to !== undefined) {
315
+ if (!sheet)
316
+ throw new Error('Sheet.Trigger with `to` must be used inside a <Sheet>.');
317
+ return (_jsx(Drawer.Trigger, { handle: sheet.stack.handle(to), payload: payload, render: render, ...props, children: children }));
318
+ }
235
319
  return (_jsx(Drawer.Trigger, { render: render, ...props, children: children }));
236
320
  }
237
321
  SheetTrigger.displayName = 'Sheet.Trigger';
322
+ /**
323
+ * A sheet opened from the one it is declared in, by any `Sheet.Trigger` whose
324
+ * `to` is its `name`. Put it anywhere inside the first sheet's content; it
325
+ * takes `Sheet.Content`'s props and draws its own content, so its children are
326
+ * the header, body and footer.
327
+ */
328
+ function SheetNext({ name, children, side, dismissible, onOpenChange, ...content }) {
329
+ const parent = useSheet('Sheet.Next');
330
+ return (_jsx(SheetFrame, { handle: parent.stack.handle(name), side: side ?? parent.side, dismissible: dismissible, onOpenChange: onOpenChange, children: (payload) => (_jsx(SheetContent, { ...content, children: typeof children === 'function' ? _jsx(NextParts, { render: children, payload: payload }) : children })) }));
331
+ }
332
+ SheetNext.displayName = 'Sheet.Next';
333
+ /**
334
+ * Calls a `Sheet.Next` children function. As a component of its own it only
335
+ * runs once the sheet is open and its content mounted, which is when the
336
+ * trigger has handed over its payload.
337
+ */
338
+ function NextParts({ render, payload }) {
339
+ return _jsx(_Fragment, { children: render(payload) });
340
+ }
238
341
  /** The sheet itself, with the backdrop behind it. */
239
342
  function SheetContent({ children, detached = true, size = 'auto', showGrabber, showClose = true, blur = false, inline = false, className, ...props }) {
240
343
  const sheet = useSheet('Sheet.Content');
@@ -250,7 +353,12 @@ function SheetContent({ children, detached = true, size = 'auto', showGrabber, s
250
353
  if (inline) {
251
354
  return (_jsx(SheetContext.Provider, { value: context, children: _jsx("div", { className: slots.popup({ className: ['[transform:none]', className] }), ...props, children: inside }) }));
252
355
  }
253
- return (_jsxs(Drawer.Portal, { children: [sheet.nested ? null : _jsx(Drawer.Backdrop, { className: slots.backdrop() }), _jsx(Drawer.Viewport, { className: slots.viewport(), children: _jsx(Drawer.Popup, { className: slots.popup({ className }), ...props, children: _jsx(SheetContext.Provider, { value: context, children: inside }) }) })] }));
356
+ return (_jsxs(Drawer.Portal, { children: [sheet.nested ? null : (_jsx(Drawer.Backdrop, { className: slots.backdrop(),
357
+ // With a second sheet open, a press here is outside both, and closes both.
358
+ onClick: () => {
359
+ if (sheet.hasOpenChild())
360
+ sheet.closeTree();
361
+ } })), _jsx(Drawer.Viewport, { className: slots.viewport(), children: _jsx(Drawer.Popup, { className: slots.popup({ className: [vertical && slots.stacked(), className] }), ...props, children: _jsx(SheetContext.Provider, { value: context, children: inside }) }) })] }));
254
362
  }
255
363
  SheetContent.displayName = 'Sheet.Content';
256
364
  /**
@@ -318,6 +426,7 @@ function SheetStage({ className, children, ...props }) {
318
426
  SheetStage.displayName = 'Sheet.Stage';
319
427
  export const Sheet = Object.assign(SheetRoot, {
320
428
  Trigger: SheetTrigger,
429
+ Next: SheetNext,
321
430
  Content: SheetContent,
322
431
  Header: SheetHeader,
323
432
  Title: SheetTitle,
@@ -327,4 +436,4 @@ export const Sheet = Object.assign(SheetRoot, {
327
436
  Close: SheetClose,
328
437
  Stage: SheetStage,
329
438
  });
330
- export { SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, SheetClose, SheetStage, };
439
+ export { SheetTrigger, SheetNext, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, SheetClose, SheetStage, };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Switch — on or off, for a setting that takes effect the moment it changes.
3
+ *
4
+ * ```tsx
5
+ * <Switch label="Dark mode" defaultChecked />
6
+ *
7
+ * <Label>
8
+ * <Label.Text>Email me a receipt</Label.Text>
9
+ * <Label.Action><Switch /></Label.Action>
10
+ * </Label>
11
+ * ```
12
+ *
13
+ * ## It moves like a thing with weight
14
+ *
15
+ * The colour does not fade in. It sweeps along the track behind the thumb,
16
+ * so the track fills from the side the thumb left and reads as the thumb's
17
+ * doing. While it is pressed, the thumb stretches towards where it is going,
18
+ * so a press that has not yet let go already says which way it will flip.
19
+ *
20
+ * ## While a change is saved
21
+ *
22
+ * A setting saved to a server is not on until the server says so. `pending`
23
+ * puts a turning ring in the thumb and stops the switch being pressed again,
24
+ * so the switch can show the new state at once and still be honest that it
25
+ * has not landed yet.
26
+ */
27
+ import { Switch as BaseSwitch } from '@base-ui/react/switch';
28
+ import type { CSSProperties } from 'react';
29
+ export type SwitchSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
30
+ export type SwitchColor = 'primary' | 'success' | 'info' | 'destructive';
31
+ export interface SwitchProps extends Omit<BaseSwitch.Root.Props, 'className' | 'color'> {
32
+ /** Default `md`. */
33
+ size?: SwitchSize;
34
+ /** The colour it fills with when on. `primary` by default; pick another for what it means. */
35
+ color?: SwitchColor;
36
+ /**
37
+ * What a screen reader calls it, when there is no visible label tied to it.
38
+ * A `Label` wrapped round it, or pointing at its `id`, is better when there is room.
39
+ */
40
+ label?: string;
41
+ /** A change is being saved: a ring turns in the thumb, and it cannot be pressed again until it is done. */
42
+ pending?: boolean;
43
+ className?: string;
44
+ style?: CSSProperties;
45
+ }
46
+ /** A switch. Takes `checked`/`onCheckedChange` or `defaultChecked`, and `name` for a form. */
47
+ declare function Switch({ size, color, label, pending, className, style, readOnly, ...props }: SwitchProps): import("react").JSX.Element;
48
+ declare namespace Switch {
49
+ var displayName: string;
50
+ }
51
+ export { Switch };