@dust-tt/sparkle 0.2.225 → 0.2.227-rc

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 (52) hide show
  1. package/dist/cjs/index.js +283 -113
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/esm/components/DataTable.d.ts +5 -2
  4. package/dist/esm/components/DataTable.d.ts.map +1 -1
  5. package/dist/esm/components/DataTable.js +48 -29
  6. package/dist/esm/components/DataTable.js.map +1 -1
  7. package/dist/esm/components/Item.d.ts +6 -52
  8. package/dist/esm/components/Item.d.ts.map +1 -1
  9. package/dist/esm/components/Item.js.map +1 -1
  10. package/dist/esm/components/PaginatedCitationsGrid.d.ts.map +1 -1
  11. package/dist/esm/components/PaginatedCitationsGrid.js +8 -4
  12. package/dist/esm/components/PaginatedCitationsGrid.js.map +1 -1
  13. package/dist/esm/components/Pagination.d.ts +5 -4
  14. package/dist/esm/components/Pagination.d.ts.map +1 -1
  15. package/dist/esm/components/Pagination.js +35 -35
  16. package/dist/esm/components/Pagination.js.map +1 -1
  17. package/dist/esm/hooks/index.d.ts +3 -0
  18. package/dist/esm/hooks/index.d.ts.map +1 -0
  19. package/dist/esm/hooks/index.js +3 -0
  20. package/dist/esm/hooks/index.js.map +1 -0
  21. package/dist/esm/hooks/useHashParams.d.ts +18 -0
  22. package/dist/esm/hooks/useHashParams.d.ts.map +1 -0
  23. package/dist/esm/hooks/useHashParams.js +67 -0
  24. package/dist/esm/hooks/useHashParams.js.map +1 -0
  25. package/dist/esm/hooks/usePaginationFromUrl.d.ts +6 -0
  26. package/dist/esm/hooks/usePaginationFromUrl.d.ts.map +1 -0
  27. package/dist/esm/hooks/usePaginationFromUrl.js +23 -0
  28. package/dist/esm/hooks/usePaginationFromUrl.js.map +1 -0
  29. package/dist/esm/index.d.ts +1 -0
  30. package/dist/esm/index.d.ts.map +1 -1
  31. package/dist/esm/index.js +1 -0
  32. package/dist/esm/index.js.map +1 -1
  33. package/dist/esm/stories/DataTable.stories.d.ts +2 -0
  34. package/dist/esm/stories/DataTable.stories.d.ts.map +1 -1
  35. package/dist/esm/stories/DataTable.stories.js +133 -106
  36. package/dist/esm/stories/DataTable.stories.js.map +1 -1
  37. package/dist/esm/stories/Pagination.stories.d.ts +1 -0
  38. package/dist/esm/stories/Pagination.stories.d.ts.map +1 -1
  39. package/dist/esm/stories/Pagination.stories.js +25 -3
  40. package/dist/esm/stories/Pagination.stories.js.map +1 -1
  41. package/dist/sparkle.css +4 -0
  42. package/package.json +1 -1
  43. package/src/components/DataTable.tsx +114 -75
  44. package/src/components/Item.tsx +47 -52
  45. package/src/components/PaginatedCitationsGrid.tsx +11 -6
  46. package/src/components/Pagination.tsx +48 -45
  47. package/src/hooks/index.ts +2 -0
  48. package/src/hooks/useHashParams.ts +95 -0
  49. package/src/hooks/usePaginationFromUrl.ts +36 -0
  50. package/src/index.ts +1 -0
  51. package/src/stories/DataTable.stories.tsx +200 -130
  52. package/src/stories/Pagination.stories.tsx +38 -10
@@ -4,9 +4,12 @@ import {
4
4
  flexRender,
5
5
  getCoreRowModel,
6
6
  getFilteredRowModel,
7
+ getPaginationRowModel,
7
8
  getSortedRowModel,
9
+ PaginationState,
8
10
  SortingFn,
9
11
  type SortingState,
12
+ Updater,
10
13
  useReactTable,
11
14
  } from "@tanstack/react-table";
