The 43 react-refresh/only-export-components warnings the gate let through meant an edit to a provider (or AnchoredPopover, Toaster, plexDetailUi, …) could not be hot-swapped: Fast Refresh reloads a module that also exports hooks or helpers. - Each provider's contexts, the types its hooks return and the hooks themselves live in a sibling context module (navigationContext.ts, channelsContext.ts, …); the provider file keeps the component. loadAccountFilters moved next to the feed filters' defaults (feedFiltersInit.ts), following navigationInit/playlistsInit. - Helpers that sat in component files have their own modules: welcomeMode, downloadTitle, plexPlayable, channelSync, ui/anchoredPopoverHooks, ui/pageToolbarSlot, ui/pagerContent, ui/toastLevels, ui/overlayOrder, ui/tableAlign, lib/plexDetail; the cells of the channel columns and the registry's page wrappers are components files of their own (channelCells, modulePages). - usePersistedTab, an alias of useAccountPersistedState, is gone; its callers use the original. npm run lint is now eslint --max-warnings 0, so a warning fails the gate like an error does.
92 lines
3.4 KiB
TypeScript
92 lines
3.4 KiB
TypeScript
import { useState } from "react";
|
|
import { Check, Globe } from "lucide-react";
|
|
import { LANGUAGES, type LangCode } from "../i18n";
|
|
import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
|
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
|
|
|
// 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 (
|
|
<>
|
|
<button
|
|
ref={pop.refs.setReference}
|
|
{...pop.getReferenceProps()}
|
|
title={current.label}
|
|
aria-label={current.label}
|
|
className={
|
|
rail
|
|
? "relative p-2 rounded-lg text-muted hover:text-fg hover:bg-card transition"
|
|
: "flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-sm text-muted hover:text-fg hover:bg-card transition"
|
|
}
|
|
>
|
|
<Globe className={rail ? "w-5 h-5" : "w-4 h-4"} />
|
|
{rail ? (
|
|
/* Current-language badge (HU/EN) so the active language reads at a glance without
|
|
opening the menu. The ring matches the page background to detach it from the icon. */
|
|
<span className="absolute -bottom-1 -right-1 text-[0.5rem] font-bold leading-none px-1 py-px rounded-sm bg-accent text-accent-fg ring-2 ring-bg">
|
|
{current.code.toUpperCase()}
|
|
</span>
|
|
) : (
|
|
<span className="text-xs font-semibold uppercase">{current.code}</span>
|
|
)}
|
|
</button>
|
|
|
|
<PopoverSurface
|
|
popover={pop}
|
|
className="glass-menu w-40 rounded-xl p-1.5 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 justify-between gap-2 text-sm px-2 py-2 rounded-lg text-muted outline-hidden transition hover:text-fg"
|
|
>
|
|
<span>{l.label}</span>
|
|
{l.code === value && <Check className="w-4 h-4 text-accent" />}
|
|
</PopoverItem>
|
|
))}
|
|
</PopoverSurface>
|
|
</>
|
|
);
|
|
}
|