"use client"; // The standard path, and the only part of the bar a first-time visitor has to // understand: a query box, a Search button, and one row of chips for everything // else. Below lg it sticks under the header, so the search box is always one // glance away no matter how far down the results the reader has gone. // // Everything that used to sit ABOVE this — the profile row, five filter // sections, the availability tree, the date pickers — now lives in FiltersPanel // behind the Filters chip (or inline from xl). That stack was two phone screens // tall and it sat between the visitor and the thing they came for. import { useMemo, type ReactNode } from "react"; import { LayersIcon, SearchIcon, Share2Icon } from "lucide-react"; import { isCompactRoot } from "../lib/searchQuery"; import { MISSING_STATES } from "../lib/availability"; import { Button } from "./ui/button"; import QueryBuilder, { compactLayerActions } from "./QueryBuilder"; import { FiltersTrigger } from "./FiltersContainer"; import { useMediaQuery } from "../lib/useMediaQuery"; import { SEARCH_IN_REFUSAL, useSearchSession } from "./SearchSessionContext"; export default function SearchBar({ nav }: { nav?: ReactNode }) { const { commitSearch, mounted, draftRoot, setDraftRoot, leafStates, groupStates, queryDirty, filtersDirty, searchedThisPageLife, handleResetLayers, layersResetDisabled, hasSubs, liveChatTotalCount, shareCopied, handleShareCurrentSearch, draftExcludedChannels, draftNov, draftNol, draftNop, draftNotr, draftLc, draftSearchInEmpty, draftNaa, draftNar, draftStates, draftDateFrom, draftDateTo, draftTags, } = useSearchSession(); // From xl the filters are an inline block under the bar with their own // disclosure, so the chip (and the bar's share button, which the profile row // already carries) would be a second copy of a control that is on screen. const inlineFilters = useMediaQuery("(min-width: 1280px)"); const compact = isCompactRoot(draftRoot); const layers = compactLayerActions(draftRoot, setDraftRoot); // What the Filters chip's badge counts: anything the reader has narrowed — // and the "Search in" row off its default, which can widen (Live chat) as // well as narrow, because either way a plain query no longer reads what it // reads by default and the panel that says so may be behind the chip. const activeFilters = useMemo(() => { let n = 0; if (draftExcludedChannels.size > 0) n += 1; if (draftNotr || draftNop || draftLc) n += 1; if (draftNov || draftNol) n += 1; if (draftNaa || draftNar) n += 1; if (draftStates.size < MISSING_STATES.length + 1) n += 1; if (draftDateFrom || draftDateTo) n += 1; if (draftTags.size > 0) n += 1; return n; }, [ draftExcludedChannels, draftNov, draftNol, draftNop, draftNotr, draftLc, draftNaa, draftNar, draftStates, draftDateFrom, draftDateTo, draftTags, ]); // The line under the bar says what to do: after an edit that is not applied // yet, and before the first Search of the page life, when the results area // is empty and waits for it. Not while Search is refused ("Search in" has // nothing ticked) — the panel's own line says what to fix. const promptApply = !draftSearchInEmpty && (queryDirty || filtersDirty || !searchedThisPageLife); // The hint points at the row, so it goes once the box is ticked. const chatHint = hasSubs && !draftLc; const submit = ( ); const resetLayers = ( ); return ( // A fragment, not a wrapper: `position: sticky` only travels inside its // containing block, so the form has to be a DIRECT child of the tall column // that spans the bar and the results (SiteWorkspace / TranscriptSearch). // Wrapped in a div of its own it stuck for its own height and no further. <>
{ e.preventDefault(); commitSearch(); }} // Sticky ONLY in compact mode. A full query tree is several leaf cards // and group boxes — 725px at 390px wide — and pinning that would park a // block taller than the viewport over the results it produced. className={`flex flex-col gap-1.5 sm:gap-2 ${ compact ? "max-lg:sticky max-lg:top-14 max-lg:z-10 max-lg:bg-background max-lg:py-1.5" : "" }`} data-testid="search-form" > {mounted ? ( ) : (
)} {!compact && (
{submit}
)} {/* The Search button's description. Here rather than pointing at the Filters panel's line, which below xl lives in a sheet that is not mounted while closed. Not a live region: the panel's line is the one announced, where the box was unticked. */} {draftSearchInEmpty ? SEARCH_IN_REFUSAL : ""} {/* The chip row. Rendered OUTSIDE the `mounted` gate so the workspace nav exists as plain anchors before hydration — a nav click lost to the pre-hydration window is a click that goes nowhere. It WRAPS rather than scrolling sideways: an `overflow-x-auto` row in normal flow still widens `documentElement.scrollWidth` under Chrome's mobile emulation (measured: 498px at a 390px viewport, and neither `overflow-x: hidden` nor a fixed width fixed it), which is the exact horizontal overflow this redesign exists to remove. The modal's control strip can scroll because it is `position: fixed` and so contributes nothing to the document's scroll width. */}
{nav} {compact && ( <> )} {/* Not gated on `channelOptions.length` — the panel also holds the profile row and the advanced options, which used to be visible unconditionally, and the channel list is empty for the first moment of every page load. */} {!inlineFilters && ( )} {!inlineFilters && ( )} {resetLayers}
{/* One status line under the bar instead of two hints competing for room inside it — and a SIBLING of the form, so the pinned block is never more than the input row plus the chips. */} {(promptApply || chatHint) && (

{/* The site's accent, not the warning hue: an unapplied edit is the next step, not a fault. `--brand` clears 4.5:1 on every base's page ground (lib/brand.ts MIN_ACCENT_CONTRAST). */} {promptApply && ( Press Enter or click Search to apply )} {chatHint && ( Live chat available on {liveChatTotalCount} video {liveChatTotalCount === 1 ? "" : "s"} — tick Live chat under Search in. )}

)} ); }