@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
JavaScript
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,