tempest-react-sdk 0.67.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) 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/tempest-react-sdk.cjs +1 -1
  131. package/dist/tempest-react-sdk.d.ts +658 -22
  132. package/dist/tempest-react-sdk.js +347 -342
  133. package/dist/vite/tempest-csp.cjs +3 -0
  134. package/dist/vite/tempest-csp.cjs.map +1 -0
  135. package/dist/vite/tempest-csp.js +189 -0
  136. package/dist/vite/tempest-csp.js.map +1 -0
  137. package/dist/vite.cjs +1 -1
  138. package/dist/vite.d.ts +145 -0
  139. package/dist/vite.js +2 -1
  140. package/package.json +2 -2
  141. package/template/vite.config.ts +8 -1
  142. package/template-pwa/vite.config.ts +6 -0
@@ -1,71 +1,74 @@
1
1
  import { cn as e } from "../../utils/cn.js";
2
2
  import { useEscapeLayer as t } from "../Portal/escape-layer.js";
3
- import { usePortalHost as n } from "../Portal/portal-host.js";
4
- import r from "./Modal.module.js";
5
- import { useEffect as i, useId as a } from "react";
6
- import { jsx as o, jsxs as s } from "react/jsx-runtime";
7
- import { createPortal as c } from "react-dom";
3
+ import { useFocusTrap as n } from "../../hooks/use-focus-trap.js";
4
+ import { usePortalHost as r } from "../Portal/portal-host.js";
5
+ import i from "./Modal.module.js";
6
+ import { useEffect as a, useId as o, useRef as s } from "react";
7
+ import { jsx as c, jsxs as l } from "react/jsx-runtime";
8
+ import { createPortal as u } from "react-dom";
8
9
  //#region src/components/Modal/Modal.tsx
