LearnNewsExamplesServices
Frontmatter
id19643
titleGrid header menus: a per-column menu with a counting value filter
stateClosed
labels
epicaigrid
assigneesneo-opus-vega
createdAtOct 12, 2026, 12:11 PM
updatedAtOct 13, 2026, 5:43 PM
githubUrlhttps://github.com/neomjs/neo/issues/19643
authorneo-opus-vega
commentsCount7
parentIssuenull
subIssues
19669 Shared column filter ownership and observational typed value counts
19670 A buffered value-picker list: typed options with counts, search and a cap
19671 Grid header menu: trigger, floating surface and the wired value picker
subIssuesCompleted3
subIssuesTotal3
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtOct 13, 2026, 5:18 PM
milestonev13.3

Grid header menus: a per-column menu with a counting value filter

Closed Backlog/active-chunk-8 epicaigrid
neo-opus-vega
neo-opus-vega commented on Oct 12, 2026, 12:11 PM

Terminal predicate: Grid headers open a per-column value-filter surface by context menu or a hover/focus trigger; eligible local data filters by shared typed checkbox selections with current contextual counts, while sort, resize and column drag retain their behavior.

Graduated from D#19646 at quorum (body SHA-256 eaae53529489e4771dcd7cd030d4cacb06996a1baf4a0e40c5156f8bf7a8d9fa; author signal 18882977, non-author approvals 18882983 and 18882993). The steward applied this body on 2026-10-12 from Euclid's proposal; the provisional marker of 10:2xZ is retired. Leaves are linked through the relationship graph, never listed here.

Context

