@ttt-productions/ui-core 0.11.9 → 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"]}
@@ -6,9 +6,10 @@ import { type ButtonProps } from "./button.js";
6
6
  * consumer can never route them through the passthrough channel: `aria-label`
7
7
  * is set via {@link ScrollToTopButtonProps.ariaLabel}, extra classes via the
8
8
  * merge-aware {@link ScrollToTopButtonProps.className}, content via
9
- * {@link ScrollToTopButtonProps.icon}, and `onClick` is fixed to scroll-to-top.
10
- * This mirrors how the sibling `Button` destructures its owned `className` out
11
- * of the forwarded `...props`.
9
+ * {@link ScrollToTopButtonProps.icon}, and `onClick` is fixed to scroll-to-top
10
+ * (its scroll animation configurable via
11
+ * {@link ScrollToTopButtonProps.scrollBehavior}). This mirrors how the sibling
12
+ * `Button` destructures its owned `className` out of the forwarded `...props`.
12
13
  */
13
14
  type ScrollToTopButtonPassthrough = Omit<React.ComponentPropsWithoutRef<"button">, "className" | "onClick" | "aria-label" | "children">;
14
15
  export interface ScrollToTopButtonProps extends ScrollToTopButtonPassthrough {
@@ -24,7 +25,16 @@ export interface ScrollToTopButtonProps extends ScrollToTopButtonPassthrough {
24
25
  variant?: ButtonProps["variant"];
25
26
  /** Button size. Forwarded to the underlying Button. Default "icon". */
26
27
  size?: ButtonProps["size"];
28
+ /**
29
+ * Scroll behavior passed to `window.scrollTo`. Default "smooth". Pass "auto"
30
+ * to skip the smooth animation (e.g. for a reduced-motion mode). The package
31
+ * stays motion-policy agnostic: the consumer decides what to pass here — an
32
+ * explicit JS `behavior: "smooth"` is not overridden by a CSS
33
+ * `scroll-behavior` kill-switch, so an app-level reduced-motion mode must
34
+ * pass "auto" to actually stop the animation.
35
+ */
36
+ scrollBehavior?: ScrollBehavior;
27
37
  }
28
- export declare function ScrollToTopButton({ threshold, ariaLabel, className, icon, variant, size, ...rest }: ScrollToTopButtonProps): React.JSX.Element;
38
+ export declare function ScrollToTopButton({ threshold, ariaLabel, className, icon, variant, size, scrollBehavior, ...rest }: ScrollToTopButtonProps): React.JSX.Element;
29
39
  export {};
30
40
  //# sourceMappingURL=scroll-to-top-button.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scroll-to-top-button.d.ts","sourceRoot":"","sources":["../../../src/react/components/scroll-to-top-button.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAGvD;;;;;;;;;GASG;AACH,KAAK,4BAA4B,GAAG,IAAI,CACtC,KAAK,CAAC,wBAAwB,CAAC,QAAQ,CAAC,EACxC,WAAW,GAAG,SAAS,GAAG,YAAY,GAAG,UAAU,CACpD,CAAC;AAEF,MAAM,WAAW,sBAAuB,SAAQ,4BAA4B;IAC1E,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,4FAA4F;IAC5F,OAAO,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IACjC,uEAAuE;IACvE,IAAI,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED,wBAAgB,iBAAiB,CAAC,EAChC,SAAe,EACf,SAA2B,EAC3B,SAAS,EACT,IAAI,EACJ,OAAO,EACP,IAAa,EACb,GAAG,IAAI,EACR,EAAE,sBAAsB,qBA4BxB"}
1
+ {"version":3,"file":"scroll-to-top-button.d.ts","sourceRoot":"","sources":["../../../src/react/components/scroll-to-top-button.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAGvD;;;;;;;;;;GAUG;AACH,KAAK,4BAA4B,GAAG,IAAI,CACtC,KAAK,CAAC,wBAAwB,CAAC,QAAQ,CAAC,EACxC,WAAW,GAAG,SAAS,GAAG,YAAY,GAAG,UAAU,CACpD,CAAC;AAEF,MAAM,WAAW,sBAAuB,SAAQ,4BAA4B;IAC1E,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,4FAA4F;IAC5F,OAAO,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IACjC,uEAAuE;IACvE,IAAI,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAED,wBAAgB,iBAAiB,CAAC,EAChC,SAAe,EACf,SAA2B,EAC3B,SAAS,EACT,IAAI,EACJ,OAAO,EACP,IAAa,EACb,cAAyB,EACzB,GAAG,IAAI,EACR,EAAE,sBAAsB,qBA4BxB"}
@@ -4,7 +4,7 @@ import * as React from "react";
4
4
  import { ChevronUp } from "lucide-react";
5
5
  import { Button } from "./button.js";
6
6
  import { cn } from "../../lib/utils.js";
7
- export function ScrollToTopButton({ threshold = 400, ariaLabel = "Scroll to top", className, icon, variant, size = "icon", ...rest }) {
7
+ export function ScrollToTopButton({ threshold = 400, ariaLabel = "Scroll to top", className, icon, variant, size = "icon", scrollBehavior = "smooth", ...rest }) {
8
8
  const [isVisible, setIsVisible] = React.useState(false);
9
9
  React.useEffect(() => {
10
10
  const toggle = () => setIsVisible(window.scrollY > threshold);
@@ -12,7 +12,7 @@ export function ScrollToTopButton({ threshold = 400, ariaLabel = "Scroll to top"
12
12
  window.addEventListener("scroll", toggle, { passive: true });
13
13
  return () => window.removeEventListener("scroll", toggle);
14
14
  }, [threshold]);
15
- const onClick = () => window.scrollTo({ top: 0, behavior: "smooth" });
15
+ const onClick = () => window.scrollTo({ top: 0, behavior: scrollBehavior });
16
16
  return (_jsx(Button, { ...rest, variant: variant, size: size, onClick: onClick, "aria-label": ariaLabel, className: cn("fixed bottom-24 left-1/2 -translate-x-1/2 z-50 rounded-full shadow-lg transition-opacity duration-300", isVisible ? "opacity-100" : "opacity-0 pointer-events-none", className), children: icon ?? _jsx(ChevronUp, { className: "h-5 w-5" }) }));
17
17
  }
18
18
  //# sourceMappingURL=scroll-to-top-button.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"scroll-to-top-button.js","sourceRoot":"","sources":["../../../src/react/components/scroll-to-top-button.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,MAAM,EAAoB,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAC;AAgCxC,MAAM,UAAU,iBAAiB,CAAC,EAChC,SAAS,GAAG,GAAG,EACf,SAAS,GAAG,eAAe,EAC3B,SAAS,EACT,IAAI,EACJ,OAAO,EACP,IAAI,GAAG,MAAM,EACb,GAAG,IAAI,EACgB;IACvB,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAExD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;QAC9D,MAAM,EAAE,CAAC;QACT,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC5D,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAEhB,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;IAEtE,OAAO,CACL,KAAC,MAAM,OACD,IAAI,EACR,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,OAAO,gBACJ,SAAS,EACrB,SAAS,EAAE,EAAE,CACX,uGAAuG,EACvG,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,+BAA+B,EAC3D,SAAS,CACV,YAEA,IAAI,IAAI,KAAC,SAAS,IAAC,SAAS,EAAC,SAAS,GAAG,GACnC,CACV,CAAC;AACJ,CAAC","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { ChevronUp } from \"lucide-react\";\nimport { Button, type ButtonProps } from \"./button.js\";\nimport { cn } from \"../../lib/utils.js\";\n\n/**\n * Standard button DOM props forwarded to the rendered element, minus the props\n * this component owns. Owned props are excluded (not just overridden) so a\n * consumer can never route them through the passthrough channel: `aria-label`\n * is set via {@link ScrollToTopButtonProps.ariaLabel}, extra classes via the\n * merge-aware {@link ScrollToTopButtonProps.className}, content via\n * {@link ScrollToTopButtonProps.icon}, and `onClick` is fixed to scroll-to-top.\n * This mirrors how the sibling `Button` destructures its owned `className` out\n * of the forwarded `...props`.\n */\ntype ScrollToTopButtonPassthrough = Omit<\n React.ComponentPropsWithoutRef<\"button\">,\n \"className\" | \"onClick\" | \"aria-label\" | \"children\"\n>;\n\nexport interface ScrollToTopButtonProps extends ScrollToTopButtonPassthrough {\n /** Scroll threshold (px) before the button appears. Default 400. */\n threshold?: number;\n /** Aria label. Default \"Scroll to top\". */\n ariaLabel?: string;\n /** Optional extra classes for the button. Merged with the component's own classes. */\n className?: string;\n /** Optional override for the icon. */\n icon?: React.ReactNode;\n /** Button variant. Forwarded to the underlying Button. Defaults to Button's own default. */\n variant?: ButtonProps[\"variant\"];\n /** Button size. Forwarded to the underlying Button. Default \"icon\". */\n size?: ButtonProps[\"size\"];\n}\n\nexport function ScrollToTopButton({\n threshold = 400,\n ariaLabel = \"Scroll to top\",\n className,\n icon,\n variant,\n size = \"icon\",\n ...rest\n}: ScrollToTopButtonProps) {\n const [isVisible, setIsVisible] = React.useState(false);\n\n React.useEffect(() => {\n const toggle = () => setIsVisible(window.scrollY > threshold);\n toggle();\n window.addEventListener(\"scroll\", toggle, { passive: true });\n return () => window.removeEventListener(\"scroll\", toggle);\n }, [threshold]);\n\n const onClick = () => window.scrollTo({ top: 0, behavior: \"smooth\" });\n\n return (\n <Button\n {...rest}\n variant={variant}\n size={size}\n onClick={onClick}\n aria-label={ariaLabel}\n className={cn(\n \"fixed bottom-24 left-1/2 -translate-x-1/2 z-50 rounded-full shadow-lg transition-opacity duration-300\",\n isVisible ? \"opacity-100\" : \"opacity-0 pointer-events-none\",\n className,\n )}\n >\n {icon ?? <ChevronUp className=\"h-5 w-5\" />}\n </Button>\n );\n}\n"]}
1
+ {"version":3,"file":"scroll-to-top-button.js","sourceRoot":"","sources":["../../../src/react/components/scroll-to-top-button.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,MAAM,EAAoB,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAC;AA0CxC,MAAM,UAAU,iBAAiB,CAAC,EAChC,SAAS,GAAG,GAAG,EACf,SAAS,GAAG,eAAe,EAC3B,SAAS,EACT,IAAI,EACJ,OAAO,EACP,IAAI,GAAG,MAAM,EACb,cAAc,GAAG,QAAQ,EACzB,GAAG,IAAI,EACgB;IACvB,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAExD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;QAC9D,MAAM,EAAE,CAAC;QACT,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC5D,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAEhB,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAC,CAAC;IAE5E,OAAO,CACL,KAAC,MAAM,OACD,IAAI,EACR,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,OAAO,gBACJ,SAAS,EACrB,SAAS,EAAE,EAAE,CACX,uGAAuG,EACvG,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,+BAA+B,EAC3D,SAAS,CACV,YAEA,IAAI,IAAI,KAAC,SAAS,IAAC,SAAS,EAAC,SAAS,GAAG,GACnC,CACV,CAAC;AACJ,CAAC","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { ChevronUp } from \"lucide-react\";\nimport { Button, type ButtonProps } from \"./button.js\";\nimport { cn } from \"../../lib/utils.js\";\n\n/**\n * Standard button DOM props forwarded to the rendered element, minus the props\n * this component owns. Owned props are excluded (not just overridden) so a\n * consumer can never route them through the passthrough channel: `aria-label`\n * is set via {@link ScrollToTopButtonProps.ariaLabel}, extra classes via the\n * merge-aware {@link ScrollToTopButtonProps.className}, content via\n * {@link ScrollToTopButtonProps.icon}, and `onClick` is fixed to scroll-to-top\n * (its scroll animation configurable via\n * {@link ScrollToTopButtonProps.scrollBehavior}). This mirrors how the sibling\n * `Button` destructures its owned `className` out of the forwarded `...props`.\n */\ntype ScrollToTopButtonPassthrough = Omit<\n React.ComponentPropsWithoutRef<\"button\">,\n \"className\" | \"onClick\" | \"aria-label\" | \"children\"\n>;\n\nexport interface ScrollToTopButtonProps extends ScrollToTopButtonPassthrough {\n /** Scroll threshold (px) before the button appears. Default 400. */\n threshold?: number;\n /** Aria label. Default \"Scroll to top\". */\n ariaLabel?: string;\n /** Optional extra classes for the button. Merged with the component's own classes. */\n className?: string;\n /** Optional override for the icon. */\n icon?: React.ReactNode;\n /** Button variant. Forwarded to the underlying Button. Defaults to Button's own default. */\n variant?: ButtonProps[\"variant\"];\n /** Button size. Forwarded to the underlying Button. Default \"icon\". */\n size?: ButtonProps[\"size\"];\n /**\n * Scroll behavior passed to `window.scrollTo`. Default \"smooth\". Pass \"auto\"\n * to skip the smooth animation (e.g. for a reduced-motion mode). The package\n * stays motion-policy agnostic: the consumer decides what to pass here — an\n * explicit JS `behavior: \"smooth\"` is not overridden by a CSS\n * `scroll-behavior` kill-switch, so an app-level reduced-motion mode must\n * pass \"auto\" to actually stop the animation.\n */\n scrollBehavior?: ScrollBehavior;\n}\n\nexport function ScrollToTopButton({\n threshold = 400,\n ariaLabel = \"Scroll to top\",\n className,\n icon,\n variant,\n size = \"icon\",\n scrollBehavior = \"smooth\",\n ...rest\n}: ScrollToTopButtonProps) {\n const [isVisible, setIsVisible] = React.useState(false);\n\n React.useEffect(() => {\n const toggle = () => setIsVisible(window.scrollY > threshold);\n toggle();\n window.addEventListener(\"scroll\", toggle, { passive: true });\n return () => window.removeEventListener(\"scroll\", toggle);\n }, [threshold]);\n\n const onClick = () => window.scrollTo({ top: 0, behavior: scrollBehavior });\n\n return (\n <Button\n {...rest}\n variant={variant}\n size={size}\n onClick={onClick}\n aria-label={ariaLabel}\n className={cn(\n \"fixed bottom-24 left-1/2 -translate-x-1/2 z-50 rounded-full shadow-lg transition-opacity duration-300\",\n isVisible ? \"opacity-100\" : \"opacity-0 pointer-events-none\",\n className,\n )}\n >\n {icon ?? <ChevronUp className=\"h-5 w-5\" />}\n </Button>\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":"AAEA,YAAY,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC7G,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AACpL,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACjG,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,eAAe,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC1W,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,cAAc,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,aAAa,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AACtS,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAChQ,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC5E,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC5E,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACnI,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACvE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAChM,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AACnG,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACrI,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACzG,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AACzE,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,YAAY,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACjI,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzK,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAClG,YAAY,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEjE,YAAY,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,kCAAkC,EAAE,MAAM,oCAAoC,CAAC;AAC9I,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAEvE,YAAY,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,6CAA6C,CAAC;AAC3G,OAAO,EAAE,kBAAkB,EAAE,MAAM,6CAA6C,CAAC;AAEjF,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAG3D,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC7D,YAAY,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAEvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAC3E,YAAY,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAC;AAErF,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AACzE,YAAY,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAEnF,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAC1E,YAAY,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC"}
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.9",
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
+ }
@@ -11,9 +11,10 @@ import { cn } from "../../lib/utils.js";
11
11
  * consumer can never route them through the passthrough channel: `aria-label`
12
12
  * is set via {@link ScrollToTopButtonProps.ariaLabel}, extra classes via the
13
13
  * merge-aware {@link ScrollToTopButtonProps.className}, content via
14
- * {@link ScrollToTopButtonProps.icon}, and `onClick` is fixed to scroll-to-top.
15
- * This mirrors how the sibling `Button` destructures its owned `className` out
16
- * of the forwarded `...props`.
14
+ * {@link ScrollToTopButtonProps.icon}, and `onClick` is fixed to scroll-to-top
15
+ * (its scroll animation configurable via
16
+ * {@link ScrollToTopButtonProps.scrollBehavior}). This mirrors how the sibling
17
+ * `Button` destructures its owned `className` out of the forwarded `...props`.
17
18
  */
18
19
  type ScrollToTopButtonPassthrough = Omit<
19
20
  React.ComponentPropsWithoutRef<"button">,
@@ -33,6 +34,15 @@ export interface ScrollToTopButtonProps extends ScrollToTopButtonPassthrough {
33
34
  variant?: ButtonProps["variant"];
34
35
  /** Button size. Forwarded to the underlying Button. Default "icon". */
35
36
  size?: ButtonProps["size"];
37
+ /**
38
+ * Scroll behavior passed to `window.scrollTo`. Default "smooth". Pass "auto"
39
+ * to skip the smooth animation (e.g. for a reduced-motion mode). The package
40
+ * stays motion-policy agnostic: the consumer decides what to pass here — an
41
+ * explicit JS `behavior: "smooth"` is not overridden by a CSS
42
+ * `scroll-behavior` kill-switch, so an app-level reduced-motion mode must
43
+ * pass "auto" to actually stop the animation.
44
+ */
45
+ scrollBehavior?: ScrollBehavior;
36
46
  }
37
47
 
38
48
  export function ScrollToTopButton({
@@ -42,6 +52,7 @@ export function ScrollToTopButton({
42
52
  icon,
43
53
  variant,
44
54
  size = "icon",
55
+ scrollBehavior = "smooth",
45
56
  ...rest
46
57
  }: ScrollToTopButtonProps) {
47
58
  const [isVisible, setIsVisible] = React.useState(false);
@@ -53,7 +64,7 @@ export function ScrollToTopButton({
53
64
  return () => window.removeEventListener("scroll", toggle);
54
65
  }, [threshold]);
55
66
 
56
- const onClick = () => window.scrollTo({ top: 0, behavior: "smooth" });
67
+ const onClick = () => window.scrollTo({ top: 0, behavior: scrollBehavior });
57
68
 
58
69
  return (
59
70
  <Button
@@ -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";