- DataTable: keep `pagerNode` NULLABLE. Extracting Pager made it an always-truthy element, so the `controls` / `bottomControls` guards always fired and a table with nothing to page rendered empty `my-3` bands (plus an empty in-band PageToolbar). - ChannelsProvider: `setView` bails when the tab is unchanged (re-selecting the active tab, or goToFullHistory forcing "subscribed", stacked dead Back entries), and pushes a FRESH state like setPage does instead of spreading `_chan`/`_yt` markers into the tab entry. - Lift the layout into its own narrow context (`useChannelLayout`): App read the whole ChannelsState for the fade, so every keystroke in the manager's search box re-rendered the app root. - Reset the card page when the row set or sort changes (both tabs) — the clamp only hid a stale page, which came back when the filter was cleared. - Sort + slice the card rows only in cards mode; table mode was copy-sorting ~300 rows per render for a result nothing read. - Validate persisted `cardSize` / `sort` (`parseCardSize`, `parseSortState`), matching the clamp-on-read convention the module's other persisted values already follow; a corrupt size made the page count NaN and emptied the grid. - Pager: page-size changes scroll to the top too (they re-slice from a new first item). - ColumnSortControl: a stored sort naming a column this table no longer offers is treated as no sort, instead of showing a direction arrow for a sort that isn't applied. - Drop the stale "rows" layout comments (that variant was retired this sprint).
65 lines
2.6 KiB
TypeScript
65 lines
2.6 KiB
TypeScript
import { ArrowDown, ArrowUp } from "lucide-react";
|
|
import { type Column } from "./DataTable";
|
|
import { type SortState } from "../lib/columnSort";
|
|
|
|
// A sort control for views without clickable column headers (the channel manager's card layout):
|
|
// a dropdown of the sortable columns + an asc/desc toggle, mirroring the feed toolbar's sort. It's
|
|
// driven by the SAME `SortState` the DataTable header uses, so table and cards stay in sync. The
|
|
// dropdown's options come straight from the columns' `sortable`/`sortValue` metadata — no new list.
|
|
export default function ColumnSortControl<T>({
|
|
columns,
|
|
sort,
|
|
onChange,
|
|
label,
|
|
defaultLabel,
|
|
ascLabel,
|
|
descLabel,
|
|
}: {
|
|
columns: Column<T>[];
|
|
sort: SortState;
|
|
onChange: (next: SortState) => void;
|
|
/** Accessible name for the dropdown (e.g. "Sort"). */
|
|
label: string;
|
|
/** The "no sort / original order" option's text. */
|
|
defaultLabel: string;
|
|
ascLabel: string;
|
|
descLabel: string;
|
|
}) {
|
|
const sortable = columns.filter((c) => c.sortable && c.sortValue);
|
|
// A stored sort can name a column this table no longer offers (renamed, or `sortable` dropped).
|
|
// `sortRows` already leaves the rows unsorted then, so treat it as no sort here too — otherwise the
|
|
// select falls back to showing "default" while a direction arrow claims a sort that isn't applied.
|
|
const active = sortable.some((c) => c.key === sort?.key) ? sort : null;
|
|
return (
|
|
<div className="flex items-center gap-1">
|
|
<select
|
|
value={active?.key ?? ""}
|
|
onChange={(e) => {
|
|
const key = e.target.value;
|
|
// Keep the direction when re-picking the same column; a new column starts ascending.
|
|
onChange(key ? { key, dir: active?.key === key ? active.dir : "asc" } : null);
|
|
}}
|
|
aria-label={label}
|
|
className="bg-card border border-border rounded-lg px-2 py-1.5 text-sm outline-none focus:border-accent"
|
|
>
|
|
<option value="">{defaultLabel}</option>
|
|
{sortable.map((c) => (
|
|
<option key={c.key} value={c.key}>
|
|
{c.header}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{active && (
|
|
<button
|
|
onClick={() => onChange({ key: active.key, dir: active.dir === "asc" ? "desc" : "asc" })}
|
|
title={active.dir === "asc" ? ascLabel : descLabel}
|
|
aria-label={active.dir === "asc" ? ascLabel : descLabel}
|
|
className="shrink-0 p-1.5 rounded-lg border border-border bg-card text-fg hover:border-accent hover:text-accent active:translate-y-px transition"
|
|
>
|
|
{active.dir === "asc" ? <ArrowUp className="w-4 h-4" /> : <ArrowDown className="w-4 h-4" />}
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|