@boxcustodia/library 2.0.0-alpha.22 → 2.0.0-alpha.23

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 (102) hide show
  1. package/dist/components/calendar/calendar.cjs.js +1 -1
  2. package/dist/components/calendar/calendar.es.js +43 -44
  3. package/dist/components/date-picker/date-input.cjs.js +1 -1
  4. package/dist/components/date-picker/date-input.es.js +160 -140
  5. package/dist/components/pagination/pagination.cjs.js +1 -1
  6. package/dist/components/pagination/pagination.es.js +37 -35
  7. package/dist/components/scroll-area/scroll-area.cjs.js +1 -1
  8. package/dist/components/scroll-area/scroll-area.es.js +4 -4
  9. package/dist/components/select/select.cjs.js +1 -1
  10. package/dist/components/select/select.es.js +94 -90
  11. package/dist/hooks/use-action/use-action.cjs.js +1 -0
  12. package/dist/hooks/use-action/use-action.es.js +41 -0
  13. package/dist/hooks/use-pagination/use-pagination.cjs.js +1 -1
  14. package/dist/hooks/use-pagination/use-pagination.es.js +77 -32
  15. package/dist/hooks/use-range-pagination/use-range-pagination.cjs.js +1 -1
  16. package/dist/hooks/use-range-pagination/use-range-pagination.es.js +8 -5
  17. package/dist/hooks/use-selection/use-selection.cjs.js +1 -1
  18. package/dist/hooks/use-selection/use-selection.es.js +95 -33
  19. package/dist/hooks/use-session-storage/use-session-storage.cjs.js +1 -0
  20. package/dist/hooks/use-session-storage/use-session-storage.es.js +57 -0
  21. package/dist/index.cjs.js +1 -1
  22. package/dist/index.es.js +61 -63
  23. package/dist/src/components/select/select.d.ts +9 -2
  24. package/dist/src/hooks/index.d.ts +2 -3
  25. package/dist/src/hooks/internal/index.d.ts +1 -0
  26. package/dist/src/hooks/internal/serializer.d.ts +4 -0
  27. package/dist/src/hooks/use-action/index.d.ts +1 -0
  28. package/dist/src/hooks/use-action/use-action.d.ts +22 -0
  29. package/dist/src/hooks/use-local-storage/use-local-storage.d.ts +2 -4
  30. package/dist/src/hooks/use-pagination/use-pagination.d.ts +47 -32
  31. package/dist/src/hooks/use-range-pagination/use-range-pagination.d.ts +16 -10
  32. package/dist/src/hooks/use-selection/use-selection.d.ts +39 -45
  33. package/dist/src/hooks/use-session-storage/index.d.ts +1 -0
  34. package/dist/src/hooks/use-session-storage/use-session-storage.d.ts +11 -0
  35. package/package.json +1 -1
  36. package/src/components/calendar/calendar.tsx +10 -8
  37. package/src/components/combobox/combobox.stories.tsx +16 -0
  38. package/src/components/date-picker/date-input.tsx +23 -2
  39. package/src/components/form/form.tsx +3 -2
  40. package/src/components/pagination/pagination.tsx +5 -3
  41. package/src/components/scroll-area/scroll-area.tsx +2 -2
  42. package/src/components/select/select.tsx +14 -3
  43. package/src/hooks/index.ts +2 -3
  44. package/src/hooks/internal/index.ts +1 -0
  45. package/src/hooks/internal/serializer.ts +4 -0
  46. package/src/hooks/use-action/index.ts +1 -0
  47. package/src/hooks/{use-mutation/use-mutation.stories.tsx → use-action/use-action.stories.tsx} +34 -34
  48. package/src/hooks/{use-mutation/use-mutation.test.ts → use-action/use-action.test.ts} +53 -53
  49. package/src/hooks/{use-mutation/use-mutation.ts → use-action/use-action.ts} +20 -20
  50. package/src/hooks/use-click-outside/use-click-outside.stories.tsx +0 -1
  51. package/src/hooks/use-clipboard/use-clipboard.stories.tsx +0 -1
  52. package/src/hooks/use-document-title/use-document-title.stories.tsx +0 -1
  53. package/src/hooks/use-is-visible/use-is-visible.test.tsx +1 -1
  54. package/src/hooks/use-local-storage/use-local-storage.stories.tsx +0 -1
  55. package/src/hooks/use-local-storage/use-local-storage.ts +2 -5
  56. package/src/hooks/use-media-query/use-media-query.stories.tsx +0 -1
  57. package/src/hooks/use-pagination/use-pagination.stories.tsx +720 -57
  58. package/src/hooks/use-pagination/use-pagination.test.tsx +560 -48
  59. package/src/hooks/use-pagination/use-pagination.ts +266 -0
  60. package/src/hooks/use-prevent-page-close/use-prevent-page-close.stories.tsx +0 -1
  61. package/src/hooks/use-range-pagination/use-range-pagination.test.tsx +2 -2
  62. package/src/hooks/use-range-pagination/use-range-pagination.tsx +24 -21
  63. package/src/hooks/use-selection/use-selection.stories.tsx +339 -84
  64. package/src/hooks/use-selection/use-selection.test.tsx +417 -2
  65. package/src/hooks/use-selection/use-selection.ts +212 -102
  66. package/src/hooks/use-session-storage/index.ts +1 -0
  67. package/src/hooks/use-session-storage/use-session-storage.stories.tsx +122 -0
  68. package/src/hooks/use-session-storage/use-session-storage.test.ts +164 -0
  69. package/src/hooks/use-session-storage/use-session-storage.ts +115 -0
  70. package/dist/hooks/use-async/use-async.cjs.js +0 -1
  71. package/dist/hooks/use-async/use-async.es.js +0 -57
  72. package/dist/hooks/use-focus-trap/scope-tab.cjs.js +0 -1
  73. package/dist/hooks/use-focus-trap/scope-tab.es.js +0 -21
  74. package/dist/hooks/use-focus-trap/tabbable.cjs.js +0 -1
  75. package/dist/hooks/use-focus-trap/tabbable.es.js +0 -38
  76. package/dist/hooks/use-focus-trap/use-focus-trap.cjs.js +0 -1
  77. package/dist/hooks/use-focus-trap/use-focus-trap.es.js +0 -34
  78. package/dist/hooks/use-mutation/use-mutation.cjs.js +0 -1
  79. package/dist/hooks/use-mutation/use-mutation.es.js +0 -41
  80. package/dist/src/hooks/use-async/index.d.ts +0 -1
  81. package/dist/src/hooks/use-async/use-async.d.ts +0 -21
  82. package/dist/src/hooks/use-focus-trap/index.d.ts +0 -1
  83. package/dist/src/hooks/use-focus-trap/scope-tab.d.ts +0 -1
  84. package/dist/src/hooks/use-focus-trap/tabbable.d.ts +0 -4
  85. package/dist/src/hooks/use-focus-trap/use-focus-trap.d.ts +0 -1
  86. package/dist/src/hooks/use-mutation/index.d.ts +0 -1
  87. package/dist/src/hooks/use-mutation/use-mutation.d.ts +0 -22
  88. package/dist/src/hooks/use-mutation/use-mutation.test.d.ts +0 -1
  89. package/src/hooks/use-async/index.ts +0 -1
  90. package/src/hooks/use-async/use-async.stories.tsx +0 -272
  91. package/src/hooks/use-async/use-async.test.ts +0 -397
  92. package/src/hooks/use-async/use-async.ts +0 -135
  93. package/src/hooks/use-focus-trap/index.ts +0 -1
  94. package/src/hooks/use-focus-trap/scope-tab.ts +0 -38
  95. package/src/hooks/use-focus-trap/tabbable.ts +0 -70
  96. package/src/hooks/use-focus-trap/use-focus-trap.stories.tsx +0 -37
  97. package/src/hooks/use-focus-trap/use-focus-trap.test.ts +0 -355
  98. package/src/hooks/use-focus-trap/use-focus-trap.ts +0 -78
  99. package/src/hooks/use-mutation/index.ts +0 -1
  100. package/src/hooks/use-pagination/use-pagination.tsx +0 -84
  101. /package/dist/src/hooks/{use-async/use-async.test.d.ts → use-action/use-action.test.d.ts} +0 -0
  102. /package/dist/src/hooks/{use-focus-trap/use-focus-trap.test.d.ts → use-session-storage/use-session-storage.test.d.ts} +0 -0
