@pk-design/react-tailwind 1.12.0 → 1.13.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.
@@ -33,27 +33,58 @@ const panelSizeMap = {
33
33
  '2xl': 'max-w-2xl',
34
34
  full: 'w-full',
35
35
  };
36
- const Drawer = ({ isOpen, onClose = () => { }, closeOnOutsideClick = true, align: alignProp = 'end', size: sizeProp = 'md', backdrop = true, title, description, children, footer, footerSticky = false, panelClass = '', contentClass = '', titleClass = '', descriptionClass = '', titleSticky = false, showBackButton = false, showCloseButton = true, dragHandle, }) => {
36
+ // How much shorter than the window the visual viewport must be before it's
37
+ // taken to be the on-screen keyboard rather than the URL bar (≈ 56px).
38
+ const KEYBOARD_THRESHOLD = 150;
39
+ const Drawer = ({ isOpen, onClose = () => { }, closeOnOutsideClick = true, align: alignProp = 'end', size: sizeProp = 'md', backdrop = true, title, description, children, footer, footerSticky = false, panelClass = '', contentClass = '', titleClass = '', descriptionClass = '', titleSticky = false, showBackButton = false, showCloseButton = true, dragHandle, phoneLayout = 'default', headerAction, }) => {
37
40
  const align = useBreakpoint.useResponsiveValue(alignProp);
38
41
  const size = useBreakpoint.useResponsiveValue(sizeProp);
42
+ const breakpoint = useBreakpoint.useBreakpoint();
43
+ const fullscreen = phoneLayout === 'fullscreen' && breakpoint === 'compact';
39
44
  const showTitleSection = Boolean(title || description || showBackButton || showCloseButton);
40
- const shouldShowDragHandle = dragHandle !== null && dragHandle !== void 0 ? dragHandle : align === 'bottom';
41
- const [viewportHeight, setViewportHeight] = React.useState(null);
45
+ const shouldShowDragHandle = !fullscreen && (dragHandle !== null && dragHandle !== void 0 ? dragHandle : align === 'bottom');
46
+ useCloseOnBack(fullscreen && isOpen, onClose);
47
+ // The visible area while the on-screen keyboard is open; null otherwise.
48
+ const [keyboardViewport, setKeyboardViewport] = React.useState(null);
42
49
  React.useEffect(() => {
43
50
  if (!isOpen) {
44
- setViewportHeight(null);
51
+ setKeyboardViewport(null);
45
52
  return;
46
53
  }
54
+ // Follow the visual viewport only while the on-screen keyboard takes space
55
+ // (it's then much shorter than the window), so the drawer and its footer
56
+ // stay above the keyboard. Otherwise it's pinned to the edges: tracking every resize and
57
+ // scroll in state re-rendered the drawer on each frame as the browser's
58
+ // URL bar moved, and its sticky header and footer jumped while scrolling.
59
+ let frame = 0;
60
+ const measure = () => {
61
+ frame = 0;
62
+ const vv = window.visualViewport;
63
+ if (!vv)
64
+ return;
65
+ const keyboardOpen = window.innerHeight - vv.height > KEYBOARD_THRESHOLD;
66
+ const top = Math.round(vv.offsetTop);
67
+ const height = Math.round(vv.height);
68
+ // The browser scrolls the visible area to the focused field, so the
69
+ // drawer follows its offset as well as its height. An unchanged value
70
+ // doesn't re-render.
71
+ setKeyboardViewport((current) => !keyboardOpen
72
+ ? null
73
+ : current && current.top === top && current.height === height
74
+ ? current
75
+ : { top, height });
76
+ };
47
77
  const updateHeight = () => {
48
- if (typeof window !== 'undefined' && window.visualViewport) {
49
- setViewportHeight(window.visualViewport.height);
50
- }
78
+ if (!frame)
79
+ frame = window.requestAnimationFrame(measure);
51
80
  };
52
- updateHeight();
81
+ measure();
53
82
  const vv = typeof window !== 'undefined' ? window.visualViewport : null;
54
83
  vv === null || vv === void 0 ? void 0 : vv.addEventListener('resize', updateHeight);
55
84
  vv === null || vv === void 0 ? void 0 : vv.addEventListener('scroll', updateHeight);
56
85
  return () => {
86
+ if (frame)
87
+ window.cancelAnimationFrame(frame);
57
88
  vv === null || vv === void 0 ? void 0 : vv.removeEventListener('resize', updateHeight);
58
89
  vv === null || vv === void 0 ? void 0 : vv.removeEventListener('scroll', updateHeight);
59
90
  };
@@ -63,9 +94,62 @@ const Drawer = ({ isOpen, onClose = () => { }, closeOnOutsideClick = true, align
63
94
  onClose === null || onClose === void 0 ? void 0 : onClose();
64
95
  }
65
96
  };
66
- return (jsxRuntime.jsxs(react.Dialog, { open: isOpen, as: "div", className: "relative z-50 focus:outline-none", onClose: handleDialogClose, transition: true, children: [backdrop && (jsxRuntime.jsx(react.DialogBackdrop, { className: "fixed inset-0 bg-black/40 backdrop-blur-xs transition-opacity duration-200" })), jsxRuntime.jsx("div", { className: "fixed left-0 top-0 w-full", style: { height: viewportHeight ? `${viewportHeight}px` : '100dvh' }, children: jsxRuntime.jsx("div", { className: buildClassname.buildClassName('flex', alignmentMap[align]), children: jsxRuntime.jsxs(react.DialogPanel, { transition: true, className: buildClassname.buildClassName('w-full max-w-full bg-white dark:bg-gray-800 shadow-xl flex flex-col min-w-0', 'overflow-y-auto overflow-x-hidden overscroll-contain touch-pan-y [-webkit-overflow-scrolling:touch] duration-200 ease-out data-[closed]:opacity-0', panelAlignmentMap[align], panelSizeMap[size], panelClass), children: [shouldShowDragHandle && (jsxRuntime.jsx("div", { className: "w-12 h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 mx-auto my-2.5 shrink-0 cursor-grab", "data-testid": "drawer-drag-handle" })), showTitleSection && (jsxRuntime.jsxs("div", { className: buildClassname.buildClassName('space-y-1 py-3 px-4 md:px-6 flex flex-col border-b border-[var(--ui-border)] shrink-0', titleSticky && 'sticky top-0 bg-white dark:bg-gray-800 z-10'), children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-w-0", children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2 min-w-0 flex-1", children: [showBackButton && (jsxRuntime.jsx(Button.Button, { iconOnly: true, "aria-label": "Back", variant: "plain", theme: "secondary", size: "xs", onClick: onClose, children: jsxRuntime.jsx(ChevronLeft.default, { className: "size-4" }) })), title && (jsxRuntime.jsx(react.DialogTitle, { as: "h3", className: buildClassname.buildClassName('text-lg font-semibold text-gray-900 dark:text-white truncate tracking-tight min-w-0', titleClass), children: title }))] }), showCloseButton && (jsxRuntime.jsx(Button.Button, { iconOnly: true, "aria-label": "Close drawer", variant: "plain", theme: "secondary", size: "xs", onClick: onClose, className: "rounded-full text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 shrink-0", children: jsxRuntime.jsx(X.default, { className: "size-4" }) }))] }), description && (jsxRuntime.jsx("p", { className: buildClassname.buildClassName('text-sm text-gray-500 dark:text-gray-400 font-normal truncate', descriptionClass), children: description }))] })), jsxRuntime.jsx("div", { className: buildClassname.buildClassName('flex-1 p-4 md:p-6 space-y-4 min-w-0', contentClass), children: children }), footer && (jsxRuntime.jsx("div", { className: buildClassname.buildClassName('p-4 border-t border-[var(--ui-border-muted)] bg-white dark:bg-gray-800 shrink-0 min-w-0', footerSticky && 'sticky bottom-0 z-10'), children: footer }))] }) }) })] }));
97
+ return (jsxRuntime.jsxs(react.Dialog, { open: isOpen, as: "div", className: "relative z-50 focus:outline-none", onClose: handleDialogClose, transition: true, children: [backdrop && (jsxRuntime.jsx(react.DialogBackdrop, { className: "fixed inset-0 bg-black/40 backdrop-blur-xs transition-opacity duration-200" })), jsxRuntime.jsx("div", { className: "fixed inset-0", style: keyboardViewport
98
+ ? {
99
+ top: `${keyboardViewport.top}px`,
100
+ height: `${keyboardViewport.height}px`,
101
+ bottom: 'auto',
102
+ }
103
+ : undefined, "data-testid": "drawer-viewport", children: jsxRuntime.jsx("div", { className: buildClassname.buildClassName('flex', fullscreen ? 'h-full' : alignmentMap[align]), children: jsxRuntime.jsxs(react.DialogPanel, { transition: true, className: buildClassname.buildClassName('w-full max-w-full bg-white dark:bg-gray-800 shadow-xl flex flex-col min-w-0', 'overflow-y-auto overflow-x-hidden overscroll-contain touch-pan-y [-webkit-overflow-scrolling:touch] duration-200 ease-out data-[closed]:opacity-0', ...(fullscreen
104
+ ? ['h-full data-[closed]:translate-y-4']
105
+ : [panelAlignmentMap[align], panelSizeMap[size], panelClass])), "data-layout": fullscreen ? 'fullscreen' : undefined, children: [shouldShowDragHandle && (jsxRuntime.jsx("div", { className: "w-12 h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 mx-auto my-2.5 shrink-0 cursor-grab", "data-testid": "drawer-drag-handle" })), fullscreen && (jsxRuntime.jsxs("div", { className: "sticky top-0 z-10 flex h-14 shrink-0 items-center gap-1 border-b border-[var(--ui-border)] bg-white pl-1 pr-3 pt-[env(safe-area-inset-top,0px)] box-content dark:bg-gray-800", children: [jsxRuntime.jsx(Button.Button, { iconOnly: true, "aria-label": "Close", variant: "plain", theme: "secondary", onClick: onClose, className: "shrink-0", children: jsxRuntime.jsx(X.default, { className: "size-5" }) }), title && (jsxRuntime.jsx(react.DialogTitle, { as: "h3", className: "min-w-0 flex-1 truncate text-base font-semibold text-gray-900 dark:text-white", children: title })), headerAction && jsxRuntime.jsx("div", { className: "ml-auto shrink-0", children: headerAction })] })), !fullscreen && showTitleSection && (jsxRuntime.jsxs("div", { className: buildClassname.buildClassName('space-y-1 py-3 px-4 md:px-6 flex flex-col border-b border-[var(--ui-border)] shrink-0', titleSticky && 'sticky top-0 bg-white dark:bg-gray-800 z-10'), children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-w-0", children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2 min-w-0 flex-1", children: [showBackButton && (jsxRuntime.jsx(Button.Button, { iconOnly: true, "aria-label": "Back", variant: "plain", theme: "secondary", size: "xs", onClick: onClose, children: jsxRuntime.jsx(ChevronLeft.default, { className: "size-4" }) })), title && (jsxRuntime.jsx(react.DialogTitle, { as: "h3", className: buildClassname.buildClassName('text-lg font-semibold text-gray-900 dark:text-white truncate tracking-tight min-w-0', titleClass), children: title }))] }), showCloseButton && (jsxRuntime.jsx(Button.Button, { iconOnly: true, "aria-label": "Close drawer", variant: "plain", theme: "secondary", size: "xs", onClick: onClose, className: "rounded-full text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 shrink-0", children: jsxRuntime.jsx(X.default, { className: "size-4" }) }))] }), description && (jsxRuntime.jsx("p", { className: buildClassname.buildClassName('text-sm text-gray-500 dark:text-gray-400 font-normal truncate', descriptionClass), children: description }))] })), jsxRuntime.jsxs("div", { className: buildClassname.buildClassName('flex-1 p-4 md:p-6 space-y-4 min-w-0', contentClass, fullscreen && 'pb-[calc(1rem+env(safe-area-inset-bottom,0px))]'), children: [fullscreen && description && (jsxRuntime.jsx("p", { className: buildClassname.buildClassName('text-sm text-gray-500 dark:text-gray-400', descriptionClass), children: description })), children] }), !fullscreen && footer && (jsxRuntime.jsx("div", { className: buildClassname.buildClassName('p-4 border-t border-[var(--ui-border-muted)] bg-white dark:bg-gray-800 shrink-0 min-w-0', footerSticky && 'sticky bottom-0 z-10'), children: footer }))] }) }) })] }));
67
106
  };
