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.
101 lines
3.8 KiB
TypeScript
101 lines
3.8 KiB
TypeScript
import { useState } from "react";
|
|
import { Globe } from "lucide-react";
|
|
import { MenuCheck } from "./ui/MenuCheck";
|
|
import { LANGUAGES, type LangCode } from "../i18n";
|
|
import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
|
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
|
import { Pressable } from "./ui/Pressable";
|
|
import { RAIL_GLYPH, RAIL_ROW } from "./ui/NavRail";
|
|
import { SURFACE } from "./ui/surfaces";
|
|
|
|
// Compact language picker (globe + current code). Presentational: the parent decides what
|
|
// changing the language does (set it; persist server-side when signed in).
|
|
//
|
|
// Two variants, now one implementation on the popover platform: "header" opens below the pill
|
|
// (used on the landing page and the setup wizard) and "rail" is an icon-only button in the left
|
|
// nav's bottom cluster whose menu opens to the RIGHT, bottom-aligned. Both used to hand-roll their
|
|
// own anchoring, and the header one also opened on hover — dropped on purpose: the platform focuses
|
|
// the first item when a menu opens, so a hover-open would yank focus off whatever the user was doing.
|
|
export default function LanguageSwitcher({
|
|
value,
|
|
onChange,
|
|
align = "right",
|
|
variant = "header",
|
|
}: {
|
|
value: LangCode;
|
|
onChange: (code: LangCode) => void;
|
|
align?: "left" | "right";
|
|
variant?: "header" | "rail";
|
|
}) {
|
|
const [open, setOpen] = useState(false);
|
|
const currentIdx = Math.max(
|
|
0,
|
|
LANGUAGES.findIndex((l) => l.code === value),
|
|
);
|
|
const current = LANGUAGES.find((l) => l.code === value) ?? LANGUAGES[0];
|
|
const rail = variant === "rail";
|
|
|
|
const pop = useAnchoredPopover({
|
|
open,
|
|
onOpenChange: setOpen,
|
|
placement: rail ? "right-end" : align === "right" ? "bottom-end" : "bottom-start",
|
|
offset: rail ? 8 : 4,
|
|
// Single choice: opening lands on the language already in use.
|
|
selectedIndex: currentIdx,
|
|
});
|
|
|
|
const { setReference } = pop.refs;
|
|
|
|
return (
|
|
<>
|
|
<Pressable
|
|
ref={setReference}
|
|
{...pop.getReferenceProps()}
|
|
title={current.label}
|
|
aria-label={current.label}
|
|
layer
|
|
className={
|
|
rail
|
|
? `${RAIL_ROW} h-9 text-muted hover:text-fg`
|
|
: "flex h-7 items-center gap-1.5 px-2.5 rounded-lg text-sm text-muted hover:text-fg transition touch-target"
|
|
}
|
|
>
|
|
{rail ? (
|
|
/* A rail row (F32), with the current-language badge (HU/EN) on the glyph's corner so the
|
|
active language reads at a glance. The ring matches the page to detach it from the icon;
|
|
its type is the rail count badge's (text-3xs), the smallest step on the scale (it was an
|
|
8px one-off). */
|
|
<span className="relative">
|
|
<Globe className={RAIL_GLYPH} />
|
|
<span className="absolute -bottom-1.5 -right-2.5 text-3xs font-bold leading-none px-1 py-px rounded-sm bg-accent text-accent-fg ring-2 ring-bg">
|
|
{current.code.toUpperCase()}
|
|
</span>
|
|
</span>
|
|
) : (
|
|
<Globe className="w-4 h-4" />
|
|
)}
|
|
{rail ? null : <span className="text-xs font-semibold uppercase">{current.code}</span>}
|
|
</Pressable>
|
|
|
|
<PopoverSurface popover={pop} className={`${SURFACE.menu} w-40 animate-[popIn_0.16s_ease]`}>
|
|
{LANGUAGES.map((l) => (
|
|
<PopoverItem
|
|
key={l.code}
|
|
role="menuitemradio"
|
|
checked={l.code === value}
|
|
onSelect={() => {
|
|
onChange(l.code);
|
|
setOpen(false);
|
|
}}
|
|
label={l.label}
|
|
className="w-full flex items-center gap-2 text-sm px-2 py-2 rounded-lg text-muted outline-hidden transition hover:text-fg"
|
|
>
|
|
<MenuCheck kind="one" on={l.code === value} />
|
|
<span>{l.label}</span>
|
|
</PopoverItem>
|
|
))}
|
|
</PopoverSurface>
|
|
</>
|
|
);
|
|
}
|