Files
siftlode/frontend/src/lib/moduleDef.ts
T
peter 6d57d62ac8 fix(nav): a linked player, file and search survive what the app does by itself (review round 2)
- Playlists: a link over a playlist the account does not hold plays alone; the rail's own
  fallback no longer counts as a pick that gives the player up.
- The linked opener opens or clears nothing once the address names another player, and stops
  looking (and fetching its row) once it settled.
- Files: a list the server's cap cut — the sorted or searched one too — keeps the link, with a
  notice of its own (EN/HU).
- History: an in-place view rewrite (search refine, count, channel to channel, sub-view)
  drops a reloaded entry's overlay marks (replaceView); a landing on a channel opened from a
  search keeps the search; the Feed press on a search opens the plain feed.
- Files/Playlists: an emptied search is written at once.
- Client log: plf/cf (column text filters) are typed keys; the shell has none of its own.
- Contract: writers outside lib/ take a literal or a lib/ function's result; every URL key is
  sorted typed or not; the writer regexes are held by a fixture; sub-views named; the bookmark
  net reads page tags.
- Feed defaults in one constant; one live-search queue helper; Scheduler writes through the
  Activity codec; notRun copy (EN/HU) names Forward and a plain "Search YouTube" button.
2026-10-27 16:08:52 +01:00

62 lines
4.0 KiB
TypeScript

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/<page>/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<object>;
// 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<object>;
// 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;
};