68
107
  Drawer.displayName = 'Drawer';
108
+ /**
109
+ * While `active`, the browser's back button (and Android's back gesture)
110
+ * closes the drawer instead of leaving the page: opening adds a history entry
111
+ * (keeping the current state, so routers see the same location), and back
112
+ * removes it. Closed any other way, the entry is removed too, but only if it's
113
+ * still the latest one, so a form that saves and then navigates isn't undone.
114
+ */
115
+ function useCloseOnBack(active, onClose) {
116
+ const onCloseRef = React.useRef(onClose);
117
+ React.useEffect(() => {
118
+ onCloseRef.current = onClose;
119
+ });
120
+ const token = React.useId();
121
+ React.useEffect(() => {
122
+ if (!active || typeof window === 'undefined')
123
+ return;
124
+ const pushEntry = () => { var _a; return window.history.pushState({ ...((_a = window.history.state) !== null && _a !== void 0 ? _a : {}), drawer: token }, ''); };
125
+ pushEntry();
126
+ let entryPresent = true;
127
+ let disposed = false;
128
+ const handlePop = () => {
129
+ var _a;
130
+ if (((_a = window.history.state) === null || _a === void 0 ? void 0 : _a.drawer) === token)
131
+ return;
132
+ entryPresent = false;
133
+ onCloseRef.current();
134
+ // onClose may step back within the drawer instead of closing it; if it's
135
+ // still open afterwards, take back its entry so the next back works too.
136
+ window.setTimeout(() => {
137
+ if (disposed)
138
+ return;
139
+ pushEntry();
140
+ entryPresent = true;
141
+ }, 0);
142
+ };
143
+ window.addEventListener('popstate', handlePop);
144
+ return () => {
145
+ var _a;
146
+ disposed = true;
147
+ window.removeEventListener('popstate', handlePop);
148
+ if (entryPresent && ((_a = window.history.state) === null || _a === void 0 ? void 0 : _a.drawer) === token)
149
+ window.history.back();
150
+ };
151
+ }, [active, token]);
152
+ }
69
153
 
70
154
  exports.Drawer = Drawer;
71
155
  //# sourceMappingURL=Drawer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Drawer.js","sources":["../../../../../src/components/Drawer/Drawer.tsx"],"sourcesContent":["import React from 'react'\nimport { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react'\nimport { buildClassName } from '../../utils/build-classname'\nimport { Button } from '../Button'\nimport ChevronLeftIcon from '../Icons/ChevronLeft.svg'\nimport XIcon from '../Icons/X.svg'\nimport { useResponsiveValue } from '../../hooks/use-breakpoint'\nimport type { AlignmentMap, DrawerProps, SizeMap } from './Drawer.types'\n\nconst alignmentMap: AlignmentMap = {\n center: 'items-center justify-center h-full',\n start: 'justify-start h-full',\n end: 'justify-end h-full',\n top: 'items-start justify-center w-full',\n bottom: 'items-end justify-center h-full',\n}\n\nconst panelAlignmentMap: AlignmentMap = {\n center: 'data-[closed]:scale-95 rounded-2xl',\n start: 'data-[closed]:-translate-x-1/2 rounded-r-2xl',\n end: 'data-[closed]:translate-x-1/2 rounded-l-2xl',\n top: 'data-[closed]:-translate-y-1/2 rounded-b-2xl',\n // Bottom sheets clear the home indicator on notched phones (0 elsewhere).\n bottom:\n 'data-[closed]:translate-y-full rounded-t-2xl md:rounded-t-3xl max-h-[88vh] max-h-[88dvh] pb-[env(safe-area-inset-bottom,0px)]',\n}\n\nconst panelSizeMap: SizeMap = {\n xs: 'max-w-xs',\n sm: 'max-w-sm',\n md: 'max-w-md',\n lg: 'max-w-lg',\n xl: 'max-w-xl',\n '2xl': 'max-w-2xl',\n full: 'w-full',\n}\n\nexport const Drawer: React.FC<DrawerProps> = ({\n isOpen,\n onClose = () => {},\n closeOnOutsideClick = true,\n align: alignProp = 'end',\n size: sizeProp = 'md',\n backdrop = true,\n title,\n description,\n children,\n footer,\n footerSticky = false,\n panelClass = '',\n contentClass = '',\n titleClass = '',\n descriptionClass = '',\n titleSticky = false,\n showBackButton = false,\n showCloseButton = true,\n dragHandle,\n}) => {\n const align = useResponsiveValue(alignProp)\n const size = useResponsiveValue(sizeProp)\n const showTitleSection = Boolean(title || description || showBackButton || showCloseButton)\n const shouldShowDragHandle = dragHandle ?? align === 'bottom'\n const [viewportHeight, setViewportHeight] = React.useState<number | null>(null)\n\n React.useEffect(() => {\n if (!isOpen) {\n setViewportHeight(null)\n return\n }\n\n const updateHeight = () => {\n if (typeof window !== 'undefined' && window.visualViewport) {\n setViewportHeight(window.visualViewport.height)\n }\n }\n\n updateHeight()\n const vv = typeof window !== 'undefined' ? window.visualViewport : null\n vv?.addEventListener('resize', updateHeight)\n vv?.addEventListener('scroll', updateHeight)\n\n return () => {\n vv?.removeEventListener('resize', updateHeight)\n vv?.removeEventListener('scroll', updateHeight)\n }\n }, [isOpen])\n\n const handleDialogClose = () => {\n if (closeOnOutsideClick) {\n onClose?.()\n }\n }\n\n return (\n <Dialog\n open={isOpen}\n as=\"div\"\n className=\"relative z-50 focus:outline-none\"\n onClose={handleDialogClose}\n transition\n >\n {backdrop && (\n <DialogBackdrop className=\"fixed inset-0 bg-black/40 backdrop-blur-xs transition-opacity duration-200\" />\n )}\n\n <div\n className=\"fixed left-0 top-0 w-full\"\n style={{ height: viewportHeight ? `${viewportHeight}px` : '100dvh' }}\n >\n <div className={buildClassName('flex', alignmentMap[align])}>\n <DialogPanel\n transition\n className={buildClassName(\n 'w-full max-w-full bg-white dark:bg-gray-800 shadow-xl flex flex-col min-w-0',\n 'overflow-y-auto overflow-x-hidden overscroll-contain touch-pan-y [-webkit-overflow-scrolling:touch] duration-200 ease-out data-[closed]:opacity-0',\n panelAlignmentMap[align],\n panelSizeMap[size],\n panelClass,\n )}\n >\n {/* Top drag handle indicator for mobile bottom sheet */}\n {shouldShowDragHandle && (\n <div\n className=\"w-12 h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 mx-auto my-2.5 shrink-0 cursor-grab\"\n data-testid=\"drawer-drag-handle\"\n />\n )}\n\n {/* Header Section */}\n {showTitleSection && (\n <div\n className={buildClassName(\n 'space-y-1 py-3 px-4 md:px-6 flex flex-col border-b border-[var(--ui-border)] shrink-0',\n titleSticky && 'sticky top-0 bg-white dark:bg-gray-800 z-10',\n )}\n >\n <div className=\"flex items-center justify-between gap-3 min-w-0\">\n <div className=\"flex items-center gap-2 min-w-0 flex-1\">\n {showBackButton && (\n <Button\n iconOnly\n aria-label=\"Back\"\n variant=\"plain\"\n theme=\"secondary\"\n size=\"xs\"\n onClick={onClose}\n >\n <ChevronLeftIcon className=\"size-4\" />\n </Button>\n )}\n {title && (\n <DialogTitle\n as=\"h3\"\n className={buildClassName(\n 'text-lg font-semibold text-gray-900 dark:text-white truncate tracking-tight min-w-0',\n titleClass,\n )}\n >\n {title}\n </DialogTitle>\n )}\n </div>\n\n {showCloseButton && (\n <Button\n iconOnly\n aria-label=\"Close drawer\"\n variant=\"plain\"\n theme=\"secondary\"\n size=\"xs\"\n onClick={onClose}\n className=\"rounded-full text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 shrink-0\"\n >\n <XIcon className=\"size-4\" />\n </Button>\n )}\n </div>\n\n {description && (\n <p\n className={buildClassName(\n 'text-sm text-gray-500 dark:text-gray-400 font-normal truncate',\n descriptionClass,\n )}\n >\n {description}\n </p>\n )}\n </div>\n )}\n\n {/* Content Body */}\n <div className={buildClassName('flex-1 p-4 md:p-6 space-y-4 min-w-0', contentClass)}>\n {children}\n </div>\n\n {/* Footer Section */}\n {footer && (\n <div\n className={buildClassName(\n 'p-4 border-t border-[var(--ui-border-muted)] bg-white dark:bg-gray-800 shrink-0 min-w-0',\n footerSticky && 'sticky bottom-0 z-10',\n )}\n >\n {footer}\n </div>\n )}\n </DialogPanel>\n </div>\n </div>\n </Dialog>\n )\n}\n\nDrawer.displayName = 'Drawer'\n"],"names":["useResponsiveValue","_jsxs","Dialog","_jsx","DialogBackdrop","buildClassName","DialogPanel","Button","ChevronLeftIcon","DialogTitle","XIcon"],"mappings":"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"}
