refactor(sort): source-agnostic SortControl + SortOption model (R13 S1 foundation)

Introduce the one sort control every content surface will share:
- columnSort.ts gains the SortOption vocabulary + pure pickSort/healSort/
  hydrateSort helpers (direction lives in SortState, never in the key).
- SortControl.tsx: the generic dropdown + asc/desc toggle, with an optional
  "no sort" choice (tables) and a trailing adornment slot (the feed's reshuffle).
- ColumnSortControl is now a thin wrapper over SortControl (API unchanged, so
  Channels + ChannelDiscovery are untouched); optionsFromColumns moved to
  lib/sortOptions.ts to keep the component file fast-refresh clean.
- useSortState.ts: the per-account persist/hydrate/heal plumbing the channel
  manager and discovery each hand-rolled, ready to replace both.

Pure logic is fully unit-tested (columnSort.test.ts 10 -> 23). Gate green:
tsc/eslint/prettier clean, vitest 93 pass. Feed/Plex/channels migration follows.
This commit is contained in:
2026-08-13 23:24:59 +02:00
parent f3b95ed543
commit 7d9da80795
6 changed files with 260 additions and 44 deletions
+24
View File
@@ -0,0 +1,24 @@
import { useCallback, useState } from "react";
import { getAccountRaw, setAccountRaw } from "./storage";
import { hydrateSort, type SortOption, type SortState } from "./columnSort";
// The per-account persisted sort state, hydrated + healed against the current options — the plumbing
// the channel manager and channel discovery each hand-rolled (useState + parseSortState + a persist
// key). One home so a new sortable surface (Plex, a future table) gets persistence for free.
export function useSortState(
storageBase: string,
options: SortOption[],
fallback: SortState = null,
): [SortState, (next: SortState) => void] {
const [sort, setSort] = useState<SortState>(() =>
hydrateSort(getAccountRaw(storageBase), options, fallback),
);
const set = useCallback(
(next: SortState) => {
setSort(next);
setAccountRaw(storageBase, JSON.stringify(next));
},
[storageBase],
);
return [sort, set];
}