tempest-react-sdk 0.67.0 → 0.69.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 (147) 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 +34 -30
  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 +78 -77
  15. package/dist/components/Combobox/Combobox.js.map +1 -1
  16. package/dist/components/Combobox/Combobox.module.cjs.map +1 -1
  17. package/dist/components/Combobox/Combobox.module.js.map +1 -1
  18. package/dist/components/Command/Command.cjs +1 -1
  19. package/dist/components/Command/Command.js +4 -4
  20. package/dist/components/DataTable/DataTable.cjs +1 -1
  21. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  22. package/dist/components/DataTable/DataTable.js +138 -127
  23. package/dist/components/DataTable/DataTable.js.map +1 -1
  24. package/dist/components/DataTable/use-dev-warnings.cjs +1 -1
  25. package/dist/components/DataTable/use-dev-warnings.cjs.map +1 -1
  26. package/dist/components/DataTable/use-dev-warnings.js +5 -3
  27. package/dist/components/DataTable/use-dev-warnings.js.map +1 -1
  28. package/dist/components/Drawer/Drawer.cjs +1 -1
  29. package/dist/components/Drawer/Drawer.cjs.map +1 -1
  30. package/dist/components/Drawer/Drawer.js +43 -40
  31. package/dist/components/Drawer/Drawer.js.map +1 -1
  32. package/dist/components/FilterPanel/FilterPanel.cjs +2 -0
  33. package/dist/components/FilterPanel/FilterPanel.cjs.map +1 -0
  34. package/dist/components/FilterPanel/FilterPanel.js +75 -0
  35. package/dist/components/FilterPanel/FilterPanel.js.map +1 -0
  36. package/dist/components/FilterPanel/FilterPanel.module.cjs +3 -0
  37. package/dist/components/FilterPanel/FilterPanel.module.cjs.map +1 -0
  38. package/dist/components/FilterPanel/FilterPanel.module.js +12 -0
  39. package/dist/components/FilterPanel/FilterPanel.module.js.map +1 -0
  40. package/dist/components/Lightbox/Lightbox.cjs +1 -1
  41. package/dist/components/Lightbox/Lightbox.js +5 -5
  42. package/dist/components/Modal/Modal.cjs +1 -1
  43. package/dist/components/Modal/Modal.cjs.map +1 -1
  44. package/dist/components/Modal/Modal.js +44 -41
  45. package/dist/components/Modal/Modal.js.map +1 -1
  46. package/dist/components/Pagination/Pagination.cjs +1 -1
  47. package/dist/components/Pagination/Pagination.cjs.map +1 -1
  48. package/dist/components/Pagination/Pagination.js +31 -30
  49. package/dist/components/Pagination/Pagination.js.map +1 -1
  50. package/dist/components/Portal/tab-bridge.cjs +1 -1
  51. package/dist/components/Portal/tab-bridge.cjs.map +1 -1
  52. package/dist/components/Portal/tab-bridge.js +21 -23
  53. package/dist/components/Portal/tab-bridge.js.map +1 -1
  54. package/dist/components/Select/CaretIcon.cjs +2 -0
  55. package/dist/components/Select/CaretIcon.cjs.map +1 -0
  56. package/dist/components/Select/CaretIcon.js +21 -0
  57. package/dist/components/Select/CaretIcon.js.map +1 -0
  58. package/dist/components/Select/Select.cjs +1 -1
  59. package/dist/components/Select/Select.cjs.map +1 -1
  60. package/dist/components/Select/Select.js +33 -47
  61. package/dist/components/Select/Select.js.map +1 -1
  62. package/dist/components/Select/Select.module.cjs +1 -1
  63. package/dist/components/Select/Select.module.cjs.map +1 -1
  64. package/dist/components/Select/Select.module.js +4 -3
  65. package/dist/components/Select/Select.module.js.map +1 -1
  66. package/dist/components/Select/options.cjs +2 -0
  67. package/dist/components/Select/options.cjs.map +1 -0
  68. package/dist/components/Select/options.js +25 -0
  69. package/dist/components/Select/options.js.map +1 -0
  70. package/dist/components/Sidebar/Sidebar.cjs +1 -1
  71. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  72. package/dist/components/Sidebar/Sidebar.js +49 -43
  73. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  74. package/dist/components/Sidebar/active-nav-key.cjs +2 -0
  75. package/dist/components/Sidebar/active-nav-key.cjs.map +1 -0
  76. package/dist/components/Sidebar/active-nav-key.js +22 -0
  77. package/dist/components/Sidebar/active-nav-key.js.map +1 -0
  78. package/dist/hooks/tab-sequence.cjs +2 -0
  79. package/dist/hooks/tab-sequence.cjs.map +1 -0
  80. package/dist/hooks/tab-sequence.js +53 -0
  81. package/dist/hooks/tab-sequence.js.map +1 -0
  82. package/dist/hooks/use-deep-memo.cjs +1 -1
  83. package/dist/hooks/use-deep-memo.cjs.map +1 -1
  84. package/dist/hooks/use-deep-memo.js +1 -1
  85. package/dist/hooks/use-deep-memo.js.map +1 -1
  86. package/dist/hooks/use-draft-filters.cjs +2 -0
  87. package/dist/hooks/use-draft-filters.cjs.map +1 -0
  88. package/dist/hooks/use-draft-filters.js +49 -0
  89. package/dist/hooks/use-draft-filters.js.map +1 -0
  90. package/dist/hooks/use-focus-trap.cjs +1 -1
  91. package/dist/hooks/use-focus-trap.cjs.map +1 -1
  92. package/dist/hooks/use-focus-trap.js +31 -34
  93. package/dist/hooks/use-focus-trap.js.map +1 -1
  94. package/dist/http/api-client.cjs +1 -1
  95. package/dist/http/api-client.cjs.map +1 -1
  96. package/dist/http/api-client.js +92 -80
  97. package/dist/http/api-client.js.map +1 -1
  98. package/dist/http/credential-scope.cjs +1 -1
  99. package/dist/http/credential-scope.cjs.map +1 -1
  100. package/dist/http/credential-scope.js +5 -3
  101. package/dist/http/credential-scope.js.map +1 -1
  102. package/dist/http/csrf.cjs +2 -0
  103. package/dist/http/csrf.cjs.map +1 -0
  104. package/dist/http/csrf.js +46 -0
  105. package/dist/http/csrf.js.map +1 -0
  106. package/dist/http/resumable-upload.cjs +1 -1
  107. package/dist/http/resumable-upload.cjs.map +1 -1
  108. package/dist/http/resumable-upload.js +131 -123
  109. package/dist/http/resumable-upload.js.map +1 -1
  110. package/dist/http/upload-with-progress.cjs +1 -1
  111. package/dist/http/upload-with-progress.cjs.map +1 -1
  112. package/dist/http/upload-with-progress.js +41 -33
  113. package/dist/http/upload-with-progress.js.map +1 -1
  114. package/dist/query/use-paginated-query.cjs +1 -1
  115. package/dist/query/use-paginated-query.cjs.map +1 -1
  116. package/dist/query/use-paginated-query.js +47 -30
  117. package/dist/query/use-paginated-query.js.map +1 -1
  118. package/dist/styles/FilterPanel.css +2 -0
  119. package/dist/styles/component/Card.css +1 -1
  120. package/dist/styles/component/Combobox.css +4 -3
  121. package/dist/styles/component/FilterPanel.css +5 -0
  122. package/dist/styles/component/Select.css +13 -6
  123. package/dist/styles/core.css +4 -4
  124. package/dist/styles/data.css +4 -0
  125. package/dist/styles/forms.css +17 -9
  126. package/dist/styles/identity.css +1 -1
  127. package/dist/styles/manifest.json +6 -0
  128. package/dist/styles/tokens.css +4 -4
  129. package/dist/styles.css +1 -1
  130. package/dist/sw/create-push-handler.cjs +1 -1
  131. package/dist/sw/create-push-handler.cjs.map +1 -1
  132. package/dist/sw/create-push-handler.js +58 -30
  133. package/dist/sw/create-push-handler.js.map +1 -1
  134. package/dist/sw.d.ts +53 -2
  135. package/dist/tempest-react-sdk.cjs +1 -1
  136. package/dist/tempest-react-sdk.d.ts +711 -24
  137. package/dist/tempest-react-sdk.js +347 -342
  138. package/dist/vite/tempest-csp.cjs +3 -0
  139. package/dist/vite/tempest-csp.cjs.map +1 -0
  140. package/dist/vite/tempest-csp.js +189 -0
  141. package/dist/vite/tempest-csp.js.map +1 -0
  142. package/dist/vite.cjs +1 -1
  143. package/dist/vite.d.ts +145 -0
  144. package/dist/vite.js +2 -1
  145. package/package.json +2 -2
  146. package/template/vite.config.ts +8 -1
  147. package/template-pwa/vite.config.ts +6 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"active-nav-key.js","names":[],"sources":["../../../src/components/Sidebar/active-nav-key.ts"],"sourcesContent":["/**\n * Reduce a path or a key to the form two of them are compared in.\n *\n * The query string and the hash are dropped, because `/users?tab=2` is still the\n * `/users` screen, and trailing slashes are dropped, because `/users/` and\n * `/users` are the same route to every router this SDK pairs with. The root keeps\n * its single slash — stripping it would leave an empty string, which is the\n * \"nothing matched\" answer, not a path.\n *\n * @param path - A pathname, an `href`, or a key used as a URL.\n * @returns The comparable form of `path`.\n */\nfunction normalizePath(path: string): string {\n const cut = path.search(/[?#]/);\n const bare = cut === -1 ? path : path.slice(0, cut);\n const trimmed = bare.replace(/\\/+$/, \"\");\n return trimmed === \"\" && bare.startsWith(\"/\") ? \"/\" : trimmed;\n}\n\n/**\n * Whether `candidate` names the screen at `pathname` or one of its ancestors.\n *\n * The match is on **segment boundaries**, not on string prefixes: `/users` covers\n * `/users/3` but not `/users-admin`. The root `/` covers only itself — the same\n * rule react-router's `NavLink` applies — because a root that prefixed every path\n * would light \"Home\" on every screen the menu does not list.\n *\n * @param pathname - The normalized current path.\n * @param candidate - The normalized key or `href`.\n * @returns `true` when the candidate covers the path.\n */\nfunction covers(pathname: string, candidate: string): boolean {\n if (candidate === \"/\") {\n return pathname === \"/\";\n }\n return pathname === candidate || pathname.startsWith(`${candidate}/`);\n}\n\n/**\n * Index of the candidate that owns `pathname`: the longest one covering it.\n *\n * Longest wins so that `/dashboard` does not stay highlighted on\n * `/dashboard/tips/42` — with a first-match rule the order of the menu array would\n * decide, and nobody reading the menu knows it carries meaning. On a tie (`/users`\n * and `/users/` both present), the first one wins.\n *\n * @param pathname - The current route, as the router reports it.\n * @param candidates - One string per navigable entry, in menu order.\n * @returns The winning index, or `-1` when nothing covers the path.\n */\nexport function activeNavIndex(pathname: string, candidates: readonly string[]): number {\n const path = normalizePath(pathname);\n let winner = -1;\n let winnerLength = -1;\n candidates.forEach((raw, index) => {\n const candidate = normalizePath(raw);\n if (candidate !== \"\" && covers(path, candidate) && candidate.length > winnerLength) {\n winner = index;\n winnerLength = candidate.length;\n }\n });\n return winner;\n}\n\n/**\n * Resolve which navigation key is active for the current route.\n *\n * A menu whose keys are URLs almost never has a key equal to the pathname —\n * `/dashboard/tips/42` has to highlight `/dashboard/tips`. This picks the\n * **longest key that covers the path on a segment boundary**, which is what fixes\n * the two ways the one-line `keys.find((k) => pathname.startsWith(k))` goes wrong:\n * `/dashboard` stays lit on every screen of the panel (the array order decides),\n * and `/dashboard/tip` lights `/dashboard/tips`.\n *\n * The rules:\n *\n * - Query string and hash are ignored, on both sides.\n * - A trailing slash, on the route or on the key, changes nothing.\n * - `/` is active only on `/` itself, as with react-router's `NavLink`.\n * - The comparison is case-sensitive, as URL paths are.\n * - No match returns `\"\"` — never `undefined`, never a throw — so the result can\n * go straight into a `value` prop.\n *\n * The keys and the pathname have to live in the same space: with a router\n * `basename`, pass `useLocation().pathname` (which has the basename stripped) and\n * keys without it.\n *\n * Framework-agnostic on purpose: it takes a string, so it works with\n * react-router, with `window.location.pathname`, and for `BottomNavigation`,\n * `NavigationRail` or a `Drawer` menu just as for `Sidebar` — which does the same\n * resolution itself with `match=\"route\"`.\n *\n * @param pathname - The current route, e.g. `useLocation().pathname`.\n * @param keys - The navigation keys, each one a URL path.\n * @returns The active key exactly as given in `keys`, or `\"\"` when none covers\n * the route.\n *\n * @example\n * const { pathname } = useLocation();\n * const value = activeNavKey(pathname, [\"/dashboard\", \"/dashboard/tips\"]);\n * // \"/dashboard/tips/42\" → \"/dashboard/tips\"\n * // \"/dashboard/tip\" → \"/dashboard\"\n */\nexport function activeNavKey(pathname: string, keys: readonly string[]): string {\n return keys[activeNavIndex(pathname, keys)] ?? \"\";\n}\n"],"mappings":";AAYA,SAAS,EAAc,GAAsB;CACzC,IAAM,IAAM,EAAK,OAAO,MAAM,GACxB,IAAO,MAAQ,KAAK,IAAO,EAAK,MAAM,GAAG,CAAG,GAC5C,IAAU,EAAK,QAAQ,QAAQ,EAAE;CACvC,OAAO,MAAY,MAAM,EAAK,WAAW,GAAG,IAAI,MAAM;AAC1D;AAcA,SAAS,EAAO,GAAkB,GAA4B;CAI1D,OAHI,MAAc,MACP,MAAa,MAEjB,MAAa,KAAa,EAAS,WAAW,GAAG,EAAU,EAAE;AACxE;AAcA,SAAgB,EAAe,GAAkB,GAAuC;CACpF,IAAM,IAAO,EAAc,CAAQ,GAC/B,IAAS,IACT,IAAe;CAQnB,OAPA,EAAW,SAAS,GAAK,MAAU;EAC/B,IAAM,IAAY,EAAc,CAAG;EACnC,AAAI,MAAc,MAAM,EAAO,GAAM,CAAS,KAAK,EAAU,SAAS,MAClE,IAAS,GACT,IAAe,EAAU;CAEjC,CAAC,GACM;AACX;AAyCA,SAAgB,EAAa,GAAkB,GAAiC;CAC5E,OAAO,EAAK,EAAe,GAAU,CAAI,MAAM;AACnD"}
