From b443ac0c848113a003fefd31f1d9fa32ee1633cc Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 23 Oct 2026 13:45:40 +0200 Subject: [PATCH] fix(frontend): the layer gates cover what the move left outside them, and each page keeps its own logic Gates the folder move had narrowed or left open: the MenuCheck contract scans the whole app again (it saw only components/); local/layers refuses an import of an entry (main.tsx, the /watch page) from any layer, and reads a backtick import() as the path it is; the /watch contract forbids the shell, the modules and the features by folder (its one path to App had gone stale); chunkGuard's own test gets the vitest focus/skip rules. The root tests' exemption from local/layers is in the gate register, with its three imports. Placement: Plex's chip builder moves into lib beside the feed's, the folder names into features/folders, and three files only one page uses into that page. ui/FilterRail takes its reset from useRail instead of a page id. The channel page reads the feed's view and builds "Show in feed" itself, and the feed's scroll key is the feed's hook, so the shell holds no feed logic. The floating header re-measures when one page's pills replace another's in the same commit (the claim count stayed equal, so only the observer caught it). A Plex view applied and edited now stays across a visit to another page, as the feed's does: pinned by a test. docs/development.md says what a page with a rail adds in lib. --- CLAUDE.md | 2 +- docs/development.md | 5 + docs/gates.md | 3 +- frontend/eslint.config.js | 34 +++- frontend/src/components/ui/FilterRail.tsx | 13 +- .../ui/PageShell.header.dom.test.tsx | 40 +++++ frontend/src/components/ui/PageShell.tsx | 17 +- .../components/ui/menuCheck.contract.test.ts | 3 +- .../ui/pressableSelectable.contract.test.ts | 2 +- .../src/features/downloads/DownloadTray.tsx | 3 +- .../features/filters/PersonalViewsModal.tsx | 2 +- .../folders}/fileFolders.tsx | 0 .../navigation/moduleStores.contract.test.tsx | 3 +- frontend/src/lib/gates.contract.test.ts | 7 +- frontend/src/lib/gates.snapshot.json | 61 +++++++ frontend/src/lib/layers.contract.test.ts | 29 ++++ frontend/src/lib/plexActiveFilters.ts | 154 +++++++++++++++++ frontend/src/lib/useRail.ts | 12 +- frontend/src/modules/channels/Channels.tsx | 4 +- .../channels}/TagFilterChips.dom.test.tsx | 0 .../channels}/TagFilterChips.tsx | 0 .../src/modules/channels/channelColumns.tsx | 2 +- .../channels}/useTagSuggestions.dom.test.tsx | 0 .../channels}/useTagSuggestions.ts | 2 +- .../feed/ChannelPage.backfill.dom.test.tsx | 4 +- .../feed/ChannelPage.banner.dom.test.tsx | 4 +- .../feed/ChannelPage.busyFocus.dom.test.tsx | 7 +- .../feed/ChannelPage.fold.dom.test.tsx | 4 +- frontend/src/modules/feed/ChannelPage.tsx | 22 ++- .../feed/ChannelPage.watchState.dom.test.tsx | 4 +- frontend/src/modules/feed/Sidebar.tsx | 8 +- frontend/src/modules/feed/index.ts | 5 +- .../feed/useFeedPageHooks.dom.test.tsx | 63 +++++++ frontend/src/modules/feed/useFeedPageHooks.ts | 30 ++++ .../src/modules/files/Files.menu.dom.test.tsx | 2 +- frontend/src/modules/files/Files.tsx | 4 +- frontend/src/modules/files/FilesRail.tsx | 6 +- .../files}/VideoEditor.partial.dom.test.tsx | 0 .../player => modules/files}/VideoEditor.tsx | 0 frontend/src/modules/files/fileBadges.tsx | 2 +- frontend/src/modules/files/index.ts | 3 +- frontend/src/modules/files/useFileActions.tsx | 2 +- .../src/modules/files/useFolderActions.tsx | 2 +- .../src/modules/playlists/PlaylistsRail.tsx | 4 +- .../plex/PlexSavedViewsProvider.dom.test.tsx | 45 ++++- frontend/src/modules/plex/PlexSidebar.tsx | 8 +- frontend/src/modules/plex/index.ts | 3 +- .../src/modules/plex/usePlexActiveFilters.ts | 155 +----------------- frontend/src/modules/settings/ApiTokens.tsx | 2 +- frontend/src/shell/App.tsx | 52 +----- frontend/src/watchEntry.contract.test.ts | 12 +- 51 files changed, 561 insertions(+), 290 deletions(-) rename frontend/src/{modules/files => features/folders}/fileFolders.tsx (100%) create mode 100644 frontend/src/lib/plexActiveFilters.ts rename frontend/src/{features/filters => modules/channels}/TagFilterChips.dom.test.tsx (100%) rename frontend/src/{features/filters => modules/channels}/TagFilterChips.tsx (100%) rename frontend/src/{features/channel => modules/channels}/useTagSuggestions.dom.test.tsx (100%) rename frontend/src/{features/channel => modules/channels}/useTagSuggestions.ts (98%) create mode 100644 frontend/src/modules/feed/useFeedPageHooks.dom.test.tsx create mode 100644 frontend/src/modules/feed/useFeedPageHooks.ts rename frontend/src/{features/player => modules/files}/VideoEditor.partial.dom.test.tsx (100%) rename frontend/src/{features/player => modules/files}/VideoEditor.tsx (100%) diff --git a/CLAUDE.md b/CLAUDE.md index 867ac765..223ce247 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,7 +78,7 @@ boot; a shared `/watch` link boots from its own entry, `watch.html` → `watchMa app-free by a contract test) → **`modules//`** (one folder per page: the page, its rail, its store, its tests, and its `module.ts` definition — label, icon, gate, the lazy page and rail, its store — collected in nav order by `features/navigation/moduleRegistry`, which also mounts every store at the root; a new page is its folder, its id in -`lib/urlState` and its entry there) → +`lib/urlState` and its entry there, plus its panel in `lib/panelLayout` if it has a rail) → **`features//`** (what several pages share: video cards, filters, downloads, the player, navigation) → the **platform layer `components/ui/`** (domain-free primitives: the popover engine and its menus, `Modal`/`Overlay`, `Button`/`form`, `DataTable`/`VirtualGrid`, `QueryState`, diff --git a/docs/development.md b/docs/development.md index 8af4586e..0bb8d445 100644 --- a/docs/development.md +++ b/docs/development.md @@ -51,6 +51,11 @@ A frontend page is one folder, `frontend/src/modules//` (the layering is in 4. The id in `lib/urlState`'s `PAGES`, and the definition in `features/navigation/moduleRegistry.ts`, in nav-rail order. The rail, the palette, the `g` chords, the tab title and the root's stores all read from there. 5. Its strings in both locales (`src/i18n/locales`). +6. A page with a side rail also names its panel below the module, in `lib/` (rails share one layout + and collapse machinery, which lib may not learn from a module): the `PanelId` and its default group + order, storage and pref keys in `lib/panelLayout.ts`, its collapse flag in `lib/useRail.ts` + (`COLLAPSE`), and those keys in `lib/storage.ts`. The rail calls `useRail("")` and hands what it + returns to `ui/FilterRail`; tsc names each table still missing the new id. What holds it: `moduleRegistry.contract.test.ts` (every id defined once, from its own folder, lazily), `moduleStores.contract.test.tsx` (no store reads another module), the `local/layers` lint rule (imports only go diff --git a/docs/gates.md b/docs/gates.md index da680464..b0e23f5f 100644 --- a/docs/gates.md +++ b/docs/gates.md @@ -61,7 +61,8 @@ them itself (`no-undef`, `no-redeclare`, `no-dupe-keys`, …), nor the formattin | src tests · ui tests · lib tests | `local/literal-a11y-text`, `local/input-primitive`, `local/surface-role`, `local/glyph-link` — off | A test renders throwaway markup (a raw input as a focus target, an untranslated label) that is not product UI. `local/button-primitive` stays on: in a test file (and in a control primitive) it only refuses a raw `