@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.
Files changed (300) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +11 -0
  3. package/account-menu.d.ts +49 -0
  4. package/account-menu.d.ts.map +1 -0
  5. package/account-menu.js +66 -0
  6. package/account-menu.js.map +1 -0
  7. package/alert.d.ts +13 -0
  8. package/alert.d.ts.map +1 -0
  9. package/alert.js +22 -0
  10. package/alert.js.map +1 -0
  11. package/animated-number.d.ts +13 -0
  12. package/animated-number.d.ts.map +1 -0
  13. package/animated-number.js +53 -0
  14. package/animated-number.js.map +1 -0
  15. package/api-reference.d.ts +44 -0
  16. package/api-reference.d.ts.map +1 -0
  17. package/api-reference.js +82 -0
  18. package/api-reference.js.map +1 -0
  19. package/app-shell.d.ts +39 -0
  20. package/app-shell.d.ts.map +1 -0
  21. package/app-shell.js +21 -0
  22. package/app-shell.js.map +1 -0
  23. package/app-sidebar.d.ts +22 -0
  24. package/app-sidebar.d.ts.map +1 -0
  25. package/app-sidebar.js +30 -0
  26. package/app-sidebar.js.map +1 -0
  27. package/avatar.d.ts +9 -0
  28. package/avatar.d.ts.map +1 -0
  29. package/avatar.js +18 -0
  30. package/avatar.js.map +1 -0
  31. package/badge.d.ts +9 -0
  32. package/badge.d.ts.map +1 -0
  33. package/badge.js +23 -0
  34. package/badge.js.map +1 -0
  35. package/button.d.ts +24 -0
  36. package/button.d.ts.map +1 -0
  37. package/button.js +50 -0
  38. package/button.js.map +1 -0
  39. package/card.d.ts +21 -0
  40. package/card.d.ts.map +1 -0
  41. package/card.js +36 -0
  42. package/card.js.map +1 -0
  43. package/checkbox.d.ts +4 -0
  44. package/checkbox.d.ts.map +1 -0
  45. package/checkbox.js +6 -0
  46. package/checkbox.js.map +1 -0
  47. package/confirm-dialog.d.ts +23 -0
  48. package/confirm-dialog.d.ts.map +1 -0
  49. package/confirm-dialog.js +67 -0
  50. package/confirm-dialog.js.map +1 -0
  51. package/context-menu.d.ts +47 -0
  52. package/context-menu.d.ts.map +1 -0
  53. package/context-menu.js +90 -0
  54. package/context-menu.js.map +1 -0
  55. package/dialog.d.ts +25 -0
  56. package/dialog.d.ts.map +1 -0
  57. package/dialog.js +89 -0
  58. package/dialog.js.map +1 -0
  59. package/document-title.d.ts +11 -0
  60. package/document-title.d.ts.map +1 -0
  61. package/document-title.js +17 -0
  62. package/document-title.js.map +1 -0
  63. package/drawer.d.ts +63 -0
  64. package/drawer.d.ts.map +1 -0
  65. package/drawer.js +188 -0
  66. package/drawer.js.map +1 -0
  67. package/empty-state.d.ts +10 -0
  68. package/empty-state.d.ts.map +1 -0
  69. package/empty-state.js +7 -0
  70. package/empty-state.js.map +1 -0
  71. package/file-uploader.d.ts +67 -0
  72. package/file-uploader.d.ts.map +1 -0
  73. package/file-uploader.js +156 -0
  74. package/file-uploader.js.map +1 -0
  75. package/filter-chips.d.ts +47 -0
  76. package/filter-chips.d.ts.map +1 -0
  77. package/filter-chips.js +62 -0
  78. package/filter-chips.js.map +1 -0
  79. package/global-search.d.ts +41 -0
  80. package/global-search.d.ts.map +1 -0
  81. package/global-search.js +116 -0
  82. package/global-search.js.map +1 -0
  83. package/index.d.ts +70 -0
  84. package/index.d.ts.map +1 -0
  85. package/index.js +70 -0
  86. package/index.js.map +1 -0
  87. package/input.d.ts +9 -0
  88. package/input.d.ts.map +1 -0
  89. package/input.js +11 -0
  90. package/input.js.map +1 -0
  91. package/label.d.ts +3 -0
  92. package/label.d.ts.map +1 -0
  93. package/label.js +6 -0
  94. package/label.js.map +1 -0
  95. package/line-grid.d.ts +48 -0
  96. package/line-grid.d.ts.map +1 -0
  97. package/line-grid.js +155 -0
  98. package/line-grid.js.map +1 -0
  99. package/link-context.d.ts +44 -0
  100. package/link-context.d.ts.map +1 -0
  101. package/link-context.js +32 -0
  102. package/link-context.js.map +1 -0
  103. package/list-nav.d.ts +28 -0
  104. package/list-nav.d.ts.map +1 -0
  105. package/list-nav.js +41 -0
  106. package/list-nav.js.map +1 -0
  107. package/list-pagination.d.ts +27 -0
  108. package/list-pagination.d.ts.map +1 -0
  109. package/list-pagination.js +43 -0
  110. package/list-pagination.js.map +1 -0
  111. package/list-params.d.ts +41 -0
  112. package/list-params.d.ts.map +1 -0
  113. package/list-params.js +84 -0
  114. package/list-params.js.map +1 -0
  115. package/list-search-input-state.d.ts +13 -0
  116. package/list-search-input-state.d.ts.map +1 -0
  117. package/list-search-input-state.js +27 -0
  118. package/list-search-input-state.js.map +1 -0
  119. package/list-search-input.d.ts +16 -0
  120. package/list-search-input.d.ts.map +1 -0
  121. package/list-search-input.js +51 -0
  122. package/list-search-input.js.map +1 -0
  123. package/mobile-tab-bar.d.ts +14 -0
  124. package/mobile-tab-bar.d.ts.map +1 -0
  125. package/mobile-tab-bar.js +25 -0
  126. package/mobile-tab-bar.js.map +1 -0
  127. package/multi-select.d.ts +32 -0
  128. package/multi-select.d.ts.map +1 -0
  129. package/multi-select.js +52 -0
  130. package/multi-select.js.map +1 -0
  131. package/nav-overflow.d.ts +12 -0
  132. package/nav-overflow.d.ts.map +1 -0
  133. package/nav-overflow.js +30 -0
  134. package/nav-overflow.js.map +1 -0
  135. package/navigation-config-editor.d.ts +26 -0
  136. package/navigation-config-editor.d.ts.map +1 -0
  137. package/navigation-config-editor.js +57 -0
  138. package/navigation-config-editor.js.map +1 -0
  139. package/navigation-config.d.ts +48 -0
  140. package/navigation-config.d.ts.map +1 -0
  141. package/navigation-config.js +75 -0
  142. package/navigation-config.js.map +1 -0
  143. package/navigation-mode.d.ts +16 -0
  144. package/navigation-mode.d.ts.map +1 -0
  145. package/navigation-mode.js +30 -0
  146. package/navigation-mode.js.map +1 -0
  147. package/notifications-bell.d.ts +22 -0
  148. package/notifications-bell.d.ts.map +1 -0
  149. package/notifications-bell.js +30 -0
  150. package/notifications-bell.js.map +1 -0
  151. package/org-chart.d.ts +85 -0
  152. package/org-chart.d.ts.map +1 -0
  153. package/org-chart.js +193 -0
  154. package/org-chart.js.map +1 -0
  155. package/package.json +61 -0
  156. package/page-header.d.ts +32 -0
  157. package/page-header.d.ts.map +1 -0
  158. package/page-header.js +14 -0
  159. package/page-header.js.map +1 -0
  160. package/page-layout.d.ts +29 -0
  161. package/page-layout.d.ts.map +1 -0
  162. package/page-layout.js +23 -0
  163. package/page-layout.js.map +1 -0
  164. package/page-transition.d.ts +13 -0
  165. package/page-transition.d.ts.map +1 -0
  166. package/page-transition.js +14 -0
  167. package/page-transition.js.map +1 -0
  168. package/paged-table.d.ts +54 -0
  169. package/paged-table.d.ts.map +1 -0
  170. package/paged-table.js +74 -0
  171. package/paged-table.js.map +1 -0
  172. package/popover.d.ts +16 -0
  173. package/popover.d.ts.map +1 -0
  174. package/popover.js +99 -0
  175. package/popover.js.map +1 -0
  176. package/progress.d.ts +7 -0
  177. package/progress.d.ts.map +1 -0
  178. package/progress.js +8 -0
  179. package/progress.js.map +1 -0
  180. package/prompt-dialog.d.ts +21 -0
  181. package/prompt-dialog.d.ts.map +1 -0
  182. package/prompt-dialog.js +109 -0
  183. package/prompt-dialog.js.map +1 -0
  184. package/record-list.d.ts +76 -0
  185. package/record-list.d.ts.map +1 -0
  186. package/record-list.js +51 -0
  187. package/record-list.js.map +1 -0
  188. package/search-select.d.ts +42 -0
  189. package/search-select.d.ts.map +1 -0
  190. package/search-select.js +194 -0
  191. package/search-select.js.map +1 -0
  192. package/select-options.d.ts +16 -0
  193. package/select-options.d.ts.map +1 -0
  194. package/select-options.js +119 -0
  195. package/select-options.js.map +1 -0
  196. package/select.d.ts +24 -0
  197. package/select.d.ts.map +1 -0
  198. package/select.js +65 -0
  199. package/select.js.map +1 -0
  200. package/separator.d.ts +6 -0
  201. package/separator.d.ts.map +1 -0
  202. package/separator.js +7 -0
  203. package/separator.js.map +1 -0
  204. package/settings-layout.d.ts +79 -0
  205. package/settings-layout.d.ts.map +1 -0
  206. package/settings-layout.js +74 -0
  207. package/settings-layout.js.map +1 -0
  208. package/sidebar-nav.d.ts +67 -0
  209. package/sidebar-nav.d.ts.map +1 -0
  210. package/sidebar-nav.js +291 -0
  211. package/sidebar-nav.js.map +1 -0
  212. package/signature-pad.d.ts +16 -0
  213. package/signature-pad.d.ts.map +1 -0
  214. package/signature-pad.js +355 -0
  215. package/signature-pad.js.map +1 -0
  216. package/skeleton.d.ts +9 -0
  217. package/skeleton.d.ts.map +1 -0
  218. package/skeleton.js +12 -0
  219. package/skeleton.js.map +1 -0
  220. package/slider.d.ts +13 -0
  221. package/slider.d.ts.map +1 -0
  222. package/slider.js +15 -0
  223. package/slider.js.map +1 -0
  224. package/sortable-th.d.ts +28 -0
  225. package/sortable-th.d.ts.map +1 -0
  226. package/sortable-th.js +35 -0
  227. package/sortable-th.js.map +1 -0
  228. package/sparkline.d.ts +16 -0
  229. package/sparkline.d.ts.map +1 -0
  230. package/sparkline.js +46 -0
  231. package/sparkline.js.map +1 -0
  232. package/spinner.d.ts +11 -0
  233. package/spinner.d.ts.map +1 -0
  234. package/spinner.js +11 -0
  235. package/spinner.js.map +1 -0
  236. package/styles.css +166 -0
  237. package/subtab-nav.d.ts +17 -0
  238. package/subtab-nav.d.ts.map +1 -0
  239. package/subtab-nav.js +11 -0
  240. package/subtab-nav.js.map +1 -0
  241. package/switch.d.ts +9 -0
  242. package/switch.d.ts.map +1 -0
  243. package/switch.js +11 -0
  244. package/switch.js.map +1 -0
  245. package/tab-content.d.ts +24 -0
  246. package/tab-content.d.ts.map +1 -0
  247. package/tab-content.js +26 -0
  248. package/tab-content.js.map +1 -0
  249. package/table.d.ts +28 -0
  250. package/table.d.ts.map +1 -0
  251. package/table.js +21 -0
  252. package/table.js.map +1 -0
  253. package/tabs.d.ts +18 -0
  254. package/tabs.d.ts.map +1 -0
  255. package/tabs.js +18 -0
  256. package/tabs.js.map +1 -0
  257. package/text-context.d.ts +9 -0
  258. package/text-context.d.ts.map +1 -0
  259. package/text-context.js +12 -0
  260. package/text-context.js.map +1 -0
  261. package/textarea.d.ts +4 -0
  262. package/textarea.d.ts.map +1 -0
  263. package/textarea.js +6 -0
  264. package/textarea.js.map +1 -0
  265. package/theme-script.d.ts +3 -0
  266. package/theme-script.d.ts.map +1 -0
  267. package/theme-script.js +6 -0
  268. package/theme-script.js.map +1 -0
  269. package/theme.d.ts +28 -0
  270. package/theme.d.ts.map +1 -0
  271. package/theme.js +92 -0
  272. package/theme.js.map +1 -0
  273. package/toast.d.ts +52 -0
  274. package/toast.d.ts.map +1 -0
  275. package/toast.js +178 -0
  276. package/toast.js.map +1 -0
  277. package/tooltip.d.ts +12 -0
  278. package/tooltip.d.ts.map +1 -0
  279. package/tooltip.js +14 -0
  280. package/tooltip.js.map +1 -0
  281. package/top-nav.d.ts +18 -0
  282. package/top-nav.d.ts.map +1 -0
  283. package/top-nav.js +160 -0
  284. package/top-nav.js.map +1 -0
  285. package/upload-limits.d.ts +4 -0
  286. package/upload-limits.d.ts.map +1 -0
  287. package/upload-limits.js +21 -0
  288. package/upload-limits.js.map +1 -0
  289. package/use-element-size.d.ts +7 -0
  290. package/use-element-size.d.ts.map +1 -0
  291. package/use-element-size.js +21 -0
  292. package/use-element-size.js.map +1 -0
  293. package/utils.d.ts +4 -0
  294. package/utils.d.ts.map +1 -0
  295. package/utils.js +7 -0
  296. package/utils.js.map +1 -0
  297. package/workspace-launcher.d.ts +50 -0
  298. package/workspace-launcher.d.ts.map +1 -0
  299. package/workspace-launcher.js +127 -0
  300. package/workspace-launcher.js.map +1 -0
@@ -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
@@ -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
@@ -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