/** @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. */}
);
}
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();
}}
>