tempest-react-sdk 0.66.0 → 0.68.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 (232) hide show
  1. package/README.md +97 -93
  2. package/dist/auth/create-tempest-auth.cjs +1 -1
  3. package/dist/auth/create-tempest-auth.cjs.map +1 -1
  4. package/dist/auth/create-tempest-auth.js +41 -40
  5. package/dist/auth/create-tempest-auth.js.map +1 -1
  6. package/dist/components/BottomSheet/BottomSheet.cjs +1 -1
  7. package/dist/components/BottomSheet/BottomSheet.cjs.map +1 -1
  8. package/dist/components/BottomSheet/BottomSheet.js +35 -40
  9. package/dist/components/BottomSheet/BottomSheet.js.map +1 -1
  10. package/dist/components/Card/Card.module.cjs.map +1 -1
  11. package/dist/components/Card/Card.module.js.map +1 -1
  12. package/dist/components/Combobox/Combobox.cjs +1 -1
  13. package/dist/components/Combobox/Combobox.cjs.map +1 -1
  14. package/dist/components/Combobox/Combobox.js +96 -74
  15. package/dist/components/Combobox/Combobox.js.map +1 -1
  16. package/dist/components/Combobox/Combobox.module.cjs +1 -1
  17. package/dist/components/Combobox/Combobox.module.cjs.map +1 -1
  18. package/dist/components/Combobox/Combobox.module.js +4 -3
  19. package/dist/components/Combobox/Combobox.module.js.map +1 -1
  20. package/dist/components/Command/Command.cjs +1 -1
  21. package/dist/components/Command/Command.js +4 -4
  22. package/dist/components/ContextMenu/ContextMenu.cjs +1 -1
  23. package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
  24. package/dist/components/ContextMenu/ContextMenu.js +80 -83
  25. package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
  26. package/dist/components/DataTable/DataTable.cjs +1 -1
  27. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  28. package/dist/components/DataTable/DataTable.js +138 -127
  29. package/dist/components/DataTable/DataTable.js.map +1 -1
  30. package/dist/components/DataTable/use-dev-warnings.cjs +1 -1
  31. package/dist/components/DataTable/use-dev-warnings.cjs.map +1 -1
  32. package/dist/components/DataTable/use-dev-warnings.js +5 -3
  33. package/dist/components/DataTable/use-dev-warnings.js.map +1 -1
  34. package/dist/components/Drawer/Drawer.cjs +1 -1
  35. package/dist/components/Drawer/Drawer.cjs.map +1 -1
  36. package/dist/components/Drawer/Drawer.js +46 -46
  37. package/dist/components/Drawer/Drawer.js.map +1 -1
  38. package/dist/components/DropdownMenu/DropdownMenu.cjs +1 -1
  39. package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
  40. package/dist/components/DropdownMenu/DropdownMenu.js +129 -105
  41. package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
  42. package/dist/components/DropdownMenu/DropdownMenu.module.cjs +1 -1
  43. package/dist/components/DropdownMenu/DropdownMenu.module.cjs.map +1 -1
  44. package/dist/components/DropdownMenu/DropdownMenu.module.js +12 -11
  45. package/dist/components/DropdownMenu/DropdownMenu.module.js.map +1 -1
  46. package/dist/components/FilterPanel/FilterPanel.cjs +2 -0
  47. package/dist/components/FilterPanel/FilterPanel.cjs.map +1 -0
  48. package/dist/components/FilterPanel/FilterPanel.js +75 -0
  49. package/dist/components/FilterPanel/FilterPanel.js.map +1 -0
  50. package/dist/components/FilterPanel/FilterPanel.module.cjs +3 -0
  51. package/dist/components/FilterPanel/FilterPanel.module.cjs.map +1 -0
  52. package/dist/components/FilterPanel/FilterPanel.module.js +12 -0
  53. package/dist/components/FilterPanel/FilterPanel.module.js.map +1 -0
  54. package/dist/components/HoverCard/HoverCard.cjs +1 -1
  55. package/dist/components/HoverCard/HoverCard.cjs.map +1 -1
  56. package/dist/components/HoverCard/HoverCard.js +54 -29
  57. package/dist/components/HoverCard/HoverCard.js.map +1 -1
  58. package/dist/components/HoverCard/HoverCard.module.cjs +1 -1
  59. package/dist/components/HoverCard/HoverCard.module.cjs.map +1 -1
  60. package/dist/components/HoverCard/HoverCard.module.js +4 -3
  61. package/dist/components/HoverCard/HoverCard.module.js.map +1 -1
  62. package/dist/components/Lightbox/Lightbox.cjs +1 -1
  63. package/dist/components/Lightbox/Lightbox.cjs.map +1 -1
  64. package/dist/components/Lightbox/Lightbox.js +84 -87
  65. package/dist/components/Lightbox/Lightbox.js.map +1 -1
  66. package/dist/components/Menubar/Menubar.cjs +1 -1
  67. package/dist/components/Menubar/Menubar.cjs.map +1 -1
  68. package/dist/components/Menubar/Menubar.js +86 -65
  69. package/dist/components/Menubar/Menubar.js.map +1 -1
  70. package/dist/components/Menubar/Menubar.module.cjs +1 -1
  71. package/dist/components/Menubar/Menubar.module.cjs.map +1 -1
  72. package/dist/components/Menubar/Menubar.module.js +4 -3
  73. package/dist/components/Menubar/Menubar.module.js.map +1 -1
  74. package/dist/components/Modal/Modal.cjs +1 -1
  75. package/dist/components/Modal/Modal.cjs.map +1 -1
  76. package/dist/components/Modal/Modal.js +47 -51
  77. package/dist/components/Modal/Modal.js.map +1 -1
  78. package/dist/components/MultiSelect/MultiSelect.cjs +1 -1
  79. package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
  80. package/dist/components/MultiSelect/MultiSelect.js +113 -96
  81. package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
  82. package/dist/components/MultiSelect/MultiSelect.module.cjs +1 -1
  83. package/dist/components/MultiSelect/MultiSelect.module.cjs.map +1 -1
  84. package/dist/components/MultiSelect/MultiSelect.module.js +4 -3
  85. package/dist/components/MultiSelect/MultiSelect.module.js.map +1 -1
  86. package/dist/components/NavigationMenu/NavigationMenu.cjs +1 -1
  87. package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
  88. package/dist/components/NavigationMenu/NavigationMenu.js +83 -61
  89. package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
  90. package/dist/components/NavigationMenu/NavigationMenu.module.cjs +1 -1
  91. package/dist/components/NavigationMenu/NavigationMenu.module.cjs.map +1 -1
  92. package/dist/components/NavigationMenu/NavigationMenu.module.js +4 -3
  93. package/dist/components/NavigationMenu/NavigationMenu.module.js.map +1 -1
  94. package/dist/components/Pagination/Pagination.cjs +1 -1
  95. package/dist/components/Pagination/Pagination.cjs.map +1 -1
  96. package/dist/components/Pagination/Pagination.js +31 -30
  97. package/dist/components/Pagination/Pagination.js.map +1 -1
  98. package/dist/components/Popover/Popover.cjs +1 -1
  99. package/dist/components/Popover/Popover.cjs.map +1 -1
  100. package/dist/components/Popover/Popover.js +57 -36
  101. package/dist/components/Popover/Popover.js.map +1 -1
  102. package/dist/components/Popover/Popover.module.cjs +1 -1
  103. package/dist/components/Popover/Popover.module.cjs.map +1 -1
  104. package/dist/components/Popover/Popover.module.js +7 -6
  105. package/dist/components/Popover/Popover.module.js.map +1 -1
  106. package/dist/components/Portal/anchor-position.cjs +2 -0
  107. package/dist/components/Portal/anchor-position.cjs.map +1 -0
  108. package/dist/components/Portal/anchor-position.js +99 -0
  109. package/dist/components/Portal/anchor-position.js.map +1 -0
  110. package/dist/components/Portal/escape-layer.cjs +2 -0
  111. package/dist/components/Portal/escape-layer.cjs.map +1 -0
  112. package/dist/components/Portal/escape-layer.js +30 -0
  113. package/dist/components/Portal/escape-layer.js.map +1 -0
  114. package/dist/components/Portal/tab-bridge.cjs +2 -0
  115. package/dist/components/Portal/tab-bridge.cjs.map +1 -0
  116. package/dist/components/Portal/tab-bridge.js +37 -0
  117. package/dist/components/Portal/tab-bridge.js.map +1 -0
  118. package/dist/components/Select/CaretIcon.cjs +2 -0
  119. package/dist/components/Select/CaretIcon.cjs.map +1 -0
  120. package/dist/components/Select/CaretIcon.js +21 -0
  121. package/dist/components/Select/CaretIcon.js.map +1 -0
  122. package/dist/components/Select/Select.cjs +1 -1
  123. package/dist/components/Select/Select.cjs.map +1 -1
  124. package/dist/components/Select/Select.js +33 -47
  125. package/dist/components/Select/Select.js.map +1 -1
  126. package/dist/components/Select/Select.module.cjs +1 -1
  127. package/dist/components/Select/Select.module.cjs.map +1 -1
  128. package/dist/components/Select/Select.module.js +4 -3
  129. package/dist/components/Select/Select.module.js.map +1 -1
  130. package/dist/components/Select/options.cjs +2 -0
  131. package/dist/components/Select/options.cjs.map +1 -0
  132. package/dist/components/Select/options.js +25 -0
  133. package/dist/components/Select/options.js.map +1 -0
  134. package/dist/components/Sidebar/Sidebar.cjs +1 -1
  135. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  136. package/dist/components/Sidebar/Sidebar.js +49 -43
  137. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  138. package/dist/components/Sidebar/active-nav-key.cjs +2 -0
  139. package/dist/components/Sidebar/active-nav-key.cjs.map +1 -0
  140. package/dist/components/Sidebar/active-nav-key.js +22 -0
  141. package/dist/components/Sidebar/active-nav-key.js.map +1 -0
  142. package/dist/components/Tooltip/Tooltip.cjs +1 -1
  143. package/dist/components/Tooltip/Tooltip.cjs.map +1 -1
  144. package/dist/components/Tooltip/Tooltip.js +43 -29
  145. package/dist/components/Tooltip/Tooltip.js.map +1 -1
  146. package/dist/components/Tooltip/Tooltip.module.cjs +1 -1
  147. package/dist/components/Tooltip/Tooltip.module.cjs.map +1 -1
  148. package/dist/components/Tooltip/Tooltip.module.js +4 -3
  149. package/dist/components/Tooltip/Tooltip.module.js.map +1 -1
  150. package/dist/hooks/tab-sequence.cjs +2 -0
  151. package/dist/hooks/tab-sequence.cjs.map +1 -0
  152. package/dist/hooks/tab-sequence.js +53 -0
  153. package/dist/hooks/tab-sequence.js.map +1 -0
  154. package/dist/hooks/use-deep-memo.cjs +1 -1
  155. package/dist/hooks/use-deep-memo.cjs.map +1 -1
  156. package/dist/hooks/use-deep-memo.js +1 -1
  157. package/dist/hooks/use-deep-memo.js.map +1 -1
  158. package/dist/hooks/use-draft-filters.cjs +2 -0
  159. package/dist/hooks/use-draft-filters.cjs.map +1 -0
  160. package/dist/hooks/use-draft-filters.js +49 -0
  161. package/dist/hooks/use-draft-filters.js.map +1 -0
  162. package/dist/hooks/use-focus-trap.cjs +1 -1
  163. package/dist/hooks/use-focus-trap.cjs.map +1 -1
  164. package/dist/hooks/use-focus-trap.js +31 -34
  165. package/dist/hooks/use-focus-trap.js.map +1 -1
  166. package/dist/hooks/use-sortable.cjs +1 -1
  167. package/dist/hooks/use-sortable.cjs.map +1 -1
  168. package/dist/hooks/use-sortable.js +3 -3
  169. package/dist/hooks/use-sortable.js.map +1 -1
  170. package/dist/http/api-client.cjs +1 -1
  171. package/dist/http/api-client.cjs.map +1 -1
  172. package/dist/http/api-client.js +92 -80
  173. package/dist/http/api-client.js.map +1 -1
  174. package/dist/http/credential-scope.cjs +1 -1
  175. package/dist/http/credential-scope.cjs.map +1 -1
  176. package/dist/http/credential-scope.js +5 -3
  177. package/dist/http/credential-scope.js.map +1 -1
  178. package/dist/http/csrf.cjs +2 -0
  179. package/dist/http/csrf.cjs.map +1 -0
  180. package/dist/http/csrf.js +46 -0
  181. package/dist/http/csrf.js.map +1 -0
  182. package/dist/http/describe-api-error.cjs +1 -1
  183. package/dist/http/describe-api-error.cjs.map +1 -1
  184. package/dist/http/describe-api-error.js +4 -2
  185. package/dist/http/describe-api-error.js.map +1 -1
  186. package/dist/http/resumable-upload.cjs +1 -1
  187. package/dist/http/resumable-upload.cjs.map +1 -1
  188. package/dist/http/resumable-upload.js +131 -123
  189. package/dist/http/resumable-upload.js.map +1 -1
  190. package/dist/http/upload-with-progress.cjs +1 -1
  191. package/dist/http/upload-with-progress.cjs.map +1 -1
  192. package/dist/http/upload-with-progress.js +41 -33
  193. package/dist/http/upload-with-progress.js.map +1 -1
  194. package/dist/query/use-paginated-query.cjs +1 -1
  195. package/dist/query/use-paginated-query.cjs.map +1 -1
  196. package/dist/query/use-paginated-query.js +47 -30
  197. package/dist/query/use-paginated-query.js.map +1 -1
  198. package/dist/styles/FilterPanel.css +2 -0
  199. package/dist/styles/actions.css +3 -0
  200. package/dist/styles/advanced.css +1 -0
  201. package/dist/styles/component/Card.css +1 -1
  202. package/dist/styles/component/Combobox.css +5 -3
  203. package/dist/styles/component/DropdownMenu.css +1 -0
  204. package/dist/styles/component/FilterPanel.css +5 -0
  205. package/dist/styles/component/HoverCard.css +1 -0
  206. package/dist/styles/component/Menubar.css +1 -0
  207. package/dist/styles/component/MultiSelect.css +1 -0
  208. package/dist/styles/component/NavigationMenu.css +1 -0
  209. package/dist/styles/component/Popover.css +1 -0
  210. package/dist/styles/component/Select.css +13 -6
  211. package/dist/styles/component/Tooltip.css +1 -0
  212. package/dist/styles/core.css +4 -4
  213. package/dist/styles/data.css +4 -0
  214. package/dist/styles/forms.css +19 -9
  215. package/dist/styles/identity.css +1 -1
  216. package/dist/styles/manifest.json +6 -0
  217. package/dist/styles/navigation.css +2 -0
  218. package/dist/styles/tokens.css +4 -4
  219. package/dist/styles.css +1 -1
  220. package/dist/tempest-react-sdk.cjs +1 -1
  221. package/dist/tempest-react-sdk.d.ts +805 -35
  222. package/dist/tempest-react-sdk.js +347 -342
  223. package/dist/vite/tempest-csp.cjs +3 -0
  224. package/dist/vite/tempest-csp.cjs.map +1 -0
  225. package/dist/vite/tempest-csp.js +189 -0
  226. package/dist/vite/tempest-csp.js.map +1 -0
  227. package/dist/vite.cjs +1 -1
  228. package/dist/vite.d.ts +145 -0
  229. package/dist/vite.js +2 -1
  230. package/package.json +2 -2
  231. package/template/vite.config.ts +8 -1
  232. package/template-pwa/vite.config.ts +6 -0
