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.
This commit is contained in:
parent
36c27250c3
commit
2739f25e6c
17 changed files with 420 additions and 141 deletions
|
|
@ -183,7 +183,14 @@ function Row({
|
|||
);
|
||||
}
|
||||
|
||||
export default function Playlists({ canWrite }: { canWrite: boolean }) {
|
||||
export default function Playlists({
|
||||
canWrite,
|
||||
search,
|
||||
}: {
|
||||
canWrite: boolean;
|
||||
// The playlist-name filter, from the shared top SearchBar (App-owned state).
|
||||
search: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
const confirm = useConfirm();
|
||||
|
|
@ -321,6 +328,14 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [playlists, plSort]);
|
||||
|
||||
// Client-side name filter driven by the shared top SearchBar.
|
||||
const q = search.trim().toLowerCase();
|
||||
const visiblePlaylists = useMemo(
|
||||
() => (q ? sortedPlaylists.filter((p) => plName(p).toLowerCase().includes(q)) : sortedPlaylists),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[sortedPlaylists, q]
|
||||
);
|
||||
|
||||
// Scroll the restored selection into view once after the rail first renders.
|
||||
useEffect(() => {
|
||||
if (!scrolledRef.current && selectedRef.current) {
|
||||
|
|
@ -599,8 +614,11 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
|
|||
{playlists.length === 0 && !listQuery.isLoading && (
|
||||
<div className="text-xs text-muted px-1 py-2">{t("playlists.noneYet")}</div>
|
||||
)}
|
||||
{playlists.length > 0 && visiblePlaylists.length === 0 && (
|
||||
<div className="text-xs text-muted px-1 py-2">{t("playlists.noMatches")}</div>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
{sortedPlaylists.map((pl) => (
|
||||
{visiblePlaylists.map((pl) => (
|
||||
<button
|
||||
key={pl.id}
|
||||
ref={pl.id === selectedId ? selectedRef : null}
|
||||
|
|
@ -636,7 +654,7 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
|
|||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="flex-1 min-w-0 overflow-y-auto p-4">
|
||||
<main className="flex-1 min-w-0 overflow-y-auto px-4 pb-4 pt-[var(--hdr-h)]">
|
||||
{selectedId == null || !detail ? (
|
||||
<div className="text-muted text-sm p-4">
|
||||
{selectedId == null ? t("playlists.pickOne") : t("playlists.loading")}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue