@dust-tt/sparkle 0.2.224 → 0.2.226

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 (71) hide show
  1. package/dist/cjs/index.js +293 -153
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/esm/components/Breadcrumbs.d.ts +7 -6
  4. package/dist/esm/components/Breadcrumbs.d.ts.map +1 -1
  5. package/dist/esm/components/Breadcrumbs.js +3 -2
  6. package/dist/esm/components/Breadcrumbs.js.map +1 -1
  7. package/dist/esm/components/DataTable.d.ts +5 -2
  8. package/dist/esm/components/DataTable.d.ts.map +1 -1
  9. package/dist/esm/components/DataTable.js +50 -33
  10. package/dist/esm/components/DataTable.js.map +1 -1
  11. package/dist/esm/components/DropdownMenu.d.ts +10 -10
  12. package/dist/esm/components/DropdownMenu.d.ts.map +1 -1
  13. package/dist/esm/components/DropdownMenu.js +7 -7
  14. package/dist/esm/components/DropdownMenu.js.map +1 -1
  15. package/dist/esm/components/Item.d.ts +18 -15
  16. package/dist/esm/components/Item.d.ts.map +1 -1
  17. package/dist/esm/components/Item.js +2 -1
  18. package/dist/esm/components/Item.js.map +1 -1
  19. package/dist/esm/components/PaginatedCitationsGrid.d.ts.map +1 -1
  20. package/dist/esm/components/PaginatedCitationsGrid.js +8 -4
  21. package/dist/esm/components/PaginatedCitationsGrid.js.map +1 -1
  22. package/dist/esm/components/Pagination.d.ts +5 -4
  23. package/dist/esm/components/Pagination.d.ts.map +1 -1
  24. package/dist/esm/components/Pagination.js +35 -35
  25. package/dist/esm/components/Pagination.js.map +1 -1
  26. package/dist/esm/hooks/index.d.ts +3 -0
  27. package/dist/esm/hooks/index.d.ts.map +1 -0
  28. package/dist/esm/hooks/index.js +3 -0
  29. package/dist/esm/hooks/index.js.map +1 -0
  30. package/dist/esm/hooks/useHashParams.d.ts +18 -0
  31. package/dist/esm/hooks/useHashParams.d.ts.map +1 -0
  32. package/dist/esm/hooks/useHashParams.js +67 -0
  33. package/dist/esm/hooks/useHashParams.js.map +1 -0
  34. package/dist/esm/hooks/usePaginationFromUrl.d.ts +6 -0
  35. package/dist/esm/hooks/usePaginationFromUrl.d.ts.map +1 -0
  36. package/dist/esm/hooks/usePaginationFromUrl.js +23 -0
  37. package/dist/esm/hooks/usePaginationFromUrl.js.map +1 -0
  38. package/dist/esm/index.d.ts +1 -0
  39. package/dist/esm/index.d.ts.map +1 -1
  40. package/dist/esm/index.js +1 -0
  41. package/dist/esm/index.js.map +1 -1
  42. package/dist/esm/stories/DataTable.stories.d.ts +2 -0
  43. package/dist/esm/stories/DataTable.stories.d.ts.map +1 -1
  44. package/dist/esm/stories/DataTable.stories.js +133 -105
  45. package/dist/esm/stories/DataTable.stories.js.map +1 -1
  46. package/dist/esm/stories/DropdownMenu.stories.d.ts.map +1 -1
  47. package/dist/esm/stories/DropdownMenu.stories.js +32 -31
  48. package/dist/esm/stories/DropdownMenu.stories.js.map +1 -1
  49. package/dist/esm/stories/Item.stories.d.ts.map +1 -1
  50. package/dist/esm/stories/Item.stories.js +8 -6
  51. package/dist/esm/stories/Item.stories.js.map +1 -1
  52. package/dist/esm/stories/Pagination.stories.d.ts +1 -0
  53. package/dist/esm/stories/Pagination.stories.d.ts.map +1 -1
  54. package/dist/esm/stories/Pagination.stories.js +25 -3
  55. package/dist/esm/stories/Pagination.stories.js.map +1 -1
  56. package/dist/sparkle.css +4 -0
  57. package/package.json +1 -1
  58. package/src/components/Breadcrumbs.tsx +11 -10
  59. package/src/components/DataTable.tsx +116 -81
  60. package/src/components/DropdownMenu.tsx +13 -13
  61. package/src/components/Item.tsx +20 -16
  62. package/src/components/PaginatedCitationsGrid.tsx +11 -6
  63. package/src/components/Pagination.tsx +48 -45
  64. package/src/hooks/index.ts +2 -0
  65. package/src/hooks/useHashParams.ts +95 -0
  66. package/src/hooks/usePaginationFromUrl.ts +36 -0
  67. package/src/index.ts +1 -0
  68. package/src/stories/DataTable.stories.tsx +200 -129
  69. package/src/stories/DropdownMenu.stories.tsx +43 -31
  70. package/src/stories/Item.stories.tsx +26 -6
  71. 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,8 +20,9 @@ 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";