12
15
  import React, { ReactNode, useEffect, useState } from "react";
@@ -17,6 +20,7 @@ import {
17
20
  DropdownMenu,
18
21
  } from "@sparkle/components/DropdownMenu";
19
22
  import { IconButton } from "@sparkle/components/IconButton";
23
+ import { Pagination } from "@sparkle/components/Pagination";
20
24
  import { Tooltip } from "@sparkle/components/Tooltip";
21
25
  import { ArrowDownIcon, ArrowUpIcon, MoreIcon } from "@sparkle/icons";
22
26
  import { classNames } from "@sparkle/lib/utils";
@@ -35,10 +39,13 @@ interface ColumnBreakpoint {
35
39
 
36
40
  interface DataTableProps<TData extends TBaseData> {
37
41
  data: TData[];
42
+ totalRowCount?: number;
38
43
  columns: ColumnDef<TData, any>[]; // eslint-disable-line @typescript-eslint/no-explicit-any
39
44
  className?: string;
40
45
  filter?: string;
41
46
  filterColumn?: string;
47
+ pagination?: PaginationState;
48
+ setPagination?: (pagination: PaginationState) => void;
42
49
  initialColumnOrder?: SortingState;
43
50
  columnsBreakpoints?: ColumnBreakpoint;
44
51
  sortingFn?: SortingFn<TData>;
@@ -56,12 +63,15 @@ function shouldRenderColumn(
56
63
 
57
64
  export function DataTable<TData extends TBaseData>({
58
65
  data,
66
+ totalRowCount,
59
67
  columns,
60
68
  className,
61
69
  filter,
62
70
  filterColumn,
63
71
  initialColumnOrder,
64
72
  columnsBreakpoints = {},
73
+ pagination,
74
+ setPagination,
65
75
  }: DataTableProps<TData>) {
66
76
  const windowSize = useWindowSize();
67
77
  const [sorting, setSorting] = useState<SortingState>(
@@ -69,18 +79,36 @@ export function DataTable<TData extends TBaseData>({
69
79
  );
70
80
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
71
81
 
82
+ const isServerSidePagination = !!totalRowCount && totalRowCount > data.length;
83
+ const onPaginationChange =
84
+ pagination && setPagination
85
+ ? (updater: Updater<PaginationState>) => {
86
+ const newValue =
87
+ typeof updater === "function" ? updater(pagination) : updater;
88
+ setPagination(newValue);
89
+ }
90
+ : undefined;
91
+
72
92
  const table = useReactTable({
73
93
  data,
74
94
  columns,
95
+ rowCount: totalRowCount,
96
+ manualPagination: isServerSidePagination,
75
97
  onSortingChange: setSorting,
76
98
  getCoreRowModel: getCoreRowModel(),
77
99
  getSortedRowModel: getSortedRowModel(),
78
100
  getFilteredRowModel: getFilteredRowModel(),
101
+ getPaginationRowModel: pagination ? getPaginationRowModel() : undefined,
79
102
  onColumnFiltersChange: setColumnFilters,
80
103
  state: {
81
104
  columnFilters,
82
- sorting,
105
+ sorting: isServerSidePagination ? undefined : sorting,
106
+ pagination,
107
+ },
108
+ initialState: {
109
+ pagination,
83
110
  },
111
+ onPaginationChange: onPaginationChange,
84
112
  });
85
113
 
86
114
  useEffect(() => {
@@ -90,81 +118,92 @@ export function DataTable<TData extends TBaseData>({
90
118
  }, [filter, filterColumn]);
91
119
 
92
120
  return (
93
- <DataTable.Root className={className}>
94
- <DataTable.Header>
95
- {table.getHeaderGroups().map((headerGroup) => (
96
- <DataTable.Row key={headerGroup.id}>
97
- {headerGroup.headers.map((header) => {
98
- const breakpoint = columnsBreakpoints[header.id];
99
- if (
100
- !windowSize.width ||
101
- !shouldRenderColumn(windowSize.width, breakpoint)
102
- ) {
103
- return null;
104
- }
105
- return (
106
- <DataTable.Head
107
- key={header.id}
108
- onClick={header.column.getToggleSortingHandler()}
109
- className={classNames(
110
- header.column.getCanSort() ? "s-cursor-pointer" : ""
111
- )}
112
- >
113
- <div className="s-flex s-items-center s-space-x-1 s-whitespace-nowrap">
114
- {flexRender(
115
- header.column.columnDef.header,
116
- header.getContext()
117
- )}
118
- {header.column.getCanSort() && (
119
- <Icon
120
- visual={
121
- header.column.getIsSorted() === "asc"
122
- ? ArrowUpIcon
123
- : header.column.getIsSorted() === "desc"
124
- ? ArrowDownIcon
125
- : ArrowDownIcon
126
- }
127
- size="xs"
128
- className={classNames(
129
- "s-ml-1",
130
- header.column.getIsSorted()
131
- ? "s-opacity-100"
132
- : "s-opacity-0"
133
- )}
134
- />
121
+ <div className="s-flex s-flex-col s-gap-2">
122
+ <DataTable.Root className={className}>
123
+ <DataTable.Header>
124
+ {table.getHeaderGroups().map((headerGroup) => (
125
+ <DataTable.Row key={headerGroup.id}>
126
+ {headerGroup.headers.map((header) => {
127
+ const breakpoint = columnsBreakpoints[header.id];
128
+ if (
129
+ !windowSize.width ||
130
+ !shouldRenderColumn(windowSize.width, breakpoint)
131
+ ) {
132
+ return null;
133
+ }
134
+ return (
135
+ <DataTable.Head
136
+ key={header.id}
137
+ onClick={header.column.getToggleSortingHandler()}
138
+ className={classNames(
139
+ header.column.getCanSort() ? "s-cursor-pointer" : ""
135
140
  )}
136
- </div>
137
- </DataTable.Head>
138
- );
139
- })}
140
- </DataTable.Row>
141
- ))}
142
- </DataTable.Header>
143
- <DataTable.Body>
144
- {table.getRowModel().rows.map((row) => (
145
- <DataTable.Row
146
- key={row.id}
147
- onClick={row.original.onClick}
148
- moreMenuItems={row.original.moreMenuItems}
149
- >
150
- {row.getVisibleCells().map((cell) => {
151
- const breakpoint = columnsBreakpoints[cell.column.id];
152
- if (
153
- !windowSize.width ||
154
- !shouldRenderColumn(windowSize.width, breakpoint)
155
- ) {
156
- return null;
157
- }
158
- return (
159
- <DataTable.Cell key={cell.id}>
160
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
161
- </DataTable.Cell>
162
- );
163
- })}
164
- </DataTable.Row>
165
- ))}
166
- </DataTable.Body>
167
- </DataTable.Root>
141
+ >
142
+ <div className="s-flex s-items-center s-space-x-1 s-whitespace-nowrap">
143
+ {flexRender(
144
+ header.column.columnDef.header,
145
+ header.getContext()
146
+ )}
147
+ {header.column.getCanSort() && (
148
+ <Icon
149
+ visual={
150
+ header.column.getIsSorted() === "asc"
151
+ ? ArrowUpIcon
152
+ : header.column.getIsSorted() === "desc"
153
+ ? ArrowDownIcon
154
+ : ArrowDownIcon
155
+ }
156
+ size="xs"
157
+ className={classNames(
158
+ "s-ml-1",
159
+ header.column.getIsSorted()
160
+ ? "s-opacity-100"
161
+ : "s-opacity-0"
162
+ )}
163
+ />
164
+ )}
165
+ </div>
166
+ </DataTable.Head>
167
+ );
168
+ })}
169
+ </DataTable.Row>
170
+ ))}
171
+ </DataTable.Header>
172
+ <DataTable.Body>
173
+ {table.getRowModel().rows.map((row) => (
174
+ <DataTable.Row
175
+ key={row.id}
176
+ onClick={row.original.onClick}
177
+ moreMenuItems={row.original.moreMenuItems}
178
+ >
179
+ {row.getVisibleCells().map((cell) => {
180
+ const breakpoint = columnsBreakpoints[cell.column.id];
181
+ if (
182
+ !windowSize.width ||
183
+ !shouldRenderColumn(windowSize.width, breakpoint)
184
+ ) {
185
+ return null;
186
+ }
187
+ return (
188
+ <DataTable.Cell key={cell.id}>
189
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
190
+ </DataTable.Cell>
191
+ );
192
+ })}
193
+ </DataTable.Row>
194
+ ))}
195
+ </DataTable.Body>
196
+ </DataTable.Root>
197
+ {pagination && (
198
+ <div className="s-p-1">
199
+ <Pagination
200
+ pagination={table.getState().pagination}
201
+ setPagination={table.setPagination}
202
+ rowCount={table.getRowCount()}
203
+ />
204
+ </div>
205
+ )}
206
+ </div>
168
207
  );
169
208
  }
170
209
 
@@ -184,30 +184,26 @@ export function Item({
184
184
  );
185
185
  }
186
186
 
187
- interface EntryItemProps {
188
- onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
189
- disabled?: boolean;
190
- selected?: boolean;
191
- label: string;
192
- icon?: ComponentType;
193
- className?: string;
194
- href?: string;
195
- }
187
+ type EntryItemProps = Pick<
188
+ ItemProps,
189
+ "onClick" | "disabled" | "selected" | "label" | "icon" | "className" | "link"
190
+ >;
196
191
 
197
192
  Item.Entry = function (props: EntryItemProps) {
198
193
  return <Item {...props} spacing="sm" style="item" hasAction={"hover"} />;
199
194
  };
200
195
 
201
- interface AvatarItemProps {
202
- onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
203
- disabled?: boolean;
204
- label: string;
205
- description?: string;
206
- visual?: string | React.ReactNode;
207
- className?: string;
208
- hasAction?: boolean | "hover";
209
- href?: string;
210
- }
196
+ type AvatarItemProps = Pick<
197
+ ItemProps,
198
+ | "className"
199
+ | "description"
200
+ | "disabled"
201
+ | "hasAction"
202
+ | "label"
203
+ | "link"
204
+ | "onClick"
205
+ | "visual"
206
+ >;
211
207
 
212
208
  Item.Avatar = function ({
213
209
  description,
@@ -225,34 +221,28 @@ Item.Avatar = function ({
225
221
  );
226
222
  };
227
223
 
228
- interface NavigationListItemProps {
229
- onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
230
- selected?: boolean;
231
- disabled?: boolean;
232
- label: string;
233
- description?: string;
234
- icon?: ComponentType;
235
- className?: string;
236
- href?: string;
237
- target?: string;
238
- hasAction?: boolean | "hover";
239
- action?: ComponentType;
240
- }
224
+ type NavigationListItemProps = Pick<
225
+ ItemProps,
226
+ | "action"
227
+ | "className"
228
+ | "description"
229
+ | "disabled"
230
+ | "hasAction"
231
+ | "icon"
232
+ | "label"
233
+ | "link"
234
+ | "onClick"
235
+ | "selected"
236
+ >;
241
237
 
242
238
  Item.Navigation = function (props: NavigationListItemProps) {
243
239
  return <Item {...props} style="action" spacing="md" />;
244
240
  };
245
241
 
246
- interface LinkItemProps {
247
- onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
248
- label: string;
249
- description?: string;
250
- visual?: string | React.ReactNode;
251
- icon?: ComponentType;
252
- className?: string;
253
- href?: string;
254
- target?: string;
255
- }
242
+ type LinkItemProps = Pick<
243
+ ItemProps,
244
+ "onClick" | "label" | "description" | "visual" | "icon" | "className" | "link"
245
+ >;
256
246
 
257
247
  Item.Link = function ({ ...props }: LinkItemProps) {
258
248
  return (
@@ -267,19 +257,24 @@ Item.Link = function ({ ...props }: LinkItemProps) {
267
257
  );
268
258
  };
269
259
 
270
- interface DropdownListItemProps {
260
+ interface DropdownListItemBaseProps {
271
261
  style?: "default" | "warning";
272
- onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
273
- selected?: boolean;
274
- disabled?: boolean;
275
- label: string;
276
- description?: string;
277
- visual?: string | React.ReactNode;
278
- icon?: ComponentType;
279
- className?: string;
280
- link?: LinkProps;
281
262
  }
282
263
 
264
+ type DropdownListItemProps = DropdownListItemBaseProps &
265
+ Pick<
266
+ ItemProps,
267
+ | "className"
268
+ | "description"
269
+ | "disabled"
270
+ | "icon"
271
+ | "label"
272
+ | "link"
273
+ | "onClick"
274
+ | "selected"
275
+ | "visual"
276
+ >;
277
+
283
278
  Item.Dropdown = function ({ style, ...props }: DropdownListItemProps) {
284
279
  return (
285
280
  <Item
@@ -1,3 +1,4 @@
1
+ import { PaginationState } from "@tanstack/react-table";
1
2
  import { useState } from "react";
2
3
  import React from "react";
3
4
 
@@ -21,7 +22,10 @@ export function PaginatedCitationsGrid({
21
22
  items,
22
23
  maxItemsPerPage = 9,
23
24
  }: PaginatedCitationsGridProps) {
24
- const [currentPage, setCurrentPage] = useState(1);
25
+ const [pagination, setPagination] = useState<PaginationState>({
26
+ pageIndex: 0,
27
+ pageSize: maxItemsPerPage,
28
+ });
25
29
  const cols = 3;
26
30
  const rows = Math.ceil(Math.min(maxItemsPerPage, items.length) / cols);
27
31
 
@@ -29,10 +33,11 @@ export function PaginatedCitationsGrid({
29
33
  return null;
30
34
  }
31
35
 
36
+ const { pageIndex, pageSize } = pagination;
32
37
  // Calculate start index.
33
- const startIndex = (currentPage - 1) * maxItemsPerPage;
38
+ const startIndex = pageIndex * pageSize;
34
39
  // Slice items for current page.
35
- const paginatedItems = items.slice(startIndex, startIndex + maxItemsPerPage);
40
+ const paginatedItems = items.slice(startIndex, startIndex + pageSize);
36
41
 
37
42
  return (
38
43
  <div className="s-flex s-w-full s-flex-col">
@@ -65,12 +70,12 @@ export function PaginatedCitationsGrid({
65
70
  )}
66
71
  >
67
72
  <Pagination
68
- itemsCount={items.length}
69
- maxItemsPerPage={maxItemsPerPage}
73
+ rowCount={items.length}
74
+ pagination={pagination}
75
+ setPagination={setPagination}
70
76
  size="xs"
71
77
  showDetails={false}
72
78
  showPageButtons={false}
73
- onButtonClick={(pageNb) => setCurrentPage(pageNb)}
74
79
  />
75
80
  </div>
76
81
  </div>
@@ -1,5 +1,5 @@
1
+ import { PaginationState } from "@tanstack/react-table";
1
2
  import React, { useCallback } from "react";
2
- import { useState } from "react";
3
3
 
4
4
  import { ChevronLeftIcon, ChevronRightIcon } from "@sparkle/icons/solid";
5
5
  import { classNames } from "@sparkle/lib/utils";
@@ -11,42 +11,49 @@ type Size = "sm" | "xs";
11
11
  const pagesShownInControls = 7;
12
12
 
13
13
  interface PaginationProps {
14
- itemsCount: number;
15
- maxItemsPerPage: number;
16
- onButtonClick: (pageNumber: number) => void;
17
14
  size?: Size;
18
15
  showDetails?: boolean;
19
16
  showPageButtons?: boolean;
17
+ rowCount: number;
18
+ pagination: PaginationState;
19
+ setPagination: (pagination: PaginationState) => void;
20
20
  }
21
21
 
22
22
  export function Pagination({
23
- itemsCount,
24
- maxItemsPerPage,
25
- onButtonClick,
26
23
  size = "sm",
27
24
  showDetails = true,
28
25
  showPageButtons = true,
26
+ rowCount,
27
+ pagination,
28
+ setPagination,
29
29
  }: PaginationProps) {
30
- const numPages = Math.ceil(itemsCount / maxItemsPerPage);
30
+ // pageIndex is 0-based
31
+ const { pageIndex, pageSize } = pagination;
32
+
33
+ const numPages = Math.ceil(rowCount / pageSize);
34
+
35
+ const canNextPage = pagination.pageIndex < numPages - 1;
36
+ const canPreviousPage = pageIndex > 0;
37
+ const nextPage = () => setPagination({ pageSize, pageIndex: pageIndex + 1 });
38
+ const previousPage = () =>
39
+ setPagination({ pageSize, pageIndex: pageIndex - 1 });
40
+
31
41
  const controlsAreHidden = Boolean(numPages <= 1);
32
- const [currentPage, setCurrentPage] = useState(1);
33
- const firstFileOnPageIndex =
34
- currentPage * maxItemsPerPage - maxItemsPerPage + 1;
35
- const lastFileOnPageIndex =
36
- itemsCount > currentPage * maxItemsPerPage
37
- ? currentPage * maxItemsPerPage
38
- : itemsCount;
42
+ const firstItemOnPageIndex = pageIndex * pageSize + 1;
43
+ const lastItemOnPageIndex =
44
+ rowCount > (pageIndex + 1) * pageSize
45
+ ? (pageIndex + 1) * pageSize
46
+ : rowCount;
39
47
 
40
48
  const onPaginationButtonClick = useCallback(
41
- (pageNb: number) => {
42
- setCurrentPage(pageNb);
43
- onButtonClick(pageNb);
49
+ (pageIndex: number) => {
50
+ setPagination({ pageSize, pageIndex });
44
51
  },
45
- [onButtonClick, setCurrentPage]
52
+ [pageIndex, setPagination]
46
53
  );
47
54
 
48
55
  const pageButtons: React.ReactNode[] = getPageButtons(
49
- currentPage,
56
+ pageIndex,
50
57
  numPages,
51
58
  pagesShownInControls,
52
59
  onPaginationButtonClick,
@@ -73,11 +80,9 @@ export function Pagination({
73
80
  label="previous"
74
81
  labelVisible={false}
75
82
  disabledTooltip={true}
76
- disabled={currentPage === 1 ? true : false}
83
+ disabled={!canPreviousPage}
77
84
  icon={ChevronLeftIcon}
78
- onClick={() => {
79
- onPaginationButtonClick(currentPage - 1);
80
- }}
85
+ onClick={previousPage}
81
86
  />
82
87
 
83
88
  <div
@@ -96,11 +101,9 @@ export function Pagination({
96
101
  label="next"
97
102
  labelVisible={false}
98
103
  disabledTooltip={true}
99
- disabled={currentPage === numPages ? true : false}
104
+ disabled={!canNextPage}
100
105
  icon={ChevronRightIcon}
101
- onClick={() => {
102
- onPaginationButtonClick(currentPage + 1);
103
- }}
106
+ onClick={nextPage}
104
107
  />
105
108
  </div>
106
109
 
@@ -112,8 +115,8 @@ export function Pagination({
112
115
  )}
113
116
  >
114
117
  {controlsAreHidden
115
- ? `${itemsCount} items`
116
- : `Showing ${firstFileOnPageIndex}-${lastFileOnPageIndex} of ${itemsCount} items`}
118
+ ? `${rowCount} items`
119
+ : `Showing ${firstItemOnPageIndex}-${lastItemOnPageIndex} of ${rowCount} items`}
117
120
  </span>
118
121
  </div>
119
122
  );
@@ -135,7 +138,7 @@ function renderPageNumber(
135
138
  )}
136
139
  onClick={() => onPageClick(pageNumber)}
137
140
  >
138
- {pageNumber}
141
+ {pageNumber + 1}
139
142
  </button>
140
143
  );
141
144
  }
@@ -164,7 +167,7 @@ function getPageButtons(
164
167
 
165
168
  // If total pages are less than or equal to slots, show all pages
166
169
  if (totalPages <= slots) {
167
- for (let i = 1; i <= totalPages; i++) {
170
+ for (let i = 0; i < totalPages; i++) {
168
171
  pagination.push(renderPageNumber(i, currentPage, onPageClick, size));
169
172
  }
170
173
  return pagination;
@@ -174,25 +177,23 @@ function getPageButtons(
174
177
  const halfSlots = Math.floor(remainingSlots / 2);
175
178
 
176
179
  // Ensure current page is within bounds
177
- currentPage = Math.max(1, Math.min(currentPage, totalPages));
178
-
179
- pagination.push(renderPageNumber(1, currentPage, onPageClick, size)); // Always show the first page
180
+ currentPage = Math.max(0, Math.min(currentPage, totalPages - 1));
180
181
 
182
+ pagination.push(renderPageNumber(0, currentPage, onPageClick, size)); // Always show the first page
181
183
  // Determine the range of pages to display
182
184
  let start, end;
183
- if (currentPage <= halfSlots + 2) {
184
- start = 2;
185
- end = remainingSlots;
186
- } else if (currentPage >= totalPages - halfSlots - 1) {
187
- start = totalPages - remainingSlots + 1;
188
- end = totalPages - 1;
185
+ if (currentPage <= halfSlots + 1) {
186
+ start = 1;
187
+ end = remainingSlots - 1;
188
+ } else if (currentPage >= totalPages - halfSlots - 2) {
189
+ start = totalPages - remainingSlots;
190
+ end = totalPages - 2;
189
191
  } else {
190
192
  start = currentPage - halfSlots + 1;
191
193
  end = currentPage + halfSlots - 1;
192
194
  }
193
-
194
195
  // Add ellipsis if there is a gap between the first page and the start of the range
195
- if (start > 2) {
196
+ if (start > 1) {
196
197
  pagination.push(renderEllipses(size));
197
198
  }
198
199
 
@@ -202,11 +203,13 @@ function getPageButtons(
202
203
  }
203
204
 
204
205
  // Add ellipsis if there is a gap between the end of the range and the last page
205
- if (end < totalPages - 1) {
206
+ if (end < totalPages - 2) {
206
207
  pagination.push(renderEllipses(size));
207
208
  }
208
209
 
209
- pagination.push(renderPageNumber(totalPages, currentPage, onPageClick, size)); // Always show the last page
210
+ pagination.push(
211
+ renderPageNumber(totalPages - 1, currentPage, onPageClick, size)
212
+ ); // Always show the last page
210
213
 
211
214
  return pagination;
212
215
  }
@@ -0,0 +1,2 @@
1
+ export * from "./useHashParams";
2
+ export * from "./usePaginationFromUrl";