The filter sidebar, its controls, sorting and the criteria they produce.
The filter block turns a list of field definitions into a working filter sidebar, a sort picker and a quick search, and hands you back the criteria the user chose. It depends on no other block.
Filter the eight products below by name, category, price or whether they are for sale, and sort by name or price. Your choices go into the page's URL, so the state survives a reload and a shared link carries it.
A surface declares the complete set of filters up front, as data:
const filters: FilterDefs = [
{ name: "name", type: "text", label: "Name" },
{
name: "category",
type: "multiselect",
label: "Category",
items: categories,
},
{
name: "price",
type: "number",
control: "slider",
label: "Price",
min: 0,
max: 10000,
},
{ name: "active", type: "boolean", label: "For sale" },
];The whole array has to exist before the first render, because it is what the block deserialises the URL against and what it hydrates the initial state from. A registry that collected filters as their components mounted would learn the set too late, and the page would flicker through an empty state on every load.
type is the kind of data, and it decides the operators and how a value is compared. control decides the input, and defaults from the type — so a number is a pair of boxes unless you ask for a slider, and a multi-select is a checklist unless you ask for a combobox. An empty items hides its field, which is how a storefront's brand filter disappears on a catalogue that has no brands, without the page testing for it.
A label is looked up as a copy path and falls through to itself when it is not one, so a label that only exists at runtime — a category name out of your database — passes straight through.
useFiltersContext() returns the state and the actions. The state carries the active filters, the sort list, the search text and its debounced twin; the actions add, update, remove and clear. Two matchers, doesPassFilters and doesPassSearch, apply the same semantics in the browser that a server query would apply in the database, so a small list can be filtered where it stands and a large one can send the criteria on to be paginated.
FilterSidebar is the panel, FilterFields renders one collapsible row per definition, and FilterSidebarTrigger opens and closes it. SortMenu and SortToggle drive the sort list. The individual controls — text, select, async select, number, slider, date, switch, toggle — carry no chrome of their own, so the sidebar row and the pill bar are made of the same ones. Whether a control takes one value or several is read from the filter's type, never passed in.
FilterPersistenceProvider decides that. It writes to the URL, so a filtered view is a link you can send someone, and optionally to the device as well, under the entity name you put above it. When the state grows too large for a URL, it stays on the device and tells the user. The URL half is not namespaced, so one page carries one URL-persisted surface.