import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { hasPagerContent, pageCount } from "./pagerContent"; import { FocusSafeButton } from "./FocusSafeButton"; import SegmentedToggle from "./SegmentedToggle"; // The Prev / page-input / Next + "N per page" cluster, lifted out of DataTable so a non-table view // (the channel manager's card layout) can paginate the same way. Controlled: the caller owns page + // size and slices its own rows; this only renders the controls and reports changes. `sizeLabel` lets // the caller say "Rows per page" or "Cards per page". Returns null when there's nothing to page. export default function Pager({ page, size, total, pageSizeOptions, onPageChange, onSizeChange, sizeLabel, }: { /** Current page (0-based). */ page: number; /** Page size; `<= 0` means "All" (one page). */ size: number; /** Total item count across all pages. */ total: number; pageSizeOptions: number[]; onPageChange: (page: number) => void; onSizeChange: (size: number) => void; sizeLabel: string; }) { const { t } = useTranslation(); const totalPages = pageCount(total, size); const safePage = Math.min(page, totalPages - 1); // Local text for the editable "jump to page" box (committed on Enter/blur). const [pageInput, setPageInput] = useState(String(safePage + 1)); useEffect(() => setPageInput(String(safePage + 1)), [safePage]); // Turning the page — or resizing it, which re-slices from a different first item — swaps the whole // list, so put the reader at its top; landing mid-list in a page you haven't seen is disorienting. // The page scroller is the app's
(PageScroller documents itself as the ONE scroller; the // same implicit contract BackToTop and lib/listPaging's scrollParent (VirtualGrid, listNav) already rely on). function scrollListToTop() { document.querySelector("main")?.scrollTo({ top: 0 }); } function goToPage(p: number) { onPageChange(p); scrollListToTop(); } function commitPageInput() { const n = parseInt(pageInput, 10); if (!isNaN(n)) goToPage(Math.min(totalPages - 1, Math.max(0, n - 1))); else setPageInput(String(safePage + 1)); } if (!hasPagerContent(total, size)) return null; return (
{totalPages > 1 && (
goToPage(Math.max(0, safePage - 1))} disabled={safePage === 0} className="glass-card glass-hover flex items-center gap-1 px-3 py-1.5 rounded-lg disabled:opacity-40 transition" > {t("datatable.pager.prev")} {t("datatable.pager.pageLabel")} setPageInput(e.target.value)} onBlur={commitPageInput} onKeyDown={(e) => e.key === "Enter" && commitPageInput()} aria-label={t("datatable.pager.pageLabel")} className="w-14 bg-surface border border-border rounded-md px-1.5 py-1 text-xs text-center tabular-nums focus:outline-hidden focus:ring-2 focus:ring-accent/40" /> {t("datatable.pager.ofTotal", { total: totalPages })} goToPage(Math.min(totalPages - 1, safePage + 1))} disabled={safePage >= totalPages - 1} className="glass-card glass-hover flex items-center gap-1 px-3 py-1.5 rounded-lg disabled:opacity-40 transition" > {t("datatable.pager.next")}
)} {total > 0 && (
{/* A handful of fixed sizes plus "All": segmented, the house control for a short fixed set. */} { onSizeChange(Number(v)); scrollListToTop(); }} options={[ ...pageSizeOptions.map((n) => ({ value: String(n), label: String(n) })), { value: "0", label: t("datatable.all") }, ]} />
)}
); }