9
- function l({ open: l, onClose: f, title: p, children: m, footer: h, size: g = "md", closeOnBackdrop: _ = !0, closeOnEsc: v = !0, className: y, hideCloseButton: b = !1, fullscreen: x = !1, fullscreenOnMobile: S = !1, "aria-label": C }) {
10
- let w = a();
11
- t(l, v ? f : null), i(() => {
12
- if (!l) return;
10
+ function d({ open: d, onClose: m, title: h, children: g, footer: _, size: v = "md", closeOnBackdrop: y = !0, closeOnEsc: b = !0, className: x, hideCloseButton: S = !1, fullscreen: C = !1, fullscreenOnMobile: w = !1, "aria-label": T }) {
11
+ let E = o(), D = s(null);
12
+ n(D, d), t(d, b ? m : null), a(() => {
13
+ if (!d) return;
13
14
  let e = document.body.style.overflow;
14
15
  return document.body.style.overflow = "hidden", () => {
15
16
  document.body.style.overflow = e;
16
17
  };
17
- }, [l]);
18
- let T = n();
19
- return !l || !T ? null : c(/* @__PURE__ */ o("div", {
20
- className: r.overlay,
18
+ }, [d]);
19
+ let O = r();
20
+ return !d || !O ? null : u(/* @__PURE__ */ c("div", {
21
+ className: i.overlay,
21
22
  role: "presentation",
22
23
  onClick: () => {
23
- _ && f();
24
+ y && m();
24
25
  },
25
- children: /* @__PURE__ */ s("div", {
26
+ children: /* @__PURE__ */ l("div", {
27
+ ref: D,
26
28
  role: "dialog",
27
29
  "aria-modal": "true",
28
- "aria-labelledby": p ? w : void 0,
29
- "aria-label": p ? void 0 : C,
30
- className: e(r.dialog, u(g), x && r.fullscreen, S && r.fullscreenOnMobile, y),
30
+ tabIndex: -1,
31
+ "aria-labelledby": h ? E : void 0,
32
+ "aria-label": h ? void 0 : T,
33
+ className: e(i.dialog, f(v), C && i.fullscreen, w && i.fullscreenOnMobile, x),
31
34
  onClick: (e) => e.stopPropagation(),
32
35
  children: [
33
- (p || !b) && /* @__PURE__ */ s("header", {
34
- className: r.header,
35
- children: [p && /* @__PURE__ */ o("h3", {
36
- id: w,
37
- className: r.title,
38
- children: p
39
- }), !b && /* @__PURE__ */ o("button", {
36
+ (h || !S) && /* @__PURE__ */ l("header", {
37
+ className: i.header,
38
+ children: [h && /* @__PURE__ */ c("h3", {
39
+ id: E,
40
+ className: i.title,
41
+ children: h
42
+ }), !S && /* @__PURE__ */ c("button", {
40
43
  type: "button",
41
- className: r.close,
44
+ className: i.close,
42
45
  "aria-label": "Fechar",
43
- onClick: f,
44
- children: /* @__PURE__ */ o(d, {})
46
+ onClick: m,
47
+ children: /* @__PURE__ */ c(p, {})
45
48
  })]
46
49
  }),
47
- /* @__PURE__ */ o("div", {
48
- className: r.body,
49
- children: m
50
+ /* @__PURE__ */ c("div", {
51
+ className: i.body,
52
+ children: g
50
53
  }),
51
- h && /* @__PURE__ */ o("footer", {
52
- className: r.footer,
53
- children: h
54
+ _ && /* @__PURE__ */ c("footer", {
55
+ className: i.footer,
56
+ children: _
54
57
  })
55
58
  ]
56
59
  })
57
- }), T);
60
+ }), O);
58
61
  }
59
- function u(e) {
60
- return e === "2xl" ? r.size2xl : e === "3xl" ? r.size3xl : r[e];
62
+ function f(e) {
63
+ return e === "2xl" ? i.size2xl : e === "3xl" ? i.size3xl : i[e];
61
64
  }
62
- function d() {
63
- return /* @__PURE__ */ o("svg", {
65
+ function p() {
66
+ return /* @__PURE__ */ c("svg", {
64
67
  width: "18",
65
68
  height: "18",
66
69
  viewBox: "0 0 24 24",
67
70
  fill: "none",
68
- children: /* @__PURE__ */ o("path", {
71
+ children: /* @__PURE__ */ c("path", {
69
72
  d: "M6 6l12 12M6 18L18 6",
70
73
  stroke: "currentColor",
71
74
  strokeWidth: "2",
@@ -74,6 +77,6 @@ function d() {
74
77
  });
75
78
  }
76
79
  //#endregion
77
- export { l as Modal };
80
+ export { d as Modal };
78
81
 
79
82
  //# sourceMappingURL=Modal.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Modal.js","names":[],"sources":["../../../src/components/Modal/Modal.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — size, fullscreen and\n * fullscreenOnMobile are three distinct decisions (a fixed width, always full, full\n * only on a phone), and the rest is the dialog contract: open, onClose, title,\n * children, footer, closeOnBackdrop, closeOnEsc, hideCloseButton. The body is the\n * focus trap and the scroll lock, which must be installed and torn down together.\n */\nimport { useEffect, useId } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useEscapeLayer } from \"@/components/Portal/escape-layer\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Modal.module.css\";\nimport { usePortalHost } from \"../Portal/portal-host\";\n\nexport type ModalSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\";\n\nexport interface ModalProps {\n open: boolean;\n onClose: () => void;\n title?: ReactNode;\n children?: ReactNode;\n footer?: ReactNode;\n size?: ModalSize;\n closeOnBackdrop?: boolean;\n closeOnEsc?: boolean;\n className?: string;\n hideCloseButton?: boolean;\n /** Force fullscreen at all breakpoints. */\n fullscreen?: boolean;\n /** Auto-fullscreen on mobile viewports (< 640px). Default `false`. */\n fullscreenOnMobile?: boolean;\n /**\n * Accessible name for the dialog. Only needed when there is no `title` —\n * with a `title` the dialog is named by it through `aria-labelledby`.\n */\n \"aria-label\"?: string;\n}\n\n/**\n * Portal-rendered modal dialog with backdrop, Esc handler, and slots for\n * header/body/footer. Locks body scroll while open.\n *\n * The `dialog` role needs an accessible name: a `title` supplies it via\n * `aria-labelledby`, and a titleless dialog should pass `aria-label`. The\n * header heading is only rendered when there is a title, so a close-button-only\n * header does not leave an empty `h3` behind.\n */\nexport function Modal({\n open,\n onClose,\n title,\n children,\n footer,\n size = \"md\",\n closeOnBackdrop = true,\n closeOnEsc = true,\n className,\n hideCloseButton = false,\n fullscreen = false,\n fullscreenOnMobile = false,\n \"aria-label\": ariaLabel,\n}: ModalProps) {\n const titleId = useId();\n useEscapeLayer(open, closeOnEsc ? onClose : null);\n useEffect(() => {\n if (!open) return;\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n return () => {\n document.body.style.overflow = previousOverflow;\n };\n }, [open]);\n\n const portalHost = usePortalHost();\n\n if (!open || !portalHost) return null;\n\n return createPortal(\n <div\n className={styles.overlay}\n role=\"presentation\"\n onClick={() => {\n if (closeOnBackdrop) onClose();\n }}\n >\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={title ? titleId : undefined}\n aria-label={title ? undefined : ariaLabel}\n className={cn(\n styles.dialog,\n sizeClassName(size),\n fullscreen && styles.fullscreen,\n fullscreenOnMobile && styles.fullscreenOnMobile,\n className,\n )}\n onClick={(event) => event.stopPropagation()}\n >\n {(title || !hideCloseButton) && (\n <header className={styles.header}>\n {title && (\n <h3 id={titleId} className={styles.title}>\n {title}\n </h3>\n )}\n {!hideCloseButton && (\n <button\n type=\"button\"\n className={styles.close}\n aria-label=\"Fechar\"\n onClick={onClose}\n >\n <CloseIcon />\n </button>\n )}\n </header>\n )}\n <div className={styles.body}>{children}</div>\n {footer && <footer className={styles.footer}>{footer}</footer>}\n </div>\n </div>,\n portalHost,\n );\n}\n\nfunction sizeClassName(size: ModalSize): string | undefined {\n if (size === \"2xl\") return styles.size2xl;\n if (size === \"3xl\") return styles.size3xl;\n return styles[size];\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 6l12 12M6 18L18 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":";;;;;;;;AAgDA,SAAgB,EAAM,EAClB,SACA,YACA,UACA,aACA,WACA,UAAO,MACP,qBAAkB,IAClB,gBAAa,IACb,cACA,qBAAkB,IAClB,gBAAa,IACb,wBAAqB,IACrB,cAAc,KACH;CACX,IAAM,IAAU,EAAM;CAEtB,AADA,EAAe,GAAM,IAAa,IAAU,IAAI,GAChD,QAAgB;EACZ,IAAI,CAAC,GAAM;EACX,IAAM,IAAmB,SAAS,KAAK,MAAM;EAE7C,OADA,SAAS,KAAK,MAAM,WAAW,gBAClB;GACT,SAAS,KAAK,MAAM,WAAW;EACnC;CACJ,GAAG,CAAC,CAAI,CAAC;CAET,IAAM,IAAa,EAAc;CAIjC,OAFI,CAAC,KAAQ,CAAC,IAAmB,OAE1B,EACH,kBAAC,OAAD;EACI,WAAW,EAAO;EAClB,MAAK;EACL,eAAe;GACX,AAAI,KAAiB,EAAQ;EACjC;EAEA,UAAA,kBAAC,OAAD;GACI,MAAK;GACL,cAAW;GACX,mBAAiB,IAAQ,IAAU,KAAA;GACnC,cAAY,IAAQ,KAAA,IAAY;GAChC,WAAW,EACP,EAAO,QACP,EAAc,CAAI,GAClB,KAAc,EAAO,YACrB,KAAsB,EAAO,oBAC7B,CACJ;GACA,UAAU,MAAU,EAAM,gBAAgB;GAZ9C,UAAA;KAcM,KAAS,CAAC,MACR,kBAAC,UAAD;KAAQ,WAAW,EAAO;KAA1B,UAAA,CACK,KACG,kBAAC,MAAD;MAAI,IAAI;MAAS,WAAW,EAAO;MAC9B,UAAA;KACD,CAAA,GAEP,CAAC,KACE,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,cAAW;MACX,SAAS;MAET,UAAA,kBAAC,GAAD,CAAY,CAAA;KACR,CAAA,CAER;;IAEZ,kBAAC,OAAD;KAAK,WAAW,EAAO;KAAO;IAAc,CAAA;IAC3C,KAAU,kBAAC,UAAD;KAAQ,WAAW,EAAO;KAAS,UAAA;IAAe,CAAA;GAC5D;;CACJ,CAAA,GACL,CACJ;AACJ;AAEA,SAAS,EAAc,GAAqC;CAGxD,OAFI,MAAS,QAAc,EAAO,UAC9B,MAAS,QAAc,EAAO,UAC3B,EAAO;AAClB;AAEA,SAAS,IAAY;CACjB,OACI,kBAAC,OAAD;EAAK,OAAM;EAAK,QAAO;EAAK,SAAQ;EAAY,MAAK;EACjD,UAAA,kBAAC,QAAD;GACI,GAAE;GACF,QAAO;GACP,aAAY;GACZ,eAAc;EACjB,CAAA;CACA,CAAA;AAEb"}
1
+ {"version":3,"file":"Modal.js","names":[],"sources":["../../../src/components/Modal/Modal.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — size, fullscreen and\n * fullscreenOnMobile are three distinct decisions (a fixed width, always full, full\n * only on a phone), and the rest is the dialog contract: open, onClose, title,\n * children, footer, closeOnBackdrop, closeOnEsc, hideCloseButton. The body is the\n * focus trap and the scroll lock, which must be installed and torn down together.\n */\nimport { useEffect, useId, useRef } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useEscapeLayer } from \"@/components/Portal/escape-layer\";\nimport { useFocusTrap } from \"@/hooks/use-focus-trap\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Modal.module.css\";\nimport { usePortalHost } from \"../Portal/portal-host\";\n\nexport type ModalSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\";\n\nexport interface ModalProps {\n open: boolean;\n onClose: () => void;\n title?: ReactNode;\n children?: ReactNode;\n footer?: ReactNode;\n size?: ModalSize;\n closeOnBackdrop?: boolean;\n closeOnEsc?: boolean;\n className?: string;\n hideCloseButton?: boolean;\n /** Force fullscreen at all breakpoints. */\n fullscreen?: boolean;\n /** Auto-fullscreen on mobile viewports (< 640px). Default `false`. */\n fullscreenOnMobile?: boolean;\n /**\n * Accessible name for the dialog. Only needed when there is no `title` —\n * with a `title` the dialog is named by it through `aria-labelledby`.\n */\n \"aria-label\"?: string;\n}\n\n/**\n * Portal-rendered modal dialog with backdrop, Esc handler, and slots for\n * header/body/footer. Locks body scroll while open.\n *\n * Keyboard focus is trapped in the dialog while it is open (`useFocusTrap`): it\n * enters on the first tab stop — or the dialog itself when there is none —,\n * `Tab`/`Shift+Tab` cycle inside it, including the panels of a `Popover` or\n * `HoverCard` opened from it, and closing returns focus to the element that\n * opened it. `aria-modal=\"true\"` tells a screen reader the page behind is inert;\n * the trap is what makes that true for the keyboard.\n *\n * The `dialog` role needs an accessible name: a `title` supplies it via\n * `aria-labelledby`, and a titleless dialog should pass `aria-label`. The\n * header heading is only rendered when there is a title, so a close-button-only\n * header does not leave an empty `h3` behind.\n */\nexport function Modal({\n open,\n onClose,\n title,\n children,\n footer,\n size = \"md\",\n closeOnBackdrop = true,\n closeOnEsc = true,\n className,\n hideCloseButton = false,\n fullscreen = false,\n fullscreenOnMobile = false,\n \"aria-label\": ariaLabel,\n}: ModalProps) {\n const titleId = useId();\n const dialogRef = useRef<HTMLDivElement>(null);\n useFocusTrap(dialogRef, open);\n useEscapeLayer(open, closeOnEsc ? onClose : null);\n useEffect(() => {\n if (!open) return;\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n return () => {\n document.body.style.overflow = previousOverflow;\n };\n }, [open]);\n\n const portalHost = usePortalHost();\n\n if (!open || !portalHost) return null;\n\n return createPortal(\n <div\n className={styles.overlay}\n role=\"presentation\"\n onClick={() => {\n if (closeOnBackdrop) onClose();\n }}\n >\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n tabIndex={-1}\n aria-labelledby={title ? titleId : undefined}\n aria-label={title ? undefined : ariaLabel}\n className={cn(\n styles.dialog,\n sizeClassName(size),\n fullscreen && styles.fullscreen,\n fullscreenOnMobile && styles.fullscreenOnMobile,\n className,\n )}\n onClick={(event) => event.stopPropagation()}\n >\n {(title || !hideCloseButton) && (\n <header className={styles.header}>\n {title && (\n <h3 id={titleId} className={styles.title}>\n {title}\n </h3>\n )}\n {!hideCloseButton && (\n <button\n type=\"button\"\n className={styles.close}\n aria-label=\"Fechar\"\n onClick={onClose}\n >\n <CloseIcon />\n </button>\n )}\n </header>\n )}\n <div className={styles.body}>{children}</div>\n {footer && <footer className={styles.footer}>{footer}</footer>}\n </div>\n </div>,\n portalHost,\n );\n}\n\nfunction sizeClassName(size: ModalSize): string | undefined {\n if (size === \"2xl\") return styles.size2xl;\n if (size === \"3xl\") return styles.size3xl;\n return styles[size];\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 6l12 12M6 18L18 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":";;;;;;;;;AAwDA,SAAgB,EAAM,EAClB,SACA,YACA,UACA,aACA,WACA,UAAO,MACP,qBAAkB,IAClB,gBAAa,IACb,cACA,qBAAkB,IAClB,gBAAa,IACb,wBAAqB,IACrB,cAAc,KACH;CACX,IAAM,IAAU,EAAM,GAChB,IAAY,EAAuB,IAAI;CAG7C,AAFA,EAAa,GAAW,CAAI,GAC5B,EAAe,GAAM,IAAa,IAAU,IAAI,GAChD,QAAgB;EACZ,IAAI,CAAC,GAAM;EACX,IAAM,IAAmB,SAAS,KAAK,MAAM;EAE7C,OADA,SAAS,KAAK,MAAM,WAAW,gBAClB;GACT,SAAS,KAAK,MAAM,WAAW;EACnC;CACJ,GAAG,CAAC,CAAI,CAAC;CAET,IAAM,IAAa,EAAc;CAIjC,OAFI,CAAC,KAAQ,CAAC,IAAmB,OAE1B,EACH,kBAAC,OAAD;EACI,WAAW,EAAO;EAClB,MAAK;EACL,eAAe;GACX,AAAI,KAAiB,EAAQ;EACjC;EAEA,UAAA,kBAAC,OAAD;GACI,KAAK;GACL,MAAK;GACL,cAAW;GACX,UAAU;GACV,mBAAiB,IAAQ,IAAU,KAAA;GACnC,cAAY,IAAQ,KAAA,IAAY;GAChC,WAAW,EACP,EAAO,QACP,EAAc,CAAI,GAClB,KAAc,EAAO,YACrB,KAAsB,EAAO,oBAC7B,CACJ;GACA,UAAU,MAAU,EAAM,gBAAgB;GAd9C,UAAA;KAgBM,KAAS,CAAC,MACR,kBAAC,UAAD;KAAQ,WAAW,EAAO;KAA1B,UAAA,CACK,KACG,kBAAC,MAAD;MAAI,IAAI;MAAS,WAAW,EAAO;MAC9B,UAAA;KACD,CAAA,GAEP,CAAC,KACE,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,cAAW;MACX,SAAS;MAET,UAAA,kBAAC,GAAD,CAAY,CAAA;KACR,CAAA,CAER;;IAEZ,kBAAC,OAAD;KAAK,WAAW,EAAO;KAAO;IAAc,CAAA;IAC3C,KAAU,kBAAC,UAAD;KAAQ,WAAW,EAAO;KAAS,UAAA;IAAe,CAAA;GAC5D;;CACJ,CAAA,GACL,CACJ;AACJ;AAEA,SAAS,EAAc,GAAqC;CAGxD,OAFI,MAAS,QAAc,EAAO,UAC9B,MAAS,QAAc,EAAO,UAC3B,EAAO;AAClB;AAEA,SAAS,IAAY;CACjB,OACI,kBAAC,OAAD;EAAK,OAAM;EAAK,QAAO;EAAK,SAAQ;EAAY,MAAK;EACjD,UAAA,kBAAC,QAAD;GACI,GAAE;GACF,QAAO;GACP,aAAY;GACZ,eAAc;EACjB,CAAA;CACA,CAAA;AAEb"}
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/cn.cjs"),t=require("./Pagination.module.cjs");let n=require("react"),r=require("react/jsx-runtime");function i(e,t,n){if(t<=n+4)return Array.from({length:t},(e,t)=>t+1);let r=Math.max(2,e-Math.floor(n/2)),i=Math.min(t-1,r+n-1),a=[1];r>2&&a.push(`...`);for(let e=r;e<=i;e++)a.push(e);return i<t-1&&a.push(`...`),a.push(t),a}function a({page:a,totalPages:o,onPageChange:s,pageSize:c,onPageSizeChange:l,pageSizeOptions:u=[10,25,50,100],totalItems:d,siblingCount:f=3,compactOnMobile:p=!0,className:m}){let h=(0,n.useMemo)(()=>i(a,o,f),[a,o,f]),g=(0,n.useRef)(null);return(0,n.useEffect)(()=>{p||g.current?.scrollIntoView?.({block:`nearest`,inline:`nearest`})},[a,p]),o<=1&&!l?null:(0,r.jsxs)(`div`,{className:e.cn(t.default.wrapper,m),"data-compact":String(p),children:[(0,r.jsx)(`div`,{className:t.default.summary,children:typeof d==`number`?`${d} resultado${d===1?``:`s`}`:`Página ${a} de ${o}`}),(0,r.jsxs)(`div`,{className:t.default.controls,children:[(0,r.jsx)(`button`,{type:`button`,className:t.default.page,onClick:()=>s(Math.max(1,a-1)),disabled:a<=1,"aria-label":`Página anterior`,children:`‹`}),h.map((n,i)=>n===`...`?(0,r.jsx)(`span`,{className:e.cn(t.default.ellipsis,t.default.numeric),children:`…`},`ellipsis-${i}`):(0,r.jsx)(`button`,{type:`button`,ref:n===a?g:void 0,className:e.cn(t.default.page,t.default.numeric,n===a&&t.default.active),onClick:()=>s(n),"aria-current":n===a?`page`:void 0,children:n},n)),(0,r.jsx)(`button`,{type:`button`,className:t.default.page,onClick:()=>s(Math.min(o,a+1)),disabled:a>=o,"aria-label":`Próxima página`,children:`›`}),l&&(0,r.jsx)(`select`,{className:t.default.sizeSelect,value:c,onChange:e=>l(Number(e.target.value)),"aria-label":`Itens por página`,children:u.map(e=>(0,r.jsxs)(`option`,{value:e,children:[e,` / página`]},e))})]})]})}exports.Pagination=a;
1
+ const e=require("../../utils/cn.cjs"),t=require("./Pagination.module.cjs");let n=require("react"),r=require("react/jsx-runtime");var i=[10,25,50,100];function a(e,t,n){if(t<=n+4)return Array.from({length:t},(e,t)=>t+1);let r=Math.max(2,e-Math.floor(n/2)),i=Math.min(t-1,r+n-1),a=[1];r>2&&a.push(`...`);for(let e=r;e<=i;e++)a.push(e);return i<t-1&&a.push(`...`),a.push(t),a}function o({page:o,totalPages:s,onPageChange:c,pageSize:l,onPageSizeChange:u,pageSizeOptions:d=i,totalItems:f,siblingCount:p=3,compactOnMobile:m=!0,className:h}){let g=(0,n.useMemo)(()=>a(o,s,p),[o,s,p]),_=(0,n.useMemo)(()=>l===void 0||d.includes(l)?d:[...d,l].sort((e,t)=>e-t),[l,d]),v=(0,n.useRef)(null);return(0,n.useEffect)(()=>{m||v.current?.scrollIntoView?.({block:`nearest`,inline:`nearest`})},[o,m]),s<=1&&!u?null:(0,r.jsxs)(`div`,{className:e.cn(t.default.wrapper,h),"data-compact":String(m),children:[(0,r.jsx)(`div`,{className:t.default.summary,children:typeof f==`number`?`${f} resultado${f===1?``:`s`}`:`Página ${o} de ${s}`}),(0,r.jsxs)(`div`,{className:t.default.controls,children:[(0,r.jsx)(`button`,{type:`button`,className:t.default.page,onClick:()=>c(Math.max(1,o-1)),disabled:o<=1,"aria-label":`Página anterior`,children:`‹`}),g.map((n,i)=>n===`...`?(0,r.jsx)(`span`,{className:e.cn(t.default.ellipsis,t.default.numeric),children:`…`},`ellipsis-${i}`):(0,r.jsx)(`button`,{type:`button`,ref:n===o?v:void 0,className:e.cn(t.default.page,t.default.numeric,n===o&&t.default.active),onClick:()=>c(n),"aria-current":n===o?`page`:void 0,children:n},n)),(0,r.jsx)(`button`,{type:`button`,className:t.default.page,onClick:()=>c(Math.min(s,o+1)),disabled:o>=s,"aria-label":`Próxima página`,children:`›`}),u&&(0,r.jsx)(`select`,{className:t.default.sizeSelect,value:l,onChange:e=>u(Number(e.target.value)),"aria-label":`Itens por página`,children:_.map(e=>(0,r.jsxs)(`option`,{value:e,children:[e,` / página`]},e))})]})]})}exports.Pagination=o;
2
2
  //# sourceMappingURL=Pagination.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"Pagination.cjs","names":[],"sources":["../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — page/totalPages/onPageChange is the pager,\n * pageSize/onPageSizeChange/ pageSizeOptions is the size control that sits in the\n * same bar, and totalItems plus siblingCount decide the copy and how many page\n * buttons are rendered. They ship together because a footer that pages but cannot\n * resize is half a footer.\n */\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Pagination.module.css\";\n\nexport interface PaginationProps {\n page: number;\n totalPages: number;\n onPageChange: (page: number) => void;\n pageSize?: number;\n onPageSizeChange?: (size: number) => void;\n pageSizeOptions?: number[];\n /** Total items count; if provided, renders the summary text. */\n totalItems?: number;\n /** Pages kept around the current one; ~`siblingCount + 4` buttons render. Default 3. */\n siblingCount?: number;\n /**\n * Below 640px, drop to `‹` / `›` plus the summary and hide the numbered\n * buttons and the size select. Default `true`.\n *\n * The default suits a desktop-first app, and costs the thing pagination is\n * for on a phone: reaching page 7 becomes six taps on \"next\" instead of one\n * tap on `7`. Pass `false` in a mobile-first app and the numbers stay at\n * every width — the row scrolls horizontally by itself rather than pushing\n * the page wider, and the current page is scrolled into view when it\n * changes.\n */\n compactOnMobile?: boolean;\n className?: string;\n}\n\nfunction buildRange(page: number, totalPages: number, siblings: number): (number | \"...\")[] {\n const totalSlots = siblings + 4;\n if (totalPages <= totalSlots) {\n return Array.from({ length: totalPages }, (_, i) => i + 1);\n }\n const start = Math.max(2, page - Math.floor(siblings / 2));\n const end = Math.min(totalPages - 1, start + siblings - 1);\n const range: (number | \"...\")[] = [1];\n if (start > 2) range.push(\"...\");\n for (let i = start; i <= end; i++) range.push(i);\n if (end < totalPages - 1) range.push(\"...\");\n range.push(totalPages);\n return range;\n}\n\n/**\n * Numeric pagination controls. Pair with {@link usePagination} for state.\n *\n * Narrow screens collapse to `‹` / `›` plus the summary by default, which turns\n * random access into sequential access — fine for a desktop-first app, wrong for\n * one whose traffic is phones. `compactOnMobile={false}` keeps the numbers at\n * every width: the row scrolls on its own instead of widening the page, and the\n * effect below keeps the current page inside the visible part of that row.\n *\n * The scroll is why this is one boolean rather than a `variant`. Simply\n * un-hiding the buttons would push a nine-button row past a 360px viewport and\n * give the whole document a horizontal scrollbar — the opt-out has to carry its\n * own layout, so it does.\n */\nexport function Pagination({\n page,\n totalPages,\n onPageChange,\n pageSize,\n onPageSizeChange,\n pageSizeOptions = [10, 25, 50, 100],\n totalItems,\n siblingCount = 3,\n compactOnMobile = true,\n className,\n}: PaginationProps) {\n const pages = useMemo(\n () => buildRange(page, totalPages, siblingCount),\n [page, totalPages, siblingCount],\n );\n const activePage = useRef<HTMLButtonElement | null>(null);\n\n useEffect(() => {\n if (compactOnMobile) return;\n activePage.current?.scrollIntoView?.({ block: \"nearest\", inline: \"nearest\" });\n }, [page, compactOnMobile]);\n\n if (totalPages <= 1 && !onPageSizeChange) return null;\n\n return (\n <div className={cn(styles.wrapper, className)} data-compact={String(compactOnMobile)}>\n <div className={styles.summary}>\n {typeof totalItems === \"number\"\n ? `${totalItems} resultado${totalItems === 1 ? \"\" : \"s\"}`\n : `Página ${page} de ${totalPages}`}\n </div>\n <div className={styles.controls}>\n <button\n type=\"button\"\n className={styles.page}\n onClick={() => onPageChange(Math.max(1, page - 1))}\n disabled={page <= 1}\n aria-label=\"Página anterior\"\n >\n ‹\n </button>\n {pages.map((entry, index) =>\n entry === \"...\" ? (\n <span\n key={`ellipsis-${index}`}\n className={cn(styles.ellipsis, styles.numeric)}\n >\n …\n </span>\n ) : (\n <button\n type=\"button\"\n key={entry}\n ref={entry === page ? activePage : undefined}\n className={cn(\n styles.page,\n styles.numeric,\n entry === page && styles.active,\n )}\n onClick={() => onPageChange(entry)}\n aria-current={entry === page ? \"page\" : undefined}\n >\n {entry}\n </button>\n ),\n )}\n <button\n type=\"button\"\n className={styles.page}\n onClick={() => onPageChange(Math.min(totalPages, page + 1))}\n disabled={page >= totalPages}\n aria-label=\"Próxima página\"\n >\n ›\n </button>\n {onPageSizeChange && (\n <select\n className={styles.sizeSelect}\n value={pageSize}\n onChange={(event) => onPageSizeChange(Number(event.target.value))}\n aria-label=\"Itens por página\"\n >\n {pageSizeOptions.map((opt) => (\n <option key={opt} value={opt}>\n {opt} / página\n </option>\n ))}\n </select>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Pagination.cjs","names":[],"sources":["../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — page/totalPages/onPageChange is the pager,\n * pageSize/onPageSizeChange/ pageSizeOptions is the size control that sits in the\n * same bar, and totalItems plus siblingCount decide the copy and how many page\n * buttons are rendered. They ship together because a footer that pages but cannot\n * resize is half a footer.\n */\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Pagination.module.css\";\n\nexport interface PaginationProps {\n page: number;\n totalPages: number;\n onPageChange: (page: number) => void;\n pageSize?: number;\n onPageSizeChange?: (size: number) => void;\n /**\n * Sizes offered in the selector. Default `[10, 25, 50, 100]`.\n *\n * The current `pageSize` is always offered too, merged in ascending order. A\n * `<select>` whose value matches no option displays its first option, so\n * `pageSize={20}` against the default list used to read \"10 / página\" over a\n * table showing 20 rows.\n */\n pageSizeOptions?: number[];\n /** Total items count; if provided, renders the summary text. */\n totalItems?: number;\n /** Pages kept around the current one; ~`siblingCount + 4` buttons render. Default 3. */\n siblingCount?: number;\n /**\n * Below 640px, drop to `‹` / `›` plus the summary and hide the numbered\n * buttons and the size select. Default `true`.\n *\n * The default suits a desktop-first app, and costs the thing pagination is\n * for on a phone: reaching page 7 becomes six taps on \"next\" instead of one\n * tap on `7`. Pass `false` in a mobile-first app and the numbers stay at\n * every width — the row scrolls horizontally by itself rather than pushing\n * the page wider, and the current page is scrolled into view when it\n * changes.\n */\n compactOnMobile?: boolean;\n className?: string;\n}\n\nconst DEFAULT_PAGE_SIZE_OPTIONS: number[] = [10, 25, 50, 100];\n\nfunction buildRange(page: number, totalPages: number, siblings: number): (number | \"...\")[] {\n const totalSlots = siblings + 4;\n if (totalPages <= totalSlots) {\n return Array.from({ length: totalPages }, (_, i) => i + 1);\n }\n const start = Math.max(2, page - Math.floor(siblings / 2));\n const end = Math.min(totalPages - 1, start + siblings - 1);\n const range: (number | \"...\")[] = [1];\n if (start > 2) range.push(\"...\");\n for (let i = start; i <= end; i++) range.push(i);\n if (end < totalPages - 1) range.push(\"...\");\n range.push(totalPages);\n return range;\n}\n\n/**\n * Numeric pagination controls. Pair with {@link usePagination} for state.\n *\n * Narrow screens collapse to `‹` / `›` plus the summary by default, which turns\n * random access into sequential access — fine for a desktop-first app, wrong for\n * one whose traffic is phones. `compactOnMobile={false}` keeps the numbers at\n * every width: the row scrolls on its own instead of widening the page, and the\n * effect below keeps the current page inside the visible part of that row.\n *\n * The scroll is why this is one boolean rather than a `variant`. Simply\n * un-hiding the buttons would push a nine-button row past a 360px viewport and\n * give the whole document a horizontal scrollbar — the opt-out has to carry its\n * own layout, so it does.\n */\nexport function Pagination({\n page,\n totalPages,\n onPageChange,\n pageSize,\n onPageSizeChange,\n pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,\n totalItems,\n siblingCount = 3,\n compactOnMobile = true,\n className,\n}: PaginationProps) {\n const pages = useMemo(\n () => buildRange(page, totalPages, siblingCount),\n [page, totalPages, siblingCount],\n );\n const sizes = useMemo(\n () =>\n pageSize === undefined || pageSizeOptions.includes(pageSize)\n ? pageSizeOptions\n : [...pageSizeOptions, pageSize].sort((a, b) => a - b),\n [pageSize, pageSizeOptions],\n );\n const activePage = useRef<HTMLButtonElement | null>(null);\n\n useEffect(() => {\n if (compactOnMobile) return;\n activePage.current?.scrollIntoView?.({ block: \"nearest\", inline: \"nearest\" });\n }, [page, compactOnMobile]);\n\n if (totalPages <= 1 && !onPageSizeChange) return null;\n\n return (\n <div className={cn(styles.wrapper, className)} data-compact={String(compactOnMobile)}>\n <div className={styles.summary}>\n {typeof totalItems === \"number\"\n ? `${totalItems} resultado${totalItems === 1 ? \"\" : \"s\"}`\n : `Página ${page} de ${totalPages}`}\n </div>\n <div className={styles.controls}>\n <button\n type=\"button\"\n className={styles.page}\n onClick={() => onPageChange(Math.max(1, page - 1))}\n disabled={page <= 1}\n aria-label=\"Página anterior\"\n >\n ‹\n </button>\n {pages.map((entry, index) =>\n entry === \"...\" ? (\n <span\n key={`ellipsis-${index}`}\n className={cn(styles.ellipsis, styles.numeric)}\n >\n …\n </span>\n ) : (\n <button\n type=\"button\"\n key={entry}\n ref={entry === page ? activePage : undefined}\n className={cn(\n styles.page,\n styles.numeric,\n entry === page && styles.active,\n )}\n onClick={() => onPageChange(entry)}\n aria-current={entry === page ? \"page\" : undefined}\n >\n {entry}\n </button>\n ),\n )}\n <button\n type=\"button\"\n className={styles.page}\n onClick={() => onPageChange(Math.min(totalPages, page + 1))}\n disabled={page >= totalPages}\n aria-label=\"Próxima página\"\n >\n ›\n </button>\n {onPageSizeChange && (\n <select\n className={styles.sizeSelect}\n value={pageSize}\n onChange={(event) => onPageSizeChange(Number(event.target.value))}\n aria-label=\"Itens por página\"\n >\n {sizes.map((opt) => (\n <option key={opt} value={opt}>\n {opt} / página\n </option>\n ))}\n </select>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -3,71 +3,72 @@ import t from "./Pagination.module.js";
3
3
  import { useEffect as n, useMemo as r, useRef as i } from "react";
4
4
  import { jsx as a, jsxs as o } from "react/jsx-runtime";
5
5
  //#region src/components/Pagination/Pagination.tsx
6
- function s(e, t, n) {
6
+ var s = [
7
+ 10,
8
+ 25,
9
+ 50,
10
+ 100
11
+ ];
12
+ function c(e, t, n) {
7
13
  if (t <= n + 4) return Array.from({ length: t }, (e, t) => t + 1);
8
14
  let r = Math.max(2, e - Math.floor(n / 2)), i = Math.min(t - 1, r + n - 1), a = [1];
9
15
  r > 2 && a.push("...");
10
16
  for (let e = r; e <= i; e++) a.push(e);
11
17
  return i < t - 1 && a.push("..."), a.push(t), a;
12
18
  }
13
- function c({ page: c, totalPages: l, onPageChange: u, pageSize: d, onPageSizeChange: f, pageSizeOptions: p = [
14
- 10,
15
- 25,
16
- 50,
17
- 100
18
- ], totalItems: m, siblingCount: h = 3, compactOnMobile: g = !0, className: _ }) {
19
- let v = r(() => s(c, l, h), [
20
- c,
19
+ function l({ page: l, totalPages: u, onPageChange: d, pageSize: f, onPageSizeChange: p, pageSizeOptions: m = s, totalItems: h, siblingCount: g = 3, compactOnMobile: _ = !0, className: v }) {
20
+ let y = r(() => c(l, u, g), [
21
21
  l,
22
- h
23
- ]), y = i(null);
22
+ u,
23
+ g
24
+ ]), b = r(() => f === void 0 || m.includes(f) ? m : [...m, f].sort((e, t) => e - t), [f, m]), x = i(null);
24
25
  return n(() => {
25
- g || y.current?.scrollIntoView?.({
26
+ _ || x.current?.scrollIntoView?.({
26
27
  block: "nearest",
27
28
  inline: "nearest"
28
29
  });
29
- }, [c, g]), l <= 1 && !f ? null : /* @__PURE__ */ o("div", {
30
- className: e(t.wrapper, _),
31
- "data-compact": String(g),
30
+ }, [l, _]), u <= 1 && !p ? null : /* @__PURE__ */ o("div", {
31
+ className: e(t.wrapper, v),
32
+ "data-compact": String(_),
32
33
  children: [/* @__PURE__ */ a("div", {
33
34
  className: t.summary,
34
- children: typeof m == "number" ? `${m} resultado${m === 1 ? "" : "s"}` : `Página ${c} de ${l}`
35
+ children: typeof h == "number" ? `${h} resultado${h === 1 ? "" : "s"}` : `Página ${l} de ${u}`
35
36
  }), /* @__PURE__ */ o("div", {
36
37
  className: t.controls,
37
38
  children: [
38
39
  /* @__PURE__ */ a("button", {
39
40
  type: "button",
40
41
  className: t.page,
41
- onClick: () => u(Math.max(1, c - 1)),
42
- disabled: c <= 1,
42
+ onClick: () => d(Math.max(1, l - 1)),
43
+ disabled: l <= 1,
43
44
  "aria-label": "Página anterior",
44
45
  children: "‹"
45
46
  }),
46
- v.map((n, r) => n === "..." ? /* @__PURE__ */ a("span", {
47
+ y.map((n, r) => n === "..." ? /* @__PURE__ */ a("span", {
47
48
  className: e(t.ellipsis, t.numeric),
48
49
  children: "…"
49
50
  }, `ellipsis-${r}`) : /* @__PURE__ */ a("button", {
50
51
  type: "button",
51
- ref: n === c ? y : void 0,
52
- className: e(t.page, t.numeric, n === c && t.active),
53
- onClick: () => u(n),
54
- "aria-current": n === c ? "page" : void 0,
52
+ ref: n === l ? x : void 0,
53
+ className: e(t.page, t.numeric, n === l && t.active),
54
+ onClick: () => d(n),
55
+ "aria-current": n === l ? "page" : void 0,
55
56
  children: n
56
57
  }, n)),
57
58
  /* @__PURE__ */ a("button", {
58
59
  type: "button",
59
60
  className: t.page,
60
- onClick: () => u(Math.min(l, c + 1)),
61
- disabled: c >= l,
61
+ onClick: () => d(Math.min(u, l + 1)),
62
+ disabled: l >= u,
62
63
  "aria-label": "Próxima página",
63
64
  children: "›"
64
65
  }),
65
- f && /* @__PURE__ */ a("select", {
66
+ p && /* @__PURE__ */ a("select", {
66
67
  className: t.sizeSelect,
67
- value: d,
68
- onChange: (e) => f(Number(e.target.value)),
68
+ value: f,
69
+ onChange: (e) => p(Number(e.target.value)),
69
70
  "aria-label": "Itens por página",
70
- children: p.map((e) => /* @__PURE__ */ o("option", {
71
+ children: b.map((e) => /* @__PURE__ */ o("option", {
71
72
  value: e,
72
73
  children: [e, " / página"]
73
74
  }, e))
@@ -77,6 +78,6 @@ function c({ page: c, totalPages: l, onPageChange: u, pageSize: d, onPageSizeCha
77
78
  });
78
79
  }
79
80
  //#endregion
80
- export { c as Pagination };
81
+ export { l as Pagination };
81
82
 
82
83
  //# sourceMappingURL=Pagination.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Pagination.js","names":[],"sources":["../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — page/totalPages/onPageChange is the pager,\n * pageSize/onPageSizeChange/ pageSizeOptions is the size control that sits in the\n * same bar, and totalItems plus siblingCount decide the copy and how many page\n * buttons are rendered. They ship together because a footer that pages but cannot\n * resize is half a footer.\n */\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Pagination.module.css\";\n\nexport interface PaginationProps {\n page: number;\n totalPages: number;\n onPageChange: (page: number) => void;\n pageSize?: number;\n onPageSizeChange?: (size: number) => void;\n pageSizeOptions?: number[];\n /** Total items count; if provided, renders the summary text. */\n totalItems?: number;\n /** Pages kept around the current one; ~`siblingCount + 4` buttons render. Default 3. */\n siblingCount?: number;\n /**\n * Below 640px, drop to `‹` / `›` plus the summary and hide the numbered\n * buttons and the size select. Default `true`.\n *\n * The default suits a desktop-first app, and costs the thing pagination is\n * for on a phone: reaching page 7 becomes six taps on \"next\" instead of one\n * tap on `7`. Pass `false` in a mobile-first app and the numbers stay at\n * every width — the row scrolls horizontally by itself rather than pushing\n * the page wider, and the current page is scrolled into view when it\n * changes.\n */\n compactOnMobile?: boolean;\n className?: string;\n}\n\nfunction buildRange(page: number, totalPages: number, siblings: number): (number | \"...\")[] {\n const totalSlots = siblings + 4;\n if (totalPages <= totalSlots) {\n return Array.from({ length: totalPages }, (_, i) => i + 1);\n }\n const start = Math.max(2, page - Math.floor(siblings / 2));\n const end = Math.min(totalPages - 1, start + siblings - 1);\n const range: (number | \"...\")[] = [1];\n if (start > 2) range.push(\"...\");\n for (let i = start; i <= end; i++) range.push(i);\n if (end < totalPages - 1) range.push(\"...\");\n range.push(totalPages);\n return range;\n}\n\n/**\n * Numeric pagination controls. Pair with {@link usePagination} for state.\n *\n * Narrow screens collapse to `‹` / `›` plus the summary by default, which turns\n * random access into sequential access — fine for a desktop-first app, wrong for\n * one whose traffic is phones. `compactOnMobile={false}` keeps the numbers at\n * every width: the row scrolls on its own instead of widening the page, and the\n * effect below keeps the current page inside the visible part of that row.\n *\n * The scroll is why this is one boolean rather than a `variant`. Simply\n * un-hiding the buttons would push a nine-button row past a 360px viewport and\n * give the whole document a horizontal scrollbar — the opt-out has to carry its\n * own layout, so it does.\n */\nexport function Pagination({\n page,\n totalPages,\n onPageChange,\n pageSize,\n onPageSizeChange,\n pageSizeOptions = [10, 25, 50, 100],\n totalItems,\n siblingCount = 3,\n compactOnMobile = true,\n className,\n}: PaginationProps) {\n const pages = useMemo(\n () => buildRange(page, totalPages, siblingCount),\n [page, totalPages, siblingCount],\n );\n const activePage = useRef<HTMLButtonElement | null>(null);\n\n useEffect(() => {\n if (compactOnMobile) return;\n activePage.current?.scrollIntoView?.({ block: \"nearest\", inline: \"nearest\" });\n }, [page, compactOnMobile]);\n\n if (totalPages <= 1 && !onPageSizeChange) return null;\n\n return (\n <div className={cn(styles.wrapper, className)} data-compact={String(compactOnMobile)}>\n <div className={styles.summary}>\n {typeof totalItems === \"number\"\n ? `${totalItems} resultado${totalItems === 1 ? \"\" : \"s\"}`\n : `Página ${page} de ${totalPages}`}\n </div>\n <div className={styles.controls}>\n <button\n type=\"button\"\n className={styles.page}\n onClick={() => onPageChange(Math.max(1, page - 1))}\n disabled={page <= 1}\n aria-label=\"Página anterior\"\n >\n ‹\n </button>\n {pages.map((entry, index) =>\n entry === \"...\" ? (\n <span\n key={`ellipsis-${index}`}\n className={cn(styles.ellipsis, styles.numeric)}\n >\n …\n </span>\n ) : (\n <button\n type=\"button\"\n key={entry}\n ref={entry === page ? activePage : undefined}\n className={cn(\n styles.page,\n styles.numeric,\n entry === page && styles.active,\n )}\n onClick={() => onPageChange(entry)}\n aria-current={entry === page ? \"page\" : undefined}\n >\n {entry}\n </button>\n ),\n )}\n <button\n type=\"button\"\n className={styles.page}\n onClick={() => onPageChange(Math.min(totalPages, page + 1))}\n disabled={page >= totalPages}\n aria-label=\"Próxima página\"\n >\n ›\n </button>\n {onPageSizeChange && (\n <select\n className={styles.sizeSelect}\n value={pageSize}\n onChange={(event) => onPageSizeChange(Number(event.target.value))}\n aria-label=\"Itens por página\"\n >\n {pageSizeOptions.map((opt) => (\n <option key={opt} value={opt}>\n {opt} / página\n </option>\n ))}\n </select>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;AAqCA,SAAS,EAAW,GAAc,GAAoB,GAAsC;CAExF,IAAI,KADe,IAAW,GAE1B,OAAO,MAAM,KAAK,EAAE,QAAQ,EAAW,IAAI,GAAG,MAAM,IAAI,CAAC;CAE7D,IAAM,IAAQ,KAAK,IAAI,GAAG,IAAO,KAAK,MAAM,IAAW,CAAC,CAAC,GACnD,IAAM,KAAK,IAAI,IAAa,GAAG,IAAQ,IAAW,CAAC,GACnD,IAA4B,CAAC,CAAC;CACpC,AAAI,IAAQ,KAAG,EAAM,KAAK,KAAK;CAC/B,KAAK,IAAI,IAAI,GAAO,KAAK,GAAK,KAAK,EAAM,KAAK,CAAC;CAG/C,OAFI,IAAM,IAAa,KAAG,EAAM,KAAK,KAAK,GAC1C,EAAM,KAAK,CAAU,GACd;AACX;AAgBA,SAAgB,EAAW,EACvB,SACA,eACA,iBACA,aACA,qBACA,qBAAkB;CAAC;CAAI;CAAI;CAAI;AAAG,GAClC,eACA,kBAAe,GACf,qBAAkB,IAClB,gBACgB;CAChB,IAAM,IAAQ,QACJ,EAAW,GAAM,GAAY,CAAY,GAC/C;EAAC;EAAM;EAAY;CAAY,CACnC,GACM,IAAa,EAAiC,IAAI;CASxD,OAPA,QAAgB;EACR,KACJ,EAAW,SAAS,iBAAiB;GAAE,OAAO;GAAW,QAAQ;EAAU,CAAC;CAChF,GAAG,CAAC,GAAM,CAAe,CAAC,GAEtB,KAAc,KAAK,CAAC,IAAyB,OAG7C,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,SAAS,CAAS;EAAG,gBAAc,OAAO,CAAe;EAAnF,UAAA,CACI,kBAAC,OAAD;GAAK,WAAW,EAAO;GAClB,UAAA,OAAO,KAAe,WACjB,GAAG,EAAW,YAAY,MAAe,IAAI,KAAK,QAClD,UAAU,EAAK,MAAM;EAC1B,CAAA,GACL,kBAAC,OAAD;GAAK,WAAW,EAAO;GAAvB,UAAA;IACI,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,eAAe,EAAa,KAAK,IAAI,GAAG,IAAO,CAAC,CAAC;KACjD,UAAU,KAAQ;KAClB,cAAW;KACd,UAAA;IAEO,CAAA;IACP,EAAM,KAAK,GAAO,MACf,MAAU,QACN,kBAAC,QAAD;KAEI,WAAW,EAAG,EAAO,UAAU,EAAO,OAAO;KAChD,UAAA;IAEK,GAJG,YAAY,GAIf,IAEN,kBAAC,UAAD;KACI,MAAK;KAEL,KAAK,MAAU,IAAO,IAAa,KAAA;KACnC,WAAW,EACP,EAAO,MACP,EAAO,SACP,MAAU,KAAQ,EAAO,MAC7B;KACA,eAAe,EAAa,CAAK;KACjC,gBAAc,MAAU,IAAO,SAAS,KAAA;KAEvC,UAAA;IACG,GAXC,CAWD,CAEhB;IACA,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,eAAe,EAAa,KAAK,IAAI,GAAY,IAAO,CAAC,CAAC;KAC1D,UAAU,KAAQ;KAClB,cAAW;KACd,UAAA;IAEO,CAAA;IACP,KACG,kBAAC,UAAD;KACI,WAAW,EAAO;KAClB,OAAO;KACP,WAAW,MAAU,EAAiB,OAAO,EAAM,OAAO,KAAK,CAAC;KAChE,cAAW;KAEV,UAAA,EAAgB,KAAK,MAClB,kBAAC,UAAD;MAAkB,OAAO;MAAzB,UAAA,CACK,GAAI,WACD;KAFK,GAAA,CAEL,CACX;IACG,CAAA;GAEX;EACJ,CAAA,CAAA;;AAEb"}
1
+ {"version":3,"file":"Pagination.js","names":[],"sources":["../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — page/totalPages/onPageChange is the pager,\n * pageSize/onPageSizeChange/ pageSizeOptions is the size control that sits in the\n * same bar, and totalItems plus siblingCount decide the copy and how many page\n * buttons are rendered. They ship together because a footer that pages but cannot\n * resize is half a footer.\n */\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Pagination.module.css\";\n\nexport interface PaginationProps {\n page: number;\n totalPages: number;\n onPageChange: (page: number) => void;\n pageSize?: number;\n onPageSizeChange?: (size: number) => void;\n /**\n * Sizes offered in the selector. Default `[10, 25, 50, 100]`.\n *\n * The current `pageSize` is always offered too, merged in ascending order. A\n * `<select>` whose value matches no option displays its first option, so\n * `pageSize={20}` against the default list used to read \"10 / página\" over a\n * table showing 20 rows.\n */\n pageSizeOptions?: number[];\n /** Total items count; if provided, renders the summary text. */\n totalItems?: number;\n /** Pages kept around the current one; ~`siblingCount + 4` buttons render. Default 3. */\n siblingCount?: number;\n /**\n * Below 640px, drop to `‹` / `›` plus the summary and hide the numbered\n * buttons and the size select. Default `true`.\n *\n * The default suits a desktop-first app, and costs the thing pagination is\n * for on a phone: reaching page 7 becomes six taps on \"next\" instead of one\n * tap on `7`. Pass `false` in a mobile-first app and the numbers stay at\n * every width — the row scrolls horizontally by itself rather than pushing\n * the page wider, and the current page is scrolled into view when it\n * changes.\n */\n compactOnMobile?: boolean;\n className?: string;\n}\n\nconst DEFAULT_PAGE_SIZE_OPTIONS: number[] = [10, 25, 50, 100];\n\nfunction buildRange(page: number, totalPages: number, siblings: number): (number | \"...\")[] {\n const totalSlots = siblings + 4;\n if (totalPages <= totalSlots) {\n return Array.from({ length: totalPages }, (_, i) => i + 1);\n }\n const start = Math.max(2, page - Math.floor(siblings / 2));\n const end = Math.min(totalPages - 1, start + siblings - 1);\n const range: (number | \"...\")[] = [1];\n if (start > 2) range.push(\"...\");\n for (let i = start; i <= end; i++) range.push(i);\n if (end < totalPages - 1) range.push(\"...\");\n range.push(totalPages);\n return range;\n}\n\n/**\n * Numeric pagination controls. Pair with {@link usePagination} for state.\n *\n * Narrow screens collapse to `‹` / `›` plus the summary by default, which turns\n * random access into sequential access — fine for a desktop-first app, wrong for\n * one whose traffic is phones. `compactOnMobile={false}` keeps the numbers at\n * every width: the row scrolls on its own instead of widening the page, and the\n * effect below keeps the current page inside the visible part of that row.\n *\n * The scroll is why this is one boolean rather than a `variant`. Simply\n * un-hiding the buttons would push a nine-button row past a 360px viewport and\n * give the whole document a horizontal scrollbar — the opt-out has to carry its\n * own layout, so it does.\n */\nexport function Pagination({\n page,\n totalPages,\n onPageChange,\n pageSize,\n onPageSizeChange,\n pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,\n totalItems,\n siblingCount = 3,\n compactOnMobile = true,\n className,\n}: PaginationProps) {\n const pages = useMemo(\n () => buildRange(page, totalPages, siblingCount),\n [page, totalPages, siblingCount],\n );\n const sizes = useMemo(\n () =>\n pageSize === undefined || pageSizeOptions.includes(pageSize)\n ? pageSizeOptions\n : [...pageSizeOptions, pageSize].sort((a, b) => a - b),\n [pageSize, pageSizeOptions],\n );\n const activePage = useRef<HTMLButtonElement | null>(null);\n\n useEffect(() => {\n if (compactOnMobile) return;\n activePage.current?.scrollIntoView?.({ block: \"nearest\", inline: \"nearest\" });\n }, [page, compactOnMobile]);\n\n if (totalPages <= 1 && !onPageSizeChange) return null;\n\n return (\n <div className={cn(styles.wrapper, className)} data-compact={String(compactOnMobile)}>\n <div className={styles.summary}>\n {typeof totalItems === \"number\"\n ? `${totalItems} resultado${totalItems === 1 ? \"\" : \"s\"}`\n : `Página ${page} de ${totalPages}`}\n </div>\n <div className={styles.controls}>\n <button\n type=\"button\"\n className={styles.page}\n onClick={() => onPageChange(Math.max(1, page - 1))}\n disabled={page <= 1}\n aria-label=\"Página anterior\"\n >\n ‹\n </button>\n {pages.map((entry, index) =>\n entry === \"...\" ? (\n <span\n key={`ellipsis-${index}`}\n className={cn(styles.ellipsis, styles.numeric)}\n >\n …\n </span>\n ) : (\n <button\n type=\"button\"\n key={entry}\n ref={entry === page ? activePage : undefined}\n className={cn(\n styles.page,\n styles.numeric,\n entry === page && styles.active,\n )}\n onClick={() => onPageChange(entry)}\n aria-current={entry === page ? \"page\" : undefined}\n >\n {entry}\n </button>\n ),\n )}\n <button\n type=\"button\"\n className={styles.page}\n onClick={() => onPageChange(Math.min(totalPages, page + 1))}\n disabled={page >= totalPages}\n aria-label=\"Próxima página\"\n >\n ›\n </button>\n {onPageSizeChange && (\n <select\n className={styles.sizeSelect}\n value={pageSize}\n onChange={(event) => onPageSizeChange(Number(event.target.value))}\n aria-label=\"Itens por página\"\n >\n {sizes.map((opt) => (\n <option key={opt} value={opt}>\n {opt} / página\n </option>\n ))}\n </select>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;AA6CA,IAAM,IAAsC;CAAC;CAAI;CAAI;CAAI;AAAG;AAE5D,SAAS,EAAW,GAAc,GAAoB,GAAsC;CAExF,IAAI,KADe,IAAW,GAE1B,OAAO,MAAM,KAAK,EAAE,QAAQ,EAAW,IAAI,GAAG,MAAM,IAAI,CAAC;CAE7D,IAAM,IAAQ,KAAK,IAAI,GAAG,IAAO,KAAK,MAAM,IAAW,CAAC,CAAC,GACnD,IAAM,KAAK,IAAI,IAAa,GAAG,IAAQ,IAAW,CAAC,GACnD,IAA4B,CAAC,CAAC;CACpC,AAAI,IAAQ,KAAG,EAAM,KAAK,KAAK;CAC/B,KAAK,IAAI,IAAI,GAAO,KAAK,GAAK,KAAK,EAAM,KAAK,CAAC;CAG/C,OAFI,IAAM,IAAa,KAAG,EAAM,KAAK,KAAK,GAC1C,EAAM,KAAK,CAAU,GACd;AACX;AAgBA,SAAgB,EAAW,EACvB,SACA,eACA,iBACA,aACA,qBACA,qBAAkB,GAClB,eACA,kBAAe,GACf,qBAAkB,IAClB,gBACgB;CAChB,IAAM,IAAQ,QACJ,EAAW,GAAM,GAAY,CAAY,GAC/C;EAAC;EAAM;EAAY;CAAY,CACnC,GACM,IAAQ,QAEN,MAAa,KAAA,KAAa,EAAgB,SAAS,CAAQ,IACrD,IACA,CAAC,GAAG,GAAiB,CAAQ,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC,GAC7D,CAAC,GAAU,CAAe,CAC9B,GACM,IAAa,EAAiC,IAAI;CASxD,OAPA,QAAgB;EACR,KACJ,EAAW,SAAS,iBAAiB;GAAE,OAAO;GAAW,QAAQ;EAAU,CAAC;CAChF,GAAG,CAAC,GAAM,CAAe,CAAC,GAEtB,KAAc,KAAK,CAAC,IAAyB,OAG7C,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,SAAS,CAAS;EAAG,gBAAc,OAAO,CAAe;EAAnF,UAAA,CACI,kBAAC,OAAD;GAAK,WAAW,EAAO;GAClB,UAAA,OAAO,KAAe,WACjB,GAAG,EAAW,YAAY,MAAe,IAAI,KAAK,QAClD,UAAU,EAAK,MAAM;EAC1B,CAAA,GACL,kBAAC,OAAD;GAAK,WAAW,EAAO;GAAvB,UAAA;IACI,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,eAAe,EAAa,KAAK,IAAI,GAAG,IAAO,CAAC,CAAC;KACjD,UAAU,KAAQ;KAClB,cAAW;KACd,UAAA;IAEO,CAAA;IACP,EAAM,KAAK,GAAO,MACf,MAAU,QACN,kBAAC,QAAD;KAEI,WAAW,EAAG,EAAO,UAAU,EAAO,OAAO;KAChD,UAAA;IAEK,GAJG,YAAY,GAIf,IAEN,kBAAC,UAAD;KACI,MAAK;KAEL,KAAK,MAAU,IAAO,IAAa,KAAA;KACnC,WAAW,EACP,EAAO,MACP,EAAO,SACP,MAAU,KAAQ,EAAO,MAC7B;KACA,eAAe,EAAa,CAAK;KACjC,gBAAc,MAAU,IAAO,SAAS,KAAA;KAEvC,UAAA;IACG,GAXC,CAWD,CAEhB;IACA,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,eAAe,EAAa,KAAK,IAAI,GAAY,IAAO,CAAC,CAAC;KAC1D,UAAU,KAAQ;KAClB,cAAW;KACd,UAAA;IAEO,CAAA;IACP,KACG,kBAAC,UAAD;KACI,WAAW,EAAO;KAClB,OAAO;KACP,WAAW,MAAU,EAAiB,OAAO,EAAM,OAAO,KAAK,CAAC;KAChE,cAAW;KAEV,UAAA,EAAM,KAAK,MACR,kBAAC,UAAD;MAAkB,OAAO;MAAzB,UAAA,CACK,GAAI,WACD;KAFK,GAAA,CAEL,CACX;IACG,CAAA;GAEX;EACJ,CAAA,CAAA;;AAEb"}
@@ -1,2 +1,2 @@
1
- const e=require("../../hooks/use-focus-trap.cjs");let t=require("react");function n(t){return Array.from(t.querySelectorAll(e.FOCUSABLE_SELECTOR)).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 r({enabled:e,anchor:r,panel:i}){(0,t.useEffect)(()=>{if(!e||!r||!i)return;let t=(e,t)=>{t&&(e.preventDefault(),t.focus())},a=e=>{if(e.key!==`Tab`||e.defaultPrevented)return;let a=n(i);if(a.length===0)return;let o=document.activeElement;if(!o)return;let s=n(document).filter(e=>!i.contains(e)),c=s.filter(e=>r.contains(e)).at(-1),l=c?s[s.indexOf(c)+1]:void 0;if(i.contains(o)){!e.shiftKey&&o===a.at(-1)&&t(e,l),e.shiftKey&&o===a[0]&&t(e,c);return}!e.shiftKey&&o===c&&t(e,a[0]),e.shiftKey&&l&&o===l&&t(e,a.at(-1))};return document.addEventListener(`keydown`,a,!0),()=>document.removeEventListener(`keydown`,a,!0)},[e,r,i])}exports.usePortalTabOrder=r;
1
+ const e=require("../../hooks/tab-sequence.cjs");let t=require("react");function n({enabled:n,anchor:r,panel:i}){(0,t.useEffect)(()=>{if(!n||!r||!i)return;let t=(e,t)=>{t&&(e.preventDefault(),t.focus())},a=n=>{if(n.key!==`Tab`||n.defaultPrevented)return;let a=e.tabbables(i);if(a.length===0)return;let o=document.activeElement;if(!o)return;let s=e.tabbables(document).filter(e=>!i.contains(e)),c=s.filter(e=>r.contains(e)).at(-1),l=c?s[s.indexOf(c)+1]:void 0;if(i.contains(o)){!n.shiftKey&&o===a.at(-1)&&t(n,l),n.shiftKey&&o===a[0]&&t(n,c);return}!n.shiftKey&&o===c&&t(n,a[0]),n.shiftKey&&l&&o===l&&t(n,a.at(-1))},o=e.registerPortalLayer({anchor:r,panel:i});return document.addEventListener(`keydown`,a,!0),()=>{o(),document.removeEventListener(`keydown`,a,!0)}},[n,r,i])}exports.usePortalTabOrder=n;
2
2
  //# sourceMappingURL=tab-bridge.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tab-bridge.cjs","names":[],"sources":["../../../src/components/Portal/tab-bridge.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { FOCUSABLE_SELECTOR } from \"@/hooks/use-focus-trap\";\n\n/** Options of {@link usePortalTabOrder}. */\nexport interface PortalTabOrderOptions {\n /** Whether the panel is open and portalled. */\n enabled: boolean;\n /** Wrapper of the trigger — the spot in the page the panel belongs to. */\n anchor: HTMLElement | null;\n /** The portalled panel. */\n panel: HTMLElement | null;\n}\n\n/**\n * 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 */\nfunction 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 * Put a portalled panel back in the tab order, right after its trigger.\n *\n * A portal moves the panel to the end of `body`, so the browser's own order goes\n * from the trigger straight to whatever follows it in the page and reaches the\n * panel last, if ever. Measured in Chrome on a `Popover` holding two inputs:\n * in flow, `Tab` from the open trigger landed on the first input; in a portal it\n * skipped the panel and went to the next button of the page.\n *\n * While the panel is open, this restores the in-flow order:\n *\n * - `Tab` on the last stop of the anchor enters the panel's first stop.\n * - `Tab` on the panel's last stop leaves to the first stop after the anchor.\n * - `Shift+Tab` on the panel's first stop returns to the anchor.\n * - `Shift+Tab` on the first stop after the anchor enters the panel's last stop.\n *\n * Listened on `document` in the capture phase and marked consumed, so it runs\n * before an enclosing `Modal`'s focus trap, which skips a consumed `Tab`.\n *\n * @param options - Whether it is active, the anchor and the panel.\n * @returns Nothing.\n */\nexport function usePortalTabOrder({ enabled, anchor, panel }: PortalTabOrderOptions): void {\n useEffect(() => {\n if (!enabled || !anchor || !panel) return;\n\n const move = (event: KeyboardEvent, target: HTMLElement | undefined): void => {\n if (!target) return;\n event.preventDefault();\n target.focus();\n };\n\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key !== \"Tab\" || event.defaultPrevented) return;\n const inPanel = tabbables(panel);\n if (inPanel.length === 0) return;\n const active = document.activeElement as HTMLElement | null;\n if (!active) return;\n const page = tabbables(document).filter((el) => !panel.contains(el));\n const inAnchor = page.filter((el) => anchor.contains(el));\n const lastOfAnchor = inAnchor.at(-1);\n const afterAnchor = lastOfAnchor ? page[page.indexOf(lastOfAnchor) + 1] : undefined;\n\n if (panel.contains(active)) {\n if (!event.shiftKey && active === inPanel.at(-1)) move(event, afterAnchor);\n if (event.shiftKey && active === inPanel[0]) move(event, lastOfAnchor);\n return;\n }\n if (!event.shiftKey && active === lastOfAnchor) move(event, inPanel[0]);\n if (event.shiftKey && afterAnchor && active === afterAnchor) {\n move(event, inPanel.at(-1));\n }\n };\n\n document.addEventListener(\"keydown\", onKeyDown, true);\n return () => document.removeEventListener(\"keydown\", onKeyDown, true);\n }, [enabled, anchor, panel]);\n}\n"],"mappings":"yEAoBA,SAAS,EAAU,EAAiC,CAChD,OAAO,MAAM,KAAK,EAAK,iBAA8B,EAAA,kBAAkB,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,CAwBA,SAAgB,EAAkB,CAAE,UAAS,SAAQ,SAAsC,EACvF,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,GAAW,CAAC,GAAU,CAAC,EAAO,OAEnC,IAAM,GAAQ,EAAsB,IAA0C,CACrE,IACL,EAAM,eAAe,EACrB,EAAO,MAAM,EACjB,EAEM,EAAa,GAA+B,CAC9C,GAAI,EAAM,MAAQ,OAAS,EAAM,iBAAkB,OACnD,IAAM,EAAU,EAAU,CAAK,EAC/B,GAAI,EAAQ,SAAW,EAAG,OAC1B,IAAM,EAAS,SAAS,cACxB,GAAI,CAAC,EAAQ,OACb,IAAM,EAAO,EAAU,QAAQ,CAAC,CAAC,OAAQ,GAAO,CAAC,EAAM,SAAS,CAAE,CAAC,EAE7D,EADW,EAAK,OAAQ,GAAO,EAAO,SAAS,CAAE,CAClC,CAAA,CAAS,GAAG,EAAE,EAC7B,EAAc,EAAe,EAAK,EAAK,QAAQ,CAAY,EAAI,GAAK,IAAA,GAE1E,GAAI,EAAM,SAAS,CAAM,EAAG,CACpB,CAAC,EAAM,UAAY,IAAW,EAAQ,GAAG,EAAE,GAAG,EAAK,EAAO,CAAW,EACrE,EAAM,UAAY,IAAW,EAAQ,IAAI,EAAK,EAAO,CAAY,EACrE,MACJ,CACI,CAAC,EAAM,UAAY,IAAW,GAAc,EAAK,EAAO,EAAQ,EAAE,EAClE,EAAM,UAAY,GAAe,IAAW,GAC5C,EAAK,EAAO,EAAQ,GAAG,EAAE,CAAC,CAElC,EAGA,OADA,SAAS,iBAAiB,UAAW,EAAW,EAAI,MACvC,SAAS,oBAAoB,UAAW,EAAW,EAAI,CACxE,EAAG,CAAC,EAAS,EAAQ,CAAK,CAAC,CAC/B"}
1
+ {"version":3,"file":"tab-bridge.cjs","names":[],"sources":["../../../src/components/Portal/tab-bridge.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { registerPortalLayer, tabbables } from \"@/hooks/tab-sequence\";\n\n/** Options of {@link usePortalTabOrder}. */\nexport interface PortalTabOrderOptions {\n /** Whether the panel is open and portalled. */\n enabled: boolean;\n /** Wrapper of the trigger — the spot in the page the panel belongs to. */\n anchor: HTMLElement | null;\n /** The portalled panel. */\n panel: HTMLElement | null;\n}\n\n/**\n * Put a portalled panel back in the tab order, right after its trigger.\n *\n * A portal moves the panel to the end of `body`, so the browser's own order goes\n * from the trigger straight to whatever follows it in the page and reaches the\n * panel last, if ever. Measured in Chrome on a `Popover` holding two inputs:\n * in flow, `Tab` from the open trigger landed on the first input; in a portal it\n * skipped the panel and went to the next button of the page.\n *\n * While the panel is open, this restores the in-flow order:\n *\n * - `Tab` on the last stop of the anchor enters the panel's first stop.\n * - `Tab` on the panel's last stop leaves to the first stop after the anchor.\n * - `Shift+Tab` on the panel's first stop returns to the anchor.\n * - `Shift+Tab` on the first stop after the anchor enters the panel's last stop.\n *\n * Listened on `document` in the capture phase and marked consumed, so it runs\n * before an enclosing `Modal`'s focus trap, which skips a consumed `Tab`. The\n * panel is also registered as a layer of its anchor, which is how that trap\n * knows the panel belongs inside it although the portal put it outside.\n *\n * @param options - Whether it is active, the anchor and the panel.\n * @returns Nothing.\n */\nexport function usePortalTabOrder({ enabled, anchor, panel }: PortalTabOrderOptions): void {\n useEffect(() => {\n if (!enabled || !anchor || !panel) return;\n\n const move = (event: KeyboardEvent, target: HTMLElement | undefined): void => {\n if (!target) return;\n event.preventDefault();\n target.focus();\n };\n\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key !== \"Tab\" || event.defaultPrevented) return;\n const inPanel = tabbables(panel);\n if (inPanel.length === 0) return;\n const active = document.activeElement as HTMLElement | null;\n if (!active) return;\n const page = tabbables(document).filter((el) => !panel.contains(el));\n const inAnchor = page.filter((el) => anchor.contains(el));\n const lastOfAnchor = inAnchor.at(-1);\n const afterAnchor = lastOfAnchor ? page[page.indexOf(lastOfAnchor) + 1] : undefined;\n\n if (panel.contains(active)) {\n if (!event.shiftKey && active === inPanel.at(-1)) move(event, afterAnchor);\n if (event.shiftKey && active === inPanel[0]) move(event, lastOfAnchor);\n return;\n }\n if (!event.shiftKey && active === lastOfAnchor) move(event, inPanel[0]);\n if (event.shiftKey && afterAnchor && active === afterAnchor) {\n move(event, inPanel.at(-1));\n }\n };\n\n const unregister = registerPortalLayer({ anchor, panel });\n document.addEventListener(\"keydown\", onKeyDown, true);\n return () => {\n unregister();\n document.removeEventListener(\"keydown\", onKeyDown, true);\n };\n }, [enabled, anchor, panel]);\n}\n"],"mappings":"uEAqCA,SAAgB,EAAkB,CAAE,UAAS,SAAQ,SAAsC,EACvF,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,GAAW,CAAC,GAAU,CAAC,EAAO,OAEnC,IAAM,GAAQ,EAAsB,IAA0C,CACrE,IACL,EAAM,eAAe,EACrB,EAAO,MAAM,EACjB,EAEM,EAAa,GAA+B,CAC9C,GAAI,EAAM,MAAQ,OAAS,EAAM,iBAAkB,OACnD,IAAM,EAAU,EAAA,UAAU,CAAK,EAC/B,GAAI,EAAQ,SAAW,EAAG,OAC1B,IAAM,EAAS,SAAS,cACxB,GAAI,CAAC,EAAQ,OACb,IAAM,EAAO,EAAA,UAAU,QAAQ,CAAC,CAAC,OAAQ,GAAO,CAAC,EAAM,SAAS,CAAE,CAAC,EAE7D,EADW,EAAK,OAAQ,GAAO,EAAO,SAAS,CAAE,CAClC,CAAA,CAAS,GAAG,EAAE,EAC7B,EAAc,EAAe,EAAK,EAAK,QAAQ,CAAY,EAAI,GAAK,IAAA,GAE1E,GAAI,EAAM,SAAS,CAAM,EAAG,CACpB,CAAC,EAAM,UAAY,IAAW,EAAQ,GAAG,EAAE,GAAG,EAAK,EAAO,CAAW,EACrE,EAAM,UAAY,IAAW,EAAQ,IAAI,EAAK,EAAO,CAAY,EACrE,MACJ,CACI,CAAC,EAAM,UAAY,IAAW,GAAc,EAAK,EAAO,EAAQ,EAAE,EAClE,EAAM,UAAY,GAAe,IAAW,GAC5C,EAAK,EAAO,EAAQ,GAAG,EAAE,CAAC,CAElC,EAEM,EAAa,EAAA,oBAAoB,CAAE,SAAQ,OAAM,CAAC,EAExD,OADA,SAAS,iBAAiB,UAAW,EAAW,EAAI,MACvC,CACT,EAAW,EACX,SAAS,oBAAoB,UAAW,EAAW,EAAI,CAC3D,CACJ,EAAG,CAAC,EAAS,EAAQ,CAAK,CAAC,CAC/B"}
@@ -1,36 +1,34 @@
1
- import { FOCUSABLE_SELECTOR as e } from "../../hooks/use-focus-trap.js";
2
- import { useEffect as t } from "react";
1
+ import { registerPortalLayer as e, tabbables as t } from "../../hooks/tab-sequence.js";
2
+ import { useEffect as n } from "react";
3
3
  //#region src/components/Portal/tab-bridge.ts
4
- function n(t) {
5
- return Array.from(t.querySelectorAll(e)).filter((e) => {
6
- if (e.tabIndex < 0 || e.hasAttribute("aria-hidden")) return !1;
7
- let t = window.getComputedStyle(e);
8
- return t.display !== "none" && t.visibility !== "hidden";
9
- });
10
- }
11
- function r({ enabled: e, anchor: r, panel: i }) {
12
- t(() => {
13
- if (!e || !r || !i) return;
14
- let t = (e, t) => {
4
+ function r({ enabled: r, anchor: i, panel: a }) {
5
+ n(() => {
6
+ if (!r || !i || !a) return;
7
+ let n = (e, t) => {
15
8
  t && (e.preventDefault(), t.focus());
16
- }, a = (e) => {
9
+ }, o = (e) => {
17
10
  if (e.key !== "Tab" || e.defaultPrevented) return;
18
- let a = n(i);
19
- if (a.length === 0) return;
11
+ let r = t(a);
12
+ if (r.length === 0) return;
20
13
  let o = document.activeElement;
21
14
  if (!o) return;
22
- let s = n(document).filter((e) => !i.contains(e)), c = s.filter((e) => r.contains(e)).at(-1), l = c ? s[s.indexOf(c) + 1] : void 0;
23
- if (i.contains(o)) {
24
- !e.shiftKey && o === a.at(-1) && t(e, l), e.shiftKey && o === a[0] && t(e, c);
15
+ let s = t(document).filter((e) => !a.contains(e)), c = s.filter((e) => i.contains(e)).at(-1), l = c ? s[s.indexOf(c) + 1] : void 0;
16
+ if (a.contains(o)) {
17
+ !e.shiftKey && o === r.at(-1) && n(e, l), e.shiftKey && o === r[0] && n(e, c);
25
18
  return;
26
19
  }
27
- !e.shiftKey && o === c && t(e, a[0]), e.shiftKey && l && o === l && t(e, a.at(-1));
20
+ !e.shiftKey && o === c && n(e, r[0]), e.shiftKey && l && o === l && n(e, r.at(-1));
21
+ }, s = e({
22
+ anchor: i,
23
+ panel: a
24
+ });
25
+ return document.addEventListener("keydown", o, !0), () => {
26
+ s(), document.removeEventListener("keydown", o, !0);
28
27
  };
29
- return document.addEventListener("keydown", a, !0), () => document.removeEventListener("keydown", a, !0);
30
28
  }, [
31
- e,
32
29
  r,
33
- i
30
+ i,
31
+ a
34
32
  ]);
35
33
  }
36
34
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"tab-bridge.js","names":[],"sources":["../../../src/components/Portal/tab-bridge.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { FOCUSABLE_SELECTOR } from \"@/hooks/use-focus-trap\";\n\n/** Options of {@link usePortalTabOrder}. */\nexport interface PortalTabOrderOptions {\n /** Whether the panel is open and portalled. */\n enabled: boolean;\n /** Wrapper of the trigger — the spot in the page the panel belongs to. */\n anchor: HTMLElement | null;\n /** The portalled panel. */\n panel: HTMLElement | null;\n}\n\n/**\n * 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 */\nfunction 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 * Put a portalled panel back in the tab order, right after its trigger.\n *\n * A portal moves the panel to the end of `body`, so the browser's own order goes\n * from the trigger straight to whatever follows it in the page and reaches the\n * panel last, if ever. Measured in Chrome on a `Popover` holding two inputs:\n * in flow, `Tab` from the open trigger landed on the first input; in a portal it\n * skipped the panel and went to the next button of the page.\n *\n * While the panel is open, this restores the in-flow order:\n *\n * - `Tab` on the last stop of the anchor enters the panel's first stop.\n * - `Tab` on the panel's last stop leaves to the first stop after the anchor.\n * - `Shift+Tab` on the panel's first stop returns to the anchor.\n * - `Shift+Tab` on the first stop after the anchor enters the panel's last stop.\n *\n * Listened on `document` in the capture phase and marked consumed, so it runs\n * before an enclosing `Modal`'s focus trap, which skips a consumed `Tab`.\n *\n * @param options - Whether it is active, the anchor and the panel.\n * @returns Nothing.\n */\nexport function usePortalTabOrder({ enabled, anchor, panel }: PortalTabOrderOptions): void {\n useEffect(() => {\n if (!enabled || !anchor || !panel) return;\n\n const move = (event: KeyboardEvent, target: HTMLElement | undefined): void => {\n if (!target) return;\n event.preventDefault();\n target.focus();\n };\n\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key !== \"Tab\" || event.defaultPrevented) return;\n const inPanel = tabbables(panel);\n if (inPanel.length === 0) return;\n const active = document.activeElement as HTMLElement | null;\n if (!active) return;\n const page = tabbables(document).filter((el) => !panel.contains(el));\n const inAnchor = page.filter((el) => anchor.contains(el));\n const lastOfAnchor = inAnchor.at(-1);\n const afterAnchor = lastOfAnchor ? page[page.indexOf(lastOfAnchor) + 1] : undefined;\n\n if (panel.contains(active)) {\n if (!event.shiftKey && active === inPanel.at(-1)) move(event, afterAnchor);\n if (event.shiftKey && active === inPanel[0]) move(event, lastOfAnchor);\n return;\n }\n if (!event.shiftKey && active === lastOfAnchor) move(event, inPanel[0]);\n if (event.shiftKey && afterAnchor && active === afterAnchor) {\n move(event, inPanel.at(-1));\n }\n };\n\n document.addEventListener(\"keydown\", onKeyDown, true);\n return () => document.removeEventListener(\"keydown\", onKeyDown, true);\n }, [enabled, anchor, panel]);\n}\n"],"mappings":";;;AAoBA,SAAS,EAAU,GAAiC;CAChD,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;AAwBA,SAAgB,EAAkB,EAAE,YAAS,WAAQ,YAAsC;CACvF,QAAgB;EACZ,IAAI,CAAC,KAAW,CAAC,KAAU,CAAC,GAAO;EAEnC,IAAM,KAAQ,GAAsB,MAA0C;GACrE,MACL,EAAM,eAAe,GACrB,EAAO,MAAM;EACjB,GAEM,KAAa,MAA+B;GAC9C,IAAI,EAAM,QAAQ,SAAS,EAAM,kBAAkB;GACnD,IAAM,IAAU,EAAU,CAAK;GAC/B,IAAI,EAAQ,WAAW,GAAG;GAC1B,IAAM,IAAS,SAAS;GACxB,IAAI,CAAC,GAAQ;GACb,IAAM,IAAO,EAAU,QAAQ,CAAC,CAAC,QAAQ,MAAO,CAAC,EAAM,SAAS,CAAE,CAAC,GAE7D,IADW,EAAK,QAAQ,MAAO,EAAO,SAAS,CAAE,CAClC,CAAA,CAAS,GAAG,EAAE,GAC7B,IAAc,IAAe,EAAK,EAAK,QAAQ,CAAY,IAAI,KAAK,KAAA;GAE1E,IAAI,EAAM,SAAS,CAAM,GAAG;IAExB,AADI,CAAC,EAAM,YAAY,MAAW,EAAQ,GAAG,EAAE,KAAG,EAAK,GAAO,CAAW,GACrE,EAAM,YAAY,MAAW,EAAQ,MAAI,EAAK,GAAO,CAAY;IACrE;GACJ;GAEA,AADI,CAAC,EAAM,YAAY,MAAW,KAAc,EAAK,GAAO,EAAQ,EAAE,GAClE,EAAM,YAAY,KAAe,MAAW,KAC5C,EAAK,GAAO,EAAQ,GAAG,EAAE,CAAC;EAElC;EAGA,OADA,SAAS,iBAAiB,WAAW,GAAW,EAAI,SACvC,SAAS,oBAAoB,WAAW,GAAW,EAAI;CACxE,GAAG;EAAC;EAAS;EAAQ;CAAK,CAAC;AAC/B"}
1
+ {"version":3,"file":"tab-bridge.js","names":[],"sources":["../../../src/components/Portal/tab-bridge.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { registerPortalLayer, tabbables } from \"@/hooks/tab-sequence\";\n\n/** Options of {@link usePortalTabOrder}. */\nexport interface PortalTabOrderOptions {\n /** Whether the panel is open and portalled. */\n enabled: boolean;\n /** Wrapper of the trigger — the spot in the page the panel belongs to. */\n anchor: HTMLElement | null;\n /** The portalled panel. */\n panel: HTMLElement | null;\n}\n\n/**\n * Put a portalled panel back in the tab order, right after its trigger.\n *\n * A portal moves the panel to the end of `body`, so the browser's own order goes\n * from the trigger straight to whatever follows it in the page and reaches the\n * panel last, if ever. Measured in Chrome on a `Popover` holding two inputs:\n * in flow, `Tab` from the open trigger landed on the first input; in a portal it\n * skipped the panel and went to the next button of the page.\n *\n * While the panel is open, this restores the in-flow order:\n *\n * - `Tab` on the last stop of the anchor enters the panel's first stop.\n * - `Tab` on the panel's last stop leaves to the first stop after the anchor.\n * - `Shift+Tab` on the panel's first stop returns to the anchor.\n * - `Shift+Tab` on the first stop after the anchor enters the panel's last stop.\n *\n * Listened on `document` in the capture phase and marked consumed, so it runs\n * before an enclosing `Modal`'s focus trap, which skips a consumed `Tab`. The\n * panel is also registered as a layer of its anchor, which is how that trap\n * knows the panel belongs inside it although the portal put it outside.\n *\n * @param options - Whether it is active, the anchor and the panel.\n * @returns Nothing.\n */\nexport function usePortalTabOrder({ enabled, anchor, panel }: PortalTabOrderOptions): void {\n useEffect(() => {\n if (!enabled || !anchor || !panel) return;\n\n const move = (event: KeyboardEvent, target: HTMLElement | undefined): void => {\n if (!target) return;\n event.preventDefault();\n target.focus();\n };\n\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key !== \"Tab\" || event.defaultPrevented) return;\n const inPanel = tabbables(panel);\n if (inPanel.length === 0) return;\n const active = document.activeElement as HTMLElement | null;\n if (!active) return;\n const page = tabbables(document).filter((el) => !panel.contains(el));\n const inAnchor = page.filter((el) => anchor.contains(el));\n const lastOfAnchor = inAnchor.at(-1);\n const afterAnchor = lastOfAnchor ? page[page.indexOf(lastOfAnchor) + 1] : undefined;\n\n if (panel.contains(active)) {\n if (!event.shiftKey && active === inPanel.at(-1)) move(event, afterAnchor);\n if (event.shiftKey && active === inPanel[0]) move(event, lastOfAnchor);\n return;\n }\n if (!event.shiftKey && active === lastOfAnchor) move(event, inPanel[0]);\n if (event.shiftKey && afterAnchor && active === afterAnchor) {\n move(event, inPanel.at(-1));\n }\n };\n\n const unregister = registerPortalLayer({ anchor, panel });\n document.addEventListener(\"keydown\", onKeyDown, true);\n return () => {\n unregister();\n document.removeEventListener(\"keydown\", onKeyDown, true);\n };\n }, [enabled, anchor, panel]);\n}\n"],"mappings":";;;AAqCA,SAAgB,EAAkB,EAAE,YAAS,WAAQ,YAAsC;CACvF,QAAgB;EACZ,IAAI,CAAC,KAAW,CAAC,KAAU,CAAC,GAAO;EAEnC,IAAM,KAAQ,GAAsB,MAA0C;GACrE,MACL,EAAM,eAAe,GACrB,EAAO,MAAM;EACjB,GAEM,KAAa,MAA+B;GAC9C,IAAI,EAAM,QAAQ,SAAS,EAAM,kBAAkB;GACnD,IAAM,IAAU,EAAU,CAAK;GAC/B,IAAI,EAAQ,WAAW,GAAG;GAC1B,IAAM,IAAS,SAAS;GACxB,IAAI,CAAC,GAAQ;GACb,IAAM,IAAO,EAAU,QAAQ,CAAC,CAAC,QAAQ,MAAO,CAAC,EAAM,SAAS,CAAE,CAAC,GAE7D,IADW,EAAK,QAAQ,MAAO,EAAO,SAAS,CAAE,CAClC,CAAA,CAAS,GAAG,EAAE,GAC7B,IAAc,IAAe,EAAK,EAAK,QAAQ,CAAY,IAAI,KAAK,KAAA;GAE1E,IAAI,EAAM,SAAS,CAAM,GAAG;IAExB,AADI,CAAC,EAAM,YAAY,MAAW,EAAQ,GAAG,EAAE,KAAG,EAAK,GAAO,CAAW,GACrE,EAAM,YAAY,MAAW,EAAQ,MAAI,EAAK,GAAO,CAAY;IACrE;GACJ;GAEA,AADI,CAAC,EAAM,YAAY,MAAW,KAAc,EAAK,GAAO,EAAQ,EAAE,GAClE,EAAM,YAAY,KAAe,MAAW,KAC5C,EAAK,GAAO,EAAQ,GAAG,EAAE,CAAC;EAElC,GAEM,IAAa,EAAoB;GAAE;GAAQ;EAAM,CAAC;EAExD,OADA,SAAS,iBAAiB,WAAW,GAAW,EAAI,SACvC;GAET,AADA,EAAW,GACX,SAAS,oBAAoB,WAAW,GAAW,EAAI;EAC3D;CACJ,GAAG;EAAC;EAAS;EAAQ;CAAK,CAAC;AAC/B"}
@@ -0,0 +1,2 @@
1
+ let e=require("react/jsx-runtime");function t(){return(0,e.jsx)(`svg`,{width:`14`,height:`14`,viewBox:`0 0 24 24`,fill:`none`,children:(0,e.jsx)(`path`,{d:`M6 9l6 6 6-6`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`})})}exports.CaretIcon=t;
2
+ //# sourceMappingURL=CaretIcon.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CaretIcon.cjs","names":[],"sources":["../../../src/components/Select/CaretIcon.tsx"],"sourcesContent":["import type { ReactElement } from \"react\";\n\n/**\n * The chevron `Select` and `Combobox` draw on the right of the control.\n *\n * Internal to the SDK: the public way to change it is `Select`'s `caretIcon`\n * prop. Its size comes from the `--tempest-control-caret-size` token through the\n * caret slot's stylesheet, so the `width`/`height` attributes here are only the\n * fallback for a page that loaded no SDK CSS.\n *\n * @returns The chevron as an inline SVG drawn in `currentColor`.\n */\nexport function CaretIcon(): ReactElement {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 9l6 6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"mCAYA,SAAgB,GAA0B,CACtC,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,MAAM,KAAK,OAAO,KAAK,QAAQ,YAAY,KAAK,OACjD,UAAA,EAAA,EAAA,IAAA,CAAC,OAAD,CACI,EAAE,eACF,OAAO,eACP,YAAY,IACZ,cAAc,QACd,eAAe,OAClB,CAAA,CACA,CAAA,CAEb"}
@@ -0,0 +1,21 @@
1
+ import { jsx as e } from "react/jsx-runtime";
2
+ //#region src/components/Select/CaretIcon.tsx
3
+ function t() {
4
+ return /* @__PURE__ */ e("svg", {
5
+ width: "14",
6
+ height: "14",
7
+ viewBox: "0 0 24 24",
8
+ fill: "none",
9
+ children: /* @__PURE__ */ e("path", {
10
+ d: "M6 9l6 6 6-6",
11
+ stroke: "currentColor",
12
+ strokeWidth: "2",
13
+ strokeLinecap: "round",
14
+ strokeLinejoin: "round"
15
+ })
16
+ });
17
+ }
18
+ //#endregion
19
+ export { t as CaretIcon };
20
+
21
+ //# sourceMappingURL=CaretIcon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CaretIcon.js","names":[],"sources":["../../../src/components/Select/CaretIcon.tsx"],"sourcesContent":["import type { ReactElement } from \"react\";\n\n/**\n * The chevron `Select` and `Combobox` draw on the right of the control.\n *\n * Internal to the SDK: the public way to change it is `Select`'s `caretIcon`\n * prop. Its size comes from the `--tempest-control-caret-size` token through the\n * caret slot's stylesheet, so the `width`/`height` attributes here are only the\n * fallback for a page that loaded no SDK CSS.\n *\n * @returns The chevron as an inline SVG drawn in `currentColor`.\n */\nexport function CaretIcon(): ReactElement {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 9l6 6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":";;AAYA,SAAgB,IAA0B;CACtC,OACI,kBAAC,OAAD;EAAK,OAAM;EAAK,QAAO;EAAK,SAAQ;EAAY,MAAK;EACjD,UAAA,kBAAC,QAAD;GACI,GAAE;GACF,QAAO;GACP,aAAY;GACZ,eAAc;GACd,gBAAe;EAClB,CAAA;CACA,CAAA;AAEb"}
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/cn.cjs"),t=require("./Select.module.cjs");let n=require("react"),r=require("react/jsx-runtime");var i=(0,n.forwardRef)(function(i,o){let{options:s,placeholder:c,wrapperClassName:l,className:u,children:d,id:f,required:p,variant:m=`field`,label:h,helperText:g,error:_,...v}=i,y=m===`chip`,b=(0,n.useId)(),x=f??b,S=(0,r.jsxs)(`div`,{className:e.cn(t.default.field,y&&t.default.chip),children:[(0,r.jsxs)(`select`,{ref:o,id:x,"aria-invalid":!!_,required:p,className:e.cn(t.default.select,u),...v,children:[c&&(0,r.jsx)(`option`,{value:``,disabled:!0,hidden:!0,children:c}),s?.map(e=>(0,r.jsx)(`option`,{value:e.value,disabled:e.disabled,children:e.label},e.value)),d]}),(0,r.jsx)(`span`,{className:t.default.caret,"aria-hidden":!0,children:(0,r.jsx)(a,{})})]});return y?(0,r.jsx)(`div`,{className:e.cn(t.default.chipWrapper,l),children:S}):(0,r.jsxs)(`div`,{className:e.cn(t.default.wrapper,_&&t.default.error,l),children:[h&&(0,r.jsxs)(`label`,{htmlFor:x,className:t.default.label,children:[h,p&&(0,r.jsx)(`span`,{className:t.default.required,children:`*`})]}),S,_?(0,r.jsx)(`span`,{className:t.default.errorText,children:_}):g?(0,r.jsx)(`span`,{className:t.default.helper,children:g}):null]})});function a(){return(0,r.jsx)(`svg`,{width:`14`,height:`14`,viewBox:`0 0 24 24`,fill:`none`,children:(0,r.jsx)(`path`,{d:`M6 9l6 6 6-6`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`})})}exports.Select=i;
1
+ const e=require("../../utils/cn.cjs"),t=require("./CaretIcon.cjs"),n=require("./Select.module.cjs");let r=require("react"),i=require("react/jsx-runtime");var a=(0,r.forwardRef)(function(a,o){let{options:s,placeholder:c,wrapperClassName:l,className:u,children:d,id:f,required:p,variant:m=`field`,label:h,helperText:g,error:_,caretIcon:v,...y}=a,b=m===`chip`,x=m===`bare`,S=(0,r.useId)(),C=f??S,w=(0,i.jsxs)(`div`,{className:e.cn(n.default.field,b&&n.default.chip,x&&n.default.bare,x&&l),children:[(0,i.jsxs)(`select`,{ref:o,id:C,"aria-invalid":!!_,required:p,className:e.cn(n.default.select,u),...y,children:[c&&(0,i.jsx)(`option`,{value:``,disabled:!0,hidden:!0,children:c}),s?.map(e=>(0,i.jsx)(`option`,{value:e.value,disabled:e.disabled,children:e.label},e.value)),d]}),(0,i.jsx)(`span`,{className:n.default.caret,"aria-hidden":!0,children:v??(0,i.jsx)(t.CaretIcon,{})})]});return x?w:b?(0,i.jsx)(`div`,{className:e.cn(n.default.chipWrapper,l),children:w}):(0,i.jsxs)(`div`,{className:e.cn(n.default.wrapper,_&&n.default.error,l),children:[h&&(0,i.jsxs)(`label`,{htmlFor:C,className:n.default.label,children:[h,p&&(0,i.jsx)(`span`,{className:n.default.required,children:`*`})]}),w,_?(0,i.jsx)(`span`,{className:n.default.errorText,children:_}):g?(0,i.jsx)(`span`,{className:n.default.helper,children:g}):null]})});exports.Select=a;
2
2
  //# sourceMappingURL=Select.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"Select.cjs","names":[],"sources":["../../../src/components/Select/Select.tsx"],"sourcesContent":["import { forwardRef, useId } from \"react\";\nimport type { SelectHTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Select.module.css\";\n\nexport interface SelectOption {\n value: string | number;\n label: string;\n disabled?: boolean;\n}\n\n/** What both variants of {@link Select} accept. */\nexport interface SelectBaseProps extends SelectHTMLAttributes<HTMLSelectElement> {\n options?: SelectOption[];\n placeholder?: string;\n wrapperClassName?: string;\n}\n\n/** The full form field: label, control, helper text and error slot. */\nexport interface SelectFieldProps extends SelectBaseProps {\n variant?: \"field\";\n label?: string;\n helperText?: string;\n error?: string;\n}\n\n/**\n * The bare control, sized to its content.\n *\n * `aria-label` is required rather than optional, because the variant removes the\n * only visible label the component had. A settings row names the control in the\n * row's own text, which the `<select>` has no relationship to — so without this\n * the screen reader announces a combobox with no name at all.\n */\nexport interface SelectChipProps extends SelectBaseProps {\n variant: \"chip\";\n \"aria-label\": string;\n}\n\nexport type SelectProps = SelectFieldProps | SelectChipProps;\n\n/**\n * Native `<select>` wrapper. Either provide `options` for a quick render, or\n * pass `<option>` children directly.\n *\n * Two variants:\n *\n * - `\"field\"` (default) — label, control, helper text and error slot, for a form.\n * - `\"chip\"` — the control alone, sized to its content, for a settings row where\n * the icon and the label are already on the left and the control belongs on the\n * right. Rendering the full field there stacks two labelled fields inside one\n * list item, which is why every app ended up hand-rolling a native `<select>`\n * with `appearance: none` — and hand-rolling it loses the focus ring, the\n * disabled styling and the caret this component already solves.\n *\n * @example\n * <Select label=\"Estado\" options={states} placeholder=\"Selecione\" />\n *\n * @example\n * <ListTile title=\"Idioma\" trailing={<Select variant=\"chip\" aria-label=\"Idioma\" options={langs} />} />\n */\nexport const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(props, ref) {\n const {\n options,\n placeholder,\n wrapperClassName,\n className,\n children,\n id,\n required,\n variant = \"field\",\n label,\n helperText,\n error,\n ...rest\n } = props as SelectBaseProps & {\n variant?: \"field\" | \"chip\";\n label?: string;\n helperText?: string;\n error?: string;\n };\n const isChip = variant === \"chip\";\n const generatedId = useId();\n const selectId = id ?? generatedId;\n\n const control = (\n <div className={cn(styles.field, isChip && styles.chip)}>\n <select\n ref={ref}\n id={selectId}\n aria-invalid={!!error}\n required={required}\n className={cn(styles.select, className)}\n {...rest}\n >\n {placeholder && (\n <option value=\"\" disabled hidden>\n {placeholder}\n </option>\n )}\n {options?.map((opt) => (\n <option key={opt.value} value={opt.value} disabled={opt.disabled}>\n {opt.label}\n </option>\n ))}\n {children}\n </select>\n <span className={styles.caret} aria-hidden>\n <CaretIcon />\n </span>\n </div>\n );\n\n if (isChip) {\n return <div className={cn(styles.chipWrapper, wrapperClassName)}>{control}</div>;\n }\n\n return (\n <div className={cn(styles.wrapper, error && styles.error, wrapperClassName)}>\n {label && (\n <label htmlFor={selectId} className={styles.label}>\n {label}\n {required && <span className={styles.required}>*</span>}\n </label>\n )}\n {control}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n});\n\nfunction CaretIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 9l6 6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"6HA6DA,IAAa,GAAA,EAAS,EAAA,WAAA,CAA2C,SAAgB,EAAO,EAAK,CACzF,GAAM,CACF,UACA,cACA,mBACA,YACA,WACA,KACA,WACA,UAAU,QACV,QACA,aACA,QACA,GAAG,GACH,EAME,EAAS,IAAY,OACrB,GAAA,EAAc,EAAA,MAAA,CAAM,EACpB,EAAW,GAAM,EAEjB,GACF,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,MAAO,GAAU,EAAA,QAAO,IAAI,EAAtD,SAAA,EACI,EAAA,EAAA,KAAA,CAAC,SAAD,CACS,MACL,GAAI,EACJ,eAAc,CAAC,CAAC,EACN,WACV,UAAW,EAAA,GAAG,EAAA,QAAO,OAAQ,CAAS,EACtC,GAAI,EANR,SAAA,CAQK,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,MAAM,GAAG,SAAA,GAAS,OAAA,GACrB,SAAA,CACG,CAAA,EAEX,GAAS,IAAK,IACX,EAAA,EAAA,IAAA,CAAC,SAAD,CAAwB,MAAO,EAAI,MAAO,SAAU,EAAI,SACnD,SAAA,EAAI,KACD,EAFK,EAAI,KAET,CACX,EACA,CACG,CACR,CAAA,GAAA,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAO,cAAA,GAC3B,UAAA,EAAA,EAAA,IAAA,CAAC,EAAD,CAAY,CAAA,CACV,CAAA,CACL,IAOT,OAJI,GACO,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,YAAa,CAAgB,EAAI,SAAA,CAAa,CAAA,GAI/E,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,GAAS,EAAA,QAAO,MAAO,CAAgB,EAA1E,SAAA,CACK,IACG,EAAA,EAAA,KAAA,CAAC,QAAD,CAAO,QAAS,EAAU,UAAW,EAAA,QAAO,MAA5C,SAAA,CACK,EACA,IAAY,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,SAAU,SAAA,GAAO,CAAA,CACnD,IAEV,EACA,GACG,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,UAAY,SAAA,CAAY,CAAA,EAChD,GACA,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,OAAS,SAAA,CAAiB,CAAA,EAClD,IACH,GAEb,CAAC,EAED,SAAS,GAAY,CACjB,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,MAAM,KAAK,OAAO,KAAK,QAAQ,YAAY,KAAK,OACjD,UAAA,EAAA,EAAA,IAAA,CAAC,OAAD,CACI,EAAE,eACF,OAAO,eACP,YAAY,IACZ,cAAc,QACd,eAAe,OAClB,CAAA,CACA,CAAA,CAEb"}
1
+ {"version":3,"file":"Select.cjs","names":[],"sources":["../../../src/components/Select/Select.tsx"],"sourcesContent":["import { forwardRef, useId } from \"react\";\nimport type { ReactNode, SelectHTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { CaretIcon } from \"./CaretIcon\";\nimport styles from \"./Select.module.css\";\n\nexport interface SelectOption {\n value: string | number;\n label: string;\n disabled?: boolean;\n}\n\n/** What every variant of {@link Select} accepts. */\nexport interface SelectBaseProps extends SelectHTMLAttributes<HTMLSelectElement> {\n options?: SelectOption[];\n placeholder?: string;\n /**\n * Class for the outermost element. On `\"bare\"` that element is the shell\n * holding the `<select>` and the caret, which is where the app's own look goes.\n */\n wrapperClassName?: string;\n /**\n * Replaces the built-in chevron, following the `icon?: ReactNode` convention\n * the rest of the SDK uses.\n *\n * The icon is drawn in the caret slot, which is positioned by\n * `--tempest-control-caret-offset` and sized by `--tempest-control-caret-size`;\n * an `<svg>` passed here is stretched to that size, so the token — not the\n * icon's own `width`/`height` — decides how big the caret is in every density.\n */\n caretIcon?: ReactNode;\n}\n\n/** The full form field: label, control, helper text and error slot. */\nexport interface SelectFieldProps extends SelectBaseProps {\n variant?: \"field\";\n label?: string;\n helperText?: string;\n error?: string;\n}\n\n/**\n * The bare control, sized to its content.\n *\n * `aria-label` is required rather than optional, because the variant removes the\n * only visible label the component had. A settings row names the control in the\n * row's own text, which the `<select>` has no relationship to — so without this\n * the screen reader announces a combobox with no name at all.\n */\nexport interface SelectChipProps extends SelectBaseProps {\n variant: \"chip\";\n \"aria-label\": string;\n}\n\n/**\n * The mechanism without the look: `appearance: none`, the caret and the focus\n * ring, and no border, background, radius, shadow or height.\n *\n * The `<select>` inherits `background-color` and `color` from the shell (the\n * element `wrapperClassName` lands on). The background is not cosmetic: the\n * native popup paints each `<option>` from the `<select>`'s background, so a\n * transparent control would hand the menu back to the system white.\n *\n * Like `\"chip\"`, the variant has no visible label, so an accessible name is\n * required — through `aria-label` or `aria-labelledby`.\n */\nexport type SelectBareProps = SelectBaseProps & { variant: \"bare\" } & (\n { \"aria-label\": string } | { \"aria-labelledby\": string }\n );\n\nexport type SelectProps = SelectFieldProps | SelectChipProps | SelectBareProps;\n\n/**\n * Native `<select>` wrapper. Either provide `options` for a quick render, or\n * pass `<option>` children directly.\n *\n * Three variants:\n *\n * - `\"field\"` (default) — label, control, helper text and error slot, for a form.\n * - `\"chip\"` — the control alone, sized to its content, for a settings row where\n * the icon and the label are already on the left and the control belongs on the\n * right. Rendering the full field there stacks two labelled fields inside one\n * list item, which is why every app ended up hand-rolling a native `<select>`\n * with `appearance: none` — and hand-rolling it loses the focus ring, the\n * disabled styling and the caret this component already solves.\n * - `\"bare\"` — the same mechanism with none of the look, for an app with its own\n * visual identity: the shell renders `wrapper > select + caret`, and the app\n * dresses the wrapper through `wrapperClassName`. See {@link SelectBareProps}.\n *\n * @example\n * <Select label=\"Estado\" options={states} placeholder=\"Selecione\" />\n *\n * @example\n * <ListTile title=\"Idioma\" trailing={<Select variant=\"chip\" aria-label=\"Idioma\" options={langs} />} />\n *\n * @example\n * <Select variant=\"bare\" wrapperClassName={styles.filtro} aria-label=\"Status\" options={status} />\n */\nexport const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(props, ref) {\n const {\n options,\n placeholder,\n wrapperClassName,\n className,\n children,\n id,\n required,\n variant = \"field\",\n label,\n helperText,\n error,\n caretIcon,\n ...rest\n } = props as SelectBaseProps & {\n variant?: SelectProps[\"variant\"];\n label?: string;\n helperText?: string;\n error?: string;\n };\n const isChip = variant === \"chip\";\n const isBare = variant === \"bare\";\n const generatedId = useId();\n const selectId = id ?? generatedId;\n\n const control = (\n <div\n className={cn(\n styles.field,\n isChip && styles.chip,\n isBare && styles.bare,\n isBare && wrapperClassName,\n )}\n >\n <select\n ref={ref}\n id={selectId}\n aria-invalid={!!error}\n required={required}\n className={cn(styles.select, className)}\n {...rest}\n >\n {placeholder && (\n <option value=\"\" disabled hidden>\n {placeholder}\n </option>\n )}\n {options?.map((opt) => (\n <option key={opt.value} value={opt.value} disabled={opt.disabled}>\n {opt.label}\n </option>\n ))}\n {children}\n </select>\n <span className={styles.caret} aria-hidden>\n {caretIcon ?? <CaretIcon />}\n </span>\n </div>\n );\n\n if (isBare) {\n return control;\n }\n\n if (isChip) {\n return <div className={cn(styles.chipWrapper, wrapperClassName)}>{control}</div>;\n }\n\n return (\n <div className={cn(styles.wrapper, error && styles.error, wrapperClassName)}>\n {label && (\n <label htmlFor={selectId} className={styles.label}>\n {label}\n {required && <span className={styles.required}>*</span>}\n </label>\n )}\n {control}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n});\n"],"mappings":"0JAkGA,IAAa,GAAA,EAAS,EAAA,WAAA,CAA2C,SAAgB,EAAO,EAAK,CACzF,GAAM,CACF,UACA,cACA,mBACA,YACA,WACA,KACA,WACA,UAAU,QACV,QACA,aACA,QACA,YACA,GAAG,GACH,EAME,EAAS,IAAY,OACrB,EAAS,IAAY,OACrB,GAAA,EAAc,EAAA,MAAA,CAAM,EACpB,EAAW,GAAM,EAEjB,GACF,EAAA,EAAA,KAAA,CAAC,MAAD,CACI,UAAW,EAAA,GACP,EAAA,QAAO,MACP,GAAU,EAAA,QAAO,KACjB,GAAU,EAAA,QAAO,KACjB,GAAU,CACd,EANJ,SAAA,EAQI,EAAA,EAAA,KAAA,CAAC,SAAD,CACS,MACL,GAAI,EACJ,eAAc,CAAC,CAAC,EACN,WACV,UAAW,EAAA,GAAG,EAAA,QAAO,OAAQ,CAAS,EACtC,GAAI,EANR,SAAA,CAQK,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,MAAM,GAAG,SAAA,GAAS,OAAA,GACrB,SAAA,CACG,CAAA,EAEX,GAAS,IAAK,IACX,EAAA,EAAA,IAAA,CAAC,SAAD,CAAwB,MAAO,EAAI,MAAO,SAAU,EAAI,SACnD,SAAA,EAAI,KACD,EAFK,EAAI,KAET,CACX,EACA,CACG,CACR,CAAA,GAAA,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAO,cAAA,GAC1B,SAAA,IAAa,EAAA,EAAA,IAAA,CAAC,EAAA,UAAD,CAAY,CAAA,CACxB,CAAA,CACL,IAWT,OARI,EACO,EAGP,GACO,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,YAAa,CAAgB,EAAI,SAAA,CAAa,CAAA,GAI/E,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,GAAS,EAAA,QAAO,MAAO,CAAgB,EAA1E,SAAA,CACK,IACG,EAAA,EAAA,KAAA,CAAC,QAAD,CAAO,QAAS,EAAU,UAAW,EAAA,QAAO,MAA5C,SAAA,CACK,EACA,IAAY,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,SAAU,SAAA,GAAO,CAAA,CACnD,IAEV,EACA,GACG,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,UAAY,SAAA,CAAY,CAAA,EAChD,GACA,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,OAAS,SAAA,CAAiB,CAAA,EAClD,IACH,GAEb,CAAC"}