diff --git a/CLAUDE.md b/CLAUDE.md index 3ff78371..867ac765 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 one line there) → +`lib/urlState` and its entry there) → **`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`, @@ -115,6 +115,9 @@ CQRS, a repository/DI framework, a router library, a generic CRUD-router factory | FE keyboard shortcut | a row in `lib/keymap.ts` + `dispatch()` | a private `keydown` listener with its own key literals | | FE list/grid keys | `NavGrid`/`keyboardNav` + `navItemAttrs` + `data-nav-open` | a private arrow-key / roving-tabindex handler | | FE view push (history) | `rememberNavPosition()` right before `history.pushState` | a push that forgets: Back strands focus on `` | +| FE a page's header content | the page renders it into the floating header itself (`PageHeader`, `ui/PageShell`) | a `page === …` branch in the shell's header, or a bridge carrying a page's state up to it | +| FE module store | its definition's `Store` (`module.ts`), mounted at the root by `ModuleStores`; it reads no other module's store (contract-held) | a provider hand-nested in `main.tsx` | +| FE saved views | the module's own store from `features/filters` (`createSavedViewsStore` + `useSavedViewsStore`) | one store switched by the active page — every visit elsewhere reset it | | FE static label / button-like link | `Badge` (flat, no edge) · `LinkButton` / `IconLink` | a glass pill that reads as a chip; an `` hand-painted like a button | | FE loading/empty/error | `QueryState` / `StateMessage` | ad-hoc conditionals | | FE failed write | `meta.failTitle`, else `notifyActionError(err, title)`; a YouTube write action (subscribe, sync, …): `notifyYouTubeActionError` — a 403 offers Connect | a bare `catch {}` or `notify()` with no error attached | diff --git a/docs/development.md b/docs/development.md index 285ae0b1..8af4586e 100644 --- a/docs/development.md +++ b/docs/development.md @@ -36,6 +36,27 @@ The dev server also serves the **design lab** at `http://localhost:5173/design-l decisions and the chosen patterns, rendered with the app's real CSS. It never reaches a production build (see [`DESIGN.md`](DESIGN.md)). +## Adding a page + +A frontend page is one folder, `frontend/src/modules//` (the layering is in +[`CLAUDE.md`](../CLAUDE.md) § Architecture shape): + +1. The page itself, default-exported, and anything only it uses. Header content (a search box, a toggle) the + page renders into the floating header itself through `PageHeader`; the shell knows no page. +2. `module.ts`: its `ModuleDef` (`lib/moduleDef.ts`): id, label keys, icon, an optional `gate`, the page as a + `lazyWithPreload(() => import(…))`, and optionally a rail and a `Store`. Of its own folder it imports statically + only its store (a `…Provider` / `…Store` file); the page and the rail load lazily. +3. `index.ts`, the module's public face: the definition, and only what other folders must read (its store's + hooks). Another folder reaches the module through this file or not at all. +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`). + +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 +down, a module only through its face) and `chunkGuard` in the build (no lazy page on the boot path), plus the +i18n parity test. + ## The gate A change is done when this is green, from the repo root: diff --git a/frontend/src/components/ui/README.md b/frontend/src/components/ui/README.md index ed7cd8ba..2253e6a0 100644 --- a/frontend/src/components/ui/README.md +++ b/frontend/src/components/ui/README.md @@ -35,13 +35,13 @@ Counter-examples, deliberately NOT here: `PosterTile` (knows about Plex montage ## What lives here -| area | components | -| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| overlays & anchoring | `AnchoredPopover` + `anchoredPopoverHooks` (the popover engine: arrows, typeahead, Escape, anchor-flip), `ActionMenu`, `MembershipMenu` (the body of a "which sets is it in" menu: checkbox rows + a field naming a new set), `ContextMenu` + `useContextMenu` (the SAME menu, opened by right-click / long-press / Shift+F10 / a row's `MoreButton` "…" via `buttonProps`; `anchor` for a panel a row opens; `PageMenu` for a page's background menu), `Modal` (+ `ModalFooter`; `initialFocus` by purpose, C4), `Overlay`, `SidePanel`, `Tooltip`, `Toaster` (+ `toastLevels`, `overlayOrder`; `lib/toastDock` docks one control under the stack, in every host), `CommandPalette`, `KeyboardHelp` | -| actions & inputs | `Button`, `IconButton`, `Pressable` (`selectable` when its text is a title or a name), `Chip` + `chipStyles`, `DragHandle` + `OrderButtons` / `useOrderItems` (the non-drag reorder, WCAG 2.5.7) (one per button family, docs/DESIGN.md; a raw `