@@ -0,0 +1,2 @@
1
+ var e=[`a[href]`,`button:not([disabled])`,`textarea:not([disabled])`,`input:not([disabled])`,`select:not([disabled])`,`[tabindex]:not([tabindex='-1'])`].join(`,`),t=new Set;function n(e){return t.add(e),()=>{t.delete(e)}}function r(t){return Array.from(t.querySelectorAll(e)).filter(e=>{if(e.tabIndex<0||e.hasAttribute(`aria-hidden`))return!1;let t=window.getComputedStyle(e);return t.display!==`none`&&t.visibility!==`hidden`})}function i(e,t){let n=t.filter(t=>e.contains(t.anchor)&&!e.contains(t.panel));return{owned:n,rest:t.filter(e=>!n.includes(e))}}function a(e){let n=[e],r=[...t];for(let e=0;e<n.length;e+=1){let{owned:t,rest:a}=i(n[e],r);n.push(...t.map(e=>e.panel)),r=a}return n}function o(e,n=[...t]){let a=r(e),{owned:s,rest:c}=i(e,n),l=new Map;for(let e of s){let t=-1;a.forEach((n,r)=>{((n.compareDocumentPosition(e.anchor)&Node.DOCUMENT_POSITION_FOLLOWING)!==0||e.anchor.contains(n))&&(t=r)}),l.set(t,[...l.get(t)??[],...o(e.panel,c)])}let u=[...l.get(-1)??[]];return a.forEach((e,t)=>{u.push(e,...l.get(t)??[])}),u}exports.FOCUSABLE_SELECTOR=e,exports.registerPortalLayer=n,exports.tabRegion=a,exports.tabSequence=o,exports.tabbables=r;
2
+ //# sourceMappingURL=tab-sequence.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab-sequence.cjs","names":[],"sources":["../../src/hooks/tab-sequence.ts"],"sourcesContent":["/** Elements that can take keyboard focus — shared by the focus trap and the portal tab order. */\nexport const FOCUSABLE_SELECTOR = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\n/** A portalled panel and the element of the page it was opened from. */\nexport interface PortalLayer {\n /** Wrapper of the trigger — the spot in the page the panel belongs to. */\n anchor: HTMLElement;\n /** The panel, mounted at the end of `body`. */\n panel: HTMLElement;\n}\n\nconst layers = new Set<PortalLayer>();\n\n/**\n * Declare a portalled panel as belonging to the spot of its anchor.\n *\n * A portal takes the panel out of the subtree it was opened from, so a focus\n * trap around that subtree would see the panel as \"outside\" and pull focus away\n * from it. Registered layers are what {@link tabSequence} and\n * {@link tabRegion} splice back in.\n *\n * @param layer - The anchor and the panel.\n * @returns A function that removes the registration.\n */\nexport function registerPortalLayer(layer: PortalLayer): () => void {\n layers.add(layer);\n return () => {\n layers.delete(layer);\n };\n}\n\n/**\n * The elements `Tab` can stop on inside `root`, in document order.\n *\n * @param root - Where to look.\n * @returns The tabbable descendants that are not disabled, hidden or removed\n * from the tab order.\n */\nexport function tabbables(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter((el) => {\n if (el.tabIndex < 0 || el.hasAttribute(\"aria-hidden\")) return false;\n const style = window.getComputedStyle(el);\n return style.display !== \"none\" && style.visibility !== \"hidden\";\n });\n}\n\n/**\n * Split the layers into those opened from inside `root` and the rest.\n *\n * @param root - The subtree the layers are matched against.\n * @param pending - The layers not yet assigned to a subtree.\n * @returns The layers whose anchor lives in `root` (and whose panel does not),\n * and the remaining ones.\n */\nfunction ownedBy(\n root: HTMLElement,\n pending: PortalLayer[],\n): { owned: PortalLayer[]; rest: PortalLayer[] } {\n const owned = pending.filter(\n (layer) => root.contains(layer.anchor) && !root.contains(layer.panel),\n );\n return { owned, rest: pending.filter((layer) => !owned.includes(layer)) };\n}\n\n/**\n * Every subtree focus may sit in while `root` is trapped: `root` itself plus the\n * panels opened from it, transitively (a popover opened from a popover).\n *\n * @param root - The trapped container.\n * @returns `root` followed by the panels that belong to it.\n */\nexport function tabRegion(root: HTMLElement): HTMLElement[] {\n const region: HTMLElement[] = [root];\n let pending = [...layers];\n for (let index = 0; index < region.length; index += 1) {\n const { owned, rest } = ownedBy(region[index]!, pending);\n region.push(...owned.map((layer) => layer.panel));\n pending = rest;\n }\n return region;\n}\n\n/**\n * The order `Tab` walks inside `root`, with each owned portalled panel spliced\n * right after the last stop of its anchor — where it would sit had it rendered\n * in flow. That is the same order `usePortalTabOrder` restores between the\n * trigger and the panel, so the trap wraps at the same ends the user walks.\n *\n * @param root - The trapped container.\n * @param pending - Layers not yet placed; defaults to every registered one.\n * @returns The tab stops of `root` and of its panels, in walking order.\n */\nexport function tabSequence(\n root: HTMLElement,\n pending: PortalLayer[] = [...layers],\n): HTMLElement[] {\n const native = tabbables(root);\n const { owned, rest } = ownedBy(root, pending);\n const inserted = new Map<number, HTMLElement[]>();\n for (const layer of owned) {\n let at = -1;\n native.forEach((el, index) => {\n const precedes =\n (el.compareDocumentPosition(layer.anchor) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;\n if (precedes || layer.anchor.contains(el)) at = index;\n });\n inserted.set(at, [...(inserted.get(at) ?? []), ...tabSequence(layer.panel, rest)]);\n }\n const sequence: HTMLElement[] = [...(inserted.get(-1) ?? [])];\n native.forEach((el, index) => {\n sequence.push(el, ...(inserted.get(index) ?? []));\n });\n return sequence;\n}\n"],"mappings":"AACA,IAAa,EAAqB,CAC9B,UACA,yBACA,2BACA,wBACA,yBACA,iCACJ,CAAC,CAAC,KAAK,GAAG,EAUJ,EAAS,IAAI,IAanB,SAAgB,EAAoB,EAAgC,CAEhE,OADA,EAAO,IAAI,CAAK,MACH,CACT,EAAO,OAAO,CAAK,CACvB,CACJ,CASA,SAAgB,EAAU,EAAiC,CACvD,OAAO,MAAM,KAAK,EAAK,iBAA8B,CAAkB,CAAC,CAAC,CAAC,OAAQ,GAAO,CACrF,GAAI,EAAG,SAAW,GAAK,EAAG,aAAa,aAAa,EAAG,MAAO,GAC9D,IAAM,EAAQ,OAAO,iBAAiB,CAAE,EACxC,OAAO,EAAM,UAAY,QAAU,EAAM,aAAe,QAC5D,CAAC,CACL,CAUA,SAAS,EACL,EACA,EAC6C,CAC7C,IAAM,EAAQ,EAAQ,OACjB,GAAU,EAAK,SAAS,EAAM,MAAM,GAAK,CAAC,EAAK,SAAS,EAAM,KAAK,CACxE,EACA,MAAO,CAAE,QAAO,KAAM,EAAQ,OAAQ,GAAU,CAAC,EAAM,SAAS,CAAK,CAAC,CAAE,CAC5E,CASA,SAAgB,EAAU,EAAkC,CACxD,IAAM,EAAwB,CAAC,CAAI,EAC/B,EAAU,CAAC,GAAG,CAAM,EACxB,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAO,OAAQ,GAAS,EAAG,CACnD,GAAM,CAAE,QAAO,QAAS,EAAQ,EAAO,GAAS,CAAO,EACvD,EAAO,KAAK,GAAG,EAAM,IAAK,GAAU,EAAM,KAAK,CAAC,EAChD,EAAU,CACd,CACA,OAAO,CACX,CAYA,SAAgB,EACZ,EACA,EAAyB,CAAC,GAAG,CAAM,EACtB,CACb,IAAM,EAAS,EAAU,CAAI,EACvB,CAAE,QAAO,QAAS,EAAQ,EAAM,CAAO,EACvC,EAAW,IAAI,IACrB,IAAK,IAAM,KAAS,EAAO,CACvB,IAAI,EAAK,GACT,EAAO,SAAS,EAAI,IAAU,GAErB,EAAG,wBAAwB,EAAM,MAAM,EAAI,KAAK,+BAAiC,GACtE,EAAM,OAAO,SAAS,CAAE,KAAG,EAAK,EACpD,CAAC,EACD,EAAS,IAAI,EAAI,CAAC,GAAI,EAAS,IAAI,CAAE,GAAK,CAAC,EAAI,GAAG,EAAY,EAAM,MAAO,CAAI,CAAC,CAAC,CACrF,CACA,IAAM,EAA0B,CAAC,GAAI,EAAS,IAAI,EAAE,GAAK,CAAC,CAAE,EAI5D,OAHA,EAAO,SAAS,EAAI,IAAU,CAC1B,EAAS,KAAK,EAAI,GAAI,EAAS,IAAI,CAAK,GAAK,CAAC,CAAE,CACpD,CAAC,EACM,CACX"}
@@ -0,0 +1,53 @@
1
+ //#region src/hooks/tab-sequence.ts
2
+ var e = [
3
+ "a[href]",
4
+ "button:not([disabled])",
5
+ "textarea:not([disabled])",
6
+ "input:not([disabled])",
7
+ "select:not([disabled])",
8
+ "[tabindex]:not([tabindex='-1'])"
9
+ ].join(","), t = /* @__PURE__ */ new Set();
10
+ function n(e) {
11
+ return t.add(e), () => {
12
+ t.delete(e);
13
+ };
14
+ }
15
+ function r(t) {
16
+ return Array.from(t.querySelectorAll(e)).filter((e) => {
17
+ if (e.tabIndex < 0 || e.hasAttribute("aria-hidden")) return !1;
18
+ let t = window.getComputedStyle(e);
19
+ return t.display !== "none" && t.visibility !== "hidden";
20
+ });
21
+ }
22
+ function i(e, t) {
23
+ let n = t.filter((t) => e.contains(t.anchor) && !e.contains(t.panel));
24
+ return {
25
+ owned: n,
26
+ rest: t.filter((e) => !n.includes(e))
27
+ };
28
+ }
29
+ function a(e) {
30
+ let n = [e], r = [...t];
31
+ for (let e = 0; e < n.length; e += 1) {
32
+ let { owned: t, rest: a } = i(n[e], r);
33
+ n.push(...t.map((e) => e.panel)), r = a;
34
+ }
35
+ return n;
36
+ }
37
+ function o(e, n = [...t]) {
38
+ let a = r(e), { owned: s, rest: c } = i(e, n), l = /* @__PURE__ */ new Map();
39
+ for (let e of s) {
40
+ let t = -1;
41
+ a.forEach((n, r) => {
42
+ ((n.compareDocumentPosition(e.anchor) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0 || e.anchor.contains(n)) && (t = r);
43
+ }), l.set(t, [...l.get(t) ?? [], ...o(e.panel, c)]);
44
+ }
45
+ let u = [...l.get(-1) ?? []];
46
+ return a.forEach((e, t) => {
47
+ u.push(e, ...l.get(t) ?? []);
48
+ }), u;
49
+ }
50
+ //#endregion
51
+ export { e as FOCUSABLE_SELECTOR, n as registerPortalLayer, a as tabRegion, o as tabSequence, r as tabbables };
52
+
53
+ //# sourceMappingURL=tab-sequence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab-sequence.js","names":[],"sources":["../../src/hooks/tab-sequence.ts"],"sourcesContent":["/** Elements that can take keyboard focus — shared by the focus trap and the portal tab order. */\nexport const FOCUSABLE_SELECTOR = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\n/** A portalled panel and the element of the page it was opened from. */\nexport interface PortalLayer {\n /** Wrapper of the trigger — the spot in the page the panel belongs to. */\n anchor: HTMLElement;\n /** The panel, mounted at the end of `body`. */\n panel: HTMLElement;\n}\n\nconst layers = new Set<PortalLayer>();\n\n/**\n * Declare a portalled panel as belonging to the spot of its anchor.\n *\n * A portal takes the panel out of the subtree it was opened from, so a focus\n * trap around that subtree would see the panel as \"outside\" and pull focus away\n * from it. Registered layers are what {@link tabSequence} and\n * {@link tabRegion} splice back in.\n *\n * @param layer - The anchor and the panel.\n * @returns A function that removes the registration.\n */\nexport function registerPortalLayer(layer: PortalLayer): () => void {\n layers.add(layer);\n return () => {\n layers.delete(layer);\n };\n}\n\n/**\n * The elements `Tab` can stop on inside `root`, in document order.\n *\n * @param root - Where to look.\n * @returns The tabbable descendants that are not disabled, hidden or removed\n * from the tab order.\n */\nexport function tabbables(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter((el) => {\n if (el.tabIndex < 0 || el.hasAttribute(\"aria-hidden\")) return false;\n const style = window.getComputedStyle(el);\n return style.display !== \"none\" && style.visibility !== \"hidden\";\n });\n}\n\n/**\n * Split the layers into those opened from inside `root` and the rest.\n *\n * @param root - The subtree the layers are matched against.\n * @param pending - The layers not yet assigned to a subtree.\n * @returns The layers whose anchor lives in `root` (and whose panel does not),\n * and the remaining ones.\n */\nfunction ownedBy(\n root: HTMLElement,\n pending: PortalLayer[],\n): { owned: PortalLayer[]; rest: PortalLayer[] } {\n const owned = pending.filter(\n (layer) => root.contains(layer.anchor) && !root.contains(layer.panel),\n );\n return { owned, rest: pending.filter((layer) => !owned.includes(layer)) };\n}\n\n/**\n * Every subtree focus may sit in while `root` is trapped: `root` itself plus the\n * panels opened from it, transitively (a popover opened from a popover).\n *\n * @param root - The trapped container.\n * @returns `root` followed by the panels that belong to it.\n */\nexport function tabRegion(root: HTMLElement): HTMLElement[] {\n const region: HTMLElement[] = [root];\n let pending = [...layers];\n for (let index = 0; index < region.length; index += 1) {\n const { owned, rest } = ownedBy(region[index]!, pending);\n region.push(...owned.map((layer) => layer.panel));\n pending = rest;\n }\n return region;\n}\n\n/**\n * The order `Tab` walks inside `root`, with each owned portalled panel spliced\n * right after the last stop of its anchor — where it would sit had it rendered\n * in flow. That is the same order `usePortalTabOrder` restores between the\n * trigger and the panel, so the trap wraps at the same ends the user walks.\n *\n * @param root - The trapped container.\n * @param pending - Layers not yet placed; defaults to every registered one.\n * @returns The tab stops of `root` and of its panels, in walking order.\n */\nexport function tabSequence(\n root: HTMLElement,\n pending: PortalLayer[] = [...layers],\n): HTMLElement[] {\n const native = tabbables(root);\n const { owned, rest } = ownedBy(root, pending);\n const inserted = new Map<number, HTMLElement[]>();\n for (const layer of owned) {\n let at = -1;\n native.forEach((el, index) => {\n const precedes =\n (el.compareDocumentPosition(layer.anchor) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;\n if (precedes || layer.anchor.contains(el)) at = index;\n });\n inserted.set(at, [...(inserted.get(at) ?? []), ...tabSequence(layer.panel, rest)]);\n }\n const sequence: HTMLElement[] = [...(inserted.get(-1) ?? [])];\n native.forEach((el, index) => {\n sequence.push(el, ...(inserted.get(index) ?? []));\n });\n return sequence;\n}\n"],"mappings":";AACA,IAAa,IAAqB;CAC9B;CACA;CACA;CACA;CACA;CACA;AACJ,CAAC,CAAC,KAAK,GAAG,GAUJ,oBAAS,IAAI,IAAiB;AAapC,SAAgB,EAAoB,GAAgC;CAEhE,OADA,EAAO,IAAI,CAAK,SACH;EACT,EAAO,OAAO,CAAK;CACvB;AACJ;AASA,SAAgB,EAAU,GAAiC;CACvD,OAAO,MAAM,KAAK,EAAK,iBAA8B,CAAkB,CAAC,CAAC,CAAC,QAAQ,MAAO;EACrF,IAAI,EAAG,WAAW,KAAK,EAAG,aAAa,aAAa,GAAG,OAAO;EAC9D,IAAM,IAAQ,OAAO,iBAAiB,CAAE;EACxC,OAAO,EAAM,YAAY,UAAU,EAAM,eAAe;CAC5D,CAAC;AACL;AAUA,SAAS,EACL,GACA,GAC6C;CAC7C,IAAM,IAAQ,EAAQ,QACjB,MAAU,EAAK,SAAS,EAAM,MAAM,KAAK,CAAC,EAAK,SAAS,EAAM,KAAK,CACxE;CACA,OAAO;EAAE;EAAO,MAAM,EAAQ,QAAQ,MAAU,CAAC,EAAM,SAAS,CAAK,CAAC;CAAE;AAC5E;AASA,SAAgB,EAAU,GAAkC;CACxD,IAAM,IAAwB,CAAC,CAAI,GAC/B,IAAU,CAAC,GAAG,CAAM;CACxB,KAAK,IAAI,IAAQ,GAAG,IAAQ,EAAO,QAAQ,KAAS,GAAG;EACnD,IAAM,EAAE,UAAO,YAAS,EAAQ,EAAO,IAAS,CAAO;EAEvD,AADA,EAAO,KAAK,GAAG,EAAM,KAAK,MAAU,EAAM,KAAK,CAAC,GAChD,IAAU;CACd;CACA,OAAO;AACX;AAYA,SAAgB,EACZ,GACA,IAAyB,CAAC,GAAG,CAAM,GACtB;CACb,IAAM,IAAS,EAAU,CAAI,GACvB,EAAE,UAAO,YAAS,EAAQ,GAAM,CAAO,GACvC,oBAAW,IAAI,IAA2B;CAChD,KAAK,IAAM,KAAS,GAAO;EACvB,IAAI,IAAK;EAMT,AALA,EAAO,SAAS,GAAI,MAAU;GAG1B,EADK,EAAG,wBAAwB,EAAM,MAAM,IAAI,KAAK,iCAAiC,KACtE,EAAM,OAAO,SAAS,CAAE,OAAG,IAAK;EACpD,CAAC,GACD,EAAS,IAAI,GAAI,CAAC,GAAI,EAAS,IAAI,CAAE,KAAK,CAAC,GAAI,GAAG,EAAY,EAAM,OAAO,CAAI,CAAC,CAAC;CACrF;CACA,IAAM,IAA0B,CAAC,GAAI,EAAS,IAAI,EAAE,KAAK,CAAC,CAAE;CAI5D,OAHA,EAAO,SAAS,GAAI,MAAU;EAC1B,EAAS,KAAK,GAAI,GAAI,EAAS,IAAI,CAAK,KAAK,CAAC,CAAE;CACpD,CAAC,GACM;AACX"}
@@ -1,2 +1,2 @@
1
- let e=require("react");function t(e,n){if(Object.is(e,n))return!0;if(typeof e!=`object`||typeof n!=`object`||e===null||n===null||Array.isArray(e)!==Array.isArray(n))return!1;if(Array.isArray(e)&&Array.isArray(n))return e.length===n.length&&e.every((e,r)=>t(e,n[r]));let r=Object.keys(e),i=Object.keys(n);return r.length===i.length&&r.every(r=>t(e[r],n[r]))}function n(n){let r=(0,e.useRef)(n);return t(r.current,n)||(r.current=n),r.current}exports.useDeepMemo=n;
1
+ let e=require("react");function t(e,n){if(Object.is(e,n))return!0;if(typeof e!=`object`||typeof n!=`object`||e===null||n===null||Array.isArray(e)!==Array.isArray(n))return!1;if(Array.isArray(e)&&Array.isArray(n))return e.length===n.length&&e.every((e,r)=>t(e,n[r]));let r=Object.keys(e),i=Object.keys(n);return r.length===i.length&&r.every(r=>t(e[r],n[r]))}function n(n){let r=(0,e.useRef)(n);return t(r.current,n)||(r.current=n),r.current}exports.deepEqual=t,exports.useDeepMemo=n;
2
2
  //# sourceMappingURL=use-deep-memo.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-deep-memo.cjs","names":[],"sources":["../../src/hooks/use-deep-memo.ts"],"sourcesContent":["/* eslint-disable react-hooks/refs -- the structural cache is read and written during render by design (see the docstring) */\nimport { useRef } from \"react\";\n\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (typeof a !== \"object\" || typeof b !== \"object\" || a === null || b === null) return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((value, index) => deepEqual(value, b[index]));\n }\n const aKeys = Object.keys(a as object);\n const bKeys = Object.keys(b as object);\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every((key) =>\n deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n );\n}\n\n/**\n * Memoize a value with a structural equality check. Use when an object/array\n * created during render is fed to `useEffect` dependencies and you want to\n * avoid spurious effect runs when only the reference changes.\n *\n * The cache is a ref read and written during render. That is flagged by the React\n * Compiler rules and is nonetheless correct here: the write is idempotent — a\n * discarded render either wrote a structurally equal value or nothing at all — so\n * replaying the render cannot change the result.\n */\nexport function useDeepMemo<T>(value: T): T {\n const ref = useRef<T>(value);\n if (!deepEqual(ref.current, value)) {\n ref.current = value;\n }\n return ref.current;\n}\n"],"mappings":"uBAGA,SAAS,EAAU,EAAY,EAAqB,CAChD,GAAI,OAAO,GAAG,EAAG,CAAC,EAAG,MAAO,GAE5B,GADI,OAAO,GAAM,UAAY,OAAO,GAAM,UAAY,IAAM,MAAQ,IAAM,MACtE,MAAM,QAAQ,CAAC,IAAM,MAAM,QAAQ,CAAC,EAAG,MAAO,GAClD,GAAI,MAAM,QAAQ,CAAC,GAAK,MAAM,QAAQ,CAAC,EAEnC,OADI,EAAE,SAAW,EAAE,QACZ,EAAE,OAAO,EAAO,IAAU,EAAU,EAAO,EAAE,EAAM,CAAC,EAE/D,IAAM,EAAQ,OAAO,KAAK,CAAW,EAC/B,EAAQ,OAAO,KAAK,CAAW,EAErC,OADI,EAAM,SAAW,EAAM,QACpB,EAAM,MAAO,GAChB,EAAW,EAA8B,GAAO,EAA8B,EAAI,CACtF,CACJ,CAYA,SAAgB,EAAe,EAAa,CACxC,IAAM,GAAA,EAAM,EAAA,OAAA,CAAU,CAAK,EAI3B,OAHK,EAAU,EAAI,QAAS,CAAK,IAC7B,EAAI,QAAU,GAEX,EAAI,OACf"}
1
+ {"version":3,"file":"use-deep-memo.cjs","names":[],"sources":["../../src/hooks/use-deep-memo.ts"],"sourcesContent":["/* eslint-disable react-hooks/refs -- the structural cache is read and written during render by design (see the docstring) */\nimport { useRef } from \"react\";\n\n/**\n * Structural equality over plain objects and arrays, the comparison `useDeepMemo`\n * and `useDraftFilters` share. Internal: not re-exported from the barrel.\n *\n * @param a - First value.\n * @param b - Second value.\n * @returns Whether both values have the same shape and leaves.\n */\nexport function deepEqual(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (typeof a !== \"object\" || typeof b !== \"object\" || a === null || b === null) return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((value, index) => deepEqual(value, b[index]));\n }\n const aKeys = Object.keys(a as object);\n const bKeys = Object.keys(b as object);\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every((key) =>\n deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n );\n}\n\n/**\n * Memoize a value with a structural equality check. Use when an object/array\n * created during render is fed to `useEffect` dependencies and you want to\n * avoid spurious effect runs when only the reference changes.\n *\n * The cache is a ref read and written during render. That is flagged by the React\n * Compiler rules and is nonetheless correct here: the write is idempotent — a\n * discarded render either wrote a structurally equal value or nothing at all — so\n * replaying the render cannot change the result.\n */\nexport function useDeepMemo<T>(value: T): T {\n const ref = useRef<T>(value);\n if (!deepEqual(ref.current, value)) {\n ref.current = value;\n }\n return ref.current;\n}\n"],"mappings":"uBAWA,SAAgB,EAAU,EAAY,EAAqB,CACvD,GAAI,OAAO,GAAG,EAAG,CAAC,EAAG,MAAO,GAE5B,GADI,OAAO,GAAM,UAAY,OAAO,GAAM,UAAY,IAAM,MAAQ,IAAM,MACtE,MAAM,QAAQ,CAAC,IAAM,MAAM,QAAQ,CAAC,EAAG,MAAO,GAClD,GAAI,MAAM,QAAQ,CAAC,GAAK,MAAM,QAAQ,CAAC,EAEnC,OADI,EAAE,SAAW,EAAE,QACZ,EAAE,OAAO,EAAO,IAAU,EAAU,EAAO,EAAE,EAAM,CAAC,EAE/D,IAAM,EAAQ,OAAO,KAAK,CAAW,EAC/B,EAAQ,OAAO,KAAK,CAAW,EAErC,OADI,EAAM,SAAW,EAAM,QACpB,EAAM,MAAO,GAChB,EAAW,EAA8B,GAAO,EAA8B,EAAI,CACtF,CACJ,CAYA,SAAgB,EAAe,EAAa,CACxC,IAAM,GAAA,EAAM,EAAA,OAAA,CAAU,CAAK,EAI3B,OAHK,EAAU,EAAI,QAAS,CAAK,IAC7B,EAAI,QAAU,GAEX,EAAI,OACf"}
@@ -12,6 +12,6 @@ function n(n) {
12
12
  return t(r.current, n) || (r.current = n), r.current;
13
13
  }
14
14
  //#endregion
15
- export { n as useDeepMemo };
15
+ export { t as deepEqual, n as useDeepMemo };
16
16
 
17
17
  //# sourceMappingURL=use-deep-memo.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-deep-memo.js","names":[],"sources":["../../src/hooks/use-deep-memo.ts"],"sourcesContent":["/* eslint-disable react-hooks/refs -- the structural cache is read and written during render by design (see the docstring) */\nimport { useRef } from \"react\";\n\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (typeof a !== \"object\" || typeof b !== \"object\" || a === null || b === null) return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((value, index) => deepEqual(value, b[index]));\n }\n const aKeys = Object.keys(a as object);\n const bKeys = Object.keys(b as object);\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every((key) =>\n deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n );\n}\n\n/**\n * Memoize a value with a structural equality check. Use when an object/array\n * created during render is fed to `useEffect` dependencies and you want to\n * avoid spurious effect runs when only the reference changes.\n *\n * The cache is a ref read and written during render. That is flagged by the React\n * Compiler rules and is nonetheless correct here: the write is idempotent — a\n * discarded render either wrote a structurally equal value or nothing at all — so\n * replaying the render cannot change the result.\n */\nexport function useDeepMemo<T>(value: T): T {\n const ref = useRef<T>(value);\n if (!deepEqual(ref.current, value)) {\n ref.current = value;\n }\n return ref.current;\n}\n"],"mappings":";;AAGA,SAAS,EAAU,GAAY,GAAqB;CAChD,IAAI,OAAO,GAAG,GAAG,CAAC,GAAG,OAAO;CAE5B,IADI,OAAO,KAAM,YAAY,OAAO,KAAM,YAAY,MAAM,QAAQ,MAAM,QACtE,MAAM,QAAQ,CAAC,MAAM,MAAM,QAAQ,CAAC,GAAG,OAAO;CAClD,IAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GAEnC,OADI,EAAE,WAAW,EAAE,UACZ,EAAE,OAAO,GAAO,MAAU,EAAU,GAAO,EAAE,EAAM,CAAC;CAE/D,IAAM,IAAQ,OAAO,KAAK,CAAW,GAC/B,IAAQ,OAAO,KAAK,CAAW;CAErC,OADI,EAAM,WAAW,EAAM,UACpB,EAAM,OAAO,MAChB,EAAW,EAA8B,IAAO,EAA8B,EAAI,CACtF;AACJ;AAYA,SAAgB,EAAe,GAAa;CACxC,IAAM,IAAM,EAAU,CAAK;CAI3B,OAHK,EAAU,EAAI,SAAS,CAAK,MAC7B,EAAI,UAAU,IAEX,EAAI;AACf"}
1
+ {"version":3,"file":"use-deep-memo.js","names":[],"sources":["../../src/hooks/use-deep-memo.ts"],"sourcesContent":["/* eslint-disable react-hooks/refs -- the structural cache is read and written during render by design (see the docstring) */\nimport { useRef } from \"react\";\n\n/**\n * Structural equality over plain objects and arrays, the comparison `useDeepMemo`\n * and `useDraftFilters` share. Internal: not re-exported from the barrel.\n *\n * @param a - First value.\n * @param b - Second value.\n * @returns Whether both values have the same shape and leaves.\n */\nexport function deepEqual(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (typeof a !== \"object\" || typeof b !== \"object\" || a === null || b === null) return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((value, index) => deepEqual(value, b[index]));\n }\n const aKeys = Object.keys(a as object);\n const bKeys = Object.keys(b as object);\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every((key) =>\n deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n );\n}\n\n/**\n * Memoize a value with a structural equality check. Use when an object/array\n * created during render is fed to `useEffect` dependencies and you want to\n * avoid spurious effect runs when only the reference changes.\n *\n * The cache is a ref read and written during render. That is flagged by the React\n * Compiler rules and is nonetheless correct here: the write is idempotent — a\n * discarded render either wrote a structurally equal value or nothing at all — so\n * replaying the render cannot change the result.\n */\nexport function useDeepMemo<T>(value: T): T {\n const ref = useRef<T>(value);\n if (!deepEqual(ref.current, value)) {\n ref.current = value;\n }\n return ref.current;\n}\n"],"mappings":";;AAWA,SAAgB,EAAU,GAAY,GAAqB;CACvD,IAAI,OAAO,GAAG,GAAG,CAAC,GAAG,OAAO;CAE5B,IADI,OAAO,KAAM,YAAY,OAAO,KAAM,YAAY,MAAM,QAAQ,MAAM,QACtE,MAAM,QAAQ,CAAC,MAAM,MAAM,QAAQ,CAAC,GAAG,OAAO;CAClD,IAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GAEnC,OADI,EAAE,WAAW,EAAE,UACZ,EAAE,OAAO,GAAO,MAAU,EAAU,GAAO,EAAE,EAAM,CAAC;CAE/D,IAAM,IAAQ,OAAO,KAAK,CAAW,GAC/B,IAAQ,OAAO,KAAK,CAAW;CAErC,OADI,EAAM,WAAW,EAAM,UACpB,EAAM,OAAO,MAChB,EAAW,EAA8B,IAAO,EAA8B,EAAI,CACtF;AACJ;AAYA,SAAgB,EAAe,GAAa;CACxC,IAAM,IAAM,EAAU,CAAK;CAI3B,OAHK,EAAU,EAAI,SAAS,CAAK,MAC7B,EAAI,UAAU,IAEX,EAAI;AACf"}
@@ -0,0 +1,2 @@
1
+ const e=require("./use-deep-memo.cjs"),t=require("./use-latest-ref.cjs");let n=require("react");function r(e,t){return typeof t==`function`?t(e):{...e,...t}}function i(i,a={}){let[o,s]=(0,n.useState)(()=>({draft:i,applied:i})),c=(0,n.useRef)(o.applied),l=(0,n.useRef)(o),u=t.useLatestRef(a),d=(0,n.useCallback)((t,n)=>(u.current.isEqual??e.deepEqual)(t,n),[u]),f=(0,n.useCallback)(e=>{l.current=e,s(e)},[]),p=(0,n.useCallback)(e=>{let t=l.current;f({draft:r(t.draft,e),applied:t.applied})},[f]),m=(0,n.useCallback)(e=>{let t=l.current,n=d(e,t.applied)?t.applied:e;f({draft:e,applied:n}),u.current.onApply?.(n)},[f,d,u]),h=(0,n.useCallback)(e=>{let t=l.current.draft;m(e===void 0?t:r(t,e))},[m]),g=(0,n.useCallback)(()=>m(c.current),[m]);return{draft:o.draft,applied:o.applied,isDirty:!(a.isEqual??e.deepEqual)(o.draft,o.applied),set:p,apply:h,clear:g}}exports.useDraftFilters=i;
2
+ //# sourceMappingURL=use-draft-filters.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-draft-filters.cjs","names":[],"sources":["../../src/hooks/use-draft-filters.ts"],"sourcesContent":["import { useCallback, useRef, useState } from \"react\";\n\nimport { deepEqual } from \"./use-deep-memo\";\nimport { useLatestRef } from \"./use-latest-ref\";\n\n/** An edit to the draft: a partial object merged over it, or an updater. */\nexport type DraftFiltersPatch<T> = Partial<T> | ((draft: T) => T);\n\nexport interface UseDraftFiltersOptions<T> {\n /**\n * Called with the applied value on every `apply()` and `clear()` — the place to\n * reset a page number you keep yourself. `usePaginatedQuery` does not need it:\n * it goes back to page 1 on its own when `applied` changes its `queryKey`.\n */\n onApply?: (applied: T) => void;\n /** Equality behind `isDirty` and the stable `applied`. Default: structural. */\n isEqual?: (a: T, b: T) => boolean;\n}\n\nexport interface UseDraftFiltersResult<T> {\n /** What the filter controls edit. Changes on every keystroke. */\n draft: T;\n /**\n * What the query reads. Changes only on `apply()`/`clear()`, and keeps its\n * reference when the value applied is equal to the current one — safe to put\n * in a `queryKey`.\n */\n applied: T;\n /** Whether the draft differs from what is applied — enable the apply button on it. */\n isDirty: boolean;\n /** Edit the draft, merging a partial object or running an updater. Never applies. */\n set: (patch: DraftFiltersPatch<T>) => void;\n /**\n * Publish the draft. With a patch, merge it into the draft **and** publish the\n * result in the same call — \"filter by this code\" from a table row.\n */\n apply: (patch?: DraftFiltersPatch<T>) => void;\n /** Reset the draft and the applied value to the initial one, together. */\n clear: () => void;\n}\n\n/** Both halves, kept in one state so they can never be committed apart. */\ninterface DraftFiltersState<T> {\n draft: T;\n applied: T;\n}\n\n/**\n * Resolve a patch against a draft.\n *\n * @param draft - The current draft.\n * @param patch - A partial object to merge, or an updater.\n * @returns The next draft.\n */\nfunction patchDraft<T>(draft: T, patch: DraftFiltersPatch<T>): T {\n return typeof patch === \"function\" ? patch(draft) : { ...draft, ...patch };\n}\n\n/**\n * Draft vs applied state for a server-side filter: the controls edit a draft\n * freely, and the query only sees it when the user applies (a click, or Enter).\n *\n * A server-side filter cannot react to every keystroke — each character typed in\n * a search box would be a request. Every admin screen then writes the same pair of\n * `useState`s, and gets the same three details wrong:\n *\n * - **Clearing moves both halves.** Resetting only the draft leaves a filtered\n * list under an empty bar; resetting only the applied value leaves the opposite.\n * - **Applying a value that is not in state yet.** \"Filter by this code\" has to\n * write the draft and publish it in one gesture; calling `setDraft(x)` and then\n * publishing `draft` reads the previous value. `apply(patch)` computes the next\n * draft from a ref that every call keeps current, so it never depends on the\n * order React flushes state in.\n * - **Resetting the page.** Filtering while on page 7 of a result that now has two\n * pages shows an empty table. `usePaginatedQuery` handles it by itself when\n * `applied` is in its `queryKey`; a page kept elsewhere resets in `onApply`.\n *\n * There is no effect in here: applying happens inside the event that asked for it,\n * so one click is one request. `applied` keeps its reference while nothing new is\n * applied, so it can sit in a `queryKey` without a phantom refetch.\n *\n * Like `useState`, the initial value is read on the first render only; `clear()`\n * returns to it.\n *\n * @param initial - The empty filter form: what the screen shows and queries first.\n * @param options - `onApply` and `isEqual`.\n * @returns The draft, the applied value and the gestures between them.\n *\n * @example\n * const filters = useDraftFilters({ code: \"\", level: \"all\" });\n * const logs = usePaginatedQuery({\n * queryKey: [\"logs\", filters.applied],\n * queryFn: (params) => api.listLogs({ ...params, ...toQuery(filters.applied) }),\n * });\n * // <Input value={filters.draft.code} onChange={(e) => filters.set({ code: e.target.value })} />\n * // <Button disabled={!filters.isDirty} onClick={() => filters.apply()}>Filtrar</Button>\n */\nexport function useDraftFilters<T extends object>(\n initial: T,\n options: UseDraftFiltersOptions<T> = {},\n): UseDraftFiltersResult<T> {\n const [state, setState] = useState<DraftFiltersState<T>>(() => ({\n draft: initial,\n applied: initial,\n }));\n const initialRef = useRef<T>(state.applied);\n const stateRef = useRef<DraftFiltersState<T>>(state);\n const optionsRef = useLatestRef(options);\n\n const equal = useCallback(\n (a: T, b: T): boolean => (optionsRef.current.isEqual ?? deepEqual)(a, b),\n [optionsRef],\n );\n\n const commit = useCallback((next: DraftFiltersState<T>): void => {\n stateRef.current = next;\n setState(next);\n }, []);\n\n const set = useCallback(\n (patch: DraftFiltersPatch<T>): void => {\n const current = stateRef.current;\n commit({ draft: patchDraft(current.draft, patch), applied: current.applied });\n },\n [commit],\n );\n\n const publish = useCallback(\n (next: T): void => {\n const current = stateRef.current;\n const applied = equal(next, current.applied) ? current.applied : next;\n commit({ draft: next, applied });\n optionsRef.current.onApply?.(applied);\n },\n [commit, equal, optionsRef],\n );\n\n const apply = useCallback(\n (patch?: DraftFiltersPatch<T>): void => {\n const draft = stateRef.current.draft;\n publish(patch === undefined ? draft : patchDraft(draft, patch));\n },\n [publish],\n );\n\n const clear = useCallback((): void => publish(initialRef.current), [publish]);\n\n return {\n draft: state.draft,\n applied: state.applied,\n isDirty: !(options.isEqual ?? deepEqual)(state.draft, state.applied),\n set,\n apply,\n clear,\n };\n}\n"],"mappings":"gGAsDA,SAAS,EAAc,EAAU,EAAgC,CAC7D,OAAO,OAAO,GAAU,WAAa,EAAM,CAAK,EAAI,CAAE,GAAG,EAAO,GAAG,CAAM,CAC7E,CAyCA,SAAgB,EACZ,EACA,EAAqC,CAAC,EACd,CACxB,GAAM,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,MAAsC,CAC5D,MAAO,EACP,QAAS,CACb,EAAE,EACI,GAAA,EAAa,EAAA,OAAA,CAAU,EAAM,OAAO,EACpC,GAAA,EAAW,EAAA,OAAA,CAA6B,CAAK,EAC7C,EAAa,EAAA,aAAa,CAAO,EAEjC,GAAA,EAAQ,EAAA,YAAA,EACT,EAAM,KAAmB,EAAW,QAAQ,SAAW,EAAA,UAAA,CAAW,EAAG,CAAC,EACvE,CAAC,CAAU,CACf,EAEM,GAAA,EAAS,EAAA,YAAA,CAAa,GAAqC,CAC7D,EAAS,QAAU,EACnB,EAAS,CAAI,CACjB,EAAG,CAAC,CAAC,EAEC,GAAA,EAAM,EAAA,YAAA,CACP,GAAsC,CACnC,IAAM,EAAU,EAAS,QACzB,EAAO,CAAE,MAAO,EAAW,EAAQ,MAAO,CAAK,EAAG,QAAS,EAAQ,OAAQ,CAAC,CAChF,EACA,CAAC,CAAM,CACX,EAEM,GAAA,EAAU,EAAA,YAAA,CACX,GAAkB,CACf,IAAM,EAAU,EAAS,QACnB,EAAU,EAAM,EAAM,EAAQ,OAAO,EAAI,EAAQ,QAAU,EACjE,EAAO,CAAE,MAAO,EAAM,SAAQ,CAAC,EAC/B,EAAW,QAAQ,UAAU,CAAO,CACxC,EACA,CAAC,EAAQ,EAAO,CAAU,CAC9B,EAEM,GAAA,EAAQ,EAAA,YAAA,CACT,GAAuC,CACpC,IAAM,EAAQ,EAAS,QAAQ,MAC/B,EAAQ,IAAU,IAAA,GAAY,EAAQ,EAAW,EAAO,CAAK,CAAC,CAClE,EACA,CAAC,CAAO,CACZ,EAEM,GAAA,EAAQ,EAAA,YAAA,KAAwB,EAAQ,EAAW,OAAO,EAAG,CAAC,CAAO,CAAC,EAE5E,MAAO,CACH,MAAO,EAAM,MACb,QAAS,EAAM,QACf,QAAS,EAAE,EAAQ,SAAW,EAAA,UAAA,CAAW,EAAM,MAAO,EAAM,OAAO,EACnE,MACA,QACA,OACJ,CACJ"}
@@ -0,0 +1,49 @@
1
+ import { deepEqual as e } from "./use-deep-memo.js";
2
+ import { useLatestRef as t } from "./use-latest-ref.js";
3
+ import { useCallback as n, useRef as r, useState as i } from "react";
4
+ //#region src/hooks/use-draft-filters.ts
5
+ function a(e, t) {
6
+ return typeof t == "function" ? t(e) : {
7
+ ...e,
8
+ ...t
9
+ };
10
+ }
11
+ function o(o, s = {}) {
12
+ let [c, l] = i(() => ({
13
+ draft: o,
14
+ applied: o
15
+ })), u = r(c.applied), d = r(c), f = t(s), p = n((t, n) => (f.current.isEqual ?? e)(t, n), [f]), m = n((e) => {
16
+ d.current = e, l(e);
17
+ }, []), h = n((e) => {
18
+ let t = d.current;
19
+ m({
20
+ draft: a(t.draft, e),
21
+ applied: t.applied
22
+ });
23
+ }, [m]), g = n((e) => {
24
+ let t = d.current, n = p(e, t.applied) ? t.applied : e;
25
+ m({
26
+ draft: e,
27
+ applied: n
28
+ }), f.current.onApply?.(n);
29
+ }, [
30
+ m,
31
+ p,
32
+ f
33
+ ]), _ = n((e) => {
34
+ let t = d.current.draft;
35
+ g(e === void 0 ? t : a(t, e));
36
+ }, [g]), v = n(() => g(u.current), [g]);
37
+ return {
38
+ draft: c.draft,
39
+ applied: c.applied,
40
+ isDirty: !(s.isEqual ?? e)(c.draft, c.applied),
41
+ set: h,
42
+ apply: _,
43
+ clear: v
44
+ };
45
+ }
46
+ //#endregion
47
+ export { o as useDraftFilters };
48
+
49
+ //# sourceMappingURL=use-draft-filters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-draft-filters.js","names":[],"sources":["../../src/hooks/use-draft-filters.ts"],"sourcesContent":["import { useCallback, useRef, useState } from \"react\";\n\nimport { deepEqual } from \"./use-deep-memo\";\nimport { useLatestRef } from \"./use-latest-ref\";\n\n/** An edit to the draft: a partial object merged over it, or an updater. */\nexport type DraftFiltersPatch<T> = Partial<T> | ((draft: T) => T);\n\nexport interface UseDraftFiltersOptions<T> {\n /**\n * Called with the applied value on every `apply()` and `clear()` — the place to\n * reset a page number you keep yourself. `usePaginatedQuery` does not need it:\n * it goes back to page 1 on its own when `applied` changes its `queryKey`.\n */\n onApply?: (applied: T) => void;\n /** Equality behind `isDirty` and the stable `applied`. Default: structural. */\n isEqual?: (a: T, b: T) => boolean;\n}\n\nexport interface UseDraftFiltersResult<T> {\n /** What the filter controls edit. Changes on every keystroke. */\n draft: T;\n /**\n * What the query reads. Changes only on `apply()`/`clear()`, and keeps its\n * reference when the value applied is equal to the current one — safe to put\n * in a `queryKey`.\n */\n applied: T;\n /** Whether the draft differs from what is applied — enable the apply button on it. */\n isDirty: boolean;\n /** Edit the draft, merging a partial object or running an updater. Never applies. */\n set: (patch: DraftFiltersPatch<T>) => void;\n /**\n * Publish the draft. With a patch, merge it into the draft **and** publish the\n * result in the same call — \"filter by this code\" from a table row.\n */\n apply: (patch?: DraftFiltersPatch<T>) => void;\n /** Reset the draft and the applied value to the initial one, together. */\n clear: () => void;\n}\n\n/** Both halves, kept in one state so they can never be committed apart. */\ninterface DraftFiltersState<T> {\n draft: T;\n applied: T;\n}\n\n/**\n * Resolve a patch against a draft.\n *\n * @param draft - The current draft.\n * @param patch - A partial object to merge, or an updater.\n * @returns The next draft.\n */\nfunction patchDraft<T>(draft: T, patch: DraftFiltersPatch<T>): T {\n return typeof patch === \"function\" ? patch(draft) : { ...draft, ...patch };\n}\n\n/**\n * Draft vs applied state for a server-side filter: the controls edit a draft\n * freely, and the query only sees it when the user applies (a click, or Enter).\n *\n * A server-side filter cannot react to every keystroke — each character typed in\n * a search box would be a request. Every admin screen then writes the same pair of\n * `useState`s, and gets the same three details wrong:\n *\n * - **Clearing moves both halves.** Resetting only the draft leaves a filtered\n * list under an empty bar; resetting only the applied value leaves the opposite.\n * - **Applying a value that is not in state yet.** \"Filter by this code\" has to\n * write the draft and publish it in one gesture; calling `setDraft(x)` and then\n * publishing `draft` reads the previous value. `apply(patch)` computes the next\n * draft from a ref that every call keeps current, so it never depends on the\n * order React flushes state in.\n * - **Resetting the page.** Filtering while on page 7 of a result that now has two\n * pages shows an empty table. `usePaginatedQuery` handles it by itself when\n * `applied` is in its `queryKey`; a page kept elsewhere resets in `onApply`.\n *\n * There is no effect in here: applying happens inside the event that asked for it,\n * so one click is one request. `applied` keeps its reference while nothing new is\n * applied, so it can sit in a `queryKey` without a phantom refetch.\n *\n * Like `useState`, the initial value is read on the first render only; `clear()`\n * returns to it.\n *\n * @param initial - The empty filter form: what the screen shows and queries first.\n * @param options - `onApply` and `isEqual`.\n * @returns The draft, the applied value and the gestures between them.\n *\n * @example\n * const filters = useDraftFilters({ code: \"\", level: \"all\" });\n * const logs = usePaginatedQuery({\n * queryKey: [\"logs\", filters.applied],\n * queryFn: (params) => api.listLogs({ ...params, ...toQuery(filters.applied) }),\n * });\n * // <Input value={filters.draft.code} onChange={(e) => filters.set({ code: e.target.value })} />\n * // <Button disabled={!filters.isDirty} onClick={() => filters.apply()}>Filtrar</Button>\n */\nexport function useDraftFilters<T extends object>(\n initial: T,\n options: UseDraftFiltersOptions<T> = {},\n): UseDraftFiltersResult<T> {\n const [state, setState] = useState<DraftFiltersState<T>>(() => ({\n draft: initial,\n applied: initial,\n }));\n const initialRef = useRef<T>(state.applied);\n const stateRef = useRef<DraftFiltersState<T>>(state);\n const optionsRef = useLatestRef(options);\n\n const equal = useCallback(\n (a: T, b: T): boolean => (optionsRef.current.isEqual ?? deepEqual)(a, b),\n [optionsRef],\n );\n\n const commit = useCallback((next: DraftFiltersState<T>): void => {\n stateRef.current = next;\n setState(next);\n }, []);\n\n const set = useCallback(\n (patch: DraftFiltersPatch<T>): void => {\n const current = stateRef.current;\n commit({ draft: patchDraft(current.draft, patch), applied: current.applied });\n },\n [commit],\n );\n\n const publish = useCallback(\n (next: T): void => {\n const current = stateRef.current;\n const applied = equal(next, current.applied) ? current.applied : next;\n commit({ draft: next, applied });\n optionsRef.current.onApply?.(applied);\n },\n [commit, equal, optionsRef],\n );\n\n const apply = useCallback(\n (patch?: DraftFiltersPatch<T>): void => {\n const draft = stateRef.current.draft;\n publish(patch === undefined ? draft : patchDraft(draft, patch));\n },\n [publish],\n );\n\n const clear = useCallback((): void => publish(initialRef.current), [publish]);\n\n return {\n draft: state.draft,\n applied: state.applied,\n isDirty: !(options.isEqual ?? deepEqual)(state.draft, state.applied),\n set,\n apply,\n clear,\n };\n}\n"],"mappings":";;;;AAsDA,SAAS,EAAc,GAAU,GAAgC;CAC7D,OAAO,OAAO,KAAU,aAAa,EAAM,CAAK,IAAI;EAAE,GAAG;EAAO,GAAG;CAAM;AAC7E;AAyCA,SAAgB,EACZ,GACA,IAAqC,CAAC,GACd;CACxB,IAAM,CAAC,GAAO,KAAY,SAAsC;EAC5D,OAAO;EACP,SAAS;CACb,EAAE,GACI,IAAa,EAAU,EAAM,OAAO,GACpC,IAAW,EAA6B,CAAK,GAC7C,IAAa,EAAa,CAAO,GAEjC,IAAQ,GACT,GAAM,OAAmB,EAAW,QAAQ,WAAW,EAAA,CAAW,GAAG,CAAC,GACvE,CAAC,CAAU,CACf,GAEM,IAAS,GAAa,MAAqC;EAE7D,AADA,EAAS,UAAU,GACnB,EAAS,CAAI;CACjB,GAAG,CAAC,CAAC,GAEC,IAAM,GACP,MAAsC;EACnC,IAAM,IAAU,EAAS;EACzB,EAAO;GAAE,OAAO,EAAW,EAAQ,OAAO,CAAK;GAAG,SAAS,EAAQ;EAAQ,CAAC;CAChF,GACA,CAAC,CAAM,CACX,GAEM,IAAU,GACX,MAAkB;EACf,IAAM,IAAU,EAAS,SACnB,IAAU,EAAM,GAAM,EAAQ,OAAO,IAAI,EAAQ,UAAU;EAEjE,AADA,EAAO;GAAE,OAAO;GAAM;EAAQ,CAAC,GAC/B,EAAW,QAAQ,UAAU,CAAO;CACxC,GACA;EAAC;EAAQ;EAAO;CAAU,CAC9B,GAEM,IAAQ,GACT,MAAuC;EACpC,IAAM,IAAQ,EAAS,QAAQ;EAC/B,EAAQ,MAAU,KAAA,IAAY,IAAQ,EAAW,GAAO,CAAK,CAAC;CAClE,GACA,CAAC,CAAO,CACZ,GAEM,IAAQ,QAAwB,EAAQ,EAAW,OAAO,GAAG,CAAC,CAAO,CAAC;CAE5E,OAAO;EACH,OAAO,EAAM;EACb,SAAS,EAAM;EACf,SAAS,EAAE,EAAQ,WAAW,EAAA,CAAW,EAAM,OAAO,EAAM,OAAO;EACnE;EACA;EACA;CACJ;AACJ"}
@@ -1,2 +1,2 @@
1
- let e=require("react");var t=[`a[href]`,`button:not([disabled])`,`textarea:not([disabled])`,`input:not([disabled])`,`select:not([disabled])`,`[tabindex]:not([tabindex='-1'])`].join(`,`);function n(n,r){(0,e.useEffect)(()=>{if(!r)return;let e=n.current;if(!e)return;let i=document.activeElement;function a(){return Array.from(e?.querySelectorAll(t)??[]).filter(e=>{if(e.hasAttribute(`aria-hidden`))return!1;let t=typeof window<`u`?window.getComputedStyle(e):null;return!t||t.display!==`none`&&t.visibility!==`hidden`})}function o(t){if(t.key!==`Tab`||t.defaultPrevented)return;let n=a();if(n.length===0){t.preventDefault();return}let r=n[0],i=n[n.length-1],o=document.activeElement;t.shiftKey?(o===r||!o||!e?.contains(o))&&(t.preventDefault(),i.focus()):o===i&&(t.preventDefault(),r.focus())}return a()[0]?.focus(),document.addEventListener(`keydown`,o),()=>{document.removeEventListener(`keydown`,o),i?.focus?.()}},[n,r])}exports.FOCUSABLE_SELECTOR=t,exports.useFocusTrap=n;
1
+ const e=require("./tab-sequence.cjs");let t=require("react");var n=[];function r(r,i){let[a,o]=(0,t.useState)(!1),[s,c]=(0,t.useState)(null);i!==a&&(o(i),i&&typeof document<`u`&&c(document.activeElement)),(0,t.useEffect)(()=>{if(!i)return;let t=r.current;if(!t)return;let a={container:t};n.push(a),t.contains(document.activeElement)||(e.tabSequence(t)[0]??t).focus();let o=r=>{if(r.key!==`Tab`||n.at(-1)!==a)return;let i=e.tabSequence(t),o=i[0],s=i.at(-1),c=document.activeElement,l=c!==null&&e.tabRegion(t).some(e=>e.contains(c));if(r.defaultPrevented){l||((r.shiftKey?s:o)??t).focus();return}if(!o||!s){r.preventDefault(),t.focus();return}let u=r.shiftKey?c===o||c===t:c===s;(!l||u)&&(r.preventDefault(),(r.shiftKey?s:o).focus())};return document.addEventListener(`keydown`,o),()=>{document.removeEventListener(`keydown`,o),n.splice(n.indexOf(a),1),s?.isConnected&&s.focus()}},[r,i,s])}exports.useFocusTrap=r;
2
2
  //# sourceMappingURL=use-focus-trap.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-focus-trap.cjs","names":[],"sources":["../../src/hooks/use-focus-trap.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport type { RefObject } from \"react\";\n\n/** Elements that can take keyboard focus — shared with the portal tab order. */\nexport const FOCUSABLE_SELECTOR = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\n/**\n * Trap keyboard focus inside `containerRef` while `active` is true. Cycles\n * Tab and Shift+Tab between the first and last focusable descendants. Pair\n * with Modal/Drawer for fully-accessible overlays.\n */\nexport function useFocusTrap(containerRef: RefObject<HTMLElement | null>, active: boolean): void {\n useEffect(() => {\n if (!active) return;\n const container = containerRef.current;\n if (!container) return;\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n\n function getFocusable(): HTMLElement[] {\n return Array.from(\n container?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR) ?? [],\n ).filter((el) => {\n if (el.hasAttribute(\"aria-hidden\")) return false;\n const style = typeof window !== \"undefined\" ? window.getComputedStyle(el) : null;\n if (style && (style.display === \"none\" || style.visibility === \"hidden\")) {\n return false;\n }\n return true;\n });\n }\n\n /**\n * Keep `Tab` and `Shift+Tab` inside the container.\n *\n * A `Tab` already consumed is left alone: a portalled panel inside the\n * container (a `Popover`, say) routes its own `Tab` back next to its\n * trigger, and wrapping here as well would move focus a second time.\n */\n function handleKeydown(event: KeyboardEvent): void {\n if (event.key !== \"Tab\" || event.defaultPrevented) return;\n const elements = getFocusable();\n if (elements.length === 0) {\n event.preventDefault();\n return;\n }\n const first = elements[0]!;\n const last = elements[elements.length - 1]!;\n const current = document.activeElement as HTMLElement | null;\n if (event.shiftKey) {\n if (current === first || !current || !container?.contains(current)) {\n event.preventDefault();\n last.focus();\n }\n } else {\n if (current === last) {\n event.preventDefault();\n first.focus();\n }\n }\n }\n\n const focusable = getFocusable();\n focusable[0]?.focus();\n document.addEventListener(\"keydown\", handleKeydown);\n return () => {\n document.removeEventListener(\"keydown\", handleKeydown);\n previouslyFocused?.focus?.();\n };\n }, [containerRef, active]);\n}\n"],"mappings":"uBAIA,IAAa,EAAqB,CAC9B,UACA,yBACA,2BACA,wBACA,yBACA,iCACJ,CAAC,CAAC,KAAK,GAAG,EAOV,SAAgB,EAAa,EAA6C,EAAuB,EAC7F,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,EAAQ,OACb,IAAM,EAAY,EAAa,QAC/B,GAAI,CAAC,EAAW,OAEhB,IAAM,EAAoB,SAAS,cAEnC,SAAS,GAA8B,CACnC,OAAO,MAAM,KACT,GAAW,iBAA8B,CAAkB,GAAK,CAAC,CACrE,CAAC,CAAC,OAAQ,GAAO,CACb,GAAI,EAAG,aAAa,aAAa,EAAG,MAAO,GAC3C,IAAM,EAAQ,OAAO,OAAW,IAAc,OAAO,iBAAiB,CAAE,EAAI,KAI5E,MAHI,IAAU,EAAM,UAAY,QAAU,EAAM,aAAe,QAInE,CAAC,CACL,CASA,SAAS,EAAc,EAA4B,CAC/C,GAAI,EAAM,MAAQ,OAAS,EAAM,iBAAkB,OACnD,IAAM,EAAW,EAAa,EAC9B,GAAI,EAAS,SAAW,EAAG,CACvB,EAAM,eAAe,EACrB,MACJ,CACA,IAAM,EAAQ,EAAS,GACjB,EAAO,EAAS,EAAS,OAAS,GAClC,EAAU,SAAS,cACrB,EAAM,UACF,IAAY,GAAS,CAAC,GAAW,CAAC,GAAW,SAAS,CAAO,KAC7D,EAAM,eAAe,EACrB,EAAK,MAAM,GAGX,IAAY,IACZ,EAAM,eAAe,EACrB,EAAM,MAAM,EAGxB,CAKA,OAFA,EAAA,CAAA,CAAU,EAAE,EAAE,MAAM,EACpB,SAAS,iBAAiB,UAAW,CAAa,MACrC,CACT,SAAS,oBAAoB,UAAW,CAAa,EACrD,GAAmB,QAAQ,CAC/B,CACJ,EAAG,CAAC,EAAc,CAAM,CAAC,CAC7B"}
1
+ {"version":3,"file":"use-focus-trap.cjs","names":[],"sources":["../../src/hooks/use-focus-trap.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { RefObject } from \"react\";\nimport { tabRegion, tabSequence } from \"./tab-sequence\";\n\nexport { FOCUSABLE_SELECTOR } from \"./tab-sequence\";\n\n/** One armed trap; the identity is what the stack compares. */\ninterface TrapEntry {\n container: HTMLElement;\n}\n\nconst stack: TrapEntry[] = [];\n\n/**\n * Trap keyboard focus inside `containerRef` while `active` is true.\n *\n * - **On activation** focus moves to the first tab stop, or to the container\n * itself when it has none (give it `tabIndex={-1}`). Content that already put\n * focus inside — an `autoFocus` input — is left where it is.\n * - **`Tab` / `Shift+Tab`** wrap between the first and the last stop, and pull\n * focus back in when it sits outside.\n * - **On release** focus returns to the element that held it when the trap\n * armed. That element is read during the render that arms the trap, before\n * React commits: an `autoFocus` child takes focus in the commit, ahead of any\n * effect, and reading it there would \"restore\" to the child that is about to\n * unmount — which dropped focus on `body` in `Command`.\n *\n * **Portalled panels stay reachable.** A `Popover` or `HoverCard` opened from\n * inside the container renders at the end of `body`, outside it. Those panels\n * register themselves (see `usePortalTabOrder`), and the trap treats them as\n * part of the container, spliced right after their trigger. A `Tab` another\n * layer already consumed is not moved again, unless it moved focus out of the\n * container and its panels — the portal bridge leaving the last panel stop for\n * whatever follows the trap in the page.\n *\n * **Traps stack.** Only the most recent active trap handles `Tab`, so a modal\n * opened over another owns the keyboard until it closes, and then hands focus\n * back to the element it was opened from, inside the one below.\n *\n * The listener is on `document` in the bubble phase, after React's own\n * handlers: a menu that closes on `Tab` and returns focus to its trigger has\n * already done so when the trap looks at where focus is.\n *\n * @param containerRef - The element focus is confined to.\n * @param active - Whether the trap is armed.\n * @returns Nothing.\n */\nexport function useFocusTrap(containerRef: RefObject<HTMLElement | null>, active: boolean): void {\n const [armed, setArmed] = useState(false);\n const [opener, setOpener] = useState<HTMLElement | null>(null);\n if (active !== armed) {\n setArmed(active);\n if (active && typeof document !== \"undefined\") {\n setOpener(document.activeElement as HTMLElement | null);\n }\n }\n\n useEffect(() => {\n if (!active) return;\n const container = containerRef.current;\n if (!container) return;\n\n const entry: TrapEntry = { container };\n stack.push(entry);\n\n if (!container.contains(document.activeElement)) {\n (tabSequence(container)[0] ?? container).focus();\n }\n\n const handleKeydown = (event: KeyboardEvent): void => {\n if (event.key !== \"Tab\" || stack.at(-1) !== entry) return;\n const sequence = tabSequence(container);\n const first = sequence[0];\n const last = sequence.at(-1);\n const current = document.activeElement as HTMLElement | null;\n const inside =\n current !== null && tabRegion(container).some((root) => root.contains(current));\n\n if (event.defaultPrevented) {\n if (!inside) ((event.shiftKey ? last : first) ?? container).focus();\n return;\n }\n if (!first || !last) {\n event.preventDefault();\n container.focus();\n return;\n }\n const atEnd = event.shiftKey\n ? current === first || current === container\n : current === last;\n if (!inside || atEnd) {\n event.preventDefault();\n (event.shiftKey ? last : first).focus();\n }\n };\n\n document.addEventListener(\"keydown\", handleKeydown);\n return () => {\n document.removeEventListener(\"keydown\", handleKeydown);\n stack.splice(stack.indexOf(entry), 1);\n if (opener?.isConnected) opener.focus();\n };\n }, [containerRef, active, opener]);\n}\n"],"mappings":"6DAWA,IAAM,EAAqB,CAAC,EAoC5B,SAAgB,EAAa,EAA6C,EAAuB,CAC7F,GAAM,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAS,EAAK,EAClC,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAA6B,IAAI,EACzD,IAAW,IACX,EAAS,CAAM,EACX,GAAU,OAAO,SAAa,KAC9B,EAAU,SAAS,aAAmC,IAI9D,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,EAAQ,OACb,IAAM,EAAY,EAAa,QAC/B,GAAI,CAAC,EAAW,OAEhB,IAAM,EAAmB,CAAE,WAAU,EACrC,EAAM,KAAK,CAAK,EAEX,EAAU,SAAS,SAAS,aAAa,IACzC,EAAA,YAAY,CAAS,CAAC,CAAC,IAAM,EAAA,CAAW,MAAM,EAGnD,IAAM,EAAiB,GAA+B,CAClD,GAAI,EAAM,MAAQ,OAAS,EAAM,GAAG,EAAE,IAAM,EAAO,OACnD,IAAM,EAAW,EAAA,YAAY,CAAS,EAChC,EAAQ,EAAS,GACjB,EAAO,EAAS,GAAG,EAAE,EACrB,EAAU,SAAS,cACnB,EACF,IAAY,MAAQ,EAAA,UAAU,CAAS,CAAC,CAAC,KAAM,GAAS,EAAK,SAAS,CAAO,CAAC,EAElF,GAAI,EAAM,iBAAkB,CACnB,KAAU,EAAM,SAAW,EAAO,IAAU,EAAA,CAAW,MAAM,EAClE,MACJ,CACA,GAAI,CAAC,GAAS,CAAC,EAAM,CACjB,EAAM,eAAe,EACrB,EAAU,MAAM,EAChB,MACJ,CACA,IAAM,EAAQ,EAAM,SACd,IAAY,GAAS,IAAY,EACjC,IAAY,GACd,CAAC,GAAU,KACX,EAAM,eAAe,GACpB,EAAM,SAAW,EAAO,EAAA,CAAO,MAAM,EAE9C,EAGA,OADA,SAAS,iBAAiB,UAAW,CAAa,MACrC,CACT,SAAS,oBAAoB,UAAW,CAAa,EACrD,EAAM,OAAO,EAAM,QAAQ,CAAK,EAAG,CAAC,EAChC,GAAQ,aAAa,EAAO,MAAM,CAC1C,CACJ,EAAG,CAAC,EAAc,EAAQ,CAAM,CAAC,CACrC"}
@@ -1,42 +1,39 @@
1
- import { useEffect as e } from "react";
1
+ import { tabRegion as e, tabSequence as t } from "./tab-sequence.js";
2
+ import { useEffect as n, useState as r } from "react";
2
3
  //#region src/hooks/use-focus-trap.ts
3
- var t = [
4
- "a[href]",
5
- "button:not([disabled])",
6
- "textarea:not([disabled])",
7
- "input:not([disabled])",
8
- "select:not([disabled])",
9
- "[tabindex]:not([tabindex='-1'])"
10
- ].join(",");
11
- function n(n, r) {
12
- e(() => {
13
- if (!r) return;
14
- let e = n.current;
15
- if (!e) return;
16
- let i = document.activeElement;
17
- function a() {
18
- return Array.from(e?.querySelectorAll(t) ?? []).filter((e) => {
19
- if (e.hasAttribute("aria-hidden")) return !1;
20
- let t = typeof window < "u" ? window.getComputedStyle(e) : null;
21
- return !t || t.display !== "none" && t.visibility !== "hidden";
22
- });
23
- }
24
- function o(t) {
25
- if (t.key !== "Tab" || t.defaultPrevented) return;
26
- let n = a();
27
- if (n.length === 0) {
28
- t.preventDefault();
4
+ var i = [];
5
+ function a(a, o) {
6
+ let [s, c] = r(!1), [l, u] = r(null);
7
+ o !== s && (c(o), o && typeof document < "u" && u(document.activeElement)), n(() => {
8
+ if (!o) return;
9
+ let n = a.current;
10
+ if (!n) return;
11
+ let r = { container: n };
12
+ i.push(r), n.contains(document.activeElement) || (t(n)[0] ?? n).focus();
13
+ let s = (a) => {
14
+ if (a.key !== "Tab" || i.at(-1) !== r) return;
15
+ let o = t(n), s = o[0], c = o.at(-1), l = document.activeElement, u = l !== null && e(n).some((e) => e.contains(l));
16
+ if (a.defaultPrevented) {
17
+ u || ((a.shiftKey ? c : s) ?? n).focus();
29
18
  return;
30
19
  }
31
- let r = n[0], i = n[n.length - 1], o = document.activeElement;
32
- t.shiftKey ? (o === r || !o || !e?.contains(o)) && (t.preventDefault(), i.focus()) : o === i && (t.preventDefault(), r.focus());
33
- }
34
- return a()[0]?.focus(), document.addEventListener("keydown", o), () => {
35
- document.removeEventListener("keydown", o), i?.focus?.();
20
+ if (!s || !c) {
21
+ a.preventDefault(), n.focus();
22
+ return;
23
+ }
24
+ let d = a.shiftKey ? l === s || l === n : l === c;
25
+ (!u || d) && (a.preventDefault(), (a.shiftKey ? c : s).focus());
26
+ };
27
+ return document.addEventListener("keydown", s), () => {
28
+ document.removeEventListener("keydown", s), i.splice(i.indexOf(r), 1), l?.isConnected && l.focus();
36
29
  };
37
- }, [n, r]);
30
+ }, [
31
+ a,
32
+ o,
33
+ l
34
+ ]);
38
35
  }
39
36
  //#endregion
40
- export { t as FOCUSABLE_SELECTOR, n as useFocusTrap };
37
+ export { a as useFocusTrap };
41
38
 
42
39
  //# sourceMappingURL=use-focus-trap.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-focus-trap.js","names":[],"sources":["../../src/hooks/use-focus-trap.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport type { RefObject } from \"react\";\n\n/** Elements that can take keyboard focus — shared with the portal tab order. */\nexport const FOCUSABLE_SELECTOR = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\n/**\n * Trap keyboard focus inside `containerRef` while `active` is true. Cycles\n * Tab and Shift+Tab between the first and last focusable descendants. Pair\n * with Modal/Drawer for fully-accessible overlays.\n */\nexport function useFocusTrap(containerRef: RefObject<HTMLElement | null>, active: boolean): void {\n useEffect(() => {\n if (!active) return;\n const container = containerRef.current;\n if (!container) return;\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n\n function getFocusable(): HTMLElement[] {\n return Array.from(\n container?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR) ?? [],\n ).filter((el) => {\n if (el.hasAttribute(\"aria-hidden\")) return false;\n const style = typeof window !== \"undefined\" ? window.getComputedStyle(el) : null;\n if (style && (style.display === \"none\" || style.visibility === \"hidden\")) {\n return false;\n }\n return true;\n });\n }\n\n /**\n * Keep `Tab` and `Shift+Tab` inside the container.\n *\n * A `Tab` already consumed is left alone: a portalled panel inside the\n * container (a `Popover`, say) routes its own `Tab` back next to its\n * trigger, and wrapping here as well would move focus a second time.\n */\n function handleKeydown(event: KeyboardEvent): void {\n if (event.key !== \"Tab\" || event.defaultPrevented) return;\n const elements = getFocusable();\n if (elements.length === 0) {\n event.preventDefault();\n return;\n }\n const first = elements[0]!;\n const last = elements[elements.length - 1]!;\n const current = document.activeElement as HTMLElement | null;\n if (event.shiftKey) {\n if (current === first || !current || !container?.contains(current)) {\n event.preventDefault();\n last.focus();\n }\n } else {\n if (current === last) {\n event.preventDefault();\n first.focus();\n }\n }\n }\n\n const focusable = getFocusable();\n focusable[0]?.focus();\n document.addEventListener(\"keydown\", handleKeydown);\n return () => {\n document.removeEventListener(\"keydown\", handleKeydown);\n previouslyFocused?.focus?.();\n };\n }, [containerRef, active]);\n}\n"],"mappings":";;AAIA,IAAa,IAAqB;CAC9B;CACA;CACA;CACA;CACA;CACA;AACJ,CAAC,CAAC,KAAK,GAAG;AAOV,SAAgB,EAAa,GAA6C,GAAuB;CAC7F,QAAgB;EACZ,IAAI,CAAC,GAAQ;EACb,IAAM,IAAY,EAAa;EAC/B,IAAI,CAAC,GAAW;EAEhB,IAAM,IAAoB,SAAS;EAEnC,SAAS,IAA8B;GACnC,OAAO,MAAM,KACT,GAAW,iBAA8B,CAAkB,KAAK,CAAC,CACrE,CAAC,CAAC,QAAQ,MAAO;IACb,IAAI,EAAG,aAAa,aAAa,GAAG,OAAO;IAC3C,IAAM,IAAQ,OAAO,SAAW,MAAc,OAAO,iBAAiB,CAAE,IAAI;IAI5E,OAHI,MAAU,EAAM,YAAY,UAAU,EAAM,eAAe;GAInE,CAAC;EACL;EASA,SAAS,EAAc,GAA4B;GAC/C,IAAI,EAAM,QAAQ,SAAS,EAAM,kBAAkB;GACnD,IAAM,IAAW,EAAa;GAC9B,IAAI,EAAS,WAAW,GAAG;IACvB,EAAM,eAAe;IACrB;GACJ;GACA,IAAM,IAAQ,EAAS,IACjB,IAAO,EAAS,EAAS,SAAS,IAClC,IAAU,SAAS;GACzB,AAAI,EAAM,YACF,MAAY,KAAS,CAAC,KAAW,CAAC,GAAW,SAAS,CAAO,OAC7D,EAAM,eAAe,GACrB,EAAK,MAAM,KAGX,MAAY,MACZ,EAAM,eAAe,GACrB,EAAM,MAAM;EAGxB;EAKA,OAFA,EAAA,CAAA,CAAU,EAAE,EAAE,MAAM,GACpB,SAAS,iBAAiB,WAAW,CAAa,SACrC;GAET,AADA,SAAS,oBAAoB,WAAW,CAAa,GACrD,GAAmB,QAAQ;EAC/B;CACJ,GAAG,CAAC,GAAc,CAAM,CAAC;AAC7B"}
1
+ {"version":3,"file":"use-focus-trap.js","names":[],"sources":["../../src/hooks/use-focus-trap.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { RefObject } from \"react\";\nimport { tabRegion, tabSequence } from \"./tab-sequence\";\n\nexport { FOCUSABLE_SELECTOR } from \"./tab-sequence\";\n\n/** One armed trap; the identity is what the stack compares. */\ninterface TrapEntry {\n container: HTMLElement;\n}\n\nconst stack: TrapEntry[] = [];\n\n/**\n * Trap keyboard focus inside `containerRef` while `active` is true.\n *\n * - **On activation** focus moves to the first tab stop, or to the container\n * itself when it has none (give it `tabIndex={-1}`). Content that already put\n * focus inside — an `autoFocus` input — is left where it is.\n * - **`Tab` / `Shift+Tab`** wrap between the first and the last stop, and pull\n * focus back in when it sits outside.\n * - **On release** focus returns to the element that held it when the trap\n * armed. That element is read during the render that arms the trap, before\n * React commits: an `autoFocus` child takes focus in the commit, ahead of any\n * effect, and reading it there would \"restore\" to the child that is about to\n * unmount — which dropped focus on `body` in `Command`.\n *\n * **Portalled panels stay reachable.** A `Popover` or `HoverCard` opened from\n * inside the container renders at the end of `body`, outside it. Those panels\n * register themselves (see `usePortalTabOrder`), and the trap treats them as\n * part of the container, spliced right after their trigger. A `Tab` another\n * layer already consumed is not moved again, unless it moved focus out of the\n * container and its panels — the portal bridge leaving the last panel stop for\n * whatever follows the trap in the page.\n *\n * **Traps stack.** Only the most recent active trap handles `Tab`, so a modal\n * opened over another owns the keyboard until it closes, and then hands focus\n * back to the element it was opened from, inside the one below.\n *\n * The listener is on `document` in the bubble phase, after React's own\n * handlers: a menu that closes on `Tab` and returns focus to its trigger has\n * already done so when the trap looks at where focus is.\n *\n * @param containerRef - The element focus is confined to.\n * @param active - Whether the trap is armed.\n * @returns Nothing.\n */\nexport function useFocusTrap(containerRef: RefObject<HTMLElement | null>, active: boolean): void {\n const [armed, setArmed] = useState(false);\n const [opener, setOpener] = useState<HTMLElement | null>(null);\n if (active !== armed) {\n setArmed(active);\n if (active && typeof document !== \"undefined\") {\n setOpener(document.activeElement as HTMLElement | null);\n }\n }\n\n useEffect(() => {\n if (!active) return;\n const container = containerRef.current;\n if (!container) return;\n\n const entry: TrapEntry = { container };\n stack.push(entry);\n\n if (!container.contains(document.activeElement)) {\n (tabSequence(container)[0] ?? container).focus();\n }\n\n const handleKeydown = (event: KeyboardEvent): void => {\n if (event.key !== \"Tab\" || stack.at(-1) !== entry) return;\n const sequence = tabSequence(container);\n const first = sequence[0];\n const last = sequence.at(-1);\n const current = document.activeElement as HTMLElement | null;\n const inside =\n current !== null && tabRegion(container).some((root) => root.contains(current));\n\n if (event.defaultPrevented) {\n if (!inside) ((event.shiftKey ? last : first) ?? container).focus();\n return;\n }\n if (!first || !last) {\n event.preventDefault();\n container.focus();\n return;\n }\n const atEnd = event.shiftKey\n ? current === first || current === container\n : current === last;\n if (!inside || atEnd) {\n event.preventDefault();\n (event.shiftKey ? last : first).focus();\n }\n };\n\n document.addEventListener(\"keydown\", handleKeydown);\n return () => {\n document.removeEventListener(\"keydown\", handleKeydown);\n stack.splice(stack.indexOf(entry), 1);\n if (opener?.isConnected) opener.focus();\n };\n }, [containerRef, active, opener]);\n}\n"],"mappings":";;;AAWA,IAAM,IAAqB,CAAC;AAoC5B,SAAgB,EAAa,GAA6C,GAAuB;CAC7F,IAAM,CAAC,GAAO,KAAY,EAAS,EAAK,GAClC,CAAC,GAAQ,KAAa,EAA6B,IAAI;CAQ7D,AAPI,MAAW,MACX,EAAS,CAAM,GACX,KAAU,OAAO,WAAa,OAC9B,EAAU,SAAS,aAAmC,IAI9D,QAAgB;EACZ,IAAI,CAAC,GAAQ;EACb,IAAM,IAAY,EAAa;EAC/B,IAAI,CAAC,GAAW;EAEhB,IAAM,IAAmB,EAAE,aAAU;EAGrC,AAFA,EAAM,KAAK,CAAK,GAEX,EAAU,SAAS,SAAS,aAAa,MACzC,EAAY,CAAS,CAAC,CAAC,MAAM,EAAA,CAAW,MAAM;EAGnD,IAAM,KAAiB,MAA+B;GAClD,IAAI,EAAM,QAAQ,SAAS,EAAM,GAAG,EAAE,MAAM,GAAO;GACnD,IAAM,IAAW,EAAY,CAAS,GAChC,IAAQ,EAAS,IACjB,IAAO,EAAS,GAAG,EAAE,GACrB,IAAU,SAAS,eACnB,IACF,MAAY,QAAQ,EAAU,CAAS,CAAC,CAAC,MAAM,MAAS,EAAK,SAAS,CAAO,CAAC;GAElF,IAAI,EAAM,kBAAkB;IACxB,AAAK,OAAU,EAAM,WAAW,IAAO,MAAU,EAAA,CAAW,MAAM;IAClE;GACJ;GACA,IAAI,CAAC,KAAS,CAAC,GAAM;IAEjB,AADA,EAAM,eAAe,GACrB,EAAU,MAAM;IAChB;GACJ;GACA,IAAM,IAAQ,EAAM,WACd,MAAY,KAAS,MAAY,IACjC,MAAY;GAClB,CAAI,CAAC,KAAU,OACX,EAAM,eAAe,IACpB,EAAM,WAAW,IAAO,EAAA,CAAO,MAAM;EAE9C;EAGA,OADA,SAAS,iBAAiB,WAAW,CAAa,SACrC;GAGT,AAFA,SAAS,oBAAoB,WAAW,CAAa,GACrD,EAAM,OAAO,EAAM,QAAQ,CAAK,GAAG,CAAC,GAChC,GAAQ,eAAa,EAAO,MAAM;EAC1C;CACJ,GAAG;EAAC;EAAc;EAAQ;CAAM,CAAC;AACrC"}
@@ -1,2 +1,2 @@
1
- const e=require("../utils/ids.cjs"),t=require("./build-url.cjs"),n=require("./credential-scope.cjs"),r=require("./decode-response.cjs"),i=require("./errors.cjs"),a=require("./retry.cjs"),o=require("./timeout.cjs");var s=new Set([`GET`,`HEAD`,`OPTIONS`]);function c(e,t){return!s.has(t)||!(e instanceof i.TempestApiError)?!1:i.isRetriableStatus(e.status)}function l(e){return e?e===!0?{}:e:null}var u=15e3,d=3e5;function f(e){return typeof FormData<`u`&&e instanceof FormData}function p(e){return Math.round(performance.now()-e)}async function m(e,t){let n;try{n=await e.clone().json()}catch{try{n=await e.text()}catch{n=null}}return new i.TempestApiError(i.buildApiError(e.status,n,e.headers,t))}function h(s){let h=s.fetcher??globalThis.fetch.bind(globalThis);function g(e,t){if(t)return{};let r=s.getToken?.();return r?n.isTrustedCredentialTarget(e,s.baseURL,s.trustedOrigins)?{Authorization:`Bearer ${r}`}:(n.reportSuppressedCredential(e,s.baseURL),{}):{}}async function _(e,n,r,a){let{body:c,params:l,headers:p,signal:m,timeout:_,...v}=n,y=f(c),b=y?s.uploadTimeout:s.timeout,x=_===void 0?b===void 0?y?d:u:b:_,S=t.buildApiUrl(s.baseURL,e,{prefix:s.prefix,params:l}),C={...y?{}:{"Content-Type":`application/json`},...r?{"X-Request-ID":r}:{},...s.headers,...g(S,a),...p},w=o.withTimeout(m,x),T={...v,signal:w.signal,headers:C,credentials:s.withCredentials?`include`:v.credentials,body:c==null?void 0:y?c:JSON.stringify(c)};try{return await h(S,T)}catch(e){throw w.timedOut()?new i.TempestApiError({status:0,detail:`A requisição excedeu ${x}ms e foi abandonada.`}):e}finally{w.dispose()}}async function v(e,t,n,r,i){let a=s.logger;if(!a)return _(e,t,n,i);let o=performance.now();try{let s=await _(e,t,n,i),c={requestId:n,status:s.status,ms:p(o)},l=`${r} ${e} → ${s.status}`;return s.status>=400?a.warn(l,c):a.debug(l,c),s}catch(t){throw a.warn(`${r} ${e} → no response`,{requestId:n,ms:p(o),error:t}),t}}async function y(e,t){if(s.onUnauthorized){s.logger?.warn(`unauthorized — calling onUnauthorized`,{requestId:t,status:e.status});try{await s.onUnauthorized(e)}catch(n){s.logger?.warn(`onUnauthorized threw — keeping the original response error`,{requestId:t,status:e.status,error:n})}}}async function b(t,n,r){let{skipAuth:i=!1,skipAuthRetry:a=!1,...o}=n,c=s.requestId?s.requestId():e.randomId(),l=(o.method??`GET`).toUpperCase(),u=await v(t,o,c,l,i);if(u.status===401&&!i&&!a){if(s.refresh){try{await s.refresh(),u=await v(t,o,c,l,i)}catch{throw await y(u,c),await m(u,c)}u.status===401&&await y(u,c)}else await y(u,c)}if(!u.ok)throw await m(u,c);return r(u)}async function x(e,t,n){let{retry:r,...i}=t,o=l(r===void 0?s.retry:r);if(!o)return b(e,i,n);let u=(i.method??`GET`).toUpperCase();return a.retry(()=>b(e,i,n),{...o,shouldRetry:o.shouldRetry??(e=>c(e,u))})}async function S(e,t={}){return x(e,t,r.decodeByContentType)}async function C(e,t,n=`POST`,r){return S(e,{...r,method:n,body:t})}return{request:S,get:(e,t)=>S(e,{...t,method:`GET`}),post:(e,t)=>S(e,{...t,method:`POST`}),put:(e,t)=>S(e,{...t,method:`PUT`}),patch:(e,t)=>S(e,{...t,method:`PATCH`}),delete:(e,t)=>S(e,{...t,method:`DELETE`}),blob:(e,t)=>x(e,{...t},e=>e.blob()),arrayBuffer:(e,t)=>x(e,{...t},e=>e.arrayBuffer()),upload:C}}exports.createApiClient=h;
1
+ const e=require("../utils/ids.cjs"),t=require("./build-url.cjs"),n=require("./credential-scope.cjs"),r=require("./csrf.cjs"),i=require("./decode-response.cjs"),a=require("./errors.cjs"),o=require("./retry.cjs"),s=require("./timeout.cjs");var c=new Set([`GET`,`HEAD`,`OPTIONS`]);function l(e,t){return!c.has(t)||!(e instanceof a.TempestApiError)?!1:a.isRetriableStatus(e.status)}function u(e){return e?e===!0?{}:e:null}var d=15e3,f=3e5;function p(e){return typeof FormData<`u`&&e instanceof FormData}function m(e){return Math.round(performance.now()-e)}async function h(e,t){let n;try{n=await e.clone().json()}catch{try{n=await e.text()}catch{n=null}}return new a.TempestApiError(a.buildApiError(e.status,n,e.headers,t))}function g(c){let g=c.fetcher??globalThis.fetch.bind(globalThis);function _(e,t){if(t)return{};let r=c.getToken?.();return r?n.isTrustedCredentialTarget(e,c.baseURL,c.trustedOrigins)?{Authorization:`Bearer ${r}`}:(n.reportSuppressedCredential(e,c.baseURL),{}):{}}async function v(e,n,i,o){let{body:l,params:u,headers:m,signal:h,timeout:v,...y}=n,b=p(l),x=b?c.uploadTimeout:c.timeout,S=v===void 0?x===void 0?b?f:d:x:v,C=t.buildApiUrl(c.baseURL,e,{prefix:c.prefix,params:u}),w=m,T={...b?{}:{"Content-Type":`application/json`},...i?{"X-Request-ID":i}:{},...c.headers,..._(C,o),...r.csrfHeaders({method:y.method??`GET`,url:C,reference:c.baseURL,csrf:c.csrf,trustedOrigins:c.trustedOrigins,headers:{...c.headers,...w}}),...w},E=s.withTimeout(h,S),D={...y,signal:E.signal,headers:T,credentials:c.withCredentials?`include`:y.credentials,body:l==null?void 0:b?l:JSON.stringify(l)};try{return await g(C,D)}catch(e){throw E.timedOut()?new a.TempestApiError({status:0,detail:`A requisição excedeu ${S}ms e foi abandonada.`}):e}finally{E.dispose()}}async function y(e,t,n,r,i){let a=c.logger;if(!a)return v(e,t,n,i);let o=performance.now();try{let s=await v(e,t,n,i),c={requestId:n,status:s.status,ms:m(o)},l=`${r} ${e} → ${s.status}`;return s.status>=400?a.warn(l,c):a.debug(l,c),s}catch(t){throw a.warn(`${r} ${e} → no response`,{requestId:n,ms:m(o),error:t}),t}}async function b(e,t){if(c.onUnauthorized){c.logger?.warn(`unauthorized — calling onUnauthorized`,{requestId:t,status:e.status});try{await c.onUnauthorized(e)}catch(n){c.logger?.warn(`onUnauthorized threw — keeping the original response error`,{requestId:t,status:e.status,error:n})}}}async function x(t,n,r){let{skipAuth:i=!1,skipAuthRetry:a=!1,...o}=n,s=c.requestId?c.requestId():e.randomId(),l=(o.method??`GET`).toUpperCase(),u=await y(t,o,s,l,i);if(u.status===401&&!i&&!a){if(c.refresh){try{await c.refresh(),u=await y(t,o,s,l,i)}catch{throw await b(u,s),await h(u,s)}u.status===401&&await b(u,s)}else await b(u,s)}if(!u.ok)throw await h(u,s);return r(u)}async function S(e,t,n){let{retry:r,...i}=t,a=u(r===void 0?c.retry:r);if(!a)return x(e,i,n);let s=(i.method??`GET`).toUpperCase();return o.retry(()=>x(e,i,n),{...a,shouldRetry:a.shouldRetry??(e=>l(e,s))})}async function C(e,t={}){return S(e,t,i.decodeByContentType)}async function w(e,t,n=`POST`,r){return C(e,{...r,method:n,body:t})}return{request:C,get:(e,t)=>C(e,{...t,method:`GET`}),post:(e,t)=>C(e,{...t,method:`POST`}),put:(e,t)=>C(e,{...t,method:`PUT`}),patch:(e,t)=>C(e,{...t,method:`PATCH`}),delete:(e,t)=>C(e,{...t,method:`DELETE`}),blob:(e,t)=>S(e,{...t},e=>e.blob()),arrayBuffer:(e,t)=>S(e,{...t},e=>e.arrayBuffer()),upload:w}}exports.createApiClient=g;
2
2
  //# sourceMappingURL=api-client.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-client.cjs","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { isTrustedCredentialTarget, reportSuppressedCredential } from \"./credential-scope\";\nimport { decodeByContentType } from \"./decode-response\";\nimport type { ResponseDecoder } from \"./decode-response\";\nimport { buildApiError, TempestApiError, isRetriableStatus } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport { withTimeout } from \"./timeout\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return isRetriableStatus(error.status);\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\n/** Default milliseconds before a request is abandoned. */\nconst DEFAULT_TIMEOUT = 15_000;\n\n/** Default milliseconds before a `FormData` request is abandoned. */\nconst DEFAULT_UPLOAD_TIMEOUT = 300_000;\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Binary bodies** have their own methods — `blob()` and `arrayBuffer()` — which\n * run the same pipeline and skip the parse. `request()` no longer decodes an\n * unknown `Content-Type` as text either: it returns a `Blob` and says so once in\n * a development build, because reading `image/jpeg` as UTF-8 destroyed the bytes\n * on the way in.\n *\n * **The bearer token is scoped to `baseURL`'s origin** since 0.66.0. A path may\n * be an absolute URL, which overrides the base entirely, so without a scope the\n * destination of a credentialed request could come from a value read off the\n * network. A request to another origin still goes out; it goes without the\n * header, and a development build says so once. Declare the exceptions in\n * {@link ApiClientConfig.trustedOrigins}.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write. A single call overrides it\n * with {@link RequestOptions.retry}, and opts out of auth entirely with\n * {@link RequestOptions.skipAuth}.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`blob`/\n * `arrayBuffer`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n /**\n * The `Authorization` header for a request, or nothing.\n *\n * Takes the resolved URL rather than reading `config.baseURL`, because an\n * absolute path overrides the base entirely (see `buildApiUrl`) and the\n * credential is scoped to an origin, not to a client.\n *\n * @param url - The absolute URL the request is going to.\n * @param skipAuth - Whether the caller opted this request out of auth.\n * @returns The header, or an empty object.\n */\n function authHeaders(url: string, skipAuth: boolean): Record<string, string> {\n if (skipAuth) return {};\n const token = config.getToken?.();\n if (!token) return {};\n if (!isTrustedCredentialTarget(url, config.baseURL, config.trustedOrigins)) {\n reportSuppressedCredential(url, config.baseURL);\n return {};\n }\n return { Authorization: `Bearer ${token}` };\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId: string | undefined,\n skipAuth: boolean,\n ): Promise<Response> {\n const { body, params, headers, signal, timeout, ...rest } = options;\n const isForm = isFormData(body);\n const configured = isForm ? config.uploadTimeout : config.timeout;\n const fallback = isForm ? DEFAULT_UPLOAD_TIMEOUT : DEFAULT_TIMEOUT;\n const limit =\n timeout !== undefined ? timeout : configured !== undefined ? configured : fallback;\n\n const url = buildApiUrl(config.baseURL, path, { prefix: config.prefix, params });\n\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(url, skipAuth),\n ...(headers as Record<string, string> | undefined),\n };\n\n const timed = withTimeout(signal, limit);\n const init: RequestInit = {\n ...rest,\n signal: timed.signal,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n try {\n return await fetcher(url, init);\n } catch (cause) {\n if (timed.timedOut()) {\n throw new TempestApiError({\n status: 0,\n detail: `A requisição excedeu ${limit}ms e foi abandonada.`,\n });\n }\n throw cause;\n } finally {\n timed.dispose();\n }\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n skipAuth: boolean,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId, skipAuth);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId, skipAuth);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n async function notifyUnauthorized(response: Response, requestId: string): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response);\n } catch (error) {\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n async function attempt<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const { skipAuth = false, skipAuthRetry = false, ...init } = options;\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (init.method ?? \"GET\").toUpperCase();\n let response = await send(path, init, requestId, method, skipAuth);\n\n if (response.status === 401 && !skipAuth && !skipAuthRetry) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await send(path, init, requestId, method, skipAuth);\n } catch {\n await notifyUnauthorized(response, requestId);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId);\n }\n } else {\n await notifyUnauthorized(response, requestId);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n return decode(response);\n }\n\n async function run<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const { retry: perRequest, ...rest } = options;\n const retryOptions = resolveRetry(perRequest !== undefined ? perRequest : config.retry);\n if (!retryOptions) return attempt<T>(path, rest, decode);\n\n const method = (rest.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, rest, decode), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n return run<T>(path, options, decodeByContentType);\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n options?: Omit<RequestOptions, \"body\" | \"method\">,\n ): Promise<T> {\n return request<T>(path, { ...options, method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n blob: (path: string, options?: RequestOptions) =>\n run<Blob>(path, { ...options }, (response) => response.blob()),\n arrayBuffer: (path: string, options?: RequestOptions) =>\n run<ArrayBuffer>(path, { ...options }, (response) => response.arrayBuffer()),\n upload,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"api-client.cjs","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { isTrustedCredentialTarget, reportSuppressedCredential } from \"./credential-scope\";\nimport { csrfHeaders } from \"./csrf\";\nimport { decodeByContentType } from \"./decode-response\";\nimport type { ResponseDecoder } from \"./decode-response\";\nimport { buildApiError, TempestApiError, isRetriableStatus } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport { withTimeout } from \"./timeout\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return isRetriableStatus(error.status);\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\n/** Default milliseconds before a request is abandoned. */\nconst DEFAULT_TIMEOUT = 15_000;\n\n/** Default milliseconds before a `FormData` request is abandoned. */\nconst DEFAULT_UPLOAD_TIMEOUT = 300_000;\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Binary bodies** have their own methods — `blob()` and `arrayBuffer()` — which\n * run the same pipeline and skip the parse. `request()` no longer decodes an\n * unknown `Content-Type` as text either: it returns a `Blob` and says so once in\n * a development build, because reading `image/jpeg` as UTF-8 destroyed the bytes\n * on the way in.\n *\n * **The bearer token is scoped to `baseURL`'s origin** since 0.66.0. A path may\n * be an absolute URL, which overrides the base entirely, so without a scope the\n * destination of a credentialed request could come from a value read off the\n * network. A request to another origin still goes out; it goes without the\n * header, and a development build says so once. Declare the exceptions in\n * {@link ApiClientConfig.trustedOrigins}.\n *\n * **CSRF** is off unless you set `csrf`. With it, every unsafe request (anything\n * but `GET`/`HEAD`/`OPTIONS`/`TRACE`) echoes the `csrf_token` cookie in an\n * `X-CSRF-Token` header — the double-submit contract of `tempest-fastapi-sdk`'s\n * `CSRFMiddleware` — scoped to the same origins as the bearer token and sent on\n * `skipAuth` requests too. See {@link ApiClientConfig.csrf}.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write. A single call overrides it\n * with {@link RequestOptions.retry}, and opts out of auth entirely with\n * {@link RequestOptions.skipAuth}.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`blob`/\n * `arrayBuffer`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n /**\n * The `Authorization` header for a request, or nothing.\n *\n * Takes the resolved URL rather than reading `config.baseURL`, because an\n * absolute path overrides the base entirely (see `buildApiUrl`) and the\n * credential is scoped to an origin, not to a client.\n *\n * @param url - The absolute URL the request is going to.\n * @param skipAuth - Whether the caller opted this request out of auth.\n * @returns The header, or an empty object.\n */\n function authHeaders(url: string, skipAuth: boolean): Record<string, string> {\n if (skipAuth) return {};\n const token = config.getToken?.();\n if (!token) return {};\n if (!isTrustedCredentialTarget(url, config.baseURL, config.trustedOrigins)) {\n reportSuppressedCredential(url, config.baseURL);\n return {};\n }\n return { Authorization: `Bearer ${token}` };\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId: string | undefined,\n skipAuth: boolean,\n ): Promise<Response> {\n const { body, params, headers, signal, timeout, ...rest } = options;\n const isForm = isFormData(body);\n const configured = isForm ? config.uploadTimeout : config.timeout;\n const fallback = isForm ? DEFAULT_UPLOAD_TIMEOUT : DEFAULT_TIMEOUT;\n const limit =\n timeout !== undefined ? timeout : configured !== undefined ? configured : fallback;\n\n const url = buildApiUrl(config.baseURL, path, { prefix: config.prefix, params });\n\n const callerHeaders = headers as Record<string, string> | undefined;\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(url, skipAuth),\n ...csrfHeaders({\n method: rest.method ?? \"GET\",\n url,\n reference: config.baseURL,\n csrf: config.csrf,\n trustedOrigins: config.trustedOrigins,\n headers: { ...config.headers, ...callerHeaders },\n }),\n ...callerHeaders,\n };\n\n const timed = withTimeout(signal, limit);\n const init: RequestInit = {\n ...rest,\n signal: timed.signal,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n try {\n return await fetcher(url, init);\n } catch (cause) {\n if (timed.timedOut()) {\n throw new TempestApiError({\n status: 0,\n detail: `A requisição excedeu ${limit}ms e foi abandonada.`,\n });\n }\n throw cause;\n } finally {\n timed.dispose();\n }\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n skipAuth: boolean,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId, skipAuth);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId, skipAuth);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n async function notifyUnauthorized(response: Response, requestId: string): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response);\n } catch (error) {\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n async function attempt<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const { skipAuth = false, skipAuthRetry = false, ...init } = options;\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (init.method ?? \"GET\").toUpperCase();\n let response = await send(path, init, requestId, method, skipAuth);\n\n if (response.status === 401 && !skipAuth && !skipAuthRetry) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await send(path, init, requestId, method, skipAuth);\n } catch {\n await notifyUnauthorized(response, requestId);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId);\n }\n } else {\n await notifyUnauthorized(response, requestId);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n return decode(response);\n }\n\n async function run<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const { retry: perRequest, ...rest } = options;\n const retryOptions = resolveRetry(perRequest !== undefined ? perRequest : config.retry);\n if (!retryOptions) return attempt<T>(path, rest, decode);\n\n const method = (rest.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, rest, decode), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n return run<T>(path, options, decodeByContentType);\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n options?: Omit<RequestOptions, \"body\" | \"method\">,\n ): Promise<T> {\n return request<T>(path, { ...options, method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n blob: (path: string, options?: RequestOptions) =>\n run<Blob>(path, { ...options }, (response) => response.blob()),\n arrayBuffer: (path: string, options?: RequestOptions) =>\n run<ArrayBuffer>(path, { ...options }, (response) => response.arrayBuffer()),\n upload,\n };\n}\n"],"mappings":"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"}