@@ -1,9 +1,9 @@
1
1
  import { isDevBuild as e } from "../../utils/dev-mode.js";
2
2
  import { useEffect as t } from "react";
3
3
  //#region src/components/DataTable/use-dev-warnings.ts
4
- function n({ serverMode: n, controlledPage: r, onPageChange: i, sortIsManual: a, hasSortableColumn: o, onSortChange: s, searchable: c, onSearchChange: l }) {
4
+ function n({ serverMode: n, controlledPage: r, onPageChange: i, pageSizeControlled: a, onPageSizeChange: o, sortIsManual: s, hasSortableColumn: c, onSortChange: l, searchable: u, onSearchChange: d }) {
5
5
  t(() => {
6
- e() && (n && r === void 0 && console.warn("[tempest] <DataTable totalItems> is server mode, which needs a controlled `page`. Without it the pager moves the internal page while `data` keeps showing page 1."), r !== void 0 && !i && console.warn("[tempest] <DataTable page> is controlled but `onPageChange` is missing, so the pager cannot do anything."), n && c && !l && console.warn("[tempest] <DataTable totalItems searchable> delegates searching, so the box needs `onSearchChange`. Without it the user types and nothing filters and nothing is reported."), a && o && !s && console.warn("[tempest] <DataTable> is sorting manually but `onSortChange` is missing: clicking a sortable header changes the arrow and nothing else."));
6
+ e() && (n && r === void 0 && console.warn("[tempest] <DataTable totalItems> is server mode, which needs a controlled `page`. Without it the pager moves the internal page while `data` keeps showing page 1."), r !== void 0 && !i && console.warn("[tempest] <DataTable page> is controlled but `onPageChange` is missing, so the pager cannot do anything."), o && !a && console.warn("[tempest] <DataTable onPageSizeChange> needs a controlled `pageSize`. Without it the selector reports the new size and the table keeps counting pages by 10."), n && u && !d && console.warn("[tempest] <DataTable totalItems searchable> delegates searching, so the box needs `onSearchChange`. Without it the user types and nothing filters and nothing is reported."), s && c && !l && console.warn("[tempest] <DataTable> is sorting manually but `onSortChange` is missing: clicking a sortable header changes the arrow and nothing else."));
7
7
  }, [
8
8
  n,
9
9
  r,
@@ -12,7 +12,9 @@ function n({ serverMode: n, controlledPage: r, onPageChange: i, sortIsManual: a,
12
12
  o,
13
13
  s,
14
14
  c,
15
- l
15
+ l,
16
+ u,
17
+ d
16
18
  ]);
17
19
  }
18
20
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"use-dev-warnings.js","names":[],"sources":["../../../src/components/DataTable/use-dev-warnings.ts"],"sourcesContent":["import { useEffect } from \"react\";\n\nimport { isDevBuild } from \"../../utils/dev-mode\";\n\n/** What {@link useDevWarnings} needs to judge the prop combination. */\nexport interface DataTableDevWarningsInput {\n /** Whether `totalItems` was passed. */\n serverMode: boolean;\n /** The controlled `page`, if any. */\n controlledPage: number | undefined;\n /** The page callback, if any. */\n onPageChange: ((page: number) => void) | undefined;\n /** Whether sorting is delegated. */\n sortIsManual: boolean;\n /** Whether any column renders a header the user can click to sort. */\n hasSortableColumn: boolean;\n /** The sort callback, if any. */\n onSortChange: ((sort: never) => void) | undefined;\n /** Whether a search box is rendered at all. */\n searchable: boolean;\n /** The search callback, if any. */\n onSearchChange: ((term: string) => void) | undefined;\n}\n\n/**\n * Warn, in development only, about prop combinations that render a table which\n * silently lies.\n *\n * Each of these produces a screen that looks like it works: the header sorts and\n * nothing moves, the pager renders and clicking it does nothing.\n *\n * Three of them **are** type errors now — `DataTablePagingProps`,\n * `DataTableSortProps` and `DataTableSearchProps` reject them at the call site —\n * so those warnings only reach a caller the types cannot: plain JavaScript, or\n * props arriving through an `any`-typed spread.\n *\n * Each one also has to be reachable. `totalItems` implies `manualSort`, so the\n * sort warning fired on every server-mode table — including one whose columns are\n * all plain, where no header is clickable and the sentence it prints (\"clicking a\n * sortable header changes the arrow and nothing else\") describes a state the user\n * cannot reach. A warning nobody can act on is not a warning: it teaches the\n * reader to skim past the two next to it, which *are* real, and the way out it\n * suggests is `onSortChange={() => {}}` — a callback that does nothing, that the\n * type-checker accepts, and that would hide the warning on the day a `sortable`\n * column does appear. So the sort warning asks for a sortable column first, the\n * way the search one already asks for `searchable`.\n *\n * The fourth is the one the types genuinely cannot afford. `totalItems` *implies*\n * `manualSearch`, so `searchable` with no `onSearchChange` is an inert search box\n * in server mode without anybody writing `manualSearch`. Expressing that means\n * the search axis has to read the paging axis, crossing two three-member unions\n * into nine — so it stays here, which is what \"runtime is the cheaper check\"\n * actually looks like when it is true.\n *\n * @param input - The resolved prop combination.\n */\nexport function useDevWarnings({\n serverMode,\n controlledPage,\n onPageChange,\n sortIsManual,\n hasSortableColumn,\n onSortChange,\n searchable,\n onSearchChange,\n}: DataTableDevWarningsInput): void {\n useEffect(() => {\n if (!isDevBuild()) return;\n\n if (serverMode && controlledPage === undefined) {\n console.warn(\n \"[tempest] <DataTable totalItems> is server mode, which needs a controlled `page`. \" +\n \"Without it the pager moves the internal page while `data` keeps showing page 1.\",\n );\n }\n if (controlledPage !== undefined && !onPageChange) {\n console.warn(\n \"[tempest] <DataTable page> is controlled but `onPageChange` is missing, so the pager cannot do anything.\",\n );\n }\n if (serverMode && searchable && !onSearchChange) {\n console.warn(\n \"[tempest] <DataTable totalItems searchable> delegates searching, so the box needs \" +\n \"`onSearchChange`. Without it the user types and nothing filters and nothing is reported.\",\n );\n }\n if (sortIsManual && hasSortableColumn && !onSortChange) {\n console.warn(\n \"[tempest] <DataTable> is sorting manually but `onSortChange` is missing: clicking a sortable header changes the arrow and nothing else.\",\n );\n }\n }, [\n serverMode,\n controlledPage,\n onPageChange,\n sortIsManual,\n hasSortableColumn,\n onSortChange,\n searchable,\n onSearchChange,\n ]);\n}\n"],"mappings":";;;AAwDA,SAAgB,EAAe,EAC3B,eACA,mBACA,iBACA,iBACA,sBACA,iBACA,eACA,qBACgC;CAChC,QAAgB;EACP,EAAW,MAEZ,KAAc,MAAmB,KAAA,KACjC,QAAQ,KACJ,mKAEJ,GAEA,MAAmB,KAAA,KAAa,CAAC,KACjC,QAAQ,KACJ,0GACJ,GAEA,KAAc,KAAc,CAAC,KAC7B,QAAQ,KACJ,4KAEJ,GAEA,KAAgB,KAAqB,CAAC,KACtC,QAAQ,KACJ,yIACJ;CAER,GAAG;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACJ,CAAC;AACL"}
1
+ {"version":3,"file":"use-dev-warnings.js","names":[],"sources":["../../../src/components/DataTable/use-dev-warnings.ts"],"sourcesContent":["import { useEffect } from \"react\";\n\nimport { isDevBuild } from \"../../utils/dev-mode\";\n\n/** What {@link useDevWarnings} needs to judge the prop combination. */\nexport interface DataTableDevWarningsInput {\n /** Whether `totalItems` was passed. */\n serverMode: boolean;\n /** The controlled `page`, if any. */\n controlledPage: number | undefined;\n /** The page callback, if any. */\n onPageChange: ((page: number) => void) | undefined;\n /** Whether the caller passed `pageSize` at all. */\n pageSizeControlled: boolean;\n /** The size-selector callback, if any. */\n onPageSizeChange: ((size: number) => void) | undefined;\n /** Whether sorting is delegated. */\n sortIsManual: boolean;\n /** Whether any column renders a header the user can click to sort. */\n hasSortableColumn: boolean;\n /** The sort callback, if any. */\n onSortChange: ((sort: never) => void) | undefined;\n /** Whether a search box is rendered at all. */\n searchable: boolean;\n /** The search callback, if any. */\n onSearchChange: ((term: string) => void) | undefined;\n}\n\n/**\n * Warn, in development only, about prop combinations that render a table which\n * silently lies.\n *\n * Each of these produces a screen that looks like it works: the header sorts and\n * nothing moves, the pager renders and clicking it does nothing.\n *\n * Four of them **are** type errors now — `DataTablePagingProps`,\n * `DataTablePageSizeProps`, `DataTableSortProps` and `DataTableSearchProps` reject\n * them at the call site —\n * so those warnings only reach a caller the types cannot: plain JavaScript, or\n * props arriving through an `any`-typed spread.\n *\n * Each one also has to be reachable. `totalItems` implies `manualSort`, so the\n * sort warning fired on every server-mode table — including one whose columns are\n * all plain, where no header is clickable and the sentence it prints (\"clicking a\n * sortable header changes the arrow and nothing else\") describes a state the user\n * cannot reach. A warning nobody can act on is not a warning: it teaches the\n * reader to skim past the two next to it, which *are* real, and the way out it\n * suggests is `onSortChange={() => {}}` — a callback that does nothing, that the\n * type-checker accepts, and that would hide the warning on the day a `sortable`\n * column does appear. So the sort warning asks for a sortable column first, the\n * way the search one already asks for `searchable`.\n *\n * The server-mode search warning is the one the types genuinely cannot afford. `totalItems` *implies*\n * `manualSearch`, so `searchable` with no `onSearchChange` is an inert search box\n * in server mode without anybody writing `manualSearch`. Expressing that means\n * the search axis has to read the paging axis, crossing two three-member unions\n * into nine — so it stays here, which is what \"runtime is the cheaper check\"\n * actually looks like when it is true.\n *\n * @param input - The resolved prop combination.\n */\nexport function useDevWarnings({\n serverMode,\n controlledPage,\n onPageChange,\n pageSizeControlled,\n onPageSizeChange,\n sortIsManual,\n hasSortableColumn,\n onSortChange,\n searchable,\n onSearchChange,\n}: DataTableDevWarningsInput): void {\n useEffect(() => {\n if (!isDevBuild()) return;\n\n if (serverMode && controlledPage === undefined) {\n console.warn(\n \"[tempest] <DataTable totalItems> is server mode, which needs a controlled `page`. \" +\n \"Without it the pager moves the internal page while `data` keeps showing page 1.\",\n );\n }\n if (controlledPage !== undefined && !onPageChange) {\n console.warn(\n \"[tempest] <DataTable page> is controlled but `onPageChange` is missing, so the pager cannot do anything.\",\n );\n }\n if (onPageSizeChange && !pageSizeControlled) {\n console.warn(\n \"[tempest] <DataTable onPageSizeChange> needs a controlled `pageSize`. Without it the \" +\n \"selector reports the new size and the table keeps counting pages by 10.\",\n );\n }\n if (serverMode && searchable && !onSearchChange) {\n console.warn(\n \"[tempest] <DataTable totalItems searchable> delegates searching, so the box needs \" +\n \"`onSearchChange`. Without it the user types and nothing filters and nothing is reported.\",\n );\n }\n if (sortIsManual && hasSortableColumn && !onSortChange) {\n console.warn(\n \"[tempest] <DataTable> is sorting manually but `onSortChange` is missing: clicking a sortable header changes the arrow and nothing else.\",\n );\n }\n }, [\n serverMode,\n controlledPage,\n onPageChange,\n pageSizeControlled,\n onPageSizeChange,\n sortIsManual,\n hasSortableColumn,\n onSortChange,\n searchable,\n onSearchChange,\n ]);\n}\n"],"mappings":";;;AA6DA,SAAgB,EAAe,EAC3B,eACA,mBACA,iBACA,uBACA,qBACA,iBACA,sBACA,iBACA,eACA,qBACgC;CAChC,QAAgB;EACP,EAAW,MAEZ,KAAc,MAAmB,KAAA,KACjC,QAAQ,KACJ,mKAEJ,GAEA,MAAmB,KAAA,KAAa,CAAC,KACjC,QAAQ,KACJ,0GACJ,GAEA,KAAoB,CAAC,KACrB,QAAQ,KACJ,8JAEJ,GAEA,KAAc,KAAc,CAAC,KAC7B,QAAQ,KACJ,4KAEJ,GAEA,KAAgB,KAAqB,CAAC,KACtC,QAAQ,KACJ,yIACJ;CAER,GAAG;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACJ,CAAC;AACL"}
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/cn.cjs"),t=require("../../hooks/use-breakpoint.cjs"),n=require("../Portal/portal-host.cjs"),r=require("./Drawer.module.cjs");let i=require("react"),a=require("react/jsx-runtime"),o=require("react-dom");function s({open:s,onClose:l,placement:u=`right`,title:d,children:f,footer:p,closeOnBackdrop:m=!0,closeOnEsc:h=!0,hideCloseButton:g=!1,className:_,mobilePlacement:v,showHandle:y=!1}){let{isMobile:b}=t.useBreakpoint(),x=b&&v?v:u;(0,i.useEffect)(()=>{if(!s)return;let e=document.body.style.overflow;document.body.style.overflow=`hidden`;let t=e=>{h&&e.key===`Escape`&&l()};return window.addEventListener(`keydown`,t),()=>{document.body.style.overflow=e,window.removeEventListener(`keydown`,t)}},[s,h,l]);let S=n.usePortalHost();return!s||!S?null:(0,o.createPortal)((0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(`div`,{className:r.default.overlay,onClick:()=>{m&&l()}}),(0,a.jsxs)(`aside`,{role:`dialog`,"aria-modal":`true`,className:e.cn(r.default.panel,r.default[x],_),children:[y&&(x===`bottom`||x===`top`)&&(0,a.jsx)(`div`,{className:e.cn(r.default.handle,x===`top`&&r.default.handleTop),"aria-hidden":!0}),(d||!g)&&(0,a.jsxs)(`header`,{className:r.default.header,children:[(0,a.jsx)(`h3`,{className:r.default.title,children:d}),!g&&(0,a.jsx)(`button`,{type:`button`,"aria-label":`Fechar`,className:r.default.close,onClick:l,children:(0,a.jsx)(c,{})})]}),(0,a.jsx)(`div`,{className:r.default.body,children:f}),p&&(0,a.jsx)(`footer`,{className:r.default.footer,children:p})]})]}),S)}function c(){return(0,a.jsx)(`svg`,{width:`18`,height:`18`,viewBox:`0 0 24 24`,fill:`none`,children:(0,a.jsx)(`path`,{d:`M6 6l12 12M6 18L18 6`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`})})}exports.Drawer=s;
1
+ const e=require("../../utils/cn.cjs"),t=require("../../hooks/use-breakpoint.cjs"),n=require("../Portal/escape-layer.cjs"),r=require("../../hooks/use-focus-trap.cjs"),i=require("../Portal/portal-host.cjs"),a=require("./Drawer.module.cjs");let o=require("react"),s=require("react/jsx-runtime"),c=require("react-dom");function l({open:l,onClose:d,placement:f=`right`,title:p,children:m,footer:h,closeOnBackdrop:g=!0,closeOnEsc:_=!0,hideCloseButton:v=!1,className:y,mobilePlacement:b,showHandle:x=!1}){let{isMobile:S}=t.useBreakpoint(),C=S&&b?b:f,w=(0,o.useRef)(null);r.useFocusTrap(w,l),n.useEscapeLayer(l,_?d:null),(0,o.useEffect)(()=>{if(!l)return;let e=document.body.style.overflow;return document.body.style.overflow=`hidden`,()=>{document.body.style.overflow=e}},[l,_,d]);let T=i.usePortalHost();return!l||!T?null:(0,c.createPortal)((0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(`div`,{className:a.default.overlay,onClick:()=>{g&&d()}}),(0,s.jsxs)(`aside`,{ref:w,role:`dialog`,"aria-modal":`true`,tabIndex:-1,className:e.cn(a.default.panel,a.default[C],y),children:[x&&(C===`bottom`||C===`top`)&&(0,s.jsx)(`div`,{className:e.cn(a.default.handle,C===`top`&&a.default.handleTop),"aria-hidden":!0}),(p||!v)&&(0,s.jsxs)(`header`,{className:a.default.header,children:[(0,s.jsx)(`h3`,{className:a.default.title,children:p}),!v&&(0,s.jsx)(`button`,{type:`button`,"aria-label":`Fechar`,className:a.default.close,onClick:d,children:(0,s.jsx)(u,{})})]}),(0,s.jsx)(`div`,{className:a.default.body,children:m}),h&&(0,s.jsx)(`footer`,{className:a.default.footer,children:h})]})]}),T)}function u(){return(0,s.jsx)(`svg`,{width:`18`,height:`18`,viewBox:`0 0 24 24`,fill:`none`,children:(0,s.jsx)(`path`,{d:`M6 6l12 12M6 18L18 6`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`})})}exports.Drawer=l;
2
2
  //# sourceMappingURL=Drawer.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"Drawer.cjs","names":[],"sources":["../../../src/components/Drawer/Drawer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — placement plus mobilePlacement is\n * the whole point — a drawer that comes from the side on desktop and from the bottom\n * on a phone — and the rest is the dialog contract the SDK's Modal also has (open,\n * onClose, title, children, footer, closeOnBackdrop, closeOnEsc, hideCloseButton)\n * plus showHandle for the sheet affordance.\n */\nimport { useEffect } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/utils/cn\";\nimport { useBreakpoint } from \"@/hooks/use-breakpoint\";\nimport styles from \"./Drawer.module.css\";\nimport { usePortalHost } from \"../Portal/portal-host\";\n\nexport type DrawerPlacement = \"right\" | \"left\" | \"top\" | \"bottom\";\n\nexport interface DrawerProps {\n open: boolean;\n onClose: () => void;\n placement?: DrawerPlacement;\n title?: ReactNode;\n children?: ReactNode;\n footer?: ReactNode;\n closeOnBackdrop?: boolean;\n closeOnEsc?: boolean;\n hideCloseButton?: boolean;\n className?: string;\n /**\n * Auto-switch to bottom-sheet placement on mobile viewports (< md).\n * Modern mobile apps default to bottom drawers; on desktop the original\n * `placement` is preserved.\n */\n mobilePlacement?: DrawerPlacement;\n /** Render a drag handle indicator at the leading edge (bottom-sheet style). */\n showHandle?: boolean;\n}\n\n/**\n * Sliding side panel. Same building blocks as {@link Modal} but anchored to\n * an edge. Locks body scroll while open.\n */\nexport function Drawer({\n open,\n onClose,\n placement = \"right\",\n title,\n children,\n footer,\n closeOnBackdrop = true,\n closeOnEsc = true,\n hideCloseButton = false,\n className,\n mobilePlacement,\n showHandle = false,\n}: DrawerProps) {\n const { isMobile } = useBreakpoint();\n const effectivePlacement: DrawerPlacement =\n isMobile && mobilePlacement ? mobilePlacement : placement;\n\n useEffect(() => {\n if (!open) return;\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n const handleKey = (event: KeyboardEvent): void => {\n if (closeOnEsc && event.key === \"Escape\") onClose();\n };\n window.addEventListener(\"keydown\", handleKey);\n return () => {\n document.body.style.overflow = previousOverflow;\n window.removeEventListener(\"keydown\", handleKey);\n };\n }, [open, closeOnEsc, onClose]);\n\n const portalHost = usePortalHost();\n\n if (!open || !portalHost) return null;\n\n return createPortal(\n <>\n <div\n className={styles.overlay}\n onClick={() => {\n if (closeOnBackdrop) onClose();\n }}\n />\n <aside\n role=\"dialog\"\n aria-modal=\"true\"\n className={cn(styles.panel, styles[effectivePlacement], className)}\n >\n {showHandle &&\n (effectivePlacement === \"bottom\" || effectivePlacement === \"top\") && (\n <div\n className={cn(\n styles.handle,\n effectivePlacement === \"top\" && styles.handleTop,\n )}\n aria-hidden\n />\n )}\n {(title || !hideCloseButton) && (\n <header className={styles.header}>\n <h3 className={styles.title}>{title}</h3>\n {!hideCloseButton && (\n <button\n type=\"button\"\n aria-label=\"Fechar\"\n className={styles.close}\n onClick={onClose}\n >\n <CloseIcon />\n </button>\n )}\n </header>\n )}\n <div className={styles.body}>{children}</div>\n {footer && <footer className={styles.footer}>{footer}</footer>}\n </aside>\n </>,\n portalHost,\n );\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 6l12 12M6 18L18 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"uOA0CA,SAAgB,EAAO,CACnB,OACA,UACA,YAAY,QACZ,QACA,WACA,SACA,kBAAkB,GAClB,aAAa,GACb,kBAAkB,GAClB,YACA,kBACA,aAAa,IACD,CACZ,GAAM,CAAE,YAAa,EAAA,cAAc,EAC7B,EACF,GAAY,EAAkB,EAAkB,GAEpD,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,EAAM,OACX,IAAM,EAAmB,SAAS,KAAK,MAAM,SAC7C,SAAS,KAAK,MAAM,SAAW,SAC/B,IAAM,EAAa,GAA+B,CAC1C,GAAc,EAAM,MAAQ,UAAU,EAAQ,CACtD,EAEA,OADA,OAAO,iBAAiB,UAAW,CAAS,MAC/B,CACT,SAAS,KAAK,MAAM,SAAW,EAC/B,OAAO,oBAAoB,UAAW,CAAS,CACnD,CACJ,EAAG,CAAC,EAAM,EAAY,CAAO,CAAC,EAE9B,IAAM,EAAa,EAAA,cAAc,EAIjC,MAFI,CAAC,GAAQ,CAAC,EAAmB,MAEjC,EAAO,EAAA,aAAA,EACH,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,UAAW,EAAA,QAAO,QAClB,YAAe,CACP,GAAiB,EAAQ,CACjC,CACH,CAAA,GACD,EAAA,EAAA,KAAA,CAAC,QAAD,CACI,KAAK,SACL,aAAW,OACX,UAAW,EAAA,GAAG,EAAA,QAAO,MAAO,EAAA,QAAO,GAAqB,CAAS,EAHrE,SAAA,CAKK,IACI,IAAuB,UAAY,IAAuB,SACvD,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,UAAW,EAAA,GACP,EAAA,QAAO,OACP,IAAuB,OAAS,EAAA,QAAO,SAC3C,EACA,cAAA,EACH,CAAA,GAEP,GAAS,CAAC,KACR,EAAA,EAAA,KAAA,CAAC,SAAD,CAAQ,UAAW,EAAA,QAAO,OAA1B,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,MAAQ,SAAA,CAAU,CAAA,EACvC,CAAC,IACE,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,aAAW,SACX,UAAW,EAAA,QAAO,MAClB,QAAS,EAET,UAAA,EAAA,EAAA,IAAA,CAAC,EAAD,CAAY,CAAA,CACR,CAAA,CAER,KAEZ,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAO,UAAc,CAAA,EAC3C,IAAU,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,UAAW,EAAA,QAAO,OAAS,SAAA,CAAe,CAAA,CAC1D,CACT,CAAA,CAAA,CAAA,CAAA,EACF,CACJ,CACJ,CAEA,SAAS,GAAY,CACjB,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,MAAM,KAAK,OAAO,KAAK,QAAQ,YAAY,KAAK,OACjD,UAAA,EAAA,EAAA,IAAA,CAAC,OAAD,CACI,EAAE,uBACF,OAAO,eACP,YAAY,IACZ,cAAc,OACjB,CAAA,CACA,CAAA,CAEb"}
1
+ {"version":3,"file":"Drawer.cjs","names":[],"sources":["../../../src/components/Drawer/Drawer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — placement plus mobilePlacement is\n * the whole point — a drawer that comes from the side on desktop and from the bottom\n * on a phone — and the rest is the dialog contract the SDK's Modal also has (open,\n * onClose, title, children, footer, closeOnBackdrop, closeOnEsc, hideCloseButton)\n * plus showHandle for the sheet affordance.\n */\nimport { useEffect, useRef } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useEscapeLayer } from \"@/components/Portal/escape-layer\";\nimport { cn } from \"@/utils/cn\";\nimport { useBreakpoint } from \"@/hooks/use-breakpoint\";\nimport { useFocusTrap } from \"@/hooks/use-focus-trap\";\nimport styles from \"./Drawer.module.css\";\nimport { usePortalHost } from \"../Portal/portal-host\";\n\nexport type DrawerPlacement = \"right\" | \"left\" | \"top\" | \"bottom\";\n\nexport interface DrawerProps {\n open: boolean;\n onClose: () => void;\n placement?: DrawerPlacement;\n title?: ReactNode;\n children?: ReactNode;\n footer?: ReactNode;\n closeOnBackdrop?: boolean;\n closeOnEsc?: boolean;\n hideCloseButton?: boolean;\n className?: string;\n /**\n * Auto-switch to bottom-sheet placement on mobile viewports (< md).\n * Modern mobile apps default to bottom drawers; on desktop the original\n * `placement` is preserved.\n */\n mobilePlacement?: DrawerPlacement;\n /** Render a drag handle indicator at the leading edge (bottom-sheet style). */\n showHandle?: boolean;\n}\n\n/**\n * Sliding side panel. Same building blocks as {@link Modal} but anchored to\n * an edge. Locks body scroll while open, and traps keyboard focus in the panel\n * the same way `Modal` does: focus enters on open, `Tab` cycles inside (portalled\n * panels opened from it included) and returns to the opener on close.\n */\nexport function Drawer({\n open,\n onClose,\n placement = \"right\",\n title,\n children,\n footer,\n closeOnBackdrop = true,\n closeOnEsc = true,\n hideCloseButton = false,\n className,\n mobilePlacement,\n showHandle = false,\n}: DrawerProps) {\n const { isMobile } = useBreakpoint();\n const effectivePlacement: DrawerPlacement =\n isMobile && mobilePlacement ? mobilePlacement : placement;\n\n const panelRef = useRef<HTMLElement>(null);\n useFocusTrap(panelRef, open);\n useEscapeLayer(open, closeOnEsc ? onClose : null);\n useEffect(() => {\n if (!open) return;\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n return () => {\n document.body.style.overflow = previousOverflow;\n };\n }, [open, closeOnEsc, onClose]);\n\n const portalHost = usePortalHost();\n\n if (!open || !portalHost) return null;\n\n return createPortal(\n <>\n <div\n className={styles.overlay}\n onClick={() => {\n if (closeOnBackdrop) onClose();\n }}\n />\n <aside\n ref={panelRef}\n role=\"dialog\"\n aria-modal=\"true\"\n tabIndex={-1}\n className={cn(styles.panel, styles[effectivePlacement], className)}\n >\n {showHandle &&\n (effectivePlacement === \"bottom\" || effectivePlacement === \"top\") && (\n <div\n className={cn(\n styles.handle,\n effectivePlacement === \"top\" && styles.handleTop,\n )}\n aria-hidden\n />\n )}\n {(title || !hideCloseButton) && (\n <header className={styles.header}>\n <h3 className={styles.title}>{title}</h3>\n {!hideCloseButton && (\n <button\n type=\"button\"\n aria-label=\"Fechar\"\n className={styles.close}\n onClick={onClose}\n >\n <CloseIcon />\n </button>\n )}\n </header>\n )}\n <div className={styles.body}>{children}</div>\n {footer && <footer className={styles.footer}>{footer}</footer>}\n </aside>\n </>,\n portalHost,\n );\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 6l12 12M6 18L18 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"2TA8CA,SAAgB,EAAO,CACnB,OACA,UACA,YAAY,QACZ,QACA,WACA,SACA,kBAAkB,GAClB,aAAa,GACb,kBAAkB,GAClB,YACA,kBACA,aAAa,IACD,CACZ,GAAM,CAAE,YAAa,EAAA,cAAc,EAC7B,EACF,GAAY,EAAkB,EAAkB,EAE9C,GAAA,EAAW,EAAA,OAAA,CAAoB,IAAI,EACzC,EAAA,aAAa,EAAU,CAAI,EAC3B,EAAA,eAAe,EAAM,EAAa,EAAU,IAAI,GAChD,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,EAAM,OACX,IAAM,EAAmB,SAAS,KAAK,MAAM,SAE7C,MADA,UAAS,KAAK,MAAM,SAAW,aAClB,CACT,SAAS,KAAK,MAAM,SAAW,CACnC,CACJ,EAAG,CAAC,EAAM,EAAY,CAAO,CAAC,EAE9B,IAAM,EAAa,EAAA,cAAc,EAIjC,MAFI,CAAC,GAAQ,CAAC,EAAmB,MAEjC,EAAO,EAAA,aAAA,EACH,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,UAAW,EAAA,QAAO,QAClB,YAAe,CACP,GAAiB,EAAQ,CACjC,CACH,CAAA,GACD,EAAA,EAAA,KAAA,CAAC,QAAD,CACI,IAAK,EACL,KAAK,SACL,aAAW,OACX,SAAU,GACV,UAAW,EAAA,GAAG,EAAA,QAAO,MAAO,EAAA,QAAO,GAAqB,CAAS,EALrE,SAAA,CAOK,IACI,IAAuB,UAAY,IAAuB,SACvD,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,UAAW,EAAA,GACP,EAAA,QAAO,OACP,IAAuB,OAAS,EAAA,QAAO,SAC3C,EACA,cAAA,EACH,CAAA,GAEP,GAAS,CAAC,KACR,EAAA,EAAA,KAAA,CAAC,SAAD,CAAQ,UAAW,EAAA,QAAO,OAA1B,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,MAAQ,SAAA,CAAU,CAAA,EACvC,CAAC,IACE,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,aAAW,SACX,UAAW,EAAA,QAAO,MAClB,QAAS,EAET,UAAA,EAAA,EAAA,IAAA,CAAC,EAAD,CAAY,CAAA,CACR,CAAA,CAER,KAEZ,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAO,UAAc,CAAA,EAC3C,IAAU,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,UAAW,EAAA,QAAO,OAAS,SAAA,CAAe,CAAA,CAC1D,CACT,CAAA,CAAA,CAAA,CAAA,EACF,CACJ,CACJ,CAEA,SAAS,GAAY,CACjB,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,MAAM,KAAK,OAAO,KAAK,QAAQ,YAAY,KAAK,OACjD,UAAA,EAAA,EAAA,IAAA,CAAC,OAAD,CACI,EAAE,uBACF,OAAO,eACP,YAAY,IACZ,cAAc,OACjB,CAAA,CACA,CAAA,CAEb"}
@@ -1,74 +1,74 @@
1
1
  import { cn as e } from "../../utils/cn.js";
2
2
  import { useBreakpoint as t } from "../../hooks/use-breakpoint.js";
3
- import { usePortalHost as n } from "../Portal/portal-host.js";
4
- import r from "./Drawer.module.js";
5
- import { useEffect as i } from "react";
6
- import { Fragment as a, jsx as o, jsxs as s } from "react/jsx-runtime";
7
- import { createPortal as c } from "react-dom";
3
+ import { useEscapeLayer as n } from "../Portal/escape-layer.js";
4
+ import { useFocusTrap as r } from "../../hooks/use-focus-trap.js";
5
+ import { usePortalHost as i } from "../Portal/portal-host.js";
6
+ import a from "./Drawer.module.js";
7
+ import { useEffect as o, useRef as s } from "react";
8
+ import { Fragment as c, jsx as l, jsxs as u } from "react/jsx-runtime";
9
+ import { createPortal as d } from "react-dom";
8
10
  //#region src/components/Drawer/Drawer.tsx
9
- function l({ open: l, onClose: d, placement: f = "right", title: p, children: m, footer: h, closeOnBackdrop: g = !0, closeOnEsc: _ = !0, hideCloseButton: v = !1, className: y, mobilePlacement: b, showHandle: x = !1 }) {
10
- let { isMobile: S } = t(), C = S && b ? b : f;
11
- i(() => {
12
- if (!l) return;
11
+ function f({ open: f, onClose: m, placement: h = "right", title: g, children: _, footer: v, closeOnBackdrop: y = !0, closeOnEsc: b = !0, hideCloseButton: x = !1, className: S, mobilePlacement: C, showHandle: w = !1 }) {
12
+ let { isMobile: T } = t(), E = T && C ? C : h, D = s(null);
13
+ r(D, f), n(f, b ? m : null), o(() => {
14
+ if (!f) return;
13
15
  let e = document.body.style.overflow;
14
- document.body.style.overflow = "hidden";
15
- let t = (e) => {
16
- _ && e.key === "Escape" && d();
17
- };
18
- return window.addEventListener("keydown", t), () => {
19
- document.body.style.overflow = e, window.removeEventListener("keydown", t);
16
+ return document.body.style.overflow = "hidden", () => {
17
+ document.body.style.overflow = e;
20
18
  };
21
19
  }, [
22
- l,
23
- _,
24
- d
20
+ f,
21
+ b,
22
+ m
25
23
  ]);
26
- let w = n();
27
- return !l || !w ? null : c(/* @__PURE__ */ s(a, { children: [/* @__PURE__ */ o("div", {
28
- className: r.overlay,
24
+ let O = i();
25
+ return !f || !O ? null : d(/* @__PURE__ */ u(c, { children: [/* @__PURE__ */ l("div", {
26
+ className: a.overlay,
29
27
  onClick: () => {
30
- g && d();
28
+ y && m();
31
29
  }
32
- }), /* @__PURE__ */ s("aside", {
30
+ }), /* @__PURE__ */ u("aside", {
31
+ ref: D,
33
32
  role: "dialog",
34
33
  "aria-modal": "true",
35
- className: e(r.panel, r[C], y),
34
+ tabIndex: -1,
35
+ className: e(a.panel, a[E], S),
36
36
  children: [
37
- x && (C === "bottom" || C === "top") && /* @__PURE__ */ o("div", {
38
- className: e(r.handle, C === "top" && r.handleTop),
37
+ w && (E === "bottom" || E === "top") && /* @__PURE__ */ l("div", {
38
+ className: e(a.handle, E === "top" && a.handleTop),
39
39
  "aria-hidden": !0
40
40
  }),
41
- (p || !v) && /* @__PURE__ */ s("header", {
42
- className: r.header,
43
- children: [/* @__PURE__ */ o("h3", {
44
- className: r.title,
45
- children: p
46
- }), !v && /* @__PURE__ */ o("button", {
41
+ (g || !x) && /* @__PURE__ */ u("header", {
42
+ className: a.header,
43
+ children: [/* @__PURE__ */ l("h3", {
44
+ className: a.title,
45
+ children: g
46
+ }), !x && /* @__PURE__ */ l("button", {
47
47
  type: "button",
48
48
  "aria-label": "Fechar",
49
- className: r.close,
50
- onClick: d,
51
- children: /* @__PURE__ */ o(u, {})
49
+ className: a.close,
50
+ onClick: m,
51
+ children: /* @__PURE__ */ l(p, {})
52
52
  })]
53
53
  }),
54
- /* @__PURE__ */ o("div", {
55
- className: r.body,
56
- children: m
54
+ /* @__PURE__ */ l("div", {
55
+ className: a.body,
56
+ children: _
57
57
  }),
58
- h && /* @__PURE__ */ o("footer", {
59
- className: r.footer,
60
- children: h
58
+ v && /* @__PURE__ */ l("footer", {
59
+ className: a.footer,
60
+ children: v
61
61
  })
62
62
  ]
63
- })] }), w);
63
+ })] }), O);
64
64
  }
65
- function u() {
66
- return /* @__PURE__ */ o("svg", {
65
+ function p() {
66
+ return /* @__PURE__ */ l("svg", {
67
67
  width: "18",
68
68
  height: "18",
69
69
  viewBox: "0 0 24 24",
70
70
  fill: "none",
71
- children: /* @__PURE__ */ o("path", {
71
+ children: /* @__PURE__ */ l("path", {
72
72
  d: "M6 6l12 12M6 18L18 6",
73
73
  stroke: "currentColor",
74
74
  strokeWidth: "2",
@@ -77,6 +77,6 @@ function u() {
77
77
  });
78
78
  }
79
79
  //#endregion
80
- export { l as Drawer };
80
+ export { f as Drawer };
81
81
 
82
82
  //# sourceMappingURL=Drawer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Drawer.js","names":[],"sources":["../../../src/components/Drawer/Drawer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — placement plus mobilePlacement is\n * the whole point — a drawer that comes from the side on desktop and from the bottom\n * on a phone — and the rest is the dialog contract the SDK's Modal also has (open,\n * onClose, title, children, footer, closeOnBackdrop, closeOnEsc, hideCloseButton)\n * plus showHandle for the sheet affordance.\n */\nimport { useEffect } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/utils/cn\";\nimport { useBreakpoint } from \"@/hooks/use-breakpoint\";\nimport styles from \"./Drawer.module.css\";\nimport { usePortalHost } from \"../Portal/portal-host\";\n\nexport type DrawerPlacement = \"right\" | \"left\" | \"top\" | \"bottom\";\n\nexport interface DrawerProps {\n open: boolean;\n onClose: () => void;\n placement?: DrawerPlacement;\n title?: ReactNode;\n children?: ReactNode;\n footer?: ReactNode;\n closeOnBackdrop?: boolean;\n closeOnEsc?: boolean;\n hideCloseButton?: boolean;\n className?: string;\n /**\n * Auto-switch to bottom-sheet placement on mobile viewports (< md).\n * Modern mobile apps default to bottom drawers; on desktop the original\n * `placement` is preserved.\n */\n mobilePlacement?: DrawerPlacement;\n /** Render a drag handle indicator at the leading edge (bottom-sheet style). */\n showHandle?: boolean;\n}\n\n/**\n * Sliding side panel. Same building blocks as {@link Modal} but anchored to\n * an edge. Locks body scroll while open.\n */\nexport function Drawer({\n open,\n onClose,\n placement = \"right\",\n title,\n children,\n footer,\n closeOnBackdrop = true,\n closeOnEsc = true,\n hideCloseButton = false,\n className,\n mobilePlacement,\n showHandle = false,\n}: DrawerProps) {\n const { isMobile } = useBreakpoint();\n const effectivePlacement: DrawerPlacement =\n isMobile && mobilePlacement ? mobilePlacement : placement;\n\n useEffect(() => {\n if (!open) return;\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n const handleKey = (event: KeyboardEvent): void => {\n if (closeOnEsc && event.key === \"Escape\") onClose();\n };\n window.addEventListener(\"keydown\", handleKey);\n return () => {\n document.body.style.overflow = previousOverflow;\n window.removeEventListener(\"keydown\", handleKey);\n };\n }, [open, closeOnEsc, onClose]);\n\n const portalHost = usePortalHost();\n\n if (!open || !portalHost) return null;\n\n return createPortal(\n <>\n <div\n className={styles.overlay}\n onClick={() => {\n if (closeOnBackdrop) onClose();\n }}\n />\n <aside\n role=\"dialog\"\n aria-modal=\"true\"\n className={cn(styles.panel, styles[effectivePlacement], className)}\n >\n {showHandle &&\n (effectivePlacement === \"bottom\" || effectivePlacement === \"top\") && (\n <div\n className={cn(\n styles.handle,\n effectivePlacement === \"top\" && styles.handleTop,\n )}\n aria-hidden\n />\n )}\n {(title || !hideCloseButton) && (\n <header className={styles.header}>\n <h3 className={styles.title}>{title}</h3>\n {!hideCloseButton && (\n <button\n type=\"button\"\n aria-label=\"Fechar\"\n className={styles.close}\n onClick={onClose}\n >\n <CloseIcon />\n </button>\n )}\n </header>\n )}\n <div className={styles.body}>{children}</div>\n {footer && <footer className={styles.footer}>{footer}</footer>}\n </aside>\n </>,\n portalHost,\n );\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 6l12 12M6 18L18 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":";;;;;;;;AA0CA,SAAgB,EAAO,EACnB,SACA,YACA,eAAY,SACZ,UACA,aACA,WACA,qBAAkB,IAClB,gBAAa,IACb,qBAAkB,IAClB,cACA,oBACA,gBAAa,MACD;CACZ,IAAM,EAAE,gBAAa,EAAc,GAC7B,IACF,KAAY,IAAkB,IAAkB;CAEpD,QAAgB;EACZ,IAAI,CAAC,GAAM;EACX,IAAM,IAAmB,SAAS,KAAK,MAAM;EAC7C,SAAS,KAAK,MAAM,WAAW;EAC/B,IAAM,KAAa,MAA+B;GAC9C,AAAI,KAAc,EAAM,QAAQ,YAAU,EAAQ;EACtD;EAEA,OADA,OAAO,iBAAiB,WAAW,CAAS,SAC/B;GAET,AADA,SAAS,KAAK,MAAM,WAAW,GAC/B,OAAO,oBAAoB,WAAW,CAAS;EACnD;CACJ,GAAG;EAAC;EAAM;EAAY;CAAO,CAAC;CAE9B,IAAM,IAAa,EAAc;CAIjC,OAFI,CAAC,KAAQ,CAAC,IAAmB,OAE1B,EACH,kBAAA,GAAA,EAAA,UAAA,CACI,kBAAC,OAAD;EACI,WAAW,EAAO;EAClB,eAAe;GACX,AAAI,KAAiB,EAAQ;EACjC;CACH,CAAA,GACD,kBAAC,SAAD;EACI,MAAK;EACL,cAAW;EACX,WAAW,EAAG,EAAO,OAAO,EAAO,IAAqB,CAAS;EAHrE,UAAA;GAKK,MACI,MAAuB,YAAY,MAAuB,UACvD,kBAAC,OAAD;IACI,WAAW,EACP,EAAO,QACP,MAAuB,SAAS,EAAO,SAC3C;IACA,eAAA;GACH,CAAA;IAEP,KAAS,CAAC,MACR,kBAAC,UAAD;IAAQ,WAAW,EAAO;IAA1B,UAAA,CACI,kBAAC,MAAD;KAAI,WAAW,EAAO;KAAQ,UAAA;IAAU,CAAA,GACvC,CAAC,KACE,kBAAC,UAAD;KACI,MAAK;KACL,cAAW;KACX,WAAW,EAAO;KAClB,SAAS;KAET,UAAA,kBAAC,GAAD,CAAY,CAAA;IACR,CAAA,CAER;;GAEZ,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAO;GAAc,CAAA;GAC3C,KAAU,kBAAC,UAAD;IAAQ,WAAW,EAAO;IAAS,UAAA;GAAe,CAAA;EAC1D;CACT,CAAA,CAAA,EAAA,CAAA,GACF,CACJ;AACJ;AAEA,SAAS,IAAY;CACjB,OACI,kBAAC,OAAD;EAAK,OAAM;EAAK,QAAO;EAAK,SAAQ;EAAY,MAAK;EACjD,UAAA,kBAAC,QAAD;GACI,GAAE;GACF,QAAO;GACP,aAAY;GACZ,eAAc;EACjB,CAAA;CACA,CAAA;AAEb"}
1
+ {"version":3,"file":"Drawer.js","names":[],"sources":["../../../src/components/Drawer/Drawer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — placement plus mobilePlacement is\n * the whole point — a drawer that comes from the side on desktop and from the bottom\n * on a phone — and the rest is the dialog contract the SDK's Modal also has (open,\n * onClose, title, children, footer, closeOnBackdrop, closeOnEsc, hideCloseButton)\n * plus showHandle for the sheet affordance.\n */\nimport { useEffect, useRef } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useEscapeLayer } from \"@/components/Portal/escape-layer\";\nimport { cn } from \"@/utils/cn\";\nimport { useBreakpoint } from \"@/hooks/use-breakpoint\";\nimport { useFocusTrap } from \"@/hooks/use-focus-trap\";\nimport styles from \"./Drawer.module.css\";\nimport { usePortalHost } from \"../Portal/portal-host\";\n\nexport type DrawerPlacement = \"right\" | \"left\" | \"top\" | \"bottom\";\n\nexport interface DrawerProps {\n open: boolean;\n onClose: () => void;\n placement?: DrawerPlacement;\n title?: ReactNode;\n children?: ReactNode;\n footer?: ReactNode;\n closeOnBackdrop?: boolean;\n closeOnEsc?: boolean;\n hideCloseButton?: boolean;\n className?: string;\n /**\n * Auto-switch to bottom-sheet placement on mobile viewports (< md).\n * Modern mobile apps default to bottom drawers; on desktop the original\n * `placement` is preserved.\n */\n mobilePlacement?: DrawerPlacement;\n /** Render a drag handle indicator at the leading edge (bottom-sheet style). */\n showHandle?: boolean;\n}\n\n/**\n * Sliding side panel. Same building blocks as {@link Modal} but anchored to\n * an edge. Locks body scroll while open, and traps keyboard focus in the panel\n * the same way `Modal` does: focus enters on open, `Tab` cycles inside (portalled\n * panels opened from it included) and returns to the opener on close.\n */\nexport function Drawer({\n open,\n onClose,\n placement = \"right\",\n title,\n children,\n footer,\n closeOnBackdrop = true,\n closeOnEsc = true,\n hideCloseButton = false,\n className,\n mobilePlacement,\n showHandle = false,\n}: DrawerProps) {\n const { isMobile } = useBreakpoint();\n const effectivePlacement: DrawerPlacement =\n isMobile && mobilePlacement ? mobilePlacement : placement;\n\n const panelRef = useRef<HTMLElement>(null);\n useFocusTrap(panelRef, open);\n useEscapeLayer(open, closeOnEsc ? onClose : null);\n useEffect(() => {\n if (!open) return;\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n return () => {\n document.body.style.overflow = previousOverflow;\n };\n }, [open, closeOnEsc, onClose]);\n\n const portalHost = usePortalHost();\n\n if (!open || !portalHost) return null;\n\n return createPortal(\n <>\n <div\n className={styles.overlay}\n onClick={() => {\n if (closeOnBackdrop) onClose();\n }}\n />\n <aside\n ref={panelRef}\n role=\"dialog\"\n aria-modal=\"true\"\n tabIndex={-1}\n className={cn(styles.panel, styles[effectivePlacement], className)}\n >\n {showHandle &&\n (effectivePlacement === \"bottom\" || effectivePlacement === \"top\") && (\n <div\n className={cn(\n styles.handle,\n effectivePlacement === \"top\" && styles.handleTop,\n )}\n aria-hidden\n />\n )}\n {(title || !hideCloseButton) && (\n <header className={styles.header}>\n <h3 className={styles.title}>{title}</h3>\n {!hideCloseButton && (\n <button\n type=\"button\"\n aria-label=\"Fechar\"\n className={styles.close}\n onClick={onClose}\n >\n <CloseIcon />\n </button>\n )}\n </header>\n )}\n <div className={styles.body}>{children}</div>\n {footer && <footer className={styles.footer}>{footer}</footer>}\n </aside>\n </>,\n portalHost,\n );\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 6l12 12M6 18L18 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":";;;;;;;;;;AA8CA,SAAgB,EAAO,EACnB,SACA,YACA,eAAY,SACZ,UACA,aACA,WACA,qBAAkB,IAClB,gBAAa,IACb,qBAAkB,IAClB,cACA,oBACA,gBAAa,MACD;CACZ,IAAM,EAAE,gBAAa,EAAc,GAC7B,IACF,KAAY,IAAkB,IAAkB,GAE9C,IAAW,EAAoB,IAAI;CAGzC,AAFA,EAAa,GAAU,CAAI,GAC3B,EAAe,GAAM,IAAa,IAAU,IAAI,GAChD,QAAgB;EACZ,IAAI,CAAC,GAAM;EACX,IAAM,IAAmB,SAAS,KAAK,MAAM;EAE7C,OADA,SAAS,KAAK,MAAM,WAAW,gBAClB;GACT,SAAS,KAAK,MAAM,WAAW;EACnC;CACJ,GAAG;EAAC;EAAM;EAAY;CAAO,CAAC;CAE9B,IAAM,IAAa,EAAc;CAIjC,OAFI,CAAC,KAAQ,CAAC,IAAmB,OAE1B,EACH,kBAAA,GAAA,EAAA,UAAA,CACI,kBAAC,OAAD;EACI,WAAW,EAAO;EAClB,eAAe;GACX,AAAI,KAAiB,EAAQ;EACjC;CACH,CAAA,GACD,kBAAC,SAAD;EACI,KAAK;EACL,MAAK;EACL,cAAW;EACX,UAAU;EACV,WAAW,EAAG,EAAO,OAAO,EAAO,IAAqB,CAAS;EALrE,UAAA;GAOK,MACI,MAAuB,YAAY,MAAuB,UACvD,kBAAC,OAAD;IACI,WAAW,EACP,EAAO,QACP,MAAuB,SAAS,EAAO,SAC3C;IACA,eAAA;GACH,CAAA;IAEP,KAAS,CAAC,MACR,kBAAC,UAAD;IAAQ,WAAW,EAAO;IAA1B,UAAA,CACI,kBAAC,MAAD;KAAI,WAAW,EAAO;KAAQ,UAAA;IAAU,CAAA,GACvC,CAAC,KACE,kBAAC,UAAD;KACI,MAAK;KACL,cAAW;KACX,WAAW,EAAO;KAClB,SAAS;KAET,UAAA,kBAAC,GAAD,CAAY,CAAA;IACR,CAAA,CAER;;GAEZ,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAO;GAAc,CAAA;GAC3C,KAAU,kBAAC,UAAD;IAAQ,WAAW,EAAO;IAAS,UAAA;GAAe,CAAA;EAC1D;CACT,CAAA,CAAA,EAAA,CAAA,GACF,CACJ;AACJ;AAEA,SAAS,IAAY;CACjB,OACI,kBAAC,OAAD;EAAK,OAAM;EAAK,QAAO;EAAK,SAAQ;EAAY,MAAK;EACjD,UAAA,kBAAC,QAAD;GACI,GAAE;GACF,QAAO;GACP,aAAY;GACZ,eAAc;EACjB,CAAA;CACA,CAAA;AAEb"}
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/cn.cjs"),t=require("./DropdownMenu.module.cjs");let n=require("react"),r=require("react/jsx-runtime");function i(e){switch(e){case`bottom-end`:return t.default.bottomEnd;case`top-start`:return t.default.topStart;case`top-end`:return t.default.topEnd;default:return t.default.bottomStart}}function a(e){return(e.type===`item`||e.type===`checkbox`)&&!e.disabled}function o({trigger:o,items:s,placement:c=`bottom-start`,className:l}){let[u,d]=(0,n.useState)(!1),[f,p]=(0,n.useState)(-1),m=(0,n.useId)(),h=(0,n.useRef)(null),g=(0,n.useRef)([]),_=(0,n.useRef)(null),v=s.map((e,t)=>a(e)?t:-1).filter(e=>e!==-1),y=(0,n.useCallback)(()=>{h.current?.querySelector(`[aria-haspopup='menu']`)?.focus()},[]),b=(0,n.useCallback)(e=>{d(!1),p(-1),e&&y()},[y]),x=(0,n.useCallback)(e=>{p(e),g.current[e]?.focus()},[]),S=e=>{if(v.length===0)return;let t=(v.indexOf(f)+e+v.length)%v.length;x(v[t]??-1)};(0,n.useEffect)(()=>{if(!u||_.current===null)return;let e=_.current===`last`?v.at(-1):v[0];_.current=null,e!==void 0&&x(e)},[u,v,x]),(0,n.useEffect)(()=>{if(!u)return;let e=e=>{h.current&&!h.current.contains(e.target)&&b(!1)};return window.addEventListener(`mousedown`,e),()=>window.removeEventListener(`mousedown`,e)},[u,b]);let C=e=>{_.current=e,d(!0)},w=e=>{if(o.props.onKeyDown?.(e),!e.defaultPrevented){if(e.key===`ArrowDown`||!u&&(e.key===`Enter`||e.key===` `)){e.preventDefault(),C(`first`);return}e.key===`ArrowUp`&&(e.preventDefault(),C(`last`))}},T=e=>{switch(e.key){case`ArrowDown`:e.preventDefault(),S(1);break;case`ArrowUp`:e.preventDefault(),S(-1);break;case`Home`:e.preventDefault(),v[0]!==void 0&&x(v[0]);break;case`End`:{e.preventDefault();let t=v.at(-1);t!==void 0&&x(t);break}case`Escape`:e.preventDefault(),b(!0);break;case`Tab`:b(!1)}},E=e=>{if(o.props.onClick?.(e),!e.defaultPrevented){if(u){b(!1);return}C(`first`)}},D={...o,props:{...o.props,onClick:E,onKeyDown:w,"aria-expanded":u,"aria-controls":m,"aria-haspopup":`menu`}},O=e=>{e.onSelect(),e.type!==`checkbox`&&b(!0)};return(0,r.jsxs)(`span`,{ref:h,className:t.default.root,children:[D,u&&(0,r.jsx)(`ul`,{id:m,role:`menu`,className:e.cn(t.default.menu,i(c),l),onKeyDown:T,children:s.map((n,i)=>{if(n.type===`separator`)return(0,r.jsx)(`li`,{role:`separator`,className:t.default.separator,"aria-hidden":!0},n.id);if(n.type===`label`)return(0,r.jsx)(`li`,{role:`presentation`,className:t.default.label,children:n.label},n.id);let a=n.type===`checkbox`;return(0,r.jsx)(`li`,{role:`none`,children:(0,r.jsxs)(`button`,{ref:e=>{g.current[i]=e},type:`button`,role:a?`menuitemcheckbox`:`menuitem`,"aria-checked":a?n.checked:void 0,tabIndex:f===i?0:-1,className:e.cn(t.default.item,n.type===`item`&&n.danger&&t.default.danger,f===i&&t.default.active),disabled:n.disabled,onClick:()=>O(n),onMouseEnter:()=>p(i),children:[n.icon&&(0,r.jsx)(`span`,{"aria-hidden":!0,children:n.icon}),n.label]})},n.id)})})]})}exports.DropdownMenu=o;
1
+ const e=require("../../utils/cn.cjs"),t=require("../Portal/Portal.cjs"),n=require("../Portal/anchor-position.cjs"),r=require("./DropdownMenu.module.cjs");let i=require("react"),a=require("react/jsx-runtime");function o(e){switch(e){case`bottom-end`:return r.default.bottomEnd;case`top-start`:return r.default.topStart;case`top-end`:return r.default.topEnd;default:return r.default.bottomStart}}var s={"bottom-start":[`bottom`,`start`],"bottom-end":[`bottom`,`end`],"top-start":[`top`,`start`],"top-end":[`top`,`end`]},c=4;function l(e){return(e.type===`item`||e.type===`checkbox`)&&!e.disabled}function u({trigger:u,items:d,placement:f=`bottom-start`,portal:p=!0,className:m}){let[h,g]=(0,i.useState)(!1),[_,v]=(0,i.useState)(-1),y=(0,i.useId)(),[b,x]=(0,i.useState)(null),[S,C]=(0,i.useState)(null),w=(0,i.useRef)([]),T=(0,i.useRef)(null),E=d.map((e,t)=>l(e)?t:-1).filter(e=>e!==-1),D=(0,i.useCallback)(()=>{b?.querySelector(`[aria-haspopup='menu']`)?.focus()},[b]),O=(0,i.useCallback)(e=>{g(!1),v(-1),e&&D()},[D]),k=(0,i.useCallback)(e=>{v(e),w.current[e]?.focus()},[]),A=e=>{if(E.length===0)return;let t=(E.indexOf(_)+e+E.length)%E.length;k(E[t]??-1)},[j,M]=s[f],N=n.useAnchorPosition({anchor:b,floating:S,side:j,align:M,offset:c,enabled:p&&h});(0,i.useEffect)(()=>{if(!h||!S||T.current===null)return;let e=T.current===`last`?E.at(-1):E[0];T.current=null,e!==void 0&&k(e)},[h,S,E,k]),(0,i.useEffect)(()=>{if(!h)return;let e=e=>{let t=e.target;b?.contains(t)||S?.contains(t)||O(!1)};return window.addEventListener(`mousedown`,e),()=>window.removeEventListener(`mousedown`,e)},[h,O,b,S]);let P=e=>{T.current=e,g(!0)},F=e=>{if(u.props.onKeyDown?.(e),!e.defaultPrevented){if(e.key===`ArrowDown`||!h&&(e.key===`Enter`||e.key===` `)){e.preventDefault(),P(`first`);return}e.key===`ArrowUp`&&(e.preventDefault(),P(`last`))}},I=e=>{switch(e.key){case`ArrowDown`:e.preventDefault(),A(1);break;case`ArrowUp`:e.preventDefault(),A(-1);break;case`Home`:e.preventDefault(),E[0]!==void 0&&k(E[0]);break;case`End`:{e.preventDefault();let t=E.at(-1);t!==void 0&&k(t);break}case`Escape`:e.preventDefault(),O(!0);break;case`Tab`:O(!0)}},L=e=>{if(u.props.onClick?.(e),!e.defaultPrevented){if(h){O(!1);return}P(`first`)}},R={...u,props:{...u.props,onClick:L,onKeyDown:F,"aria-expanded":h,"aria-controls":y,"aria-haspopup":`menu`}},z=e=>{e.onSelect(),e.type!==`checkbox`&&O(!0)},B=(0,a.jsx)(`ul`,{ref:C,id:y,role:`menu`,className:e.cn(r.default.menu,p?r.default.portalled:o(f),m),style:N,onKeyDown:I,children:d.map((t,n)=>{if(t.type===`separator`)return(0,a.jsx)(`li`,{role:`separator`,className:r.default.separator,"aria-hidden":!0},t.id);if(t.type===`label`)return(0,a.jsx)(`li`,{role:`presentation`,className:r.default.label,children:t.label},t.id);let i=t.type===`checkbox`;return(0,a.jsx)(`li`,{role:`none`,children:(0,a.jsxs)(`button`,{ref:e=>{w.current[n]=e},type:`button`,role:i?`menuitemcheckbox`:`menuitem`,"aria-checked":i?t.checked:void 0,tabIndex:_===n?0:-1,className:e.cn(r.default.item,t.type===`item`&&t.danger&&r.default.danger,_===n&&r.default.active),disabled:t.disabled,onClick:()=>z(t),onMouseEnter:()=>v(n),children:[t.icon&&(0,a.jsx)(`span`,{"aria-hidden":!0,children:t.icon}),t.label]})},t.id)})});return(0,a.jsxs)(`span`,{ref:x,className:r.default.root,children:[R,h&&(p?(0,a.jsx)(t.Portal,{children:B}):B)]})}exports.DropdownMenu=u;
2
2
  //# sourceMappingURL=DropdownMenu.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"DropdownMenu.cjs","names":[],"sources":["../../../src/components/DropdownMenu/DropdownMenu.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — the body owns placement, outside-\n * click, and the whole APG menu-button keyboard model, which has to see the entire\n * entry list to know where the next stop is. Splitting it would move the focus\n * bookkeeping away from the list it indexes into.\n */\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport type { KeyboardEvent as ReactKeyboardEvent, ReactElement, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./DropdownMenu.module.css\";\n\nexport type DropdownMenuPlacement = \"bottom-start\" | \"bottom-end\" | \"top-start\" | \"top-end\";\n\nexport type DropdownMenuEntry =\n | {\n type: \"item\";\n id: string;\n label: ReactNode;\n icon?: ReactNode;\n danger?: boolean;\n disabled?: boolean;\n onSelect: () => void;\n }\n | {\n /**\n * An entry that carries on/off state.\n *\n * Rendered as `role=\"menuitemcheckbox\"` with `aria-checked`, which is how\n * a screen reader announces \"checked\" instead of leaving the state\n * invisible. Selecting it does **not** close the menu: toggling two\n * settings in a row is the ordinary case, and closing after the first\n * would make the second a second trip.\n */\n type: \"checkbox\";\n id: string;\n label: ReactNode;\n icon?: ReactNode;\n checked: boolean;\n disabled?: boolean;\n onSelect: () => void;\n }\n | { type: \"separator\"; id: string }\n | { type: \"label\"; id: string; label: ReactNode };\n\n/** The entry kinds a user can move focus to and activate. */\ntype SelectableEntry = Extract<DropdownMenuEntry, { type: \"item\" | \"checkbox\" }>;\n\nexport interface DropdownMenuProps {\n trigger: ReactElement<{\n onClick?: (e: React.MouseEvent) => void;\n onKeyDown?: (e: ReactKeyboardEvent) => void;\n \"aria-expanded\"?: boolean;\n \"aria-controls\"?: string;\n \"aria-haspopup\"?: boolean | \"menu\";\n }>;\n items: DropdownMenuEntry[];\n placement?: DropdownMenuPlacement;\n className?: string;\n}\n\nfunction placementClass(placement: DropdownMenuPlacement): string {\n switch (placement) {\n case \"bottom-end\":\n return styles.bottomEnd;\n case \"top-start\":\n return styles.topStart;\n case \"top-end\":\n return styles.topEnd;\n case \"bottom-start\":\n default:\n return styles.bottomStart;\n }\n}\n\n/** Whether an entry can take focus — an item or checkbox that is not disabled. */\nfunction isSelectable(entry: DropdownMenuEntry): entry is SelectableEntry {\n return (entry.type === \"item\" || entry.type === \"checkbox\") && !entry.disabled;\n}\n\n/**\n * Dropdown menu — a list of actions anchored to a trigger, following the\n * [APG menu button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/).\n *\n * `role=\"menu\"` is a promise about the keyboard, and this component keeps it:\n *\n * - `Enter`, `Space` or `ArrowDown` on the trigger opens and focuses the first\n * entry; `ArrowUp` opens and focuses the last.\n * - `ArrowUp` / `ArrowDown` move with wrap, `Home` / `End` jump to the ends.\n * - `Escape` closes and returns focus to the trigger, so the next `Tab` continues\n * from where the user was rather than from the top of the document.\n * - `Tab` closes the menu and lets the page's own tab order take over.\n * - Focus is managed: entries carry `tabIndex={-1}` and only the active one is\n * `0`, which is what stops `Tab` from walking the menu one entry at a time.\n *\n * Disabled entries, separators and labels are skipped by every movement.\n */\nexport function DropdownMenu({\n trigger,\n items,\n placement = \"bottom-start\",\n className,\n}: DropdownMenuProps) {\n const [open, setOpen] = useState(false);\n const [activeIndex, setActiveIndex] = useState<number>(-1);\n const id = useId();\n const rootRef = useRef<HTMLSpanElement>(null);\n const itemRefs = useRef<Array<HTMLButtonElement | null>>([]);\n const pendingFocus = useRef<\"first\" | \"last\" | null>(null);\n\n const selectable = items\n .map((entry, index) => (isSelectable(entry) ? index : -1))\n .filter((index) => index !== -1);\n\n /**\n * Focus the trigger again.\n *\n * Found through the `aria-haspopup` this component puts on it, rather than\n * through a ref: `ref` lives in different places in React 18 and 19, and a\n * consumer's custom trigger is under no obligation to forward one. The\n * attribute is on the element either way.\n *\n * @returns Nothing.\n */\n const focusTrigger = useCallback((): void => {\n rootRef.current?.querySelector<HTMLElement>(\"[aria-haspopup='menu']\")?.focus();\n }, []);\n\n const close = useCallback(\n (restoreFocus: boolean): void => {\n setOpen(false);\n setActiveIndex(-1);\n if (restoreFocus) focusTrigger();\n },\n [focusTrigger],\n );\n\n const focusIndex = useCallback((index: number): void => {\n setActiveIndex(index);\n itemRefs.current[index]?.focus();\n }, []);\n\n /**\n * Move focus to the entry `step` positions away, wrapping at both ends.\n *\n * Walks the selectable positions rather than the raw list, so separators,\n * labels and disabled entries are never a stop.\n */\n const move = (step: number): void => {\n if (selectable.length === 0) return;\n const current = selectable.indexOf(activeIndex);\n const next = (current + step + selectable.length) % selectable.length;\n focusIndex(selectable[next] ?? -1);\n };\n\n useEffect(() => {\n if (!open || pendingFocus.current === null) return;\n const target = pendingFocus.current === \"last\" ? selectable.at(-1) : selectable[0];\n pendingFocus.current = null;\n if (target !== undefined) focusIndex(target);\n }, [open, selectable, focusIndex]);\n\n useEffect(() => {\n if (!open) return;\n const onDown = (event: MouseEvent): void => {\n if (rootRef.current && !rootRef.current.contains(event.target as Node)) close(false);\n };\n window.addEventListener(\"mousedown\", onDown);\n return () => window.removeEventListener(\"mousedown\", onDown);\n }, [open, close]);\n\n const openWith = (edge: \"first\" | \"last\"): void => {\n pendingFocus.current = edge;\n setOpen(true);\n };\n\n const handleTriggerKeyDown = (event: ReactKeyboardEvent): void => {\n trigger.props.onKeyDown?.(event);\n if (event.defaultPrevented) return;\n if (event.key === \"ArrowDown\" || (!open && (event.key === \"Enter\" || event.key === \" \"))) {\n event.preventDefault();\n openWith(\"first\");\n return;\n }\n if (event.key === \"ArrowUp\") {\n event.preventDefault();\n openWith(\"last\");\n }\n };\n\n /**\n * Keyboard model of the open menu.\n *\n * Handled on the list rather than on `window`: focus is inside the menu by\n * then, so the keys arrive here by bubbling and cannot be pre-empted by an\n * unrelated global listener — which is how the arrows used to go missing in a\n * host application.\n */\n const handleMenuKeyDown = (event: ReactKeyboardEvent<HTMLUListElement>): void => {\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n move(1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n move(-1);\n break;\n case \"Home\":\n event.preventDefault();\n if (selectable[0] !== undefined) focusIndex(selectable[0]);\n break;\n case \"End\": {\n event.preventDefault();\n const last = selectable.at(-1);\n if (last !== undefined) focusIndex(last);\n break;\n }\n case \"Escape\":\n event.preventDefault();\n close(true);\n break;\n case \"Tab\":\n close(false);\n break;\n default:\n break;\n }\n };\n\n const handleTriggerClick = (event: React.MouseEvent): void => {\n trigger.props.onClick?.(event);\n if (event.defaultPrevented) return;\n if (open) {\n close(false);\n return;\n }\n openWith(\"first\");\n };\n\n const triggerClone = {\n ...trigger,\n props: {\n ...trigger.props,\n onClick: handleTriggerClick,\n onKeyDown: handleTriggerKeyDown,\n \"aria-expanded\": open,\n \"aria-controls\": id,\n \"aria-haspopup\": \"menu\" as const,\n },\n } as ReactElement;\n\n const handleSelect = (entry: SelectableEntry): void => {\n entry.onSelect();\n if (entry.type === \"checkbox\") return;\n close(true);\n };\n\n return (\n <span ref={rootRef} className={styles.root}>\n {triggerClone}\n {open && (\n <ul\n id={id}\n role=\"menu\"\n className={cn(styles.menu, placementClass(placement), className)}\n onKeyDown={handleMenuKeyDown}\n >\n {items.map((entry, index) => {\n if (entry.type === \"separator\") {\n return (\n <li\n key={entry.id}\n role=\"separator\"\n className={styles.separator}\n aria-hidden\n />\n );\n }\n if (entry.type === \"label\") {\n return (\n <li key={entry.id} role=\"presentation\" className={styles.label}>\n {entry.label}\n </li>\n );\n }\n const checkbox = entry.type === \"checkbox\";\n return (\n <li key={entry.id} role=\"none\">\n <button\n ref={(el) => {\n itemRefs.current[index] = el;\n }}\n type=\"button\"\n role={checkbox ? \"menuitemcheckbox\" : \"menuitem\"}\n aria-checked={checkbox ? entry.checked : undefined}\n tabIndex={activeIndex === index ? 0 : -1}\n className={cn(\n styles.item,\n entry.type === \"item\" && entry.danger && styles.danger,\n activeIndex === index && styles.active,\n )}\n disabled={entry.disabled}\n onClick={() => handleSelect(entry)}\n onMouseEnter={() => setActiveIndex(index)}\n >\n {entry.icon && <span aria-hidden>{entry.icon}</span>}\n {entry.label}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </span>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DropdownMenu.cjs","names":[],"sources":["../../../src/components/DropdownMenu/DropdownMenu.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — the body owns placement, outside-\n * click, and the whole APG menu-button keyboard model, which has to see the entire\n * entry list to know where the next stop is. Splitting it would move the focus\n * bookkeeping away from the list it indexes into.\n */\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport type { KeyboardEvent as ReactKeyboardEvent, ReactElement, ReactNode } from \"react\";\nimport { Portal } from \"@/components/Portal\";\nimport { useAnchorPosition } from \"@/components/Portal/anchor-position\";\nimport type { AnchorAlign, AnchorSide } from \"@/components/Portal/anchor-position\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./DropdownMenu.module.css\";\n\nexport type DropdownMenuPlacement = \"bottom-start\" | \"bottom-end\" | \"top-start\" | \"top-end\";\n\nexport type DropdownMenuEntry =\n | {\n type: \"item\";\n id: string;\n label: ReactNode;\n icon?: ReactNode;\n danger?: boolean;\n disabled?: boolean;\n onSelect: () => void;\n }\n | {\n /**\n * An entry that carries on/off state.\n *\n * Rendered as `role=\"menuitemcheckbox\"` with `aria-checked`, which is how\n * a screen reader announces \"checked\" instead of leaving the state\n * invisible. Selecting it does **not** close the menu: toggling two\n * settings in a row is the ordinary case, and closing after the first\n * would make the second a second trip.\n */\n type: \"checkbox\";\n id: string;\n label: ReactNode;\n icon?: ReactNode;\n checked: boolean;\n disabled?: boolean;\n onSelect: () => void;\n }\n | { type: \"separator\"; id: string }\n | { type: \"label\"; id: string; label: ReactNode };\n\n/** The entry kinds a user can move focus to and activate. */\ntype SelectableEntry = Extract<DropdownMenuEntry, { type: \"item\" | \"checkbox\" }>;\n\nexport interface DropdownMenuProps {\n trigger: ReactElement<{\n onClick?: (e: React.MouseEvent) => void;\n onKeyDown?: (e: ReactKeyboardEvent) => void;\n \"aria-expanded\"?: boolean;\n \"aria-controls\"?: string;\n \"aria-haspopup\"?: boolean | \"menu\";\n }>;\n items: DropdownMenuEntry[];\n placement?: DropdownMenuPlacement;\n /**\n * Render the menu in a portal, positioned against the trigger. Default `true`.\n *\n * Without it the menu is `position: absolute` inside the trigger's wrapper,\n * and any ancestor with `overflow` other than `visible` clips it. The ordinary\n * victim is the action column of a `DataTable`: its wrapper scrolls\n * horizontally, `overflow-x: auto` forces `overflow-y` to `auto` too, and the\n * menu of the last row showed one of three entries (measured in Chrome at\n * 1440 px). In a portal the menu escapes the clip, flips above the trigger\n * when there is no room below, and follows the trigger on scroll and resize.\n *\n * The portalled menu stacks at `--tempest-z-popover`, above\n * `--tempest-z-modal`, so a menu opened inside a `Modal` still paints on top.\n * `false` keeps the in-flow menu, for a container that must own the menu's\n * stacking or clipping on purpose.\n */\n portal?: boolean;\n className?: string;\n}\n\nfunction placementClass(placement: DropdownMenuPlacement): string {\n switch (placement) {\n case \"bottom-end\":\n return styles.bottomEnd;\n case \"top-start\":\n return styles.topStart;\n case \"top-end\":\n return styles.topEnd;\n case \"bottom-start\":\n default:\n return styles.bottomStart;\n }\n}\n\nconst PLACEMENT_GEOMETRY: Record<DropdownMenuPlacement, [AnchorSide, AnchorAlign]> = {\n \"bottom-start\": [\"bottom\", \"start\"],\n \"bottom-end\": [\"bottom\", \"end\"],\n \"top-start\": [\"top\", \"start\"],\n \"top-end\": [\"top\", \"end\"],\n};\n\n/** Gap between trigger and menu, in px — the in-flow CSS uses the same 4 px. */\nconst MENU_OFFSET = 4;\n\n/** Whether an entry can take focus — an item or checkbox that is not disabled. */\nfunction isSelectable(entry: DropdownMenuEntry): entry is SelectableEntry {\n return (entry.type === \"item\" || entry.type === \"checkbox\") && !entry.disabled;\n}\n\n/**\n * Dropdown menu — a list of actions anchored to a trigger, following the\n * [APG menu button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/).\n *\n * `role=\"menu\"` is a promise about the keyboard, and this component keeps it:\n *\n * - `Enter`, `Space` or `ArrowDown` on the trigger opens and focuses the first\n * entry; `ArrowUp` opens and focuses the last.\n * - `ArrowUp` / `ArrowDown` move with wrap, `Home` / `End` jump to the ends.\n * - `Escape` closes and returns focus to the trigger, so the next `Tab` continues\n * from where the user was rather than from the top of the document.\n * - `Tab` closes the menu and lets the page's own tab order take over, counted\n * from the trigger: focus goes back to the trigger synchronously and the key's\n * default action then moves on from there. Without that, a portalled menu —\n * the last thing in `body` — would hand `Tab` to whatever follows it, which\n * inside a `Modal` is outside the dialog.\n * - Focus is managed: entries carry `tabIndex={-1}` and only the active one is\n * `0`, which is what stops `Tab` from walking the menu one entry at a time.\n *\n * Disabled entries, separators and labels are skipped by every movement.\n */\nexport function DropdownMenu({\n trigger,\n items,\n placement = \"bottom-start\",\n portal = true,\n className,\n}: DropdownMenuProps) {\n const [open, setOpen] = useState(false);\n const [activeIndex, setActiveIndex] = useState<number>(-1);\n const id = useId();\n const [rootNode, setRootNode] = useState<HTMLSpanElement | null>(null);\n const [menuNode, setMenuNode] = useState<HTMLUListElement | null>(null);\n const itemRefs = useRef<Array<HTMLButtonElement | null>>([]);\n const pendingFocus = useRef<\"first\" | \"last\" | null>(null);\n\n const selectable = items\n .map((entry, index) => (isSelectable(entry) ? index : -1))\n .filter((index) => index !== -1);\n\n /**\n * Focus the trigger again.\n *\n * Found through the `aria-haspopup` this component puts on it, rather than\n * through a ref: `ref` lives in different places in React 18 and 19, and a\n * consumer's custom trigger is under no obligation to forward one. The\n * attribute is on the element either way.\n *\n * @returns Nothing.\n */\n const focusTrigger = useCallback((): void => {\n rootNode?.querySelector<HTMLElement>(\"[aria-haspopup='menu']\")?.focus();\n }, [rootNode]);\n\n const close = useCallback(\n (restoreFocus: boolean): void => {\n setOpen(false);\n setActiveIndex(-1);\n if (restoreFocus) focusTrigger();\n },\n [focusTrigger],\n );\n\n const focusIndex = useCallback((index: number): void => {\n setActiveIndex(index);\n itemRefs.current[index]?.focus();\n }, []);\n\n /**\n * Move focus to the entry `step` positions away, wrapping at both ends.\n *\n * Walks the selectable positions rather than the raw list, so separators,\n * labels and disabled entries are never a stop.\n */\n const move = (step: number): void => {\n if (selectable.length === 0) return;\n const current = selectable.indexOf(activeIndex);\n const next = (current + step + selectable.length) % selectable.length;\n focusIndex(selectable[next] ?? -1);\n };\n\n const [side, align] = PLACEMENT_GEOMETRY[placement];\n const floatingStyle = useAnchorPosition({\n anchor: rootNode,\n floating: menuNode,\n side,\n align,\n offset: MENU_OFFSET,\n enabled: portal && open,\n });\n\n /**\n * Focus the entry the opening key asked for.\n *\n * Keyed off the menu node rather than off `open` alone: in a portal the menu\n * mounts one commit after `open` flips, and an effect that ran on `open` would\n * find no entries to focus.\n */\n useEffect(() => {\n if (!open || !menuNode || pendingFocus.current === null) return;\n const target = pendingFocus.current === \"last\" ? selectable.at(-1) : selectable[0];\n pendingFocus.current = null;\n if (target !== undefined) focusIndex(target);\n }, [open, menuNode, selectable, focusIndex]);\n\n /**\n * Close on a press outside both the trigger and the menu.\n *\n * The menu is checked separately because in a portal it is not inside the\n * trigger's wrapper: testing the wrapper alone would treat a press on an entry\n * as outside and close the menu before the entry's click lands.\n */\n useEffect(() => {\n if (!open) return;\n const onDown = (event: MouseEvent): void => {\n const target = event.target as Node;\n if (rootNode?.contains(target) || menuNode?.contains(target)) return;\n close(false);\n };\n window.addEventListener(\"mousedown\", onDown);\n return () => window.removeEventListener(\"mousedown\", onDown);\n }, [open, close, rootNode, menuNode]);\n\n const openWith = (edge: \"first\" | \"last\"): void => {\n pendingFocus.current = edge;\n setOpen(true);\n };\n\n const handleTriggerKeyDown = (event: ReactKeyboardEvent): void => {\n trigger.props.onKeyDown?.(event);\n if (event.defaultPrevented) return;\n if (event.key === \"ArrowDown\" || (!open && (event.key === \"Enter\" || event.key === \" \"))) {\n event.preventDefault();\n openWith(\"first\");\n return;\n }\n if (event.key === \"ArrowUp\") {\n event.preventDefault();\n openWith(\"last\");\n }\n };\n\n /**\n * Keyboard model of the open menu.\n *\n * Handled on the list rather than on `window`: focus is inside the menu by\n * then, so the keys arrive here by bubbling and cannot be pre-empted by an\n * unrelated global listener — which is how the arrows used to go missing in a\n * host application.\n */\n const handleMenuKeyDown = (event: ReactKeyboardEvent<HTMLUListElement>): void => {\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n move(1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n move(-1);\n break;\n case \"Home\":\n event.preventDefault();\n if (selectable[0] !== undefined) focusIndex(selectable[0]);\n break;\n case \"End\": {\n event.preventDefault();\n const last = selectable.at(-1);\n if (last !== undefined) focusIndex(last);\n break;\n }\n case \"Escape\":\n event.preventDefault();\n close(true);\n break;\n case \"Tab\":\n close(true);\n break;\n default:\n break;\n }\n };\n\n const handleTriggerClick = (event: React.MouseEvent): void => {\n trigger.props.onClick?.(event);\n if (event.defaultPrevented) return;\n if (open) {\n close(false);\n return;\n }\n openWith(\"first\");\n };\n\n const triggerClone = {\n ...trigger,\n props: {\n ...trigger.props,\n onClick: handleTriggerClick,\n onKeyDown: handleTriggerKeyDown,\n \"aria-expanded\": open,\n \"aria-controls\": id,\n \"aria-haspopup\": \"menu\" as const,\n },\n } as ReactElement;\n\n const handleSelect = (entry: SelectableEntry): void => {\n entry.onSelect();\n if (entry.type === \"checkbox\") return;\n close(true);\n };\n\n const menu = (\n <ul\n ref={setMenuNode}\n id={id}\n role=\"menu\"\n className={cn(\n styles.menu,\n portal ? styles.portalled : placementClass(placement),\n className,\n )}\n style={floatingStyle}\n onKeyDown={handleMenuKeyDown}\n >\n {items.map((entry, index) => {\n if (entry.type === \"separator\") {\n return (\n <li\n key={entry.id}\n role=\"separator\"\n className={styles.separator}\n aria-hidden\n />\n );\n }\n if (entry.type === \"label\") {\n return (\n <li key={entry.id} role=\"presentation\" className={styles.label}>\n {entry.label}\n </li>\n );\n }\n const checkbox = entry.type === \"checkbox\";\n return (\n <li key={entry.id} role=\"none\">\n <button\n ref={(el) => {\n itemRefs.current[index] = el;\n }}\n type=\"button\"\n role={checkbox ? \"menuitemcheckbox\" : \"menuitem\"}\n aria-checked={checkbox ? entry.checked : undefined}\n tabIndex={activeIndex === index ? 0 : -1}\n className={cn(\n styles.item,\n entry.type === \"item\" && entry.danger && styles.danger,\n activeIndex === index && styles.active,\n )}\n disabled={entry.disabled}\n onClick={() => handleSelect(entry)}\n onMouseEnter={() => setActiveIndex(index)}\n >\n {entry.icon && <span aria-hidden>{entry.icon}</span>}\n {entry.label}\n </button>\n </li>\n );\n })}\n </ul>\n );\n\n return (\n <span ref={setRootNode} className={styles.root}>\n {triggerClone}\n {open && (portal ? <Portal>{menu}</Portal> : menu)}\n </span>\n );\n}\n"],"mappings":"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"}