@ttt-productions/ui-core 0.11.10 → 0.11.12
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.
- package/dist/react/components/list-pagination.d.ts +36 -0
- package/dist/react/components/list-pagination.d.ts.map +1 -0
- package/dist/react/components/list-pagination.js +35 -0
- package/dist/react/components/list-pagination.js.map +1 -0
- package/dist/react/hooks/use-cursor-page.d.ts +77 -0
- package/dist/react/hooks/use-cursor-page.d.ts.map +1 -0
- package/dist/react/hooks/use-cursor-page.js +74 -0
- package/dist/react/hooks/use-cursor-page.js.map +1 -0
- package/dist/react/hooks/use-paged-list.d.ts +60 -0
- package/dist/react/hooks/use-paged-list.d.ts.map +1 -0
- package/dist/react/hooks/use-paged-list.js +49 -0
- package/dist/react/hooks/use-paged-list.js.map +1 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -0
- package/dist/react/index.js.map +1 -1
- package/package.json +3 -3
- package/src/react/components/list-pagination.tsx +81 -0
- package/src/react/hooks/use-cursor-page.ts +139 -0
- package/src/react/hooks/use-paged-list.ts +106 -0
- package/src/react/index.ts +11 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ListPaginationState } from "../hooks/use-paged-list.js";
|
|
2
|
+
export interface ListPaginationProps {
|
|
3
|
+
/**
|
|
4
|
+
* Page state from `usePagedList` (known total),
|
|
5
|
+
* `useCursorPage().paginationFor(hasMore)` (unknown total), or any
|
|
6
|
+
* caller-owned state of the same shape.
|
|
7
|
+
*/
|
|
8
|
+
pagination: ListPaginationState;
|
|
9
|
+
/**
|
|
10
|
+
* Disables BOTH controls while the next page is in flight, without hiding the
|
|
11
|
+
* row. Server-paged surfaces pass their query's `isFetching`; a slice-paged
|
|
12
|
+
* list has nothing to wait for and omits it.
|
|
13
|
+
*/
|
|
14
|
+
busy?: boolean;
|
|
15
|
+
/** Optional extra classes. Merged with the component's own classes. */
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The ONE Previous / counter / Next control for paginated lists, in both
|
|
20
|
+
* flavors. Pair it with the hook that owns the page state (`usePagedList` for a
|
|
21
|
+
* client-side slice, `useCursorPage` — or the caller's own query state — for a
|
|
22
|
+
* cursor feed; a cursor feed passes `pager.paginationFor(hasMore)`, which binds
|
|
23
|
+
* the query's answer to these controls); this component owns only the controls,
|
|
24
|
+
* so every paginated list reads and behaves the same (ENG-002, FRONTEND-001).
|
|
25
|
+
*
|
|
26
|
+
* The counter follows the flavor: a known `totalPages` reads "2 of 5", an
|
|
27
|
+
* omitted one reads "Page 2". Everything else is identical in both flavors —
|
|
28
|
+
* which is exactly why there is one component and not two.
|
|
29
|
+
*
|
|
30
|
+
* It renders NOTHING when neither direction is available, so no call site
|
|
31
|
+
* repeats a `totalPages > 1` / `page > 1 || hasMore` guard. Edge state is real
|
|
32
|
+
* `disabled` on the buttons (announced, and unclickable), and the counter is a
|
|
33
|
+
* `status` live region so a page change is announced to screen readers.
|
|
34
|
+
*/
|
|
35
|
+
export declare function ListPagination({ pagination, busy, className }: ListPaginationProps): import("react").JSX.Element | null;
|
|
36
|
+
//# sourceMappingURL=list-pagination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-pagination.d.ts","sourceRoot":"","sources":["../../../src/react/components/list-pagination.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,UAAU,EAAE,mBAAmB,CAAC;IAChC;;;;OAIG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,EAAE,UAAU,EAAE,IAAY,EAAE,SAAS,EAAE,EAAE,mBAAmB,sCAuC1F"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
|
+
import { Button } from "./button.js";
|
|
5
|
+
import { cn } from "../../lib/utils.js";
|
|
6
|
+
/**
|
|
7
|
+
* The ONE Previous / counter / Next control for paginated lists, in both
|
|
8
|
+
* flavors. Pair it with the hook that owns the page state (`usePagedList` for a
|
|
9
|
+
* client-side slice, `useCursorPage` — or the caller's own query state — for a
|
|
10
|
+
* cursor feed; a cursor feed passes `pager.paginationFor(hasMore)`, which binds
|
|
11
|
+
* the query's answer to these controls); this component owns only the controls,
|
|
12
|
+
* so every paginated list reads and behaves the same (ENG-002, FRONTEND-001).
|
|
13
|
+
*
|
|
14
|
+
* The counter follows the flavor: a known `totalPages` reads "2 of 5", an
|
|
15
|
+
* omitted one reads "Page 2". Everything else is identical in both flavors —
|
|
16
|
+
* which is exactly why there is one component and not two.
|
|
17
|
+
*
|
|
18
|
+
* It renders NOTHING when neither direction is available, so no call site
|
|
19
|
+
* repeats a `totalPages > 1` / `page > 1 || hasMore` guard. Edge state is real
|
|
20
|
+
* `disabled` on the buttons (announced, and unclickable), and the counter is a
|
|
21
|
+
* `status` live region so a page change is announced to screen readers.
|
|
22
|
+
*/
|
|
23
|
+
export function ListPagination({ pagination, busy = false, className }) {
|
|
24
|
+
const { currentPage, totalPages, canPreviousPage, canNextPage, goToPreviousPage, goToNextPage } = pagination;
|
|
25
|
+
// One visibility rule for both flavors. For a known total this is exactly
|
|
26
|
+
// `totalPages > 1`: page state is clamped into `1..totalPages`, so a
|
|
27
|
+
// single-page list has no reachable neighbour in either direction, and a
|
|
28
|
+
// multi-page list always has one. For a cursor feed it is the hand-rolled
|
|
29
|
+
// `page > 1 || hasMore`. `busy` deliberately does not hide the row — the
|
|
30
|
+
// controls grey out in place instead of vanishing mid-fetch.
|
|
31
|
+
if (!canPreviousPage && !canNextPage)
|
|
32
|
+
return null;
|
|
33
|
+
return (_jsxs("div", { className: cn("flex w-full items-center justify-between gap-4 mt-4", className), children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: goToPreviousPage, disabled: busy || !canPreviousPage, className: "h-11 min-w-11", children: [_jsx(ChevronLeft, { className: "icon-xs mr-1" }), "Previous"] }), _jsx("span", { className: "text-counter", role: "status", children: totalPages === undefined ? `Page ${currentPage}` : `${currentPage} of ${totalPages}` }), _jsxs(Button, { variant: "outline", size: "sm", onClick: goToNextPage, disabled: busy || !canNextPage, className: "h-11 min-w-11", children: ["Next", _jsx(ChevronRight, { className: "icon-xs ml-1" })] })] }));
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=list-pagination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-pagination.js","sourceRoot":"","sources":["../../../src/react/components/list-pagination.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAC;AAoBxC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,UAAU,EAAE,IAAI,GAAG,KAAK,EAAE,SAAS,EAAuB;IACzF,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,GAC7F,UAAU,CAAC;IAEb,0EAA0E;IAC1E,qEAAqE;IACrE,yEAAyE;IACzE,0EAA0E;IAC1E,yEAAyE;IACzE,6DAA6D;IAC7D,IAAI,CAAC,eAAe,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAElD,OAAO,CACL,eAAK,SAAS,EAAE,EAAE,CAAC,qDAAqD,EAAE,SAAS,CAAC,aAClF,MAAC,MAAM,IACL,OAAO,EAAC,SAAS,EACjB,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,gBAAgB,EACzB,QAAQ,EAAE,IAAI,IAAI,CAAC,eAAe,EAClC,SAAS,EAAC,eAAe,aAEzB,KAAC,WAAW,IAAC,SAAS,EAAC,cAAc,GAAG,gBAEjC,EACT,eAAM,SAAS,EAAC,cAAc,EAAC,IAAI,EAAC,QAAQ,YACzC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,WAAW,EAAE,CAAC,CAAC,CAAC,GAAG,WAAW,OAAO,UAAU,EAAE,GAChF,EACP,MAAC,MAAM,IACL,OAAO,EAAC,SAAS,EACjB,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,YAAY,EACrB,QAAQ,EAAE,IAAI,IAAI,CAAC,WAAW,EAC9B,SAAS,EAAC,eAAe,qBAGzB,KAAC,YAAY,IAAC,SAAS,EAAC,cAAc,GAAG,IAClC,IACL,CACP,CAAC;AACJ,CAAC","sourcesContent":["\"use client\";\n\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { Button } from \"./button.js\";\nimport { cn } from \"../../lib/utils.js\";\nimport type { ListPaginationState } from \"../hooks/use-paged-list.js\";\n\nexport interface ListPaginationProps {\n /**\n * Page state from `usePagedList` (known total),\n * `useCursorPage().paginationFor(hasMore)` (unknown total), or any\n * caller-owned state of the same shape.\n */\n pagination: ListPaginationState;\n /**\n * Disables BOTH controls while the next page is in flight, without hiding the\n * row. Server-paged surfaces pass their query's `isFetching`; a slice-paged\n * list has nothing to wait for and omits it.\n */\n busy?: boolean;\n /** Optional extra classes. Merged with the component's own classes. */\n className?: string;\n}\n\n/**\n * The ONE Previous / counter / Next control for paginated lists, in both\n * flavors. Pair it with the hook that owns the page state (`usePagedList` for a\n * client-side slice, `useCursorPage` — or the caller's own query state — for a\n * cursor feed; a cursor feed passes `pager.paginationFor(hasMore)`, which binds\n * the query's answer to these controls); this component owns only the controls,\n * so every paginated list reads and behaves the same (ENG-002, FRONTEND-001).\n *\n * The counter follows the flavor: a known `totalPages` reads \"2 of 5\", an\n * omitted one reads \"Page 2\". Everything else is identical in both flavors —\n * which is exactly why there is one component and not two.\n *\n * It renders NOTHING when neither direction is available, so no call site\n * repeats a `totalPages > 1` / `page > 1 || hasMore` guard. Edge state is real\n * `disabled` on the buttons (announced, and unclickable), and the counter is a\n * `status` live region so a page change is announced to screen readers.\n */\nexport function ListPagination({ pagination, busy = false, className }: ListPaginationProps) {\n const { currentPage, totalPages, canPreviousPage, canNextPage, goToPreviousPage, goToNextPage } =\n pagination;\n\n // One visibility rule for both flavors. For a known total this is exactly\n // `totalPages > 1`: page state is clamped into `1..totalPages`, so a\n // single-page list has no reachable neighbour in either direction, and a\n // multi-page list always has one. For a cursor feed it is the hand-rolled\n // `page > 1 || hasMore`. `busy` deliberately does not hide the row — the\n // controls grey out in place instead of vanishing mid-fetch.\n if (!canPreviousPage && !canNextPage) return null;\n\n return (\n <div className={cn(\"flex w-full items-center justify-between gap-4 mt-4\", className)}>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={goToPreviousPage}\n disabled={busy || !canPreviousPage}\n className=\"h-11 min-w-11\"\n >\n <ChevronLeft className=\"icon-xs mr-1\" />\n Previous\n </Button>\n <span className=\"text-counter\" role=\"status\">\n {totalPages === undefined ? `Page ${currentPage}` : `${currentPage} of ${totalPages}`}\n </span>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={goToNextPage}\n disabled={busy || !canNextPage}\n className=\"h-11 min-w-11\"\n >\n Next\n <ChevronRight className=\"icon-xs ml-1\" />\n </Button>\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ListPaginationState } from "./use-paged-list.js";
|
|
2
|
+
/**
|
|
3
|
+
* Render-time page state for a cursor feed, shaped as {@link ListPaginationState}
|
|
4
|
+
* so it drops straight into the ONE `ListPagination` control. Produced by
|
|
5
|
+
* {@link CursorPager.paginationFor} once the query for `currentPage` has
|
|
6
|
+
* answered with its `hasMore`.
|
|
7
|
+
*/
|
|
8
|
+
export interface CursorPage extends ListPaginationState {
|
|
9
|
+
/** Never known for cursor pagination — the counter reads "Page N". */
|
|
10
|
+
totalPages?: undefined;
|
|
11
|
+
}
|
|
12
|
+
export interface UseCursorPageOptions {
|
|
13
|
+
/** Called after the page actually changed. Same contract as `usePagedList`. */
|
|
14
|
+
onPageChange?: () => void;
|
|
15
|
+
/**
|
|
16
|
+
* A snapshot of the query inputs the page number belongs to — a tag, a filter,
|
|
17
|
+
* a sort. When it changes, the page is 1 again **in the same render**, so the
|
|
18
|
+
* query never fires for "page 4" of inputs whose page 4 may not exist.
|
|
19
|
+
*
|
|
20
|
+
* Use this when the input change arrives as a prop (there is no event handler
|
|
21
|
+
* of yours to hang the reset on); use {@link CursorPager.reset} when your own
|
|
22
|
+
* handler already owns the change. Compose several inputs into one string
|
|
23
|
+
* (`` `${type}|${projectId ?? ""}` ``) — the type is deliberately narrowed to
|
|
24
|
+
* primitives so a fresh object literal cannot pin the list to page 1 forever.
|
|
25
|
+
*/
|
|
26
|
+
resetKey?: string | number | boolean | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* What {@link useCursorPage} owns: the page number, the guarded step controls,
|
|
30
|
+
* and the return-to-page-1 control.
|
|
31
|
+
*/
|
|
32
|
+
export interface CursorPager {
|
|
33
|
+
/** 1-based page to feed into the paged query. */
|
|
34
|
+
currentPage: number;
|
|
35
|
+
/**
|
|
36
|
+
* Return to page 1 without walking back. Call it from the handler that changes
|
|
37
|
+
* the query's inputs (a new filter, a new tag, a new sort) — page 4 of the old
|
|
38
|
+
* result set is not page 4 of the new one. A no-op on page 1, so it never
|
|
39
|
+
* fires `onPageChange` for nothing.
|
|
40
|
+
*/
|
|
41
|
+
reset: () => void;
|
|
42
|
+
/**
|
|
43
|
+
* Bind the query's answer to the controls: pass the `hasMore` that came back
|
|
44
|
+
* for {@link currentPage} and render the result as
|
|
45
|
+
* `<ListPagination pagination={…} />`. `hasMore` binds HERE, after the query,
|
|
46
|
+
* because that is the only moment it exists — a cursor query needs
|
|
47
|
+
* `currentPage` as its input, so `hasMore` cannot be an argument to the hook
|
|
48
|
+
* that produces `currentPage`.
|
|
49
|
+
*/
|
|
50
|
+
paginationFor: (hasMore: boolean) => CursorPage;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Page state for a server-paged (cursor / `hasMore`) list: the 1-based page
|
|
54
|
+
* number, the guarded step controls, and the reset — the pieces each
|
|
55
|
+
* server-paged surface was otherwise re-declaring as a local `useState(1)` plus
|
|
56
|
+
* its own clamped increment/decrement and its own "reset when the filter
|
|
57
|
+
* changed" adjustment (ENG-002, ENG-003).
|
|
58
|
+
*
|
|
59
|
+
* The data stays the caller's, and the two halves bind at different moments:
|
|
60
|
+
*
|
|
61
|
+
* ```tsx
|
|
62
|
+
* const pager = useCursorPage({ resetKey: tag });
|
|
63
|
+
* const { data, isFetching } = useThingsByTag(tag, pager.currentPage);
|
|
64
|
+
* …
|
|
65
|
+
* <ListPagination pagination={pager.paginationFor(data?.hasMore ?? false)} busy={isFetching} />
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* The guards live in here, not at the call site: a step that cannot move the
|
|
69
|
+
* page changes nothing and does not fire `onPageChange`.
|
|
70
|
+
*
|
|
71
|
+
* A surface whose data hook ALREADY owns its page state (it returns
|
|
72
|
+
* `page`/`hasNextPage`/`nextPage`/`prevPage` itself) does not need this hook —
|
|
73
|
+
* it maps those fields onto {@link ListPaginationState} directly. The hook
|
|
74
|
+
* exists for the surfaces that would otherwise hand-roll the state.
|
|
75
|
+
*/
|
|
76
|
+
export declare function useCursorPage(options?: UseCursorPageOptions): CursorPager;
|
|
77
|
+
//# sourceMappingURL=use-cursor-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-cursor-page.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-cursor-page.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE/D;;;;;GAKG;AACH,MAAM,WAAW,UAAW,SAAQ,mBAAmB;IACrD,sEAAsE;IACtE,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;CAC7C;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,iDAAiD;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;;;;;;OAOG;IACH,aAAa,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,UAAU,CAAC;CACjD;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,WAAW,CAwD7E"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Page state for a server-paged (cursor / `hasMore`) list: the 1-based page
|
|
5
|
+
* number, the guarded step controls, and the reset — the pieces each
|
|
6
|
+
* server-paged surface was otherwise re-declaring as a local `useState(1)` plus
|
|
7
|
+
* its own clamped increment/decrement and its own "reset when the filter
|
|
8
|
+
* changed" adjustment (ENG-002, ENG-003).
|
|
9
|
+
*
|
|
10
|
+
* The data stays the caller's, and the two halves bind at different moments:
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const pager = useCursorPage({ resetKey: tag });
|
|
14
|
+
* const { data, isFetching } = useThingsByTag(tag, pager.currentPage);
|
|
15
|
+
* …
|
|
16
|
+
* <ListPagination pagination={pager.paginationFor(data?.hasMore ?? false)} busy={isFetching} />
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* The guards live in here, not at the call site: a step that cannot move the
|
|
20
|
+
* page changes nothing and does not fire `onPageChange`.
|
|
21
|
+
*
|
|
22
|
+
* A surface whose data hook ALREADY owns its page state (it returns
|
|
23
|
+
* `page`/`hasNextPage`/`nextPage`/`prevPage` itself) does not need this hook —
|
|
24
|
+
* it maps those fields onto {@link ListPaginationState} directly. The hook
|
|
25
|
+
* exists for the surfaces that would otherwise hand-roll the state.
|
|
26
|
+
*/
|
|
27
|
+
export function useCursorPage(options = {}) {
|
|
28
|
+
const { onPageChange, resetKey } = options;
|
|
29
|
+
// The stored page is only meaningful for the inputs it was reached under, so
|
|
30
|
+
// the key it belongs to is stored WITH it.
|
|
31
|
+
const [pageState, setPageState] = useState({ page: 1, key: resetKey });
|
|
32
|
+
// `Object.is`, the same comparison React uses for state and deps: a `NaN` key
|
|
33
|
+
// must compare equal to itself, or the adjustment below would re-fire forever.
|
|
34
|
+
const keyMatches = Object.is(pageState.key, resetKey);
|
|
35
|
+
// React's "adjusting state when a prop changes": a new key retires the stored
|
|
36
|
+
// page, because page 4 of the old result set is not page 4 of the new one.
|
|
37
|
+
// Retiring it (rather than only ignoring it) is what makes going BACK to
|
|
38
|
+
// earlier inputs start at page 1 too, instead of reviving a dead page.
|
|
39
|
+
if (!keyMatches) {
|
|
40
|
+
setPageState({ page: 1, key: resetKey });
|
|
41
|
+
}
|
|
42
|
+
// Derived as well as adjusted, so even the render that triggers the adjustment
|
|
43
|
+
// already reports page 1 — the caller's query is never asked for a page of the
|
|
44
|
+
// new inputs that the user never navigated to.
|
|
45
|
+
const currentPage = keyMatches ? pageState.page : 1;
|
|
46
|
+
const goToPreviousPage = useCallback(() => {
|
|
47
|
+
if (currentPage <= 1)
|
|
48
|
+
return;
|
|
49
|
+
setPageState({ page: currentPage - 1, key: resetKey });
|
|
50
|
+
onPageChange?.();
|
|
51
|
+
}, [currentPage, resetKey, onPageChange]);
|
|
52
|
+
const reset = useCallback(() => {
|
|
53
|
+
if (currentPage === 1)
|
|
54
|
+
return;
|
|
55
|
+
setPageState({ page: 1, key: resetKey });
|
|
56
|
+
onPageChange?.();
|
|
57
|
+
}, [currentPage, resetKey, onPageChange]);
|
|
58
|
+
// Memoized on the page state itself, so a consumer may hold it in a dep array
|
|
59
|
+
// and it changes only when the page (or the callback/inputs) actually change.
|
|
60
|
+
const paginationFor = useCallback((hasMore) => ({
|
|
61
|
+
currentPage,
|
|
62
|
+
canPreviousPage: currentPage > 1,
|
|
63
|
+
canNextPage: hasMore,
|
|
64
|
+
goToPreviousPage,
|
|
65
|
+
goToNextPage: () => {
|
|
66
|
+
if (!hasMore)
|
|
67
|
+
return;
|
|
68
|
+
setPageState({ page: currentPage + 1, key: resetKey });
|
|
69
|
+
onPageChange?.();
|
|
70
|
+
},
|
|
71
|
+
}), [currentPage, resetKey, onPageChange, goToPreviousPage]);
|
|
72
|
+
return { currentPage, reset, paginationFor };
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=use-cursor-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-cursor-page.js","sourceRoot":"","sources":["../../../src/react/hooks/use-cursor-page.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAwD9C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,aAAa,CAAC,UAAgC,EAAE;IAC9D,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;IAE3C,6EAA6E;IAC7E,2CAA2C;IAC3C,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAGvC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;IAE/B,8EAA8E;IAC9E,+EAA+E;IAC/E,MAAM,UAAU,GAAG,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAEtD,8EAA8E;IAC9E,2EAA2E;IAC3E,yEAAyE;IACzE,uEAAuE;IACvE,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,YAAY,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC3C,CAAC;IACD,+EAA+E;IAC/E,+EAA+E;IAC/E,+CAA+C;IAC/C,MAAM,WAAW,GAAG,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAEpD,MAAM,gBAAgB,GAAG,WAAW,CAAC,GAAG,EAAE;QACxC,IAAI,WAAW,IAAI,CAAC;YAAE,OAAO;QAC7B,YAAY,CAAC,EAAE,IAAI,EAAE,WAAW,GAAG,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;QACvD,YAAY,EAAE,EAAE,CAAC;IACnB,CAAC,EAAE,CAAC,WAAW,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC;IAE1C,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,IAAI,WAAW,KAAK,CAAC;YAAE,OAAO;QAC9B,YAAY,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;QACzC,YAAY,EAAE,EAAE,CAAC;IACnB,CAAC,EAAE,CAAC,WAAW,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC;IAE1C,8EAA8E;IAC9E,8EAA8E;IAC9E,MAAM,aAAa,GAAG,WAAW,CAC/B,CAAC,OAAgB,EAAc,EAAE,CAAC,CAAC;QACjC,WAAW;QACX,eAAe,EAAE,WAAW,GAAG,CAAC;QAChC,WAAW,EAAE,OAAO;QACpB,gBAAgB;QAChB,YAAY,EAAE,GAAG,EAAE;YACjB,IAAI,CAAC,OAAO;gBAAE,OAAO;YACrB,YAAY,CAAC,EAAE,IAAI,EAAE,WAAW,GAAG,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;YACvD,YAAY,EAAE,EAAE,CAAC;QACnB,CAAC;KACF,CAAC,EACF,CAAC,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,gBAAgB,CAAC,CACxD,CAAC;IAEF,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC;AAC/C,CAAC","sourcesContent":["\"use client\";\n\nimport { useCallback, useState } from \"react\";\nimport type { ListPaginationState } from \"./use-paged-list.js\";\n\n/**\n * Render-time page state for a cursor feed, shaped as {@link ListPaginationState}\n * so it drops straight into the ONE `ListPagination` control. Produced by\n * {@link CursorPager.paginationFor} once the query for `currentPage` has\n * answered with its `hasMore`.\n */\nexport interface CursorPage extends ListPaginationState {\n /** Never known for cursor pagination — the counter reads \"Page N\". */\n totalPages?: undefined;\n}\n\nexport interface UseCursorPageOptions {\n /** Called after the page actually changed. Same contract as `usePagedList`. */\n onPageChange?: () => void;\n /**\n * A snapshot of the query inputs the page number belongs to — a tag, a filter,\n * a sort. When it changes, the page is 1 again **in the same render**, so the\n * query never fires for \"page 4\" of inputs whose page 4 may not exist.\n *\n * Use this when the input change arrives as a prop (there is no event handler\n * of yours to hang the reset on); use {@link CursorPager.reset} when your own\n * handler already owns the change. Compose several inputs into one string\n * (`` `${type}|${projectId ?? \"\"}` ``) — the type is deliberately narrowed to\n * primitives so a fresh object literal cannot pin the list to page 1 forever.\n */\n resetKey?: string | number | boolean | null;\n}\n\n/**\n * What {@link useCursorPage} owns: the page number, the guarded step controls,\n * and the return-to-page-1 control.\n */\nexport interface CursorPager {\n /** 1-based page to feed into the paged query. */\n currentPage: number;\n /**\n * Return to page 1 without walking back. Call it from the handler that changes\n * the query's inputs (a new filter, a new tag, a new sort) — page 4 of the old\n * result set is not page 4 of the new one. A no-op on page 1, so it never\n * fires `onPageChange` for nothing.\n */\n reset: () => void;\n /**\n * Bind the query's answer to the controls: pass the `hasMore` that came back\n * for {@link currentPage} and render the result as\n * `<ListPagination pagination={…} />`. `hasMore` binds HERE, after the query,\n * because that is the only moment it exists — a cursor query needs\n * `currentPage` as its input, so `hasMore` cannot be an argument to the hook\n * that produces `currentPage`.\n */\n paginationFor: (hasMore: boolean) => CursorPage;\n}\n\n/**\n * Page state for a server-paged (cursor / `hasMore`) list: the 1-based page\n * number, the guarded step controls, and the reset — the pieces each\n * server-paged surface was otherwise re-declaring as a local `useState(1)` plus\n * its own clamped increment/decrement and its own \"reset when the filter\n * changed\" adjustment (ENG-002, ENG-003).\n *\n * The data stays the caller's, and the two halves bind at different moments:\n *\n * ```tsx\n * const pager = useCursorPage({ resetKey: tag });\n * const { data, isFetching } = useThingsByTag(tag, pager.currentPage);\n * …\n * <ListPagination pagination={pager.paginationFor(data?.hasMore ?? false)} busy={isFetching} />\n * ```\n *\n * The guards live in here, not at the call site: a step that cannot move the\n * page changes nothing and does not fire `onPageChange`.\n *\n * A surface whose data hook ALREADY owns its page state (it returns\n * `page`/`hasNextPage`/`nextPage`/`prevPage` itself) does not need this hook —\n * it maps those fields onto {@link ListPaginationState} directly. The hook\n * exists for the surfaces that would otherwise hand-roll the state.\n */\nexport function useCursorPage(options: UseCursorPageOptions = {}): CursorPager {\n const { onPageChange, resetKey } = options;\n\n // The stored page is only meaningful for the inputs it was reached under, so\n // the key it belongs to is stored WITH it.\n const [pageState, setPageState] = useState<{\n page: number;\n key: UseCursorPageOptions[\"resetKey\"];\n }>({ page: 1, key: resetKey });\n\n // `Object.is`, the same comparison React uses for state and deps: a `NaN` key\n // must compare equal to itself, or the adjustment below would re-fire forever.\n const keyMatches = Object.is(pageState.key, resetKey);\n\n // React's \"adjusting state when a prop changes\": a new key retires the stored\n // page, because page 4 of the old result set is not page 4 of the new one.\n // Retiring it (rather than only ignoring it) is what makes going BACK to\n // earlier inputs start at page 1 too, instead of reviving a dead page.\n if (!keyMatches) {\n setPageState({ page: 1, key: resetKey });\n }\n // Derived as well as adjusted, so even the render that triggers the adjustment\n // already reports page 1 — the caller's query is never asked for a page of the\n // new inputs that the user never navigated to.\n const currentPage = keyMatches ? pageState.page : 1;\n\n const goToPreviousPage = useCallback(() => {\n if (currentPage <= 1) return;\n setPageState({ page: currentPage - 1, key: resetKey });\n onPageChange?.();\n }, [currentPage, resetKey, onPageChange]);\n\n const reset = useCallback(() => {\n if (currentPage === 1) return;\n setPageState({ page: 1, key: resetKey });\n onPageChange?.();\n }, [currentPage, resetKey, onPageChange]);\n\n // Memoized on the page state itself, so a consumer may hold it in a dep array\n // and it changes only when the page (or the callback/inputs) actually change.\n const paginationFor = useCallback(\n (hasMore: boolean): CursorPage => ({\n currentPage,\n canPreviousPage: currentPage > 1,\n canNextPage: hasMore,\n goToPreviousPage,\n goToNextPage: () => {\n if (!hasMore) return;\n setPageState({ page: currentPage + 1, key: resetKey });\n onPageChange?.();\n },\n }),\n [currentPage, resetKey, onPageChange, goToPreviousPage],\n );\n\n return { currentPage, reset, paginationFor };\n}\n"]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The control state a paginated list exposes to its paginator UI, rendered by
|
|
3
|
+
* the ONE `ListPagination` component. Nothing else builds
|
|
4
|
+
* Previous / counter / Next controls.
|
|
5
|
+
*
|
|
6
|
+
* `totalPages` is the flavor discriminant, and the ONLY difference between the
|
|
7
|
+
* two flavors:
|
|
8
|
+
*
|
|
9
|
+
* - **Known total** (client-side slice pagination — {@link usePagedList}):
|
|
10
|
+
* `totalPages` is a number and the counter reads `"2 of 5"`.
|
|
11
|
+
* - **Unknown total** (server/cursor pagination — `useCursorPage().paginationFor(hasMore)`,
|
|
12
|
+
* or a caller's own query hook): `totalPages` is omitted and the counter reads
|
|
13
|
+
* `"Page 2"`.
|
|
14
|
+
*
|
|
15
|
+
* Both flavors share one visibility rule, one disabled-edge rule, one live
|
|
16
|
+
* region, and one set of classes because they share one component (ENG-002,
|
|
17
|
+
* FRONTEND-001).
|
|
18
|
+
*/
|
|
19
|
+
export interface ListPaginationState {
|
|
20
|
+
/** 1-based page currently on screen. */
|
|
21
|
+
currentPage: number;
|
|
22
|
+
/**
|
|
23
|
+
* Total page count when it is knowable — at least 1, even for an empty list
|
|
24
|
+
* (an empty list is one empty page). Omitted for cursor/`hasMore` pagination,
|
|
25
|
+
* where the total is unknown until the last page is reached.
|
|
26
|
+
*/
|
|
27
|
+
totalPages?: number;
|
|
28
|
+
canPreviousPage: boolean;
|
|
29
|
+
canNextPage: boolean;
|
|
30
|
+
goToPreviousPage: () => void;
|
|
31
|
+
goToNextPage: () => void;
|
|
32
|
+
}
|
|
33
|
+
export interface PagedList<T> extends ListPaginationState {
|
|
34
|
+
/** Always known for a slice-paginated list. */
|
|
35
|
+
totalPages: number;
|
|
36
|
+
/** The slice of `items` belonging to `currentPage`. */
|
|
37
|
+
pageItems: T[];
|
|
38
|
+
}
|
|
39
|
+
export interface UsePagedListOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Called after the page actually changed. Surfaces that keep a per-page
|
|
42
|
+
* selection (an open detail row) clear it here — the old selection is not on
|
|
43
|
+
* the new page.
|
|
44
|
+
*/
|
|
45
|
+
onPageChange?: () => void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The single owner of client-side slice pagination: local page state, the page
|
|
49
|
+
* math, and the current slice over an ALREADY-FETCHED list.
|
|
50
|
+
*
|
|
51
|
+
* Page size is the caller's — every surface passes its own page-size constant
|
|
52
|
+
* (ENG-005), never a literal. Ordering and filtering stay with the caller: pass
|
|
53
|
+
* the list already sorted and filtered.
|
|
54
|
+
*
|
|
55
|
+
* Not for server-paged lists — a cursor/`hasMore` feed pages through the
|
|
56
|
+
* caller's data layer, not through a slice; use `useCursorPage` (or the query
|
|
57
|
+
* hook's own page state) with the same `ListPagination` control.
|
|
58
|
+
*/
|
|
59
|
+
export declare function usePagedList<T>(items: readonly T[], pageSize: number, options?: UsePagedListOptions): PagedList<T>;
|
|
60
|
+
//# sourceMappingURL=use-paged-list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-paged-list.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-paged-list.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,mBAAmB;IAClC,wCAAwC;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;IACzB,WAAW,EAAE,OAAO,CAAC;IACrB,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS,CAAC,CAAC,CAAE,SAAQ,mBAAmB;IACvD,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,SAAS,EAAE,CAAC,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAC5B,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,mBAAwB,GAChC,SAAS,CAAC,CAAC,CAAC,CAoCd"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* The single owner of client-side slice pagination: local page state, the page
|
|
5
|
+
* math, and the current slice over an ALREADY-FETCHED list.
|
|
6
|
+
*
|
|
7
|
+
* Page size is the caller's — every surface passes its own page-size constant
|
|
8
|
+
* (ENG-005), never a literal. Ordering and filtering stay with the caller: pass
|
|
9
|
+
* the list already sorted and filtered.
|
|
10
|
+
*
|
|
11
|
+
* Not for server-paged lists — a cursor/`hasMore` feed pages through the
|
|
12
|
+
* caller's data layer, not through a slice; use `useCursorPage` (or the query
|
|
13
|
+
* hook's own page state) with the same `ListPagination` control.
|
|
14
|
+
*/
|
|
15
|
+
export function usePagedList(items, pageSize, options = {}) {
|
|
16
|
+
const { onPageChange } = options;
|
|
17
|
+
const [requestedPage, setRequestedPage] = useState(1);
|
|
18
|
+
const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
|
|
19
|
+
// The list can shrink under a page that is already open (an item is deleted,
|
|
20
|
+
// a filter narrows). Clamping keeps the paginator on a real page instead of
|
|
21
|
+
// rendering an empty slice under an out-of-range "4 of 2".
|
|
22
|
+
const currentPage = Math.min(Math.max(1, requestedPage), totalPages);
|
|
23
|
+
const pageItems = useMemo(() => {
|
|
24
|
+
const startIndex = (currentPage - 1) * pageSize;
|
|
25
|
+
return items.slice(startIndex, startIndex + pageSize);
|
|
26
|
+
}, [items, currentPage, pageSize]);
|
|
27
|
+
const goToPreviousPage = useCallback(() => {
|
|
28
|
+
if (currentPage <= 1)
|
|
29
|
+
return;
|
|
30
|
+
setRequestedPage(currentPage - 1);
|
|
31
|
+
onPageChange?.();
|
|
32
|
+
}, [currentPage, onPageChange]);
|
|
33
|
+
const goToNextPage = useCallback(() => {
|
|
34
|
+
if (currentPage >= totalPages)
|
|
35
|
+
return;
|
|
36
|
+
setRequestedPage(currentPage + 1);
|
|
37
|
+
onPageChange?.();
|
|
38
|
+
}, [currentPage, totalPages, onPageChange]);
|
|
39
|
+
return {
|
|
40
|
+
currentPage,
|
|
41
|
+
totalPages,
|
|
42
|
+
pageItems,
|
|
43
|
+
canPreviousPage: currentPage > 1,
|
|
44
|
+
canNextPage: currentPage < totalPages,
|
|
45
|
+
goToPreviousPage,
|
|
46
|
+
goToNextPage,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=use-paged-list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-paged-list.js","sourceRoot":"","sources":["../../../src/react/hooks/use-paged-list.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAmDvD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY,CAC1B,KAAmB,EACnB,QAAgB,EAChB,UAA+B,EAAE;IAEjC,MAAM,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC;IACjC,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAEtD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC;IACnE,6EAA6E;IAC7E,4EAA4E;IAC5E,2DAA2D;IAC3D,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,aAAa,CAAC,EAAE,UAAU,CAAC,CAAC;IAErE,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE;QAC7B,MAAM,UAAU,GAAG,CAAC,WAAW,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC;QAChD,OAAO,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,UAAU,GAAG,QAAQ,CAAC,CAAC;IACxD,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC;IAEnC,MAAM,gBAAgB,GAAG,WAAW,CAAC,GAAG,EAAE;QACxC,IAAI,WAAW,IAAI,CAAC;YAAE,OAAO;QAC7B,gBAAgB,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC;QAClC,YAAY,EAAE,EAAE,CAAC;IACnB,CAAC,EAAE,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC,CAAC;IAEhC,MAAM,YAAY,GAAG,WAAW,CAAC,GAAG,EAAE;QACpC,IAAI,WAAW,IAAI,UAAU;YAAE,OAAO;QACtC,gBAAgB,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC;QAClC,YAAY,EAAE,EAAE,CAAC;IACnB,CAAC,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC,CAAC;IAE5C,OAAO;QACL,WAAW;QACX,UAAU;QACV,SAAS;QACT,eAAe,EAAE,WAAW,GAAG,CAAC;QAChC,WAAW,EAAE,WAAW,GAAG,UAAU;QACrC,gBAAgB;QAChB,YAAY;KACb,CAAC;AACJ,CAAC","sourcesContent":["\"use client\";\n\nimport { useCallback, useMemo, useState } from \"react\";\n\n/**\n * The control state a paginated list exposes to its paginator UI, rendered by\n * the ONE `ListPagination` component. Nothing else builds\n * Previous / counter / Next controls.\n *\n * `totalPages` is the flavor discriminant, and the ONLY difference between the\n * two flavors:\n *\n * - **Known total** (client-side slice pagination — {@link usePagedList}):\n * `totalPages` is a number and the counter reads `\"2 of 5\"`.\n * - **Unknown total** (server/cursor pagination — `useCursorPage().paginationFor(hasMore)`,\n * or a caller's own query hook): `totalPages` is omitted and the counter reads\n * `\"Page 2\"`.\n *\n * Both flavors share one visibility rule, one disabled-edge rule, one live\n * region, and one set of classes because they share one component (ENG-002,\n * FRONTEND-001).\n */\nexport interface ListPaginationState {\n /** 1-based page currently on screen. */\n currentPage: number;\n /**\n * Total page count when it is knowable — at least 1, even for an empty list\n * (an empty list is one empty page). Omitted for cursor/`hasMore` pagination,\n * where the total is unknown until the last page is reached.\n */\n totalPages?: number;\n canPreviousPage: boolean;\n canNextPage: boolean;\n goToPreviousPage: () => void;\n goToNextPage: () => void;\n}\n\nexport interface PagedList<T> extends ListPaginationState {\n /** Always known for a slice-paginated list. */\n totalPages: number;\n /** The slice of `items` belonging to `currentPage`. */\n pageItems: T[];\n}\n\nexport interface UsePagedListOptions {\n /**\n * Called after the page actually changed. Surfaces that keep a per-page\n * selection (an open detail row) clear it here — the old selection is not on\n * the new page.\n */\n onPageChange?: () => void;\n}\n\n/**\n * The single owner of client-side slice pagination: local page state, the page\n * math, and the current slice over an ALREADY-FETCHED list.\n *\n * Page size is the caller's — every surface passes its own page-size constant\n * (ENG-005), never a literal. Ordering and filtering stay with the caller: pass\n * the list already sorted and filtered.\n *\n * Not for server-paged lists — a cursor/`hasMore` feed pages through the\n * caller's data layer, not through a slice; use `useCursorPage` (or the query\n * hook's own page state) with the same `ListPagination` control.\n */\nexport function usePagedList<T>(\n items: readonly T[],\n pageSize: number,\n options: UsePagedListOptions = {},\n): PagedList<T> {\n const { onPageChange } = options;\n const [requestedPage, setRequestedPage] = useState(1);\n\n const totalPages = Math.max(1, Math.ceil(items.length / pageSize));\n // The list can shrink under a page that is already open (an item is deleted,\n // a filter narrows). Clamping keeps the paginator on a real page instead of\n // rendering an empty slice under an out-of-range \"4 of 2\".\n const currentPage = Math.min(Math.max(1, requestedPage), totalPages);\n\n const pageItems = useMemo(() => {\n const startIndex = (currentPage - 1) * pageSize;\n return items.slice(startIndex, startIndex + pageSize);\n }, [items, currentPage, pageSize]);\n\n const goToPreviousPage = useCallback(() => {\n if (currentPage <= 1) return;\n setRequestedPage(currentPage - 1);\n onPageChange?.();\n }, [currentPage, onPageChange]);\n\n const goToNextPage = useCallback(() => {\n if (currentPage >= totalPages) return;\n setRequestedPage(currentPage + 1);\n onPageChange?.();\n }, [currentPage, totalPages, onPageChange]);\n\n return {\n currentPage,\n totalPages,\n pageItems,\n canPreviousPage: currentPage > 1,\n canNextPage: currentPage < totalPages,\n goToPreviousPage,\n goToNextPage,\n };\n}\n"]}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -51,4 +51,10 @@ export { ScrollToTopButton } from "./components/scroll-to-top-button.js";
|
|
|
51
51
|
export type { ScrollToTopButtonProps } from "./components/scroll-to-top-button.js";
|
|
52
52
|
export { ChunkErrorRecovery } from "./components/chunk-error-recovery.js";
|
|
53
53
|
export type { ChunkErrorRecoveryProps } from "./components/chunk-error-recovery.js";
|
|
54
|
+
export { ListPagination } from "./components/list-pagination.js";
|
|
55
|
+
export type { ListPaginationProps } from "./components/list-pagination.js";
|
|
56
|
+
export { usePagedList } from "./hooks/use-paged-list.js";
|
|
57
|
+
export type { ListPaginationState, PagedList, UsePagedListOptions } from "./hooks/use-paged-list.js";
|
|
58
|
+
export { useCursorPage } from "./hooks/use-cursor-page.js";
|
|
59
|
+
export type { CursorPage, CursorPager, UseCursorPageOptions } from "./hooks/use-cursor-page.js";
|
|
54
60
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
|
package/dist/react/index.js
CHANGED
|
@@ -41,4 +41,9 @@ export { RelativeTime } from "./components/relative-time.js";
|
|
|
41
41
|
export { EndOfListIndicator } from "./components/end-of-list-indicator.js";
|
|
42
42
|
export { ScrollToTopButton } from "./components/scroll-to-top-button.js";
|
|
43
43
|
export { ChunkErrorRecovery } from "./components/chunk-error-recovery.js";
|
|
44
|
+
// List pagination — the one Previous / counter / Next control plus the page-state
|
|
45
|
+
// hooks for its two flavors (known total, and cursor/`hasMore`).
|
|
46
|
+
export { ListPagination } from "./components/list-pagination.js";
|
|
47
|
+
export { usePagedList } from "./hooks/use-paged-list.js";
|
|
48
|
+
export { useCursorPage } from "./hooks/use-cursor-page.js";
|
|
44
49
|
//# sourceMappingURL=index.js.map
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nexport type { ButtonProps } from \"./components/button.js\";\nexport { Button, buttonVariants } from \"./components/button.js\";\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from \"./components/card.js\";\nexport { Label } from \"./components/label.js\";\nexport { Textarea } from \"./components/textarea.js\";\nexport { Skeleton } from \"./components/skeleton.js\";\nexport { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription } from \"./components/dialog.js\";\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } from \"./components/popover.js\";\nexport { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuGroup, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup } from \"./components/dropdown-menu.js\";\nexport { Menubar, MenubarMenu, MenubarTrigger, MenubarContent, MenubarItem, MenubarSeparator, MenubarLabel, MenubarCheckboxItem, MenubarRadioGroup, MenubarRadioItem, MenubarPortal, MenubarSubContent, MenubarSubTrigger, MenubarGroup, MenubarSub, MenubarShortcut } from \"./components/menubar.js\";\nexport { AlertDialog, AlertDialogPortal, AlertDialogOverlay, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel } from \"./components/alert-dialog.js\";\nexport { Alert, AlertTitle, AlertDescription } from \"./components/alert.js\";\nexport type { ToastProps, ToastActionElement } from \"./components/toast.js\";\nexport { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction } from \"./components/toast.js\";\nexport { Toaster } from \"./components/toaster.js\";\nexport type { ToastVariant, ToasterToast } from \"./hooks/use-toast.js\";\nexport { toast, useToast } from \"./hooks/use-toast.js\";\nexport { Input } from \"./components/input.js\";\nexport { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton } from \"./components/select.js\";\nexport { Tabs, TabsList, TabsTrigger, TabsContent } from \"./components/tabs.js\";\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from \"./components/tooltip.js\";\nexport { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField } from \"./components/form.js\";\nexport { Checkbox } from \"./components/checkbox.js\";\nexport { Switch } from \"./components/switch.js\";\nexport type { BadgeProps } from \"./components/badge.js\";\nexport { Badge, badgeVariants } from \"./components/badge.js\";\nexport { Avatar, AvatarImage, AvatarFallback } from \"./components/avatar.js\";\nexport { Separator } from \"./components/separator.js\";\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from \"./components/accordion.js\";\nexport { RadioGroup, RadioGroupItem } from \"./components/radio-group.js\";\nexport { Progress } from \"./components/progress.js\";\nexport { Slider } from \"./components/slider.js\";\nexport type { DatePickerProps } from \"./components/date-picker.js\";\nexport { DatePicker } from \"./components/date-picker.js\";\nexport { ScrollArea, ScrollBar } from \"./components/scroll-area.js\";\nexport { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption } from \"./components/table.js\";\nexport { Sheet, SheetPortal, SheetOverlay, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription } from \"./components/sheet.js\";\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent } from \"./components/collapsible.js\";\nexport type { SearchDropdownProps } from \"./components/search-dropdown.js\";\nexport { SearchDropdown } from \"./components/search-dropdown.js\";\n\nexport type { ConsequenceDialogProps, ConsequenceDialogReason, ConsequenceDialogTypedConfirmation } from \"./components/consequence-dialog.js\";\nexport { ConsequenceDialog } from \"./components/consequence-dialog.js\";\n\nexport type { MaxWidthOption, ScreenAdaptiveViewProps } from \"./components/layout/screen-adaptive-view.js\";\nexport { ScreenAdaptiveView } from \"./components/layout/screen-adaptive-view.js\";\n\nexport { useMediaQuery } from \"./hooks/use-media-query.js\";\n\n// Shared building blocks\nexport { RelativeTime } from \"./components/relative-time.js\";\nexport type { RelativeTimeProps } from \"./components/relative-time.js\";\n\nexport { EndOfListIndicator } from \"./components/end-of-list-indicator.js\";\nexport type { EndOfListIndicatorProps } from \"./components/end-of-list-indicator.js\";\n\nexport { ScrollToTopButton } from \"./components/scroll-to-top-button.js\";\nexport type { ScrollToTopButtonProps } from \"./components/scroll-to-top-button.js\";\n\nexport { ChunkErrorRecovery } from \"./components/chunk-error-recovery.js\";\nexport type { ChunkErrorRecoveryProps } from \"./components/chunk-error-recovery.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nexport type { ButtonProps } from \"./components/button.js\";\nexport { Button, buttonVariants } from \"./components/button.js\";\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from \"./components/card.js\";\nexport { Label } from \"./components/label.js\";\nexport { Textarea } from \"./components/textarea.js\";\nexport { Skeleton } from \"./components/skeleton.js\";\nexport { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription } from \"./components/dialog.js\";\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } from \"./components/popover.js\";\nexport { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuGroup, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup } from \"./components/dropdown-menu.js\";\nexport { Menubar, MenubarMenu, MenubarTrigger, MenubarContent, MenubarItem, MenubarSeparator, MenubarLabel, MenubarCheckboxItem, MenubarRadioGroup, MenubarRadioItem, MenubarPortal, MenubarSubContent, MenubarSubTrigger, MenubarGroup, MenubarSub, MenubarShortcut } from \"./components/menubar.js\";\nexport { AlertDialog, AlertDialogPortal, AlertDialogOverlay, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel } from \"./components/alert-dialog.js\";\nexport { Alert, AlertTitle, AlertDescription } from \"./components/alert.js\";\nexport type { ToastProps, ToastActionElement } from \"./components/toast.js\";\nexport { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction } from \"./components/toast.js\";\nexport { Toaster } from \"./components/toaster.js\";\nexport type { ToastVariant, ToasterToast } from \"./hooks/use-toast.js\";\nexport { toast, useToast } from \"./hooks/use-toast.js\";\nexport { Input } from \"./components/input.js\";\nexport { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton } from \"./components/select.js\";\nexport { Tabs, TabsList, TabsTrigger, TabsContent } from \"./components/tabs.js\";\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from \"./components/tooltip.js\";\nexport { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField } from \"./components/form.js\";\nexport { Checkbox } from \"./components/checkbox.js\";\nexport { Switch } from \"./components/switch.js\";\nexport type { BadgeProps } from \"./components/badge.js\";\nexport { Badge, badgeVariants } from \"./components/badge.js\";\nexport { Avatar, AvatarImage, AvatarFallback } from \"./components/avatar.js\";\nexport { Separator } from \"./components/separator.js\";\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from \"./components/accordion.js\";\nexport { RadioGroup, RadioGroupItem } from \"./components/radio-group.js\";\nexport { Progress } from \"./components/progress.js\";\nexport { Slider } from \"./components/slider.js\";\nexport type { DatePickerProps } from \"./components/date-picker.js\";\nexport { DatePicker } from \"./components/date-picker.js\";\nexport { ScrollArea, ScrollBar } from \"./components/scroll-area.js\";\nexport { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption } from \"./components/table.js\";\nexport { Sheet, SheetPortal, SheetOverlay, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription } from \"./components/sheet.js\";\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent } from \"./components/collapsible.js\";\nexport type { SearchDropdownProps } from \"./components/search-dropdown.js\";\nexport { SearchDropdown } from \"./components/search-dropdown.js\";\n\nexport type { ConsequenceDialogProps, ConsequenceDialogReason, ConsequenceDialogTypedConfirmation } from \"./components/consequence-dialog.js\";\nexport { ConsequenceDialog } from \"./components/consequence-dialog.js\";\n\nexport type { MaxWidthOption, ScreenAdaptiveViewProps } from \"./components/layout/screen-adaptive-view.js\";\nexport { ScreenAdaptiveView } from \"./components/layout/screen-adaptive-view.js\";\n\nexport { useMediaQuery } from \"./hooks/use-media-query.js\";\n\n// Shared building blocks\nexport { RelativeTime } from \"./components/relative-time.js\";\nexport type { RelativeTimeProps } from \"./components/relative-time.js\";\n\nexport { EndOfListIndicator } from \"./components/end-of-list-indicator.js\";\nexport type { EndOfListIndicatorProps } from \"./components/end-of-list-indicator.js\";\n\nexport { ScrollToTopButton } from \"./components/scroll-to-top-button.js\";\nexport type { ScrollToTopButtonProps } from \"./components/scroll-to-top-button.js\";\n\nexport { ChunkErrorRecovery } from \"./components/chunk-error-recovery.js\";\nexport type { ChunkErrorRecoveryProps } from \"./components/chunk-error-recovery.js\";\n\n// List pagination — the one Previous / counter / Next control plus the page-state\n// hooks for its two flavors (known total, and cursor/`hasMore`).\nexport { ListPagination } from \"./components/list-pagination.js\";\nexport type { ListPaginationProps } from \"./components/list-pagination.js\";\n\nexport { usePagedList } from \"./hooks/use-paged-list.js\";\nexport type { ListPaginationState, PagedList, UsePagedListOptions } from \"./hooks/use-paged-list.js\";\n\nexport { useCursorPage } from \"./hooks/use-cursor-page.js\";\nexport type { CursorPage, CursorPager, UseCursorPageOptions } from \"./hooks/use-cursor-page.js\";\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttt-productions/ui-core",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.12",
|
|
4
4
|
"description": "Shared UI components for TTT Productions projects",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"react-dom": ">=19.0.0",
|
|
50
50
|
"react-hook-form": ">=7.0.0",
|
|
51
51
|
"@hookform/resolvers": ">=3.0.0",
|
|
52
|
-
"@ttt-productions/firebase-helpers": "^0.12.
|
|
52
|
+
"@ttt-productions/firebase-helpers": "^0.12.4"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"react": {
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"typescript": "^5.8.3",
|
|
100
100
|
"react-hook-form": "^7.69.0",
|
|
101
101
|
"@hookform/resolvers": "^5.2.2",
|
|
102
|
-
"@ttt-productions/firebase-helpers": "^0.12.
|
|
102
|
+
"@ttt-productions/firebase-helpers": "^0.12.4"
|
|
103
103
|
},
|
|
104
104
|
"author": "DJ (TTT Productions)",
|
|
105
105
|
"license": "MIT"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
|
+
import { Button } from "./button.js";
|
|
5
|
+
import { cn } from "../../lib/utils.js";
|
|
6
|
+
import type { ListPaginationState } from "../hooks/use-paged-list.js";
|
|
7
|
+
|
|
8
|
+
export interface ListPaginationProps {
|
|
9
|
+
/**
|
|
10
|
+
* Page state from `usePagedList` (known total),
|
|
11
|
+
* `useCursorPage().paginationFor(hasMore)` (unknown total), or any
|
|
12
|
+
* caller-owned state of the same shape.
|
|
13
|
+
*/
|
|
14
|
+
pagination: ListPaginationState;
|
|
15
|
+
/**
|
|
16
|
+
* Disables BOTH controls while the next page is in flight, without hiding the
|
|
17
|
+
* row. Server-paged surfaces pass their query's `isFetching`; a slice-paged
|
|
18
|
+
* list has nothing to wait for and omits it.
|
|
19
|
+
*/
|
|
20
|
+
busy?: boolean;
|
|
21
|
+
/** Optional extra classes. Merged with the component's own classes. */
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The ONE Previous / counter / Next control for paginated lists, in both
|
|
27
|
+
* flavors. Pair it with the hook that owns the page state (`usePagedList` for a
|
|
28
|
+
* client-side slice, `useCursorPage` — or the caller's own query state — for a
|
|
29
|
+
* cursor feed; a cursor feed passes `pager.paginationFor(hasMore)`, which binds
|
|
30
|
+
* the query's answer to these controls); this component owns only the controls,
|
|
31
|
+
* so every paginated list reads and behaves the same (ENG-002, FRONTEND-001).
|
|
32
|
+
*
|
|
33
|
+
* The counter follows the flavor: a known `totalPages` reads "2 of 5", an
|
|
34
|
+
* omitted one reads "Page 2". Everything else is identical in both flavors —
|
|
35
|
+
* which is exactly why there is one component and not two.
|
|
36
|
+
*
|
|
37
|
+
* It renders NOTHING when neither direction is available, so no call site
|
|
38
|
+
* repeats a `totalPages > 1` / `page > 1 || hasMore` guard. Edge state is real
|
|
39
|
+
* `disabled` on the buttons (announced, and unclickable), and the counter is a
|
|
40
|
+
* `status` live region so a page change is announced to screen readers.
|
|
41
|
+
*/
|
|
42
|
+
export function ListPagination({ pagination, busy = false, className }: ListPaginationProps) {
|
|
43
|
+
const { currentPage, totalPages, canPreviousPage, canNextPage, goToPreviousPage, goToNextPage } =
|
|
44
|
+
pagination;
|
|
45
|
+
|
|
46
|
+
// One visibility rule for both flavors. For a known total this is exactly
|
|
47
|
+
// `totalPages > 1`: page state is clamped into `1..totalPages`, so a
|
|
48
|
+
// single-page list has no reachable neighbour in either direction, and a
|
|
49
|
+
// multi-page list always has one. For a cursor feed it is the hand-rolled
|
|
50
|
+
// `page > 1 || hasMore`. `busy` deliberately does not hide the row — the
|
|
51
|
+
// controls grey out in place instead of vanishing mid-fetch.
|
|
52
|
+
if (!canPreviousPage && !canNextPage) return null;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className={cn("flex w-full items-center justify-between gap-4 mt-4", className)}>
|
|
56
|
+
<Button
|
|
57
|
+
variant="outline"
|
|
58
|
+
size="sm"
|
|
59
|
+
onClick={goToPreviousPage}
|
|
60
|
+
disabled={busy || !canPreviousPage}
|
|
61
|
+
className="h-11 min-w-11"
|
|
62
|
+
>
|
|
63
|
+
<ChevronLeft className="icon-xs mr-1" />
|
|
64
|
+
Previous
|
|
65
|
+
</Button>
|
|
66
|
+
<span className="text-counter" role="status">
|
|
67
|
+
{totalPages === undefined ? `Page ${currentPage}` : `${currentPage} of ${totalPages}`}
|
|
68
|
+
</span>
|
|
69
|
+
<Button
|
|
70
|
+
variant="outline"
|
|
71
|
+
size="sm"
|
|
72
|
+
onClick={goToNextPage}
|
|
73
|
+
disabled={busy || !canNextPage}
|
|
74
|
+
className="h-11 min-w-11"
|
|
75
|
+
>
|
|
76
|
+
Next
|
|
77
|
+
<ChevronRight className="icon-xs ml-1" />
|
|
78
|
+
</Button>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import type { ListPaginationState } from "./use-paged-list.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Render-time page state for a cursor feed, shaped as {@link ListPaginationState}
|
|
8
|
+
* so it drops straight into the ONE `ListPagination` control. Produced by
|
|
9
|
+
* {@link CursorPager.paginationFor} once the query for `currentPage` has
|
|
10
|
+
* answered with its `hasMore`.
|
|
11
|
+
*/
|
|
12
|
+
export interface CursorPage extends ListPaginationState {
|
|
13
|
+
/** Never known for cursor pagination — the counter reads "Page N". */
|
|
14
|
+
totalPages?: undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UseCursorPageOptions {
|
|
18
|
+
/** Called after the page actually changed. Same contract as `usePagedList`. */
|
|
19
|
+
onPageChange?: () => void;
|
|
20
|
+
/**
|
|
21
|
+
* A snapshot of the query inputs the page number belongs to — a tag, a filter,
|
|
22
|
+
* a sort. When it changes, the page is 1 again **in the same render**, so the
|
|
23
|
+
* query never fires for "page 4" of inputs whose page 4 may not exist.
|
|
24
|
+
*
|
|
25
|
+
* Use this when the input change arrives as a prop (there is no event handler
|
|
26
|
+
* of yours to hang the reset on); use {@link CursorPager.reset} when your own
|
|
27
|
+
* handler already owns the change. Compose several inputs into one string
|
|
28
|
+
* (`` `${type}|${projectId ?? ""}` ``) — the type is deliberately narrowed to
|
|
29
|
+
* primitives so a fresh object literal cannot pin the list to page 1 forever.
|
|
30
|
+
*/
|
|
31
|
+
resetKey?: string | number | boolean | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What {@link useCursorPage} owns: the page number, the guarded step controls,
|
|
36
|
+
* and the return-to-page-1 control.
|
|
37
|
+
*/
|
|
38
|
+
export interface CursorPager {
|
|
39
|
+
/** 1-based page to feed into the paged query. */
|
|
40
|
+
currentPage: number;
|
|
41
|
+
/**
|
|
42
|
+
* Return to page 1 without walking back. Call it from the handler that changes
|
|
43
|
+
* the query's inputs (a new filter, a new tag, a new sort) — page 4 of the old
|
|
44
|
+
* result set is not page 4 of the new one. A no-op on page 1, so it never
|
|
45
|
+
* fires `onPageChange` for nothing.
|
|
46
|
+
*/
|
|
47
|
+
reset: () => void;
|
|
48
|
+
/**
|
|
49
|
+
* Bind the query's answer to the controls: pass the `hasMore` that came back
|
|
50
|
+
* for {@link currentPage} and render the result as
|
|
51
|
+
* `<ListPagination pagination={…} />`. `hasMore` binds HERE, after the query,
|
|
52
|
+
* because that is the only moment it exists — a cursor query needs
|
|
53
|
+
* `currentPage` as its input, so `hasMore` cannot be an argument to the hook
|
|
54
|
+
* that produces `currentPage`.
|
|
55
|
+
*/
|
|
56
|
+
paginationFor: (hasMore: boolean) => CursorPage;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Page state for a server-paged (cursor / `hasMore`) list: the 1-based page
|
|
61
|
+
* number, the guarded step controls, and the reset — the pieces each
|
|
62
|
+
* server-paged surface was otherwise re-declaring as a local `useState(1)` plus
|
|
63
|
+
* its own clamped increment/decrement and its own "reset when the filter
|
|
64
|
+
* changed" adjustment (ENG-002, ENG-003).
|
|
65
|
+
*
|
|
66
|
+
* The data stays the caller's, and the two halves bind at different moments:
|
|
67
|
+
*
|
|
68
|
+
* ```tsx
|
|
69
|
+
* const pager = useCursorPage({ resetKey: tag });
|
|
70
|
+
* const { data, isFetching } = useThingsByTag(tag, pager.currentPage);
|
|
71
|
+
* …
|
|
72
|
+
* <ListPagination pagination={pager.paginationFor(data?.hasMore ?? false)} busy={isFetching} />
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* The guards live in here, not at the call site: a step that cannot move the
|
|
76
|
+
* page changes nothing and does not fire `onPageChange`.
|
|
77
|
+
*
|
|
78
|
+
* A surface whose data hook ALREADY owns its page state (it returns
|
|
79
|
+
* `page`/`hasNextPage`/`nextPage`/`prevPage` itself) does not need this hook —
|
|
80
|
+
* it maps those fields onto {@link ListPaginationState} directly. The hook
|
|
81
|
+
* exists for the surfaces that would otherwise hand-roll the state.
|
|
82
|
+
*/
|
|
83
|
+
export function useCursorPage(options: UseCursorPageOptions = {}): CursorPager {
|
|
84
|
+
const { onPageChange, resetKey } = options;
|
|
85
|
+
|
|
86
|
+
// The stored page is only meaningful for the inputs it was reached under, so
|
|
87
|
+
// the key it belongs to is stored WITH it.
|
|
88
|
+
const [pageState, setPageState] = useState<{
|
|
89
|
+
page: number;
|
|
90
|
+
key: UseCursorPageOptions["resetKey"];
|
|
91
|
+
}>({ page: 1, key: resetKey });
|
|
92
|
+
|
|
93
|
+
// `Object.is`, the same comparison React uses for state and deps: a `NaN` key
|
|
94
|
+
// must compare equal to itself, or the adjustment below would re-fire forever.
|
|
95
|
+
const keyMatches = Object.is(pageState.key, resetKey);
|
|
96
|
+
|
|
97
|
+
// React's "adjusting state when a prop changes": a new key retires the stored
|
|
98
|
+
// page, because page 4 of the old result set is not page 4 of the new one.
|
|
99
|
+
// Retiring it (rather than only ignoring it) is what makes going BACK to
|
|
100
|
+
// earlier inputs start at page 1 too, instead of reviving a dead page.
|
|
101
|
+
if (!keyMatches) {
|
|
102
|
+
setPageState({ page: 1, key: resetKey });
|
|
103
|
+
}
|
|
104
|
+
// Derived as well as adjusted, so even the render that triggers the adjustment
|
|
105
|
+
// already reports page 1 — the caller's query is never asked for a page of the
|
|
106
|
+
// new inputs that the user never navigated to.
|
|
107
|
+
const currentPage = keyMatches ? pageState.page : 1;
|
|
108
|
+
|
|
109
|
+
const goToPreviousPage = useCallback(() => {
|
|
110
|
+
if (currentPage <= 1) return;
|
|
111
|
+
setPageState({ page: currentPage - 1, key: resetKey });
|
|
112
|
+
onPageChange?.();
|
|
113
|
+
}, [currentPage, resetKey, onPageChange]);
|
|
114
|
+
|
|
115
|
+
const reset = useCallback(() => {
|
|
116
|
+
if (currentPage === 1) return;
|
|
117
|
+
setPageState({ page: 1, key: resetKey });
|
|
118
|
+
onPageChange?.();
|
|
119
|
+
}, [currentPage, resetKey, onPageChange]);
|
|
120
|
+
|
|
121
|
+
// Memoized on the page state itself, so a consumer may hold it in a dep array
|
|
122
|
+
// and it changes only when the page (or the callback/inputs) actually change.
|
|
123
|
+
const paginationFor = useCallback(
|
|
124
|
+
(hasMore: boolean): CursorPage => ({
|
|
125
|
+
currentPage,
|
|
126
|
+
canPreviousPage: currentPage > 1,
|
|
127
|
+
canNextPage: hasMore,
|
|
128
|
+
goToPreviousPage,
|
|
129
|
+
goToNextPage: () => {
|
|
130
|
+
if (!hasMore) return;
|
|
131
|
+
setPageState({ page: currentPage + 1, key: resetKey });
|
|
132
|
+
onPageChange?.();
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
[currentPage, resetKey, onPageChange, goToPreviousPage],
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return { currentPage, reset, paginationFor };
|
|
139
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useState } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The control state a paginated list exposes to its paginator UI, rendered by
|
|
7
|
+
* the ONE `ListPagination` component. Nothing else builds
|
|
8
|
+
* Previous / counter / Next controls.
|
|
9
|
+
*
|
|
10
|
+
* `totalPages` is the flavor discriminant, and the ONLY difference between the
|
|
11
|
+
* two flavors:
|
|
12
|
+
*
|
|
13
|
+
* - **Known total** (client-side slice pagination — {@link usePagedList}):
|
|
14
|
+
* `totalPages` is a number and the counter reads `"2 of 5"`.
|
|
15
|
+
* - **Unknown total** (server/cursor pagination — `useCursorPage().paginationFor(hasMore)`,
|
|
16
|
+
* or a caller's own query hook): `totalPages` is omitted and the counter reads
|
|
17
|
+
* `"Page 2"`.
|
|
18
|
+
*
|
|
19
|
+
* Both flavors share one visibility rule, one disabled-edge rule, one live
|
|
20
|
+
* region, and one set of classes because they share one component (ENG-002,
|
|
21
|
+
* FRONTEND-001).
|
|
22
|
+
*/
|
|
23
|
+
export interface ListPaginationState {
|
|
24
|
+
/** 1-based page currently on screen. */
|
|
25
|
+
currentPage: number;
|
|
26
|
+
/**
|
|
27
|
+
* Total page count when it is knowable — at least 1, even for an empty list
|
|
28
|
+
* (an empty list is one empty page). Omitted for cursor/`hasMore` pagination,
|
|
29
|
+
* where the total is unknown until the last page is reached.
|
|
30
|
+
*/
|
|
31
|
+
totalPages?: number;
|
|
32
|
+
canPreviousPage: boolean;
|
|
33
|
+
canNextPage: boolean;
|
|
34
|
+
goToPreviousPage: () => void;
|
|
35
|
+
goToNextPage: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PagedList<T> extends ListPaginationState {
|
|
39
|
+
/** Always known for a slice-paginated list. */
|
|
40
|
+
totalPages: number;
|
|
41
|
+
/** The slice of `items` belonging to `currentPage`. */
|
|
42
|
+
pageItems: T[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface UsePagedListOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Called after the page actually changed. Surfaces that keep a per-page
|
|
48
|
+
* selection (an open detail row) clear it here — the old selection is not on
|
|
49
|
+
* the new page.
|
|
50
|
+
*/
|
|
51
|
+
onPageChange?: () => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The single owner of client-side slice pagination: local page state, the page
|
|
56
|
+
* math, and the current slice over an ALREADY-FETCHED list.
|
|
57
|
+
*
|
|
58
|
+
* Page size is the caller's — every surface passes its own page-size constant
|
|
59
|
+
* (ENG-005), never a literal. Ordering and filtering stay with the caller: pass
|
|
60
|
+
* the list already sorted and filtered.
|
|
61
|
+
*
|
|
62
|
+
* Not for server-paged lists — a cursor/`hasMore` feed pages through the
|
|
63
|
+
* caller's data layer, not through a slice; use `useCursorPage` (or the query
|
|
64
|
+
* hook's own page state) with the same `ListPagination` control.
|
|
65
|
+
*/
|
|
66
|
+
export function usePagedList<T>(
|
|
67
|
+
items: readonly T[],
|
|
68
|
+
pageSize: number,
|
|
69
|
+
options: UsePagedListOptions = {},
|
|
70
|
+
): PagedList<T> {
|
|
71
|
+
const { onPageChange } = options;
|
|
72
|
+
const [requestedPage, setRequestedPage] = useState(1);
|
|
73
|
+
|
|
74
|
+
const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
|
|
75
|
+
// The list can shrink under a page that is already open (an item is deleted,
|
|
76
|
+
// a filter narrows). Clamping keeps the paginator on a real page instead of
|
|
77
|
+
// rendering an empty slice under an out-of-range "4 of 2".
|
|
78
|
+
const currentPage = Math.min(Math.max(1, requestedPage), totalPages);
|
|
79
|
+
|
|
80
|
+
const pageItems = useMemo(() => {
|
|
81
|
+
const startIndex = (currentPage - 1) * pageSize;
|
|
82
|
+
return items.slice(startIndex, startIndex + pageSize);
|
|
83
|
+
}, [items, currentPage, pageSize]);
|
|
84
|
+
|
|
85
|
+
const goToPreviousPage = useCallback(() => {
|
|
86
|
+
if (currentPage <= 1) return;
|
|
87
|
+
setRequestedPage(currentPage - 1);
|
|
88
|
+
onPageChange?.();
|
|
89
|
+
}, [currentPage, onPageChange]);
|
|
90
|
+
|
|
91
|
+
const goToNextPage = useCallback(() => {
|
|
92
|
+
if (currentPage >= totalPages) return;
|
|
93
|
+
setRequestedPage(currentPage + 1);
|
|
94
|
+
onPageChange?.();
|
|
95
|
+
}, [currentPage, totalPages, onPageChange]);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
currentPage,
|
|
99
|
+
totalPages,
|
|
100
|
+
pageItems,
|
|
101
|
+
canPreviousPage: currentPage > 1,
|
|
102
|
+
canNextPage: currentPage < totalPages,
|
|
103
|
+
goToPreviousPage,
|
|
104
|
+
goToNextPage,
|
|
105
|
+
};
|
|
106
|
+
}
|
package/src/react/index.ts
CHANGED
|
@@ -61,3 +61,14 @@ export type { ScrollToTopButtonProps } from "./components/scroll-to-top-button.j
|
|
|
61
61
|
|
|
62
62
|
export { ChunkErrorRecovery } from "./components/chunk-error-recovery.js";
|
|
63
63
|
export type { ChunkErrorRecoveryProps } from "./components/chunk-error-recovery.js";
|
|
64
|
+
|
|
65
|
+
// List pagination — the one Previous / counter / Next control plus the page-state
|
|
66
|
+
// hooks for its two flavors (known total, and cursor/`hasMore`).
|
|
67
|
+
export { ListPagination } from "./components/list-pagination.js";
|
|
68
|
+
export type { ListPaginationProps } from "./components/list-pagination.js";
|
|
69
|
+
|
|
70
|
+
export { usePagedList } from "./hooks/use-paged-list.js";
|
|
71
|
+
export type { ListPaginationState, PagedList, UsePagedListOptions } from "./hooks/use-paged-list.js";
|
|
72
|
+
|
|
73
|
+
export { useCursorPage } from "./hooks/use-cursor-page.js";
|
|
74
|
+
export type { CursorPage, CursorPager, UseCursorPageOptions } from "./hooks/use-cursor-page.js";
|