@@ -1,355 +0,0 @@
1
- import { renderHook } from "@testing-library/react";
2
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
- import { scopeTab } from "./scope-tab";
4
- import {
5
- FOCUS_SELECTOR,
6
- findTabbableDescendants,
7
- focusable,
8
- tabbable,
9
- } from "./tabbable";
10
- import { useFocusTrap } from "./use-focus-trap";
11
-
12
- // ─── tabbable.ts ─────────────────────────────────────────────────────────────
13
-
14
- describe("focusable", () => {
15
- it("returns true for a non-disabled button", () => {
16
- const btn = document.createElement("button");
17
- document.body.appendChild(btn);
18
- expect(focusable(btn)).toBe(true);
19
- btn.remove();
20
- });
21
-
22
- it("returns false for a disabled button", () => {
23
- const btn = document.createElement("button");
24
- btn.disabled = true;
25
- document.body.appendChild(btn);
26
- expect(focusable(btn)).toBe(false);
27
- btn.remove();
28
- });
29
-
30
- it("returns true for an anchor with href", () => {
31
- const a = document.createElement("a");
32
- a.href = "https://example.com";
33
- document.body.appendChild(a);
34
- expect(focusable(a)).toBe(true);
35
- a.remove();
36
- });
37
-
38
- it("returns false for an anchor without href and no tabindex", () => {
39
- const a = document.createElement("a");
40
- document.body.appendChild(a);
41
- expect(focusable(a)).toBe(false);
42
- a.remove();
43
- });
44
-
45
- it("returns true for div with tabindex='0'", () => {
46
- const div = document.createElement("div");
47
- div.setAttribute("tabindex", "0");
48
- document.body.appendChild(div);
49
- expect(focusable(div)).toBe(true);
50
- div.remove();
51
- });
52
-
53
- it("returns false for div without tabindex", () => {
54
- const div = document.createElement("div");
55
- document.body.appendChild(div);
56
- expect(focusable(div)).toBe(false);
57
- div.remove();
58
- });
59
-
60
- it("returns false for element with aria-hidden='true'", () => {
61
- const btn = document.createElement("button");
62
- btn.setAttribute("aria-hidden", "true");
63
- document.body.appendChild(btn);
64
- expect(focusable(btn)).toBe(false);
65
- btn.remove();
66
- });
67
-
68
- it("returns false for element with hidden='true'", () => {
69
- const btn = document.createElement("button");
70
- btn.setAttribute("hidden", "true");
71
- document.body.appendChild(btn);
72
- expect(focusable(btn)).toBe(false);
73
- btn.remove();
74
- });
75
-
76
- it("returns false for input with type='hidden'", () => {
77
- const input = document.createElement("input");
78
- input.setAttribute("type", "hidden");
79
- document.body.appendChild(input);
80
- expect(focusable(input)).toBe(false);
81
- input.remove();
82
- });
83
- });
84
-
85
- describe("tabbable", () => {
86
- it("returns true for a button (no tabindex)", () => {
87
- const btn = document.createElement("button");
88
- document.body.appendChild(btn);
89
- expect(tabbable(btn)).toBe(true);
90
- btn.remove();
91
- });
92
-
93
- it("returns false for element with tabindex='-1'", () => {
94
- const btn = document.createElement("button");
95
- btn.setAttribute("tabindex", "-1");
96
- document.body.appendChild(btn);
97
- expect(tabbable(btn)).toBe(false);
98
- btn.remove();
99
- });
100
-
101
- it("returns true for element with tabindex='0'", () => {
102
- const btn = document.createElement("button");
103
- btn.setAttribute("tabindex", "0");
104
- document.body.appendChild(btn);
105
- expect(tabbable(btn)).toBe(true);
106
- btn.remove();
107
- });
108
- });
109
-
110
- describe("findTabbableDescendants", () => {
111
- it("returns tabbable children of a container", () => {
112
- const container = document.createElement("div");
113
- const btn1 = document.createElement("button");
114
- const btn2 = document.createElement("button");
115
- btn2.disabled = true;
116
- const input = document.createElement("input");
117
- container.append(btn1, btn2, input);
118
- document.body.appendChild(container);
119
-
120
- const result = findTabbableDescendants(container);
121
- expect(result).toContain(btn1);
122
- expect(result).toContain(input);
123
- expect(result).not.toContain(btn2);
124
-
125
- container.remove();
126
- });
127
-
128
- it("returns empty array when no tabbable descendants", () => {
129
- const container = document.createElement("div");
130
- document.body.appendChild(container);
131
- expect(findTabbableDescendants(container)).toHaveLength(0);
132
- container.remove();
133
- });
134
- });
135
-
136
- describe("FOCUS_SELECTOR", () => {
137
- it("is a non-empty string covering focusable elements", () => {
138
- expect(typeof FOCUS_SELECTOR).toBe("string");
139
- expect(FOCUS_SELECTOR).toContain("button");
140
- expect(FOCUS_SELECTOR).toContain("input");
141
- });
142
- });
143
-
144
- // ─── scope-tab.ts ─────────────────────────────────────────────────────────────
145
-
146
- describe("scopeTab", () => {
147
- let container: HTMLDivElement;
148
-
149
- beforeEach(() => {
150
- container = document.createElement("div");
151
- document.body.appendChild(container);
152
- });
153
-
154
- afterEach(() => {
155
- container.remove();
156
- });
157
-
158
- it("calls preventDefault when no tabbable descendants", () => {
159
- const event = {
160
- key: "Tab",
161
- shiftKey: false,
162
- preventDefault: vi.fn(),
163
- } as any;
164
- scopeTab(container, event);
165
- expect(event.preventDefault).toHaveBeenCalledOnce();
166
- });
167
-
168
- it("wraps focus to first element on Tab when last is focused", () => {
169
- const btn1 = document.createElement("button");
170
- const btn2 = document.createElement("button");
171
- container.append(btn1, btn2);
172
- btn2.focus();
173
-
174
- const event = {
175
- key: "Tab",
176
- shiftKey: false,
177
- preventDefault: vi.fn(),
178
- } as any;
179
- scopeTab(container, event);
180
-
181
- expect(event.preventDefault).toHaveBeenCalledOnce();
182
- expect(document.activeElement).toBe(btn1);
183
- });
184
-
185
- it("wraps focus to last element on Shift+Tab when first is focused", () => {
186
- const btn1 = document.createElement("button");
187
- const btn2 = document.createElement("button");
188
- container.append(btn1, btn2);
189
- btn1.focus();
190
-
191
- const event = {
192
- key: "Tab",
193
- shiftKey: true,
194
- preventDefault: vi.fn(),
195
- } as any;
196
- scopeTab(container, event);
197
-
198
- expect(event.preventDefault).toHaveBeenCalledOnce();
199
- expect(document.activeElement).toBe(btn2);
200
- });
201
-
202
- it("does not call preventDefault when mid-element is focused (Tab forward)", () => {
203
- const btn1 = document.createElement("button");
204
- const btn2 = document.createElement("button");
205
- const btn3 = document.createElement("button");
206
- container.append(btn1, btn2, btn3);
207
- btn1.focus();
208
-
209
- const event = {
210
- key: "Tab",
211
- shiftKey: false,
212
- preventDefault: vi.fn(),
213
- } as any;
214
- scopeTab(container, event);
215
-
216
- expect(event.preventDefault).not.toHaveBeenCalled();
217
- });
218
-
219
- it("radio group: Tab from any radio in group triggers wrap when last element is same group", () => {
220
- const btn = document.createElement("button");
221
- const radio1 = document.createElement("input");
222
- radio1.setAttribute("type", "radio");
223
- radio1.setAttribute("name", "choice");
224
- const radio2 = document.createElement("input");
225
- radio2.setAttribute("type", "radio");
226
- radio2.setAttribute("name", "choice");
227
- container.append(btn, radio1, radio2);
228
- radio1.focus();
229
-
230
- const event = {
231
- key: "Tab",
232
- shiftKey: false,
233
- preventDefault: vi.fn(),
234
- } as any;
235
- scopeTab(container, event);
236
-
237
- expect(event.preventDefault).toHaveBeenCalledOnce();
238
- expect(document.activeElement).toBe(btn);
239
- });
240
- });
241
-
242
- // ─── use-focus-trap.ts ────────────────────────────────────────────────────────
243
-
244
- describe("useFocusTrap", () => {
245
- beforeEach(() => {
246
- vi.useFakeTimers();
247
- });
248
-
249
- afterEach(() => {
250
- vi.useRealTimers();
251
- });
252
-
253
- it("returns a ref callback function", () => {
254
- const { result } = renderHook(() => useFocusTrap());
255
- expect(typeof result.current).toBe("function");
256
- });
257
-
258
- it("does nothing when active=false", () => {
259
- const { result } = renderHook(() => useFocusTrap(false));
260
- const container = document.createElement("div");
261
- const btn = document.createElement("button");
262
- container.appendChild(btn);
263
- document.body.appendChild(container);
264
-
265
- result.current(container);
266
- vi.runAllTimers();
267
-
268
- expect(document.activeElement).not.toBe(btn);
269
- container.remove();
270
- });
271
-
272
- it("focuses first tabbable element when active=true", () => {
273
- const { result } = renderHook(() => useFocusTrap(true));
274
- const container = document.createElement("div");
275
- const btn = document.createElement("button");
276
- container.appendChild(btn);
277
- document.body.appendChild(container);
278
-
279
- result.current(container);
280
- vi.runAllTimers();
281
-
282
- expect(document.activeElement).toBe(btn);
283
- container.remove();
284
- });
285
-
286
- it("focuses [data-autofocus] element when present", () => {
287
- const { result } = renderHook(() => useFocusTrap(true));
288
- const container = document.createElement("div");
289
- const btn1 = document.createElement("button");
290
- const btn2 = document.createElement("button");
291
- btn2.setAttribute("data-autofocus", "");
292
- container.append(btn1, btn2);
293
- document.body.appendChild(container);
294
-
295
- result.current(container);
296
- vi.runAllTimers();
297
-
298
- expect(document.activeElement).toBe(btn2);
299
- container.remove();
300
- });
301
-
302
- it("does nothing when setRef called with null", () => {
303
- const { result } = renderHook(() => useFocusTrap(true));
304
- expect(() => result.current(null)).not.toThrow();
305
- });
306
-
307
- it("focuses the container itself when no tabbable children but container is focusable", () => {
308
- const { result } = renderHook(() => useFocusTrap(true));
309
- const container = document.createElement("div");
310
- container.setAttribute("tabindex", "0");
311
- document.body.appendChild(container);
312
-
313
- result.current(container);
314
- vi.runAllTimers();
315
-
316
- expect(document.activeElement).toBe(container);
317
- container.remove();
318
- });
319
-
320
- it("does not re-focus when same node is assigned a second time", () => {
321
- const { result } = renderHook(() => useFocusTrap(true));
322
- const container = document.createElement("div");
323
- const btn = document.createElement("button");
324
- container.appendChild(btn);
325
- document.body.appendChild(container);
326
-
327
- result.current(container);
328
- vi.runAllTimers();
329
- expect(document.activeElement).toBe(btn);
330
-
331
- result.current(container);
332
- vi.runAllTimers();
333
- expect(document.activeElement).toBe(btn);
334
- container.remove();
335
- });
336
-
337
- it("adds keydown listener that calls scopeTab on Tab key", () => {
338
- const { result } = renderHook(() => useFocusTrap(true));
339
- const container = document.createElement("div");
340
- const btn1 = document.createElement("button");
341
- const btn2 = document.createElement("button");
342
- container.append(btn1, btn2);
343
- document.body.appendChild(container);
344
-
345
- result.current(container);
346
- vi.runAllTimers();
347
- btn2.focus();
348
-
349
- const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true });
350
- document.dispatchEvent(event);
351
-
352
- expect(document.activeElement).toBe(btn1);
353
- container.remove();
354
- });
355
- });
@@ -1,78 +0,0 @@
1
- import { useCallback, useEffect, useRef } from "react";
2
- import { scopeTab } from "./scope-tab";
3
- import { FOCUS_SELECTOR, focusable, tabbable } from "./tabbable";
4
-
5
- export function useFocusTrap(
6
- active = true,
7
- ): (instance: HTMLElement | null) => void {
8
- const ref = useRef<HTMLElement | null>(null);
9
-
10
- const focusNode = (node: HTMLElement) => {
11
- let focusElement: HTMLElement | null =
12
- node.querySelector("[data-autofocus]");
13
-
14
- if (!focusElement) {
15
- const children = Array.from<HTMLElement>(
16
- node.querySelectorAll(FOCUS_SELECTOR),
17
- );
18
- focusElement =
19
- children.find(tabbable) || children.find(focusable) || null;
20
- if (!focusElement && focusable(node)) {
21
- focusElement = node;
22
- }
23
- }
24
-
25
- if (focusElement) {
26
- focusElement.focus({ preventScroll: true });
27
- }
28
- };
29
-
30
- const setRef = useCallback(
31
- (node: HTMLElement | null) => {
32
- if (!active) {
33
- return;
34
- }
35
-
36
- if (node === null) {
37
- return;
38
- }
39
-
40
- if (ref.current === node) {
41
- return;
42
- }
43
-
44
- if (node) {
45
- // Delay processing the HTML node by a frame. This ensures focus is assigned correctly.
46
- setTimeout(() => {
47
- if (node.getRootNode()) {
48
- focusNode(node);
49
- }
50
- });
51
-
52
- ref.current = node;
53
- } else {
54
- ref.current = null;
55
- }
56
- },
57
- [active],
58
- );
59
-
60
- useEffect(() => {
61
- if (!active) {
62
- return undefined;
63
- }
64
-
65
- ref.current && setTimeout(() => focusNode(ref.current!));
66
-
67
- const handleKeyDown = (event: KeyboardEvent) => {
68
- if (event.key === "Tab" && ref.current) {
69
- scopeTab(ref.current, event);
70
- }
71
- };
72
-
73
- document.addEventListener("keydown", handleKeyDown);
74
- return () => document.removeEventListener("keydown", handleKeyDown);
75
- }, [active]);
76
-
77
- return setRef;
78
- }
@@ -1 +0,0 @@
1
- export * from "./use-mutation";
@@ -1,84 +0,0 @@
1
- import { useControllableState } from "@radix-ui/react-use-controllable-state";
2
-
3
- export type usePaginationProps = {
4
- /**
5
- * Cantidad total de elementos
6
- */
7
- totalItems: number;
8
- /**
9
- * Cantidad de elementos por página
10
- */
11
- pageSize: number;
12
- /**
13
- * Página controlada
14
- */
15
- currentPage?: number;
16
- /**
17
- * Página inicial (uncontrolled)
18
- */
19
- defaultCurrentPage?: number;
20
- /**
21
- * Callback cuando cambia la página
22
- */
23
- onCurrentPageChange?: (page: number) => void;
24
- /**
25
- * @deprecated Usá `defaultCurrentPage`
26
- */
27
- initialCurrentPage?: number;
28
- /**
29
- * @deprecated Usá `onCurrentPageChange`
30
- */
31
- onChange?: (value: { currentPage: number; pageSize: number }) => void;
32
- };
33
-
34
- export function usePagination({
35
- totalItems,
36
- pageSize,
37
- currentPage: currentPageProp,
38
- defaultCurrentPage,
39
- onCurrentPageChange,
40
- initialCurrentPage,
41
- onChange,
42
- }: usePaginationProps) {
43
- const [currentPage = 1, setCurrentPage] = useControllableState<number>({
44
- prop: currentPageProp,
45
- defaultProp: defaultCurrentPage ?? initialCurrentPage ?? 1,
46
- onChange: (page) => {
47
- onCurrentPageChange?.(page);
48
- onChange?.({ currentPage: page, pageSize });
49
- },
50
- });
51
-
52
- const maxPage = Math.max(1, Math.ceil(totalItems / pageSize));
53
- const isLastPage = currentPage >= maxPage;
54
- const isFirstPage = currentPage <= 1;
55
- const start = (currentPage - 1) * pageSize + 1;
56
- const end = Math.min(currentPage * pageSize, totalItems);
57
-
58
- const next = () => {
59
- if (isLastPage) return;
60
- setCurrentPage(Math.min(currentPage + 1, maxPage));
61
- };
62
-
63
- const prev = () => {
64
- if (isFirstPage) return;
65
- setCurrentPage(Math.max(currentPage - 1, 1));
66
- };
67
-
68
- const goTo = (page: number) => {
69
- const clamped = Math.min(Math.max(1, page), maxPage);
70
- if (clamped === currentPage) return;
71
- setCurrentPage(clamped);
72
- };
73
-
74
- return {
75
- next,
76
- prev,
77
- goTo,
78
- currentPage,
79
- maxPage,
80
- isFirstPage,
81
- isLastPage,
82
- range: { start, end },
83
- };
84
- }