1
+ {"version":3,"file":"Drawer.js","sources":["../../../../../src/components/Drawer/Drawer.tsx"],"sourcesContent":["import React from 'react'\nimport { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react'\nimport { buildClassName } from '../../utils/build-classname'\nimport { Button } from '../Button'\nimport ChevronLeftIcon from '../Icons/ChevronLeft.svg'\nimport XIcon from '../Icons/X.svg'\nimport { useBreakpoint, useResponsiveValue } from '../../hooks/use-breakpoint'\nimport type { AlignmentMap, DrawerProps, SizeMap } from './Drawer.types'\n\nconst alignmentMap: AlignmentMap = {\n center: 'items-center justify-center h-full',\n start: 'justify-start h-full',\n end: 'justify-end h-full',\n top: 'items-start justify-center w-full',\n bottom: 'items-end justify-center h-full',\n}\n\nconst panelAlignmentMap: AlignmentMap = {\n center: 'data-[closed]:scale-95 rounded-2xl',\n start: 'data-[closed]:-translate-x-1/2 rounded-r-2xl',\n end: 'data-[closed]:translate-x-1/2 rounded-l-2xl',\n top: 'data-[closed]:-translate-y-1/2 rounded-b-2xl',\n // Bottom sheets clear the home indicator on notched phones (0 elsewhere).\n bottom:\n 'data-[closed]:translate-y-full rounded-t-2xl md:rounded-t-3xl max-h-[88vh] max-h-[88dvh] pb-[env(safe-area-inset-bottom,0px)]',\n}\n\nconst panelSizeMap: SizeMap = {\n xs: 'max-w-xs',\n sm: 'max-w-sm',\n md: 'max-w-md',\n lg: 'max-w-lg',\n xl: 'max-w-xl',\n '2xl': 'max-w-2xl',\n full: 'w-full',\n}\n\n// How much shorter than the window the visual viewport must be before it's\n// taken to be the on-screen keyboard rather than the URL bar (≈ 56px).\nconst KEYBOARD_THRESHOLD = 150\nexport const Drawer: React.FC<DrawerProps> = ({\n isOpen,\n onClose = () => {},\n closeOnOutsideClick = true,\n align: alignProp = 'end',\n size: sizeProp = 'md',\n backdrop = true,\n title,\n description,\n children,\n footer,\n footerSticky = false,\n panelClass = '',\n contentClass = '',\n titleClass = '',\n descriptionClass = '',\n titleSticky = false,\n showBackButton = false,\n showCloseButton = true,\n dragHandle,\n phoneLayout = 'default',\n headerAction,\n}) => {\n const align = useResponsiveValue(alignProp)\n const size = useResponsiveValue(sizeProp)\n const breakpoint = useBreakpoint()\n const fullscreen = phoneLayout === 'fullscreen' && breakpoint === 'compact'\n const showTitleSection = Boolean(title || description || showBackButton || showCloseButton)\n const shouldShowDragHandle = !fullscreen && (dragHandle ?? align === 'bottom')\n useCloseOnBack(fullscreen && isOpen, onClose)\n // The visible area while the on-screen keyboard is open; null otherwise.\n const [keyboardViewport, setKeyboardViewport] = React.useState<{\n top: number\n height: number\n } | null>(null)\n\n React.useEffect(() => {\n if (!isOpen) {\n setKeyboardViewport(null)\n return\n }\n\n // Follow the visual viewport only while the on-screen keyboard takes space\n // (it's then much shorter than the window), so the drawer and its footer\n // stay above the keyboard. Otherwise it's pinned to the edges: tracking every resize and\n // scroll in state re-rendered the drawer on each frame as the browser's\n // URL bar moved, and its sticky header and footer jumped while scrolling.\n let frame = 0\n const measure = () => {\n frame = 0\n const vv = window.visualViewport\n if (!vv) return\n const keyboardOpen = window.innerHeight - vv.height > KEYBOARD_THRESHOLD\n const top = Math.round(vv.offsetTop)\n const height = Math.round(vv.height)\n // The browser scrolls the visible area to the focused field, so the\n // drawer follows its offset as well as its height. An unchanged value\n // doesn't re-render.\n setKeyboardViewport((current) =>\n !keyboardOpen\n ? null\n : current && current.top === top && current.height === height\n ? current\n : { top, height },\n )\n }\n const updateHeight = () => {\n if (!frame) frame = window.requestAnimationFrame(measure)\n }\n\n measure()\n const vv = typeof window !== 'undefined' ? window.visualViewport : null\n vv?.addEventListener('resize', updateHeight)\n vv?.addEventListener('scroll', updateHeight)\n\n return () => {\n if (frame) window.cancelAnimationFrame(frame)\n vv?.removeEventListener('resize', updateHeight)\n vv?.removeEventListener('scroll', updateHeight)\n }\n }, [isOpen])\n\n const handleDialogClose = () => {\n if (closeOnOutsideClick) {\n onClose?.()\n }\n }\n\n return (\n <Dialog\n open={isOpen}\n as=\"div\"\n className=\"relative z-50 focus:outline-none\"\n onClose={handleDialogClose}\n transition\n >\n {backdrop && (\n <DialogBackdrop className=\"fixed inset-0 bg-black/40 backdrop-blur-xs transition-opacity duration-200\" />\n )}\n\n {/* Pinned to all four edges rather than given a height: mobile browsers\n move fixed edges with the URL bar on every frame, while 100dvh only\n updates once its animation ends (the drawer briefly came up short).\n With the keyboard open it takes the visible area's offset and height. */}\n <div\n className=\"fixed inset-0\"\n style={\n keyboardViewport\n ? {\n top: `${keyboardViewport.top}px`,\n height: `${keyboardViewport.height}px`,\n bottom: 'auto',\n }\n : undefined\n }\n data-testid=\"drawer-viewport\"\n >\n <div className={buildClassName('flex', fullscreen ? 'h-full' : alignmentMap[align])}>\n <DialogPanel\n transition\n className={buildClassName(\n 'w-full max-w-full bg-white dark:bg-gray-800 shadow-xl flex flex-col min-w-0',\n 'overflow-y-auto overflow-x-hidden overscroll-contain touch-pan-y [-webkit-overflow-scrolling:touch] duration-200 ease-out data-[closed]:opacity-0',\n ...(fullscreen\n ? ['h-full data-[closed]:translate-y-4']\n : [panelAlignmentMap[align], panelSizeMap[size], panelClass]),\n )}\n data-layout={fullscreen ? 'fullscreen' : undefined}\n >\n {/* Top drag handle indicator for mobile bottom sheet */}\n {shouldShowDragHandle && (\n <div\n className=\"w-12 h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 mx-auto my-2.5 shrink-0 cursor-grab\"\n data-testid=\"drawer-drag-handle\"\n />\n )}\n\n {/* Phone full-screen bar: the only fixed part, so the fields keep the room. */}\n {fullscreen && (\n <div className=\"sticky top-0 z-10 flex h-14 shrink-0 items-center gap-1 border-b border-[var(--ui-border)] bg-white pl-1 pr-3 pt-[env(safe-area-inset-top,0px)] box-content dark:bg-gray-800\">\n <Button\n iconOnly\n aria-label=\"Close\"\n variant=\"plain\"\n theme=\"secondary\"\n onClick={onClose}\n className=\"shrink-0\"\n >\n <XIcon className=\"size-5\" />\n </Button>\n {title && (\n <DialogTitle\n as=\"h3\"\n className=\"min-w-0 flex-1 truncate text-base font-semibold text-gray-900 dark:text-white\"\n >\n {title}\n </DialogTitle>\n )}\n {headerAction && <div className=\"ml-auto shrink-0\">{headerAction}</div>}\n </div>\n )}\n\n {/* Header Section */}\n {!fullscreen && showTitleSection && (\n <div\n className={buildClassName(\n 'space-y-1 py-3 px-4 md:px-6 flex flex-col border-b border-[var(--ui-border)] shrink-0',\n titleSticky && 'sticky top-0 bg-white dark:bg-gray-800 z-10',\n )}\n >\n <div className=\"flex items-center justify-between gap-3 min-w-0\">\n <div className=\"flex items-center gap-2 min-w-0 flex-1\">\n {showBackButton && (\n <Button\n iconOnly\n aria-label=\"Back\"\n variant=\"plain\"\n theme=\"secondary\"\n size=\"xs\"\n onClick={onClose}\n >\n <ChevronLeftIcon className=\"size-4\" />\n </Button>\n )}\n {title && (\n <DialogTitle\n as=\"h3\"\n className={buildClassName(\n 'text-lg font-semibold text-gray-900 dark:text-white truncate tracking-tight min-w-0',\n titleClass,\n )}\n >\n {title}\n </DialogTitle>\n )}\n </div>\n\n {showCloseButton && (\n <Button\n iconOnly\n aria-label=\"Close drawer\"\n variant=\"plain\"\n theme=\"secondary\"\n size=\"xs\"\n onClick={onClose}\n className=\"rounded-full text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 shrink-0\"\n >\n <XIcon className=\"size-4\" />\n </Button>\n )}\n </div>\n\n {description && (\n <p\n className={buildClassName(\n 'text-sm text-gray-500 dark:text-gray-400 font-normal truncate',\n descriptionClass,\n )}\n >\n {description}\n </p>\n )}\n </div>\n )}\n\n {/* Content Body */}\n <div\n className={buildClassName(\n 'flex-1 p-4 md:p-6 space-y-4 min-w-0',\n contentClass,\n fullscreen && 'pb-[calc(1rem+env(safe-area-inset-bottom,0px))]',\n )}\n >\n {fullscreen && description && (\n <p\n className={buildClassName(\n 'text-sm text-gray-500 dark:text-gray-400',\n descriptionClass,\n )}\n >\n {description}\n </p>\n )}\n {children}\n </div>\n\n {/* Footer Section (the phone full-screen bar carries the action instead) */}\n {!fullscreen && footer && (\n <div\n className={buildClassName(\n 'p-4 border-t border-[var(--ui-border-muted)] bg-white dark:bg-gray-800 shrink-0 min-w-0',\n footerSticky && 'sticky bottom-0 z-10',\n )}\n >\n {footer}\n </div>\n )}\n </DialogPanel>\n </div>\n </div>\n </Dialog>\n )\n}\n\nDrawer.displayName = 'Drawer'\n\n/**\n * While `active`, the browser's back button (and Android's back gesture)\n * closes the drawer instead of leaving the page: opening adds a history entry\n * (keeping the current state, so routers see the same location), and back\n * removes it. Closed any other way, the entry is removed too, but only if it's\n * still the latest one, so a form that saves and then navigates isn't undone.\n */\nfunction useCloseOnBack(active: boolean, onClose: () => void) {\n const onCloseRef = React.useRef(onClose)\n React.useEffect(() => {\n onCloseRef.current = onClose\n })\n const token = React.useId()\n\n React.useEffect(() => {\n if (!active || typeof window === 'undefined') return\n const pushEntry = () =>\n window.history.pushState({ ...(window.history.state ?? {}), drawer: token }, '')\n pushEntry()\n let entryPresent = true\n let disposed = false\n const handlePop = () => {\n if (window.history.state?.drawer === token) return\n entryPresent = false\n onCloseRef.current()\n // onClose may step back within the drawer instead of closing it; if it's\n // still open afterwards, take back its entry so the next back works too.\n window.setTimeout(() => {\n if (disposed) return\n pushEntry()\n entryPresent = true\n }, 0)\n }\n window.addEventListener('popstate', handlePop)\n return () => {\n disposed = true\n window.removeEventListener('popstate', handlePop)\n if (entryPresent && window.history.state?.drawer === token) window.history.back()\n }\n }, [active, token])\n}\n"],"names":["useResponsiveValue","useBreakpoint","_jsxs","Dialog","_jsx","DialogBackdrop","buildClassName","DialogPanel","Button","XIcon","DialogTitle","ChevronLeftIcon"],"mappings":"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"}
@@ -5,7 +5,7 @@ import { buildClassName } from '../../utils/build-classname.js';
5
5
  import { Button } from '../Button/Button.js';
