panelui-web 0.9.1 → 0.10.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,230 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * Inspector — the settings panel beside an editor's canvas: a rail of
5
+ * icons, each opening a page of titled sections.
6
+ *
7
+ * ```tsx
8
+ * <Inspector defaultValue="pointer">
9
+ * <Inspector.Rail>
10
+ * <Inspector.Tab value="pointer" label="Pointer"><MousePointer2Icon /></Inspector.Tab>
11
+ * <Inspector.Tab value="audio" label="Audio"><Volume2Icon /></Inspector.Tab>
12
+ * </Inspector.Rail>
13
+ * <Inspector.Panel>
14
+ * <Inspector.Page value="pointer">
15
+ * <Inspector.Section>
16
+ * <Inspector.SectionTitle>Click sound</Inspector.SectionTitle>
17
+ * <Inspector.SectionAction><Switch label="Click sound" /></Inspector.SectionAction>
18
+ * <Inspector.SectionContent>…</Inspector.SectionContent>
19
+ * </Inspector.Section>
20
+ * </Inspector.Page>
21
+ * </Inspector.Panel>
22
+ * </Inspector>
23
+ * ```
24
+ *
25
+ * ## The rail stands apart
26
+ *
27
+ * The rail is a pill of its own beside the panel, not a strip inside it.
28
+ * It is navigation, and the panel is the thing being navigated; drawn
29
+ * apart, the eye knows which of the two it is looking at. A filled marker
30
+ * slides along the rail to the chosen page, and the page arrives from the
31
+ * direction the marker went.
32
+ *
33
+ * ## Sections carry their own switch
34
+ *
35
+ * A section's title sits on one line with its action: the switch that turns
36
+ * the whole section on, or a Reset that puts its values back. The controls
37
+ * under it are what that switch or reset applies to, so the relation never
38
+ * has to be explained. A section can fold away with `collapsible`.
39
+ */
40
+ import { Collapsible } from '@base-ui/react/collapsible';
41
+ import { ScrollArea } from '@base-ui/react/scroll-area';
42
+ import { Tabs } from '@base-ui/react/tabs';
43
+ import { Tooltip } from '@base-ui/react/tooltip';
44
+ import { createContext, useContext } from 'react';
45
+ import { tv } from 'tailwind-variants';
46
+ const EASE = 'ease-[cubic-bezier(0.3,1.2,0.4,1)]';
47
+ const inspectorVariants = tv({
48
+ slots: {
49
+ root: 'group/inspector flex items-start gap-2 data-[orientation=vertical]:flex-row',
50
+ rail: [
51
+ 'relative isolate flex shrink-0 flex-col items-center gap-1 rounded-[20px] bg-popover p-1.5 text-popover-foreground',
52
+ 'shadow-[0_10px_30px_-12px_rgb(0_0_0/0.3)] ring-1 ring-foreground/[0.06] dark:ring-foreground/10',
53
+ ],
54
+ marker: [
55
+ 'absolute -z-10 rounded-[14px] bg-primary shadow-[0_1px_2px_rgb(0_0_0/0.18),inset_0_1px_0_rgb(255_255_255/0.18)]',
56
+ 'top-(--active-tab-top) left-(--active-tab-left) h-(--active-tab-height) w-(--active-tab-width)',
57
+ `transition-[top,height] duration-[380ms] ${EASE} motion-reduce:transition-none`,
58
+ ],
59
+ tab: [
60
+ 'relative z-10 inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-[14px] outline-none select-none',
61
+ 'text-muted-foreground transition-[color,background-color,scale] duration-150',
62
+ 'hover:bg-foreground/[0.06] hover:text-foreground active:scale-[0.94]',
63
+ 'data-active:bg-transparent data-active:text-primary-foreground data-active:hover:bg-transparent',
64
+ 'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',
65
+ 'data-disabled:pointer-events-none data-disabled:opacity-40',
66
+ '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-[1.125rem]',
67
+ ],
68
+ railSeparator: 'my-1 h-px w-5 shrink-0 rounded-full bg-foreground/15',
69
+ tooltip: [
70
+ 'origin-(--transform-origin) rounded-lg bg-foreground px-2 py-1 text-xs font-medium text-background shadow-md',
71
+ 'transition-[opacity,scale] duration-150 data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:opacity-0',
72
+ 'data-instant:duration-0 motion-reduce:transition-none',
73
+ ],
74
+ panel: [
75
+ 'relative flex w-72 min-w-0 flex-col overflow-hidden rounded-[22px] bg-popover text-popover-foreground',
76
+ 'shadow-[0_10px_30px_-12px_rgb(0_0_0/0.3)] ring-1 ring-foreground/[0.06] dark:ring-foreground/10',
77
+ ],
78
+ page: [
79
+ 'flex min-h-0 flex-1 flex-col outline-none',
80
+ `transition-[opacity,translate] duration-300 ${EASE} motion-reduce:transition-none`,
81
+ 'data-starting-style:opacity-0',
82
+ 'data-[activation-direction=down]:data-starting-style:translate-y-3 data-[activation-direction=up]:data-starting-style:-translate-y-3',
83
+ 'data-ending-style:hidden',
84
+ ],
85
+ viewport: 'min-h-0 flex-1 overscroll-contain outline-none',
86
+ content: 'flex flex-col gap-1 p-3.5',
87
+ scrollbar: 'my-3 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',
88
+ thumb: 'relative flex-1 rounded-full bg-foreground/20',
89
+ section: 'grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 py-1.5',
90
+ sectionTitle: [
91
+ 'col-start-1 row-start-1 flex min-h-8 items-center text-left text-[13px] font-medium text-foreground',
92
+ // A section that folds: its title is the button, with a chevron that turns.
93
+ 'data-[slot=inspector-section-trigger]:-mx-1.5 data-[slot=inspector-section-trigger]:cursor-pointer data-[slot=inspector-section-trigger]:gap-1.5 data-[slot=inspector-section-trigger]:rounded-lg data-[slot=inspector-section-trigger]:px-1.5',
94
+ 'data-[slot=inspector-section-trigger]:outline-none data-[slot=inspector-section-trigger]:hover:bg-foreground/[0.04] data-[slot=inspector-section-trigger]:focus-visible:ring-2 data-[slot=inspector-section-trigger]:focus-visible:ring-ring',
95
+ ],
96
+ // Right after the title: up while the section is open, down while it is folded.
97
+ chevron: [
98
+ 'size-3.5 shrink-0 rotate-180 text-muted-foreground transition-transform duration-200',
99
+ 'in-data-panel-open:rotate-0 motion-reduce:transition-none',
100
+ ],
101
+ sectionAction: 'col-start-2 row-start-1 flex items-center gap-1.5',
102
+ sectionContent: [
103
+ 'col-span-2 flex flex-col gap-2 pt-2',
104
+ // Folding: the height follows the content, from and to nothing.
105
+ 'data-[slot=inspector-section-panel]:h-(--collapsible-panel-height) data-[slot=inspector-section-panel]:overflow-hidden',
106
+ 'data-[slot=inspector-section-panel]:transition-[height,opacity] data-[slot=inspector-section-panel]:duration-300',
107
+ 'data-[slot=inspector-section-panel]:ease-[cubic-bezier(0.3,1.2,0.4,1)]',
108
+ 'data-starting-style:h-0 data-starting-style:opacity-0 data-ending-style:h-0 data-ending-style:opacity-0 motion-reduce:transition-none',
109
+ ],
110
+ separator: 'my-2 h-px shrink-0 bg-foreground/[0.08]',
111
+ },
112
+ });
113
+ const SectionContext = createContext(null);
114
+ /**
115
+ * The rail and the panel side by side. `defaultValue`, or `value` with
116
+ * `onValueChange`, names the page that is open.
117
+ */
118
+ function InspectorRoot({ className, ...props }) {
119
+ const slots = inspectorVariants();
120
+ return (_jsx(Tooltip.Provider, { delay: 500, closeDelay: 0, children: _jsx(Tabs.Root, { "data-slot": "inspector", orientation: "vertical", className: slots.root({ className }), ...props }) }));
121
+ }
122
+ InspectorRoot.displayName = 'Inspector';
123
+ /** The column of page icons, in a pill of its own beside the panel. */
124
+ function InspectorRail({ className, children, ...props }) {
125
+ const slots = inspectorVariants();
126
+ return (_jsxs(Tabs.List, { "data-slot": "inspector-rail", className: slots.rail({ className }), ...props, children: [children, _jsx(Tabs.Indicator, { "data-slot": "inspector-marker", className: slots.marker(), renderBeforeHydration: true })] }));
127
+ }
128
+ InspectorRail.displayName = 'Inspector.Rail';
129
+ /** One page's icon on the rail. */
130
+ function InspectorTab({ label, className, children, ...props }) {
131
+ const slots = inspectorVariants();
132
+ return (_jsxs(Tooltip.Root, { children: [_jsx(Tooltip.Trigger, { render: _jsx(Tabs.Tab, { "data-slot": "inspector-tab", "aria-label": label, className: slots.tab({ className }), ...props, children: children }) }), _jsx(Tooltip.Portal, { children: _jsx(Tooltip.Positioner, { side: "left", sideOffset: 10, className: "z-50", children: _jsx(Tooltip.Popup, { className: slots.tooltip(), children: label }) }) })] }));
133
+ }
134
+ InspectorTab.displayName = 'Inspector.Tab';
135
+ /** A short line between groups of pages on the rail. */
136
+ function InspectorRailSeparator({ className, ...props }) {
137
+ const slots = inspectorVariants();
138
+ return _jsx("span", { "aria-hidden": true, "data-slot": "inspector-rail-separator", className: slots.railSeparator({ className }), ...props });
139
+ }
140
+ InspectorRailSeparator.displayName = 'Inspector.RailSeparator';
141
+ /**
142
+ * The card the pages open in. Give it a height, such as `h-[32rem]`, and a
143
+ * page longer than that scrolls inside it. Without pages, its children are
144
+ * one page of sections.
145
+ */
146
+ function InspectorPanel({ className, children, ...props }) {
147
+ const slots = inspectorVariants();
148
+ return (_jsx("div", { "data-slot": "inspector-panel", className: slots.panel({ className }), ...props, children: hasPages(children) ? children : _jsx(Scroll, { children: children }) }));
149
+ }
150
+ InspectorPanel.displayName = 'Inspector.Panel';
151
+ /** Whether the panel's children are pages, rather than sections written straight into it. */
152
+ function hasPages(children) {
153
+ const list = Array.isArray(children) ? children : [children];
154
+ return list.some((child) => typeof child === 'object' &&
155
+ child !== null &&
156
+ 'type' in child &&
157
+ child.type === InspectorPage);
158
+ }
159
+ /** The scrolling column of a page, with a thin bar that shows while it moves. */
160
+ function Scroll({ children }) {
161
+ const slots = inspectorVariants();
162
+ return (_jsxs(ScrollArea.Root, { className: "relative flex min-h-0 flex-1 flex-col", children: [_jsx(ScrollArea.Viewport, { className: slots.viewport(), children: _jsx(ScrollArea.Content, { className: slots.content(), children: children }) }), _jsx(ScrollArea.Scrollbar, { orientation: "vertical", className: slots.scrollbar(), children: _jsx(ScrollArea.Thumb, { className: slots.thumb() }) })] }));
163
+ }
164
+ /** What one tab opens: a column of sections that scrolls inside the panel. */
165
+ function InspectorPage({ className, children, ...props }) {
166
+ const slots = inspectorVariants();
167
+ return (_jsx(Tabs.Panel, { "data-slot": "inspector-page", className: slots.page({ className }), ...props, children: _jsx(Scroll, { children: children }) }));
168
+ }
169
+ InspectorPage.displayName = 'Inspector.Page';
170
+ /** A titled group of settings: a title, an action on its line, and the controls under them. */
171
+ function InspectorSection({ collapsible = false, defaultOpen = true, open, onOpenChange, className, children, ...props }) {
172
+ const slots = inspectorVariants();
173
+ if (!collapsible) {
174
+ return (_jsx(SectionContext.Provider, { value: { collapsible: false }, children: _jsx("div", { "data-slot": "inspector-section", className: slots.section({ className }), ...props, children: children }) }));
175
+ }
176
+ return (_jsx(SectionContext.Provider, { value: { collapsible: true }, children: _jsx(Collapsible.Root, { "data-slot": "inspector-section", defaultOpen: defaultOpen, open: open, onOpenChange: onOpenChange ? (next) => onOpenChange(next) : undefined, className: slots.section({ className }), ...props, children: children }) }));
177
+ }
178
+ InspectorSection.displayName = 'Inspector.Section';
179
+ function useSection(part) {
180
+ const section = useContext(SectionContext);
181
+ if (!section)
182
+ throw new Error(`${part} must be used inside an <Inspector.Section>.`);
183
+ return section;
184
+ }
185
+ /** The section's name. In a collapsible section it is the button that folds it, with a chevron. */
186
+ function InspectorSectionTitle({ className, children, ...props }) {
187
+ const { collapsible } = useSection('Inspector.SectionTitle');
188
+ const slots = inspectorVariants();
189
+ if (!collapsible) {
190
+ return (_jsx("h3", { "data-slot": "inspector-section-title", className: slots.sectionTitle({ className }), ...props, children: children }));
191
+ }
192
+ return (_jsx("h3", { className: "col-start-1 row-start-1 m-0", children: _jsxs(Collapsible.Trigger, { "data-slot": "inspector-section-trigger", className: slots.sectionTitle({ className: ['w-full', className] }), children: [children, _jsx("svg", { viewBox: "0 0 16 16", "aria-hidden": true, className: slots.chevron(), fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m4 10 4-4 4 4" }) })] }) }));
193
+ }
194
+ InspectorSectionTitle.displayName = 'Inspector.SectionTitle';
195
+ /** What sits at the end of the title's line: the switch for the whole section, or a Reset. */
196
+ function InspectorSectionAction({ className, ...props }) {
197
+ useSection('Inspector.SectionAction');
198
+ const slots = inspectorVariants();
199
+ return _jsx("div", { "data-slot": "inspector-section-action", className: slots.sectionAction({ className }), ...props });
200
+ }
201
+ InspectorSectionAction.displayName = 'Inspector.SectionAction';
202
+ /** The controls under the title, stacked with an even gap. */
203
+ function InspectorSectionContent({ className, children, ...props }) {
204
+ const { collapsible } = useSection('Inspector.SectionContent');
205
+ const slots = inspectorVariants();
206
+ if (!collapsible) {
207
+ return (_jsx("div", { "data-slot": "inspector-section-content", className: slots.sectionContent({ className }), ...props, children: children }));
208
+ }
209
+ return (_jsx(Collapsible.Panel, { "data-slot": "inspector-section-panel", className: slots.sectionContent({ className }), style: props.style, children: children }));
210
+ }
211
+ InspectorSectionContent.displayName = 'Inspector.SectionContent';
212
+ /** A hairline between sections that belong to different things. */
213
+ function InspectorSeparator({ className, ...props }) {
214
+ const slots = inspectorVariants();
215
+ return _jsx("div", { role: "separator", "data-slot": "inspector-separator", className: slots.separator({ className }), ...props });
216
+ }
217
+ InspectorSeparator.displayName = 'Inspector.Separator';
218
+ export const Inspector = Object.assign(InspectorRoot, {
219
+ Rail: InspectorRail,
220
+ Tab: InspectorTab,
221
+ RailSeparator: InspectorRailSeparator,
222
+ Panel: InspectorPanel,
223
+ Page: InspectorPage,
224
+ Section: InspectorSection,
225
+ SectionTitle: InspectorSectionTitle,
226
+ SectionAction: InspectorSectionAction,
227
+ SectionContent: InspectorSectionContent,
228
+ Separator: InspectorSeparator,
229
+ });
230
+ export { InspectorRail, InspectorTab, InspectorRailSeparator, InspectorPanel, InspectorPage, InspectorSection, InspectorSectionTitle, InspectorSectionAction, InspectorSectionContent, InspectorSeparator, inspectorVariants, };
@@ -84,9 +84,16 @@ export interface SheetContentProps extends Omit<ComponentProps<'div'>, 'children
84
84
  * `full` take that share of the screen, and `full` meets the edges.
85
85
  */
86
86
  size?: 'auto' | 'half' | 'full';
87
- /** Show the grabber. On by default for a top or bottom sheet. */
87
+ /**
88
+ * Show the grabber. On by default for a top or bottom sheet that a swipe
89
+ * can close, and off when `dismissible` is.
90
+ */
88
91
  showGrabber?: boolean;
89
- /** Show the round close button in the top trailing corner. Default true. */
92
+ /**
93
+ * Show the round close button in the top trailing corner. Default true.
94
+ * Write a bare `<Sheet.Close />` into the sheet to place it somewhere else,
95
+ * or set this to `false` for a sheet with no close button at all.
96
+ */
90
97
  showClose?: boolean;
91
98
  /** Frost the page behind the sheet instead of only dimming it. */
92
99
  blur?: boolean;
@@ -150,13 +157,17 @@ declare namespace SheetFooter {
150
157
  export interface SheetCloseProps extends ComponentProps<'button'> {
151
158
  /**
152
159
  * Your own element to close the sheet with, such as a Cancel button.
153
- * Without it, and without children, this is the round close button.
160
+ * Without it, and without children, this is the round close button, drawn
161
+ * where it is written in place of the one in the corner.
154
162
  */
155
163
  render?: ReactElement;
156
- /** Draw the cross glyph. Set for you on the built-in close button. */
164
+ /** Draw the round close button. A `Sheet.Close` with nothing inside it already is one. */
157
165
  icon?: boolean;
158
166
  }
159
- /** Closes the sheet. Works even when `dismissible` is off. */
167
+ /**
168
+ * Closes the sheet. Works even when `dismissible` is off. Written bare, it is
169
+ * the round close button, and the sheet stops drawing its own in the corner.
170
+ */
160
171
  declare function SheetClose({ render, icon, children, ...props }: SheetCloseProps): import("react").JSX.Element;
161
172
  declare namespace SheetClose {
162
173
  var displayName: string;
@@ -46,6 +46,18 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
46
46
  * Every sheet follows a drag and leaves on a flick toward its own edge. The
47
47
  * grabber on a top or bottom sheet says so before anybody tries.
48
48
  *
49
+ * With a mouse, the text in a sheet is for reading: a press on the title,
50
+ * the description, the body or the footer selects text the way it does on the
51
+ * page, and the sheet is dragged by its grabber and the padding around the
52
+ * header. A finger still swipes the sheet from anywhere.
53
+ *
54
+ * ## The close button
55
+ *
56
+ * Every sheet has a round close button in its top trailing corner. Write a
57
+ * bare `<Sheet.Close />` somewhere in the sheet to put it there instead, for
58
+ * example first in `Sheet.Header` to stand before the title, and the one in
59
+ * the corner steps aside. `showClose={false}` leaves the sheet with none.
60
+ *
49
61
  * ## Slides
50
62
  *
51
63
  * `Sheet.Slides` takes the place of the body with a column of rounded
@@ -58,7 +70,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
58
70
  */
59
71
  import { Drawer } from '@base-ui/react/drawer';
60
72
  import { ScrollArea } from '@base-ui/react/scroll-area';
61
- import { Children, createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
73
+ import { Children, createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
62
74
  import { tv } from 'tailwind-variants';
63
75
  /*
64
76
  * One curve for every movement: the sheet arriving, the one behind it stepping
@@ -116,12 +128,24 @@ const sheetVariants = tv({
116
128
  'before:h-[5px] before:w-9 before:rounded-full before:bg-muted-foreground/35',
117
129
  ],
118
130
  close: [
119
- 'absolute end-4 top-4 z-10 inline-flex size-8 cursor-pointer items-center justify-center rounded-full',
131
+ 'relative inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full',
120
132
  'bg-muted text-muted-foreground outline-none transition-[background-color,color,scale] duration-150',
121
133
  'after:absolute after:-inset-1.5 hover:bg-foreground/10 hover:text-foreground active:scale-95',
122
134
  'focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none',
123
135
  ],
124
- header: 'flex shrink-0 flex-col gap-1 px-6 pt-6 pb-3',
136
+ closeCorner: 'absolute end-4 top-4 z-10',
137
+ /*
138
+ * A close button written into the header shares the title's line: the
139
+ * header becomes three columns, the button takes the first or the last
140
+ * depending on where it was written, and the text keeps the middle.
141
+ */
142
+ header: [
143
+ 'flex shrink-0 flex-col gap-1 px-6 pt-6 pb-3',
144
+ 'has-[>[data-slot=sheet-close]]:grid has-[>[data-slot=sheet-close]]:grid-cols-[auto_minmax(0,1fr)_auto] has-[>[data-slot=sheet-close]]:gap-x-3',
145
+ '[&>*]:col-start-2',
146
+ '[&>[data-slot=sheet-close]]:row-span-2 [&>[data-slot=sheet-close]]:row-start-1 [&>[data-slot=sheet-close]]:-my-1 [&>[data-slot=sheet-close]]:self-start',
147
+ '[&>[data-slot=sheet-close]:first-child]:col-start-1 [&>[data-slot=sheet-close]:not(:first-child)]:col-start-3',
148
+ ],
125
149
  title: 'text-[19px] font-semibold leading-6 tracking-[-0.01em]',
126
150
  description: 'text-[15px] leading-5 text-muted-foreground',
127
151
  body: 'relative flex min-h-0 flex-1 flex-col',
@@ -319,14 +343,16 @@ function SheetFrame({ children, open, onOpenChange, defaultOpen = false, side =
319
343
  stack,
320
344
  nested,
321
345
  inline: false,
346
+ dismissible,
322
347
  showClose: true,
348
+ claimClose: () => () => { },
323
349
  register: (child) => {
324
350
  opened.current.add(child);
325
351
  return () => opened.current.delete(child);
326
352
  },
327
353
  closeTree: handle.closeTree,
328
354
  hasOpenChild: () => [...opened.current].some((child) => child.isOpen()),
329
- }), [side, stack, nested, handle]);
355
+ }), [side, stack, nested, dismissible, handle]);
330
356
  return (_jsx(SheetContext.Provider, { value: context, children: _jsx(Drawer.Root, { open: isOpen, handle: drawerHandle, swipeDirection: SWIPE[side], disablePointerDismissal: !dismissible, onOpenChange: (next, details) => {
331
357
  // The primitive has a switch for a press outside and none for
332
358
  // Escape or a swipe, so those are refused here.
@@ -384,8 +410,16 @@ function SheetContent({ children, detached = true, size = 'auto', showGrabber, s
384
410
  attached: !detached || (vertical && size === 'full'),
385
411
  blur,
386
412
  });
387
- const context = useMemo(() => ({ ...sheet, inline, showClose }), [sheet, inline, showClose]);
388
- const inside = (_jsxs(_Fragment, { children: [(showGrabber ?? vertical) ? _jsx("div", { "aria-hidden": true, className: slots.grabber() }) : null, showClose ? _jsx(SheetClose, { "aria-label": "Close", className: slots.close(), icon: true }) : null, children] }));
413
+ // Bare close buttons written into the sheet. While there is one, the
414
+ // corner button is not drawn.
415
+ const [placed, setPlaced] = useState(0);
416
+ const claimClose = useCallback(() => {
417
+ setPlaced((count) => count + 1);
418
+ return () => setPlaced((count) => count - 1);
419
+ }, []);
420
+ const corner = showClose && placed === 0;
421
+ const context = useMemo(() => ({ ...sheet, inline, showClose: corner, claimClose }), [sheet, inline, corner, claimClose]);
422
+ const inside = (_jsxs(_Fragment, { children: [(showGrabber ?? (vertical && sheet.dismissible)) ? _jsx("div", { "aria-hidden": true, className: slots.grabber() }) : null, corner ? _jsx(CloseButton, { className: slots.closeCorner() }) : null, children] }));
389
423
  if (inline) {
390
424
  return (_jsx(SheetContext.Provider, { value: context, children: _jsx("div", { className: slots.popup({ className: ['[transform:none]', className] }), ...props, children: inside }) }));
391
425
  }
@@ -413,7 +447,7 @@ function SheetTitle({ className, ...props }) {
413
447
  const slots = sheetVariants();
414
448
  if (inline)
415
449
  return _jsx("h2", { className: slots.title({ className }), ...props });
416
- return _jsx(Drawer.Title, { className: slots.title({ className }), ...props });
450
+ return _jsx(Drawer.Content, { render: _jsx(Drawer.Title, { className: slots.title({ className }), ...props }) });
417
451
  }
418
452
  SheetTitle.displayName = 'Sheet.Title';
419
453
  /** A sentence under the title, read out after it. */
@@ -422,7 +456,7 @@ function SheetDescription({ className, ...props }) {
422
456
  const slots = sheetVariants();
423
457
  if (inline)
424
458
  return _jsx("p", { className: slots.description({ className }), ...props });
425
- return _jsx(Drawer.Description, { className: slots.description({ className }), ...props });
459
+ return (_jsx(Drawer.Content, { render: _jsx(Drawer.Description, { className: slots.description({ className }), ...props }) }));
426
460
  }
427
461
  SheetDescription.displayName = 'Sheet.Description';
428
462
  /**
@@ -430,26 +464,49 @@ SheetDescription.displayName = 'Sheet.Description';
430
464
  * the scrollbar is a thin drawn thumb that shows while the content is moving.
431
465
  */
432
466
  function SheetBody({ className, children, ...props }) {
467
+ const { inline } = useSheet('Sheet.Body');
433
468
  const slots = sheetVariants();
434
- return (_jsxs(ScrollArea.Root, { 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() }) })] }));
469
+ const content = (_jsx(ScrollArea.Content, { className: slots.bodyContent({ className }), ...props, children: children }));
470
+ return (_jsxs(ScrollArea.Root, { className: slots.body(), children: [_jsx(ScrollArea.Viewport, { className: slots.bodyViewport(), children: inline ? content : _jsx(Drawer.Content, { render: content }) }), _jsx(ScrollArea.Scrollbar, { orientation: "vertical", className: slots.scrollbar(), children: _jsx(ScrollArea.Thumb, { className: slots.thumb() }) })] }));
435
471
  }
436
472
  SheetBody.displayName = 'Sheet.Body';
437
473
  /** The actions, stacked full width at the bottom of the sheet. */
438
474
  function SheetFooter({ className, ...props }) {
475
+ const { inline } = useSheet('Sheet.Footer');
439
476
  const slots = sheetVariants();
440
- return _jsx("div", { className: slots.footer({ className }), ...props });
477
+ const footer = _jsx("div", { className: slots.footer({ className }), ...props });
478
+ return inline ? footer : _jsx(Drawer.Content, { render: footer });
441
479
  }
442
480
  SheetFooter.displayName = 'Sheet.Footer';
443
- /** Closes the sheet. Works even when `dismissible` is off. */
481
+ const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
482
+ /**
483
+ * Closes the sheet. Works even when `dismissible` is off. Written bare, it is
484
+ * the round close button, and the sheet stops drawing its own in the corner.
485
+ */
444
486
  function SheetClose({ render, icon = false, children, ...props }) {
487
+ const sheet = useSheet('Sheet.Close');
488
+ const bare = icon || (render === undefined && children === undefined);
489
+ const { claimClose } = sheet;
490
+ useIsomorphicLayoutEffect(() => (bare ? claimClose() : undefined), [bare, claimClose]);
491
+ if (bare)
492
+ return _jsx(CloseButton, { ...props });
493
+ if (sheet.inline) {
494
+ return (_jsx("button", { type: "button", "data-slot": "sheet-close", ...props, children: children }));
495
+ }
496
+ return (_jsx(Drawer.Close, { "data-slot": "sheet-close", render: render, ...props, children: children }));
497
+ }
498
+ SheetClose.displayName = 'Sheet.Close';
499
+ /** The round button with a cross in it. */
500
+ function CloseButton({ className, 'aria-label': label = 'Close', ...props }) {
445
501
  const { inline } = useSheet('Sheet.Close');
446
- const content = icon ? (_jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": true, className: "size-4", fill: "none", stroke: "currentColor", strokeWidth: 2.4, strokeLinecap: "round", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) })) : (children);
502
+ const slots = sheetVariants();
503
+ const glyph = (_jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": true, className: "size-4", fill: "none", stroke: "currentColor", strokeWidth: 2.4, strokeLinecap: "round", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) }));
504
+ const shared = { 'data-slot': 'sheet-close', 'aria-label': label, className: slots.close({ className }), ...props };
447
505
  if (inline) {
448
- return (_jsx("button", { type: "button", ...props, children: content }));
506
+ return (_jsx("button", { type: "button", ...shared, children: glyph }));
449
507
  }
450
- return (_jsx(Drawer.Close, { render: render, ...props, children: content }));
508
+ return _jsx(Drawer.Close, { ...shared, children: glyph });
451
509
  }
452
- SheetClose.displayName = 'Sheet.Close';
453
510
  const SlidesContext = createContext(null);
454
511
  /** The rounded frame of a slide: the slide itself in a deck, inside its row in a column. */
455
512
  const frameOf = (slide) => slide.querySelector('[data-slide-frame]') ?? slide;