refactor: component files export only components; lint fails on a warning

The 43 react-refresh/only-export-components warnings the gate let through meant an edit to a
provider (or AnchoredPopover, Toaster, plexDetailUi, …) could not be hot-swapped: Fast Refresh
reloads a module that also exports hooks or helpers.

- Each provider's contexts, the types its hooks return and the hooks themselves live in a sibling
  context module (navigationContext.ts, channelsContext.ts, …); the provider file keeps the
  component. loadAccountFilters moved next to the feed filters' defaults (feedFiltersInit.ts),
  following navigationInit/playlistsInit.
- Helpers that sat in component files have their own modules: welcomeMode, downloadTitle,
  plexPlayable, channelSync, ui/anchoredPopoverHooks, ui/pageToolbarSlot, ui/pagerContent,
  ui/toastLevels, ui/overlayOrder, ui/tableAlign, lib/plexDetail; the cells of the channel columns
  and the registry's page wrappers are components files of their own (channelCells, modulePages).
- usePersistedTab, an alias of useAccountPersistedState, is gone; its callers use the original.

npm run lint is now eslint --max-warnings 0, so a warning fails the gate like an error does.
This commit is contained in:
2026-09-17 02:59:09 +02:00
parent 569190480d
commit c3d88d9b20
158 changed files with 1600 additions and 1431 deletions
+3 -51
View File
@@ -2,14 +2,9 @@ import type { ComponentType } from "react";
import { lazyWithPreload, type PreloadableComponent } from "../lib/lazyWithPreload";
import type { Page } from "../lib/urlState";
import type { PanelId, PanelLayout } from "../lib/panelLayout";
import { useMe } from "../lib/useMe";
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
import { useFeedView, useFeedViewActions } from "./FeedViewProvider";
import { useNavigation, useNavigationActions } from "./NavigationProvider";
import Sidebar from "./Sidebar";
import PlaylistsRail from "./PlaylistsRail";
import PersonalViewsModal from "./PersonalViewsModal";
import { useSavedViews } from "./SavedViewsProvider";
import { Feed, FeedPage, Messages, MessagesPage } from "./modulePages";
// The RENDER half of the module registry: which component (and side rail) each page renders. The
// metadata half — nav order, label, icon, gate — lives in lib/modules.ts (imported by NavSidebar /
@@ -18,21 +13,19 @@ import { useSavedViews } from "./SavedViewsProvider";
//
// Every content entry is a PROP-FREE component: it reads `me` from useMe() and its module-local state
// from the scoped providers, so App renders `<Content />` uniformly. The two that need per-render
// wiring get a thin wrapper here (FeedPage supplies the GLOBAL feed scope; the channel page threads
// its own isolated scope to <Feed> directly — see the scope note in FeedFiltersProvider).
// wiring get a thin wrapper in modulePages (FeedPage supplies the GLOBAL feed scope; the channel page
// threads its own isolated scope to <Feed> directly — see the scope note in FeedFiltersProvider).
// Route-level code splitting: each page is its own lazy chunk (admin-only pages never reach
// non-admins; the logged-out landing pulls only the shell). Rendered inside App's <Suspense>. Each
// one is preloadable: App fetches the chunks of every page the account can reach once the first page
// is on screen (preloadPages below), so switching modules never shows the chunk fallback.
const Home = lazyWithPreload(() => import("./Home"));
const Feed = lazyWithPreload(() => import("./Feed"));
const Channels = lazyWithPreload(() => import("./Channels"));
const Playlists = lazyWithPreload(() => import("./Playlists"));
const PlexBrowse = lazyWithPreload(() => import("./PlexBrowse"));
const PlexSidebar = lazyWithPreload(() => import("./PlexSidebar"));
const NotificationsPanel = lazyWithPreload(() => import("./NotificationsPanel"));
const Messages = lazyWithPreload(() => import("./Messages"));
const DownloadCenter = lazyWithPreload(() => import("./DownloadCenter"));
const Stats = lazyWithPreload(() => import("./Stats"));
const Scheduler = lazyWithPreload(() => import("./Scheduler"));
@@ -41,47 +34,6 @@ const AdminUsers = lazyWithPreload(() => import("./AdminUsers"));
const AuditLog = lazyWithPreload(() => import("./AuditLog"));
const SettingsPanel = lazyWithPreload(() => import("./SettingsPanel"));
// The GLOBAL feed page: wires <Feed> to the app-wide feed filters / view / live-search. (The channel
// page renders <Feed> itself with its own channel-scoped filters, so those two never share state.)
function FeedPage() {
const me = useMe();
const filters = useFeedFilters();
const { setFilters } = useFeedFiltersActions();
const view = useFeedView();
const { setView } = useFeedViewActions();
const { ytSearch } = useNavigation();
const { enterYtSearch, exitYtSearch } = useNavigationActions();
// The Personal Views modal lives at the feed-page level (always mounted while on the feed, unmounts
// on navigation) rather than inside the collapsible filter rail — otherwise opening it from the
// always-visible toolbar view chip while the rail is unpinned would render nothing and latch open.
const { modalOpen } = useSavedViews();
return (
<>
<Feed
filters={filters}
setFilters={setFilters}
view={view}
setView={setView}
canRead={me.can_read}
isDemo={me.is_demo}
ytSearch={ytSearch}
onYtSearch={enterYtSearch}
onExitYtSearch={exitYtSearch}
/>
{modalOpen && !me.is_demo && <PersonalViewsModal />}
</>
);
}
function MessagesPage() {
const me = useMe();
return (
<div className="p-4 max-w-3xl w-full mx-auto">
<Messages meId={me.id} />
</div>
);
}
export type PageDef = {
// Prop-free by construction: reads `me` + module state from hooks/providers. Preloadable by type, so
// a new page cannot silently opt out of preloadPages.