6
6
  import SvgChevronLeft from '../Icons/ChevronLeft.svg.js';
7
7
  import SvgX from '../Icons/X.svg.js';
8
- import { useResponsiveValue } from '../../hooks/use-breakpoint.js';
8
+ import { useResponsiveValue, useBreakpoint } from '../../hooks/use-breakpoint.js';
9
9
 
10
10
  const alignmentMap = {
11
11
  center: 'items-center justify-center h-full',
@@ -31,27 +31,58 @@ const panelSizeMap = {
31
31
  '2xl': 'max-w-2xl',
32
32
  full: 'w-full',
33
33
  };
34
- const Drawer = ({ isOpen, onClose = () => { }, closeOnOutsideClick = true, align: alignProp = 'end', size: sizeProp = 'md', backdrop = true, title, description, children, footer, footerSticky = false, panelClass = '', contentClass = '', titleClass = '', descriptionClass = '', titleSticky = false, showBackButton = false, showCloseButton = true, dragHandle, }) => {
34
+ // How much shorter than the window the visual viewport must be before it's
35
+ // taken to be the on-screen keyboard rather than the URL bar (≈ 56px).
36
+ const KEYBOARD_THRESHOLD = 150;
37
+ const Drawer = ({ isOpen, onClose = () => { }, closeOnOutsideClick = true, align: alignProp = 'end', size: sizeProp = 'md', backdrop = true, title, description, children, footer, footerSticky = false, panelClass = '', contentClass = '', titleClass = '', descriptionClass = '', titleSticky = false, showBackButton = false, showCloseButton = true, dragHandle, phoneLayout = 'default', headerAction, }) => {
35
38
  const align = useResponsiveValue(alignProp);
36
39
  const size = useResponsiveValue(sizeProp);
40
+ const breakpoint = useBreakpoint();
41
+ const fullscreen = phoneLayout === 'fullscreen' && breakpoint === 'compact';
37
42
  const showTitleSection = Boolean(title || description || showBackButton || showCloseButton);
38
- const shouldShowDragHandle = dragHandle !== null && dragHandle !== void 0 ? dragHandle : align === 'bottom';
39
- const [viewportHeight, setViewportHeight] = React__default.useState(null);
43
+ const shouldShowDragHandle = !fullscreen && (dragHandle !== null && dragHandle !== void 0 ? dragHandle : align === 'bottom');
44
+ useCloseOnBack(fullscreen && isOpen, onClose);
45
+ // The visible area while the on-screen keyboard is open; null otherwise.
46
+ const [keyboardViewport, setKeyboardViewport] = React__default.useState(null);
40
47
  React__default.useEffect(() => {
41
48
  if (!isOpen) {
42
- setViewportHeight(null);
49
+ setKeyboardViewport(null);
43
50
  return;
44
51
  }
52
+ // Follow the visual viewport only while the on-screen keyboard takes space
53
+ // (it's then much shorter than the window), so the drawer and its footer
54
+ // stay above the keyboard. Otherwise it's pinned to the edges: tracking every resize and
55
+ // scroll in state re-rendered the drawer on each frame as the browser's
56
+ // URL bar moved, and its sticky header and footer jumped while scrolling.
57
+ let frame = 0;
58
+ const measure = () => {
59
+ frame = 0;
60
+ const vv = window.visualViewport;
61
+ if (!vv)
62
+ return;
63
+ const keyboardOpen = window.innerHeight - vv.height > KEYBOARD_THRESHOLD;
64
+ const top = Math.round(vv.offsetTop);
65
+ const height = Math.round(vv.height);
66
+ // The browser scrolls the visible area to the focused field, so the
67
+ // drawer follows its offset as well as its height. An unchanged value
68
+ // doesn't re-render.
69
+ setKeyboardViewport((current) => !keyboardOpen
70
+ ? null
71
+ : current && current.top === top && current.height === height
72
+ ? current
73
+ : { top, height });
74
+ };
45
75
  const updateHeight = () => {
46
- if (typeof window !== 'undefined' && window.visualViewport) {
47
- setViewportHeight(window.visualViewport.height);
48
- }
76
+ if (!frame)
77
+ frame = window.requestAnimationFrame(measure);
49
78
  };
50
- updateHeight();
79
+ measure();
51
80
  const vv = typeof window !== 'undefined' ? window.visualViewport : null;
52
81
  vv === null || vv === void 0 ? void 0 : vv.addEventListener('resize', updateHeight);
53
82
  vv === null || vv === void 0 ? void 0 : vv.addEventListener('scroll', updateHeight);
54
83
  return () => {
84
+ if (frame)
85
+ window.cancelAnimationFrame(frame);
55
86
  vv === null || vv === void 0 ? void 0 : vv.removeEventListener('resize', updateHeight);
56
87
  vv === null || vv === void 0 ? void 0 : vv.removeEventListener('scroll', updateHeight);
57
88
  };
@@ -61,9 +92,62 @@ const Drawer = ({ isOpen, onClose = () => { }, closeOnOutsideClick = true, align
61
92
  onClose === null || onClose === void 0 ? void 0 : onClose();
62
93
  }
63
94
  };
64
- return (jsxs(Dialog, { open: isOpen, as: "div", className: "relative z-50 focus:outline-none", onClose: handleDialogClose, transition: true, children: [backdrop && (jsx(DialogBackdrop, { className: "fixed inset-0 bg-black/40 backdrop-blur-xs transition-opacity duration-200" })), jsx("div", { className: "fixed left-0 top-0 w-full", style: { height: viewportHeight ? `${viewportHeight}px` : '100dvh' }, children: jsx("div", { className: buildClassName('flex', alignmentMap[align]), children: jsxs(DialogPanel, { transition: true, className: buildClassName('w-full max-w-full bg-white dark:bg-gray-800 shadow-xl flex flex-col min-w-0', 'overflow-y-auto overflow-x-hidden overscroll-contain touch-pan-y [-webkit-overflow-scrolling:touch] duration-200 ease-out data-[closed]:opacity-0', panelAlignmentMap[align], panelSizeMap[size], panelClass), children: [shouldShowDragHandle && (jsx("div", { className: "w-12 h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 mx-auto my-2.5 shrink-0 cursor-grab", "data-testid": "drawer-drag-handle" })), showTitleSection && (jsxs("div", { className: buildClassName('space-y-1 py-3 px-4 md:px-6 flex flex-col border-b border-[var(--ui-border)] shrink-0', titleSticky && 'sticky top-0 bg-white dark:bg-gray-800 z-10'), children: [jsxs("div", { className: "flex items-center justify-between gap-3 min-w-0", children: [jsxs("div", { className: "flex items-center gap-2 min-w-0 flex-1", children: [showBackButton && (jsx(Button, { iconOnly: true, "aria-label": "Back", variant: "plain", theme: "secondary", size: "xs", onClick: onClose, children: jsx(SvgChevronLeft, { className: "size-4" }) })), title && (jsx(DialogTitle, { as: "h3", className: buildClassName('text-lg font-semibold text-gray-900 dark:text-white truncate tracking-tight min-w-0', titleClass), children: title }))] }), showCloseButton && (jsx(Button, { iconOnly: true, "aria-label": "Close drawer", variant: "plain", theme: "secondary", size: "xs", onClick: onClose, className: "rounded-full text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 shrink-0", children: jsx(SvgX, { className: "size-4" }) }))] }), description && (jsx("p", { className: buildClassName('text-sm text-gray-500 dark:text-gray-400 font-normal truncate', descriptionClass), children: description }))] })), jsx("div", { className: buildClassName('flex-1 p-4 md:p-6 space-y-4 min-w-0', contentClass), children: children }), footer && (jsx("div", { className: buildClassName('p-4 border-t border-[var(--ui-border-muted)] bg-white dark:bg-gray-800 shrink-0 min-w-0', footerSticky && 'sticky bottom-0 z-10'), children: footer }))] }) }) })] }));
95
+ return (jsxs(Dialog, { open: isOpen, as: "div", className: "relative z-50 focus:outline-none", onClose: handleDialogClose, transition: true, children: [backdrop && (jsx(DialogBackdrop, { className: "fixed inset-0 bg-black/40 backdrop-blur-xs transition-opacity duration-200" })), jsx("div", { className: "fixed inset-0", style: keyboardViewport
96
+ ? {
97
+ top: `${keyboardViewport.top}px`,
98
+ height: `${keyboardViewport.height}px`,
99
+ bottom: 'auto',
100
+ }
101
+ : undefined, "data-testid": "drawer-viewport", children: jsx("div", { className: buildClassName('flex', fullscreen ? 'h-full' : alignmentMap[align]), children: jsxs(DialogPanel, { transition: true, className: buildClassName('w-full max-w-full bg-white dark:bg-gray-800 shadow-xl flex flex-col min-w-0', 'overflow-y-auto overflow-x-hidden overscroll-contain touch-pan-y [-webkit-overflow-scrolling:touch] duration-200 ease-out data-[closed]:opacity-0', ...(fullscreen
102
+ ? ['h-full data-[closed]:translate-y-4']
103
+ : [panelAlignmentMap[align], panelSizeMap[size], panelClass])), "data-layout": fullscreen ? 'fullscreen' : undefined, children: [shouldShowDragHandle && (jsx("div", { className: "w-12 h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 mx-auto my-2.5 shrink-0 cursor-grab", "data-testid": "drawer-drag-handle" })), fullscreen && (jsxs("div", { className: "sticky top-0 z-10 flex h-14 shrink-0 items-center gap-1 border-b border-[var(--ui-border)] bg-white pl-1 pr-3 pt-[env(safe-area-inset-top,0px)] box-content dark:bg-gray-800", children: [jsx(Button, { iconOnly: true, "aria-label": "Close", variant: "plain", theme: "secondary", onClick: onClose, className: "shrink-0", children: jsx(SvgX, { className: "size-5" }) }), title && (jsx(DialogTitle, { as: "h3", className: "min-w-0 flex-1 truncate text-base font-semibold text-gray-900 dark:text-white", children: title })), headerAction && jsx("div", { className: "ml-auto shrink-0", children: headerAction })] })), !fullscreen && showTitleSection && (jsxs("div", { className: buildClassName('space-y-1 py-3 px-4 md:px-6 flex flex-col border-b border-[var(--ui-border)] shrink-0', titleSticky && 'sticky top-0 bg-white dark:bg-gray-800 z-10'), children: [jsxs("div", { className: "flex items-center justify-between gap-3 min-w-0", children: [jsxs("div", { className: "flex items-center gap-2 min-w-0 flex-1", children: [showBackButton && (jsx(Button, { iconOnly: true, "aria-label": "Back", variant: "plain", theme: "secondary", size: "xs", onClick: onClose, children: jsx(SvgChevronLeft, { className: "size-4" }) })), title && (jsx(DialogTitle, { as: "h3", className: buildClassName('text-lg font-semibold text-gray-900 dark:text-white truncate tracking-tight min-w-0', titleClass), children: title }))] }), showCloseButton && (jsx(Button, { iconOnly: true, "aria-label": "Close drawer", variant: "plain", theme: "secondary", size: "xs", onClick: onClose, className: "rounded-full text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 shrink-0", children: jsx(SvgX, { className: "size-4" }) }))] }), description && (jsx("p", { className: buildClassName('text-sm text-gray-500 dark:text-gray-400 font-normal truncate', descriptionClass), children: description }))] })), jsxs("div", { className: buildClassName('flex-1 p-4 md:p-6 space-y-4 min-w-0', contentClass, fullscreen && 'pb-[calc(1rem+env(safe-area-inset-bottom,0px))]'), children: [fullscreen && description && (jsx("p", { className: buildClassName('text-sm text-gray-500 dark:text-gray-400', descriptionClass), children: description })), children] }), !fullscreen && footer && (jsx("div", { className: buildClassName('p-4 border-t border-[var(--ui-border-muted)] bg-white dark:bg-gray-800 shrink-0 min-w-0', footerSticky && 'sticky bottom-0 z-10'), children: footer }))] }) }) })] }));
65
104
  };
66
105
  Drawer.displayName = 'Drawer';
106
+ /**
107
+ * While `active`, the browser's back button (and Android's back gesture)
108
+ * closes the drawer instead of leaving the page: opening adds a history entry
109
+ * (keeping the current state, so routers see the same location), and back
110
+ * removes it. Closed any other way, the entry is removed too, but only if it's
111
+ * still the latest one, so a form that saves and then navigates isn't undone.
112
+ */
113
+ function useCloseOnBack(active, onClose) {
114
+ const onCloseRef = React__default.useRef(onClose);
115
+ React__default.useEffect(() => {
116
+ onCloseRef.current = onClose;
117
+ });
118
+ const token = React__default.useId();
119
+ React__default.useEffect(() => {
120
+ if (!active || typeof window === 'undefined')
121
+ return;
122
+ const pushEntry = () => { var _a; return window.history.pushState({ ...((_a = window.history.state) !== null && _a !== void 0 ? _a : {}), drawer: token }, ''); };
123
+ pushEntry();
124
+ let entryPresent = true;
125
+ let disposed = false;
126
+ const handlePop = () => {
127
+ var _a;
128
+ if (((_a = window.history.state) === null || _a === void 0 ? void 0 : _a.drawer) === token)
129
+ return;
130
+ entryPresent = false;
131
+ onCloseRef.current();
132
+ // onClose may step back within the drawer instead of closing it; if it's
133
+ // still open afterwards, take back its entry so the next back works too.
134
+ window.setTimeout(() => {
135
+ if (disposed)
136
+ return;
137
+ pushEntry();
138
+ entryPresent = true;
139
+ }, 0);
140
+ };
141
+ window.addEventListener('popstate', handlePop);
142
+ return () => {
143
+ var _a;
144
+ disposed = true;
145
+ window.removeEventListener('popstate', handlePop);
146
+ if (entryPresent && ((_a = window.history.state) === null || _a === void 0 ? void 0 : _a.drawer) === token)
147
+ window.history.back();
148
+ };
149
+ }, [active, token]);
150
+ }
67
151
 
68
152
  export { Drawer };
69
153
  //# sourceMappingURL=Drawer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Drawer.js","sources":["../../../../../src/components/Drawer/Drawer.tsx"],"sourcesContent":["import React from 'react'\nimport { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react'\nimport { buildClassName } from '../../utils/build-classname'\nimport { Button } from '../Button'\nimport ChevronLeftIcon from '../Icons/ChevronLeft.svg'\nimport XIcon from '../Icons/X.svg'\nimport { useResponsiveValue } from '../../hooks/use-breakpoint'\nimport type { AlignmentMap, DrawerProps, SizeMap } from './Drawer.types'\n\nconst alignmentMap: AlignmentMap = {\n center: 'items-center justify-center h-full',\n start: 'justify-start h-full',\n end: 'justify-end h-full',\n top: 'items-start justify-center w-full',\n bottom: 'items-end justify-center h-full',\n}\n\nconst panelAlignmentMap: AlignmentMap = {\n center: 'data-[closed]:scale-95 rounded-2xl',\n start: 'data-[closed]:-translate-x-1/2 rounded-r-2xl',\n end: 'data-[closed]:translate-x-1/2 rounded-l-2xl',\n top: 'data-[closed]:-translate-y-1/2 rounded-b-2xl',\n // Bottom sheets clear the home indicator on notched phones (0 elsewhere).\n bottom:\n 'data-[closed]:translate-y-full rounded-t-2xl md:rounded-t-3xl max-h-[88vh] max-h-[88dvh] pb-[env(safe-area-inset-bottom,0px)]',\n}\n\nconst panelSizeMap: SizeMap = {\n xs: 'max-w-xs',\n sm: 'max-w-sm',\n md: 'max-w-md',\n lg: 'max-w-lg',\n xl: 'max-w-xl',\n '2xl': 'max-w-2xl',\n full: 'w-full',\n}\n\nexport const Drawer: React.FC<DrawerProps> = ({\n isOpen,\n onClose = () => {},\n closeOnOutsideClick = true,\n align: alignProp = 'end',\n size: sizeProp = 'md',\n backdrop = true,\n title,\n description,\n children,\n footer,\n footerSticky = false,\n panelClass = '',\n contentClass = '',\n titleClass = '',\n descriptionClass = '',\n titleSticky = false,\n showBackButton = false,\n showCloseButton = true,\n dragHandle,\n}) => {\n const align = useResponsiveValue(alignProp)\n const size = useResponsiveValue(sizeProp)\n const showTitleSection = Boolean(title || description || showBackButton || showCloseButton)\n const shouldShowDragHandle = dragHandle ?? align === 'bottom'\n const [viewportHeight, setViewportHeight] = React.useState<number | null>(null)\n\n React.useEffect(() => {\n if (!isOpen) {\n setViewportHeight(null)\n return\n }\n\n const updateHeight = () => {\n if (typeof window !== 'undefined' && window.visualViewport) {\n setViewportHeight(window.visualViewport.height)\n }\n }\n\n updateHeight()\n const vv = typeof window !== 'undefined' ? window.visualViewport : null\n vv?.addEventListener('resize', updateHeight)\n vv?.addEventListener('scroll', updateHeight)\n\n return () => {\n vv?.removeEventListener('resize', updateHeight)\n vv?.removeEventListener('scroll', updateHeight)\n }\n }, [isOpen])\n\n const handleDialogClose = () => {\n if (closeOnOutsideClick) {\n onClose?.()\n }\n }\n\n return (\n <Dialog\n open={isOpen}\n as=\"div\"\n className=\"relative z-50 focus:outline-none\"\n onClose={handleDialogClose}\n transition\n >\n {backdrop && (\n <DialogBackdrop className=\"fixed inset-0 bg-black/40 backdrop-blur-xs transition-opacity duration-200\" />\n )}\n\n <div\n className=\"fixed left-0 top-0 w-full\"\n style={{ height: viewportHeight ? `${viewportHeight}px` : '100dvh' }}\n >\n <div className={buildClassName('flex', alignmentMap[align])}>\n <DialogPanel\n transition\n className={buildClassName(\n 'w-full max-w-full bg-white dark:bg-gray-800 shadow-xl flex flex-col min-w-0',\n 'overflow-y-auto overflow-x-hidden overscroll-contain touch-pan-y [-webkit-overflow-scrolling:touch] duration-200 ease-out data-[closed]:opacity-0',\n panelAlignmentMap[align],\n panelSizeMap[size],\n panelClass,\n )}\n >\n {/* Top drag handle indicator for mobile bottom sheet */}\n {shouldShowDragHandle && (\n <div\n className=\"w-12 h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 mx-auto my-2.5 shrink-0 cursor-grab\"\n data-testid=\"drawer-drag-handle\"\n />\n )}\n\n {/* Header Section */}\n {showTitleSection && (\n <div\n className={buildClassName(\n 'space-y-1 py-3 px-4 md:px-6 flex flex-col border-b border-[var(--ui-border)] shrink-0',\n titleSticky && 'sticky top-0 bg-white dark:bg-gray-800 z-10',\n )}\n >\n <div className=\"flex items-center justify-between gap-3 min-w-0\">\n <div className=\"flex items-center gap-2 min-w-0 flex-1\">\n {showBackButton && (\n <Button\n iconOnly\n aria-label=\"Back\"\n variant=\"plain\"\n theme=\"secondary\"\n size=\"xs\"\n onClick={onClose}\n >\n <ChevronLeftIcon className=\"size-4\" />\n </Button>\n )}\n {title && (\n <DialogTitle\n as=\"h3\"\n className={buildClassName(\n 'text-lg font-semibold text-gray-900 dark:text-white truncate tracking-tight min-w-0',\n titleClass,\n )}\n >\n {title}\n </DialogTitle>\n )}\n </div>\n\n {showCloseButton && (\n <Button\n iconOnly\n aria-label=\"Close drawer\"\n variant=\"plain\"\n theme=\"secondary\"\n size=\"xs\"\n onClick={onClose}\n className=\"rounded-full text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 shrink-0\"\n >\n <XIcon className=\"size-4\" />\n </Button>\n )}\n </div>\n\n {description && (\n <p\n className={buildClassName(\n 'text-sm text-gray-500 dark:text-gray-400 font-normal truncate',\n descriptionClass,\n )}\n >\n {description}\n </p>\n )}\n </div>\n )}\n\n {/* Content Body */}\n <div className={buildClassName('flex-1 p-4 md:p-6 space-y-4 min-w-0', contentClass)}>\n {children}\n </div>\n\n {/* Footer Section */}\n {footer && (\n <div\n className={buildClassName(\n 'p-4 border-t border-[var(--ui-border-muted)] bg-white dark:bg-gray-800 shrink-0 min-w-0',\n footerSticky && 'sticky bottom-0 z-10',\n )}\n >\n {footer}\n </div>\n )}\n </DialogPanel>\n </div>\n </div>\n </Dialog>\n )\n}\n\nDrawer.displayName = 'Drawer'\n"],"names":["React","_jsxs","_jsx","ChevronLeftIcon","XIcon"],"mappings":"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"}
1
+ {"version":3,"file":"Drawer.js","sources":["../../../../../src/components/Drawer/Drawer.tsx"],"sourcesContent":["import React from 'react'\nimport { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react'\nimport { buildClassName } from '../../utils/build-classname'\nimport { Button } from '../Button'\nimport ChevronLeftIcon from '../Icons/ChevronLeft.svg'\nimport XIcon from '../Icons/X.svg'\nimport { useBreakpoint, useResponsiveValue } from '../../hooks/use-breakpoint'\nimport type { AlignmentMap, DrawerProps, SizeMap } from './Drawer.types'\n\nconst alignmentMap: AlignmentMap = {\n center: 'items-center justify-center h-full',\n start: 'justify-start h-full',\n end: 'justify-end h-full',\n top: 'items-start justify-center w-full',\n bottom: 'items-end justify-center h-full',\n}\n\nconst panelAlignmentMap: AlignmentMap = {\n center: 'data-[closed]:scale-95 rounded-2xl',\n start: 'data-[closed]:-translate-x-1/2 rounded-r-2xl',\n end: 'data-[closed]:translate-x-1/2 rounded-l-2xl',\n top: 'data-[closed]:-translate-y-1/2 rounded-b-2xl',\n // Bottom sheets clear the home indicator on notched phones (0 elsewhere).\n bottom:\n 'data-[closed]:translate-y-full rounded-t-2xl md:rounded-t-3xl max-h-[88vh] max-h-[88dvh] pb-[env(safe-area-inset-bottom,0px)]',\n}\n\nconst panelSizeMap: SizeMap = {\n xs: 'max-w-xs',\n sm: 'max-w-sm',\n md: 'max-w-md',\n lg: 'max-w-lg',\n xl: 'max-w-xl',\n '2xl': 'max-w-2xl',\n full: 'w-full',\n}\n\n// How much shorter than the window the visual viewport must be before it's\n// taken to be the on-screen keyboard rather than the URL bar (≈ 56px).\nconst KEYBOARD_THRESHOLD = 150\nexport const Drawer: React.FC<DrawerProps> = ({\n isOpen,\n onClose = () => {},\n closeOnOutsideClick = true,\n align: alignProp = 'end',\n size: sizeProp = 'md',\n backdrop = true,\n title,\n description,\n children,\n footer,\n footerSticky = false,\n panelClass = '',\n contentClass = '',\n titleClass = '',\n descriptionClass = '',\n titleSticky = false,\n showBackButton = false,\n showCloseButton = true,\n dragHandle,\n phoneLayout = 'default',\n headerAction,\n}) => {\n const align = useResponsiveValue(alignProp)\n const size = useResponsiveValue(sizeProp)\n const breakpoint = useBreakpoint()\n const fullscreen = phoneLayout === 'fullscreen' && breakpoint === 'compact'\n const showTitleSection = Boolean(title || description || showBackButton || showCloseButton)\n const shouldShowDragHandle = !fullscreen && (dragHandle ?? align === 'bottom')\n useCloseOnBack(fullscreen && isOpen, onClose)\n // The visible area while the on-screen keyboard is open; null otherwise.\n const [keyboardViewport, setKeyboardViewport] = React.useState<{\n top: number\n height: number\n } | null>(null)\n\n React.useEffect(() => {\n if (!isOpen) {\n setKeyboardViewport(null)\n return\n }\n\n // Follow the visual viewport only while the on-screen keyboard takes space\n // (it's then much shorter than the window), so the drawer and its footer\n // stay above the keyboard. Otherwise it's pinned to the edges: tracking every resize and\n // scroll in state re-rendered the drawer on each frame as the browser's\n // URL bar moved, and its sticky header and footer jumped while scrolling.\n let frame = 0\n const measure = () => {\n frame = 0\n const vv = window.visualViewport\n if (!vv) return\n const keyboardOpen = window.innerHeight - vv.height > KEYBOARD_THRESHOLD\n const top = Math.round(vv.offsetTop)\n const height = Math.round(vv.height)\n // The browser scrolls the visible area to the focused field, so the\n // drawer follows its offset as well as its height. An unchanged value\n // doesn't re-render.\n setKeyboardViewport((current) =>\n !keyboardOpen\n ? null\n : current && current.top === top && current.height === height\n ? current\n : { top, height },\n )\n }\n const updateHeight = () => {\n if (!frame) frame = window.requestAnimationFrame(measure)\n }\n\n measure()\n const vv = typeof window !== 'undefined' ? window.visualViewport : null\n vv?.addEventListener('resize', updateHeight)\n vv?.addEventListener('scroll', updateHeight)\n\n return () => {\n if (frame) window.cancelAnimationFrame(frame)\n vv?.removeEventListener('resize', updateHeight)\n vv?.removeEventListener('scroll', updateHeight)\n }\n }, [isOpen])\n\n const handleDialogClose = () => {\n if (closeOnOutsideClick) {\n onClose?.()\n }\n }\n\n return (\n <Dialog\n open={isOpen}\n as=\"div\"\n className=\"relative z-50 focus:outline-none\"\n onClose={handleDialogClose}\n transition\n >\n {backdrop && (\n <DialogBackdrop className=\"fixed inset-0 bg-black/40 backdrop-blur-xs transition-opacity duration-200\" />\n )}\n\n {/* Pinned to all four edges rather than given a height: mobile browsers\n move fixed edges with the URL bar on every frame, while 100dvh only\n updates once its animation ends (the drawer briefly came up short).\n With the keyboard open it takes the visible area's offset and height. */}\n <div\n className=\"fixed inset-0\"\n style={\n keyboardViewport\n ? {\n top: `${keyboardViewport.top}px`,\n height: `${keyboardViewport.height}px`,\n bottom: 'auto',\n }\n : undefined\n }\n data-testid=\"drawer-viewport\"\n >\n <div className={buildClassName('flex', fullscreen ? 'h-full' : alignmentMap[align])}>\n <DialogPanel\n transition\n className={buildClassName(\n 'w-full max-w-full bg-white dark:bg-gray-800 shadow-xl flex flex-col min-w-0',\n 'overflow-y-auto overflow-x-hidden overscroll-contain touch-pan-y [-webkit-overflow-scrolling:touch] duration-200 ease-out data-[closed]:opacity-0',\n ...(fullscreen\n ? ['h-full data-[closed]:translate-y-4']\n : [panelAlignmentMap[align], panelSizeMap[size], panelClass]),\n )}\n data-layout={fullscreen ? 'fullscreen' : undefined}\n >\n {/* Top drag handle indicator for mobile bottom sheet */}\n {shouldShowDragHandle && (\n <div\n className=\"w-12 h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 mx-auto my-2.5 shrink-0 cursor-grab\"\n data-testid=\"drawer-drag-handle\"\n />\n )}\n\n {/* Phone full-screen bar: the only fixed part, so the fields keep the room. */}\n {fullscreen && (\n <div className=\"sticky top-0 z-10 flex h-14 shrink-0 items-center gap-1 border-b border-[var(--ui-border)] bg-white pl-1 pr-3 pt-[env(safe-area-inset-top,0px)] box-content dark:bg-gray-800\">\n <Button\n iconOnly\n aria-label=\"Close\"\n variant=\"plain\"\n theme=\"secondary\"\n onClick={onClose}\n className=\"shrink-0\"\n >\n <XIcon className=\"size-5\" />\n </Button>\n {title && (\n <DialogTitle\n as=\"h3\"\n className=\"min-w-0 flex-1 truncate text-base font-semibold text-gray-900 dark:text-white\"\n >\n {title}\n </DialogTitle>\n )}\n {headerAction && <div className=\"ml-auto shrink-0\">{headerAction}</div>}\n </div>\n )}\n\n {/* Header Section */}\n {!fullscreen && showTitleSection && (\n <div\n className={buildClassName(\n 'space-y-1 py-3 px-4 md:px-6 flex flex-col border-b border-[var(--ui-border)] shrink-0',\n titleSticky && 'sticky top-0 bg-white dark:bg-gray-800 z-10',\n )}\n >\n <div className=\"flex items-center justify-between gap-3 min-w-0\">\n <div className=\"flex items-center gap-2 min-w-0 flex-1\">\n {showBackButton && (\n <Button\n iconOnly\n aria-label=\"Back\"\n variant=\"plain\"\n theme=\"secondary\"\n size=\"xs\"\n onClick={onClose}\n >\n <ChevronLeftIcon className=\"size-4\" />\n </Button>\n )}\n {title && (\n <DialogTitle\n as=\"h3\"\n className={buildClassName(\n 'text-lg font-semibold text-gray-900 dark:text-white truncate tracking-tight min-w-0',\n titleClass,\n )}\n >\n {title}\n </DialogTitle>\n )}\n </div>\n\n {showCloseButton && (\n <Button\n iconOnly\n aria-label=\"Close drawer\"\n variant=\"plain\"\n theme=\"secondary\"\n size=\"xs\"\n onClick={onClose}\n className=\"rounded-full text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 shrink-0\"\n >\n <XIcon className=\"size-4\" />\n </Button>\n )}\n </div>\n\n {description && (\n <p\n className={buildClassName(\n 'text-sm text-gray-500 dark:text-gray-400 font-normal truncate',\n descriptionClass,\n )}\n >\n {description}\n </p>\n )}\n </div>\n )}\n\n {/* Content Body */}\n <div\n className={buildClassName(\n 'flex-1 p-4 md:p-6 space-y-4 min-w-0',\n contentClass,\n fullscreen && 'pb-[calc(1rem+env(safe-area-inset-bottom,0px))]',\n )}\n >\n {fullscreen && description && (\n <p\n className={buildClassName(\n 'text-sm text-gray-500 dark:text-gray-400',\n descriptionClass,\n )}\n >\n {description}\n </p>\n )}\n {children}\n </div>\n\n {/* Footer Section (the phone full-screen bar carries the action instead) */}\n {!fullscreen && footer && (\n <div\n className={buildClassName(\n 'p-4 border-t border-[var(--ui-border-muted)] bg-white dark:bg-gray-800 shrink-0 min-w-0',\n footerSticky && 'sticky bottom-0 z-10',\n )}\n >\n {footer}\n </div>\n )}\n </DialogPanel>\n </div>\n </div>\n </Dialog>\n )\n}\n\nDrawer.displayName = 'Drawer'\n\n/**\n * While `active`, the browser's back button (and Android's back gesture)\n * closes the drawer instead of leaving the page: opening adds a history entry\n * (keeping the current state, so routers see the same location), and back\n * removes it. Closed any other way, the entry is removed too, but only if it's\n * still the latest one, so a form that saves and then navigates isn't undone.\n */\nfunction useCloseOnBack(active: boolean, onClose: () => void) {\n const onCloseRef = React.useRef(onClose)\n React.useEffect(() => {\n onCloseRef.current = onClose\n })\n const token = React.useId()\n\n React.useEffect(() => {\n if (!active || typeof window === 'undefined') return\n const pushEntry = () =>\n window.history.pushState({ ...(window.history.state ?? {}), drawer: token }, '')\n pushEntry()\n let entryPresent = true\n let disposed = false\n const handlePop = () => {\n if (window.history.state?.drawer === token) return\n entryPresent = false\n onCloseRef.current()\n // onClose may step back within the drawer instead of closing it; if it's\n // still open afterwards, take back its entry so the next back works too.\n window.setTimeout(() => {\n if (disposed) return\n pushEntry()\n entryPresent = true\n }, 0)\n }\n window.addEventListener('popstate', handlePop)\n return () => {\n disposed = true\n window.removeEventListener('popstate', handlePop)\n if (entryPresent && window.history.state?.drawer === token) window.history.back()\n }\n }, [active, token])\n}\n"],"names":["React","_jsxs","_jsx","XIcon","ChevronLeftIcon"],"mappings":"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"}
@@ -404,27 +404,58 @@ const panelSizeMap = {
404
404
  '2xl': 'max-w-2xl',
405
405
  full: 'w-full',
406
406
  };
407
- const Drawer = ({ isOpen, onClose = () => { }, closeOnOutsideClick = true, align: alignProp = 'end', size: sizeProp = 'md', backdrop = true, title, description, children, footer, footerSticky = false, panelClass = '', contentClass = '', titleClass = '', descriptionClass = '', titleSticky = false, showBackButton = false, showCloseButton = true, dragHandle, }) => {
407
+ // How much shorter than the window the visual viewport must be before it's
408
+ // taken to be the on-screen keyboard rather than the URL bar (≈ 56px).
409
+ const KEYBOARD_THRESHOLD = 150;
410
+ const Drawer = ({ isOpen, onClose = () => { }, closeOnOutsideClick = true, align: alignProp = 'end', size: sizeProp = 'md', backdrop = true, title, description, children, footer, footerSticky = false, panelClass = '', contentClass = '', titleClass = '', descriptionClass = '', titleSticky = false, showBackButton = false, showCloseButton = true, dragHandle, phoneLayout = 'default', headerAction, }) => {
408
411
  const align = useResponsiveValue(alignProp);
409
412
  const size = useResponsiveValue(sizeProp);
413
+ const breakpoint = useBreakpoint();
414
+ const fullscreen = phoneLayout === 'fullscreen' && breakpoint === 'compact';
410
415
  const showTitleSection = Boolean(title || description || showBackButton || showCloseButton);
411
- const shouldShowDragHandle = dragHandle !== null && dragHandle !== void 0 ? dragHandle : align === 'bottom';
412
- const [viewportHeight, setViewportHeight] = React.useState(null);
416
+ const shouldShowDragHandle = !fullscreen && (dragHandle !== null && dragHandle !== void 0 ? dragHandle : align === 'bottom');
417
+ useCloseOnBack(fullscreen && isOpen, onClose);
418
+ // The visible area while the on-screen keyboard is open; null otherwise.
419
+ const [keyboardViewport, setKeyboardViewport] = React.useState(null);
413
420
  React.useEffect(() => {
414
421
  if (!isOpen) {
415
- setViewportHeight(null);
422
+ setKeyboardViewport(null);
416
423
  return;
417
424
  }
425
+ // Follow the visual viewport only while the on-screen keyboard takes space
426
+ // (it's then much shorter than the window), so the drawer and its footer
427
+ // stay above the keyboard. Otherwise it's pinned to the edges: tracking every resize and
428
+ // scroll in state re-rendered the drawer on each frame as the browser's
429
+ // URL bar moved, and its sticky header and footer jumped while scrolling.
430
+ let frame = 0;
431
+ const measure = () => {
432
+ frame = 0;
433
+ const vv = window.visualViewport;
434
+ if (!vv)
435
+ return;
436
+ const keyboardOpen = window.innerHeight - vv.height > KEYBOARD_THRESHOLD;
437
+ const top = Math.round(vv.offsetTop);
438
+ const height = Math.round(vv.height);
439
+ // The browser scrolls the visible area to the focused field, so the
440
+ // drawer follows its offset as well as its height. An unchanged value
441
+ // doesn't re-render.
442
+ setKeyboardViewport((current) => !keyboardOpen
443
+ ? null
444
+ : current && current.top === top && current.height === height
445
+ ? current
446
+ : { top, height });
447
+ };
418
448
  const updateHeight = () => {
419
- if (typeof window !== 'undefined' && window.visualViewport) {
420
- setViewportHeight(window.visualViewport.height);
421
- }
449
+ if (!frame)
450
+ frame = window.requestAnimationFrame(measure);
422
451
  };
423
- updateHeight();
452
+ measure();
424
453
  const vv = typeof window !== 'undefined' ? window.visualViewport : null;
425
454
  vv === null || vv === void 0 ? void 0 : vv.addEventListener('resize', updateHeight);
426
455
  vv === null || vv === void 0 ? void 0 : vv.addEventListener('scroll', updateHeight);
427
456
  return () => {
457
+ if (frame)
458
+ window.cancelAnimationFrame(frame);
428
459
  vv === null || vv === void 0 ? void 0 : vv.removeEventListener('resize', updateHeight);
429
460
  vv === null || vv === void 0 ? void 0 : vv.removeEventListener('scroll', updateHeight);
430
461
  };
@@ -434,9 +465,62 @@ const Drawer = ({ isOpen, onClose = () => { }, closeOnOutsideClick = true, align
434
465
  onClose === null || onClose === void 0 ? void 0 : onClose();
435
466
  }
436
467
  };
437
- return (jsxRuntime.jsxs(react.Dialog, { open: isOpen, as: "div", className: "relative z-50 focus:outline-none", onClose: handleDialogClose, transition: true, children: [backdrop && (jsxRuntime.jsx(react.DialogBackdrop, { className: "fixed inset-0 bg-black/40 backdrop-blur-xs transition-opacity duration-200" })), jsxRuntime.jsx("div", { className: "fixed left-0 top-0 w-full", style: { height: viewportHeight ? `${viewportHeight}px` : '100dvh' }, children: jsxRuntime.jsx("div", { className: buildClassName('flex', alignmentMap[align]), children: jsxRuntime.jsxs(react.DialogPanel, { transition: true, className: buildClassName('w-full max-w-full bg-white dark:bg-gray-800 shadow-xl flex flex-col min-w-0', 'overflow-y-auto overflow-x-hidden overscroll-contain touch-pan-y [-webkit-overflow-scrolling:touch] duration-200 ease-out data-[closed]:opacity-0', panelAlignmentMap[align], panelSizeMap[size], panelClass), children: [shouldShowDragHandle && (jsxRuntime.jsx("div", { className: "w-12 h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 mx-auto my-2.5 shrink-0 cursor-grab", "data-testid": "drawer-drag-handle" })), showTitleSection && (jsxRuntime.jsxs("div", { className: buildClassName('space-y-1 py-3 px-4 md:px-6 flex flex-col border-b border-[var(--ui-border)] shrink-0', titleSticky && 'sticky top-0 bg-white dark:bg-gray-800 z-10'), children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-w-0", children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2 min-w-0 flex-1", children: [showBackButton && (jsxRuntime.jsx(Button, { iconOnly: true, "aria-label": "Back", variant: "plain", theme: "secondary", size: "xs", onClick: onClose, children: jsxRuntime.jsx(SvgChevronLeft, { className: "size-4" }) })), title && (jsxRuntime.jsx(react.DialogTitle, { as: "h3", className: buildClassName('text-lg font-semibold text-gray-900 dark:text-white truncate tracking-tight min-w-0', titleClass), children: title }))] }), showCloseButton && (jsxRuntime.jsx(Button, { iconOnly: true, "aria-label": "Close drawer", variant: "plain", theme: "secondary", size: "xs", onClick: onClose, className: "rounded-full text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 shrink-0", children: jsxRuntime.jsx(SvgX, { className: "size-4" }) }))] }), description && (jsxRuntime.jsx("p", { className: buildClassName('text-sm text-gray-500 dark:text-gray-400 font-normal truncate', descriptionClass), children: description }))] })), jsxRuntime.jsx("div", { className: buildClassName('flex-1 p-4 md:p-6 space-y-4 min-w-0', contentClass), children: children }), footer && (jsxRuntime.jsx("div", { className: buildClassName('p-4 border-t border-[var(--ui-border-muted)] bg-white dark:bg-gray-800 shrink-0 min-w-0', footerSticky && 'sticky bottom-0 z-10'), children: footer }))] }) }) })] }));
468
+ return (jsxRuntime.jsxs(react.Dialog, { open: isOpen, as: "div", className: "relative z-50 focus:outline-none", onClose: handleDialogClose, transition: true, children: [backdrop && (jsxRuntime.jsx(react.DialogBackdrop, { className: "fixed inset-0 bg-black/40 backdrop-blur-xs transition-opacity duration-200" })), jsxRuntime.jsx("div", { className: "fixed inset-0", style: keyboardViewport
469
+ ? {
470
+ top: `${keyboardViewport.top}px`,
471
+ height: `${keyboardViewport.height}px`,
472
+ bottom: 'auto',
473
+ }
474
+ : undefined, "data-testid": "drawer-viewport", children: jsxRuntime.jsx("div", { className: buildClassName('flex', fullscreen ? 'h-full' : alignmentMap[align]), children: jsxRuntime.jsxs(react.DialogPanel, { transition: true, className: buildClassName('w-full max-w-full bg-white dark:bg-gray-800 shadow-xl flex flex-col min-w-0', 'overflow-y-auto overflow-x-hidden overscroll-contain touch-pan-y [-webkit-overflow-scrolling:touch] duration-200 ease-out data-[closed]:opacity-0', ...(fullscreen
475
+ ? ['h-full data-[closed]:translate-y-4']
476
+ : [panelAlignmentMap[align], panelSizeMap[size], panelClass])), "data-layout": fullscreen ? 'fullscreen' : undefined, children: [shouldShowDragHandle && (jsxRuntime.jsx("div", { className: "w-12 h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 mx-auto my-2.5 shrink-0 cursor-grab", "data-testid": "drawer-drag-handle" })), fullscreen && (jsxRuntime.jsxs("div", { className: "sticky top-0 z-10 flex h-14 shrink-0 items-center gap-1 border-b border-[var(--ui-border)] bg-white pl-1 pr-3 pt-[env(safe-area-inset-top,0px)] box-content dark:bg-gray-800", children: [jsxRuntime.jsx(Button, { iconOnly: true, "aria-label": "Close", variant: "plain", theme: "secondary", onClick: onClose, className: "shrink-0", children: jsxRuntime.jsx(SvgX, { className: "size-5" }) }), title && (jsxRuntime.jsx(react.DialogTitle, { as: "h3", className: "min-w-0 flex-1 truncate text-base font-semibold text-gray-900 dark:text-white", children: title })), headerAction && jsxRuntime.jsx("div", { className: "ml-auto shrink-0", children: headerAction })] })), !fullscreen && showTitleSection && (jsxRuntime.jsxs("div", { className: buildClassName('space-y-1 py-3 px-4 md:px-6 flex flex-col border-b border-[var(--ui-border)] shrink-0', titleSticky && 'sticky top-0 bg-white dark:bg-gray-800 z-10'), children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-w-0", children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2 min-w-0 flex-1", children: [showBackButton && (jsxRuntime.jsx(Button, { iconOnly: true, "aria-label": "Back", variant: "plain", theme: "secondary", size: "xs", onClick: onClose, children: jsxRuntime.jsx(SvgChevronLeft, { className: "size-4" }) })), title && (jsxRuntime.jsx(react.DialogTitle, { as: "h3", className: buildClassName('text-lg font-semibold text-gray-900 dark:text-white truncate tracking-tight min-w-0', titleClass), children: title }))] }), showCloseButton && (jsxRuntime.jsx(Button, { iconOnly: true, "aria-label": "Close drawer", variant: "plain", theme: "secondary", size: "xs", onClick: onClose, className: "rounded-full text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 shrink-0", children: jsxRuntime.jsx(SvgX, { className: "size-4" }) }))] }), description && (jsxRuntime.jsx("p", { className: buildClassName('text-sm text-gray-500 dark:text-gray-400 font-normal truncate', descriptionClass), children: description }))] })), jsxRuntime.jsxs("div", { className: buildClassName('flex-1 p-4 md:p-6 space-y-4 min-w-0', contentClass, fullscreen && 'pb-[calc(1rem+env(safe-area-inset-bottom,0px))]'), children: [fullscreen && description && (jsxRuntime.jsx("p", { className: buildClassName('text-sm text-gray-500 dark:text-gray-400', descriptionClass), children: description })), children] }), !fullscreen && footer && (jsxRuntime.jsx("div", { className: buildClassName('p-4 border-t border-[var(--ui-border-muted)] bg-white dark:bg-gray-800 shrink-0 min-w-0', footerSticky && 'sticky bottom-0 z-10'), children: footer }))] }) }) })] }));
438
477
  };
