The probe re-run over the 15 pages at 1920x1080 found every button, icon button, chip, field, tab, segment and menu row drawn by its primitive except the S3h Plex picture, a div with the button role. It is a Pressable now, and the button-primitive lint rule refuses an element with role="button" outside the control primitives. - SegmentedToggle defaults to the toolbar rung (28, text-xs): the old small default had put the pager's page sizes and the YouTube result count on 10px type beside 28px buttons. The Plex match toggle asks for the small rung explicitly. - The language badge on the rail uses text-3xs (was an 8px one-off); the Plex playlist's watched mark and the Home hero's length use MEDIA_BADGE instead of hand-copied classes. - A media picture shows the pointer whether it is a link or a button. - DESIGN.md: a Conformance chapter (the four-step check before a UI change is done, with the S3g baseline), the segmented rung, two log rows. CLAUDE.md points at the check.
99 lines
3.8 KiB
TypeScript
99 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 R8 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,
|
|
});
|
|
|
|
return (
|
|
<>
|
|
<Pressable
|
|
ref={pop.refs.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>
|
|
</>
|
|
);
|
|
}
|