Filter Panel
alphav0.2.0Schema-driven filter panel — checkbox lists, toggles, text, and custom filter types with AND composition and debounced input.
Context
Tier 1 pro-component for the graph-system. Generic over the item type; the host supplies items, categories, predicates, and decides what to do with the filtered output. AND-across-categories with OR-or-AND-within-category controlled by the host's predicate. Pairs with the graph in force-graph v0.4 for the groups/filter panel; useful standalone wherever a faceted-filter sidebar is needed.
Installation
pnpm dlx shadcn@latest init"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}pnpm dlx shadcn@latest add @ilinxa/filter-panelAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/filter-panel-fixturesPreview
- Rina Okaforpersonsecurityauth
- Marc Bernalpersonfrontend
- Acme Corporgcustomertier-1
- Verda Holdingsorgcustomer
- auth-v2 migrationprojectsecurityauthtier-1
- registry publishprojectfrontendops
- v2-runbook.mddocsecurityauth
- design-tokens.mddocfrontend
Demo source
Usage
When to use
Reach for FilterPanel when you have a list of typed items and want a stacked filter panel — checkbox lists, toggles, debounced text, custom range pickers — composing AND-across categories. Generic over the item type; the host owns items, predicates, and what to do with the filtered output. Sync-only predicates in v0.1; async slot is a v0.2 additive prop.
Basic example
import {
FilterPanel,
type FilterCategory,
} from "@/components/filter-panel";
interface Project {
id: string;
name: string;
status: "todo" | "in-progress" | "done";
tags: string[];
}
const CATEGORIES: ReadonlyArray<FilterCategory<Project>> = [
{
id: "status",
type: "checkbox-list",
label: "Status",
options: [
{ value: "todo", label: "To do" },
{ value: "in-progress", label: "In progress" },
{ value: "done", label: "Done" },
],
predicate: (item, value) => {
const sel = (value as string[]) ?? [];
return sel.length === 0 || sel.includes(item.status);
},
},
{
id: "search",
type: "text",
label: "Search",
placeholder: "Filter by name…",
predicate: (item, value) =>
typeof value !== "string" || value.length === 0 ||
item.name.toLowerCase().includes(value.toLowerCase()),
},
];
export function ProjectFilters({ projects }: { projects: Project[] }) {
const [values, setValues] = useState<Record<string, unknown>>({});
return (
<FilterPanel<Project>
items={projects}
categories={CATEGORIES}
values={values}
onChange={setValues}
onFilteredChange={setFilteredProjects}
/>
);
}Filter types
checkbox-list— multi-select. OptionalmodeTogglefor Union / Intersection. OptionalshowSoloButtonsfor per-row solo affordance.toggle— boolean switch.isEmptyrequired (host intent governs — typically(v) => v !== true).text— debounced input.debounceMsdefaults to 250. ESC clears the field.custom— escape hatch.render(props)receives{ value, onChange, items, fieldId };isEmptyrequired.
Composition semantics
AND-across categories: every active category's predicate must return true. Within a category, the host decides OR vs AND inside predicate. The mode-toggle affordance for checkbox-list is a hint stored at values["${id}__mode"] (reserved suffix); your predicate reads it and switches.
Empty detection
A category is "empty" when its isEmpty(value) returns true; empty categories are skipped during filtering. Defaults: checkbox-list = empty array; text = empty string. Required for toggle and custom because only the host knows the value shape.
Imperative handle
const ref = useRef<FilterPanelHandle>(null);
// ...
ref.current?.clearAll();
ref.current?.clear("status");
ref.current?.isEmpty(); // true iff every category is emptyCategories reference stability
Inline categories={[...]} rebuilds category objects on every parent render and re-runs the filter pipeline. In-repo, the React Compiler memoizes inline literals at the call site. For NPM consumers without the Compiler, hoist to module scope or wrap with useMemo.
const CATEGORIES: FilterCategory<Item>[] = [/* ... */];
// or
const categories = useMemo(() => buildCategories(...), [deps]);onFilteredChange semantics
Fires when the filtered array's reference changes — "may have changed", not "definitely differs". Same items + same values + same categories → no fire. Cost-conscious hosts dedupe via shallow-equal-by-id on their side.
Reserved id suffixes
__mode is reserved for internal mode storage on checkbox-list categories with modeToggle: true. Schema validation (dev-only) flags category ids ending in reserved suffixes, duplicate ids, and checkbox-list categories with empty options.
What ships in v0.2+
- Built-in
range/date-rangefilter types. - Per-category collapsibles (
collapsible+defaultExpanded). - Horizontal layout via
directionprop. - Async predicate support.
- Deep-equal change detection for
onFilteredChange.
Features
- Four built-in filter types — checkbox-list, toggle, text, custom
- AND-across-categories composition; host-defined within-category semantics
- Mode toggle (Union / Intersection) on checkbox-list via a plain-button segmented control
- Per-option solo button with tooltip on checkbox-list
- Debounced text input (default 250ms) with flush-on-blur, ESC-clears
- Custom render slot with error boundary and label-association via fieldId
- Per-category clear button + global clear-all in footer
- Dev-only schema validation — reserved suffix, duplicate id, empty options
- Categories-reference-instability dev warning (>5 successive unstable renders)
- Imperative handle — clearAll / clear / isEmpty