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 { useTranslation } from "react-i18next";
|
||||||
import { Youtube } from "lucide-react";
|
import { Youtube } from "lucide-react";
|
||||||
import type { Me } from "../lib/api";
|
import type { Me } from "../lib/api";
|
||||||
import { moduleLabelKey, moduleOrder, stepModule } from "../lib/modules";
|
|
||||||
import ModuleName from "./ModuleName";
|
|
||||||
import SearchBar from "./SearchBar";
|
import SearchBar from "./SearchBar";
|
||||||
import PlexSearchBox from "./PlexSearchBox";
|
import PlexSearchBox from "./PlexSearchBox";
|
||||||
import PlexScopeToggle from "./PlexScopeToggle";
|
import PlexScopeToggle from "./PlexScopeToggle";
|
||||||
import SyncStatus from "./SyncStatus";
|
import SyncStatus from "./SyncStatus";
|
||||||
import { useNavigation, useNavigationActions } from "./NavigationProvider";
|
import { useNavigation } from "./NavigationProvider";
|
||||||
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
|
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
|
||||||
import { usePlaylists, usePlaylistsActions } from "./PlaylistsProvider";
|
import { usePlaylists, usePlaylistsActions } from "./PlaylistsProvider";
|
||||||
import { useChannels, useChannelsActions } from "./ChannelsProvider";
|
import { useChannels, useChannelsActions } from "./ChannelsProvider";
|
||||||
|
|
||||||
// The floating top header: a row of glass pills over the content — a ModuleName pill (label +
|
// The floating top header: a row of glass pills over the content — the module's SearchBar where it
|
||||||
// accent dot + back/forward arrows), the module's SearchBar where it has one, and the per-user
|
// has one, and the per-user sync chip at the right end.
|
||||||
// sync chip at the right end.
|
|
||||||
// `forwardRef` so PageShell can attach a ref to the header's root box and measure its real (possibly
|
// `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
|
// 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).
|
// owns both the header slot and the padded content wrapper (E5 S7).
|
||||||
@@ -30,7 +27,6 @@ const Header = forwardRef<
|
|||||||
>(function Header({ me, onYtSearch }, ref) {
|
>(function Header({ me, onYtSearch }, ref) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { page } = useNavigation();
|
const { page } = useNavigation();
|
||||||
const { setPage } = useNavigationActions();
|
|
||||||
const filters = useFeedFilters();
|
const filters = useFeedFilters();
|
||||||
const { setFilters } = useFeedFiltersActions();
|
const { setFilters } = useFeedFiltersActions();
|
||||||
// Each module's search term lives in its own provider (shared with that module's rail/content):
|
// 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 { setSearch: setPlaylistsSearch } = usePlaylistsActions();
|
||||||
const { search: channelsSearch } = useChannels();
|
const { search: channelsSearch } = useChannels();
|
||||||
const { setSearch: setChannelsSearch, goToFullHistory: onGoToFullHistory } = useChannelsActions();
|
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).
|
// 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?.();
|
const blur = () => (document.activeElement as HTMLElement | null)?.blur?.();
|
||||||
|
|
||||||
@@ -101,14 +91,6 @@ const Header = forwardRef<
|
|||||||
ref={ref}
|
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"
|
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}
|
{search}
|
||||||
{/* Library scope (movie/show/both) sits right of the search box on Plex — relocated here from
|
{/* 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. */}
|
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[] {
|
export function moduleOrder(me: Me): Page[] {
|
||||||
return MODULES.filter((m) => !m.gate || m.gate(me)).map((m) => m.id);
|
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