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,51 +1,72 @@
1
1
  import { cn as e } from "../../utils/cn.js";
2
- import t from "./Popover.module.js";
3
- import { useCallback as n, useEffect as r, useId as i, useRef as a, useState as o } from "react";
4
- import { jsx as s, jsxs as c } from "react/jsx-runtime";
2
+ import { useEscapeLayer as t } from "../Portal/escape-layer.js";
3
+ import { Portal as n } from "../Portal/Portal.js";
4
+ import { useAnchorPosition as r } from "../Portal/anchor-position.js";
5
+ import { usePortalTabOrder as i } from "../Portal/tab-bridge.js";
6
+ import a from "./Popover.module.js";
7
+ import { useCallback as o, useEffect as s, useId as c, useState as l } from "react";
8
+ import { jsx as u, jsxs as d } from "react/jsx-runtime";
5
9
  //#region src/components/Popover/Popover.tsx
6
- function l({ trigger: l, children: u, placement: d = "bottom", open: f, onOpenChange: p, defaultOpen: m = !1, closeOnEsc: h = !0, closeOnOutsideClick: g = !0, className: _ }) {
7
- let v = f !== void 0, [y, b] = o(m), x = v ? f : y, S = i(), C = a(null), w = n((e) => {
8
- v || b(e), p?.(e);
9
- }, [v, p]);
10
- r(() => {
11
- if (!x) return;
10
+ var f = 8;
11
+ function p({ trigger: p, children: m, placement: h = "bottom", portal: g = !0, open: _, onOpenChange: v, defaultOpen: y = !1, closeOnEsc: b = !0, closeOnOutsideClick: x = !0, className: S }) {
12
+ let C = _ !== void 0, [w, T] = l(y), E = C ? _ : w, D = c(), [O, k] = l(null), [A, j] = l(null), M = r({
13
+ anchor: O,
14
+ floating: A,
15
+ side: h,
16
+ align: "center",
17
+ offset: f,
18
+ enabled: g && E
19
+ });
20
+ i({
21
+ enabled: g && E,
22
+ anchor: O,
23
+ panel: A
24
+ });
25
+ let N = o((e) => {
26
+ C || T(e), v?.(e);
27
+ }, [C, v]);
28
+ t(E && b, () => N(!1)), s(() => {
29
+ if (!E) return;
12
30
  let e = (e) => {
13
- h && e.key === "Escape" && w(!1);
14
- }, t = (e) => {
15
- g && C.current && !C.current.contains(e.target) && w(!1);
31
+ if (!x) return;
32
+ let t = e.target;
33
+ O?.contains(t) || A?.contains(t) || N(!1);
16
34
  };
17
- return window.addEventListener("keydown", e), window.addEventListener("mousedown", t), () => {
18
- window.removeEventListener("keydown", e), window.removeEventListener("mousedown", t);
35
+ return window.addEventListener("mousedown", e), () => {
36
+ window.removeEventListener("mousedown", e);
19
37
  };
20
38
  }, [
39
+ E,
21
40
  x,
22
- h,
23
- g,
24
- w
41
+ N,
42
+ O,
43
+ A
25
44
  ]);
26
- let T = (e) => {
27
- l.props.onClick?.(e), w(!x);
28
- }, E = {
29
- ...l,
45
+ let P = (e) => {
46
+ p.props.onClick?.(e), N(!E);
47
+ }, F = {
48
+ ...p,
30
49
  props: {
31
- ...l.props,
32
- onClick: T,
33
- "aria-expanded": x,
34
- "aria-controls": S
50
+ ...p.props,
51
+ onClick: P,
52
+ "aria-expanded": E,
53
+ "aria-controls": D
35
54
  }
36
- };
37
- return /* @__PURE__ */ c("span", {
38
- ref: C,
39
- className: t.root,
40
- children: [E, x && /* @__PURE__ */ s("div", {
41
- id: S,
42
- role: "dialog",
43
- className: e(t.popover, t[d], _),
44
- children: u
45
- })]
55
+ }, I = /* @__PURE__ */ u("div", {
56
+ ref: j,
57
+ id: D,
58
+ role: "dialog",
59
+ className: e(a.popover, g ? a.portalled : a[h], S),
60
+ style: M,
61
+ children: m
62
+ });
63
+ return /* @__PURE__ */ d("span", {
64
+ ref: k,
65
+ className: a.root,
66
+ children: [F, E && (g ? /* @__PURE__ */ u(n, { children: I }) : I)]
46
67
  });
47
68
  }
48
69
  //#endregion
49
- export { l as Popover };
70
+ export { p as Popover };
50
71
 
51
72
  //# sourceMappingURL=Popover.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Popover.js","names":[],"sources":["../../../src/components/Popover/Popover.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — trigger and children are the two halves, placement\n * is where it lands, and open/onOpenChange/defaultOpen is the standard controlled-\n * or-not pair. closeOnEsc and closeOnOutsideClick are separate because a popover\n * holding a form wants one and not the other.\n */\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Popover.module.css\";\n\nexport type PopoverPlacement = \"top\" | \"bottom\" | \"left\" | \"right\";\n\nexport interface PopoverProps {\n /** Trigger element. Receives `onClick`/`aria-expanded`/`aria-controls`. */\n trigger: ReactElement<{\n onClick?: (e: React.MouseEvent) => void;\n \"aria-expanded\"?: boolean;\n \"aria-controls\"?: string;\n }>;\n children: ReactNode;\n placement?: PopoverPlacement;\n /** Controlled open state. */\n open?: boolean;\n /** Called when the user toggles or dismisses. */\n onOpenChange?: (open: boolean) => void;\n /** Default open state for uncontrolled usage. */\n defaultOpen?: boolean;\n /** Close on Escape. Default true. */\n closeOnEsc?: boolean;\n /** Close on outside click. Default true. */\n closeOnOutsideClick?: boolean;\n className?: string;\n}\n\n/**\n * Lightweight popover. Renders a positioned panel anchored to a trigger,\n * dismissed on outside click / Escape. Does NOT include collision detection\n * or smart positioning — pair with Floating UI if you need that.\n */\nexport function Popover({\n trigger,\n children,\n placement = \"bottom\",\n open,\n onOpenChange,\n defaultOpen = false,\n closeOnEsc = true,\n closeOnOutsideClick = true,\n className,\n}: PopoverProps) {\n const isControlled = open !== undefined;\n const [internalOpen, setInternalOpen] = useState<boolean>(defaultOpen);\n const isOpen = isControlled ? open : internalOpen;\n const id = useId();\n const rootRef = useRef<HTMLSpanElement>(null);\n\n const setOpen = useCallback(\n (next: boolean): void => {\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n },\n [isControlled, onOpenChange],\n );\n\n useEffect(() => {\n if (!isOpen) return;\n const onKey = (event: KeyboardEvent): void => {\n if (closeOnEsc && event.key === \"Escape\") setOpen(false);\n };\n const onDown = (event: MouseEvent): void => {\n if (!closeOnOutsideClick) return;\n if (rootRef.current && !rootRef.current.contains(event.target as Node)) {\n setOpen(false);\n }\n };\n window.addEventListener(\"keydown\", onKey);\n window.addEventListener(\"mousedown\", onDown);\n return () => {\n window.removeEventListener(\"keydown\", onKey);\n window.removeEventListener(\"mousedown\", onDown);\n };\n }, [isOpen, closeOnEsc, closeOnOutsideClick, setOpen]);\n\n const handleTriggerClick = (event: React.MouseEvent): void => {\n trigger.props.onClick?.(event);\n setOpen(!isOpen);\n };\n\n const triggerClone = {\n ...trigger,\n props: {\n ...trigger.props,\n onClick: handleTriggerClick,\n \"aria-expanded\": isOpen,\n \"aria-controls\": id,\n },\n } as ReactElement;\n\n return (\n <span ref={rootRef} className={styles.root}>\n {triggerClone}\n {isOpen && (\n <div\n id={id}\n role=\"dialog\"\n className={cn(styles.popover, styles[placement], className)}\n >\n {children}\n </div>\n )}\n </span>\n );\n}\n"],"mappings":";;;;;AAwCA,SAAgB,EAAQ,EACpB,YACA,aACA,eAAY,UACZ,SACA,iBACA,iBAAc,IACd,gBAAa,IACb,yBAAsB,IACtB,gBACa;CACb,IAAM,IAAe,MAAS,KAAA,GACxB,CAAC,GAAc,KAAmB,EAAkB,CAAW,GAC/D,IAAS,IAAe,IAAO,GAC/B,IAAK,EAAM,GACX,IAAU,EAAwB,IAAI,GAEtC,IAAU,GACX,MAAwB;EAErB,AADK,KAAc,EAAgB,CAAI,GACvC,IAAe,CAAI;CACvB,GACA,CAAC,GAAc,CAAY,CAC/B;CAEA,QAAgB;EACZ,IAAI,CAAC,GAAQ;EACb,IAAM,KAAS,MAA+B;GAC1C,AAAI,KAAc,EAAM,QAAQ,YAAU,EAAQ,EAAK;EAC3D,GACM,KAAU,MAA4B;GACnC,KACD,EAAQ,WAAW,CAAC,EAAQ,QAAQ,SAAS,EAAM,MAAc,KACjE,EAAQ,EAAK;EAErB;EAGA,OAFA,OAAO,iBAAiB,WAAW,CAAK,GACxC,OAAO,iBAAiB,aAAa,CAAM,SAC9B;GAET,AADA,OAAO,oBAAoB,WAAW,CAAK,GAC3C,OAAO,oBAAoB,aAAa,CAAM;EAClD;CACJ,GAAG;EAAC;EAAQ;EAAY;EAAqB;CAAO,CAAC;CAErD,IAAM,KAAsB,MAAkC;EAE1D,AADA,EAAQ,MAAM,UAAU,CAAK,GAC7B,EAAQ,CAAC,CAAM;CACnB,GAEM,IAAe;EACjB,GAAG;EACH,OAAO;GACH,GAAG,EAAQ;GACX,SAAS;GACT,iBAAiB;GACjB,iBAAiB;EACrB;CACJ;CAEA,OACI,kBAAC,QAAD;EAAM,KAAK;EAAS,WAAW,EAAO;EAAtC,UAAA,CACK,GACA,KACG,kBAAC,OAAD;GACQ;GACJ,MAAK;GACL,WAAW,EAAG,EAAO,SAAS,EAAO,IAAY,CAAS;GAEzD;EACA,CAAA,CAEP;;AAEd"}
1
+ {"version":3,"file":"Popover.js","names":[],"sources":["../../../src/components/Popover/Popover.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — trigger and children are the two halves, placement\n * is where it lands, portal is whether it escapes the tree, and open/onOpenChange/defaultOpen is the standard controlled-\n * or-not pair. closeOnEsc and closeOnOutsideClick are separate because a popover\n * holding a form wants one and not the other.\n */\nimport { useCallback, useEffect, useId, useState } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { Portal } from \"@/components/Portal\";\nimport { useAnchorPosition } from \"@/components/Portal/anchor-position\";\nimport { useEscapeLayer } from \"@/components/Portal/escape-layer\";\nimport { usePortalTabOrder } from \"@/components/Portal/tab-bridge\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Popover.module.css\";\n\nexport type PopoverPlacement = \"top\" | \"bottom\" | \"left\" | \"right\";\n\nexport interface PopoverProps {\n /** Trigger element. Receives `onClick`/`aria-expanded`/`aria-controls`. */\n trigger: ReactElement<{\n onClick?: (e: React.MouseEvent) => void;\n \"aria-expanded\"?: boolean;\n \"aria-controls\"?: string;\n }>;\n children: ReactNode;\n placement?: PopoverPlacement;\n /**\n * Render the panel in a portal, positioned against the trigger. Default `true`.\n *\n * In flow, any ancestor with `overflow` other than `visible` clips the panel —\n * inside a `DataTable` cell only 3% of it was visible (measured in Chrome at\n * 1440 px). In a portal it escapes the clip, flips to the opposite side when\n * the requested one has no room, and follows the trigger on scroll and resize.\n * `false` keeps the in-flow panel.\n */\n portal?: boolean;\n /** Controlled open state. */\n open?: boolean;\n /** Called when the user toggles or dismisses. */\n onOpenChange?: (open: boolean) => void;\n /** Default open state for uncontrolled usage. */\n defaultOpen?: boolean;\n /** Close on Escape. Default true. */\n closeOnEsc?: boolean;\n /** Close on outside click. Default true. */\n closeOnOutsideClick?: boolean;\n className?: string;\n}\n\n/** Gap between trigger and panel, in px — the in-flow CSS uses the same 8 px. */\nconst POPOVER_OFFSET = 8;\n\n/**\n * Lightweight popover. Renders a panel anchored to a trigger, dismissed on\n * outside click / Escape.\n *\n * Portalled by default, with viewport-aware placement: the panel flips to the\n * opposite side when the requested one overflows, and is clamped inside the\n * viewport otherwise.\n */\nexport function Popover({\n trigger,\n children,\n placement = \"bottom\",\n portal = true,\n open,\n onOpenChange,\n defaultOpen = false,\n closeOnEsc = true,\n closeOnOutsideClick = true,\n className,\n}: PopoverProps) {\n const isControlled = open !== undefined;\n const [internalOpen, setInternalOpen] = useState<boolean>(defaultOpen);\n const isOpen = isControlled ? open : internalOpen;\n const id = useId();\n const [rootNode, setRootNode] = useState<HTMLSpanElement | null>(null);\n const [panelNode, setPanelNode] = useState<HTMLDivElement | null>(null);\n const floatingStyle = useAnchorPosition({\n anchor: rootNode,\n floating: panelNode,\n side: placement,\n align: \"center\",\n offset: POPOVER_OFFSET,\n enabled: portal && isOpen,\n });\n usePortalTabOrder({ enabled: portal && isOpen, anchor: rootNode, panel: panelNode });\n\n const setOpen = useCallback(\n (next: boolean): void => {\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n },\n [isControlled, onOpenChange],\n );\n\n useEscapeLayer(isOpen && closeOnEsc, () => setOpen(false));\n\n /**\n * Dismiss on a press outside both trigger and panel.\n *\n * The panel is checked on its own because in a portal it is not inside the\n * trigger's wrapper, and a press inside it — on a form field, say — would\n * otherwise count as outside.\n */\n useEffect(() => {\n if (!isOpen) return;\n const onDown = (event: MouseEvent): void => {\n if (!closeOnOutsideClick) return;\n const target = event.target as Node;\n if (rootNode?.contains(target) || panelNode?.contains(target)) return;\n setOpen(false);\n };\n window.addEventListener(\"mousedown\", onDown);\n return () => {\n window.removeEventListener(\"mousedown\", onDown);\n };\n }, [isOpen, closeOnOutsideClick, setOpen, rootNode, panelNode]);\n\n const handleTriggerClick = (event: React.MouseEvent): void => {\n trigger.props.onClick?.(event);\n setOpen(!isOpen);\n };\n\n const triggerClone = {\n ...trigger,\n props: {\n ...trigger.props,\n onClick: handleTriggerClick,\n \"aria-expanded\": isOpen,\n \"aria-controls\": id,\n },\n } as ReactElement;\n\n const panel = (\n <div\n ref={setPanelNode}\n id={id}\n role=\"dialog\"\n className={cn(styles.popover, portal ? styles.portalled : styles[placement], className)}\n style={floatingStyle}\n >\n {children}\n </div>\n );\n\n return (\n <span ref={setRootNode} className={styles.root}>\n {triggerClone}\n {isOpen && (portal ? <Portal>{panel}</Portal> : panel)}\n </span>\n );\n}\n"],"mappings":";;;;;;;;;AAkDA,IAAM,IAAiB;AAUvB,SAAgB,EAAQ,EACpB,YACA,aACA,eAAY,UACZ,YAAS,IACT,SACA,iBACA,iBAAc,IACd,gBAAa,IACb,yBAAsB,IACtB,gBACa;CACb,IAAM,IAAe,MAAS,KAAA,GACxB,CAAC,GAAc,KAAmB,EAAkB,CAAW,GAC/D,IAAS,IAAe,IAAO,GAC/B,IAAK,EAAM,GACX,CAAC,GAAU,KAAe,EAAiC,IAAI,GAC/D,CAAC,GAAW,KAAgB,EAAgC,IAAI,GAChE,IAAgB,EAAkB;EACpC,QAAQ;EACR,UAAU;EACV,MAAM;EACN,OAAO;EACP,QAAQ;EACR,SAAS,KAAU;CACvB,CAAC;CACD,EAAkB;EAAE,SAAS,KAAU;EAAQ,QAAQ;EAAU,OAAO;CAAU,CAAC;CAEnF,IAAM,IAAU,GACX,MAAwB;EAErB,AADK,KAAc,EAAgB,CAAI,GACvC,IAAe,CAAI;CACvB,GACA,CAAC,GAAc,CAAY,CAC/B;CAWA,AATA,EAAe,KAAU,SAAkB,EAAQ,EAAK,CAAC,GASzD,QAAgB;EACZ,IAAI,CAAC,GAAQ;EACb,IAAM,KAAU,MAA4B;GACxC,IAAI,CAAC,GAAqB;GAC1B,IAAM,IAAS,EAAM;GACjB,GAAU,SAAS,CAAM,KAAK,GAAW,SAAS,CAAM,KAC5D,EAAQ,EAAK;EACjB;EAEA,OADA,OAAO,iBAAiB,aAAa,CAAM,SAC9B;GACT,OAAO,oBAAoB,aAAa,CAAM;EAClD;CACJ,GAAG;EAAC;EAAQ;EAAqB;EAAS;EAAU;CAAS,CAAC;CAE9D,IAAM,KAAsB,MAAkC;EAE1D,AADA,EAAQ,MAAM,UAAU,CAAK,GAC7B,EAAQ,CAAC,CAAM;CACnB,GAEM,IAAe;EACjB,GAAG;EACH,OAAO;GACH,GAAG,EAAQ;GACX,SAAS;GACT,iBAAiB;GACjB,iBAAiB;EACrB;CACJ,GAEM,IACF,kBAAC,OAAD;EACI,KAAK;EACD;EACJ,MAAK;EACL,WAAW,EAAG,EAAO,SAAS,IAAS,EAAO,YAAY,EAAO,IAAY,CAAS;EACtF,OAAO;EAEN;CACA,CAAA;CAGT,OACI,kBAAC,QAAD;EAAM,KAAK;EAAa,WAAW,EAAO;EAA1C,UAAA,CACK,GACA,MAAW,IAAS,kBAAC,GAAD,EAAA,UAAS,EAAc,CAAA,IAAI,EAC9C;;AAEd"}
@@ -1,3 +1,3 @@
1
1
  require("../../styles/component/Popover.css");
2
- var e=`tempest_root_vEMon`,t=`tempest_popover_E9K9X`,n=`tempest_tempest-popover-in_W8miP`,r=`tempest_top_TYlrh`,i=`tempest_bottom_wGs70`,a=`tempest_left_h7fCr`,o=`tempest_right_--bJW`,s={root:e,popover:t,tempestPopoverIn:n,top:r,bottom:i,left:a,right:o};exports.bottom=i,exports.default=s,exports.left=a,exports.popover=t,exports.right=o,exports.root=e,exports.tempestPopoverIn=n,exports.top=r;
2
+ var e=`tempest_root_vEMon`,t=`tempest_popover_E9K9X`,n=`tempest_tempest-popover-in_W8miP`,r=`tempest_portalled_pZ553`,i=`tempest_top_TYlrh`,a=`tempest_bottom_wGs70`,o=`tempest_left_h7fCr`,s=`tempest_right_--bJW`,c={root:e,popover:t,tempestPopoverIn:n,portalled:r,top:i,bottom:a,left:o,right:s};exports.bottom=a,exports.default=c,exports.left=o,exports.popover=t,exports.portalled=r,exports.right=s,exports.root=e,exports.tempestPopoverIn=n,exports.top=i;
3
3
  //# sourceMappingURL=Popover.module.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"Popover.module.cjs","names":[],"sources":["../../../src/components/Popover/Popover.module.css"],"sourcesContent":[".root {\n position: relative;\n display: inline-flex;\n}\n\n.popover {\n position: absolute;\n z-index: var(--tempest-z-popover);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n box-shadow: var(--tempest-shadow-lg);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n min-width: 200px;\n max-width: 320px;\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-base);\n line-height: var(--tempest-leading-snug);\n animation: tempest-popover-in var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n.popover:focus {\n outline: none;\n}\n\n/* Placements (center-aligned across the trigger). */\n.top {\n bottom: calc(100% + 8px);\n left: 50%;\n transform: translateX(-50%);\n}\n.bottom {\n top: calc(100% + 8px);\n left: 50%;\n transform: translateX(-50%);\n}\n.left {\n right: calc(100% + 8px);\n top: 50%;\n transform: translateY(-50%);\n}\n.right {\n left: calc(100% + 8px);\n top: 50%;\n transform: translateY(-50%);\n}\n\n@keyframes tempest-popover-in {\n from {\n opacity: 0;\n transform: translate(var(--tempest-popover-tx, -50%), 4px);\n }\n to {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .popover {\n animation: none;\n }\n}\n"],"mappings":";"}
1
+ {"version":3,"file":"Popover.module.cjs","names":[],"sources":["../../../src/components/Popover/Popover.module.css"],"sourcesContent":[".root {\n position: relative;\n display: inline-flex;\n}\n\n.popover {\n position: absolute;\n z-index: var(--tempest-z-popover);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n box-shadow: var(--tempest-shadow-lg);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n min-width: 200px;\n max-width: 320px;\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-base);\n line-height: var(--tempest-leading-snug);\n animation: tempest-popover-in var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n.popover:focus {\n outline: none;\n}\n\n.portalled {\n --tempest-popover-tx: 0;\n}\n\n/* Placements (center-aligned across the trigger). */\n.top {\n bottom: calc(100% + 8px);\n left: 50%;\n transform: translateX(-50%);\n}\n.bottom {\n top: calc(100% + 8px);\n left: 50%;\n transform: translateX(-50%);\n}\n.left {\n right: calc(100% + 8px);\n top: 50%;\n transform: translateY(-50%);\n}\n.right {\n left: calc(100% + 8px);\n top: 50%;\n transform: translateY(-50%);\n}\n\n@keyframes tempest-popover-in {\n from {\n opacity: 0;\n transform: translate(var(--tempest-popover-tx, -50%), 4px);\n }\n to {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .popover {\n animation: none;\n }\n}\n"],"mappings":";"}
@@ -1,15 +1,16 @@
1
1
  import "../../styles/component/Popover.css";
2
2
  //#region src/components/Popover/Popover.module.css
3
- var e = "tempest_root_vEMon", t = "tempest_popover_E9K9X", n = "tempest_tempest-popover-in_W8miP", r = "tempest_top_TYlrh", i = "tempest_bottom_wGs70", a = "tempest_left_h7fCr", o = "tempest_right_--bJW", s = {
3
+ var e = "tempest_root_vEMon", t = "tempest_popover_E9K9X", n = "tempest_tempest-popover-in_W8miP", r = "tempest_portalled_pZ553", i = "tempest_top_TYlrh", a = "tempest_bottom_wGs70", o = "tempest_left_h7fCr", s = "tempest_right_--bJW", c = {
4
4
  root: e,
5
5
  popover: t,
6
6
  tempestPopoverIn: n,
7
- top: r,
8
- bottom: i,
9
- left: a,
10
- right: o
7
+ portalled: r,
8
+ top: i,
9
+ bottom: a,
10
+ left: o,
11
+ right: s
11
12
  };
12
13
  //#endregion
13
- export { i as bottom, s as default, a as left, t as popover, o as right, e as root, n as tempestPopoverIn, r as top };
14
+ export { a as bottom, c as default, o as left, t as popover, r as portalled, s as right, e as root, n as tempestPopoverIn, i as top };
14
15
 
15
16
  //# sourceMappingURL=Popover.module.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Popover.module.js","names":[],"sources":["../../../src/components/Popover/Popover.module.css"],"sourcesContent":[".root {\n position: relative;\n display: inline-flex;\n}\n\n.popover {\n position: absolute;\n z-index: var(--tempest-z-popover);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n box-shadow: var(--tempest-shadow-lg);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n min-width: 200px;\n max-width: 320px;\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-base);\n line-height: var(--tempest-leading-snug);\n animation: tempest-popover-in var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n.popover:focus {\n outline: none;\n}\n\n/* Placements (center-aligned across the trigger). */\n.top {\n bottom: calc(100% + 8px);\n left: 50%;\n transform: translateX(-50%);\n}\n.bottom {\n top: calc(100% + 8px);\n left: 50%;\n transform: translateX(-50%);\n}\n.left {\n right: calc(100% + 8px);\n top: 50%;\n transform: translateY(-50%);\n}\n.right {\n left: calc(100% + 8px);\n top: 50%;\n transform: translateY(-50%);\n}\n\n@keyframes tempest-popover-in {\n from {\n opacity: 0;\n transform: translate(var(--tempest-popover-tx, -50%), 4px);\n }\n to {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .popover {\n animation: none;\n }\n}\n"],"mappings":";"}
1
+ {"version":3,"file":"Popover.module.js","names":[],"sources":["../../../src/components/Popover/Popover.module.css"],"sourcesContent":[".root {\n position: relative;\n display: inline-flex;\n}\n\n.popover {\n position: absolute;\n z-index: var(--tempest-z-popover);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n box-shadow: var(--tempest-shadow-lg);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n min-width: 200px;\n max-width: 320px;\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-base);\n line-height: var(--tempest-leading-snug);\n animation: tempest-popover-in var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n.popover:focus {\n outline: none;\n}\n\n.portalled {\n --tempest-popover-tx: 0;\n}\n\n/* Placements (center-aligned across the trigger). */\n.top {\n bottom: calc(100% + 8px);\n left: 50%;\n transform: translateX(-50%);\n}\n.bottom {\n top: calc(100% + 8px);\n left: 50%;\n transform: translateX(-50%);\n}\n.left {\n right: calc(100% + 8px);\n top: 50%;\n transform: translateY(-50%);\n}\n.right {\n left: calc(100% + 8px);\n top: 50%;\n transform: translateY(-50%);\n}\n\n@keyframes tempest-popover-in {\n from {\n opacity: 0;\n transform: translate(var(--tempest-popover-tx, -50%), 4px);\n }\n to {\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .popover {\n animation: none;\n }\n}\n"],"mappings":";"}
@@ -0,0 +1,2 @@
1
+ let e=require("react");var t={top:`bottom`,bottom:`top`,left:`right`,right:`left`};function n(e,t,n){return Math.max(t,Math.min(e,Math.max(t,n)))}function r(e,t){let{anchor:n,width:r,height:i,offset:a}=e;switch(t){case`top`:return n.top-a-i;case`bottom`:return n.top+n.height+a;case`left`:return n.left-a-r;default:return n.left+n.width+a}}function i(e,t){let{width:n,height:i,viewportWidth:a,viewportHeight:o,margin:s}=e,c=r(e,t),l=t===`top`||t===`bottom`?i:n,u=t===`top`||t===`bottom`?o:a;return c>=s&&c+l<=u-s}function a(e,t,n,r){return r===`start`?e:r===`end`?e+t-n:e+(t-n)/2}function o(e){let{anchor:o,width:s,height:c,viewportWidth:l,viewportHeight:u,align:d,margin:f}=e,p=e.side,m=t[p],h=!i(e,p)&&i(e,m)?m:p,g=h===`top`||h===`bottom`,_=r(e,h),v=g?_:a(o.top,o.height,c,d),y=g?a(o.left,o.width,s,d):_;return{top:n(v,f,u-c-f),left:n(y,f,l-s-f),side:h}}function s({anchor:t,floating:n,side:r,align:i,offset:a,enabled:s,matchWidth:c=!1}){let[l,u]=(0,e.useState)(null);if((0,e.useLayoutEffect)(()=>{if(!s||!t||!n){u(null);return}let e=()=>{let e=t.getBoundingClientRect(),s=c?e.width:void 0;s!==void 0&&n.style.setProperty(`width`,`${s}px`);let l={...o({anchor:{top:e.top,left:e.left,width:e.width,height:e.height},width:n.offsetWidth,height:n.offsetHeight,viewportWidth:window.innerWidth,viewportHeight:window.innerHeight,side:r,align:i,offset:a,margin:8}),width:s};u(e=>e&&e.top===l.top&&e.left===l.left&&e.side===l.side&&e.width===l.width?e:l)};e();let l=0,d=()=>{l===0&&(l=requestAnimationFrame(()=>{l=0,e()}))};window.addEventListener(`scroll`,d,{capture:!0,passive:!0}),window.addEventListener(`resize`,d);let f=typeof ResizeObserver>`u`?null:new ResizeObserver(d);return f?.observe(n),f?.observe(t),()=>{f?.disconnect(),l!==0&&cancelAnimationFrame(l),window.removeEventListener(`scroll`,d,{capture:!0}),window.removeEventListener(`resize`,d)}},[s,t,n,r,i,a,c]),s)return{position:`fixed`,top:l?.top??0,left:l?.left??0,width:l?.width}}exports.computeAnchorPosition=o,exports.useAnchorPosition=s;
2
+ //# sourceMappingURL=anchor-position.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anchor-position.cjs","names":[],"sources":["../../../src/components/Portal/anchor-position.ts"],"sourcesContent":["import { useLayoutEffect, useState } from \"react\";\nimport type { CSSProperties } from \"react\";\n\n/** Side of the anchor a floating layer is placed on. */\nexport type AnchorSide = \"top\" | \"bottom\" | \"left\" | \"right\";\n\n/** Alignment of the floating layer along the anchor's edge. */\nexport type AnchorAlign = \"start\" | \"center\" | \"end\";\n\n/** The viewport-relative box a placement is computed from. */\nexport interface AnchorRect {\n top: number;\n left: number;\n width: number;\n height: number;\n}\n\n/** Everything {@link computeAnchorPosition} needs, all in CSS pixels. */\nexport interface AnchorGeometry {\n anchor: AnchorRect;\n width: number;\n height: number;\n viewportWidth: number;\n viewportHeight: number;\n side: AnchorSide;\n align: AnchorAlign;\n offset: number;\n margin: number;\n}\n\n/** A resolved placement: viewport coordinates plus the side actually used. */\nexport interface AnchorPlacement {\n top: number;\n left: number;\n side: AnchorSide;\n}\n\n/** What {@link useAnchorPosition} keeps between measurements. */\ninterface MeasuredPlacement extends AnchorPlacement {\n /** The anchor's width, when the layer matches it. */\n width: number | undefined;\n}\n\nconst OPPOSITE: Record<AnchorSide, AnchorSide> = {\n top: \"bottom\",\n bottom: \"top\",\n left: \"right\",\n right: \"left\",\n};\n\n/** Distance kept between a floating layer and the viewport edge, in px. */\nexport const ANCHOR_VIEWPORT_MARGIN = 8;\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.max(min, Math.min(value, Math.max(min, max)));\n}\n\nfunction mainAxis(geometry: AnchorGeometry, side: AnchorSide): number {\n const { anchor, width, height, offset } = geometry;\n switch (side) {\n case \"top\":\n return anchor.top - offset - height;\n case \"bottom\":\n return anchor.top + anchor.height + offset;\n case \"left\":\n return anchor.left - offset - width;\n case \"right\":\n default:\n return anchor.left + anchor.width + offset;\n }\n}\n\nfunction fits(geometry: AnchorGeometry, side: AnchorSide): boolean {\n const { width, height, viewportWidth, viewportHeight, margin } = geometry;\n const start = mainAxis(geometry, side);\n const size = side === \"top\" || side === \"bottom\" ? height : width;\n const limit = side === \"top\" || side === \"bottom\" ? viewportHeight : viewportWidth;\n return start >= margin && start + size <= limit - margin;\n}\n\nfunction crossAxis(start: number, length: number, size: number, align: AnchorAlign): number {\n if (align === \"start\") return start;\n if (align === \"end\") return start + length - size;\n return start + (length - size) / 2;\n}\n\n/**\n * Place a floating layer next to its anchor, inside the viewport.\n *\n * The requested side wins when the layer fits there. When it does not and the\n * opposite side does, the layer flips — the case that matters is the action menu\n * of the last row of a table near the bottom of the screen, which would otherwise\n * open below the fold. When neither side fits, the requested one is kept and the\n * layer is clamped, because a partially visible menu the user can scroll to beats\n * one that jumps sides depending on a pixel.\n *\n * Both axes are then clamped to the viewport minus `margin`.\n *\n * @param geometry - The anchor box, the floating layer's size, the viewport, and\n * the requested placement.\n * @returns The viewport coordinates for a `position: fixed` layer, and the side\n * that was used after flipping.\n */\nexport function computeAnchorPosition(geometry: AnchorGeometry): AnchorPlacement {\n const { anchor, width, height, viewportWidth, viewportHeight, align, margin } = geometry;\n const requested = geometry.side;\n const flipped = OPPOSITE[requested];\n const side = !fits(geometry, requested) && fits(geometry, flipped) ? flipped : requested;\n const vertical = side === \"top\" || side === \"bottom\";\n const main = mainAxis(geometry, side);\n const top = vertical ? main : crossAxis(anchor.top, anchor.height, height, align);\n const left = vertical ? crossAxis(anchor.left, anchor.width, width, align) : main;\n return {\n top: clamp(top, margin, viewportHeight - height - margin),\n left: clamp(left, margin, viewportWidth - width - margin),\n side,\n };\n}\n\n/** Options of {@link useAnchorPosition}. */\nexport interface AnchorPositionOptions {\n /** Element the layer is anchored to. */\n anchor: HTMLElement | null;\n /** The floating layer itself, once mounted. */\n floating: HTMLElement | null;\n side: AnchorSide;\n align: AnchorAlign;\n /** Gap between anchor and layer, in px. */\n offset: number;\n /** When false the hook does nothing and returns `undefined`. */\n enabled: boolean;\n /**\n * Give the layer the anchor's width — a listbox under its input. Default\n * `false`.\n */\n matchWidth?: boolean;\n}\n\n/**\n * Keep a portalled layer glued to its anchor.\n *\n * Takes the floating element as a value rather than a ref, because `Portal`\n * renders `null` on its first pass and mounts in an effect: a layout effect keyed\n * off `open` would run while the layer does not exist and measure nothing. With\n * the node held in state (a callback ref), the effect runs on the render that has\n * it, and `useLayoutEffect` settles the position before the first paint.\n *\n * The size comes from `offsetWidth`/`offsetHeight`, not from\n * `getBoundingClientRect`, which reports the transformed box — the layers enter\n * with a `translate`/`scale` animation, and the transformed rect would place them\n * a few pixels off. The anchor's rect is the right one to read: it is where the\n * trigger is painted.\n *\n * Scroll (captured, so a scrolling table wrapper counts) and resize reposition the\n * layer on the next animation frame, so a menu open inside a table that scrolls\n * follows its row instead of floating where the row used to be. So does a change\n * in the size of either box, through a `ResizeObserver`: a combobox list flipped\n * above its input shrinks as the user types, and a multi-select field grows a\n * line as chips are added — without the observer the list would keep its old\n * top and float away from the field, or cover it.\n *\n * With `matchWidth` the anchor's width is written to the layer **before** its\n * height is read, so the height is measured at the width it will be painted at —\n * a list whose options wrap at the narrower width is taller.\n *\n * The unmeasured first pass is **not** hidden with `visibility: hidden`: an\n * element under it cannot take focus, and the menu's focus effect runs before\n * the measured re-render, so the first entry silently kept no focus (measured in\n * Chrome; jsdom does not enforce it). Nothing is painted at the unmeasured\n * position anyway — the layout effect re-renders before the browser paints.\n *\n * @param options - Anchor, floating node, placement, and whether it is active.\n * @returns A `position: fixed` style for the layer, or `undefined` when disabled.\n */\nexport function useAnchorPosition({\n anchor,\n floating,\n side,\n align,\n offset,\n enabled,\n matchWidth = false,\n}: AnchorPositionOptions): CSSProperties | undefined {\n const [placement, setPlacement] = useState<MeasuredPlacement | null>(null);\n\n useLayoutEffect(() => {\n if (!enabled || !anchor || !floating) {\n setPlacement(null);\n return;\n }\n const update = (): void => {\n const rect = anchor.getBoundingClientRect();\n const width = matchWidth ? rect.width : undefined;\n if (width !== undefined) floating.style.setProperty(\"width\", `${width}px`);\n const position = computeAnchorPosition({\n anchor: { top: rect.top, left: rect.left, width: rect.width, height: rect.height },\n width: floating.offsetWidth,\n height: floating.offsetHeight,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n side,\n align,\n offset,\n margin: ANCHOR_VIEWPORT_MARGIN,\n });\n const next: MeasuredPlacement = { ...position, width };\n setPlacement((current) =>\n current &&\n current.top === next.top &&\n current.left === next.left &&\n current.side === next.side &&\n current.width === next.width\n ? current\n : next,\n );\n };\n update();\n let frame = 0;\n const schedule = (): void => {\n if (frame !== 0) return;\n frame = requestAnimationFrame(() => {\n frame = 0;\n update();\n });\n };\n window.addEventListener(\"scroll\", schedule, { capture: true, passive: true });\n window.addEventListener(\"resize\", schedule);\n const observer =\n typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(schedule);\n observer?.observe(floating);\n observer?.observe(anchor);\n return () => {\n observer?.disconnect();\n if (frame !== 0) cancelAnimationFrame(frame);\n window.removeEventListener(\"scroll\", schedule, { capture: true });\n window.removeEventListener(\"resize\", schedule);\n };\n }, [enabled, anchor, floating, side, align, offset, matchWidth]);\n\n if (!enabled) return undefined;\n return {\n position: \"fixed\",\n top: placement?.top ?? 0,\n left: placement?.left ?? 0,\n width: placement?.width,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,99 @@
1
+ import { useLayoutEffect as e, useState as t } from "react";
2
+ //#region src/components/Portal/anchor-position.ts
3
+ var n = {
4
+ top: "bottom",
5
+ bottom: "top",
6
+ left: "right",
7
+ right: "left"
8
+ };
9
+ function r(e, t, n) {
10
+ return Math.max(t, Math.min(e, Math.max(t, n)));
11
+ }
12
+ function i(e, t) {
13
+ let { anchor: n, width: r, height: i, offset: a } = e;
14
+ switch (t) {
15
+ case "top": return n.top - a - i;
16
+ case "bottom": return n.top + n.height + a;
17
+ case "left": return n.left - a - r;
18
+ default: return n.left + n.width + a;
19
+ }
20
+ }
21
+ function a(e, t) {
22
+ let { width: n, height: r, viewportWidth: a, viewportHeight: o, margin: s } = e, c = i(e, t), l = t === "top" || t === "bottom" ? r : n, u = t === "top" || t === "bottom" ? o : a;
23
+ return c >= s && c + l <= u - s;
24
+ }
25
+ function o(e, t, n, r) {
26
+ return r === "start" ? e : r === "end" ? e + t - n : e + (t - n) / 2;
27
+ }
28
+ function s(e) {
29
+ let { anchor: t, width: s, height: c, viewportWidth: l, viewportHeight: u, align: d, margin: f } = e, p = e.side, m = n[p], h = !a(e, p) && a(e, m) ? m : p, g = h === "top" || h === "bottom", _ = i(e, h), v = g ? _ : o(t.top, t.height, c, d), y = g ? o(t.left, t.width, s, d) : _;
30
+ return {
31
+ top: r(v, f, u - c - f),
32
+ left: r(y, f, l - s - f),
33
+ side: h
34
+ };
35
+ }
36
+ function c({ anchor: n, floating: r, side: i, align: a, offset: o, enabled: c, matchWidth: l = !1 }) {
37
+ let [u, d] = t(null);
38
+ if (e(() => {
39
+ if (!c || !n || !r) {
40
+ d(null);
41
+ return;
42
+ }
43
+ let e = () => {
44
+ let e = n.getBoundingClientRect(), t = l ? e.width : void 0;
45
+ t !== void 0 && r.style.setProperty("width", `${t}px`);
46
+ let c = {
47
+ ...s({
48
+ anchor: {
49
+ top: e.top,
50
+ left: e.left,
51
+ width: e.width,
52
+ height: e.height
53
+ },
54
+ width: r.offsetWidth,
55
+ height: r.offsetHeight,
56
+ viewportWidth: window.innerWidth,
57
+ viewportHeight: window.innerHeight,
58
+ side: i,
59
+ align: a,
60
+ offset: o,
61
+ margin: 8
62
+ }),
63
+ width: t
64
+ };
65
+ d((e) => e && e.top === c.top && e.left === c.left && e.side === c.side && e.width === c.width ? e : c);
66
+ };
67
+ e();
68
+ let t = 0, u = () => {
69
+ t === 0 && (t = requestAnimationFrame(() => {
70
+ t = 0, e();
71
+ }));
72
+ };
73
+ window.addEventListener("scroll", u, {
74
+ capture: !0,
75
+ passive: !0
76
+ }), window.addEventListener("resize", u);
77
+ let f = typeof ResizeObserver > "u" ? null : new ResizeObserver(u);
78
+ return f?.observe(r), f?.observe(n), () => {
79
+ f?.disconnect(), t !== 0 && cancelAnimationFrame(t), window.removeEventListener("scroll", u, { capture: !0 }), window.removeEventListener("resize", u);
80
+ };
81
+ }, [
82
+ c,
83
+ n,
84
+ r,
85
+ i,
86
+ a,
87
+ o,
88
+ l
89
+ ]), c) return {
90
+ position: "fixed",
91
+ top: u?.top ?? 0,
92
+ left: u?.left ?? 0,
93
+ width: u?.width
94
+ };
95
+ }
96
+ //#endregion
97
+ export { s as computeAnchorPosition, c as useAnchorPosition };
98
+
99
+ //# sourceMappingURL=anchor-position.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anchor-position.js","names":[],"sources":["../../../src/components/Portal/anchor-position.ts"],"sourcesContent":["import { useLayoutEffect, useState } from \"react\";\nimport type { CSSProperties } from \"react\";\n\n/** Side of the anchor a floating layer is placed on. */\nexport type AnchorSide = \"top\" | \"bottom\" | \"left\" | \"right\";\n\n/** Alignment of the floating layer along the anchor's edge. */\nexport type AnchorAlign = \"start\" | \"center\" | \"end\";\n\n/** The viewport-relative box a placement is computed from. */\nexport interface AnchorRect {\n top: number;\n left: number;\n width: number;\n height: number;\n}\n\n/** Everything {@link computeAnchorPosition} needs, all in CSS pixels. */\nexport interface AnchorGeometry {\n anchor: AnchorRect;\n width: number;\n height: number;\n viewportWidth: number;\n viewportHeight: number;\n side: AnchorSide;\n align: AnchorAlign;\n offset: number;\n margin: number;\n}\n\n/** A resolved placement: viewport coordinates plus the side actually used. */\nexport interface AnchorPlacement {\n top: number;\n left: number;\n side: AnchorSide;\n}\n\n/** What {@link useAnchorPosition} keeps between measurements. */\ninterface MeasuredPlacement extends AnchorPlacement {\n /** The anchor's width, when the layer matches it. */\n width: number | undefined;\n}\n\nconst OPPOSITE: Record<AnchorSide, AnchorSide> = {\n top: \"bottom\",\n bottom: \"top\",\n left: \"right\",\n right: \"left\",\n};\n\n/** Distance kept between a floating layer and the viewport edge, in px. */\nexport const ANCHOR_VIEWPORT_MARGIN = 8;\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.max(min, Math.min(value, Math.max(min, max)));\n}\n\nfunction mainAxis(geometry: AnchorGeometry, side: AnchorSide): number {\n const { anchor, width, height, offset } = geometry;\n switch (side) {\n case \"top\":\n return anchor.top - offset - height;\n case \"bottom\":\n return anchor.top + anchor.height + offset;\n case \"left\":\n return anchor.left - offset - width;\n case \"right\":\n default:\n return anchor.left + anchor.width + offset;\n }\n}\n\nfunction fits(geometry: AnchorGeometry, side: AnchorSide): boolean {\n const { width, height, viewportWidth, viewportHeight, margin } = geometry;\n const start = mainAxis(geometry, side);\n const size = side === \"top\" || side === \"bottom\" ? height : width;\n const limit = side === \"top\" || side === \"bottom\" ? viewportHeight : viewportWidth;\n return start >= margin && start + size <= limit - margin;\n}\n\nfunction crossAxis(start: number, length: number, size: number, align: AnchorAlign): number {\n if (align === \"start\") return start;\n if (align === \"end\") return start + length - size;\n return start + (length - size) / 2;\n}\n\n/**\n * Place a floating layer next to its anchor, inside the viewport.\n *\n * The requested side wins when the layer fits there. When it does not and the\n * opposite side does, the layer flips — the case that matters is the action menu\n * of the last row of a table near the bottom of the screen, which would otherwise\n * open below the fold. When neither side fits, the requested one is kept and the\n * layer is clamped, because a partially visible menu the user can scroll to beats\n * one that jumps sides depending on a pixel.\n *\n * Both axes are then clamped to the viewport minus `margin`.\n *\n * @param geometry - The anchor box, the floating layer's size, the viewport, and\n * the requested placement.\n * @returns The viewport coordinates for a `position: fixed` layer, and the side\n * that was used after flipping.\n */\nexport function computeAnchorPosition(geometry: AnchorGeometry): AnchorPlacement {\n const { anchor, width, height, viewportWidth, viewportHeight, align, margin } = geometry;\n const requested = geometry.side;\n const flipped = OPPOSITE[requested];\n const side = !fits(geometry, requested) && fits(geometry, flipped) ? flipped : requested;\n const vertical = side === \"top\" || side === \"bottom\";\n const main = mainAxis(geometry, side);\n const top = vertical ? main : crossAxis(anchor.top, anchor.height, height, align);\n const left = vertical ? crossAxis(anchor.left, anchor.width, width, align) : main;\n return {\n top: clamp(top, margin, viewportHeight - height - margin),\n left: clamp(left, margin, viewportWidth - width - margin),\n side,\n };\n}\n\n/** Options of {@link useAnchorPosition}. */\nexport interface AnchorPositionOptions {\n /** Element the layer is anchored to. */\n anchor: HTMLElement | null;\n /** The floating layer itself, once mounted. */\n floating: HTMLElement | null;\n side: AnchorSide;\n align: AnchorAlign;\n /** Gap between anchor and layer, in px. */\n offset: number;\n /** When false the hook does nothing and returns `undefined`. */\n enabled: boolean;\n /**\n * Give the layer the anchor's width — a listbox under its input. Default\n * `false`.\n */\n matchWidth?: boolean;\n}\n\n/**\n * Keep a portalled layer glued to its anchor.\n *\n * Takes the floating element as a value rather than a ref, because `Portal`\n * renders `null` on its first pass and mounts in an effect: a layout effect keyed\n * off `open` would run while the layer does not exist and measure nothing. With\n * the node held in state (a callback ref), the effect runs on the render that has\n * it, and `useLayoutEffect` settles the position before the first paint.\n *\n * The size comes from `offsetWidth`/`offsetHeight`, not from\n * `getBoundingClientRect`, which reports the transformed box — the layers enter\n * with a `translate`/`scale` animation, and the transformed rect would place them\n * a few pixels off. The anchor's rect is the right one to read: it is where the\n * trigger is painted.\n *\n * Scroll (captured, so a scrolling table wrapper counts) and resize reposition the\n * layer on the next animation frame, so a menu open inside a table that scrolls\n * follows its row instead of floating where the row used to be. So does a change\n * in the size of either box, through a `ResizeObserver`: a combobox list flipped\n * above its input shrinks as the user types, and a multi-select field grows a\n * line as chips are added — without the observer the list would keep its old\n * top and float away from the field, or cover it.\n *\n * With `matchWidth` the anchor's width is written to the layer **before** its\n * height is read, so the height is measured at the width it will be painted at —\n * a list whose options wrap at the narrower width is taller.\n *\n * The unmeasured first pass is **not** hidden with `visibility: hidden`: an\n * element under it cannot take focus, and the menu's focus effect runs before\n * the measured re-render, so the first entry silently kept no focus (measured in\n * Chrome; jsdom does not enforce it). Nothing is painted at the unmeasured\n * position anyway — the layout effect re-renders before the browser paints.\n *\n * @param options - Anchor, floating node, placement, and whether it is active.\n * @returns A `position: fixed` style for the layer, or `undefined` when disabled.\n */\nexport function useAnchorPosition({\n anchor,\n floating,\n side,\n align,\n offset,\n enabled,\n matchWidth = false,\n}: AnchorPositionOptions): CSSProperties | undefined {\n const [placement, setPlacement] = useState<MeasuredPlacement | null>(null);\n\n useLayoutEffect(() => {\n if (!enabled || !anchor || !floating) {\n setPlacement(null);\n return;\n }\n const update = (): void => {\n const rect = anchor.getBoundingClientRect();\n const width = matchWidth ? rect.width : undefined;\n if (width !== undefined) floating.style.setProperty(\"width\", `${width}px`);\n const position = computeAnchorPosition({\n anchor: { top: rect.top, left: rect.left, width: rect.width, height: rect.height },\n width: floating.offsetWidth,\n height: floating.offsetHeight,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n side,\n align,\n offset,\n margin: ANCHOR_VIEWPORT_MARGIN,\n });\n const next: MeasuredPlacement = { ...position, width };\n setPlacement((current) =>\n current &&\n current.top === next.top &&\n current.left === next.left &&\n current.side === next.side &&\n current.width === next.width\n ? current\n : next,\n );\n };\n update();\n let frame = 0;\n const schedule = (): void => {\n if (frame !== 0) return;\n frame = requestAnimationFrame(() => {\n frame = 0;\n update();\n });\n };\n window.addEventListener(\"scroll\", schedule, { capture: true, passive: true });\n window.addEventListener(\"resize\", schedule);\n const observer =\n typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(schedule);\n observer?.observe(floating);\n observer?.observe(anchor);\n return () => {\n observer?.disconnect();\n if (frame !== 0) cancelAnimationFrame(frame);\n window.removeEventListener(\"scroll\", schedule, { capture: true });\n window.removeEventListener(\"resize\", schedule);\n };\n }, [enabled, anchor, floating, side, align, offset, matchWidth]);\n\n if (!enabled) return undefined;\n return {\n position: \"fixed\",\n top: placement?.top ?? 0,\n left: placement?.left ?? 0,\n width: placement?.width,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ let e=require("react");var t=[],n=0;function r(e){if(e.key!==`Escape`||e.defaultPrevented||e.isComposing)return;let n=t.at(-1);n&&(e.preventDefault(),n.current?.())}function i(i,a){let o=(0,e.useMemo)(()=>i?++n:0,[i]),s=(0,e.useRef)({current:a,order:o});(0,e.useEffect)(()=>{s.current.current=a},[a]),(0,e.useEffect)(()=>{if(!i)return;let e=s.current;e.order=o,t.length===0&&window.addEventListener(`keydown`,r);let n=t.findIndex(e=>e.order>o);return t.splice(n===-1?t.length:n,0,e),()=>{let n=t.lastIndexOf(e);n!==-1&&t.splice(n,1),t.length===0&&window.removeEventListener(`keydown`,r)}},[i,o])}exports.useEscapeLayer=i;
2
+ //# sourceMappingURL=escape-layer.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"escape-layer.cjs","names":[],"sources":["../../../src/components/Portal/escape-layer.ts"],"sourcesContent":["import { useEffect, useMemo, useRef } from \"react\";\n\n/** One open layer that `Escape` can dismiss. */\ninterface EscapeLayer {\n /** Latest dismiss callback; `null` makes the layer swallow the key without closing. */\n current: (() => void) | null;\n /** Position in the stack: higher is more recently opened, or deeper in the tree. */\n order: number;\n}\n\nconst stack: EscapeLayer[] = [];\n\nlet sequence = 0;\n\n/**\n * The single `keydown` listener behind every layer.\n *\n * Dismisses only the top of the stack. Every overlay used to listen on `window`\n * and close on any `Escape`, so one key press closed everything open at once —\n * measured in Chrome, a `Modal` holding a `DropdownMenu`, `Combobox`,\n * `MultiSelect`, `Popover`, `ContextMenu`, editable `DataTable` cell or a second\n * `Modal` closed together with the inner layer in all seven cases, and a\n * `Drawer` closed with the `Popover` inside it.\n *\n * Two kinds of inner layer exist, and this covers both:\n *\n * - Layers that listen on `window` themselves can never see each other's\n * events, so they register here and only the most recently opened one reacts.\n * - Layers that handle the key on their own element (a menu, a combobox input,\n * an inline cell editor) run first — React's delegated listener sits below\n * `window` — and call `preventDefault()`. A consumed key is left alone.\n *\n * A key pressed while an IME is composing belongs to the composition (it cancels\n * the candidate), so it is ignored too.\n *\n * @param event - The native keyboard event.\n * @returns Nothing.\n */\nfunction handleKeyDown(event: KeyboardEvent): void {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const top = stack.at(-1);\n if (!top) return;\n event.preventDefault();\n top.current?.();\n}\n\n/**\n * Register an overlay as an `Escape`-dismissable layer while it is open.\n *\n * Layers stack in the order they open, so the one opened last is dismissed\n * first, and a second `Escape` reaches the next one down. The callback is read\n * through a ref, so a new closure on every render does not move the layer in\n * the stack.\n *\n * Pass `null` as `onEscape` for a modal layer whose dismissal is turned off\n * (`closeOnEsc={false}`): it stays on the stack and swallows the key, instead of\n * letting it fall through and close the modal underneath.\n *\n * The position is taken when the layer **renders** open, not when its effect\n * runs. Effects run child-first, so a `Modal` that mounts already holding an open\n * inner `Modal` would register the inner one first and put the outer one on top —\n * `Escape` would close the wrong layer, or a locked inner modal would let the key\n * through to the outer one. Parents render before their children, so render\n * order is tree order within a commit and opening order across commits. A\n * discarded or doubled render (Strict Mode, concurrent rendering) only skips\n * numbers; the relative order stays.\n *\n * @param active - Whether the layer is currently open.\n * @param onEscape - What `Escape` does to this layer, or `null` to block only.\n * @returns Nothing.\n */\nexport function useEscapeLayer(active: boolean, onEscape: (() => void) | null): void {\n const order = useMemo(() => (active ? ++sequence : 0), [active]);\n const layer = useRef<EscapeLayer>({ current: onEscape, order });\n\n useEffect(() => {\n layer.current.current = onEscape;\n }, [onEscape]);\n\n useEffect(() => {\n if (!active) return;\n const entry = layer.current;\n entry.order = order;\n if (stack.length === 0) window.addEventListener(\"keydown\", handleKeyDown);\n const index = stack.findIndex((other) => other.order > order);\n stack.splice(index === -1 ? stack.length : index, 0, entry);\n return () => {\n const index = stack.lastIndexOf(entry);\n if (index !== -1) stack.splice(index, 1);\n if (stack.length === 0) window.removeEventListener(\"keydown\", handleKeyDown);\n };\n }, [active, order]);\n}\n"],"mappings":"uBAUA,IAAM,EAAuB,CAAC,EAE1B,EAAW,EA0Bf,SAAS,EAAc,EAA4B,CAC/C,GAAI,EAAM,MAAQ,UAAY,EAAM,kBAAoB,EAAM,YAAa,OAC3E,IAAM,EAAM,EAAM,GAAG,EAAE,EAClB,IACL,EAAM,eAAe,EACrB,EAAI,UAAU,EAClB,CA2BA,SAAgB,EAAe,EAAiB,EAAqC,CACjF,IAAM,GAAA,EAAQ,EAAA,QAAA,KAAe,EAAS,EAAE,EAAW,EAAI,CAAC,CAAM,CAAC,EACzD,GAAA,EAAQ,EAAA,OAAA,CAAoB,CAAE,QAAS,EAAU,OAAM,CAAC,GAE9D,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAM,QAAQ,QAAU,CAC5B,EAAG,CAAC,CAAQ,CAAC,GAEb,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,EAAQ,OACb,IAAM,EAAQ,EAAM,QACpB,EAAM,MAAQ,EACV,EAAM,SAAW,GAAG,OAAO,iBAAiB,UAAW,CAAa,EACxE,IAAM,EAAQ,EAAM,UAAW,GAAU,EAAM,MAAQ,CAAK,EAE5D,OADA,EAAM,OAAO,IAAU,GAAK,EAAM,OAAS,EAAO,EAAG,CAAK,MAC7C,CACT,IAAM,EAAQ,EAAM,YAAY,CAAK,EACjC,IAAU,IAAI,EAAM,OAAO,EAAO,CAAC,EACnC,EAAM,SAAW,GAAG,OAAO,oBAAoB,UAAW,CAAa,CAC/E,CACJ,EAAG,CAAC,EAAQ,CAAK,CAAC,CACtB"}
@@ -0,0 +1,30 @@
1
+ import { useEffect as e, useMemo as t, useRef as n } from "react";
2
+ //#region src/components/Portal/escape-layer.ts
3
+ var r = [], i = 0;
4
+ function a(e) {
5
+ if (e.key !== "Escape" || e.defaultPrevented || e.isComposing) return;
6
+ let t = r.at(-1);
7
+ t && (e.preventDefault(), t.current?.());
8
+ }
9
+ function o(o, s) {
10
+ let c = t(() => o ? ++i : 0, [o]), l = n({
11
+ current: s,
12
+ order: c
13
+ });
14
+ e(() => {
15
+ l.current.current = s;
16
+ }, [s]), e(() => {
17
+ if (!o) return;
18
+ let e = l.current;
19
+ e.order = c, r.length === 0 && window.addEventListener("keydown", a);
20
+ let t = r.findIndex((e) => e.order > c);
21
+ return r.splice(t === -1 ? r.length : t, 0, e), () => {
22
+ let t = r.lastIndexOf(e);
23
+ t !== -1 && r.splice(t, 1), r.length === 0 && window.removeEventListener("keydown", a);
24
+ };
25
+ }, [o, c]);
26
+ }
27
+ //#endregion
28
+ export { o as useEscapeLayer };
29
+
30
+ //# sourceMappingURL=escape-layer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"escape-layer.js","names":[],"sources":["../../../src/components/Portal/escape-layer.ts"],"sourcesContent":["import { useEffect, useMemo, useRef } from \"react\";\n\n/** One open layer that `Escape` can dismiss. */\ninterface EscapeLayer {\n /** Latest dismiss callback; `null` makes the layer swallow the key without closing. */\n current: (() => void) | null;\n /** Position in the stack: higher is more recently opened, or deeper in the tree. */\n order: number;\n}\n\nconst stack: EscapeLayer[] = [];\n\nlet sequence = 0;\n\n/**\n * The single `keydown` listener behind every layer.\n *\n * Dismisses only the top of the stack. Every overlay used to listen on `window`\n * and close on any `Escape`, so one key press closed everything open at once —\n * measured in Chrome, a `Modal` holding a `DropdownMenu`, `Combobox`,\n * `MultiSelect`, `Popover`, `ContextMenu`, editable `DataTable` cell or a second\n * `Modal` closed together with the inner layer in all seven cases, and a\n * `Drawer` closed with the `Popover` inside it.\n *\n * Two kinds of inner layer exist, and this covers both:\n *\n * - Layers that listen on `window` themselves can never see each other's\n * events, so they register here and only the most recently opened one reacts.\n * - Layers that handle the key on their own element (a menu, a combobox input,\n * an inline cell editor) run first — React's delegated listener sits below\n * `window` — and call `preventDefault()`. A consumed key is left alone.\n *\n * A key pressed while an IME is composing belongs to the composition (it cancels\n * the candidate), so it is ignored too.\n *\n * @param event - The native keyboard event.\n * @returns Nothing.\n */\nfunction handleKeyDown(event: KeyboardEvent): void {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const top = stack.at(-1);\n if (!top) return;\n event.preventDefault();\n top.current?.();\n}\n\n/**\n * Register an overlay as an `Escape`-dismissable layer while it is open.\n *\n * Layers stack in the order they open, so the one opened last is dismissed\n * first, and a second `Escape` reaches the next one down. The callback is read\n * through a ref, so a new closure on every render does not move the layer in\n * the stack.\n *\n * Pass `null` as `onEscape` for a modal layer whose dismissal is turned off\n * (`closeOnEsc={false}`): it stays on the stack and swallows the key, instead of\n * letting it fall through and close the modal underneath.\n *\n * The position is taken when the layer **renders** open, not when its effect\n * runs. Effects run child-first, so a `Modal` that mounts already holding an open\n * inner `Modal` would register the inner one first and put the outer one on top —\n * `Escape` would close the wrong layer, or a locked inner modal would let the key\n * through to the outer one. Parents render before their children, so render\n * order is tree order within a commit and opening order across commits. A\n * discarded or doubled render (Strict Mode, concurrent rendering) only skips\n * numbers; the relative order stays.\n *\n * @param active - Whether the layer is currently open.\n * @param onEscape - What `Escape` does to this layer, or `null` to block only.\n * @returns Nothing.\n */\nexport function useEscapeLayer(active: boolean, onEscape: (() => void) | null): void {\n const order = useMemo(() => (active ? ++sequence : 0), [active]);\n const layer = useRef<EscapeLayer>({ current: onEscape, order });\n\n useEffect(() => {\n layer.current.current = onEscape;\n }, [onEscape]);\n\n useEffect(() => {\n if (!active) return;\n const entry = layer.current;\n entry.order = order;\n if (stack.length === 0) window.addEventListener(\"keydown\", handleKeyDown);\n const index = stack.findIndex((other) => other.order > order);\n stack.splice(index === -1 ? stack.length : index, 0, entry);\n return () => {\n const index = stack.lastIndexOf(entry);\n if (index !== -1) stack.splice(index, 1);\n if (stack.length === 0) window.removeEventListener(\"keydown\", handleKeyDown);\n };\n }, [active, order]);\n}\n"],"mappings":";;AAUA,IAAM,IAAuB,CAAC,GAE1B,IAAW;AA0Bf,SAAS,EAAc,GAA4B;CAC/C,IAAI,EAAM,QAAQ,YAAY,EAAM,oBAAoB,EAAM,aAAa;CAC3E,IAAM,IAAM,EAAM,GAAG,EAAE;CAClB,MACL,EAAM,eAAe,GACrB,EAAI,UAAU;AAClB;AA2BA,SAAgB,EAAe,GAAiB,GAAqC;CACjF,IAAM,IAAQ,QAAe,IAAS,EAAE,IAAW,GAAI,CAAC,CAAM,CAAC,GACzD,IAAQ,EAAoB;EAAE,SAAS;EAAU;CAAM,CAAC;CAM9D,AAJA,QAAgB;EACZ,EAAM,QAAQ,UAAU;CAC5B,GAAG,CAAC,CAAQ,CAAC,GAEb,QAAgB;EACZ,IAAI,CAAC,GAAQ;EACb,IAAM,IAAQ,EAAM;EAEpB,AADA,EAAM,QAAQ,GACV,EAAM,WAAW,KAAG,OAAO,iBAAiB,WAAW,CAAa;EACxE,IAAM,IAAQ,EAAM,WAAW,MAAU,EAAM,QAAQ,CAAK;EAE5D,OADA,EAAM,OAAO,MAAU,KAAK,EAAM,SAAS,GAAO,GAAG,CAAK,SAC7C;GACT,IAAM,IAAQ,EAAM,YAAY,CAAK;GAErC,AADI,MAAU,MAAI,EAAM,OAAO,GAAO,CAAC,GACnC,EAAM,WAAW,KAAG,OAAO,oBAAoB,WAAW,CAAa;EAC/E;CACJ,GAAG,CAAC,GAAQ,CAAK,CAAC;AACtB"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../hooks/tab-sequence.cjs");let t=require("react");function n({enabled:n,anchor:r,panel:i}){(0,t.useEffect)(()=>{if(!n||!r||!i)return;let t=(e,t)=>{t&&(e.preventDefault(),t.focus())},a=n=>{if(n.key!==`Tab`||n.defaultPrevented)return;let a=e.tabbables(i);if(a.length===0)return;let o=document.activeElement;if(!o)return;let s=e.tabbables(document).filter(e=>!i.contains(e)),c=s.filter(e=>r.contains(e)).at(-1),l=c?s[s.indexOf(c)+1]:void 0;if(i.contains(o)){!n.shiftKey&&o===a.at(-1)&&t(n,l),n.shiftKey&&o===a[0]&&t(n,c);return}!n.shiftKey&&o===c&&t(n,a[0]),n.shiftKey&&l&&o===l&&t(n,a.at(-1))},o=e.registerPortalLayer({anchor:r,panel:i});return document.addEventListener(`keydown`,a,!0),()=>{o(),document.removeEventListener(`keydown`,a,!0)}},[n,r,i])}exports.usePortalTabOrder=n;
2
+ //# sourceMappingURL=tab-bridge.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab-bridge.cjs","names":[],"sources":["../../../src/components/Portal/tab-bridge.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { registerPortalLayer, tabbables } from \"@/hooks/tab-sequence\";\n\n/** Options of {@link usePortalTabOrder}. */\nexport interface PortalTabOrderOptions {\n /** Whether the panel is open and portalled. */\n enabled: boolean;\n /** Wrapper of the trigger — the spot in the page the panel belongs to. */\n anchor: HTMLElement | null;\n /** The portalled panel. */\n panel: HTMLElement | null;\n}\n\n/**\n * Put a portalled panel back in the tab order, right after its trigger.\n *\n * A portal moves the panel to the end of `body`, so the browser's own order goes\n * from the trigger straight to whatever follows it in the page and reaches the\n * panel last, if ever. Measured in Chrome on a `Popover` holding two inputs:\n * in flow, `Tab` from the open trigger landed on the first input; in a portal it\n * skipped the panel and went to the next button of the page.\n *\n * While the panel is open, this restores the in-flow order:\n *\n * - `Tab` on the last stop of the anchor enters the panel's first stop.\n * - `Tab` on the panel's last stop leaves to the first stop after the anchor.\n * - `Shift+Tab` on the panel's first stop returns to the anchor.\n * - `Shift+Tab` on the first stop after the anchor enters the panel's last stop.\n *\n * Listened on `document` in the capture phase and marked consumed, so it runs\n * before an enclosing `Modal`'s focus trap, which skips a consumed `Tab`. The\n * panel is also registered as a layer of its anchor, which is how that trap\n * knows the panel belongs inside it although the portal put it outside.\n *\n * @param options - Whether it is active, the anchor and the panel.\n * @returns Nothing.\n */\nexport function usePortalTabOrder({ enabled, anchor, panel }: PortalTabOrderOptions): void {\n useEffect(() => {\n if (!enabled || !anchor || !panel) return;\n\n const move = (event: KeyboardEvent, target: HTMLElement | undefined): void => {\n if (!target) return;\n event.preventDefault();\n target.focus();\n };\n\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key !== \"Tab\" || event.defaultPrevented) return;\n const inPanel = tabbables(panel);\n if (inPanel.length === 0) return;\n const active = document.activeElement as HTMLElement | null;\n if (!active) return;\n const page = tabbables(document).filter((el) => !panel.contains(el));\n const inAnchor = page.filter((el) => anchor.contains(el));\n const lastOfAnchor = inAnchor.at(-1);\n const afterAnchor = lastOfAnchor ? page[page.indexOf(lastOfAnchor) + 1] : undefined;\n\n if (panel.contains(active)) {\n if (!event.shiftKey && active === inPanel.at(-1)) move(event, afterAnchor);\n if (event.shiftKey && active === inPanel[0]) move(event, lastOfAnchor);\n return;\n }\n if (!event.shiftKey && active === lastOfAnchor) move(event, inPanel[0]);\n if (event.shiftKey && afterAnchor && active === afterAnchor) {\n move(event, inPanel.at(-1));\n }\n };\n\n const unregister = registerPortalLayer({ anchor, panel });\n document.addEventListener(\"keydown\", onKeyDown, true);\n return () => {\n unregister();\n document.removeEventListener(\"keydown\", onKeyDown, true);\n };\n }, [enabled, anchor, panel]);\n}\n"],"mappings":"uEAqCA,SAAgB,EAAkB,CAAE,UAAS,SAAQ,SAAsC,EACvF,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,GAAW,CAAC,GAAU,CAAC,EAAO,OAEnC,IAAM,GAAQ,EAAsB,IAA0C,CACrE,IACL,EAAM,eAAe,EACrB,EAAO,MAAM,EACjB,EAEM,EAAa,GAA+B,CAC9C,GAAI,EAAM,MAAQ,OAAS,EAAM,iBAAkB,OACnD,IAAM,EAAU,EAAA,UAAU,CAAK,EAC/B,GAAI,EAAQ,SAAW,EAAG,OAC1B,IAAM,EAAS,SAAS,cACxB,GAAI,CAAC,EAAQ,OACb,IAAM,EAAO,EAAA,UAAU,QAAQ,CAAC,CAAC,OAAQ,GAAO,CAAC,EAAM,SAAS,CAAE,CAAC,EAE7D,EADW,EAAK,OAAQ,GAAO,EAAO,SAAS,CAAE,CAClC,CAAA,CAAS,GAAG,EAAE,EAC7B,EAAc,EAAe,EAAK,EAAK,QAAQ,CAAY,EAAI,GAAK,IAAA,GAE1E,GAAI,EAAM,SAAS,CAAM,EAAG,CACpB,CAAC,EAAM,UAAY,IAAW,EAAQ,GAAG,EAAE,GAAG,EAAK,EAAO,CAAW,EACrE,EAAM,UAAY,IAAW,EAAQ,IAAI,EAAK,EAAO,CAAY,EACrE,MACJ,CACI,CAAC,EAAM,UAAY,IAAW,GAAc,EAAK,EAAO,EAAQ,EAAE,EAClE,EAAM,UAAY,GAAe,IAAW,GAC5C,EAAK,EAAO,EAAQ,GAAG,EAAE,CAAC,CAElC,EAEM,EAAa,EAAA,oBAAoB,CAAE,SAAQ,OAAM,CAAC,EAExD,OADA,SAAS,iBAAiB,UAAW,EAAW,EAAI,MACvC,CACT,EAAW,EACX,SAAS,oBAAoB,UAAW,EAAW,EAAI,CAC3D,CACJ,EAAG,CAAC,EAAS,EAAQ,CAAK,CAAC,CAC/B"}
@@ -0,0 +1,37 @@
1
+ import { registerPortalLayer as e, tabbables as t } from "../../hooks/tab-sequence.js";
2
+ import { useEffect as n } from "react";
3
+ //#region src/components/Portal/tab-bridge.ts
4
+ function r({ enabled: r, anchor: i, panel: a }) {
5
+ n(() => {
6
+ if (!r || !i || !a) return;
7
+ let n = (e, t) => {
8
+ t && (e.preventDefault(), t.focus());
9
+ }, o = (e) => {
10
+ if (e.key !== "Tab" || e.defaultPrevented) return;
11
+ let r = t(a);
12
+ if (r.length === 0) return;
13
+ let o = document.activeElement;
14
+ if (!o) return;
15
+ let s = t(document).filter((e) => !a.contains(e)), c = s.filter((e) => i.contains(e)).at(-1), l = c ? s[s.indexOf(c) + 1] : void 0;
16
+ if (a.contains(o)) {
17
+ !e.shiftKey && o === r.at(-1) && n(e, l), e.shiftKey && o === r[0] && n(e, c);
18
+ return;
19
+ }
20
+ !e.shiftKey && o === c && n(e, r[0]), e.shiftKey && l && o === l && n(e, r.at(-1));
21
+ }, s = e({
22
+ anchor: i,
23
+ panel: a
24
+ });
25
+ return document.addEventListener("keydown", o, !0), () => {
26
+ s(), document.removeEventListener("keydown", o, !0);
27
+ };
28
+ }, [
29
+ r,
30
+ i,
31
+ a
32
+ ]);
33
+ }
34
+ //#endregion
35
+ export { r as usePortalTabOrder };
36
+
37
+ //# sourceMappingURL=tab-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab-bridge.js","names":[],"sources":["../../../src/components/Portal/tab-bridge.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { registerPortalLayer, tabbables } from \"@/hooks/tab-sequence\";\n\n/** Options of {@link usePortalTabOrder}. */\nexport interface PortalTabOrderOptions {\n /** Whether the panel is open and portalled. */\n enabled: boolean;\n /** Wrapper of the trigger — the spot in the page the panel belongs to. */\n anchor: HTMLElement | null;\n /** The portalled panel. */\n panel: HTMLElement | null;\n}\n\n/**\n * Put a portalled panel back in the tab order, right after its trigger.\n *\n * A portal moves the panel to the end of `body`, so the browser's own order goes\n * from the trigger straight to whatever follows it in the page and reaches the\n * panel last, if ever. Measured in Chrome on a `Popover` holding two inputs:\n * in flow, `Tab` from the open trigger landed on the first input; in a portal it\n * skipped the panel and went to the next button of the page.\n *\n * While the panel is open, this restores the in-flow order:\n *\n * - `Tab` on the last stop of the anchor enters the panel's first stop.\n * - `Tab` on the panel's last stop leaves to the first stop after the anchor.\n * - `Shift+Tab` on the panel's first stop returns to the anchor.\n * - `Shift+Tab` on the first stop after the anchor enters the panel's last stop.\n *\n * Listened on `document` in the capture phase and marked consumed, so it runs\n * before an enclosing `Modal`'s focus trap, which skips a consumed `Tab`. The\n * panel is also registered as a layer of its anchor, which is how that trap\n * knows the panel belongs inside it although the portal put it outside.\n *\n * @param options - Whether it is active, the anchor and the panel.\n * @returns Nothing.\n */\nexport function usePortalTabOrder({ enabled, anchor, panel }: PortalTabOrderOptions): void {\n useEffect(() => {\n if (!enabled || !anchor || !panel) return;\n\n const move = (event: KeyboardEvent, target: HTMLElement | undefined): void => {\n if (!target) return;\n event.preventDefault();\n target.focus();\n };\n\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key !== \"Tab\" || event.defaultPrevented) return;\n const inPanel = tabbables(panel);\n if (inPanel.length === 0) return;\n const active = document.activeElement as HTMLElement | null;\n if (!active) return;\n const page = tabbables(document).filter((el) => !panel.contains(el));\n const inAnchor = page.filter((el) => anchor.contains(el));\n const lastOfAnchor = inAnchor.at(-1);\n const afterAnchor = lastOfAnchor ? page[page.indexOf(lastOfAnchor) + 1] : undefined;\n\n if (panel.contains(active)) {\n if (!event.shiftKey && active === inPanel.at(-1)) move(event, afterAnchor);\n if (event.shiftKey && active === inPanel[0]) move(event, lastOfAnchor);\n return;\n }\n if (!event.shiftKey && active === lastOfAnchor) move(event, inPanel[0]);\n if (event.shiftKey && afterAnchor && active === afterAnchor) {\n move(event, inPanel.at(-1));\n }\n };\n\n const unregister = registerPortalLayer({ anchor, panel });\n document.addEventListener(\"keydown\", onKeyDown, true);\n return () => {\n unregister();\n document.removeEventListener(\"keydown\", onKeyDown, true);\n };\n }, [enabled, anchor, panel]);\n}\n"],"mappings":";;;AAqCA,SAAgB,EAAkB,EAAE,YAAS,WAAQ,YAAsC;CACvF,QAAgB;EACZ,IAAI,CAAC,KAAW,CAAC,KAAU,CAAC,GAAO;EAEnC,IAAM,KAAQ,GAAsB,MAA0C;GACrE,MACL,EAAM,eAAe,GACrB,EAAO,MAAM;EACjB,GAEM,KAAa,MAA+B;GAC9C,IAAI,EAAM,QAAQ,SAAS,EAAM,kBAAkB;GACnD,IAAM,IAAU,EAAU,CAAK;GAC/B,IAAI,EAAQ,WAAW,GAAG;GAC1B,IAAM,IAAS,SAAS;GACxB,IAAI,CAAC,GAAQ;GACb,IAAM,IAAO,EAAU,QAAQ,CAAC,CAAC,QAAQ,MAAO,CAAC,EAAM,SAAS,CAAE,CAAC,GAE7D,IADW,EAAK,QAAQ,MAAO,EAAO,SAAS,CAAE,CAClC,CAAA,CAAS,GAAG,EAAE,GAC7B,IAAc,IAAe,EAAK,EAAK,QAAQ,CAAY,IAAI,KAAK,KAAA;GAE1E,IAAI,EAAM,SAAS,CAAM,GAAG;IAExB,AADI,CAAC,EAAM,YAAY,MAAW,EAAQ,GAAG,EAAE,KAAG,EAAK,GAAO,CAAW,GACrE,EAAM,YAAY,MAAW,EAAQ,MAAI,EAAK,GAAO,CAAY;IACrE;GACJ;GAEA,AADI,CAAC,EAAM,YAAY,MAAW,KAAc,EAAK,GAAO,EAAQ,EAAE,GAClE,EAAM,YAAY,KAAe,MAAW,KAC5C,EAAK,GAAO,EAAQ,GAAG,EAAE,CAAC;EAElC,GAEM,IAAa,EAAoB;GAAE;GAAQ;EAAM,CAAC;EAExD,OADA,SAAS,iBAAiB,WAAW,GAAW,EAAI,SACvC;GAET,AADA,EAAW,GACX,SAAS,oBAAoB,WAAW,GAAW,EAAI;EAC3D;CACJ,GAAG;EAAC;EAAS;EAAQ;CAAK,CAAC;AAC/B"}
@@ -0,0 +1,2 @@
1
+ let e=require("react/jsx-runtime");function t(){return(0,e.jsx)(`svg`,{width:`14`,height:`14`,viewBox:`0 0 24 24`,fill:`none`,children:(0,e.jsx)(`path`,{d:`M6 9l6 6 6-6`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`})})}exports.CaretIcon=t;
2
+ //# sourceMappingURL=CaretIcon.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CaretIcon.cjs","names":[],"sources":["../../../src/components/Select/CaretIcon.tsx"],"sourcesContent":["import type { ReactElement } from \"react\";\n\n/**\n * The chevron `Select` and `Combobox` draw on the right of the control.\n *\n * Internal to the SDK: the public way to change it is `Select`'s `caretIcon`\n * prop. Its size comes from the `--tempest-control-caret-size` token through the\n * caret slot's stylesheet, so the `width`/`height` attributes here are only the\n * fallback for a page that loaded no SDK CSS.\n *\n * @returns The chevron as an inline SVG drawn in `currentColor`.\n */\nexport function CaretIcon(): ReactElement {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 9l6 6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"mCAYA,SAAgB,GAA0B,CACtC,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,MAAM,KAAK,OAAO,KAAK,QAAQ,YAAY,KAAK,OACjD,UAAA,EAAA,EAAA,IAAA,CAAC,OAAD,CACI,EAAE,eACF,OAAO,eACP,YAAY,IACZ,cAAc,QACd,eAAe,OAClB,CAAA,CACA,CAAA,CAEb"}
@@ -0,0 +1,21 @@
1
+ import { jsx as e } from "react/jsx-runtime";
2
+ //#region src/components/Select/CaretIcon.tsx
3
+ function t() {
4
+ return /* @__PURE__ */ e("svg", {
5
+ width: "14",
6
+ height: "14",
7
+ viewBox: "0 0 24 24",
8
+ fill: "none",
9
+ children: /* @__PURE__ */ e("path", {
10
+ d: "M6 9l6 6 6-6",
11
+ stroke: "currentColor",
12
+ strokeWidth: "2",
13
+ strokeLinecap: "round",
14
+ strokeLinejoin: "round"
15
+ })
16
+ });
17
+ }
18
+ //#endregion
19
+ export { t as CaretIcon };
20
+
21
+ //# sourceMappingURL=CaretIcon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CaretIcon.js","names":[],"sources":["../../../src/components/Select/CaretIcon.tsx"],"sourcesContent":["import type { ReactElement } from \"react\";\n\n/**\n * The chevron `Select` and `Combobox` draw on the right of the control.\n *\n * Internal to the SDK: the public way to change it is `Select`'s `caretIcon`\n * prop. Its size comes from the `--tempest-control-caret-size` token through the\n * caret slot's stylesheet, so the `width`/`height` attributes here are only the\n * fallback for a page that loaded no SDK CSS.\n *\n * @returns The chevron as an inline SVG drawn in `currentColor`.\n */\nexport function CaretIcon(): ReactElement {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 9l6 6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":";;AAYA,SAAgB,IAA0B;CACtC,OACI,kBAAC,OAAD;EAAK,OAAM;EAAK,QAAO;EAAK,SAAQ;EAAY,MAAK;EACjD,UAAA,kBAAC,QAAD;GACI,GAAE;GACF,QAAO;GACP,aAAY;GACZ,eAAc;GACd,gBAAe;EAClB,CAAA;CACA,CAAA;AAEb"}
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/cn.cjs"),t=require("./Select.module.cjs");let n=require("react"),r=require("react/jsx-runtime");var i=(0,n.forwardRef)(function(i,o){let{options:s,placeholder:c,wrapperClassName:l,className:u,children:d,id:f,required:p,variant:m=`field`,label:h,helperText:g,error:_,...v}=i,y=m===`chip`,b=(0,n.useId)(),x=f??b,S=(0,r.jsxs)(`div`,{className:e.cn(t.default.field,y&&t.default.chip),children:[(0,r.jsxs)(`select`,{ref:o,id:x,"aria-invalid":!!_,required:p,className:e.cn(t.default.select,u),...v,children:[c&&(0,r.jsx)(`option`,{value:``,disabled:!0,hidden:!0,children:c}),s?.map(e=>(0,r.jsx)(`option`,{value:e.value,disabled:e.disabled,children:e.label},e.value)),d]}),(0,r.jsx)(`span`,{className:t.default.caret,"aria-hidden":!0,children:(0,r.jsx)(a,{})})]});return y?(0,r.jsx)(`div`,{className:e.cn(t.default.chipWrapper,l),children:S}):(0,r.jsxs)(`div`,{className:e.cn(t.default.wrapper,_&&t.default.error,l),children:[h&&(0,r.jsxs)(`label`,{htmlFor:x,className:t.default.label,children:[h,p&&(0,r.jsx)(`span`,{className:t.default.required,children:`*`})]}),S,_?(0,r.jsx)(`span`,{className:t.default.errorText,children:_}):g?(0,r.jsx)(`span`,{className:t.default.helper,children:g}):null]})});function a(){return(0,r.jsx)(`svg`,{width:`14`,height:`14`,viewBox:`0 0 24 24`,fill:`none`,children:(0,r.jsx)(`path`,{d:`M6 9l6 6 6-6`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`})})}exports.Select=i;
1
+ const e=require("../../utils/cn.cjs"),t=require("./CaretIcon.cjs"),n=require("./Select.module.cjs");let r=require("react"),i=require("react/jsx-runtime");var a=(0,r.forwardRef)(function(a,o){let{options:s,placeholder:c,wrapperClassName:l,className:u,children:d,id:f,required:p,variant:m=`field`,label:h,helperText:g,error:_,caretIcon:v,...y}=a,b=m===`chip`,x=m===`bare`,S=(0,r.useId)(),C=f??S,w=(0,i.jsxs)(`div`,{className:e.cn(n.default.field,b&&n.default.chip,x&&n.default.bare,x&&l),children:[(0,i.jsxs)(`select`,{ref:o,id:C,"aria-invalid":!!_,required:p,className:e.cn(n.default.select,u),...y,children:[c&&(0,i.jsx)(`option`,{value:``,disabled:!0,hidden:!0,children:c}),s?.map(e=>(0,i.jsx)(`option`,{value:e.value,disabled:e.disabled,children:e.label},e.value)),d]}),(0,i.jsx)(`span`,{className:n.default.caret,"aria-hidden":!0,children:v??(0,i.jsx)(t.CaretIcon,{})})]});return x?w:b?(0,i.jsx)(`div`,{className:e.cn(n.default.chipWrapper,l),children:w}):(0,i.jsxs)(`div`,{className:e.cn(n.default.wrapper,_&&n.default.error,l),children:[h&&(0,i.jsxs)(`label`,{htmlFor:C,className:n.default.label,children:[h,p&&(0,i.jsx)(`span`,{className:n.default.required,children:`*`})]}),w,_?(0,i.jsx)(`span`,{className:n.default.errorText,children:_}):g?(0,i.jsx)(`span`,{className:n.default.helper,children:g}):null]})});exports.Select=a;
2
2
  //# sourceMappingURL=Select.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"Select.cjs","names":[],"sources":["../../../src/components/Select/Select.tsx"],"sourcesContent":["import { forwardRef, useId } from \"react\";\nimport type { SelectHTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Select.module.css\";\n\nexport interface SelectOption {\n value: string | number;\n label: string;\n disabled?: boolean;\n}\n\n/** What both variants of {@link Select} accept. */\nexport interface SelectBaseProps extends SelectHTMLAttributes<HTMLSelectElement> {\n options?: SelectOption[];\n placeholder?: string;\n wrapperClassName?: string;\n}\n\n/** The full form field: label, control, helper text and error slot. */\nexport interface SelectFieldProps extends SelectBaseProps {\n variant?: \"field\";\n label?: string;\n helperText?: string;\n error?: string;\n}\n\n/**\n * The bare control, sized to its content.\n *\n * `aria-label` is required rather than optional, because the variant removes the\n * only visible label the component had. A settings row names the control in the\n * row's own text, which the `<select>` has no relationship to — so without this\n * the screen reader announces a combobox with no name at all.\n */\nexport interface SelectChipProps extends SelectBaseProps {\n variant: \"chip\";\n \"aria-label\": string;\n}\n\nexport type SelectProps = SelectFieldProps | SelectChipProps;\n\n/**\n * Native `<select>` wrapper. Either provide `options` for a quick render, or\n * pass `<option>` children directly.\n *\n * Two variants:\n *\n * - `\"field\"` (default) — label, control, helper text and error slot, for a form.\n * - `\"chip\"` — the control alone, sized to its content, for a settings row where\n * the icon and the label are already on the left and the control belongs on the\n * right. Rendering the full field there stacks two labelled fields inside one\n * list item, which is why every app ended up hand-rolling a native `<select>`\n * with `appearance: none` — and hand-rolling it loses the focus ring, the\n * disabled styling and the caret this component already solves.\n *\n * @example\n * <Select label=\"Estado\" options={states} placeholder=\"Selecione\" />\n *\n * @example\n * <ListTile title=\"Idioma\" trailing={<Select variant=\"chip\" aria-label=\"Idioma\" options={langs} />} />\n */\nexport const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(props, ref) {\n const {\n options,\n placeholder,\n wrapperClassName,\n className,\n children,\n id,\n required,\n variant = \"field\",\n label,\n helperText,\n error,\n ...rest\n } = props as SelectBaseProps & {\n variant?: \"field\" | \"chip\";\n label?: string;\n helperText?: string;\n error?: string;\n };\n const isChip = variant === \"chip\";\n const generatedId = useId();\n const selectId = id ?? generatedId;\n\n const control = (\n <div className={cn(styles.field, isChip && styles.chip)}>\n <select\n ref={ref}\n id={selectId}\n aria-invalid={!!error}\n required={required}\n className={cn(styles.select, className)}\n {...rest}\n >\n {placeholder && (\n <option value=\"\" disabled hidden>\n {placeholder}\n </option>\n )}\n {options?.map((opt) => (\n <option key={opt.value} value={opt.value} disabled={opt.disabled}>\n {opt.label}\n </option>\n ))}\n {children}\n </select>\n <span className={styles.caret} aria-hidden>\n <CaretIcon />\n </span>\n </div>\n );\n\n if (isChip) {\n return <div className={cn(styles.chipWrapper, wrapperClassName)}>{control}</div>;\n }\n\n return (\n <div className={cn(styles.wrapper, error && styles.error, wrapperClassName)}>\n {label && (\n <label htmlFor={selectId} className={styles.label}>\n {label}\n {required && <span className={styles.required}>*</span>}\n </label>\n )}\n {control}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n});\n\nfunction CaretIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 9l6 6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"6HA6DA,IAAa,GAAA,EAAS,EAAA,WAAA,CAA2C,SAAgB,EAAO,EAAK,CACzF,GAAM,CACF,UACA,cACA,mBACA,YACA,WACA,KACA,WACA,UAAU,QACV,QACA,aACA,QACA,GAAG,GACH,EAME,EAAS,IAAY,OACrB,GAAA,EAAc,EAAA,MAAA,CAAM,EACpB,EAAW,GAAM,EAEjB,GACF,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,MAAO,GAAU,EAAA,QAAO,IAAI,EAAtD,SAAA,EACI,EAAA,EAAA,KAAA,CAAC,SAAD,CACS,MACL,GAAI,EACJ,eAAc,CAAC,CAAC,EACN,WACV,UAAW,EAAA,GAAG,EAAA,QAAO,OAAQ,CAAS,EACtC,GAAI,EANR,SAAA,CAQK,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,MAAM,GAAG,SAAA,GAAS,OAAA,GACrB,SAAA,CACG,CAAA,EAEX,GAAS,IAAK,IACX,EAAA,EAAA,IAAA,CAAC,SAAD,CAAwB,MAAO,EAAI,MAAO,SAAU,EAAI,SACnD,SAAA,EAAI,KACD,EAFK,EAAI,KAET,CACX,EACA,CACG,CACR,CAAA,GAAA,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAO,cAAA,GAC3B,UAAA,EAAA,EAAA,IAAA,CAAC,EAAD,CAAY,CAAA,CACV,CAAA,CACL,IAOT,OAJI,GACO,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,YAAa,CAAgB,EAAI,SAAA,CAAa,CAAA,GAI/E,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,GAAS,EAAA,QAAO,MAAO,CAAgB,EAA1E,SAAA,CACK,IACG,EAAA,EAAA,KAAA,CAAC,QAAD,CAAO,QAAS,EAAU,UAAW,EAAA,QAAO,MAA5C,SAAA,CACK,EACA,IAAY,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,SAAU,SAAA,GAAO,CAAA,CACnD,IAEV,EACA,GACG,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,UAAY,SAAA,CAAY,CAAA,EAChD,GACA,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,OAAS,SAAA,CAAiB,CAAA,EAClD,IACH,GAEb,CAAC,EAED,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,eACF,OAAO,eACP,YAAY,IACZ,cAAc,QACd,eAAe,OAClB,CAAA,CACA,CAAA,CAEb"}
1
+ {"version":3,"file":"Select.cjs","names":[],"sources":["../../../src/components/Select/Select.tsx"],"sourcesContent":["import { forwardRef, useId } from \"react\";\nimport type { ReactNode, SelectHTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { CaretIcon } from \"./CaretIcon\";\nimport styles from \"./Select.module.css\";\n\nexport interface SelectOption {\n value: string | number;\n label: string;\n disabled?: boolean;\n}\n\n/** What every variant of {@link Select} accepts. */\nexport interface SelectBaseProps extends SelectHTMLAttributes<HTMLSelectElement> {\n options?: SelectOption[];\n placeholder?: string;\n /**\n * Class for the outermost element. On `\"bare\"` that element is the shell\n * holding the `<select>` and the caret, which is where the app's own look goes.\n */\n wrapperClassName?: string;\n /**\n * Replaces the built-in chevron, following the `icon?: ReactNode` convention\n * the rest of the SDK uses.\n *\n * The icon is drawn in the caret slot, which is positioned by\n * `--tempest-control-caret-offset` and sized by `--tempest-control-caret-size`;\n * an `<svg>` passed here is stretched to that size, so the token — not the\n * icon's own `width`/`height` — decides how big the caret is in every density.\n */\n caretIcon?: ReactNode;\n}\n\n/** The full form field: label, control, helper text and error slot. */\nexport interface SelectFieldProps extends SelectBaseProps {\n variant?: \"field\";\n label?: string;\n helperText?: string;\n error?: string;\n}\n\n/**\n * The bare control, sized to its content.\n *\n * `aria-label` is required rather than optional, because the variant removes the\n * only visible label the component had. A settings row names the control in the\n * row's own text, which the `<select>` has no relationship to — so without this\n * the screen reader announces a combobox with no name at all.\n */\nexport interface SelectChipProps extends SelectBaseProps {\n variant: \"chip\";\n \"aria-label\": string;\n}\n\n/**\n * The mechanism without the look: `appearance: none`, the caret and the focus\n * ring, and no border, background, radius, shadow or height.\n *\n * The `<select>` inherits `background-color` and `color` from the shell (the\n * element `wrapperClassName` lands on). The background is not cosmetic: the\n * native popup paints each `<option>` from the `<select>`'s background, so a\n * transparent control would hand the menu back to the system white.\n *\n * Like `\"chip\"`, the variant has no visible label, so an accessible name is\n * required — through `aria-label` or `aria-labelledby`.\n */\nexport type SelectBareProps = SelectBaseProps & { variant: \"bare\" } & (\n { \"aria-label\": string } | { \"aria-labelledby\": string }\n );\n\nexport type SelectProps = SelectFieldProps | SelectChipProps | SelectBareProps;\n\n/**\n * Native `<select>` wrapper. Either provide `options` for a quick render, or\n * pass `<option>` children directly.\n *\n * Three variants:\n *\n * - `\"field\"` (default) — label, control, helper text and error slot, for a form.\n * - `\"chip\"` — the control alone, sized to its content, for a settings row where\n * the icon and the label are already on the left and the control belongs on the\n * right. Rendering the full field there stacks two labelled fields inside one\n * list item, which is why every app ended up hand-rolling a native `<select>`\n * with `appearance: none` — and hand-rolling it loses the focus ring, the\n * disabled styling and the caret this component already solves.\n * - `\"bare\"` — the same mechanism with none of the look, for an app with its own\n * visual identity: the shell renders `wrapper > select + caret`, and the app\n * dresses the wrapper through `wrapperClassName`. See {@link SelectBareProps}.\n *\n * @example\n * <Select label=\"Estado\" options={states} placeholder=\"Selecione\" />\n *\n * @example\n * <ListTile title=\"Idioma\" trailing={<Select variant=\"chip\" aria-label=\"Idioma\" options={langs} />} />\n *\n * @example\n * <Select variant=\"bare\" wrapperClassName={styles.filtro} aria-label=\"Status\" options={status} />\n */\nexport const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(props, ref) {\n const {\n options,\n placeholder,\n wrapperClassName,\n className,\n children,\n id,\n required,\n variant = \"field\",\n label,\n helperText,\n error,\n caretIcon,\n ...rest\n } = props as SelectBaseProps & {\n variant?: SelectProps[\"variant\"];\n label?: string;\n helperText?: string;\n error?: string;\n };\n const isChip = variant === \"chip\";\n const isBare = variant === \"bare\";\n const generatedId = useId();\n const selectId = id ?? generatedId;\n\n const control = (\n <div\n className={cn(\n styles.field,\n isChip && styles.chip,\n isBare && styles.bare,\n isBare && wrapperClassName,\n )}\n >\n <select\n ref={ref}\n id={selectId}\n aria-invalid={!!error}\n required={required}\n className={cn(styles.select, className)}\n {...rest}\n >\n {placeholder && (\n <option value=\"\" disabled hidden>\n {placeholder}\n </option>\n )}\n {options?.map((opt) => (\n <option key={opt.value} value={opt.value} disabled={opt.disabled}>\n {opt.label}\n </option>\n ))}\n {children}\n </select>\n <span className={styles.caret} aria-hidden>\n {caretIcon ?? <CaretIcon />}\n </span>\n </div>\n );\n\n if (isBare) {\n return control;\n }\n\n if (isChip) {\n return <div className={cn(styles.chipWrapper, wrapperClassName)}>{control}</div>;\n }\n\n return (\n <div className={cn(styles.wrapper, error && styles.error, wrapperClassName)}>\n {label && (\n <label htmlFor={selectId} className={styles.label}>\n {label}\n {required && <span className={styles.required}>*</span>}\n </label>\n )}\n {control}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n});\n"],"mappings":"0JAkGA,IAAa,GAAA,EAAS,EAAA,WAAA,CAA2C,SAAgB,EAAO,EAAK,CACzF,GAAM,CACF,UACA,cACA,mBACA,YACA,WACA,KACA,WACA,UAAU,QACV,QACA,aACA,QACA,YACA,GAAG,GACH,EAME,EAAS,IAAY,OACrB,EAAS,IAAY,OACrB,GAAA,EAAc,EAAA,MAAA,CAAM,EACpB,EAAW,GAAM,EAEjB,GACF,EAAA,EAAA,KAAA,CAAC,MAAD,CACI,UAAW,EAAA,GACP,EAAA,QAAO,MACP,GAAU,EAAA,QAAO,KACjB,GAAU,EAAA,QAAO,KACjB,GAAU,CACd,EANJ,SAAA,EAQI,EAAA,EAAA,KAAA,CAAC,SAAD,CACS,MACL,GAAI,EACJ,eAAc,CAAC,CAAC,EACN,WACV,UAAW,EAAA,GAAG,EAAA,QAAO,OAAQ,CAAS,EACtC,GAAI,EANR,SAAA,CAQK,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,MAAM,GAAG,SAAA,GAAS,OAAA,GACrB,SAAA,CACG,CAAA,EAEX,GAAS,IAAK,IACX,EAAA,EAAA,IAAA,CAAC,SAAD,CAAwB,MAAO,EAAI,MAAO,SAAU,EAAI,SACnD,SAAA,EAAI,KACD,EAFK,EAAI,KAET,CACX,EACA,CACG,CACR,CAAA,GAAA,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAO,cAAA,GAC1B,SAAA,IAAa,EAAA,EAAA,IAAA,CAAC,EAAA,UAAD,CAAY,CAAA,CACxB,CAAA,CACL,IAWT,OARI,EACO,EAGP,GACO,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,YAAa,CAAgB,EAAI,SAAA,CAAa,CAAA,GAI/E,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,GAAS,EAAA,QAAO,MAAO,CAAgB,EAA1E,SAAA,CACK,IACG,EAAA,EAAA,KAAA,CAAC,QAAD,CAAO,QAAS,EAAU,UAAW,EAAA,QAAO,MAA5C,SAAA,CACK,EACA,IAAY,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,SAAU,SAAA,GAAO,CAAA,CACnD,IAEV,EACA,GACG,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,UAAY,SAAA,CAAY,CAAA,EAChD,GACA,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,OAAS,SAAA,CAAiB,CAAA,EAClD,IACH,GAEb,CAAC"}