439
478
  Drawer.displayName = 'Drawer';
479
+ /**
480
+ * While `active`, the browser's back button (and Android's back gesture)
481
+ * closes the drawer instead of leaving the page: opening adds a history entry
482
+ * (keeping the current state, so routers see the same location), and back
483
+ * removes it. Closed any other way, the entry is removed too, but only if it's
484
+ * still the latest one, so a form that saves and then navigates isn't undone.
485
+ */
486
+ function useCloseOnBack(active, onClose) {
487
+ const onCloseRef = React.useRef(onClose);
488
+ React.useEffect(() => {
489
+ onCloseRef.current = onClose;
490
+ });
491
+ const token = React.useId();
492
+ React.useEffect(() => {
493
+ if (!active || typeof window === 'undefined')
494
+ return;
495
+ const pushEntry = () => { var _a; return window.history.pushState({ ...((_a = window.history.state) !== null && _a !== void 0 ? _a : {}), drawer: token }, ''); };
496
+ pushEntry();
497
+ let entryPresent = true;
498
+ let disposed = false;
499
+ const handlePop = () => {
500
+ var _a;
501
+ if (((_a = window.history.state) === null || _a === void 0 ? void 0 : _a.drawer) === token)
502
+ return;
503
+ entryPresent = false;
504
+ onCloseRef.current();
505
+ // onClose may step back within the drawer instead of closing it; if it's
506
+ // still open afterwards, take back its entry so the next back works too.
507
+ window.setTimeout(() => {
508
+ if (disposed)
509
+ return;
510
+ pushEntry();
511
+ entryPresent = true;
512
+ }, 0);
513
+ };
514
+ window.addEventListener('popstate', handlePop);
515
+ return () => {
516
+ var _a;
517
+ disposed = true;
518
+ window.removeEventListener('popstate', handlePop);
519
+ if (entryPresent && ((_a = window.history.state) === null || _a === void 0 ? void 0 : _a.drawer) === token)
520
+ window.history.back();
521
+ };
522
+ }, [active, token]);
523
+ }
440
524
 
441
525
  function ActionSheet({ isOpen, onClose, title, description, actions, showCancelButton = true, cancelLabel = 'Cancel', onCancel, closeOnBackdropClick = true, dragHandle = true, panelClass = '', containerClass = '', }) {
442
526
  const handleItemClick = (action) => {