feat(channels): shareable manager view in the URL — tab+sort+column-filters (S8)

The Channels-manager content state now round-trips through the address bar,
so a manager view is bookmarkable/shareable — the S4 pattern applied to the
last content page the audit flagged.

- New lib/channelsUrlState.ts (channelsParamUpdates / channelsParamsToState /
  hasChannelsParams) under NAMESPACED keys ctab/csort/cf — channels-unique
  like ?v=/?pl/pscope, so no other provider reads or clears them and the
  feed's URL-stripping can't delete them. The mixed scalar/array FilterMap
  (sync/priority scalars + tags multi) rides in `cf` as a JSON blob so both
  shapes round-trip exactly. Only non-default values emit.
- ChannelsProvider mirrors {view, sort, filters} via useUrlMirror, page-gated
  on channels (off-page the keys clear; localStorage still holds the state and
  it re-mirrors on return). A shared link wins over stored state: readChannels
  UrlSeed captured once, applied before paint through the slice setters (so the
  account-pin re-hydrate sees them dirty and won't reload storage over the
  shared view), then persisted to the account once pinned so it survives a
  later plain reload — mirrors PlexProvider's seed/rehydrate. The name search
  stays ephemeral (out of the shared contract).
- NavigationProvider's page-switch nulls ctab/csort/cf alongside the plex keys
  so switching modules resets to the manager root.

Tests: channelsUrlState.dom.test.ts (9 round-trip) + ChannelsProvider.dom
seed/mirror (2); the existing tab-history test's beforeEach now resets the
query string too (the mirror writes it). FE vitest 503→514. FE-only.
This commit is contained in:
2026-08-27 04:37:53 +02:00
parent 9cd4ae09d4
commit 7fbb996297
5 changed files with 320 additions and 5 deletions
@@ -127,8 +127,8 @@ export function NavigationProvider({ children }: { children: ReactNode }) {
// app. A fresh { sfPage } (no carried-over _yt/_chan markers) so each page starts at its root, and
// the address bar reflects the page (feed is implicit) while clearing the view-specific params
// (chan/v/pl and the plex content keys pscope/psort/pshow/pf) — a page switch resets to that
// module's root. Feed filters (owned by FeedFiltersProvider) are left intact; the plex keys
// re-mirror from PlexProvider on return to the plex page.
// module's root. Feed filters (owned by FeedFiltersProvider) are left intact; the plex/channels
// keys re-mirror from PlexProvider / ChannelsProvider on return to their page.
window.history.pushState(
{ sfPage: next, sfScroll: nextNavId() },
"",
@@ -141,6 +141,9 @@ export function NavigationProvider({ children }: { children: ReactNode }) {
psort: null,
pshow: null,
pf: null,
ctab: null,
csort: null,
cf: null,
}),
);
notifyNav();