Documents / Decisions / adr-6afdbace7a45
The index filter is chip-displayed state with a token grammar
Why the published index shows applied filters as removable chips, accepts tracker-style tokens (type:x, is:stale, -status:y), records each facet step in history, and dims zero-count options instead of dropping selections.
Context¶
adr-a343140d72e2 gave the published index faceted filters: type/status/updated
checkbox popovers with live counts, OR-within/AND-across semantics, and the
combined state mirrored into the URL. A research pass against that bar
(2026-08-05; Baymard/NN/g faceted-search research plus the filter models of
GitHub Issues, Linear, Jira, Datadog and Pagefind) found five gaps, each a
documented failure mode rather than a taste call:
- Applied state was invisible. The facet summaries showed counts ("type · 2"), the pattern Baymard names as the anti-pattern: the reader must reopen every popover to learn what is filtering the list. Removable chips above the results are the ~72% desktop convention.
- Narrowed options vanished, and selections were silently dropped. The status facet hid options the type selection excluded and unchecked a selected status that became unavailable — the "where did it go?" confusion, solved in the wild by dimming at count zero and never dropping a selection (its chip stays visible as the cause of an empty list).
- Back ignored filtering. Every change wrote
replaceState, but users perceive each facet change as a view; Baymard measures 27% of sites mishandling exactly this. A step must be a history entry; a keystroke not. - Rows displayed facts the filter could not reach. Owner and staleness
were visible on rows and views but not filterable — and the review queue
(
query --stale) had no shareable URL equivalent on the site. - Zero results dead-ended. No recovery action, the failure Baymard finds on 68% of no-results pages.
Tracker muscle memory was the second input: GitHub's dropdowns write
key:value tokens into one input, Linear renders filters as operator-editable
chips. Engineers try that grammar in any filter box.
Decision¶
One state object with a single mutation door per fact, displayed as chips:
- Chips are the canonical display. Every applied filter — facet value, exclusion, stale flag, date window — is a removable chip above the list, in the order applied; "remove last filter" pops that order.
- The text box doubles as a token bar.
type:x,status:x,owner:x,is:stale,updated:30dand-negations convert to chips, but only when the value exists in the corpus; anything else stays free text and searches as words. The GUI and the grammar share one state, so neither can drift. - Facets follow the corpus. The owner facet renders only when a document
has an owner; the stale toggle only when something is stale; the stale
banner links to
?is=stale, which makes the review queue a copyable URL. - Zero-count options ghost. Dimmed and disabled, never removed; a selection is never silently dropped.
- Each facet step is
pushState; typing isreplaceState. Back undoes filtering. Load, Back/Forward and preset views all funnel through one params reader that drops unknown values instead of filtering to zero. - Preset views at browsing scale. Corpora big enough for the recent strip
get one-click views (all / stale / open issues / updated·7d), each carrying
its target query string verbatim (
data-sig), so a preset lights up when the reader assembles the same state by hand. - Zero results offer recovery: remove the last filter, or clear all.
The URL schema is extended backward-compatibly: existing type=, status=,
updated=, from=/to=, q= links keep restoring; owner=, is=stale
and --prefixed exclusion values are new.
Consequences¶
- A behaviour change, not just an addition: a selected status that stops matching now stays selected (ghosted, chip visible) where it used to be dropped. The old rationale — "filtering to zero with no visible cause" — is answered by the chips row plus the recovery buttons.
- The filter script roughly doubles, but remains dependency-free, inline and
offline-complete; nothing about
adr-a343140d72e2's self-containment moves. - The engine was designed against the portal redesign concept (concept 1 in
design-concepts/) and is intended to carry over unchanged when that skin lands; this ADR covers the behaviour, not the visual redesign. - Pinned by markup tests in
tests/modules/publishing/test_site_rendering.py(chips, tokens, ghosting, pushState, conditional facets, presets, recovery); the interactive behaviour was verified in a browser against docir's own 106-document corpus.