@ttt-productions/ui-core 0.11.10 → 0.11.11

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.
@@ -0,0 +1,34 @@
1
+ import type { ListPaginationState } from "../hooks/use-paged-list.js";
2
+ export interface ListPaginationProps {
3
+ /**
4
+ * Page state from `usePagedList` (known total), `useCursorPage` (unknown
5
+ * total), or any caller-owned state of the same shape.
6
+ */
7
+ pagination: ListPaginationState;
8
+ /**
9
+ * Disables BOTH controls while the next page is in flight, without hiding the
10
+ * row. Server-paged surfaces pass their query's `isFetching`; a slice-paged
11
+ * list has nothing to wait for and omits it.
12
+ */
13
+ busy?: boolean;
14
+ /** Optional extra classes. Merged with the component's own classes. */
15
+ className?: string;
16
+ }
17
+ /**
18
+ * The ONE Previous / counter / Next control for paginated lists, in both
19
+ * flavors. Pair it with the hook that owns the page state (`usePagedList` for a
20
+ * client-side slice, `useCursorPage` — or the caller's own query state — for a
21
+ * cursor feed); this component owns only the controls, so every paginated list
22
+ * reads and behaves the same (ENG-002, FRONTEND-001).
23
+ *
24
+ * The counter follows the flavor: a known `totalPages` reads "2 of 5", an
25
+ * omitted one reads "Page 2". Everything else is identical in both flavors —
26
+ * which is exactly why there is one component and not two.
27
+ *
28
+ * It renders NOTHING when neither direction is available, so no call site
29
+ * repeats a `totalPages > 1` / `page > 1 || hasMore` guard. Edge state is real
30
+ * `disabled` on the buttons (announced, and unclickable), and the counter is a
31
+ * `status` live region so a page change is announced to screen readers.
32
+ */
33
+ export declare function ListPagination({ pagination, busy, className }: ListPaginationProps): import("react").JSX.Element | null;
34
+ //# 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;;;OAGG;IACH,UAAU,EAAE,mBAAmB,CAAC;IAChC;;;;OAIG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,EAAE,UAAU,EAAE,IAAY,EAAE,SAAS,EAAE,EAAE,mBAAmB,sCAuC1F"}
@@ -0,0 +1,34 @@
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); this component owns only the controls, so every paginated list
11
+ * reads and behaves the same (ENG-002, FRONTEND-001).
12
+ *
13
+ * The counter follows the flavor: a known `totalPages` reads "2 of 5", an
14
+ * omitted one reads "Page 2". Everything else is identical in both flavors —
15
+ * which is exactly why there is one component and not two.
16
+ *
17
+ * It renders NOTHING when neither direction is available, so no call site
18
+ * repeats a `totalPages > 1` / `page > 1 || hasMore` guard. Edge state is real
19
+ * `disabled` on the buttons (announced, and unclickable), and the counter is a
20
+ * `status` live region so a page change is announced to screen readers.
21
+ */
22
+ export function ListPagination({ pagination, busy = false, className }) {
23
+ const { currentPage, totalPages, canPreviousPage, canNextPage, goToPreviousPage, goToNextPage } = pagination;
24
+ // One visibility rule for both flavors. For a known total this is exactly
25
+ // `totalPages > 1`: page state is clamped into `1..totalPages`, so a
26
+ // single-page list has no reachable neighbour in either direction, and a
27
+ // multi-page list always has one. For a cursor feed it is the hand-rolled
28
+ // `page > 1 || hasMore`. `busy` deliberately does not hide the row — the
29
+ // controls grey out in place instead of vanishing mid-fetch.
30
+ if (!canPreviousPage && !canNextPage)
31
+ return null;
32
+ 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" })] })] }));
33
+ }
34
+ //# 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;AAmBxC;;;;;;;;;;;;;;;GAeG;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), `useCursorPage` (unknown\n * total), or any 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); this component owns only the controls, so every paginated list\n * 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,32 @@
1
+ import type { ListPaginationState } from "./use-paged-list.js";
2
+ export interface CursorPage extends ListPaginationState {
3
+ /** Never known for cursor pagination — the counter reads "Page N". */
4
+ totalPages?: undefined;
5
+ /**
6
+ * Return to page 1 without walking back. Call it whenever the underlying
7
+ * query's inputs change (a new filter, a new tag, a new sort) — page 4 of the
8
+ * old result set is not page 4 of the new one.
9
+ */
10
+ reset: () => void;
11
+ }
12
+ export interface UseCursorPageOptions {
13
+ /** Called after the page actually changed. Same contract as `usePagedList`. */
14
+ onPageChange?: () => void;
15
+ }
16
+ /**
17
+ * Page state for a server-paged (cursor / `hasMore`) list: the 1-based page
18
+ * number plus guarded step controls, shaped as {@link ListPaginationState} so it
19
+ * drops straight into the ONE `ListPagination` control.
20
+ *
21
+ * The caller owns the data: it feeds `hasMore` from its query and passes
22
+ * `currentPage` back into that query. This hook owns only the page number — the
23
+ * piece each server-paged surface was otherwise re-declaring as a local
24
+ * `useState(1)` plus its own clamped increment/decrement (ENG-002, ENG-003).
25
+ *
26
+ * A surface whose data hook ALREADY owns its page state (it returns
27
+ * `page`/`hasNextPage`/`nextPage`/`prevPage` itself) does not need this hook —
28
+ * it maps those fields onto `ListPaginationState` directly. The hook exists for
29
+ * the surfaces that would otherwise hand-roll the state.
30
+ */
31
+ export declare function useCursorPage(hasMore: boolean, options?: UseCursorPageOptions): CursorPage;
32
+ //# 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,MAAM,WAAW,UAAW,SAAQ,mBAAmB;IACrD,sEAAsE;IACtE,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;;;OAIG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,oBAAyB,GACjC,UAAU,CAgCZ"}
@@ -0,0 +1,50 @@
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 plus guarded step controls, shaped as {@link ListPaginationState} so it
6
+ * drops straight into the ONE `ListPagination` control.
7
+ *
8
+ * The caller owns the data: it feeds `hasMore` from its query and passes
9
+ * `currentPage` back into that query. This hook owns only the page number — the
10
+ * piece each server-paged surface was otherwise re-declaring as a local
11
+ * `useState(1)` plus its own clamped increment/decrement (ENG-002, ENG-003).
12
+ *
13
+ * A surface whose data hook ALREADY owns its page state (it returns
14
+ * `page`/`hasNextPage`/`nextPage`/`prevPage` itself) does not need this hook —
15
+ * it maps those fields onto `ListPaginationState` directly. The hook exists for
16
+ * the surfaces that would otherwise hand-roll the state.
17
+ */
18
+ export function useCursorPage(hasMore, options = {}) {
19
+ const { onPageChange } = options;
20
+ const [currentPage, setCurrentPage] = useState(1);
21
+ // Guards live here, not at the call site: a click that cannot move the page
22
+ // must not fire `onPageChange` and must not push the page past the data.
23
+ const goToPreviousPage = useCallback(() => {
24
+ if (currentPage <= 1)
25
+ return;
26
+ setCurrentPage(currentPage - 1);
27
+ onPageChange?.();
28
+ }, [currentPage, onPageChange]);
29
+ const goToNextPage = useCallback(() => {
30
+ if (!hasMore)
31
+ return;
32
+ setCurrentPage(currentPage + 1);
33
+ onPageChange?.();
34
+ }, [currentPage, hasMore, onPageChange]);
35
+ const reset = useCallback(() => {
36
+ if (currentPage === 1)
37
+ return;
38
+ setCurrentPage(1);
39
+ onPageChange?.();
40
+ }, [currentPage, onPageChange]);
41
+ return {
42
+ currentPage,
43
+ canPreviousPage: currentPage > 1,
44
+ canNextPage: hasMore,
45
+ goToPreviousPage,
46
+ goToNextPage,
47
+ reset,
48
+ };
49
+ }
50
+ //# 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;AAmB9C;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAgB,EAChB,UAAgC,EAAE;IAElC,MAAM,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC;IACjC,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAElD,4EAA4E;IAC5E,yEAAyE;IACzE,MAAM,gBAAgB,GAAG,WAAW,CAAC,GAAG,EAAE;QACxC,IAAI,WAAW,IAAI,CAAC;YAAE,OAAO;QAC7B,cAAc,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC;QAChC,YAAY,EAAE,EAAE,CAAC;IACnB,CAAC,EAAE,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC,CAAC;IAEhC,MAAM,YAAY,GAAG,WAAW,CAAC,GAAG,EAAE;QACpC,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,cAAc,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC;QAChC,YAAY,EAAE,EAAE,CAAC;IACnB,CAAC,EAAE,CAAC,WAAW,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;IAEzC,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,IAAI,WAAW,KAAK,CAAC;YAAE,OAAO;QAC9B,cAAc,CAAC,CAAC,CAAC,CAAC;QAClB,YAAY,EAAE,EAAE,CAAC;IACnB,CAAC,EAAE,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC,CAAC;IAEhC,OAAO;QACL,WAAW;QACX,eAAe,EAAE,WAAW,GAAG,CAAC;QAChC,WAAW,EAAE,OAAO;QACpB,gBAAgB;QAChB,YAAY;QACZ,KAAK;KACN,CAAC;AACJ,CAAC","sourcesContent":["\"use client\";\n\nimport { useCallback, useState } from \"react\";\nimport type { ListPaginationState } from \"./use-paged-list.js\";\n\nexport interface CursorPage extends ListPaginationState {\n /** Never known for cursor pagination — the counter reads \"Page N\". */\n totalPages?: undefined;\n /**\n * Return to page 1 without walking back. Call it whenever the underlying\n * query's inputs change (a new filter, a new tag, a new sort) — page 4 of the\n * old result set is not page 4 of the new one.\n */\n reset: () => void;\n}\n\nexport interface UseCursorPageOptions {\n /** Called after the page actually changed. Same contract as `usePagedList`. */\n onPageChange?: () => void;\n}\n\n/**\n * Page state for a server-paged (cursor / `hasMore`) list: the 1-based page\n * number plus guarded step controls, shaped as {@link ListPaginationState} so it\n * drops straight into the ONE `ListPagination` control.\n *\n * The caller owns the data: it feeds `hasMore` from its query and passes\n * `currentPage` back into that query. This hook owns only the page number — the\n * piece each server-paged surface was otherwise re-declaring as a local\n * `useState(1)` plus its own clamped increment/decrement (ENG-002, ENG-003).\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 `ListPaginationState` directly. The hook exists for\n * the surfaces that would otherwise hand-roll the state.\n */\nexport function useCursorPage(\n hasMore: boolean,\n options: UseCursorPageOptions = {},\n): CursorPage {\n const { onPageChange } = options;\n const [currentPage, setCurrentPage] = useState(1);\n\n // Guards live here, not at the call site: a click that cannot move the page\n // must not fire `onPageChange` and must not push the page past the data.\n const goToPreviousPage = useCallback(() => {\n if (currentPage <= 1) return;\n setCurrentPage(currentPage - 1);\n onPageChange?.();\n }, [currentPage, onPageChange]);\n\n const goToNextPage = useCallback(() => {\n if (!hasMore) return;\n setCurrentPage(currentPage + 1);\n onPageChange?.();\n }, [currentPage, hasMore, onPageChange]);\n\n const reset = useCallback(() => {\n if (currentPage === 1) return;\n setCurrentPage(1);\n onPageChange?.();\n }, [currentPage, onPageChange]);\n\n return {\n currentPage,\n canPreviousPage: currentPage > 1,\n canNextPage: hasMore,\n goToPreviousPage,\n goToNextPage,\n reset,\n };\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`, or a
12
+ * 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`, or a\n * 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"]}
@@ -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, 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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"}
@@ -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
@@ -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, 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.10",
3
+ "version": "0.11.11",
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.2"
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.2"
102
+ "@ttt-productions/firebase-helpers": "^0.12.4"
103
103
  },
104
104
  "author": "DJ (TTT Productions)",
105
105
  "license": "MIT"
@@ -0,0 +1,79 @@
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), `useCursorPage` (unknown
11
+ * total), or any caller-owned state of the same shape.
12
+ */
13
+ pagination: ListPaginationState;
14
+ /**
15
+ * Disables BOTH controls while the next page is in flight, without hiding the
16
+ * row. Server-paged surfaces pass their query's `isFetching`; a slice-paged
17
+ * list has nothing to wait for and omits it.
18
+ */
19
+ busy?: boolean;
20
+ /** Optional extra classes. Merged with the component's own classes. */
21
+ className?: string;
22
+ }
23
+
24
+ /**
25
+ * The ONE Previous / counter / Next control for paginated lists, in both
26
+ * flavors. Pair it with the hook that owns the page state (`usePagedList` for a
27
+ * client-side slice, `useCursorPage` — or the caller's own query state — for a
28
+ * cursor feed); this component owns only the controls, so every paginated list
29
+ * reads and behaves the same (ENG-002, FRONTEND-001).
30
+ *
31
+ * The counter follows the flavor: a known `totalPages` reads "2 of 5", an
32
+ * omitted one reads "Page 2". Everything else is identical in both flavors —
33
+ * which is exactly why there is one component and not two.
34
+ *
35
+ * It renders NOTHING when neither direction is available, so no call site
36
+ * repeats a `totalPages > 1` / `page > 1 || hasMore` guard. Edge state is real
37
+ * `disabled` on the buttons (announced, and unclickable), and the counter is a
38
+ * `status` live region so a page change is announced to screen readers.
39
+ */
40
+ export function ListPagination({ pagination, busy = false, className }: ListPaginationProps) {
41
+ const { currentPage, totalPages, canPreviousPage, canNextPage, goToPreviousPage, goToNextPage } =
42
+ pagination;
43
+
44
+ // One visibility rule for both flavors. For a known total this is exactly
45
+ // `totalPages > 1`: page state is clamped into `1..totalPages`, so a
46
+ // single-page list has no reachable neighbour in either direction, and a
47
+ // multi-page list always has one. For a cursor feed it is the hand-rolled
48
+ // `page > 1 || hasMore`. `busy` deliberately does not hide the row — the
49
+ // controls grey out in place instead of vanishing mid-fetch.
50
+ if (!canPreviousPage && !canNextPage) return null;
51
+
52
+ return (
53
+ <div className={cn("flex w-full items-center justify-between gap-4 mt-4", className)}>
54
+ <Button
55
+ variant="outline"
56
+ size="sm"
57
+ onClick={goToPreviousPage}
58
+ disabled={busy || !canPreviousPage}
59
+ className="h-11 min-w-11"
60
+ >
61
+ <ChevronLeft className="icon-xs mr-1" />
62
+ Previous
63
+ </Button>
64
+ <span className="text-counter" role="status">
65
+ {totalPages === undefined ? `Page ${currentPage}` : `${currentPage} of ${totalPages}`}
66
+ </span>
67
+ <Button
68
+ variant="outline"
69
+ size="sm"
70
+ onClick={goToNextPage}
71
+ disabled={busy || !canNextPage}
72
+ className="h-11 min-w-11"
73
+ >
74
+ Next
75
+ <ChevronRight className="icon-xs ml-1" />
76
+ </Button>
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,72 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import type { ListPaginationState } from "./use-paged-list.js";
5
+
6
+ export interface CursorPage extends ListPaginationState {
7
+ /** Never known for cursor pagination — the counter reads "Page N". */
8
+ totalPages?: undefined;
9
+ /**
10
+ * Return to page 1 without walking back. Call it whenever the underlying
11
+ * query's inputs change (a new filter, a new tag, a new sort) — page 4 of the
12
+ * old result set is not page 4 of the new one.
13
+ */
14
+ reset: () => void;
15
+ }
16
+
17
+ export interface UseCursorPageOptions {
18
+ /** Called after the page actually changed. Same contract as `usePagedList`. */
19
+ onPageChange?: () => void;
20
+ }
21
+
22
+ /**
23
+ * Page state for a server-paged (cursor / `hasMore`) list: the 1-based page
24
+ * number plus guarded step controls, shaped as {@link ListPaginationState} so it
25
+ * drops straight into the ONE `ListPagination` control.
26
+ *
27
+ * The caller owns the data: it feeds `hasMore` from its query and passes
28
+ * `currentPage` back into that query. This hook owns only the page number — the
29
+ * piece each server-paged surface was otherwise re-declaring as a local
30
+ * `useState(1)` plus its own clamped increment/decrement (ENG-002, ENG-003).
31
+ *
32
+ * A surface whose data hook ALREADY owns its page state (it returns
33
+ * `page`/`hasNextPage`/`nextPage`/`prevPage` itself) does not need this hook —
34
+ * it maps those fields onto `ListPaginationState` directly. The hook exists for
35
+ * the surfaces that would otherwise hand-roll the state.
36
+ */
37
+ export function useCursorPage(
38
+ hasMore: boolean,
39
+ options: UseCursorPageOptions = {},
40
+ ): CursorPage {
41
+ const { onPageChange } = options;
42
+ const [currentPage, setCurrentPage] = useState(1);
43
+
44
+ // Guards live here, not at the call site: a click that cannot move the page
45
+ // must not fire `onPageChange` and must not push the page past the data.
46
+ const goToPreviousPage = useCallback(() => {
47
+ if (currentPage <= 1) return;
48
+ setCurrentPage(currentPage - 1);
49
+ onPageChange?.();
50
+ }, [currentPage, onPageChange]);
51
+
52
+ const goToNextPage = useCallback(() => {
53
+ if (!hasMore) return;
54
+ setCurrentPage(currentPage + 1);
55
+ onPageChange?.();
56
+ }, [currentPage, hasMore, onPageChange]);
57
+
58
+ const reset = useCallback(() => {
59
+ if (currentPage === 1) return;
60
+ setCurrentPage(1);
61
+ onPageChange?.();
62
+ }, [currentPage, onPageChange]);
63
+
64
+ return {
65
+ currentPage,
66
+ canPreviousPage: currentPage > 1,
67
+ canNextPage: hasMore,
68
+ goToPreviousPage,
69
+ goToNextPage,
70
+ reset,
71
+ };
72
+ }
@@ -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`, or a
16
+ * 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
+ }
@@ -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, UseCursorPageOptions } from "./hooks/use-cursor-page.js";