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:
2026-07-13 02:28:06 +02:00
parent a7f9032e0e
commit d159f35f31
17 changed files with 420 additions and 141 deletions
+14 -24
View File
@@ -12,7 +12,6 @@ import {
Pencil,
Plus,
RefreshCw,
Search,
UserMinus,
X,
} from "lucide-react";
@@ -44,6 +43,8 @@ const STATUS_FILTERS: { id: ChannelStatusFilter; labelKey: string }[] = [
export default function Channels({
canWrite,
isAdmin,
search,
setSearch,
onViewChannel,
onFilterByTag,
onFocusChannel,
@@ -58,6 +59,9 @@ export default function Channels({
}: {
canWrite: boolean;
isAdmin: boolean;
// The channel-name filter, lifted to App so the shared top SearchBar drives it.
search: string;
setSearch: (q: string) => void;
onViewChannel: (id: string, name: string) => void;
onFilterByTag: (tagId: number, name: string) => void;
onFocusChannel: (name: string) => void;
@@ -190,9 +194,8 @@ export default function Channels({
onError: (e) => notifyYouTubeActionError(e, t("channels.notify.fullHistoryFailed"), onOpenWizard),
});
// Channel-name search + tag-chip filtering, applied client-side over the status-filtered list
// (prominent controls instead of the DataTable's hidden per-column popovers).
const [search, setSearch] = useState("");
// Channel-name search (from the shared top SearchBar, via props) + tag-chip filtering, applied
// client-side over the status-filtered list.
const [tagFilter, setTagFilter] = useState<number[]>([]);
// A focus-channel intent (header "without full history" / tag manager) seeds the search box.
useEffect(() => {
@@ -399,7 +402,12 @@ export default function Channels({
return (
<>
{tabs}
<ChannelDiscovery canWrite={canWrite} onOpenWizard={onOpenWizard} onViewChannel={onViewChannel} />
<ChannelDiscovery
canWrite={canWrite}
search={search}
onOpenWizard={onOpenWizard}
onViewChannel={onViewChannel}
/>
</>
);
}
@@ -482,25 +490,7 @@ export default function Channels({
/>
</p>
{/* Channel-name search */}
<div className="relative mb-3 max-w-sm">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted pointer-events-none" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t("channels.searchPlaceholder")}
className="w-full bg-card border border-border rounded-lg pl-8 pr-8 py-2 text-sm outline-none focus:border-accent"
/>
{search && (
<button
onClick={() => setSearch("")}
aria-label={t("common.cancel")}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted hover:text-fg"
>
<X className="w-4 h-4" />
</button>
)}
</div>
{/* The channel-name search now lives in the shared top SearchBar (driven via props). */}
{/* Your tags — click a chip to filter the table by it (add/rename/delete live in the manager). */}
<div className="flex flex-wrap items-center gap-1.5 mb-4">