feat(managers): bottom pager on the channel tables (E4 S1)

DataTable gains a working controlsPosition="both": the leading controls
(e.g. the status chips) stay ONLY at the top; the bottom row is pager-only
(no duplicated chips). The subscribed + discovery channel tables opt in, so
the 33-page list has a pager at the bottom too — no scroll back up.
This commit is contained in:
2026-07-19 15:45:09 +02:00
parent 97611be9c4
commit 83b60df112
3 changed files with 78 additions and 64 deletions
+1 -1
View File
@@ -171,7 +171,7 @@ export default function ChannelDiscovery({
columns={columns} columns={columns}
rowKey={(c) => c.id} rowKey={(c) => c.id}
persistKey={accountKey(LS.channelDiscoveryTable) ?? undefined} persistKey={accountKey(LS.channelDiscoveryTable) ?? undefined}
controlsPosition="top" controlsPosition="both"
controlsInBand controlsInBand
controlsBandClassName="px-4 max-w-7xl mx-auto" controlsBandClassName="px-4 max-w-7xl mx-auto"
emptyText={t("channels.discovery.empty")} emptyText={t("channels.discovery.empty")}
+1 -1
View File
@@ -586,7 +586,7 @@ export default function Channels() {
columns={columns} columns={columns}
rowKey={(c) => c.id} rowKey={(c) => c.id}
persistKey={accountKey(LS.channelsTable) ?? undefined} persistKey={accountKey(LS.channelsTable) ?? undefined}
controlsPosition="top" controlsPosition="both"
controlsLeading={statusChips} controlsLeading={statusChips}
controlsInBand controlsInBand
controlsBandClassName="px-4 max-w-7xl mx-auto" controlsBandClassName="px-4 max-w-7xl mx-auto"
+76 -62
View File
@@ -288,70 +288,83 @@ export default function DataTable<T>({
); );
} }
const controls = // The pager + rows-per-page cluster on its own, so it can be rendered a second time at the bottom
controlsLeading != null || sorted.length > 0 ? ( // (controlsPosition "both") WITHOUT repeating the leading controls (e.g. the status chips) — those
<div className="flex items-center justify-between gap-3 my-3 text-sm flex-wrap"> // are header controls and belong only at the top.
<div className="flex items-center gap-3 flex-wrap">{controlsLeading}</div> const pagerNode =
<div className="flex items-center gap-3 flex-wrap"> totalPages > 1 || sorted.length > 0 ? (
{totalPages > 1 && ( <div className="flex items-center gap-3 flex-wrap">
<div className="flex items-center gap-2"> {totalPages > 1 && (
<button <div className="flex items-center gap-2">
onClick={() => setPage((p) => Math.max(0, p - 1))} <button
disabled={safePage === 0} onClick={() => setPage((p) => Math.max(0, p - 1))}
className="glass-card glass-hover flex items-center gap-1 px-3 py-1.5 rounded-lg disabled:opacity-40 transition" disabled={safePage === 0}
> className="glass-card glass-hover flex items-center gap-1 px-3 py-1.5 rounded-lg disabled:opacity-40 transition"
<ChevronLeft className="w-4 h-4" /> >
{t("datatable.pager.prev")} <ChevronLeft className="w-4 h-4" />
</button> {t("datatable.pager.prev")}
<span className="text-muted flex items-center gap-1.5"> </button>
{t("datatable.pager.pageLabel")} <span className="text-muted flex items-center gap-1.5">
<input {t("datatable.pager.pageLabel")}
type="number" <input
min={1} type="number"
max={totalPages} min={1}
value={pageInput} max={totalPages}
onChange={(e) => setPageInput(e.target.value)} value={pageInput}
onBlur={commitPageInput} onChange={(e) => setPageInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && commitPageInput()} onBlur={commitPageInput}
aria-label={t("datatable.pager.pageLabel")} onKeyDown={(e) => e.key === "Enter" && commitPageInput()}
className="w-14 bg-card border border-border rounded-md px-1.5 py-1 text-xs text-center tabular-nums outline-none focus:border-accent" aria-label={t("datatable.pager.pageLabel")}
/> className="w-14 bg-card border border-border rounded-md px-1.5 py-1 text-xs text-center tabular-nums outline-none focus:border-accent"
{t("datatable.pager.ofTotal", { total: totalPages })} />
</span> {t("datatable.pager.ofTotal", { total: totalPages })}
<button </span>
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))} <button
disabled={safePage >= totalPages - 1} onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
className="glass-card glass-hover flex items-center gap-1 px-3 py-1.5 rounded-lg disabled:opacity-40 transition" 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")} >
<ChevronRight className="w-4 h-4" /> {t("datatable.pager.next")}
</button> <ChevronRight className="w-4 h-4" />
</div> </button>
)} </div>
{sorted.length > 0 && ( )}
<label className="flex items-center gap-2 text-muted"> {sorted.length > 0 && (
{t("datatable.rowsPerPage")} <label className="flex items-center gap-2 text-muted">
<select {t("datatable.rowsPerPage")}
value={size} <select
onChange={(e) => { value={size}
setSize(Number(e.target.value)); onChange={(e) => {
setPage(0); setSize(Number(e.target.value));
}} setPage(0);
className="bg-card border border-border rounded-md px-1.5 py-1 text-xs outline-none focus:border-accent" }}
> className="bg-card border border-border rounded-md px-1.5 py-1 text-xs outline-none focus:border-accent"
{pageSizeOptions.map((n) => ( >
<option key={n} value={n}> {pageSizeOptions.map((n) => (
{n} <option key={n} value={n}>
</option> {n}
))} </option>
<option value={0}>{t("datatable.all")}</option> ))}
</select> <option value={0}>{t("datatable.all")}</option>
</label> </select>
)} </label>
</div> )}
</div> </div>
) : null; ) : null;
const controls =
controlsLeading != null || pagerNode ? (
<div className="flex items-center justify-between gap-3 my-3 text-sm flex-wrap">
<div className="flex items-center gap-3 flex-wrap">{controlsLeading}</div>
{pagerNode}
</div>
) : null;
// Bottom row for "both": just the pager, right-aligned (the leading controls stay at the top).
const bottomControls = pagerNode ? (
<div className="flex items-center justify-end gap-3 my-3 text-sm flex-wrap">{pagerNode}</div>
) : null;
const topControls = const topControls =
controlsInBand && controls ? ( controlsInBand && controls ? (
<PageToolbar> <PageToolbar>
@@ -476,7 +489,8 @@ export default function DataTable<T>({
<div className="text-muted py-8 text-center text-sm">{emptyText ?? t("datatable.empty")}</div> <div className="text-muted py-8 text-center text-sm">{emptyText ?? t("datatable.empty")}</div>
)} )}
{(controlsPosition === "bottom" || controlsPosition === "both") && controls} {controlsPosition === "bottom" && controls}
{controlsPosition === "both" && bottomControls}
</div> </div>
); );
} }