24
+ import { Tooltip } from "@sparkle/components/Tooltip";
20
25
  import { ArrowDownIcon, ArrowUpIcon, MoreIcon } from "@sparkle/icons";
21
- import { Tooltip } from "@sparkle/index_with_tw_base";
22
26
  import { classNames } from "@sparkle/lib/utils";
23
27
 
24
28
  import { Icon } from "./Icon";
@@ -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()
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" : ""
117
140
  )}
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
- />
135
- )}
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
 
@@ -263,11 +302,7 @@ DataTable.Row = function Row({
263
302
  <td className="s-w-1 s-cursor-pointer s-pl-1 s-text-element-600">
264
303
  {moreMenuItems && moreMenuItems.length > 0 && (
265
304
  <DropdownMenu className="s-mr-1.5 s-flex">
266
- <DropdownMenu.Button
267
- onClick={(e) => {
268
- e.stopPropagation();
269
- }}
270
- >
305
+ <DropdownMenu.Button>
271
306
  <IconButton icon={MoreIcon} size="sm" variant="tertiary" />
272
307
  </DropdownMenu.Button>
273
308
  <DropdownMenu.Items origin="topRight" width={220}>
@@ -4,7 +4,6 @@ import React, {
4
4
  Fragment,
5
5
  JSXElementConstructor,
6
6
  MouseEvent,
7
- MouseEventHandler,
8
7
  MutableRefObject,
9
8
  ReactElement,
10
9
  useContext,
@@ -21,7 +20,7 @@ import {
21
20
  import { classNames } from "@sparkle/lib/utils";
22
21
 
23
22
  import { Icon } from "./Icon";
24
- import { Item as StandardItem } from "./Item";
23
+ import { Item as StandardItem, LinkProps } from "./Item";
25
24
  import { Tooltip, TooltipProps } from "./Tooltip";
26
25
 
27
26
  const ButtonRefContext =
@@ -103,7 +102,6 @@ export interface DropdownButtonProps {
103
102
  className?: string;
104
103
  disabled?: boolean;
105
104
  children?: React.ReactNode;
106
- onClick?: MouseEventHandler;
107
105
  }
108
106
 
109
107
  DropdownMenu.Button = function ({
@@ -116,7 +114,6 @@ DropdownMenu.Button = function ({
116
114
  tooltipPosition = "above",
117
115
  className = "",
118
116
  disabled = false,
119
- onClick,
120
117
  }: DropdownButtonProps) {
121
118
  const finalLabelClasses = classNames(
122
119
  labelClasses.base,
@@ -159,7 +156,7 @@ DropdownMenu.Button = function ({
159
156
  "s-group/dm s-flex s-justify-items-center s-text-sm s-font-medium focus:s-outline-none focus:s-ring-0",
160
157
  label ? "s-gap-1.5" : "s-gap-0"
161
158
  )}
162
- onClick={onClick}
159
+ onClick={(e) => e.stopPropagation()}
163
160
  >
164
161
  {tooltip ? (
165
162
  <Tooltip position={tooltipPosition} label={tooltip}>
@@ -192,6 +189,7 @@ DropdownMenu.Button = function ({
192
189
  "s-group/dm s-flex s-justify-items-center s-text-sm s-font-medium focus:s-outline-none focus:s-ring-0",
193
190
  label ? (size === "md" ? "s-gap-2" : "s-gap-1.5") : "s-gap-0.5"
194
191
  )}
192
+ onClick={(e) => e.stopPropagation()}
195
193
  >
196
194
  <Icon visual={icon} size={size} className={finalIconClasses} />
197
195
  <Icon
@@ -212,6 +210,7 @@ DropdownMenu.Button = function ({
212
210
  label ? (size === "md" ? "s-gap-2" : "s-gap-1.5") : "s-gap-0.5",
213
211
  type === "submenu" ? "s-opacity-50" : ""
214
212
  )}
213
+ onClick={(e) => e.stopPropagation()}
215
214
  >
216
215
  <Icon visual={icon} size={size} className={finalIconClasses} />
217
216
  <span
@@ -234,24 +233,24 @@ DropdownMenu.Button = function ({
234
233
  };
235
234
 
236
235
  export interface DropdownItemProps {
237
- variant?: "default" | "warning";
238
- label: string;
236
+ children?: React.ReactNode;
239
237
  description?: string;
240
- href?: string;
241
238
  disabled?: boolean;
242
- visual?: string | React.ReactNode;
239
+ hasChildren?: boolean;
243
240
  icon?: ComponentType;
241
+ label: string;
242
+ link?: LinkProps;
244
243
  onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
245
244
  selected?: boolean;
246
- hasChildren?: boolean;
247
- children?: React.ReactNode;
245
+ variant?: "default" | "warning";
246
+ visual?: string | React.ReactNode;
248
247
  }
249
248
 
250
249
  DropdownMenu.Item = function ({
251
250
  variant = "default",
252
251
  label,
253
252
  description,
254
- href,
253
+ link,
255
254
  disabled,
256
255
  visual,
257
256
  icon,
@@ -278,7 +277,7 @@ DropdownMenu.Item = function ({
278
277
  className="s-w-full"
279
278
  description={description}
280
279
  disabled={disabled}
281
- href={href}
280
+ link={link}
282
281
  icon={icon}
283
282
  label={label}
284
283
  onClick={onClick}
@@ -443,6 +442,7 @@ DropdownMenu.Items = function ({
443
442
  getOriginClass(origin),
444
443
  "s-rounded-xl s-border s-border-structure-100 s-bg-structure-0 s-shadow-lg focus:s-outline-none dark:s-border-structure-100-dark dark:s-bg-structure-0-dark"
445
444
  )}
445
+ onClick={(e) => e.stopPropagation()}
446
446
  style={styleInsert(origin, marginLeft)}
447
447
  >
448
448
  {topBar}
@@ -42,23 +42,27 @@ const iconClasses = {
42
42
  "s-text-warning-400 group-hover/item:s-text-warning-300 group-active/item:s-text-warning-700 dark:group-hover/item:s-text-warning-600-dark dark:group-active/item:s-text-warning-400-dark",
43
43
  };
44
44
 
45
- type ItemProps = {
45
+ export interface LinkProps {
46
+ href: string;
47
+ target?: string;
48
+ }
49
+
50
+ interface ItemProps {
51
+ action?: ComponentType;
52
+ className?: string;
53
+ description?: string;
54
+ disabled?: boolean;
55
+ hasAction?: boolean | "hover";
56
+ icon?: ComponentType;
57
+ label: string;
58
+ link?: LinkProps;
46
59
  onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
47
- style: "action" | "warning" | "item" | "link";
48
- spacing?: "sm" | "md" | "lg";
49
60
  selectable?: boolean;
50
61
  selected?: boolean;
51
- disabled?: boolean;
52
- label: string;
53
- description?: string;
62
+ spacing?: "sm" | "md" | "lg";
63
+ style: "action" | "warning" | "item" | "link";
54
64
  visual?: string | React.ReactNode;
55
- icon?: ComponentType;
56
- action?: ComponentType;
57
- hasAction?: boolean | "hover";
58
- className?: string;
59
- href?: string;
60
- target?: string;
61
- };
65
+ }
62
66
 
63
67
  export function Item({
64
68
  label,
@@ -73,11 +77,11 @@ export function Item({
73
77
  selected = false,
74
78
  disabled = false,
75
79
  className = "",
76
- href,
77
- target,
80
+ link,
78
81
  }: ItemProps) {
79
82
  const { components } = React.useContext(SparkleContext);
80
83
 
84
+ const { href, target } = link || {};
81
85
  const Link: SparkleContextLinkType = href ? components.link : noHrefLink;
82
86
 
83
87
  let visualElement: React.ReactNode;
@@ -273,7 +277,7 @@ interface DropdownListItemProps {
273
277
  visual?: string | React.ReactNode;
274
278
  icon?: ComponentType;
275
279
  className?: string;
276
- href?: string;
280
+ link?: LinkProps;
277
281
  }
278
282
 
279
283
  Item.Dropdown = function ({ style, ...props }: DropdownListItemProps) {
@@ -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";