@braedonsaunders/appkit-ui 0.2.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.
- package/LICENSE +661 -0
- package/README.md +11 -0
- package/account-menu.d.ts +49 -0
- package/account-menu.d.ts.map +1 -0
- package/account-menu.js +66 -0
- package/account-menu.js.map +1 -0
- package/alert.d.ts +13 -0
- package/alert.d.ts.map +1 -0
- package/alert.js +22 -0
- package/alert.js.map +1 -0
- package/animated-number.d.ts +13 -0
- package/animated-number.d.ts.map +1 -0
- package/animated-number.js +53 -0
- package/animated-number.js.map +1 -0
- package/api-reference.d.ts +44 -0
- package/api-reference.d.ts.map +1 -0
- package/api-reference.js +82 -0
- package/api-reference.js.map +1 -0
- package/app-shell.d.ts +39 -0
- package/app-shell.d.ts.map +1 -0
- package/app-shell.js +21 -0
- package/app-shell.js.map +1 -0
- package/app-sidebar.d.ts +22 -0
- package/app-sidebar.d.ts.map +1 -0
- package/app-sidebar.js +30 -0
- package/app-sidebar.js.map +1 -0
- package/avatar.d.ts +9 -0
- package/avatar.d.ts.map +1 -0
- package/avatar.js +18 -0
- package/avatar.js.map +1 -0
- package/badge.d.ts +9 -0
- package/badge.d.ts.map +1 -0
- package/badge.js +23 -0
- package/badge.js.map +1 -0
- package/button.d.ts +24 -0
- package/button.d.ts.map +1 -0
- package/button.js +50 -0
- package/button.js.map +1 -0
- package/card.d.ts +21 -0
- package/card.d.ts.map +1 -0
- package/card.js +36 -0
- package/card.js.map +1 -0
- package/checkbox.d.ts +4 -0
- package/checkbox.d.ts.map +1 -0
- package/checkbox.js +6 -0
- package/checkbox.js.map +1 -0
- package/confirm-dialog.d.ts +23 -0
- package/confirm-dialog.d.ts.map +1 -0
- package/confirm-dialog.js +67 -0
- package/confirm-dialog.js.map +1 -0
- package/context-menu.d.ts +47 -0
- package/context-menu.d.ts.map +1 -0
- package/context-menu.js +90 -0
- package/context-menu.js.map +1 -0
- package/dialog.d.ts +25 -0
- package/dialog.d.ts.map +1 -0
- package/dialog.js +89 -0
- package/dialog.js.map +1 -0
- package/document-title.d.ts +11 -0
- package/document-title.d.ts.map +1 -0
- package/document-title.js +17 -0
- package/document-title.js.map +1 -0
- package/drawer.d.ts +63 -0
- package/drawer.d.ts.map +1 -0
- package/drawer.js +188 -0
- package/drawer.js.map +1 -0
- package/empty-state.d.ts +10 -0
- package/empty-state.d.ts.map +1 -0
- package/empty-state.js +7 -0
- package/empty-state.js.map +1 -0
- package/file-uploader.d.ts +67 -0
- package/file-uploader.d.ts.map +1 -0
- package/file-uploader.js +156 -0
- package/file-uploader.js.map +1 -0
- package/filter-chips.d.ts +47 -0
- package/filter-chips.d.ts.map +1 -0
- package/filter-chips.js +62 -0
- package/filter-chips.js.map +1 -0
- package/global-search.d.ts +41 -0
- package/global-search.d.ts.map +1 -0
- package/global-search.js +116 -0
- package/global-search.js.map +1 -0
- package/index.d.ts +70 -0
- package/index.d.ts.map +1 -0
- package/index.js +70 -0
- package/index.js.map +1 -0
- package/input.d.ts +9 -0
- package/input.d.ts.map +1 -0
- package/input.js +11 -0
- package/input.js.map +1 -0
- package/label.d.ts +3 -0
- package/label.d.ts.map +1 -0
- package/label.js +6 -0
- package/label.js.map +1 -0
- package/line-grid.d.ts +48 -0
- package/line-grid.d.ts.map +1 -0
- package/line-grid.js +155 -0
- package/line-grid.js.map +1 -0
- package/link-context.d.ts +44 -0
- package/link-context.d.ts.map +1 -0
- package/link-context.js +32 -0
- package/link-context.js.map +1 -0
- package/list-nav.d.ts +28 -0
- package/list-nav.d.ts.map +1 -0
- package/list-nav.js +41 -0
- package/list-nav.js.map +1 -0
- package/list-pagination.d.ts +27 -0
- package/list-pagination.d.ts.map +1 -0
- package/list-pagination.js +43 -0
- package/list-pagination.js.map +1 -0
- package/list-params.d.ts +41 -0
- package/list-params.d.ts.map +1 -0
- package/list-params.js +84 -0
- package/list-params.js.map +1 -0
- package/list-search-input-state.d.ts +13 -0
- package/list-search-input-state.d.ts.map +1 -0
- package/list-search-input-state.js +27 -0
- package/list-search-input-state.js.map +1 -0
- package/list-search-input.d.ts +16 -0
- package/list-search-input.d.ts.map +1 -0
- package/list-search-input.js +51 -0
- package/list-search-input.js.map +1 -0
- package/mobile-tab-bar.d.ts +14 -0
- package/mobile-tab-bar.d.ts.map +1 -0
- package/mobile-tab-bar.js +25 -0
- package/mobile-tab-bar.js.map +1 -0
- package/multi-select.d.ts +32 -0
- package/multi-select.d.ts.map +1 -0
- package/multi-select.js +52 -0
- package/multi-select.js.map +1 -0
- package/nav-overflow.d.ts +12 -0
- package/nav-overflow.d.ts.map +1 -0
- package/nav-overflow.js +30 -0
- package/nav-overflow.js.map +1 -0
- package/navigation-config-editor.d.ts +26 -0
- package/navigation-config-editor.d.ts.map +1 -0
- package/navigation-config-editor.js +57 -0
- package/navigation-config-editor.js.map +1 -0
- package/navigation-config.d.ts +48 -0
- package/navigation-config.d.ts.map +1 -0
- package/navigation-config.js +75 -0
- package/navigation-config.js.map +1 -0
- package/navigation-mode.d.ts +16 -0
- package/navigation-mode.d.ts.map +1 -0
- package/navigation-mode.js +30 -0
- package/navigation-mode.js.map +1 -0
- package/notifications-bell.d.ts +22 -0
- package/notifications-bell.d.ts.map +1 -0
- package/notifications-bell.js +30 -0
- package/notifications-bell.js.map +1 -0
- package/org-chart.d.ts +85 -0
- package/org-chart.d.ts.map +1 -0
- package/org-chart.js +193 -0
- package/org-chart.js.map +1 -0
- package/package.json +61 -0
- package/page-header.d.ts +32 -0
- package/page-header.d.ts.map +1 -0
- package/page-header.js +14 -0
- package/page-header.js.map +1 -0
- package/page-layout.d.ts +29 -0
- package/page-layout.d.ts.map +1 -0
- package/page-layout.js +23 -0
- package/page-layout.js.map +1 -0
- package/page-transition.d.ts +13 -0
- package/page-transition.d.ts.map +1 -0
- package/page-transition.js +14 -0
- package/page-transition.js.map +1 -0
- package/paged-table.d.ts +54 -0
- package/paged-table.d.ts.map +1 -0
- package/paged-table.js +74 -0
- package/paged-table.js.map +1 -0
- package/popover.d.ts +16 -0
- package/popover.d.ts.map +1 -0
- package/popover.js +99 -0
- package/popover.js.map +1 -0
- package/progress.d.ts +7 -0
- package/progress.d.ts.map +1 -0
- package/progress.js +8 -0
- package/progress.js.map +1 -0
- package/prompt-dialog.d.ts +21 -0
- package/prompt-dialog.d.ts.map +1 -0
- package/prompt-dialog.js +109 -0
- package/prompt-dialog.js.map +1 -0
- package/record-list.d.ts +76 -0
- package/record-list.d.ts.map +1 -0
- package/record-list.js +51 -0
- package/record-list.js.map +1 -0
- package/search-select.d.ts +42 -0
- package/search-select.d.ts.map +1 -0
- package/search-select.js +194 -0
- package/search-select.js.map +1 -0
- package/select-options.d.ts +16 -0
- package/select-options.d.ts.map +1 -0
- package/select-options.js +119 -0
- package/select-options.js.map +1 -0
- package/select.d.ts +24 -0
- package/select.d.ts.map +1 -0
- package/select.js +65 -0
- package/select.js.map +1 -0
- package/separator.d.ts +6 -0
- package/separator.d.ts.map +1 -0
- package/separator.js +7 -0
- package/separator.js.map +1 -0
- package/settings-layout.d.ts +79 -0
- package/settings-layout.d.ts.map +1 -0
- package/settings-layout.js +74 -0
- package/settings-layout.js.map +1 -0
- package/sidebar-nav.d.ts +67 -0
- package/sidebar-nav.d.ts.map +1 -0
- package/sidebar-nav.js +291 -0
- package/sidebar-nav.js.map +1 -0
- package/signature-pad.d.ts +16 -0
- package/signature-pad.d.ts.map +1 -0
- package/signature-pad.js +355 -0
- package/signature-pad.js.map +1 -0
- package/skeleton.d.ts +9 -0
- package/skeleton.d.ts.map +1 -0
- package/skeleton.js +12 -0
- package/skeleton.js.map +1 -0
- package/slider.d.ts +13 -0
- package/slider.d.ts.map +1 -0
- package/slider.js +15 -0
- package/slider.js.map +1 -0
- package/sortable-th.d.ts +28 -0
- package/sortable-th.d.ts.map +1 -0
- package/sortable-th.js +35 -0
- package/sortable-th.js.map +1 -0
- package/sparkline.d.ts +16 -0
- package/sparkline.d.ts.map +1 -0
- package/sparkline.js +46 -0
- package/sparkline.js.map +1 -0
- package/spinner.d.ts +11 -0
- package/spinner.d.ts.map +1 -0
- package/spinner.js +11 -0
- package/spinner.js.map +1 -0
- package/styles.css +166 -0
- package/subtab-nav.d.ts +17 -0
- package/subtab-nav.d.ts.map +1 -0
- package/subtab-nav.js +11 -0
- package/subtab-nav.js.map +1 -0
- package/switch.d.ts +9 -0
- package/switch.d.ts.map +1 -0
- package/switch.js +11 -0
- package/switch.js.map +1 -0
- package/tab-content.d.ts +24 -0
- package/tab-content.d.ts.map +1 -0
- package/tab-content.js +26 -0
- package/tab-content.js.map +1 -0
- package/table.d.ts +28 -0
- package/table.d.ts.map +1 -0
- package/table.js +21 -0
- package/table.js.map +1 -0
- package/tabs.d.ts +18 -0
- package/tabs.d.ts.map +1 -0
- package/tabs.js +18 -0
- package/tabs.js.map +1 -0
- package/text-context.d.ts +9 -0
- package/text-context.d.ts.map +1 -0
- package/text-context.js +12 -0
- package/text-context.js.map +1 -0
- package/textarea.d.ts +4 -0
- package/textarea.d.ts.map +1 -0
- package/textarea.js +6 -0
- package/textarea.js.map +1 -0
- package/theme-script.d.ts +3 -0
- package/theme-script.d.ts.map +1 -0
- package/theme-script.js +6 -0
- package/theme-script.js.map +1 -0
- package/theme.d.ts +28 -0
- package/theme.d.ts.map +1 -0
- package/theme.js +92 -0
- package/theme.js.map +1 -0
- package/toast.d.ts +52 -0
- package/toast.d.ts.map +1 -0
- package/toast.js +178 -0
- package/toast.js.map +1 -0
- package/tooltip.d.ts +12 -0
- package/tooltip.d.ts.map +1 -0
- package/tooltip.js +14 -0
- package/tooltip.js.map +1 -0
- package/top-nav.d.ts +18 -0
- package/top-nav.d.ts.map +1 -0
- package/top-nav.js +160 -0
- package/top-nav.js.map +1 -0
- package/upload-limits.d.ts +4 -0
- package/upload-limits.d.ts.map +1 -0
- package/upload-limits.js +21 -0
- package/upload-limits.js.map +1 -0
- package/use-element-size.d.ts +7 -0
- package/use-element-size.d.ts.map +1 -0
- package/use-element-size.js +21 -0
- package/use-element-size.js.map +1 -0
- package/utils.d.ts +4 -0
- package/utils.d.ts.map +1 -0
- package/utils.js +7 -0
- package/utils.js.map +1 -0
- package/workspace-launcher.d.ts +50 -0
- package/workspace-launcher.d.ts.map +1 -0
- package/workspace-launcher.js +127 -0
- package/workspace-launcher.js.map +1 -0
package/context-menu.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
6
|
+
import { cn } from './utils.js';
|
|
7
|
+
export function useContextMenu() {
|
|
8
|
+
const [position, setPosition] = React.useState(null);
|
|
9
|
+
const openAt = React.useCallback((x, y) => setPosition({ x, y }), []);
|
|
10
|
+
const onContextMenu = React.useCallback((e) => {
|
|
11
|
+
e.preventDefault();
|
|
12
|
+
e.stopPropagation();
|
|
13
|
+
setPosition({ x: e.clientX, y: e.clientY });
|
|
14
|
+
}, []);
|
|
15
|
+
const openBelow = React.useCallback((el) => {
|
|
16
|
+
const r = el.getBoundingClientRect();
|
|
17
|
+
setPosition({ x: r.right, y: r.bottom + 4 });
|
|
18
|
+
}, []);
|
|
19
|
+
const close = React.useCallback(() => setPosition(null), []);
|
|
20
|
+
return { open: position != null, position, openAt, onContextMenu, openBelow, close };
|
|
21
|
+
}
|
|
22
|
+
export function ContextMenu({ open, position, items, onClose, className, }) {
|
|
23
|
+
const panelRef = React.useRef(null);
|
|
24
|
+
const [mounted, setMounted] = React.useState(false);
|
|
25
|
+
const [coords, setCoords] = React.useState(null);
|
|
26
|
+
React.useEffect(() => setMounted(true), []);
|
|
27
|
+
// Clamp inside the viewport before paint so the menu never flashes off-screen.
|
|
28
|
+
React.useLayoutEffect(() => {
|
|
29
|
+
if (!open || !position) {
|
|
30
|
+
setCoords(null);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const panel = panelRef.current;
|
|
34
|
+
const pw = panel?.offsetWidth ?? 176;
|
|
35
|
+
const ph = panel?.offsetHeight ?? 0;
|
|
36
|
+
const pad = 8;
|
|
37
|
+
let x = position.x;
|
|
38
|
+
let y = position.y;
|
|
39
|
+
if (x + pw + pad > window.innerWidth)
|
|
40
|
+
x = window.innerWidth - pw - pad;
|
|
41
|
+
if (y + ph + pad > window.innerHeight)
|
|
42
|
+
y = window.innerHeight - ph - pad;
|
|
43
|
+
setCoords({ x: Math.max(pad, x), y: Math.max(pad, y) });
|
|
44
|
+
}, [open, position]);
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
if (!open)
|
|
47
|
+
return;
|
|
48
|
+
function onDown(e) {
|
|
49
|
+
if (panelRef.current?.contains(e.target))
|
|
50
|
+
return;
|
|
51
|
+
onClose();
|
|
52
|
+
}
|
|
53
|
+
function onKey(e) {
|
|
54
|
+
if (e.key === 'Escape')
|
|
55
|
+
onClose();
|
|
56
|
+
}
|
|
57
|
+
function onScroll() {
|
|
58
|
+
onClose();
|
|
59
|
+
}
|
|
60
|
+
const t = setTimeout(() => {
|
|
61
|
+
document.addEventListener('mousedown', onDown);
|
|
62
|
+
document.addEventListener('contextmenu', onDown);
|
|
63
|
+
document.addEventListener('keydown', onKey);
|
|
64
|
+
window.addEventListener('scroll', onScroll, true);
|
|
65
|
+
window.addEventListener('resize', onScroll);
|
|
66
|
+
}, 0);
|
|
67
|
+
return () => {
|
|
68
|
+
clearTimeout(t);
|
|
69
|
+
document.removeEventListener('mousedown', onDown);
|
|
70
|
+
document.removeEventListener('contextmenu', onDown);
|
|
71
|
+
document.removeEventListener('keydown', onKey);
|
|
72
|
+
window.removeEventListener('scroll', onScroll, true);
|
|
73
|
+
window.removeEventListener('resize', onScroll);
|
|
74
|
+
};
|
|
75
|
+
}, [open, onClose]);
|
|
76
|
+
if (!mounted || typeof document === 'undefined')
|
|
77
|
+
return null;
|
|
78
|
+
const at = coords ?? position;
|
|
79
|
+
return createPortal(_jsx(AnimatePresence, { children: open && at ? (_jsx(motion.div, { ref: panelRef, "data-ui-overlay": true, initial: { opacity: 0, scale: 0.96 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.96 }, transition: { duration: 0.12, ease: [0.16, 1, 0.3, 1] }, className: cn('fixed z-[70] min-w-[11rem] max-w-[16rem] overflow-hidden rounded-md border border-border bg-elevated p-1 shadow-lg', className), style: { left: at.x, top: at.y, transformOrigin: 'top left' }, role: "menu", children: items.map((item) => 'separator' in item ? (_jsx("div", { className: "my-1 h-px bg-border" }, item.key)) : (_jsxs("button", { type: "button", role: "menuitem", disabled: item.disabled, onClick: () => {
|
|
80
|
+
if (item.disabled)
|
|
81
|
+
return;
|
|
82
|
+
onClose();
|
|
83
|
+
item.onSelect();
|
|
84
|
+
}, className: cn('flex w-full items-center gap-2.5 rounded px-2.5 py-1.5 text-left text-sm transition-colors', item.disabled
|
|
85
|
+
? 'cursor-not-allowed text-fg-subtle'
|
|
86
|
+
: item.danger
|
|
87
|
+
? 'text-danger hover:bg-danger-subtle'
|
|
88
|
+
: 'text-fg hover:bg-surface-hover'), children: [item.icon ? _jsx(item.icon, { className: "size-4 shrink-0 opacity-80" }) : null, _jsx("span", { className: "flex-1 truncate", children: item.label })] }, item.key))) })) : null }), document.body);
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=context-menu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-menu.js","sourceRoot":"","sources":["../src/context-menu.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport * as React from 'react'\nimport { createPortal } from 'react-dom'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport { cn } from './utils'\n\n/**\n * Universal context menu — a floating list of actions opened at a point.\n * Trigger it via {@link useContextMenu}, either from a right-click\n * (`onContextMenu={menu.onContextMenu}`) or a kebab button\n * (`onClick={(e) => menu.openBelow(e.currentTarget)}`). The panel portals to\n * <body>, clamps inside the viewport, and closes on outside-click / Esc /\n * scroll. Items carry an icon, label, and handler; `danger` tints destructive\n * actions and `{ separator: true }` draws a divider.\n */\nexport interface ContextMenuItem {\n key: string\n label: string\n icon?: React.ComponentType<{ className?: string }>\n onSelect: () => void\n disabled?: boolean\n danger?: boolean\n}\n\nexport type ContextMenuEntry = ContextMenuItem | { key: string; separator: true }\n\nexport interface ContextMenuController {\n open: boolean\n position: { x: number; y: number } | null\n openAt: (x: number, y: number) => void\n onContextMenu: (e: React.MouseEvent) => void\n openBelow: (el: HTMLElement) => void\n close: () => void\n}\n\nexport function useContextMenu(): ContextMenuController {\n const [position, setPosition] = React.useState<{ x: number; y: number } | null>(null)\n const openAt = React.useCallback((x: number, y: number) => setPosition({ x, y }), [])\n const onContextMenu = React.useCallback((e: React.MouseEvent) => {\n e.preventDefault()\n e.stopPropagation()\n setPosition({ x: e.clientX, y: e.clientY })\n }, [])\n const openBelow = React.useCallback((el: HTMLElement) => {\n const r = el.getBoundingClientRect()\n setPosition({ x: r.right, y: r.bottom + 4 })\n }, [])\n const close = React.useCallback(() => setPosition(null), [])\n return { open: position != null, position, openAt, onContextMenu, openBelow, close }\n}\n\nexport function ContextMenu({\n open,\n position,\n items,\n onClose,\n className,\n}: {\n open: boolean\n position: { x: number; y: number } | null\n items: ContextMenuEntry[]\n onClose: () => void\n className?: string\n}) {\n const panelRef = React.useRef<HTMLDivElement>(null)\n const [mounted, setMounted] = React.useState(false)\n const [coords, setCoords] = React.useState<{ x: number; y: number } | null>(null)\n\n React.useEffect(() => setMounted(true), [])\n\n // Clamp inside the viewport before paint so the menu never flashes off-screen.\n React.useLayoutEffect(() => {\n if (!open || !position) {\n setCoords(null)\n return\n }\n const panel = panelRef.current\n const pw = panel?.offsetWidth ?? 176\n const ph = panel?.offsetHeight ?? 0\n const pad = 8\n let x = position.x\n let y = position.y\n if (x + pw + pad > window.innerWidth) x = window.innerWidth - pw - pad\n if (y + ph + pad > window.innerHeight) y = window.innerHeight - ph - pad\n setCoords({ x: Math.max(pad, x), y: Math.max(pad, y) })\n }, [open, position])\n\n React.useEffect(() => {\n if (!open) return\n function onDown(e: Event) {\n if (panelRef.current?.contains(e.target as Node)) return\n onClose()\n }\n function onKey(e: KeyboardEvent) {\n if (e.key === 'Escape') onClose()\n }\n function onScroll() {\n onClose()\n }\n const t = setTimeout(() => {\n document.addEventListener('mousedown', onDown)\n document.addEventListener('contextmenu', onDown)\n document.addEventListener('keydown', onKey)\n window.addEventListener('scroll', onScroll, true)\n window.addEventListener('resize', onScroll)\n }, 0)\n return () => {\n clearTimeout(t)\n document.removeEventListener('mousedown', onDown)\n document.removeEventListener('contextmenu', onDown)\n document.removeEventListener('keydown', onKey)\n window.removeEventListener('scroll', onScroll, true)\n window.removeEventListener('resize', onScroll)\n }\n }, [open, onClose])\n\n if (!mounted || typeof document === 'undefined') return null\n\n const at = coords ?? position\n\n return createPortal(\n <AnimatePresence>\n {open && at ? (\n <motion.div\n ref={panelRef}\n data-ui-overlay\n initial={{ opacity: 0, scale: 0.96 }}\n animate={{ opacity: 1, scale: 1 }}\n exit={{ opacity: 0, scale: 0.96 }}\n transition={{ duration: 0.12, ease: [0.16, 1, 0.3, 1] }}\n className={cn(\n 'fixed z-[70] min-w-[11rem] max-w-[16rem] overflow-hidden rounded-md border border-border bg-elevated p-1 shadow-lg',\n className,\n )}\n style={{ left: at.x, top: at.y, transformOrigin: 'top left' }}\n role=\"menu\"\n >\n {items.map((item) =>\n 'separator' in item ? (\n <div key={item.key} className=\"my-1 h-px bg-border\" />\n ) : (\n <button\n key={item.key}\n type=\"button\"\n role=\"menuitem\"\n disabled={item.disabled}\n onClick={() => {\n if (item.disabled) return\n onClose()\n item.onSelect()\n }}\n className={cn(\n 'flex w-full items-center gap-2.5 rounded px-2.5 py-1.5 text-left text-sm transition-colors',\n item.disabled\n ? 'cursor-not-allowed text-fg-subtle'\n : item.danger\n ? 'text-danger hover:bg-danger-subtle'\n : 'text-fg hover:bg-surface-hover',\n )}\n >\n {item.icon ? <item.icon className=\"size-4 shrink-0 opacity-80\" /> : null}\n <span className=\"flex-1 truncate\">{item.label}</span>\n </button>\n ),\n )}\n </motion.div>\n ) : null}\n </AnimatePresence>,\n document.body,\n )\n}\n"]}
|
package/dialog.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export type DialogSize = 'sm' | 'md' | 'lg' | 'xl';
|
|
3
|
+
export type DialogProps = {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
title?: React.ReactNode;
|
|
7
|
+
description?: React.ReactNode;
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
footer?: React.ReactNode;
|
|
10
|
+
size?: DialogSize;
|
|
11
|
+
/** Hide the corner close button. */
|
|
12
|
+
hideClose?: boolean;
|
|
13
|
+
closeLabel?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Give the panel a fixed working height and hand the body to the consumer as
|
|
16
|
+
* a flex region with no padding. Use it when the content owns its own
|
|
17
|
+
* toolbar/scroll/columns (an editor, a review pane) rather than being a short
|
|
18
|
+
* block of prose — otherwise the dialog grows with its content and the page
|
|
19
|
+
* behind it scrolls instead.
|
|
20
|
+
*/
|
|
21
|
+
fullHeight?: boolean;
|
|
22
|
+
};
|
|
23
|
+
/** Centered modal dialog: backdrop, spring scale-in, focus trap, Esc/click-out. */
|
|
24
|
+
export declare function Dialog({ open, onClose, title, description, children, footer, size, hideClose, closeLabel, fullHeight, }: DialogProps): React.ReactPortal | null;
|
|
25
|
+
//# sourceMappingURL=dialog.d.ts.map
|
package/dialog.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialog.d.ts","sourceRoot":"","sources":["../src/dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAM9B,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAWlD,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACxB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,oCAAoC;IACpC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,CAAA;AAED,mFAAmF;AACnF,wBAAgB,MAAM,CAAC,EACrB,IAAI,EACJ,OAAO,EACP,KAAK,EACL,WAAW,EACX,QAAQ,EACR,MAAM,EACN,IAAW,EACX,SAAS,EACT,UAAoB,EACpB,UAAU,GACX,EAAE,WAAW,4BAoIb"}
|
package/dialog.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
|
|
6
|
+
import { X } from 'lucide-react';
|
|
7
|
+
import { cn } from './utils.js';
|
|
8
|
+
const SIZE = {
|
|
9
|
+
sm: 'max-w-sm',
|
|
10
|
+
md: 'max-w-lg',
|
|
11
|
+
lg: 'max-w-2xl',
|
|
12
|
+
// Working dialogs — an editor with its own sidebar, a two-pane review — need
|
|
13
|
+
// more width than a confirm prompt.
|
|
14
|
+
xl: 'max-w-5xl',
|
|
15
|
+
};
|
|
16
|
+
/** Centered modal dialog: backdrop, spring scale-in, focus trap, Esc/click-out. */
|
|
17
|
+
export function Dialog({ open, onClose, title, description, children, footer, size = 'md', hideClose, closeLabel = 'Close', fullHeight, }) {
|
|
18
|
+
const reduce = useReducedMotion();
|
|
19
|
+
const [mounted, setMounted] = React.useState(false);
|
|
20
|
+
const panelRef = React.useRef(null);
|
|
21
|
+
const onCloseRef = React.useRef(onClose);
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
onCloseRef.current = onClose;
|
|
24
|
+
}, [onClose]);
|
|
25
|
+
React.useEffect(() => setMounted(true), []);
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
if (!open)
|
|
28
|
+
return;
|
|
29
|
+
const prevOverflow = document.body.style.overflow;
|
|
30
|
+
document.body.style.overflow = 'hidden';
|
|
31
|
+
const previouslyFocused = document.activeElement;
|
|
32
|
+
const focusablesSelector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
33
|
+
const focusTimer = window.setTimeout(() => {
|
|
34
|
+
const panel = panelRef.current;
|
|
35
|
+
if (!panel)
|
|
36
|
+
return;
|
|
37
|
+
// React's autoFocus may already have placed focus inside the dialog.
|
|
38
|
+
// Do not steal it, and prefer an explicit autofocus target before the
|
|
39
|
+
// first DOM-order control (which is often the corner close button).
|
|
40
|
+
if (panel.contains(document.activeElement))
|
|
41
|
+
return;
|
|
42
|
+
const preferred = panel.querySelector('[autofocus]');
|
|
43
|
+
const first = panel.querySelector(focusablesSelector);
|
|
44
|
+
(preferred ?? first ?? panel).focus();
|
|
45
|
+
}, 0);
|
|
46
|
+
function onKey(e) {
|
|
47
|
+
if (e.key === 'Escape') {
|
|
48
|
+
if (document.querySelector('[data-ui-overlay]'))
|
|
49
|
+
return;
|
|
50
|
+
onCloseRef.current();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (e.key !== 'Tab')
|
|
54
|
+
return;
|
|
55
|
+
const panel = panelRef.current;
|
|
56
|
+
if (!panel)
|
|
57
|
+
return;
|
|
58
|
+
const items = Array.from(panel.querySelectorAll(focusablesSelector)).filter((el) => el.offsetParent !== null || el === document.activeElement);
|
|
59
|
+
if (items.length === 0) {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
panel.focus();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const first = items[0];
|
|
65
|
+
const last = items[items.length - 1];
|
|
66
|
+
const activeEl = document.activeElement;
|
|
67
|
+
if (e.shiftKey && (activeEl === first || activeEl === panel)) {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
last.focus();
|
|
70
|
+
}
|
|
71
|
+
else if (!e.shiftKey && activeEl === last) {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
first.focus();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
document.addEventListener('keydown', onKey);
|
|
77
|
+
return () => {
|
|
78
|
+
document.body.style.overflow = prevOverflow;
|
|
79
|
+
window.clearTimeout(focusTimer);
|
|
80
|
+
document.removeEventListener('keydown', onKey);
|
|
81
|
+
if (previouslyFocused && document.contains(previouslyFocused))
|
|
82
|
+
previouslyFocused.focus();
|
|
83
|
+
};
|
|
84
|
+
}, [open]);
|
|
85
|
+
if (!mounted || typeof document === 'undefined')
|
|
86
|
+
return null;
|
|
87
|
+
return createPortal(_jsx(AnimatePresence, { children: open ? (_jsxs("div", { className: "fixed inset-0 z-[65] flex items-center justify-center p-4", children: [_jsx(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.15 }, className: "absolute inset-0 bg-overlay/50 backdrop-blur-[2px]", onClick: onClose, "aria-hidden": "true" }), _jsxs(motion.div, { ref: panelRef, role: "dialog", "aria-modal": "true", tabIndex: -1, initial: reduce ? { opacity: 0 } : { opacity: 0, scale: 0.96, y: 8 }, animate: reduce ? { opacity: 1 } : { opacity: 1, scale: 1, y: 0 }, exit: reduce ? { opacity: 0 } : { opacity: 0, scale: 0.96, y: 8 }, transition: { type: 'spring', damping: 30, stiffness: 380, mass: 0.7 }, className: cn('relative flex w-full flex-col overflow-hidden rounded-xl border border-border bg-surface shadow-lg', SIZE[size], fullHeight && 'h-[min(92vh,780px)]'), children: [title || description ? (_jsxs("div", { className: "space-y-1 px-6 pt-6", children: [title ? _jsx("h2", { className: "text-lg font-semibold text-fg", children: title }) : null, description ? _jsx("p", { className: "text-sm text-fg-muted", children: description }) : null] })) : null, !hideClose ? (_jsx("button", { type: "button", onClick: onClose, "aria-label": closeLabel, className: "absolute right-4 top-4 rounded-md p-1.5 text-fg-muted transition-colors hover:bg-surface-hover hover:text-fg", children: _jsx(X, { className: "size-4" }) })) : null, children ? (_jsx("div", { className: cn('text-sm text-fg', fullHeight ? 'flex min-h-0 flex-1 flex-col overflow-hidden' : 'px-6 py-5'), children: children })) : null, footer ? (_jsx("div", { className: "flex items-center justify-end gap-2 border-t border-border bg-bg-subtle px-6 py-3", children: footer })) : null] })] })) : null }), document.body);
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=dialog.js.map
|
package/dialog.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialog.js","sourceRoot":"","sources":["../src/dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport * as React from 'react'\nimport { createPortal } from 'react-dom'\nimport { AnimatePresence, motion, useReducedMotion } from 'framer-motion'\nimport { X } from 'lucide-react'\nimport { cn } from './utils'\n\nexport type DialogSize = 'sm' | 'md' | 'lg' | 'xl'\n\nconst SIZE: Record<DialogSize, string> = {\n sm: 'max-w-sm',\n md: 'max-w-lg',\n lg: 'max-w-2xl',\n // Working dialogs — an editor with its own sidebar, a two-pane review — need\n // more width than a confirm prompt.\n xl: 'max-w-5xl',\n}\n\nexport type DialogProps = {\n open: boolean\n onClose: () => void\n title?: React.ReactNode\n description?: React.ReactNode\n children?: React.ReactNode\n footer?: React.ReactNode\n size?: DialogSize\n /** Hide the corner close button. */\n hideClose?: boolean\n closeLabel?: string\n /**\n * Give the panel a fixed working height and hand the body to the consumer as\n * a flex region with no padding. Use it when the content owns its own\n * toolbar/scroll/columns (an editor, a review pane) rather than being a short\n * block of prose — otherwise the dialog grows with its content and the page\n * behind it scrolls instead.\n */\n fullHeight?: boolean\n}\n\n/** Centered modal dialog: backdrop, spring scale-in, focus trap, Esc/click-out. */\nexport function Dialog({\n open,\n onClose,\n title,\n description,\n children,\n footer,\n size = 'md',\n hideClose,\n closeLabel = 'Close',\n fullHeight,\n}: DialogProps) {\n const reduce = useReducedMotion()\n const [mounted, setMounted] = React.useState(false)\n const panelRef = React.useRef<HTMLDivElement>(null)\n const onCloseRef = React.useRef(onClose)\n React.useEffect(() => {\n onCloseRef.current = onClose\n }, [onClose])\n React.useEffect(() => setMounted(true), [])\n\n React.useEffect(() => {\n if (!open) return\n const prevOverflow = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n const previouslyFocused = document.activeElement as HTMLElement | null\n const focusablesSelector =\n 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'\n const focusTimer = window.setTimeout(() => {\n const panel = panelRef.current\n if (!panel) return\n // React's autoFocus may already have placed focus inside the dialog.\n // Do not steal it, and prefer an explicit autofocus target before the\n // first DOM-order control (which is often the corner close button).\n if (panel.contains(document.activeElement)) return\n const preferred = panel.querySelector<HTMLElement>('[autofocus]')\n const first = panel.querySelector<HTMLElement>(focusablesSelector)\n ;(preferred ?? first ?? panel).focus()\n }, 0)\n function onKey(e: KeyboardEvent) {\n if (e.key === 'Escape') {\n if (document.querySelector('[data-ui-overlay]')) return\n onCloseRef.current()\n return\n }\n if (e.key !== 'Tab') return\n const panel = panelRef.current\n if (!panel) return\n const items = Array.from(panel.querySelectorAll<HTMLElement>(focusablesSelector)).filter(\n (el) => el.offsetParent !== null || el === document.activeElement,\n )\n if (items.length === 0) {\n e.preventDefault()\n panel.focus()\n return\n }\n const first = items[0]!\n const last = items[items.length - 1]!\n const activeEl = document.activeElement\n if (e.shiftKey && (activeEl === first || activeEl === panel)) {\n e.preventDefault()\n last.focus()\n } else if (!e.shiftKey && activeEl === last) {\n e.preventDefault()\n first.focus()\n }\n }\n document.addEventListener('keydown', onKey)\n return () => {\n document.body.style.overflow = prevOverflow\n window.clearTimeout(focusTimer)\n document.removeEventListener('keydown', onKey)\n if (previouslyFocused && document.contains(previouslyFocused)) previouslyFocused.focus()\n }\n }, [open])\n\n if (!mounted || typeof document === 'undefined') return null\n\n return createPortal(\n <AnimatePresence>\n {open ? (\n <div className=\"fixed inset-0 z-[65] flex items-center justify-center p-4\">\n <motion.div\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.15 }}\n className=\"absolute inset-0 bg-overlay/50 backdrop-blur-[2px]\"\n onClick={onClose}\n aria-hidden=\"true\"\n />\n <motion.div\n ref={panelRef}\n role=\"dialog\"\n aria-modal=\"true\"\n tabIndex={-1}\n initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.96, y: 8 }}\n animate={reduce ? { opacity: 1 } : { opacity: 1, scale: 1, y: 0 }}\n exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.96, y: 8 }}\n transition={{ type: 'spring', damping: 30, stiffness: 380, mass: 0.7 }}\n className={cn(\n 'relative flex w-full flex-col overflow-hidden rounded-xl border border-border bg-surface shadow-lg',\n SIZE[size],\n fullHeight && 'h-[min(92vh,780px)]',\n )}\n >\n {title || description ? (\n <div className=\"space-y-1 px-6 pt-6\">\n {title ? <h2 className=\"text-lg font-semibold text-fg\">{title}</h2> : null}\n {description ? <p className=\"text-sm text-fg-muted\">{description}</p> : null}\n </div>\n ) : null}\n {!hideClose ? (\n <button\n type=\"button\"\n onClick={onClose}\n aria-label={closeLabel}\n className=\"absolute right-4 top-4 rounded-md p-1.5 text-fg-muted transition-colors hover:bg-surface-hover hover:text-fg\"\n >\n <X className=\"size-4\" />\n </button>\n ) : null}\n {children ? (\n <div\n className={cn(\n 'text-sm text-fg',\n fullHeight ? 'flex min-h-0 flex-1 flex-col overflow-hidden' : 'px-6 py-5',\n )}\n >\n {children}\n </div>\n ) : null}\n {footer ? (\n <div className=\"flex items-center justify-end gap-2 border-t border-border bg-bg-subtle px-6 py-3\">\n {footer}\n </div>\n ) : null}\n </motion.div>\n </div>\n ) : null}\n </AnimatePresence>,\n document.body,\n )\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps the browser tab aligned with the title already rendered by a page.
|
|
3
|
+
*
|
|
4
|
+
* Applications can set `data-application-name` on the root `<html>` element
|
|
5
|
+
* to retain their brand in the tab title. The contract remains application-
|
|
6
|
+
* agnostic so existing page components can move to AppKit unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export declare function DocumentTitle({ title }: {
|
|
9
|
+
title: string;
|
|
10
|
+
}): null;
|
|
11
|
+
//# sourceMappingURL=document-title.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-title.d.ts","sourceRoot":"","sources":["../src/document-title.tsx"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,QAOzD"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Keeps the browser tab aligned with the title already rendered by a page.
|
|
5
|
+
*
|
|
6
|
+
* Applications can set `data-application-name` on the root `<html>` element
|
|
7
|
+
* to retain their brand in the tab title. The contract remains application-
|
|
8
|
+
* agnostic so existing page components can move to AppKit unchanged.
|
|
9
|
+
*/
|
|
10
|
+
export function DocumentTitle({ title }) {
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const applicationName = document.documentElement.dataset.applicationName;
|
|
13
|
+
document.title = applicationName ? `${title} · ${applicationName}` : title;
|
|
14
|
+
}, [title]);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=document-title.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-title.js","sourceRoot":"","sources":["../src/document-title.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;AAEZ,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEjC;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,EAAE,KAAK,EAAqB;IACxD,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,eAAe,GAAG,QAAQ,CAAC,eAAe,CAAC,OAAO,CAAC,eAAe,CAAA;QACxE,QAAQ,CAAC,KAAK,GAAG,eAAe,CAAC,CAAC,CAAC,GAAG,KAAK,MAAM,eAAe,EAAE,CAAC,CAAC,CAAC,KAAK,CAAA;IAC5E,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IAEX,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["'use client'\n\nimport { useEffect } from 'react'\n\n/**\n * Keeps the browser tab aligned with the title already rendered by a page.\n *\n * Applications can set `data-application-name` on the root `<html>` element\n * to retain their brand in the tab title. The contract remains application-\n * agnostic so existing page components can move to AppKit unchanged.\n */\nexport function DocumentTitle({ title }: { title: string }) {\n useEffect(() => {\n const applicationName = document.documentElement.dataset.applicationName\n document.title = applicationName ? `${title} · ${applicationName}` : title\n }, [title])\n\n return null\n}\n"]}
|
package/drawer.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export type DrawerSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
|
|
3
|
+
export type DrawerSide = 'left' | 'right';
|
|
4
|
+
export type DrawerText = {
|
|
5
|
+
close: string;
|
|
6
|
+
fullscreen: string;
|
|
7
|
+
exitFullscreen: string;
|
|
8
|
+
translate?: (key: string) => string;
|
|
9
|
+
};
|
|
10
|
+
export declare function DrawerTextProvider({ value, children, }: {
|
|
11
|
+
value: Partial<DrawerText>;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}): React.JSX.Element;
|
|
14
|
+
export type DrawerProps = {
|
|
15
|
+
open: boolean;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
title?: React.ReactNode;
|
|
18
|
+
description?: React.ReactNode;
|
|
19
|
+
size?: DrawerSize;
|
|
20
|
+
side?: DrawerSide;
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
footer?: React.ReactNode;
|
|
23
|
+
/** Primary action buttons pinned into the header, before the close button. */
|
|
24
|
+
headerActions?: React.ReactNode;
|
|
25
|
+
/** Record-detail navigation rendered directly below the title header. */
|
|
26
|
+
subtabs?: React.ReactNode;
|
|
27
|
+
/** Override the body wrapper classes (default: scroll + px-6 py-5). */
|
|
28
|
+
bodyClassName?: string;
|
|
29
|
+
/** Extra classes for the panel (e.g. record-type tinting). */
|
|
30
|
+
panelClassName?: string;
|
|
31
|
+
/** Raise above another open drawer in a deliberate nested flow. */
|
|
32
|
+
stacked?: boolean;
|
|
33
|
+
/** Open at full viewport width; the user can still collapse it. */
|
|
34
|
+
initialFullscreen?: boolean;
|
|
35
|
+
/** Hide the fullscreen expand/collapse affordance. */
|
|
36
|
+
disableFullscreen?: boolean;
|
|
37
|
+
/** Fires after the exit animation completes (UrlDrawer defers navigation). */
|
|
38
|
+
onExitComplete?: () => void;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Slide-in flyout for sub-entity create/edit and detail views. Portals to body;
|
|
42
|
+
* spring slide-in; backdrop fade+blur; Esc + click-out + ref-counted scroll
|
|
43
|
+
* lock; focus trap + restore; a NetSuite-style expand/collapse to full viewport
|
|
44
|
+
* width (animated via max-width). Fully tokenized and reduced-motion aware.
|
|
45
|
+
*/
|
|
46
|
+
export declare function Drawer({ open, onClose, title, description, size, side, children, footer, headerActions, subtabs, bodyClassName, panelClassName, stacked, initialFullscreen, disableFullscreen, onExitComplete, }: DrawerProps): React.ReactPortal | null;
|
|
47
|
+
/**
|
|
48
|
+
* Client-side navigate fn supplied by the host app (e.g. Next's `router.push`)
|
|
49
|
+
* so `UrlDrawer` can close by changing the URL — which re-runs the server
|
|
50
|
+
* component that owns the drawer's `open` state. Falls back to a hard nav.
|
|
51
|
+
*/
|
|
52
|
+
export declare const DrawerNavigateContext: React.Context<((href: string) => void) | null>;
|
|
53
|
+
export type UrlDrawerProps = Omit<DrawerProps, 'open' | 'onClose' | 'onExitComplete'> & {
|
|
54
|
+
open: boolean;
|
|
55
|
+
closeHref: string;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* URL-state drawer for server-rendered pages: `open` derives from a search
|
|
59
|
+
* param, so closing needs a real navigation (deferred until the slide-out
|
|
60
|
+
* finishes, so the exit animation always plays).
|
|
61
|
+
*/
|
|
62
|
+
export declare function UrlDrawer({ open, closeHref, children, ...rest }: UrlDrawerProps): React.JSX.Element;
|
|
63
|
+
//# sourceMappingURL=drawer.d.ts.map
|
package/drawer.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawer.d.ts","sourceRoot":"","sources":["../src/drawer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAmB9B,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,MAAM,CAAA;AACnE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,CAAA;AAgBzC,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;CACpC,CAAA;AAUD,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,CAAA;IAC1B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B,qBAGA;AAcD,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACxB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC/B,yEAAyE;IACzE,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,8DAA8D;IAC9D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,mEAAmE;IACnE,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,sDAAsD;IACtD,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;CAC5B,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,EACrB,IAAI,EACJ,OAAO,EACP,KAAK,EACL,WAAW,EACX,IAAW,EACX,IAAc,EACd,QAAQ,EACR,MAAM,EACN,aAAa,EACb,OAAO,EACP,aAAa,EACb,cAAc,EACd,OAAe,EACf,iBAAyB,EACzB,iBAAyB,EACzB,cAAc,GACf,EAAE,WAAW,4BAiQb;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,wBAA+B,MAAM,KAAK,IAAI,SAAe,CAAA;AAE/F,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,CAAC,GAAG;IACtF,IAAI,EAAE,OAAO,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,EAAE,cAAc,qBAgB/E"}
|
package/drawer.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
|
|
6
|
+
import { cn } from './utils.js';
|
|
7
|
+
const SIZE_CLASS = {
|
|
8
|
+
sm: 'w-full sm:max-w-md',
|
|
9
|
+
md: 'w-full sm:max-w-xl',
|
|
10
|
+
lg: 'w-full sm:max-w-2xl',
|
|
11
|
+
xl: 'w-full sm:max-w-4xl',
|
|
12
|
+
'2xl': 'w-full sm:max-w-6xl',
|
|
13
|
+
full: 'w-full',
|
|
14
|
+
};
|
|
15
|
+
const DEFAULT_DRAWER_TEXT = {
|
|
16
|
+
close: 'Close',
|
|
17
|
+
fullscreen: 'Fullscreen',
|
|
18
|
+
exitFullscreen: 'Exit fullscreen',
|
|
19
|
+
};
|
|
20
|
+
const DrawerTextContext = React.createContext(DEFAULT_DRAWER_TEXT);
|
|
21
|
+
export function DrawerTextProvider({ value, children, }) {
|
|
22
|
+
const merged = React.useMemo(() => ({ ...DEFAULT_DRAWER_TEXT, ...value }), [value]);
|
|
23
|
+
return _jsx(DrawerTextContext.Provider, { value: merged, children: children });
|
|
24
|
+
}
|
|
25
|
+
function useDrawerText() {
|
|
26
|
+
return React.useContext(DrawerTextContext);
|
|
27
|
+
}
|
|
28
|
+
// Ref-counted scroll lock — nested/stacked drawers share one lock so the body
|
|
29
|
+
// only unlocks when the last drawer closes.
|
|
30
|
+
let openDrawerCount = 0;
|
|
31
|
+
let originalBodyOverflow = null;
|
|
32
|
+
const FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
33
|
+
/**
|
|
34
|
+
* Slide-in flyout for sub-entity create/edit and detail views. Portals to body;
|
|
35
|
+
* spring slide-in; backdrop fade+blur; Esc + click-out + ref-counted scroll
|
|
36
|
+
* lock; focus trap + restore; a NetSuite-style expand/collapse to full viewport
|
|
37
|
+
* width (animated via max-width). Fully tokenized and reduced-motion aware.
|
|
38
|
+
*/
|
|
39
|
+
export function Drawer({ open, onClose, title, description, size = 'md', side = 'right', children, footer, headerActions, subtabs, bodyClassName, panelClassName, stacked = false, initialFullscreen = false, disableFullscreen = false, onExitComplete, }) {
|
|
40
|
+
const text = useDrawerText();
|
|
41
|
+
const tr = text.translate ?? ((s) => s);
|
|
42
|
+
const reduce = useReducedMotion();
|
|
43
|
+
const [mounted, setMounted] = React.useState(false);
|
|
44
|
+
React.useEffect(() => setMounted(true), []);
|
|
45
|
+
const [fullscreen, setFullscreen] = React.useState(initialFullscreen);
|
|
46
|
+
React.useEffect(() => {
|
|
47
|
+
if (!open)
|
|
48
|
+
setFullscreen(initialFullscreen);
|
|
49
|
+
}, [initialFullscreen, open]);
|
|
50
|
+
const panelRef = React.useRef(null);
|
|
51
|
+
const onCloseRef = React.useRef(onClose);
|
|
52
|
+
React.useEffect(() => {
|
|
53
|
+
onCloseRef.current = onClose;
|
|
54
|
+
}, [onClose]);
|
|
55
|
+
// Esc + scroll lock. Stacked-aware: a base drawer ignores Esc while a nested
|
|
56
|
+
// drawer or a floating overlay is on top.
|
|
57
|
+
React.useEffect(() => {
|
|
58
|
+
if (!open)
|
|
59
|
+
return;
|
|
60
|
+
function onKey(e) {
|
|
61
|
+
if (e.key !== 'Escape')
|
|
62
|
+
return;
|
|
63
|
+
if (document.querySelector('[data-ui-overlay]'))
|
|
64
|
+
return;
|
|
65
|
+
if (!stacked && document.querySelector('[data-drawer-layer="nested"]'))
|
|
66
|
+
return;
|
|
67
|
+
onCloseRef.current();
|
|
68
|
+
}
|
|
69
|
+
document.addEventListener('keydown', onKey);
|
|
70
|
+
if (openDrawerCount === 0)
|
|
71
|
+
originalBodyOverflow = document.body.style.overflow;
|
|
72
|
+
openDrawerCount += 1;
|
|
73
|
+
document.body.style.overflow = 'hidden';
|
|
74
|
+
return () => {
|
|
75
|
+
document.removeEventListener('keydown', onKey);
|
|
76
|
+
openDrawerCount = Math.max(0, openDrawerCount - 1);
|
|
77
|
+
if (openDrawerCount === 0) {
|
|
78
|
+
document.body.style.overflow = originalBodyOverflow ?? '';
|
|
79
|
+
originalBodyOverflow = null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}, [open, stacked]);
|
|
83
|
+
// Focus management: move focus in on open, trap Tab, restore on close.
|
|
84
|
+
React.useEffect(() => {
|
|
85
|
+
if (!open)
|
|
86
|
+
return;
|
|
87
|
+
const previouslyFocused = document.activeElement;
|
|
88
|
+
const focusTimer = window.setTimeout(() => {
|
|
89
|
+
if (!stacked && document.querySelector('[data-drawer-layer="nested"]'))
|
|
90
|
+
return;
|
|
91
|
+
const panel = panelRef.current;
|
|
92
|
+
if (!panel)
|
|
93
|
+
return;
|
|
94
|
+
if (panel.contains(document.activeElement))
|
|
95
|
+
return;
|
|
96
|
+
const preferred = panel.querySelector('[autofocus]');
|
|
97
|
+
const first = panel.querySelector(FOCUSABLE);
|
|
98
|
+
(preferred ?? first ?? panel).focus();
|
|
99
|
+
}, 0);
|
|
100
|
+
function onKeyDown(e) {
|
|
101
|
+
if (e.key !== 'Tab')
|
|
102
|
+
return;
|
|
103
|
+
if (!stacked && document.querySelector('[data-drawer-layer="nested"]'))
|
|
104
|
+
return;
|
|
105
|
+
const panel = panelRef.current;
|
|
106
|
+
if (!panel)
|
|
107
|
+
return;
|
|
108
|
+
const focusables = Array.from(panel.querySelectorAll(FOCUSABLE)).filter((el) => el.offsetParent !== null || el === document.activeElement);
|
|
109
|
+
if (focusables.length === 0) {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
panel.focus();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const firstEl = focusables[0];
|
|
115
|
+
const lastEl = focusables[focusables.length - 1];
|
|
116
|
+
const activeEl = document.activeElement;
|
|
117
|
+
if (e.shiftKey) {
|
|
118
|
+
if (activeEl === firstEl || activeEl === panel || !panel.contains(activeEl)) {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
lastEl.focus();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (activeEl === lastEl) {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
firstEl.focus();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
document.addEventListener('keydown', onKeyDown);
|
|
129
|
+
return () => {
|
|
130
|
+
window.clearTimeout(focusTimer);
|
|
131
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
132
|
+
if (previouslyFocused && document.contains(previouslyFocused))
|
|
133
|
+
previouslyFocused.focus();
|
|
134
|
+
};
|
|
135
|
+
}, [open, stacked]);
|
|
136
|
+
if (typeof document === 'undefined')
|
|
137
|
+
return null;
|
|
138
|
+
const offscreen = side === 'left' ? '-100%' : '100%';
|
|
139
|
+
const panelMotion = reduce
|
|
140
|
+
? {
|
|
141
|
+
initial: { opacity: 0 },
|
|
142
|
+
animate: { opacity: 1 },
|
|
143
|
+
exit: { opacity: 0 },
|
|
144
|
+
transition: { duration: 0.12 },
|
|
145
|
+
}
|
|
146
|
+
: {
|
|
147
|
+
initial: { x: offscreen },
|
|
148
|
+
animate: { x: 0 },
|
|
149
|
+
exit: { x: offscreen },
|
|
150
|
+
transition: { type: 'spring', damping: 32, stiffness: 320, mass: 0.8 },
|
|
151
|
+
};
|
|
152
|
+
return createPortal(_jsx(AnimatePresence, { onExitComplete: onExitComplete, children: mounted && open ? (_jsxs("div", { "data-drawer-layer": stacked ? 'nested' : 'base', className: cn('fixed inset-x-0 bottom-0 top-[var(--drawer-top,0px)]', stacked ? 'z-[55]' : 'z-50'),
|
|
153
|
+
// Under the View Transitions API (PageTransition), a route change
|
|
154
|
+
// paints the page snapshot in the browser's top layer — above any
|
|
155
|
+
// z-index. Naming the drawer gives it its own top-layer group,
|
|
156
|
+
// stacked over the page group, so the page can never flash across
|
|
157
|
+
// an open drawer mid-navigation.
|
|
158
|
+
style: { viewTransitionName: stacked ? 'appkit-drawer-nested' : 'appkit-drawer' }, children: [_jsx(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.15 }, className: "absolute inset-0 bg-overlay/40 backdrop-blur-[2px]", onClick: onClose, "aria-hidden": "true" }), _jsxs(motion.aside, { ref: panelRef, role: "dialog", "aria-modal": "true", tabIndex: -1, ...panelMotion, className: cn('absolute inset-y-0 flex flex-col overflow-hidden border-t border-border bg-surface shadow-lg transition-[max-width] duration-300 ease-in-out', side === 'left' ? 'left-0 border-r' : 'right-0 border-l',
|
|
159
|
+
// Full replacement (not additive): two sm:max-w-* utilities on one
|
|
160
|
+
// element resolve by stylesheet order, so stacking them no-ops.
|
|
161
|
+
fullscreen ? 'w-full sm:max-w-[100vw]' : SIZE_CLASS[size], panelClassName), children: [title || description || headerActions ? (_jsxs("header", { className: "flex items-center justify-between gap-4 border-b border-border px-6 py-4", children: [_jsxs("div", { className: "min-w-0 space-y-0.5", children: [title ? (_jsx("h2", { className: "truncate text-base font-semibold text-fg", children: typeof title === 'string' ? tr(title) : title })) : null, description ? (_jsx("p", { className: "text-sm text-fg-muted", children: typeof description === 'string' ? tr(description) : description })) : null] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [headerActions ? (_jsx("div", { className: "flex flex-wrap items-center justify-end gap-2", children: headerActions })) : null, !disableFullscreen ? (_jsx("button", { type: "button", onClick: () => setFullscreen((f) => !f), className: "hidden rounded-md p-1.5 text-fg-muted transition-colors hover:bg-surface-hover hover:text-fg sm:block", "aria-label": fullscreen ? text.exitFullscreen : text.fullscreen, title: fullscreen ? text.exitFullscreen : text.fullscreen, children: fullscreen ? (_jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("polyline", { points: "4 14 10 14 10 20" }), _jsx("polyline", { points: "20 10 14 10 14 4" }), _jsx("line", { x1: "14", y1: "10", x2: "21", y2: "3" }), _jsx("line", { x1: "3", y1: "21", x2: "10", y2: "14" })] })) : (_jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("polyline", { points: "15 3 21 3 21 9" }), _jsx("polyline", { points: "9 21 3 21 3 15" }), _jsx("line", { x1: "21", y1: "3", x2: "14", y2: "10" }), _jsx("line", { x1: "3", y1: "21", x2: "10", y2: "14" })] })) })) : null, _jsx("button", { type: "button", onClick: onClose, className: "rounded-md p-1.5 text-fg-muted transition-colors hover:bg-surface-hover hover:text-fg", "aria-label": text.close, children: _jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })] })] })) : null, subtabs ? (_jsx("div", { className: "shrink-0 border-b border-border bg-surface px-6", children: subtabs })) : null, _jsx("div", { className: cn('app-scroll min-h-0 flex-1 text-fg', bodyClassName ?? 'overflow-y-auto px-6 py-5'), children: children }), footer ? (_jsx("footer", { className: "flex items-center justify-end gap-2 border-t border-border bg-bg-subtle px-6 py-3", children: footer })) : null] })] }, "drawer")) : null }), document.body);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Client-side navigate fn supplied by the host app (e.g. Next's `router.push`)
|
|
165
|
+
* so `UrlDrawer` can close by changing the URL — which re-runs the server
|
|
166
|
+
* component that owns the drawer's `open` state. Falls back to a hard nav.
|
|
167
|
+
*/
|
|
168
|
+
export const DrawerNavigateContext = React.createContext(null);
|
|
169
|
+
/**
|
|
170
|
+
* URL-state drawer for server-rendered pages: `open` derives from a search
|
|
171
|
+
* param, so closing needs a real navigation (deferred until the slide-out
|
|
172
|
+
* finishes, so the exit animation always plays).
|
|
173
|
+
*/
|
|
174
|
+
export function UrlDrawer({ open, closeHref, children, ...rest }) {
|
|
175
|
+
const navigate = React.useContext(DrawerNavigateContext);
|
|
176
|
+
const [show, setShow] = React.useState(open);
|
|
177
|
+
React.useEffect(() => setShow(open), [open]);
|
|
178
|
+
function afterExit() {
|
|
179
|
+
if (typeof window === 'undefined')
|
|
180
|
+
return;
|
|
181
|
+
if (navigate)
|
|
182
|
+
navigate(closeHref);
|
|
183
|
+
else
|
|
184
|
+
window.location.assign(closeHref);
|
|
185
|
+
}
|
|
186
|
+
return (_jsx(Drawer, { open: show, onClose: () => setShow(false), onExitComplete: afterExit, ...rest, children: children }));
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=drawer.js.map
|