import type { ComponentType, ReactNode } from "react"; import type { LucideIcon } from "lucide-react"; import type { Me } from "./api"; import type { Page } from "./urlState"; import type { PreloadableComponent } from "./lazyWithPreload"; // What a module says about itself: one definition per page, in its own folder (`modules//module.ts`, // handed out through its index.ts), collected in nav order by features/navigation/moduleRegistry. A new // page is its folder, its id in lib/urlState, and one line in that registry. export type ModuleDef = { id: Page; // i18n key for the SHORT display name — the label the nav rail and the palette show. labelKey: string; // The SHORT name under the icon when the user turns the rail labels on (B7): the rail is 3.75rem // wide with labels or without, so it has to fit there ("Alerts", "Listák"). The full `labelKey` // stays the tooltip and the accessible name. railKey: string; // i18n key for the LONGER, descriptive browser-tab title (e.g. "Channel manager" vs the rail's // "Channels"). Absent → the tab title falls back to `labelKey`. Read via `pageTitleKey`. titleKey?: string; icon: LucideIcon; // Admin/system modules render in their own section (below a divider) in the nav rail. system?: boolean; // Carries a live unread/active count in the rail (the number itself is computed in NavSidebar, // which owns the polling queries; this only flags which modules have one). hasBadge?: boolean; // Availability for THIS account, derived from language-independent account state (plex toggle, // admin role). Absent = always available. Drives the reachable set: the rail, the palette, the // `g` chords, and which page a link may land on. gate?: (me: Me) => boolean; // The page itself: lazy, its own chunk, fetched on the first visit or by the idle preload. Prop-free: // it reads `me` and its module's state from hooks. Page: PreloadableComponent; // Its side rail, if it has one: lazy like the page, and prop-free (its layout and collapse flag are // its own, lib/useRail). Rail?: PreloadableComponent; // The content column's max width. The header + toolbar centre on this same width (via --content-mw) // so the floating chrome lines up with the centred content instead of spanning the whole column. // MUST match the max-w the page's own content wrapper uses. Omit for a full-width page (feed). maxWidth?: string; // The module's store, if it has one: the state its page, its rail and its readers elsewhere (the // palette, the notifications, the header search) share. Mounted at the root for the whole session // (features/navigation/ModuleStores), so what it holds in memory survives a visit to another page. // Static, unlike the page: it is on the boot path by design. It reads no other module's store, so // the order the stores nest in does not matter (moduleStores.contract.test). Store?: ComponentType<{ children: ReactNode }>; // Every address-bar key this page's view writes: its filters, sort, tab, the item it shows. The URL // IS the view, so a bookmark reopens it. The shell clears them all on a page switch, and opens a page // with no other page's keys in its address (NavigationProvider), so one page's state never rides // along onto another's URL. Empty for a page with nothing of its own in the URL. Each key belongs to // one module and none is a shell key (moduleUrl.contract.test). urlKeys: readonly string[]; // Those of them that carry what the user TYPED (a search box's text, a column filter map holding a // text filter): the client log keeps their values out below its most detailed level (lib/logHooks). // moduleUrl.contract.test sorts every key of urlKeys as typed or not, so a new one is decided. typedUrlKeys?: readonly string[]; // Its page plays YouTube videos in the player and reopens it from an address that names one (a // reload, a bookmark — lib/playerUrlState), with its own list as the queue. A page without it gets // the shell's player for such an address: the video alone. playsVideos?: boolean; };