The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
289 lines
9.6 KiB
TypeScript
289 lines
9.6 KiB
TypeScript
import clsx from "clsx";
|
||
import { useState, type CSSProperties, type ReactNode, type Ref } from "react";
|
||
import type { LucideIcon } from "lucide-react";
|
||
import { PopoverSurface } from "./AnchoredPopover";
|
||
import { useAnchoredPopover } from "./anchoredPopoverHooks";
|
||
import { Pressable } from "./Pressable";
|
||
import { SURFACE } from "./surfaces";
|
||
import Tooltip from "./Tooltip";
|
||
import { isPlainLeftClick } from "../../lib/nav";
|
||
|
||
// B7 (docs/DESIGN.md): the app's primary navigation rail. Slim — 3.75rem wide whether or not the
|
||
// labels are on (they add height, never width) — with "you are here" as a soft accent tint plus a 3px
|
||
// edge bar, calmer than a solid fill beside the page's own solid accents. Labels under the icons are
|
||
// a per-user setting (off by default); each module has a SHORT rail label that fits, and the full name
|
||
// is the tooltip.
|
||
|
||
/** The rail's column: the glass panel, the rail's width and its padding. */
|
||
export function NavRail({
|
||
label,
|
||
className,
|
||
children,
|
||
}: {
|
||
/** The landmark's accessible name ("Primary navigation"). */
|
||
label: string;
|
||
/** Placement only (position, height); the look and width are the primitive's. */
|
||
className?: string;
|
||
children: ReactNode;
|
||
}) {
|
||
return (
|
||
<nav
|
||
aria-label={label}
|
||
className={clsx(SURFACE.panel, "flex w-15 flex-col px-1 py-2.5", className)}
|
||
>
|
||
{children}
|
||
</nav>
|
||
);
|
||
}
|
||
|
||
/** The rail's scrolling list of pages. An overflow container clips on BOTH axes, so the list spans the
|
||
* rail's padding (`-mx-1 px-1`): the active row's edge bar, which sits in that padding, stays inside
|
||
* the list's box and is not cut off. */
|
||
export function NavRailList({
|
||
ref,
|
||
style,
|
||
children,
|
||
}: {
|
||
ref?: Ref<HTMLDivElement>;
|
||
/** The scroll fade's mask. */
|
||
style?: CSSProperties;
|
||
children: ReactNode;
|
||
}) {
|
||
return (
|
||
<div
|
||
ref={ref}
|
||
style={style}
|
||
className="-mx-1 flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto px-1 no-scrollbar"
|
||
>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** A short, thick centred rule: the "new section" cue on an icon rail (the system modules). */
|
||
export function NavRailDivider() {
|
||
return <div className="mx-auto my-1.5 w-7 shrink-0 border-t-2 border-border" />;
|
||
}
|
||
|
||
/** The glyph of every rail row: a page, an action, the language switcher. */
|
||
export const RAIL_GLYPH = "size-4.5";
|
||
|
||
/** The shape of one rail row, for a row that is not a page (the account button). */
|
||
export const RAIL_ROW =
|
||
"relative flex w-full flex-col items-center justify-center gap-0.5 rounded-lg transition touch-target";
|
||
|
||
export function NavRailItem({
|
||
icon: Icon,
|
||
label,
|
||
shortLabel,
|
||
showLabel,
|
||
active = false,
|
||
badge,
|
||
href,
|
||
onClick,
|
||
testId,
|
||
}: {
|
||
icon: LucideIcon;
|
||
/** The full name: the tooltip, and the accessible name while the label is hidden. */
|
||
label: string;
|
||
/** What fits under the icon when labels are on. */
|
||
shortLabel: string;
|
||
showLabel: boolean;
|
||
active?: boolean;
|
||
/** An unread / in-progress count on the icon's corner (0 hides it). */
|
||
badge?: number;
|
||
/** The page's own address. A page row is a real link (the owner's call): a middle-click, a
|
||
* Ctrl/Cmd-click or the browser's "Open in new tab" opens the page in a tab of its own. */
|
||
href: string;
|
||
/** The in-app navigation, run only for a plain left click. */
|
||
onClick: () => void;
|
||
testId?: string;
|
||
}) {
|
||
const shown = !!badge && badge > 0;
|
||
return (
|
||
<Tooltip hint={label} side="right" always openDelay={350}>
|
||
<a
|
||
data-testid={testId}
|
||
href={href}
|
||
onClick={(e) => {
|
||
if (!isPlainLeftClick(e)) return; // the browser opens its tab or window
|
||
e.preventDefault();
|
||
onClick();
|
||
}}
|
||
// With the label visible the row's name starts with that text (WCAG 2.5.3: the spoken name
|
||
// contains the visible one); the full name stays in the tooltip. Icon-only, the full name is the
|
||
// name. The exact count rides in the name either way, so "Alerts (3)" is heard and not only
|
||
// seen; the drawn badge is hidden (labelled, it was read first and capped: "9+Alerts").
|
||
aria-label={`${showLabel ? shortLabel : label}${shown ? ` (${badge})` : ""}`}
|
||
aria-current={active ? "page" : undefined}
|
||
className={clsx(
|
||
RAIL_ROW,
|
||
showLabel ? "py-1.5" : "h-9",
|
||
active ? "bg-accent/15 text-accent" : "text-muted state-layer hover:text-fg",
|
||
)}
|
||
>
|
||
{active && (
|
||
<span
|
||
aria-hidden="true"
|
||
className="absolute top-1.5 bottom-1.5 -left-1 w-0.75 rounded-r-full bg-accent"
|
||
/>
|
||
)}
|
||
<span className="relative shrink-0">
|
||
<Icon className={RAIL_GLYPH} aria-hidden="true" />
|
||
{shown && (
|
||
<span
|
||
aria-hidden="true"
|
||
className="absolute -top-2 -right-2 grid h-4 min-w-4 place-items-center rounded-full bg-accent px-1 text-3xs leading-none font-bold text-accent-fg ring-2 ring-bg"
|
||
>
|
||
{badge > 9 ? "9+" : badge}
|
||
</span>
|
||
)}
|
||
</span>
|
||
{showLabel && (
|
||
<span className="max-w-full truncate text-3xs font-medium tracking-tight">
|
||
{shortLabel}
|
||
</span>
|
||
)}
|
||
</a>
|
||
</Tooltip>
|
||
);
|
||
}
|
||
|
||
/** A rail row that acts instead of navigating (About, Sign out): the page rows' geometry and glyph, so
|
||
* the column keeps one rhythm (F32: toolbar icon buttons here drew 32 squares with 16px glyphs). */
|
||
export function NavRailAction({
|
||
icon: Icon,
|
||
label,
|
||
onClick,
|
||
}: {
|
||
icon: LucideIcon;
|
||
/** The tooltip and the accessible name. */
|
||
label: string;
|
||
onClick: () => void;
|
||
}) {
|
||
return (
|
||
<Tooltip hint={label} side="right" always openDelay={350}>
|
||
<Pressable
|
||
onClick={onClick}
|
||
aria-label={label}
|
||
className={clsx(RAIL_ROW, "h-9 text-muted state-layer hover:text-fg")}
|
||
>
|
||
<Icon className={RAIL_GLYPH} aria-hidden="true" />
|
||
</Pressable>
|
||
</Tooltip>
|
||
);
|
||
}
|
||
|
||
/** One page of a `NavRailGroup`'s panel. */
|
||
export interface NavRailGroupPage {
|
||
id: string;
|
||
icon: LucideIcon;
|
||
/** The page's full name, written out in the panel. */
|
||
label: string;
|
||
href: string;
|
||
active: boolean;
|
||
/** The in-app navigation, run only for a plain left click. */
|
||
onSelect: () => void;
|
||
}
|
||
|
||
/** One rail row standing for a set of rare pages (the owner's pick: the admin pages), opening a
|
||
* panel of them to its right. The rail fits a 133–150% zoom without a scroll nobody sees: five admin
|
||
* rows sat below the fold behind a fade alone. The row is "you are here" while any of its pages is.
|
||
* The panel's pages are real links, as the rail's own rows are (a middle-click opens a tab), so it is a
|
||
* `dialog` popover with its own Tab order, not a roving menu of buttons. */
|
||
export function NavRailGroup({
|
||
icon: Icon,
|
||
label,
|
||
shortLabel,
|
||
showLabel,
|
||
pages,
|
||
testId,
|
||
}: {
|
||
icon: LucideIcon;
|
||
/** The group's full name: the tooltip, the row's and the panel's accessible name, the panel heading. */
|
||
label: string;
|
||
shortLabel: string;
|
||
showLabel: boolean;
|
||
pages: readonly NavRailGroupPage[];
|
||
testId?: string;
|
||
}) {
|
||
const [open, setOpen] = useState(false);
|
||
const pop = useAnchoredPopover({
|
||
open,
|
||
onOpenChange: setOpen,
|
||
mode: "dialog",
|
||
role: "dialog",
|
||
placement: "right-start",
|
||
offset: 8,
|
||
});
|
||
const { setReference } = pop.refs;
|
||
const active = pages.some((p) => p.active);
|
||
return (
|
||
<>
|
||
<Tooltip hint={label} side="right" always openDelay={350}>
|
||
<Pressable
|
||
ref={setReference}
|
||
{...pop.getReferenceProps()}
|
||
data-testid={testId}
|
||
aria-label={showLabel ? shortLabel : label}
|
||
aria-current={active ? "page" : undefined}
|
||
className={clsx(
|
||
RAIL_ROW,
|
||
showLabel ? "py-1.5" : "h-9",
|
||
active ? "bg-accent/15 text-accent" : "text-muted state-layer hover:text-fg",
|
||
)}
|
||
>
|
||
{active && (
|
||
<span
|
||
aria-hidden="true"
|
||
className="absolute top-1.5 bottom-1.5 -left-1 w-0.75 rounded-r-full bg-accent"
|
||
/>
|
||
)}
|
||
<Icon className={RAIL_GLYPH} aria-hidden="true" />
|
||
{showLabel && (
|
||
<span className="max-w-full truncate text-3xs font-medium tracking-tight">
|
||
{shortLabel}
|
||
</span>
|
||
)}
|
||
</Pressable>
|
||
</Tooltip>
|
||
<PopoverSurface
|
||
popover={pop}
|
||
ariaLabel={label}
|
||
className={`${SURFACE.popover} w-56 p-1.5 animate-[popIn_0.16s_ease]`}
|
||
>
|
||
<div className="px-2 pt-1 pb-1.5 text-2xs font-semibold tracking-wide text-muted uppercase">
|
||
{label}
|
||
</div>
|
||
<ul className="flex flex-col gap-0.5">
|
||
{pages.map(({ id, icon: PageIcon, label: name, href, active: here, onSelect }) => (
|
||
<li key={id}>
|
||
<a
|
||
data-testid={`${testId ?? "group"}-${id}`}
|
||
href={href}
|
||
aria-current={here ? "page" : undefined}
|
||
onClick={(e) => {
|
||
if (!isPlainLeftClick(e)) return; // the browser opens its tab or window
|
||
e.preventDefault();
|
||
setOpen(false);
|
||
onSelect();
|
||
}}
|
||
className={clsx(
|
||
"flex items-center gap-2.5 rounded-lg px-2 py-1.5 text-sm transition touch-target",
|
||
here ? "bg-accent/15 text-accent" : "text-fg state-layer",
|
||
)}
|
||
>
|
||
<PageIcon
|
||
className={clsx(RAIL_GLYPH, "shrink-0", !here && "text-muted")}
|
||
aria-hidden="true"
|
||
/>
|
||
<span className="min-w-0 truncate">{name}</span>
|
||
</a>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</PopoverSurface>
|
||
</>
|
||
);
|
||
}
|