refactor(frontend): each module defines itself in its folder; rails load lazily and keep their own state
A page used to be listed three times (lib/modules for its label, icon and gate; shell/moduleRegistry for its page and rail; lib/pageMeta for its title), and App held every rail's layout and collapse flag. Now each module's `module.ts` says it all — label, icon, gate, its lazy page and rail, its width — handed out through its index.ts, and features/navigation/moduleRegistry lists them in nav order for the rail, the palette, the tab title, the reachable set and the shell's render. A contract test holds the registry to the page ids and each definition to its own folder, with no static import of a page. Every rail is lazy now, loaded together with its page so a pinned rail never arrives after it and shoves the content aside; the feed's and the playlists' rails took @dnd-kit off the boot path (boot 428.5 → 393.9 KiB gzip, the App chunk 122 → 32 KiB). A rail reads its own layout and collapse flag (lib/useRail) and writes a layout change to the `me` cache too, since it now mounts anew on each visit. App's session effects, its one-shot entry-URL handlers and the admin nudge move into shell hooks; App is layout. The registry's three layer suppressions are gone: none is left.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import type { Me } from "../../lib/api";
|
||||
import type { Page } from "../../lib/urlState";
|
||||
import type { ModuleDef } from "../../lib/moduleDef";
|
||||
import { homeModule } from "../../modules/home";
|
||||
import { feedModule } from "../../modules/feed";
|
||||
import { channelsModule } from "../../modules/channels";
|
||||
import { playlistsModule } from "../../modules/playlists";
|
||||
import { plexModule } from "../../modules/plex";
|
||||
import { notificationsModule } from "../../modules/notifications";
|
||||
import { filesModule } from "../../modules/files";
|
||||
import { statsModule } from "../../modules/stats";
|
||||
import { schedulerModule } from "../../modules/scheduler";
|
||||
import { configModule } from "../../modules/config";
|
||||
import { usersModule } from "../../modules/users";
|
||||
import { activityModule } from "../../modules/activity";
|
||||
import { settingsModule } from "../../modules/settings";
|
||||
|
||||
// ── The module registry ───────────────────────────────────────────────────────────────────────
|
||||
// Every page, as each module defines itself (its folder's module.ts), in one ordered list: the nav
|
||||
// rail, the palette, the `g` chords, the tab title, the reachable set and the shell's render all read
|
||||
// from here, so a new or newly gated module shows up everywhere at once. Each definition's page and
|
||||
// rail are lazy imports, so listing them here pulls no page into the boot chunk (chunkGuard holds it).
|
||||
//
|
||||
// Ordered in nav-rail order — this array IS the sequence the rail and the palette list. Home leads: it
|
||||
// is the landing (see navigationInit.loadInitialPage). The feed takes the YouTube mark its own toolbar
|
||||
// already uses, so it and Home sit apart by shape and not just by position.
|
||||
const MODULES: readonly ModuleDef[] = [
|
||||
homeModule,
|
||||
feedModule,
|
||||
channelsModule,
|
||||
playlistsModule,
|
||||
plexModule,
|
||||
notificationsModule,
|
||||
filesModule,
|
||||
statsModule,
|
||||
schedulerModule,
|
||||
configModule,
|
||||
usersModule,
|
||||
activityModule,
|
||||
];
|
||||
|
||||
// Settings is reachable (its own rail button, a `g` chord) but is not a primary module in the ordered
|
||||
// nav list, so it lives beside the array rather than in it.
|
||||
const ALL: readonly ModuleDef[] = [...MODULES, settingsModule];
|
||||
|
||||
// Every Page has a definition: the registry contract test pins this list against lib/urlState's ids.
|
||||
export const REGISTERED_PAGES: readonly Page[] = ALL.map((m) => m.id);
|
||||
|
||||
const BY_ID = Object.fromEntries(ALL.map((m) => [m.id, m])) as Record<Page, ModuleDef>;
|
||||
|
||||
// Look up a module's definition by page id (every Page is covered, incl. settings).
|
||||
export function moduleDef(id: Page): ModuleDef {
|
||||
return BY_ID[id];
|
||||
}
|
||||
|
||||
// The i18n key for each module's SHORT display name, and its rail label.
|
||||
export const moduleLabelKey = Object.fromEntries(ALL.map((m) => [m.id, m.labelKey])) as Record<
|
||||
Page,
|
||||
string
|
||||
>;
|
||||
export const moduleRailKey = Object.fromEntries(ALL.map((m) => [m.id, m.railKey])) as Record<
|
||||
Page,
|
||||
string
|
||||
>;
|
||||
|
||||
// Admin/system modules — rendered in their own section (below a divider) in the nav rail.
|
||||
export const SYSTEM_PAGES: readonly Page[] = MODULES.filter((m) => m.system).map((m) => m.id);
|
||||
|
||||
// The ordered list of primary module pages available to THIS user, in nav-rail order. Availability
|
||||
// is derived from the account via each module's `gate`, so the reachable set changes with the
|
||||
// account state, not the UI.
|
||||
export function moduleOrder(me: Me): Page[] {
|
||||
return MODULES.filter((m) => !m.gate || m.gate(me)).map((m) => m.id);
|
||||
}
|
||||
|
||||
/** The page this account is shown for `page`: itself when its gate lets the account in, else the
|
||||
* feed (a link to an admin page, a Back onto one, an admin role taken away mid-session). Signed
|
||||
* out, no page renders, so nothing is moved. */
|
||||
export function reachablePage(page: Page, me: Me | null | undefined): Page {
|
||||
const gate = moduleDef(page).gate;
|
||||
return !me || !gate || gate(me) ? page : "feed";
|
||||
}
|
||||
|
||||
// The i18n key for a page's human title (the browser tab): a module's optional `titleKey` is the
|
||||
// longer, descriptive one (e.g. "Channel manager"), falling back to its short nav `labelKey`.
|
||||
export function pageTitleKey(page: Page): string {
|
||||
const def = moduleDef(page);
|
||||
return def.titleKey ?? def.labelKey;
|
||||
}
|
||||
|
||||
/** Fetch the code of these pages and of their side rails now, ahead of their first visit. Errors are
|
||||
* swallowed: a chunk that fails to preload is simply fetched (and its failure surfaced) on visit. */
|
||||
export function preloadPages(pages: Page[]): void {
|
||||
for (const p of pages) {
|
||||
const def = moduleDef(p);
|
||||
def.Page.preload().catch(() => {});
|
||||
def.Rail?.preload().catch(() => {});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user