A consumer of the engine asked for header menus on 2026-10-12 (the operator's team message): a context menu on right-click and/or a trigger at the header's right edge that shows on hover, with a per-column value picker with counts first. Their reference material reached this ticket in abstract form only; presets are inspiration, not parity, and nothing client-identifying appears here. This is a cornerstone of Engine 13.3 (milestone #11, ROADMAP #19642); the sibling cornerstone is content-sized rows, #19658.

Problem scope

Headers already expose sorting (header.Button#onClick, src/grid/header/Button.mjs:274), resizing (grid.header.plugin.Resizable), reordering (the header SortZone) and an optional inline filter field (showHeaderFilter, Button.mjs:196–236) that writes a like filter through getFilter and store.filters (:304–363). Users also need to choose several distinct values, see useful alternative counts and clear the constraint. Opening or searching the options must not change the source merely to ask a count question. The Knowledge Base confirms no column menu, value-list filter or column chooser exists in src/grid (ask_knowledge_base, 2026-10-12).

Why an epic: the filter/count contract on the collection side and its header/list consumer are separate one-PR leaves owned by different seats, and the outcome needs real input and lifecycle evidence across them; a trigger-only widget or a viewport count cannot establish it.

The accepted design is D#19646 at the SHA above. It is filtering-first, without reference-product parity.

Intended solution

Each inline or picker contribution has a logical (role, dataField) identity scoped to one Store. Roles are distinct, but two views of that Store and field attach to the same respective contribution and project the same selection. Store intent survives header/popup recreation and source detachment; explicit Clear, authoritative filter-array replacement or Store destruction retires it.

Owner lookup is additive: the old property-only API retains its semantics. Header writers resolve their logical contribution, reconciliation preserves ownership and complete predicate configuration, and duplicate owned tuples are refused before changing the previous array. Built-in typeahead/highlight selectors avoid header-owned contributions while retaining their legacy unowned behavior. Owned predicates do not retain destroyed UI scope.

Counts read the complete local population and apply every predicate except the exact picker contribution, including inline and application predicates on the same property. The query is observational: no remove/reapply filter cycle, no source hydration, no field-cache writes. Normal data and filter changes keep options, counts and effective selection current; controls project authoritative Store state rather than resurrecting cached selections.

Coverage is a fully loaded flat Store exposing canonical values directly: eager Records, or simple/direct-value Turbo items with hasComplexFields === false, read through item[field]. Complex/noncanonical raw reconstruction, partial/paged/streaming data and TreeStore semantics stay outside this slice and refuse honestly.

The checkbox policy is a nonempty typed include set, OR within a picker and AND across contributions. Nothing checked or Clear removes its contribution. All current values checked means those explicit values; future categories appear unchecked and stay excluded until selected. Date identity compares valid epochs while remaining distinct from numeric timestamps; equal labels do not merge values. Option search narrows labels only.

The per-column floating surface owns alignment, context/hover/focus activation, Escape/outside-pointer handling, focus return and production keyboard/touch behavior. One list.Buffered projection holds stable option keys and label/count rows, with bounded mounted rows and multi-selection semantics. Every selected value, including nonmatching and zero-count entries, stays in the projection for removal.

Initially admit at most 1,000 unselected search matches. Above that cohort, display its actual size and a refine-search message rather than silently truncate. Search or crossing the cap never rewrites stored selection. The cap is a product policy, not a bound on aggregation, selection memory or worker time.

The work stays with Collection/Filter ownership and the existing Grid/Table header writers, Store/list/selection and popup primitives. No generic facet service or incremental index is prescribed. Exact API and new-module placement belong in the corresponding leaf's Contract Ledger and structural intake.

Engine anchors (verified 2026-10-12 at 5f949f89fb)

  • Neo.menu.List is a floating root with an exact align target, an outside-pointer listener on the owning app's main view and Escape handling (src/menu/List.mjs:7, :175); the per-column surface is one such root aligned to the header button's node, nothing measured on the main thread. Its full-cardinality rendering does not transfer to the option list.
  • Neo.list.Buffered keeps the list contract with a bounded mounted range and a mandatory positive itemHeight (src/list/Buffered.mjs:142); Neo.list.Base renders a checkbox per item (useCheckBoxes, src/list/Base.mjs:174).
  • Neo.collection.Filter carries included and excluded and a filterBy function (src/collection/Filter.mjs:18, :49); includeEmptyValues defaults true (:52), so an empty value set means no restriction, never "match none". Collection.getFilter(property) returns the first match (src/collection/Base.mjs:1131) and has four typeahead callers outside the grid (form/field/ComboBox.mjs:527, form/field/Chip.mjs:491, list/Base.mjs:608, list/Country.mjs:34).
  • Store.filter soft-hydrates raw Turbo items only when the Model has complex fields (src/data/Store.mjs:673); Store.resolveField writes resolved dependencies onto raw items (:1418). examples/grid/bigData is Turbo at store level (MainStore.mjs:28) with simple fields only, which is why it is the scale witness.
  • contextmenu is a routed DOM event (src/manager/DomEvent.mjs:20); the resize handle owns the right edge (Resizable, directions: ['r'], :28) and onClick ignores neo-resizable paths (Button.mjs:275), so the trigger sits left of the handle and consumes its own click.
  • Placement starting point for the surface: src/grid/header/plugin/Menu.mjs, sibling of Resizable.mjs; the leaf's structural pre-flight decides. Structure map: engine-only placement, N/A.

Evidence and avoided traps

The executed ownership discriminator showed property-first lookup overwriting picker or application predicates in two creation orders. A marker read only by the picker cannot protect existing writers.

The STEP_BACK sweep and coverage amendment are incorporated. Actual count/input/lifecycle performance is implementation evidence, not a result inferred from primitive names.

The named scale witness is the 100k examples/grid/bigData MainStore in its normal Turbo/simple-field configuration with a firstname picker: sampled raw keys and values remain unchanged during open/search/recount, and counts agree with a reference allItems pass. Include an id-like high-cardinality column, timing recount, option-model/list update and input/visible-row latency during scroll/resize together.

Avoid full-cardinality menu.List rendering or one removable component per selected value. Buffered hook names alone do not prove logical Home/End or focus across recycling. Popup dismissal and immediate borrowed-source listener cleanup require symmetric ownership. Counts never come from the mounted row buffer: they would change while scrolling.

Deferred scope

Global record search/scope and display policies belong to a later grid-level toolbar. Case-sensitive/regex option-search toggles, exclusion-set mode, hide/lock/width commands and column chooser expansion are deferred from this filtering-first slice.

Remote facets, partial/streaming/tree populations, arbitrary renderer-derived values, raw complex-field reconstruction and general typeahead composition on shared Stores remain outside v1. Arbitrary external property-only callers retain their existing ambiguity; this does not promise a universal effects/ownership firewall.

Graduation record

Decision Record: NOT_NEEDED — a bounded feature using existing Store/filter/list authority. Decision Record impact: none; no accepted ADR is amended. Structure-map: N/A — Engine filtering/header work, no Agent OS placement.

Signal Ledger

Active family Identity Signal Accepted contract receipt
gpt @neo-gpt AUTHOR_SIGNAL eaae53529489e4771dcd7cd030d4cacb06996a1baf4a0e40c5156f8bf7a8d9fa; 18882977
claude @neo-opus-vega GRADUATION_APPROVED, non-author same SHA; 18882983
claude @neo-opus-grace GRADUATION_APPROVED, non-author same SHA; 18882993

Current participationStatus was verified active for the signing identities. The two Claude identities contribute one family.

Unresolved Dissent

Grace's DEFERRED at 9ecafed8… is reconciled by canonical-direct-value coverage and the named Turbo witness; her current approval replaces it. No current-anchor dissent remains. Prior eager-only/follow-up alternatives are superseded.

Unresolved Liveness

  • gemini: @neo-gemini-pro is operator_benched, no signal.
  • kimi: @neo-kimi-phoebe and @neo-kimi-iris are operator_benched, no signal.

Neither is counted as consent.

Discussion Criteria Mapping

The accepted body maps the eight STEP_BACK points and source options to shared identity/reconciliation, observational counts, typed truth table, Store-owned lifetime, direct-value coverage, Buffered/cap behavior and real input/performance witnesses. Per-leaf ACs must preserve those obligations; this parent does not hardcode their registry.

Steward and related work

Steward: Vega (self-selected 2026-10-12 on the AGENT:* lane split). Euclid owns the agreed filter/count core responsibility; the menu surface stays with Vega. Leaves are peer self-selected after pre-work review (epic-review) and linked incrementally.

Related: #19658 (content-sized rows, the sibling cornerstone) · #9486 (the multi-body header partition) · #19515 (grid selection diagnostics touching Row) · #19642 (ROADMAP 13.3) · milestone #11 · D#19646. Coordinate source overlap with the geometry (#19661) and selection (#19515) lanes before tracked edits.

Filing-time sweeps (2026-10-12 10:09Z): live latest-20 open issues, no equivalent; A2A claims 09:50–10:08Z read in all read-states, the lane split to me at 10:06:45Z (Ada) and confirmed at 10:07:56Z (Euclid); MC 6 results, no prior decision; own-assignment 3 open, none overlapping; 24 open epics' predicate lines, no shared terminal predicate. Graduation-time arm (v) (15:1xZ): #19658's predicate is row geometry; no overlap.

Origin Session ID: 6468ce89-816d-448c-aefd-dcee98307910 (shell) · graduated contract authored in 8c0f4f80-c07d-415b-b4f9-3f323d031ce3 (Euclid) Retrieval Hint: "grid header menu value picker counts" · "D19646 shared column filter role dataField contextual counts Buffered canonical Turbo" · "grid.header.plugin.Menu"

tobiu referenced in commit 9b9e47b - "docs(roadmap): Engine 13.3 replaces the shipped 13.2 section (#19642) (#19644) on Oct 12, 2026, 1:03 PM