UNPKG

@flanksource/clicky-ui

Version:

Flanksource Clicky UI — React component library built on shadcn/ui with light/dark and density theming.

1,171 lines (1,170 loc) 68.4 kB
import { jsx, jsxs } from "react/jsx-runtime"; import { useMemo, createContext, useRef, useState, useEffect, useContext } from "react"; import { FilterPill } from "../data/FilterPill.js"; import { Icon } from "../data/Icon.js"; import { cn } from "../lib/utils.js"; import { Button } from "./button.js"; import { DatePicker } from "./DatePicker.js"; import { DateTimePicker } from "./DateTimePicker.js"; import { MultiSelect } from "./MultiSelect.js"; import { RangeSlider } from "./RangeSlider.js"; import { Select } from "./select.js"; const FILTER_INPUT_DEBOUNCE_MS = 500; const FilterBarContext = createContext({ autoSubmit: true }); function FilterBar({ search, filters, timeRange, dateRange, children, leading, trailing, className, autoSubmit = true, onApply, applyLabel = "Apply", isPending = false }) { const hasRangeControls = Boolean(timeRange || dateRange); const showApply = !autoSubmit && !!onApply; const contextValue = useMemo(() => ({ autoSubmit }), [autoSubmit]); return /* @__PURE__ */ jsx(FilterBarContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs( "div", { className: cn( "flex flex-wrap items-center gap-2 rounded-lg border border-input bg-background px-2 py-1.5 shadow-sm", className ), children: [ leading && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: leading }), search && /* @__PURE__ */ jsx(SearchField, { search }), children, filters == null ? void 0 : filters.map((filter, index) => { const grow = !search && index === 0; if (filter.kind === "lookup") { return /* @__PURE__ */ jsx(LookupFilterField, { filter, grow }, filter.key); } if (filter.kind === "lookup-multi") { return /* @__PURE__ */ jsx(LookupMultiFilterField, { filter, grow }, filter.key); } if (filter.kind === "multi") { return /* @__PURE__ */ jsx(MultiFilterField, { filter, grow }, filter.key); } if (filter.kind === "nested-multi") { return /* @__PURE__ */ jsx(NestedMultiFilterField, { filter, grow }, filter.key); } if (filter.kind === "select-multi") { return /* @__PURE__ */ jsx(SelectMultiFilterField, { filter, grow }, filter.key); } if (filter.kind === "number") { return /* @__PURE__ */ jsx(NumberFilterField, { filter, grow }, filter.key); } if (filter.kind === "enum") { return /* @__PURE__ */ jsx(EnumFilterField, { filter, grow }, filter.key); } if (filter.kind === "boolean") { return /* @__PURE__ */ jsx(BooleanFilterField, { filter }, filter.key); } return /* @__PURE__ */ jsx(TextFilterField, { filter, grow }, filter.key); }), (hasRangeControls || trailing || showApply) && /* @__PURE__ */ jsxs("div", { className: "ml-auto flex flex-wrap items-center gap-2", children: [ dateRange && /* @__PURE__ */ jsx(RangeControlButton, { kind: "date", label: "Date range", ...dateRange }), timeRange && /* @__PURE__ */ jsx(RangeControlButton, { kind: "time", label: "Time range", ...timeRange }), trailing, showApply && /* @__PURE__ */ jsx( Button, { type: "button", variant: "default", size: "sm", disabled: isPending, onClick: onApply, children: isPending ? "Loading…" : applyLabel } ) ] }) ] } ) }); } function FilterBarFilterPanel({ filter, chrome = "full" }) { const contextValue = useMemo(() => ({ autoSubmit: true }), []); return /* @__PURE__ */ jsxs(FilterBarContext.Provider, { value: contextValue, children: [ filter.kind === "lookup" && /* @__PURE__ */ jsx(LookupFilterField, { filter, grow: true }), filter.kind === "lookup-multi" && /* @__PURE__ */ jsx(LookupMultiFilterField, { filter, grow: true }), filter.kind === "multi" && /* @__PURE__ */ jsx(MultiFilterPanel, { filter, chrome }), filter.kind === "nested-multi" && /* @__PURE__ */ jsx(NestedMultiFilterPanel, { filter, chrome }), filter.kind === "select-multi" && /* @__PURE__ */ jsx(SelectMultiFilterField, { filter, grow: true }), filter.kind === "number" && /* @__PURE__ */ jsx(NumberFilterPanel, { filter, chrome }), filter.kind === "enum" && /* @__PURE__ */ jsx(EnumFilterField, { filter, grow: true }), filter.kind === "boolean" && /* @__PURE__ */ jsx(BooleanFilterField, { filter }), filter.kind === "text" && /* @__PURE__ */ jsx(TextFilterField, { filter, grow: true }) ] }); } function FilterBarRangePanel({ kind, label, from = "", to = "", onApply, presets: _presets, fromPlaceholder, toPlaceholder, emptyLabel: _emptyLabel }) { const fromInputRef = useRef(null); const toInputRef = useRef(null); const [draftFrom, setDraftFrom] = useState(from); const [draftTo, setDraftTo] = useState(to); useEffect(() => { setDraftFrom(from); setDraftTo(to); }, [from, to]); function applyRange(nextFrom, nextTo) { onApply( kind === "time" ? normalizeDateMath(nextFrom) : nextFrom.trim(), kind === "time" ? normalizeDateMath(nextTo) : nextTo.trim() ); } return /* @__PURE__ */ jsx("div", { className: "w-72 text-popover-foreground", children: /* @__PURE__ */ jsxs("div", { className: "p-3", children: [ /* @__PURE__ */ jsx("div", { className: "mb-2 text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: label }), /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [ /* @__PURE__ */ jsx("label", { className: "text-[10px] text-muted-foreground", children: "From" }), /* @__PURE__ */ jsx( RangeInput, { inputRef: fromInputRef, kind, ariaLabel: `${label} from`, placeholder: fromPlaceholder ?? (kind === "date" ? "" : "now-24h"), value: draftFrom, onChange: setDraftFrom } ) ] }), /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [ /* @__PURE__ */ jsx("label", { className: "text-[10px] text-muted-foreground", children: "To" }), /* @__PURE__ */ jsx( RangeInput, { inputRef: toInputRef, kind, ariaLabel: `${label} to`, placeholder: toPlaceholder ?? (kind === "date" ? "" : "now"), value: draftTo, onChange: setDraftTo } ) ] }) ] }), /* @__PURE__ */ jsx("div", { className: "mt-3 flex justify-end", children: /* @__PURE__ */ jsx( Button, { type: "button", variant: "default", size: "sm", className: "h-8 px-3 text-xs", onClick: () => applyRange(draftFrom, draftTo), children: "Apply" } ) }) ] }) }); } function EnumFilterField({ filter, grow }) { return /* @__PURE__ */ jsxs( "label", { title: filter.description, className: cn( "flex h-8 items-center gap-2 rounded-md border border-input bg-muted/30 pl-2 pr-1 text-xs", grow ? "min-w-[12rem] max-w-[18rem] flex-1" : "min-w-[11rem] max-w-[15rem] shrink-0", filter.disabled && "opacity-60", filter.className ), children: [ /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap font-medium uppercase tracking-wide text-muted-foreground", children: filter.label }), /* @__PURE__ */ jsx( Select, { "aria-label": filter.label, className: "h-6 border-0 bg-transparent px-1 text-xs shadow-none focus-visible:ring-0", value: filter.value, placeholder: filter.placeholder ?? `Any ${filter.label.toLowerCase()}`, disabled: filter.disabled, onChange: (event) => filter.onChange(event.target.value), options: filter.options.map((option) => ({ value: option.value, label: option.label ?? option.value })) } ) ] } ); } function BooleanFilterField({ filter }) { return /* @__PURE__ */ jsxs( "label", { title: filter.description, className: cn( "flex h-8 shrink-0 items-center gap-2 rounded-md border border-input bg-muted/30 px-2 text-xs", filter.disabled && "opacity-60", filter.className ), children: [ /* @__PURE__ */ jsx( "input", { type: "checkbox", "aria-label": filter.label, className: "h-3.5 w-3.5 accent-primary", checked: filter.value, disabled: filter.disabled, onChange: (event) => filter.onChange(event.target.checked) } ), /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap font-medium uppercase tracking-wide text-muted-foreground", children: filter.label }) ] } ); } function SearchField({ search }) { const [draft, setDraft] = useDebouncedTextDraft(search.value, search.onChange); return /* @__PURE__ */ jsx("div", { className: "flex min-w-[14rem] max-w-[24rem] flex-1 items-center gap-2", children: /* @__PURE__ */ jsxs( "label", { className: cn( "flex h-8 min-w-0 flex-1 items-center rounded-md border border-input bg-background px-3 text-sm", search.className ), children: [ draft.trim() ? /* @__PURE__ */ jsx("span", { className: "mr-2 whitespace-nowrap text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: search.ariaLabel ?? "Search" }) : /* @__PURE__ */ jsx(Icon, { name: "codicon:search", className: "mr-2 shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsx( "input", { type: "search", "aria-label": search.ariaLabel ?? search.placeholder ?? "Search", className: "w-full bg-transparent outline-none placeholder:text-muted-foreground", placeholder: search.placeholder ?? "Search…", value: draft, onChange: (event) => setDraft(event.target.value) } ) ] } ) }); } function TextFilterField({ filter, grow }) { const [draft, setDraft] = useDebouncedTextDraft(filter.value, filter.onChange); return /* @__PURE__ */ jsxs( "label", { title: filter.description, className: cn( "flex h-8 items-center gap-2 rounded-md border border-input bg-muted/30 pl-2 pr-2 text-xs", grow ? "min-w-[12rem] max-w-[18rem] flex-1" : "min-w-[11rem] max-w-[15rem] shrink-0", filter.disabled && "opacity-60", filter.className ), children: [ /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap font-medium uppercase tracking-wide text-muted-foreground", children: filter.label }), /* @__PURE__ */ jsx( "input", { type: "text", "aria-label": filter.label, className: "w-full min-w-0 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed", placeholder: filter.placeholder ?? "Filter…", value: draft, disabled: filter.disabled, onChange: (event) => setDraft(event.target.value) } ) ] } ); } function LookupFilterField({ filter, grow }) { const [draft, setDraft] = useDebouncedTextDraft(filter.value, filter.onChange); const listId = `${filter.key}-lookup-options`; return /* @__PURE__ */ jsxs( "label", { title: filter.description, className: cn( "flex h-8 items-center gap-2 rounded-md border border-input bg-muted/30 pl-2 pr-2 text-xs", grow ? "min-w-[12rem] max-w-[18rem] flex-1" : "min-w-[11rem] max-w-[15rem] shrink-0", filter.disabled && "opacity-60", filter.className ), children: [ /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap font-medium uppercase tracking-wide text-muted-foreground", children: filter.label }), filter.inputType === "date" ? /* @__PURE__ */ jsx( DateTimePicker, { "aria-label": filter.label, className: "w-full", inputClassName: "w-full min-w-0 border-0 bg-transparent px-0 pr-6 text-sm text-foreground shadow-none focus-visible:ring-0", buttonClassName: "right-0", placeholder: filter.placeholder ?? "Filter…", value: draft, list: listId, disabled: filter.disabled, onChange: setDraft } ) : /* @__PURE__ */ jsx( "input", { type: filter.inputType === "number" ? "number" : "text", "aria-label": filter.label, className: "w-full min-w-0 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed", placeholder: filter.placeholder ?? "Filter…", value: draft, list: listId, disabled: filter.disabled, onChange: (event) => setDraft(event.target.value) } ), /* @__PURE__ */ jsx("datalist", { id: listId, children: filter.options.map((option) => /* @__PURE__ */ jsx( "option", { value: option.value, label: option.label ?? option.value, disabled: option.disabled }, option.value )) }) ] } ); } function LookupMultiFilterField({ filter, grow }) { const [draft, setDraft] = useDebouncedTextDraft( filter.value.join(", "), (next) => filter.onChange(parseLookupMultiValue(next)) ); const listId = `${filter.key}-lookup-options`; return /* @__PURE__ */ jsxs( "label", { title: filter.description, className: cn( "flex h-8 items-center gap-2 rounded-md border border-input bg-muted/30 pl-2 pr-2 text-xs", grow ? "min-w-[12rem] max-w-[18rem] flex-1" : "min-w-[11rem] max-w-[15rem] shrink-0", filter.disabled && "opacity-60", filter.className ), children: [ /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap font-medium uppercase tracking-wide text-muted-foreground", children: filter.label }), /* @__PURE__ */ jsx( "input", { type: "text", "aria-label": filter.label, className: "w-full min-w-0 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed", placeholder: filter.placeholder ?? "value-1, value-2", value: draft, list: listId, disabled: filter.disabled, onChange: (event) => setDraft(event.target.value) } ), /* @__PURE__ */ jsx("datalist", { id: listId, children: filter.options.map((option) => /* @__PURE__ */ jsx( "option", { value: option.value, label: option.label ?? option.value, disabled: option.disabled }, option.value )) }) ] } ); } function NumberFilterField({ filter, grow }) { const rootRef = useRef(null); const triggerRef = useRef(null); const [open, setOpen] = useState(false); useDismissablePopup(open, rootRef, triggerRef, () => setOpen(false)); const bounds = resolveNumberFilterBounds(filter); const [draft, setDraft] = useDebouncedNumberDraft(filter.value, filter.onChange); const sliderMin = clampNumber(parseFilterNumber(draft.min) ?? bounds.min, bounds.min, bounds.max); const sliderMax = clampNumber(parseFilterNumber(draft.max) ?? bounds.max, bounds.min, bounds.max); const activeMin = Math.min(sliderMin, sliderMax); const activeMax = Math.max(sliderMin, sliderMax); const summary = summarizeNumberFilter(filter, bounds, draft); return /* @__PURE__ */ jsxs( "div", { ref: rootRef, title: filter.description, className: cn( "relative min-w-0", grow ? "min-w-[8rem] max-w-[12rem] flex-1" : "shrink-0", filter.disabled && "opacity-60", filter.className ), children: [ /* @__PURE__ */ jsxs( Button, { ref: triggerRef, type: "button", variant: "outline", size: "sm", "aria-label": `${filter.label} filter`, "aria-haspopup": "dialog", "aria-expanded": open, disabled: filter.disabled, onClick: () => setOpen((current) => !current), className: cn( "min-w-0 gap-2 font-normal", grow ? "w-full max-w-[12rem] justify-between" : "w-auto max-w-[9.5rem] px-2.5", summary === filter.label && "text-muted-foreground" ), children: [ /* @__PURE__ */ jsx("span", { className: "truncate", children: summary }), /* @__PURE__ */ jsx( Icon, { name: open ? "codicon:chevron-up" : "codicon:chevron-down", className: "text-muted-foreground" } ) ] } ), open && /* @__PURE__ */ jsxs("div", { className: "absolute left-0 top-[calc(100%+0.375rem)] z-50 min-w-[18rem] max-w-[22rem] rounded-md border border-border bg-popover p-3 text-popover-foreground shadow-lg shadow-black/5", children: [ /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between gap-2", children: [ /* @__PURE__ */ jsx("div", { className: "text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: filter.label }), /* @__PURE__ */ jsx( "button", { type: "button", className: "text-[10px] text-primary disabled:text-muted-foreground", onClick: () => { setDraft({}); filter.onChange({}); }, disabled: !String(draft.min ?? "").trim() && !String(draft.max ?? "").trim(), children: "Clear all" } ) ] }), /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-[11px] text-muted-foreground", children: [ /* @__PURE__ */ jsx("span", { children: formatNumberValue(bounds.min, filter) }), /* @__PURE__ */ jsx("span", { children: formatNumberValue(bounds.max, filter) }) ] }), /* @__PURE__ */ jsx("div", { className: "relative h-6", children: /* @__PURE__ */ jsx( RangeSlider, { min: bounds.min, max: bounds.max, step: bounds.step, value: [activeMin, activeMax], ariaLabelMin: `${filter.label} minimum slider`, ariaLabelMax: `${filter.label} maximum slider`, onChange: ([nextMin, nextMax]) => setDraft(numberFilterValueFromSlider([nextMin, nextMax], bounds)) } ) }) ] }), /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [ /* @__PURE__ */ jsx("label", { className: "text-[10px] text-muted-foreground", children: "Min" }), /* @__PURE__ */ jsx( "input", { type: "number", inputMode: "decimal", step: bounds.step, "aria-label": `${filter.label} minimum`, className: "h-8 w-full rounded-md border border-input bg-background px-2 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring", placeholder: filter.minPlaceholder ?? formatNumberValue(bounds.min, filter), value: draft.min ?? "", onChange: (event) => setDraft( normalizeNumberFilterValue( { min: event.target.value, max: draft.max ?? "" }, "min-input" ) ) } ) ] }), /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [ /* @__PURE__ */ jsx("label", { className: "text-[10px] text-muted-foreground", children: "Max" }), /* @__PURE__ */ jsx( "input", { type: "number", inputMode: "decimal", step: bounds.step, "aria-label": `${filter.label} maximum`, className: "h-8 w-full rounded-md border border-input bg-background px-2 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring", placeholder: filter.maxPlaceholder ?? formatNumberValue(bounds.max, filter), value: draft.max ?? "", onChange: (event) => setDraft( normalizeNumberFilterValue( { min: draft.min ?? "", max: event.target.value }, "max-input" ) ) } ) ] }) ] }) ] }) ] }) ] } ); } function NumberFilterPanel({ filter, chrome = "full" }) { const bounds = resolveNumberFilterBounds(filter); const [draft, setDraft] = useDebouncedNumberDraft(filter.value, filter.onChange); const sliderMin = clampNumber(parseFilterNumber(draft.min) ?? bounds.min, bounds.min, bounds.max); const sliderMax = clampNumber(parseFilterNumber(draft.max) ?? bounds.max, bounds.min, bounds.max); const activeMin = Math.min(sliderMin, sliderMax); const activeMax = Math.max(sliderMin, sliderMax); const embedded = chrome === "embedded"; return /* @__PURE__ */ jsxs( "div", { "data-filter-panel-chrome": chrome, className: cn( "min-w-[18rem] max-w-[22rem] text-popover-foreground", embedded ? "p-0" : "rounded-md border border-border bg-popover p-3 shadow-sm shadow-black/5" ), children: [ !embedded && /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between gap-2", children: [ /* @__PURE__ */ jsx("div", { className: "text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: filter.label }), /* @__PURE__ */ jsx( "button", { type: "button", className: "text-[10px] text-primary disabled:text-muted-foreground", onClick: () => { setDraft({}); filter.onChange({}); }, disabled: !String(draft.min ?? "").trim() && !String(draft.max ?? "").trim(), children: "Clear all" } ) ] }), /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-[11px] text-muted-foreground", children: [ /* @__PURE__ */ jsx("span", { children: formatNumberValue(bounds.min, filter) }), /* @__PURE__ */ jsx("span", { children: formatNumberValue(bounds.max, filter) }) ] }), /* @__PURE__ */ jsx("div", { className: "relative h-6", children: /* @__PURE__ */ jsx( RangeSlider, { min: bounds.min, max: bounds.max, step: bounds.step, value: [activeMin, activeMax], ariaLabelMin: `${filter.label} minimum slider`, ariaLabelMax: `${filter.label} maximum slider`, onChange: ([nextMin, nextMax]) => setDraft(numberFilterValueFromSlider([nextMin, nextMax], bounds)) } ) }) ] }), /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [ /* @__PURE__ */ jsx("label", { className: "text-[10px] text-muted-foreground", children: "Min" }), /* @__PURE__ */ jsx( "input", { type: "number", inputMode: "decimal", step: bounds.step, "aria-label": `${filter.label} minimum`, className: "h-8 w-full rounded-md border border-input bg-background px-2 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring", placeholder: filter.minPlaceholder ?? formatNumberValue(bounds.min, filter), value: draft.min ?? "", onChange: (event) => setDraft( normalizeNumberFilterValue( { min: event.target.value, max: draft.max ?? "" }, "min-input" ) ) } ) ] }), /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [ /* @__PURE__ */ jsx("label", { className: "text-[10px] text-muted-foreground", children: "Max" }), /* @__PURE__ */ jsx( "input", { type: "number", inputMode: "decimal", step: bounds.step, "aria-label": `${filter.label} maximum`, className: "h-8 w-full rounded-md border border-input bg-background px-2 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring", placeholder: filter.maxPlaceholder ?? formatNumberValue(bounds.max, filter), value: draft.max ?? "", onChange: (event) => setDraft( normalizeNumberFilterValue( { min: draft.min ?? "", max: event.target.value }, "max-input" ) ) } ) ] }) ] }) ] }) ] } ); } function MultiFilterField({ filter, grow }) { const rootRef = useRef(null); const triggerRef = useRef(null); const [open, setOpen] = useState(false); const [optionQuery, setOptionQuery] = useState(""); const [draft, setDraft] = useDebouncedMultiDraft(filter.value, filter.onChange); useDismissablePopup(open, rootRef, triggerRef, () => setOpen(false)); const summary = summarizeMultiFilter(filter.label, draft); const showOptionFilter = filter.options.length > 7; const visibleOptions = useMemo(() => { const query = optionQuery.trim().toLowerCase(); if (!query) return filter.options; return filter.options.filter( (option) => multiSelectOptionText(option).toLowerCase().includes(query) ); }, [filter.options, optionQuery]); return /* @__PURE__ */ jsxs( "div", { ref: rootRef, title: filter.description, className: cn( "relative min-w-0", grow ? "min-w-[8rem] max-w-[12rem] flex-1" : "shrink-0", filter.disabled && "opacity-60", filter.className ), children: [ /* @__PURE__ */ jsxs( Button, { ref: triggerRef, type: "button", variant: "outline", size: "sm", "aria-label": `${filter.label} filter`, "aria-haspopup": "dialog", "aria-expanded": open, disabled: filter.disabled, onClick: () => setOpen((current) => !current), className: cn( "min-w-0 gap-2 font-normal", grow ? "w-full max-w-[12rem] justify-between" : "w-auto max-w-[8.5rem] px-2.5", summary === filter.label && "text-muted-foreground" ), children: [ /* @__PURE__ */ jsx("span", { className: "truncate", children: summary }), /* @__PURE__ */ jsx( Icon, { name: open ? "codicon:chevron-up" : "codicon:chevron-down", className: "text-muted-foreground" } ) ] } ), open && /* @__PURE__ */ jsxs("div", { className: "absolute left-0 top-[calc(100%+0.375rem)] z-50 min-w-[18rem] max-w-[22rem] rounded-md border border-border bg-popover p-2 text-popover-foreground shadow-lg shadow-black/5", children: [ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [ /* @__PURE__ */ jsx("div", { className: "text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: filter.label }), /* @__PURE__ */ jsx( "button", { type: "button", className: "text-[10px] text-primary disabled:text-muted-foreground", onClick: () => setDraft({}), disabled: Object.keys(draft).length === 0, children: "Clear all" } ) ] }), showOptionFilter && /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center gap-2 rounded-md border border-input bg-background px-2", children: [ /* @__PURE__ */ jsx(Icon, { name: "codicon:search", className: "shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsx( "input", { type: "search", "aria-label": `Filter ${filter.label} options`, className: "h-8 min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground", placeholder: `Filter ${filter.label.toLowerCase()}`, value: optionQuery, onChange: (event) => setOptionQuery(event.target.value) } ) ] }), /* @__PURE__ */ jsxs("div", { className: "max-h-72 space-y-0.5 overflow-auto", children: [ visibleOptions.map((option) => { const mode = draft[option.value] ?? "neutral"; const title = option.title ?? multiSelectOptionText(option); return /* @__PURE__ */ jsx( "div", { role: "button", tabIndex: 0, "data-filter-option": option.value, className: "rounded-md px-1.5 py-0.5 hover:bg-accent/50 focus-visible:bg-accent/50 focus-visible:outline-none", onClick: () => setDraft(updateMultiFilterValue(draft, option.value, nextFilterMode(mode))), onKeyDown: (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); setDraft(updateMultiFilterValue(draft, option.value, nextFilterMode(mode))); } }, children: /* @__PURE__ */ jsx( FilterPill, { className: "w-full justify-between", label: option.label, mode, title, togglePosition: "right", onModeChange: (next) => setDraft(updateMultiFilterValue(draft, option.value, next)) } ) }, option.value ); }), visibleOptions.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-3 text-sm text-muted-foreground", children: "No options found" }) ] }) ] }) ] } ); } function MultiFilterPanel({ filter, chrome = "full" }) { const [optionQuery, setOptionQuery] = useState(""); const [draft, setDraft] = useDebouncedMultiDraft(filter.value, filter.onChange); const showOptionFilter = filter.options.length > 7; const embedded = chrome === "embedded"; const visibleOptions = useMemo(() => { const query = optionQuery.trim().toLowerCase(); if (!query) return filter.options; return filter.options.filter( (option) => multiSelectOptionText(option).toLowerCase().includes(query) ); }, [filter.options, optionQuery]); return /* @__PURE__ */ jsxs( "div", { "data-filter-panel-chrome": chrome, className: cn( "min-w-[18rem] max-w-[22rem] text-popover-foreground", embedded ? "p-0" : "rounded-md border border-border bg-popover p-2 shadow-sm shadow-black/5" ), children: [ !embedded && /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [ /* @__PURE__ */ jsx("div", { className: "text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: filter.label }), /* @__PURE__ */ jsx( "button", { type: "button", className: "text-[10px] text-primary disabled:text-muted-foreground", onClick: () => setDraft({}), disabled: Object.keys(draft).length === 0, children: "Clear all" } ) ] }), showOptionFilter && /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center gap-2 rounded-md border border-input bg-background px-2", children: [ /* @__PURE__ */ jsx(Icon, { name: "codicon:search", className: "shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsx( "input", { type: "search", "aria-label": `Filter ${filter.label} options`, className: "h-8 min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground", placeholder: `Filter ${filter.label.toLowerCase()}`, value: optionQuery, onChange: (event) => setOptionQuery(event.target.value) } ) ] }), /* @__PURE__ */ jsxs("div", { className: "max-h-72 space-y-0.5 overflow-auto", children: [ visibleOptions.map((option) => { const mode = draft[option.value] ?? "neutral"; const title = option.title ?? multiSelectOptionText(option); return /* @__PURE__ */ jsx( "div", { role: "button", tabIndex: 0, "data-filter-option": option.value, className: "rounded-md px-1.5 py-0.5 hover:bg-accent/50 focus-visible:bg-accent/50 focus-visible:outline-none", onClick: () => setDraft(updateMultiFilterValue(draft, option.value, nextFilterMode(mode))), onKeyDown: (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); setDraft(updateMultiFilterValue(draft, option.value, nextFilterMode(mode))); } }, children: /* @__PURE__ */ jsx( FilterPill, { className: "w-full justify-between", label: option.label, mode, title, togglePosition: "right", onModeChange: (next) => setDraft(updateMultiFilterValue(draft, option.value, next)) } ) }, option.value ); }), visibleOptions.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-3 text-sm text-muted-foreground", children: "No options found" }) ] }) ] } ); } function NestedMultiFilterPanel({ filter, chrome = "full" }) { var _a; const [activeGroup, setActiveGroup] = useState(((_a = filter.groups[0]) == null ? void 0 : _a.groupKey) ?? null); const [draft, setDraft] = useDebouncedMultiDraft(filter.value, filter.onChange); const groups = filter.groups; const embedded = chrome === "embedded"; const activeGroupData = useMemo( () => groups.find((group) => group.groupKey === activeGroup) ?? groups[0] ?? null, [groups, activeGroup] ); const selectedByGroup = useMemo(() => { const counts = {}; for (const group of groups) { let n = 0; for (const option of group.options) { if (draft[option.value]) n += 1; } counts[group.groupKey] = n; } return counts; }, [groups, draft]); const sortedGroups = useMemo(() => { const selected = []; const rest = []; for (const group of groups) { if ((selectedByGroup[group.groupKey] ?? 0) > 0) selected.push(group); else rest.push(group); } return [...selected, ...rest]; }, [groups, selectedByGroup]); const clearGroup = (groupKey) => { const group = groups.find((g) => g.groupKey === groupKey); if (!group) return; const next = { ...draft }; for (const option of group.options) { delete next[option.value]; } setDraft(next); }; return /* @__PURE__ */ jsxs("div", { role: "dialog", className: "flex", children: [ /* @__PURE__ */ jsxs( "div", { "data-filter-panel-chrome": chrome, className: cn( "min-w-[14rem] max-w-[16rem] text-popover-foreground", embedded ? "p-0" : "rounded-md border border-border bg-popover p-2 shadow-sm shadow-black/5" ), children: [ !embedded && /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [ /* @__PURE__ */ jsx("div", { className: "text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: filter.label }), /* @__PURE__ */ jsx( "button", { type: "button", className: "text-[10px] text-primary disabled:text-muted-foreground", onClick: () => setDraft({}), disabled: Object.keys(draft).length === 0, children: "Clear all" } ) ] }), /* @__PURE__ */ jsxs("div", { className: "max-h-72 space-y-0.5 overflow-auto", children: [ sortedGroups.map((group) => { const selected = selectedByGroup[group.groupKey] ?? 0; const isActive = group.groupKey === activeGroup; return /* @__PURE__ */ jsxs( "div", { role: "button", tabIndex: 0, onMouseEnter: () => setActiveGroup(group.groupKey), onFocus: () => setActiveGroup(group.groupKey), onClick: () => setActiveGroup(group.groupKey), onKeyDown: (event) => { if (event.key === "Enter" || event.key === " " || event.key === "ArrowRight") { event.preventDefault(); setActiveGroup(group.groupKey); } }, className: cn( "flex cursor-pointer items-center justify-between gap-2 rounded-md px-1.5 py-0.5 text-sm", "hover:bg-accent/50 focus-visible:bg-accent/50 focus-visible:outline-none", isActive && "bg-accent/60" ), children: [ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: group.label ?? group.groupKey }), selected > 0 && /* @__PURE__ */ jsxs("span", { className: "rounded-full bg-primary/15 px-1.5 text-[10px] font-medium text-primary", children: [ selected, "/", group.options.length ] }), /* @__PURE__ */ jsx(Icon, { name: "codicon:chevron-right", className: "shrink-0 text-muted-foreground" }) ] }, group.groupKey ); }), groups.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-3 text-sm text-muted-foreground", children: "No groups" }) ] }) ] } ), activeGroupData && /* @__PURE__ */ jsxs( "div", { onMouseEnter: () => setActiveGroup(activeGroupData.groupKey), className: "ml-1.5 min-w-[16rem] max-w-[20rem] rounded-md border border-border bg-popover p-2 text-popover-foreground shadow-sm shadow-black/5", children: [ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [ /* @__PURE__ */ jsx("div", { className: "text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: activeGroupData.label ?? activeGroupData.groupKey }), /* @__PURE__ */ jsx( "button", { type: "button", className: "text-[10px] text-primary disabled:text-muted-foreground", onClick: () => clearGroup(activeGroupData.groupKey), disabled: (selectedByGroup[activeGroupData.groupKey] ?? 0) === 0, children: "Clear" } ) ] }), /* @__PURE__ */ jsxs("div", { className: "max-h-72 space-y-0.5 overflow-auto", children: [ activeGroupData.options.map((option) => { const mode = draft[option.value] ?? "neutral"; const title = option.title ?? multiSelectOptionText(option); return /* @__PURE__ */ jsx( "div", { role: "button", tabIndex: 0, "data-filter-option": option.value, className: "rounded-md px-1.5 py-0.5 hover:bg-accent/50 focus-visible:bg-accent/50 focus-visible:outline-none", onClick: () => setDraft(updateMultiFilterValue(draft, option.value, nextFilterMode(mode))), onKeyDown: (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); setDraft(updateMultiFilterValue(draft, option.value, nextFilterMode(mode))); } else if (event.key === "ArrowLeft") { event.preventDefault(); setActiveGroup(null); } }, children: /* @__PURE__ */ jsx( FilterPill, { className: "w-full justify-between", label: renderNestedOptionLabel(option, activeGroupData.groupKey), mode, title, togglePosition: "right", onModeChange: (next) => setDraft(updateMultiFilterValue(draft, option.value, next)) } ) }, option.value ); }), activeGroupData.options.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-3 text-sm text-muted-foreground", children: "No values" }) ] }) ] } ) ] }); } function multiSelectOptionText(option) { const label = typeof option.label === "string" ? option.label : ""; return [option.value, label, option.title ?? ""].filter(Boolean).join(" "); } function NestedMultiFilterField({ filter, grow }) { const rootRef = useRef(null); const triggerRef = useRef(null); const [open, setOpen] = useState(false); const [activeGroup, setActiveGroup] = useState(null); const [draft, setDraft] = useDebouncedMultiDraft(filter.value, filter.onChange); useDismissablePopup(open, rootRef, triggerRef, () => { setOpen(false); setActiveGroup(null); }); const summary = summarizeMultiFilter(filter.label, draft); const groups = filter.groups; const activeGroupData = useMemo( () => groups.find((group) => group.groupKey === activeGroup) ?? null, [groups, activeGroup] ); const selectedByGroup = useMemo(() => { const counts = {}; for (const group of groups) { let n = 0; for (const option of group.options) { if (draft[option.value]) n += 1; } counts[group.groupKey] = n; } return counts; }, [groups, draft]); const sortedGroups = useMemo(() => { const selected = []; const rest = []; for (const group of groups) { if ((selectedByGroup[group.groupKey] ?? 0) > 0) selected.push(group); else rest.push(group); } return [...selected, ...rest]; }, [groups, selectedByGroup]); const clearGroup = (groupKey) => { const group = groups.find((g) => g.groupKey === groupKey); if (!group) return; const next = { ...draft }; for (const option of group.options) { delete next[option.value]; } setDraft(next); }; return /* @__PURE__ */ jsxs( "div", { ref: rootRef, title: filter.description, className: cn( "relative min-w-0", grow ? "min-w-[8rem] max-w-[12rem] flex-1" : "shrink-0", filter.disabled && "opacity-60", filter.className ), children: [ /* @__PURE__ */ jsxs( Button, { ref: triggerRef, type: "button", variant: "outline", size: "sm", "aria-label": `${filter.label} filter`, "aria-haspopup": "dialog", "aria-expanded": open, disabled: filter.disabled, onClick: () => { setOpen((current) => !current); if (open) setActiveGroup(null); }, className: cn( "min-w-0 gap-2 font-normal", grow ? "w-full max-w-[12rem] justify-between" : "w-auto max-w-[8.5rem] px-2.5", summary === filter.label && "text-muted-foreground" ), children: [ /* @__PURE__ */ jsx("span", { className: "truncate", children: summary }), /* @__PURE__ */ jsx( Icon, { name: open ? "codicon:chevron-up" : "codicon:chevron-down", className: "text-muted-foreground" } ) ] } ), open && /* @__PURE__ */ jsxs( "div", { role: "dialog", onKeyDown: (event) => { if (event.key === "Escape") { event.preventDefault(); setOpen(false); setActiveGroup(null); } }, className: "absolute left-0 top-[calc(100%+0.375rem)] z-50 flex", children: [ /* @__PURE__ */ jsxs("div", { className: "min-w-[14rem] max-w-[16rem] rounded-md border border-border bg-popover p-2 text-popover-foreground shadow-lg shadow-black/5", children: [ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [ /* @__PURE__ */ jsx("div", { className: "text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: filter.label }), /* @__PURE__ */ jsx( "button", { type: "button", className: "text-[10px] text-primary disabled:text-muted-foreground", onClick: () => setDraft({}), disabled: Object.keys(draft).length === 0, children: "Clear all" } ) ] }), /* @__PURE__ */ jsxs("div", { className: "max-h-72 space-y-0.5 overflow-auto", children: [ sortedGroups.map((group) => { const selected = selectedByGroup[group.groupKey] ?? 0; const isActive = group.groupKey === activeGroup; return /* @__PURE__ */ jsxs( "div", { role: "button", tabIndex: 0, onMouseEnter: () => setActiveGroup(group.groupKey), onFocus: () => setActiveGroup(group.groupKey), onClick: () => setActiveGroup(group.groupKey), onKeyDown: (event) => { if (event.key === "Enter" || event.key === " " || event.key === "ArrowRight") { event.preventDefault(); setActiveGroup(group.groupKey); } }, className: cn( "flex cursor-pointer items-center justify-between gap-2 rounded-md px-1.5 py-0.5 text-sm", "hover:bg-accent/50 focus-visible:bg-accent/50 focus-visible:outline-none", isActive && "bg-accent/60" ), children: [ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: group.label ?? group.groupKey }), selected > 0 && /* @__PURE__ */ jsxs("span", { className: "rounded-full bg-primary/15 px-1.5 text-[10px] font-medium text-primary", children: [ selected,