/** @jsxImportSource react */ import { useCallback, useContext, useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from "react"; import type { HostQueryBarItem, HostQueryBarProps } from "../../../../ui/host"; import { WebPopover } from "./popover"; import { WebMenu } from "./menu"; import { useHorizontalOverflow } from "../host/overflow-fade"; import { StackHeaderContext } from "./stack-header"; import { CheckboxBox } from "./controls"; import { WebIcon } from "./icons"; /** Input width of a text filter that names none, in cells (the terminal's default). */ const DEFAULT_TEXT_FIELD_CELLS = 20; function Chevron() { return ; } function Cross() { return ; } function SearchIcon() { return ; } function QueryMenu({ item, onClose }: { item: HostQueryBarItem; onClose: () => void }) { return ( ({ id: option.value, label: option.label, description: option.description, disabled: option.disabled, selected: item.kind === "select" ? option.selected : undefined, checked: item.kind === "multi" ? option.selected : undefined, }))} onSelect={(value) => { item.onSelect(value); if (item.kind === "select") onClose(); }} onClose={onClose} /> ); } function TextField({ item }: { item: HostQueryBarItem }) { return (
{ event.stopPropagation(); if (!item.active) item.onActivate?.(); }} > {item.label} {/* The width sizes the input, as in the terminal; the segment grows to fit the label beside it. */}
{item.node}
{item.narrowing ? ( { event.preventDefault(); event.stopPropagation(); }} onClick={(event) => { event.stopPropagation(); item.onReset(); }} > ) : null}
); } function FilterChip({ item, open, onOpenChange }: { item: HostQueryBarItem; open: boolean; onOpenChange: (open: boolean) => void }) { if (item.kind === "text") return ; const triggerRef = useRef(null); // A shortcut can open a filter that is scrolled out of a narrow bar. useEffect(() => { if (open) triggerRef.current?.scrollIntoView({ block: "nearest", inline: "nearest" }); }, [open]); if (item.kind === "select" && item.inline) { return (
{item.label}
{item.options.map((option) => ( ))}
); } if (item.kind === "toggle") { return ( ); } const trigger = ( ); return ( onOpenChange(false)} /> ); } export function WebQueryBar({ search, items, view, onClearAll, meta, openRequest }: HostQueryBarProps) { const [openId, setOpenId] = useState(null); const rootRef = useRef(null); const scrollRef = useRef(null); // At the top of an open stack detail, take its Back and title as leading // segments so the detail shows one toolbar, not two. const stack = useContext(StackHeaderContext); const [stackAttached, setStackAttached] = useState(false); useLayoutEffect(() => { const bar = rootRef.current; const container = stack?.containerRef.current; if (!stack || !bar || !container || !container.contains(bar)) return; if (Math.abs(bar.getBoundingClientRect().top - container.getBoundingClientRect().top) > 1.5) return; const release = stack.attach(); setStackAttached(true); return () => { release(); setStackAttached(false); }; }, [stack]); const overflow = useHorizontalOverflow(scrollRef, [items.length, !!search, !!view, !!onClearAll, meta]); const searchRef = useRef(null); // When an inline choice or the view changes (a shortcut, the next expiry), // bring the new selection into a scrolled bar. Not on mount: a view at the // right edge must not scroll the search away when the pane opens. const selectionKey = [ ...items.filter((item) => item.inline).map((item) => `${item.id}=${item.options.find((option) => option.selected)?.value ?? ""}`), `view=${view?.value ?? ""}`, ].join("|"); const previousSelectionKey = useRef(selectionKey); useEffect(() => { const previous = previousSelectionKey.current; previousSelectionKey.current = selectionKey; if (previous === selectionKey) return; const before = new Map(previous.split("|").map((entry) => entry.split("=") as [string, string])); const bar = scrollRef.current; if (!bar) return; for (const entry of selectionKey.split("|")) { const [id, value] = entry.split("=") as [string, string]; if (before.get(id) === value) continue; const group = id === "view" ? bar.querySelector(":scope > .gloom-qb-view") : bar.querySelector(`[data-item-id="${CSS.escape(id)}"]`); group?.querySelector("button[data-active=true]")?.scrollIntoView({ block: "nearest", inline: "nearest" }); } }, [selectionKey]); useEffect(() => { if (search?.active) searchRef.current?.scrollIntoView({ block: "nearest", inline: "nearest" }); }, [search?.active]); useEffect(() => { if (openRequest) setOpenId(openRequest.id); }, [openRequest?.id, openRequest?.token]); const setOpen = useCallback((id: string, open: boolean) => { setOpenId((current) => open ? id : current === id ? null : current); }, []); let searchNode: ReactNode = null; if (search) { searchNode = (
{ event.stopPropagation(); if (!search.active) search.onActivate(); }} >
{search.node}
{search.filled ? ( { event.preventDefault(); event.stopPropagation(); }} onClick={(event) => { event.stopPropagation(); search.onClear(); }} > ) : !search.active ? / : null}
); } return (
{stack && stackAttached && ( <> {stack.title ? {stack.title} : null} )} {searchNode} {items.length > 0 && (
{items.map((item) => ( setOpen(item.id, open)} /> ))} {onClearAll && ( )}
)} {meta && {meta}} {view && (
{view.options.map((option) => ( ))}
)}
); }