// Shared column-sort primitives, lifted out of DataTable so a NON-table renderer (the channel // manager's card layout) can sort the same rows the same way — one comparator, one source of truth. // Kept structural (no `Column` import) so it has zero coupling to DataTable. export type SortState = { key: string; dir: "asc" | "desc" } | null; interface Sortable { key: string; sortValue?: (row: T) => string | number; } /** Sort a copy of `rows` by the sortable column named in `sort` (numeric diff or locale compare). * Returns `rows` unchanged when there's no sort or the column has no `sortValue`. */ export function sortRows(rows: T[], columns: Sortable[], sort: SortState): T[] { if (!sort) return rows; const get = columns.find((c) => c.key === sort.key)?.sortValue; if (!get) return rows; return [...rows].sort((a, b) => { const av = get(a); const bv = get(b); const cmp = typeof av === "number" && typeof bv === "number" ? av - bv : String(av).localeCompare(String(bv)); return sort.dir === "asc" ? cmp : -cmp; }); } /** Coerce a stored value to a SortState. Anything malformed (corrupt storage, a shape from an older * build) becomes "no sort" rather than flowing a junk `dir` into the comparator. */ export function parseSortState(raw: unknown): SortState { if (!raw || typeof raw !== "object") return null; const { key, dir } = raw as { key?: unknown; dir?: unknown }; return typeof key === "string" && (dir === "asc" || dir === "desc") ? { key, dir } : null; } /** The header-click cycle: unsorted → ascending → descending → unsorted. */ export function cycleSort(current: SortState, key: string): SortState { if (!current || current.key !== key) return { key, dir: "asc" }; if (current.dir === "asc") return { key, dir: "desc" }; return null; }