refactor(header): remove the ModuleName module-switcher pill

The user no longer wants the header's "FEED" pill (module label + accent dot + ◀/▶ module-step
arrows). Drop it everywhere — the header now starts with the module's SearchBar; module switching
stays on the nav rail.

Deletes ModuleName.tsx, the now-unused stepModule() in lib/modules, and the Header's
module-order/label/setPage plumbing that only fed it.
This commit is contained in:
2026-08-21 19:58:19 +02:00
parent c7b393901f
commit c3820a0b44
3 changed files with 3 additions and 123 deletions
+3 -21
View File
@@ -2,20 +2,17 @@ import { forwardRef, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { Youtube } from "lucide-react";
import type { Me } from "../lib/api";
import { moduleLabelKey, moduleOrder, stepModule } from "../lib/modules";
import ModuleName from "./ModuleName";
import SearchBar from "./SearchBar";
import PlexSearchBox from "./PlexSearchBox";
import PlexScopeToggle from "./PlexScopeToggle";
import SyncStatus from "./SyncStatus";
import { useNavigation, useNavigationActions } from "./NavigationProvider";
import { useNavigation } from "./NavigationProvider";
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
import { usePlaylists, usePlaylistsActions } from "./PlaylistsProvider";
import { useChannels, useChannelsActions } from "./ChannelsProvider";
// The floating top header: a row of glass pills over the content — a ModuleName pill (label +
// accent dot + back/forward arrows), the module's SearchBar where it has one, and the per-user
// sync chip at the right end.
// The floating top header: a row of glass pills over the content — the module's SearchBar where it
// has one, and the per-user sync chip at the right end.
// `forwardRef` so PageShell can attach a ref to the header's root box and measure its real (possibly
// wrapped) height — that measurement + the `--hdr-h` content pad it drives live in PageShell, which
// owns both the header slot and the padded content wrapper (E5 S7).
@@ -30,7 +27,6 @@ const Header = forwardRef<
>(function Header({ me, onYtSearch }, ref) {
const { t } = useTranslation();
const { page } = useNavigation();
const { setPage } = useNavigationActions();
const filters = useFeedFilters();
const { setFilters } = useFeedFiltersActions();
// Each module's search term lives in its own provider (shared with that module's rail/content):
@@ -40,12 +36,6 @@ const Header = forwardRef<
const { setSearch: setPlaylistsSearch } = usePlaylistsActions();
const { search: channelsSearch } = useChannels();
const { setSearch: setChannelsSearch, goToFullHistory: onGoToFullHistory } = useChannelsActions();
// The ◀/▶ arrows step cyclically through the user's available modules (derived from `me`, so
// it stays correct as modules are added/gated). Labels of every active module feed ModuleName's
// dynamic width so the pill is sized to the widest reachable title in the current language.
const order = moduleOrder(me);
const moduleLabels = order.map((p) => t(moduleLabelKey[p]));
const multiModule = order.length > 1;
// Drop input focus on Go/Enter for the live-filter modules (feed's Go escalates to YouTube).
const blur = () => (document.activeElement as HTMLElement | null)?.blur?.();
@@ -101,14 +91,6 @@ const Header = forwardRef<
ref={ref}
className="pointer-events-none absolute top-3 left-3 right-[10%] z-chrome flex flex-wrap items-center gap-x-3 gap-y-2"
>
<ModuleName
label={t(moduleLabelKey[page])}
labels={moduleLabels}
onPrev={() => setPage(stepModule(me, page, -1))}
onNext={() => setPage(stepModule(me, page, 1))}
canPrev={multiModule}
canNext={multiModule}
/>
{search}
{/* Library scope (movie/show/both) sits right of the search box on Plex — relocated here from
the filter sidebar (S3) so the primary "what am I browsing" control is always in reach. */}
-90
View File
@@ -1,90 +0,0 @@
import { useLayoutEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { ChevronLeft, ChevronRight } from "lucide-react";
// The floating module-name pill in the top header. The label area is sized to the WIDEST
// reachable module title (measured live, in the current language only — so dropping a locale or
// gaining/losing a module just changes the input, nothing is hard-coded), which keeps the label
// column a constant width: the name never shifts and the arrows stay put when paging between
// modules. Carries the back/forward arrows that step cyclically through the active modules, plus
// the accent dot (the former PageTitle "eyebrow" look, folded in here).
export default function ModuleName({
label,
labels,
onPrev,
onNext,
canPrev,
canNext,
}: {
label: string;
// Every reachable module's title in the current language — drives the fixed label width.
labels: string[];
onPrev: () => void;
onNext: () => void;
canPrev: boolean;
canNext: boolean;
}) {
const { t } = useTranslation();
const labelRef = useRef<HTMLSpanElement>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [labelW, setLabelW] = useState<number>();
// Measure the widest label using the label span's own rendered font (respects font-scale,
// tracking and the uppercase transform). Re-runs whenever the label set changes — i.e. on a
// language switch or an account switch that changes which modules are reachable.
useLayoutEffect(() => {
const el = labelRef.current;
if (!el || labels.length === 0) return;
const cs = getComputedStyle(el);
const canvas = canvasRef.current ?? (canvasRef.current = document.createElement("canvas"));
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.font = `${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
const ls = parseFloat(cs.letterSpacing) || 0;
const upper = cs.textTransform === "uppercase";
let max = 0;
for (const s of labels) {
const txt = upper ? s.toUpperCase() : s;
const w = ctx.measureText(txt).width + ls * txt.length;
if (w > max) max = w;
}
setLabelW(Math.ceil(max) + 2);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [labels.join("")]);
const arrow =
"p-1 rounded-lg text-muted transition hover:text-fg hover:bg-card disabled:opacity-25 disabled:pointer-events-none";
return (
<div className="pointer-events-auto shrink-0 max-w-[55vw] h-11 px-2.5 rounded-2xl glass-menu flex items-center gap-1.5">
<button
onClick={onPrev}
disabled={!canPrev}
title={t("header.prevModule")}
aria-label={t("header.prevModule")}
className={arrow}
>
<ChevronLeft className="w-4 h-4" />
</button>
<button
onClick={onNext}
disabled={!canNext}
title={t("header.nextModule")}
aria-label={t("header.nextModule")}
className={arrow}
>
<ChevronRight className="w-4 h-4" />
</button>
<span className="w-px h-5 bg-border/70 mx-1 shrink-0" />
<span className="min-w-0 inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.16em] text-fg">
<span className="w-1.5 h-1.5 rounded-full bg-accent shrink-0" />
<span
ref={labelRef}
className="min-w-0 truncate"
style={labelW ? { width: labelW } : undefined}
>
{label}
</span>
</span>
</div>
);
}
-12
View File
@@ -145,15 +145,3 @@ export const SYSTEM_PAGES: readonly Page[] = MODULES.filter((m) => m.system).map
export function moduleOrder(me: Me): Page[] {
return MODULES.filter((m) => !m.gate || m.gate(me)).map((m) => m.id);
}
// Step to the next/previous module cyclically (wraps at both ends). `dir` = +1 forward, -1 back.
// If the current page isn't a primary module (e.g. Settings), forward lands on the first module
// and back on the last.
export function stepModule(me: Me, current: Page, dir: 1 | -1): Page {
const order = moduleOrder(me);
if (order.length === 0) return current;
const i = order.indexOf(current);
// order is non-empty (guarded above) and every index below is in-bounds, so none are undefined.
if (i === -1) return dir === 1 ? order[0]! : order[order.length - 1]!;
return order[(i + dir + order.length) % order.length]!;
}