feat(filter-builder): the group header is the ALL/ANY switch

The group header ("MATCH"/"GROUP") was a dead label while the engine
honored group modes all along — buildGroupClause parenthesizes nested
groups and joins each by its own mode, and validation accepted
all/any from day one. The only thing missing was the UI: + Group
birthed ALL groups and no control anywhere could flip one, so the
user's target structures (GROUP(GROUP(... or ...) ...) — 3-level
mixed-mode nesting) were constructible only through the invisible
row-level chips, and standalone ANY-groups not at all (2026-11-01
user report: "MATCH cannot be clicked").

- The header is now the control: MATCH ALL ⇄ MATCH ANY (root — a
  synthetic header row, since the root was never projected and its
  mode was invisible too), GROUP ALL ⇄ GROUP ANY (nested). ANY is
  accented; aria-pressed carries the state. Labels are path-based
  (root reads Match, explicit groups read Group) instead of
  depth-based, which previously mislabeled top-level groups.
- The row-level chips are untouched — they mix AND/OR inside a group
  (A AND (B OR C)), which a global mode toggle cannot express; the
  header is the discoverable entry point, the chips the refinement.
- Dropdown audit (the "would it bug out" check): suggestTree is
  mode-generic — root-ANY drops group siblings, sibling ANY-groups
  survive into the constraint with their mode intact, empty groups
  never emit (no 400s) — now pinned. Deep-nesting Go pin: a 3-level
  mixed-mode tree evaluated through composed search + the suggestion
  endpoint (200, never the empty-group 400). 192 web tests green.
This commit is contained in:
John O'Keefe committed 2026-11-01 15:43:52 -05:00
1 parent 5fb80f82ed
commit 2344f5686a
7 files changed
+254 -5

No files matched your search

+28
View File
@@ -2018,6 +2018,34 @@
padding: 0.2rem 0.25rem;
outline: none;
}
/* group header = the ALL/ANY switch (2026-11-01): the header was a
dead label while the engine honored group modes — now it's the
control. ANY is accented: it's the mode that changes semantics. */
.fb-group-mode {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
padding: 0.15rem 0.5rem;
border-radius: 0.4rem;
border: 1px solid var(--border);
background-color: var(--bg-primary);
color: var(--text-secondary);
cursor: pointer;
white-space: nowrap;
}
.fb-group-mode:hover {
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
color: var(--text-primary);
}
.fb-group-mode.fb-group-any {
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
background-color: var(--accent-muted);
color: var(--accent);
}
.fb-menu {
position: absolute;
z-index: 60;