Files
siftlode/frontend/src/components/ModuleName.tsx
T
peter d159f35f31 feat(header): floating module header with cyclic nav and shared search
Replace the full-width top bar with a fixed floating header of glass pills: a
fixed-position ModuleName (accent dot + label + prev/next cyclic module stepper)
and a shared SearchBar. The header's left edge is anchored as if the nav rail
and filter sidebar were both open, so it never shifts on collapse or module
change; the right edge leaves a 10% margin.

- ModuleName label width is measured live from the reachable modules' titles in
  the current language, recomputing on a language or account change.
- Module names come from a shared moduleLabelKey (the same short labels as the
  nav rail); NavSidebar and the header now read from one source (lib/modules.ts).
- The arrows step cyclically through moduleOrder(me) — dynamic, no hard-coded
  targets, wrapping at both ends.
- Search is hoisted into the SearchBar for feed (YouTube search), plex, channels
  (including the Discovery tab) and playlists (new); Go button, Enter triggers it.
- The Playlists left rail now reaches the top; header clearance via --hdr-h.
2026-07-13 02:28:06 +02:00

91 lines
3.4 KiB
TypeScript

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>
);
}