import { useRef, type FocusEventHandler, type InputHTMLAttributes, type KeyboardEventHandler, type ReactNode, } from "react"; import { useTranslation } from "react-i18next"; import { Search, X } from "lucide-react"; import { useSearchFocusKey } from "../lib/keymap"; // The floating search pill in the top header. One component for every module that searches // (feed, Plex, channels, playlists): a glass rounded box with a search icon, the input, a // clear button, and a "Go" trigger (Enter also fires it). The feed passes its live-YouTube // escalation as the Go action; the other modules filter as you type and Go just commits/blurs. // // Optional slots keep the box reusable while letting one module enrich it: `leading` replaces the // search icon with a control (Plex's search scope-pill), and `below` renders an absolutely // positioned panel attached under the box (Plex's instant typeahead results). `onFocus`/`onBlur`/ // `onKeyDown` pass through to the input so a caller can drive that panel (open on focus, arrow-key // navigation); the built-in Enter→Go fires only if the caller didn't `preventDefault()` first. export default function SearchBar({ value, onChange, placeholder, onGo, goLabel, goIcon, goDisabled, goTitle, leading, below, onFocus, onBlur, onKeyDown, inputProps, }: { value: string; onChange: (q: string) => void; placeholder: string; // Enter / the Go button call this. Omit to hide the Go button entirely. onGo?: () => void; goLabel?: string; goIcon?: ReactNode; goDisabled?: boolean; goTitle?: string; leading?: ReactNode; below?: ReactNode; onFocus?: FocusEventHandler; onBlur?: FocusEventHandler; onKeyDown?: KeyboardEventHandler; // Extra attributes spread onto the (e.g. Plex's combobox ARIA). Spread FIRST so the // component's own value/handlers always win. inputProps?: InputHTMLAttributes; }) { const { t } = useTranslation(); // `s` puts the caret here from anywhere on the page (E11 S4) — the header mounts exactly one box. const inputRef = useRef(null); useSearchFocusKey(inputRef); const fire = () => { if (onGo && !goDisabled) onGo(); }; // Responsive rules on the SHARED search box (every module's header, not just Plex), keyed on the // content column's width via `@container/appmain` (E5 S7): `min-w-0` by default so it never forces // overflow on a truly tiny column; once the column has room (≥420px) a `min-w-[15rem]` floor makes // the box WRAP to its own line via the header's flex-wrap instead of shrinking the input to ~0; on // a narrow column (≤640px) it also takes a full line of its own (basis-full) and sorts LAST, so the // small pills (module name + Plex scope toggle / feed sync chip) share the line above it. return (
{leading ?? } onChange(e.target.value)} onFocus={onFocus} onBlur={onBlur} onKeyDown={(e) => { onKeyDown?.(e); if (e.defaultPrevented) return; if (e.key === "Enter") fire(); // Escape hands the keyboard back to the page, where `j`/`k` and the `g` chords live — `s` // got you in, so there has to be a key that gets you out. The query stays as typed. A caller // that uses Escape itself (Plex's typeahead closing its panel) preventDefaults first. else if (e.key === "Escape") e.currentTarget.blur(); }} placeholder={placeholder} className="flex-1 min-w-0 bg-transparent outline-hidden text-sm placeholder:text-muted" /> {value && ( )} {onGo && ( )}
{below}
); }