docs(frontend): the canonical table and the ui README name the module shape; adding a page is written down

CLAUDE.md gains the header-content, module-store and saved-views rows. docs/development.md says what a
new page is and which tests hold it. components/ui/README.md names every file in the directory, and a
contract test keeps it that way: seventeen had slipped in unlisted. copyItems' comment still claimed it
was a feature file.
This commit is contained in:
2026-10-23 12:52:04 +02:00
parent 6ae7825a14
commit ad05932217
14 changed files with 63 additions and 18 deletions
+7 -7
View File
@@ -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 `<button>` or a bare `FocusSafeButton` outside the control primitives — eslint `BUTTON_PRIMITIVES` — is a lint error), `Input` + `fieldStyles`, `RenameField` (a name typed in place of the name: Enter or blur saves, Escape cancels; the caller hands the keyboard back), `Meter` (how full something is against a limit — one danger line for every place showing the same measure), `TimecodeField` + `useTimecodeField` / `TimecodeError` (a cut's time typed in: the draft, held by the caller, until Enter or blur takes it, then the value as it landed; unreadable stays once offered, marked invalid, and the caller places the line saying how to type one in its flow, outside a label; a draft goes when the value moves under it), `NativeControls`, `MenuCheck` (the drawn check of a menu row or a whole-row checkbox button; its tick is the `chosen` colour, which `media-marks` turns amber on the player chrome), `mediaChrome` (`MEDIA_CHOSEN`, a chosen tab or toggle on the chrome), `SelectTrigger` (the field family; a raw `<input>` is a lint error), `useReturnFocus`, `surfaces` (the B5 glass roles; a bare glass class is a lint error), `Badge` (a static label), `LinkButton` / `IconLink` (a link wearing the button face) over `buttonStyles`, `Skeleton` (B8 first loads, through `LoadingState shape`), `NavRail` (B7; `NavRailGroup` folds a set of rare pages into one row with a panel), `form`, `DraftSaveBar`, `Combobox`, `FilterSelect`, `SegmentedToggle`, `SortControl`, `ColumnSortControl`, `PriorityStepper`, `UndoToolbar` |
| data display | `DataTable` (+ `progressiveRows`: a long page mounts in steps), `tableHeader` + `tableAlign`, `HiddenFilterCue` (names a filter whose column is off screen), `Pager` + `pagerContent`, `VirtualGrid` + `gridColumns` (the column/width maths, incl. the ultrawide `maxCols` cap), `readAhead`, `Carousel`, `MediaCard` + `mediaCardStyles` (the one media card, docs/DESIGN.md "Media card") + `MediaPreview` / `useMediaPreview` (its mouse-rest preview), `NavGrid` (a plain grid, list or table body the keyboard walks — `DataTable`, `VirtualGrid` and `Carousel` take `keyboardNav`), `RemoteImage` (every network picture: no Referer, retries past the negative cache, a fallback instead of the broken-image mark — a bare `<img>` is linted out), `Avatar`, `CountBadge`, `ItemCount`, `FilterChipGroup`, `RelativeTime` ("3d ago" with the exact minute on hover, docs/DESIGN.md "Copy"), `ClampedText` (a long text clamped to a few lines; More / Less only when it is cut), `StatTiles` + `StatTile` / `StatBars` (number tiles, a breakdown as horizontal bars), `RangeTrack` + `RulerTicks` (a time track over a window of a media item: regions, slider ends the arrows step, a playhead, a background slot — the editor's cuts, the players' clip panel), `CropFrame` (a crop frame over a picture, on the picture and not its bars: drag, edges, corners, the arrows; the editor's crop, the players' clip crop) |
| page & panel structure | `PageShell` + `pageToolbarSlot` + `pageHeaderSlot` (`PageHeader`: a page fills the floating header) + `pageHeading`, `PageScroller`, `Headings` (`PageHeading`, `SectionLabel`), `SkipLink` + `landmarks`, `FilterFold` (B4), `scrollFold` (`useScrollFold`: a fixed header that folds once its content scrolls), `PanelGroup`, `PanelGroups`, `Tabs`, `BackToTop` |
| state & messaging | `QueryState`, `Banner`, `InlineNotice`, `CrashBoundary` |
| 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`, `ConfirmProvider` + `confirmContext` (`useConfirm` / `useChoice`: the app's promise-based confirm, mounted once at the root; a decision with more than two outcomes is one `useChoice`, never two chained confirms), `dialogFocus` (the focus half of the dialog contract: `Modal`'s, and the wizard's own shell), `menuGroups` (one menu from its groups, a rule only between non-empty ones), `pageMenuSlot` + `pageMenuItems` (a page's background menu, mirroring its own toolbar) |
| 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 `<button>` or a bare `FocusSafeButton` outside the control primitives — eslint `BUTTON_PRIMITIVES` — is a lint error), `Input` + `fieldStyles`, `RenameField` (a name typed in place of the name: Enter or blur saves, Escape cancels; the caller hands the keyboard back), `Meter` (how full something is against a limit — one danger line for every place showing the same measure), `TimecodeField` + `useTimecodeField` / `TimecodeError` (a cut's time typed in: the draft, held by the caller, until Enter or blur takes it, then the value as it landed; unreadable stays once offered, marked invalid, and the caller places the line saying how to type one in its flow, outside a label; a draft goes when the value moves under it), `NativeControls`, `MenuCheck` (the drawn check of a menu row or a whole-row checkbox button; its tick is the `chosen` colour, which `media-marks` turns amber on the player chrome), `mediaChrome` (`MEDIA_CHOSEN`, a chosen tab or toggle on the chrome), `SelectTrigger` (the field family; a raw `<input>` is a lint error), `useReturnFocus`, `surfaces` (the B5 glass roles; a bare glass class is a lint error), `Badge` (a static label), `LinkButton` / `IconLink` (a link wearing the button face) over `buttonStyles`, `Skeleton` (B8 first loads, through `LoadingState shape`), `NavRail` (B7; `NavRailGroup` folds a set of rare pages into one row with a panel), `form`, `DraftSaveBar`, `Combobox`, `FilterSelect`, `SegmentedToggle`, `SortControl`, `ColumnSortControl`, `PriorityStepper`, `UndoToolbar`, `ViewSwitcher` (a list's display modes behind one toolbar icon), `copyItems` (`useCopyItems`: the "Copy title" / "Copy link" rows every object's menu ends with) + `useCopyFeedback` (a Copy button's "Copied" moment), `MatchToggle` (a multi-value filter's any/all switch), `LanguageSwitcher` (presentational: the caller decides what a change does), `sortableA11y` (what a screen reader hears during a keyboard reorder) |
| data display | `DataTable` (+ `progressiveRows`: a long page mounts in steps), `tableHeader` + `tableAlign`, `HiddenFilterCue` (names a filter whose column is off screen), `Pager` + `pagerContent`, `VirtualGrid` + `gridColumns` (the column/width maths, incl. the ultrawide `maxCols` cap), `readAhead`, `Carousel`, `MediaCard` + `mediaCardStyles` (the one media card, docs/DESIGN.md "Media card") + `MediaPreview` / `useMediaPreview` (its mouse-rest preview), `NavGrid` (a plain grid, list or table body the keyboard walks — `DataTable`, `VirtualGrid` and `Carousel` take `keyboardNav`), `RemoteImage` (every network picture: no Referer, retries past the negative cache, a fallback instead of the broken-image mark — a bare `<img>` is linted out), `Avatar`, `CountBadge`, `ItemCount`, `FilterChipGroup`, `RelativeTime` ("3d ago" with the exact minute on hover, docs/DESIGN.md "Copy"), `ClampedText` (a long text clamped to a few lines; More / Less only when it is cut), `StatTiles` + `StatTile` / `StatBars` (number tiles, a breakdown as horizontal bars), `RangeTrack` + `trackPosition` + `RulerTicks` (a time track over a window of a media item: regions, slider ends the arrows step, a playhead, a background slot — the editor's cuts, the players' clip panel), `CropFrame` (a crop frame over a picture, on the picture and not its bars: drag, edges, corners, the arrows; the editor's crop, the players' clip crop), `reveal` (a card's hover chrome: hover or keyboard focus on a mouse, never on touch) |
| page & panel structure | `PageShell` + `pageToolbarSlot` + `pageHeaderSlot` (`PageHeader`: a page fills the floating header) + `pageHeading`, `PageScroller`, `Headings` (`PageHeading`, `SectionLabel`), `SkipLink` + `landmarks`, `FilterFold` (B4), `scrollFold` (`useScrollFold`: a fixed header that folds once its content scrolls), `PanelGroup`, `PanelGroups`, `Tabs` + `tabPanel`, `BackToTop` + `backToTopState` (PageScroller keeps the page's end clear of it), `FilterRail` (the filter-rail shell a rail fills with its groups: title bar, customize, reset), `stickyTop` (a sticky header at the scroller's top turns its top fade off) |
| state & messaging | `QueryState`, `Banner`, `InlineNotice`, `CrashBoundary` |
## The size contract (E10 S1c)
+2 -1
View File
@@ -10,7 +10,8 @@ import { type ActionMenuItem } from "./ActionMenu";
// finger cannot select is copied from here. One builder, so every object says it the same way and
// the clipboard write always runs inside the gesture (`inGesture`, which Safari insists on).
//
// A feature file, not `ui/`: it speaks the app's notifications.
// Domain-free: it speaks through lib/notifications, the platform's toast service, so any module's menu
// can end with it.
type Named = "title" | "name";
@@ -0,0 +1,20 @@
import { readdirSync, readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
// components/ui/README.md is how the platform layer is found: "reuse before build" only works if a
// primitive is listed where the next author looks. A dozen files had slipped in unlisted, so every
// source file in the directory must be named there, in backticks, by its file name.
const UI = resolve(dirname(fileURLToPath(import.meta.url)), "../components/ui");
describe("the platform layer's README", () => {
it("names every file in components/ui", () => {
const readme = readFileSync(resolve(UI, "README.md"), "utf8");
const unlisted = readdirSync(UI)
.filter((f) => /\.tsx?$/.test(f) && !/\.test\.tsx?$/.test(f) && !f.endsWith(".testkit.ts"))
.map((f) => f.replace(/\.tsx?$/, ""))
.filter((name) => !readme.includes("`" + name + "`"));
expect(unlisted).toEqual([]);
});
});
+1 -1
View File
@@ -2,7 +2,7 @@ import { ScrollText } from "lucide-react";
import { lazyWithPreload } from "../../lib/lazyWithPreload";
import type { ModuleDef } from "../../lib/moduleDef";
// This page as the shell and the navigation know it (lib/moduleDef). Its page load lazily.
// This page as the shell and the navigation know it (lib/moduleDef). Its page loads lazily.
export const activityModule: ModuleDef = {
id: "activity",
labelKey: "header.account.activity",
+1 -1
View File
@@ -3,7 +3,7 @@ import { lazyWithPreload } from "../../lib/lazyWithPreload";
import type { ModuleDef } from "../../lib/moduleDef";
import { ChannelsProvider } from "./ChannelsProvider";
// This page as the shell and the navigation know it (lib/moduleDef). Its page load lazily.
// This page as the shell and the navigation know it (lib/moduleDef). Its page loads lazily.
export const channelsModule: ModuleDef = {
id: "channels",
labelKey: "header.account.channels",
+1 -1
View File
@@ -2,7 +2,7 @@ import { SlidersHorizontal } from "lucide-react";
import { lazyWithPreload } from "../../lib/lazyWithPreload";
import type { ModuleDef } from "../../lib/moduleDef";
// This page as the shell and the navigation know it (lib/moduleDef). Its page load lazily.
// This page as the shell and the navigation know it (lib/moduleDef). Its page loads lazily.
export const configModule: ModuleDef = {
id: "config",
labelKey: "header.account.config",
+1 -1
View File
@@ -2,7 +2,7 @@ import { Home } from "lucide-react";
import { lazyWithPreload } from "../../lib/lazyWithPreload";
import type { ModuleDef } from "../../lib/moduleDef";
// This page as the shell and the navigation know it (lib/moduleDef). Its page load lazily.
// This page as the shell and the navigation know it (lib/moduleDef). Its page loads lazily.
export const homeModule: ModuleDef = {
id: "home",
labelKey: "home.navLabel",
+1 -1
View File
@@ -2,7 +2,7 @@ import { Bell } from "lucide-react";
import { lazyWithPreload } from "../../lib/lazyWithPreload";
import type { ModuleDef } from "../../lib/moduleDef";
// This page as the shell and the navigation know it (lib/moduleDef). Its page load lazily.
// This page as the shell and the navigation know it (lib/moduleDef). Its page loads lazily.
export const notificationsModule: ModuleDef = {
id: "notifications",
labelKey: "inbox.navLabel",
+1 -1
View File
@@ -2,7 +2,7 @@ import { Activity } from "lucide-react";
import { lazyWithPreload } from "../../lib/lazyWithPreload";
import type { ModuleDef } from "../../lib/moduleDef";
// This page as the shell and the navigation know it (lib/moduleDef). Its page load lazily.
// This page as the shell and the navigation know it (lib/moduleDef). Its page loads lazily.
export const schedulerModule: ModuleDef = {
id: "scheduler",
labelKey: "header.account.scheduler",
+1 -1
View File
@@ -2,7 +2,7 @@ import { Settings } from "lucide-react";
import { lazyWithPreload } from "../../lib/lazyWithPreload";
import type { ModuleDef } from "../../lib/moduleDef";
// This page as the shell and the navigation know it (lib/moduleDef). Its page load lazily.
// This page as the shell and the navigation know it (lib/moduleDef). Its page loads lazily.
export const settingsModule: ModuleDef = {
id: "settings",
labelKey: "header.account.settings",
+1 -1
View File
@@ -2,7 +2,7 @@ import { BarChart3 } from "lucide-react";
import { lazyWithPreload } from "../../lib/lazyWithPreload";
import type { ModuleDef } from "../../lib/moduleDef";
// This page as the shell and the navigation know it (lib/moduleDef). Its page load lazily.
// This page as the shell and the navigation know it (lib/moduleDef). Its page loads lazily.
export const statsModule: ModuleDef = {
id: "stats",
labelKey: "header.account.stats",
+1 -1
View File
@@ -2,7 +2,7 @@ import { Users } from "lucide-react";
import { lazyWithPreload } from "../../lib/lazyWithPreload";
import type { ModuleDef } from "../../lib/moduleDef";
// This page as the shell and the navigation know it (lib/moduleDef). Its page load lazily.
// This page as the shell and the navigation know it (lib/moduleDef). Its page loads lazily.
export const usersModule: ModuleDef = {
id: "users",
labelKey: "header.account.users",