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:
@@ -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. */}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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]!;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user