Surface leaf of #19643 (graduated from D#19646 at eaae5352…; pre-work review 6127438609). The surface splits into two one-PR leaves: this list component, and the header menu plugin that hosts it (the sibling leaf, filed next and blocked by this one). The list is separable because it is unit-testable in isolation over an options Store and has a second consumer, src/table/header/Button.mjs, so it lives in src/list/, not under src/grid/. It consumes no API from the core leaf #19669: the host maps the core's {key, value, count} buckets into this list's options records.
The Problem
A column's distinct values can number in the thousands (bigData's numberN columns hold about 10,001 each; an id-like column one per record). list.Base renders one li per record (createItems, src/list/Base.mjs:623–645) and menu.List inherits that, so neither can host the picker; the accepted contract rejects full-cardinality rendering and one component per value. The picker must show checkbox rows with a label and a contextual count, keep every selected value visible and removable even at count 0 or when it no longer matches the search, narrow the options by a search string without touching the selection, admit at most 1,000 unselected search matches and say so honestly above that, and keep keyboard navigation logical across the buffered range.
The Architectural Reality
Neo.list.Buffered (src/list/Buffered.mjs) keeps the list contract (ul/li, ListModel, Navigator, Store) with pooled slots and a mandatory positive itemHeight (:142); createItemContent (:407–416) falls back to list.Base's text hook when no itemConfig is set, which is the row-content hook the contract names ("no component-per-value layer"). Focus follows logical record identity across recycling (:16–20).
Neo.list.Base adds a checkbox per item with useCheckBoxes (:174) and renders item text in createItemContent (:602); highlightFilterValue reads the list's own Store filter (:608), which is the options Store here, never the grid's.
Content-specialised list siblings: src/list/Chip.mjs, Color.mjs, Country.mjs; SCSS siblings in resources/scss/src/list/; unit siblings in test/playwright/unit/list/ (Buffered.spec.mjs).
The accepted contract (epic body, Cardinality and presentation decision): one list.Buffered projection, stable typed option keys, positive fixed item height, bounded viewport, multi-selection checkbox semantics, a 1,000-unselected-match cap with an honest refine message, selected values retained in the same projection, search and the cap never rewrite stored selection.
The options Store is owned by the host with a fresh Model (epic body: "Option records live in an owned Store with a fresh Model"); the source Store is never bound to this list.
Pre-Flight (structural fast-path): authoring src/list/ValuePicker.mjs matches the sibling pattern of src/list/Chip.mjs and src/list/Color.mjs in src/list/ (content-specialised list subclasses) on the src/list/Buffered.mjs base; sibling-file-lift applies; no novel directory choice. src/grid/header/ was considered and rejected because src/table/header/Button.mjs is the second consumer. Map-maintenance: not needed (another instance of an existing role).
The Fix
New Neo.list.ValuePicker extends Neo.list.Buffered:
store: the host-owned options Store; Model fields key (typed identity, the Store's keyProperty), value (the typed field value), label (display string), count (Number).
selection_ (reactive): the set of selected keys, a projection the host writes from the authoritative Store contribution; the list never mutates it on its own, it only reports toggles through selectionChange.
searchValue_ (reactive): narrows the visible options through one owned filterBy over selected keys and capped case-insensitive literal label matches on the options Store; selected options stay visible regardless of the match; the selection is untouched.
maxMatchingOptions_ (reactive, default 1000): the cap on unselected matching options admitted into the projection; above it the list shows the actual cohort size and a refine-search row instead of truncating silently; selected values never count against it.
Row content through the existing hook: checkbox (from useCheckBoxes), label, count with tabular digits; count 0 rendered as such, never hidden.
selectionChange event with {added, removed, selection} on a checkbox toggle, Space or Enter on the focused row; the host turns it into a filter write.
itemHeight default set in the subclass; Home/End and arrow keys act on logical records across the buffer, not on mounted slots.
SCSS resources/scss/src/list/ValuePicker.scss; unit spec test/playwright/unit/list/ValuePicker.spec.mjs (sibling Buffered.spec.mjs).
Contract Ledger
Target surface
Source of authority
Delivered behavior
Fallback
Docs
Evidence
Neo.list.ValuePicker
src/list/Buffered.mjs
Fixed-height plain-vdom option pool, visual checkbox, label and contextual count; host owns options Store
none (new class)
class JSDoc; host leaf owns the Grids guide
unit + component specs
selection_
D#19646: controls project authoritative Store state
Host-written unique keys; external writes repaint without an event; no self-selecting model
empty array
config JSDoc
external projection, pointer and key controls
searchValue_
D#19646 option-search contract
Case-insensitive literal label substring through the owned filterBy; selected keys exempt; safe text-node highlighting; selection unchanged
empty string admits capped unselected options plus selected
config JSDoc
literal metacharacters, selected nonmatches and clear controls
maxMatchingOptions_
D#19646 cardinality decision
Exactly N unselected matches plus every selected key; actual matching cohort in a non-record refine message
1000
config JSDoc
above/at/below cap, selected zero-count and footer extent
Owned options predicate
collection.Filter contribution identity and Store.filters
property: 'label', ownerRole: 'value-picker:<id>'; selected union first N matching unselected keys. Authoritative filter replacement reinstates this predicate through the next filter refresh, preserving host filters. Replacement/destroy releases only the exact owned instance
absent predicate is rebuilt from the current unfiltered options
class/refresh/release JSDoc
filter replacement, source replacement and teardown controls
Options record edits
Store.recordChange and Buffered's mounted-row writer
Count-only changes repaint mounted rows without admission scans; other edits coalesce per task, cancelled on source replacement/destroy
unknown change shape schedules one scan
callback JSDoc
500-count zero-scan and 500-label one-scan burst; queued-lifetime controls
selectionChange
D#19646 host authority
{added, removed, selection} toggle intent for pointer, Space and Enter; host must echo authoritative state
no change on external projection writes
event JSDoc
pointer/key intent and echo controls
Options fields key, value, label, count
D#19646 host-owned fresh Model/Store
Typed opaque key identity; label/count are display facts, never selection identity or recomputed source counts
missing label uses String(value)
class JSDoc
equal-label distinct keys, explicit zero, arrivals and literal text
Navigator.subscribe.workerKeys
actual Main key handler plus worker KeyNavigation
Opt-in exact native key names cancel browser default and skip mounted-DOM handling, while worker delivery continues
omitted: existing Navigator keys
subscription JSDoc
actual Home/End and cross-pool Arrow input; no synthetic Enter click
Navigator.subscribe.scrollIntoView
existing Navigator focus effects
Opt-in false suppresses both direct and focus-handler scroll calls when the worker owns logical scrolling
omitted/true: existing smooth scroll
subscription JSDoc
zero picker scrollIntoView calls; existing menu controls
neonavigate.activeRecordId
active DOM item's captured data-record-id
Navigation carries the key captured at emission; a record change on the same physical active slot also fires. This applies to all Buffered subscribers with record data; picker rejects stale slot/key pairs
no record data: existing element-identity firing
Navigator event/source JSDoc
recycled-identity unit control and real keyboard path
List accessibility
W3C APG listbox pattern
Multi-select listbox; option rows use aria-selected only. Spacers/footer are presentational; overflow footer is exposed by aria-describedby and removed below cap
generic Values label, host may supply root labeling
class/root JSDoc
unit roles/state and browser role/description checks
Design read (for the operator, before the PR)
A row reads left to right: checkbox, the value's label, then the count right-aligned in tabular digits and a muted colour. Selected rows look like any other row with a checked box; a selected value with count 0 keeps its row and shows 0. When the search narrows the options, unselected non-matches disappear and selected values stay in place. Above the cap, the last row is a non-selectable line in the muted colour: "1,240 values match, narrow the search". No Apply button: every toggle is immediate. Keyboard: arrows move, Space toggles, Home/End jump to the first and last logical option.
Acceptance Criteria
AC1: Neo.list.ValuePicker renders an options Store of 10,001 records with a bounded number of mounted rows (the Buffered pool), each row showing checkbox, label and count through the row-content hook; no component instance per option.
AC2: selection is a projection: setting it externally updates the checked rows without firing selectionChange; toggling a row fires selectionChange with {added, removed, selection} and leaves the selection config unchanged until the host writes it back.
AC3: searchValue narrows unselected options by case-insensitive substring on label, keeps every selected option visible, and never changes selection; clearing the search restores the admitted options.
AC4: With more than maxMatchingOptions unselected matches, the list admits exactly that many, shows the actual cohort size in a refine row, and keeps all selected options; at or under the cap no refine row renders.
AC5: A selected option whose count drops to 0, or whose label no longer matches the search, stays rendered and removable; a newly arriving option renders unchecked with its count.
AC6: Space and Enter toggle the focused row; ArrowDown, ArrowUp, Home and End move focus by logical record across the buffer boundary (the witness asserts the focused record id, not the slot).
AC7: The unit spec test/playwright/unit/list/ValuePicker.spec.mjs covers AC1–AC6 and runs green in the unit tier; itemHeight has a subclass default.
AC8: No change in src/grid, src/collection or src/data; the options Store is the only Store the list touches.
Out of Scope
Counting and filter ownership (#19669); the floating surface, the trigger, the search TextField UI, the Clear action and the wiring to the Store contribution (the host leaf); case-sensitive or regex search; an exclusion-set mode; the bigData journey and latency witness (host leaf).
Avoided Traps
Rendering through menu.List: full-cardinality li rendering, and its dismissal and selection behaviour do not transfer by swapping modules.
One component per value via itemConfig: the contract names the row-content hook.
Label identity: options are keyed by the typed key; equal labels never merge.
Applying the cap to selected values, or letting the search or the cap rewrite the selection.
Binding the grid's Store to the list: the list only ever sees the host-owned options Store.
Related
Parent #19643 · core leaf #19669 (Euclid; not a dependency of this list) · host leaf (filed next, blocked by this one) · source D#19646 · siblings src/list/Chip.mjs, test/playwright/unit/list/Buffered.spec.mjs.
Decision Record: Not needed, as classified by D#19646.
Decision Record impact: none.
Structure-map: N/A — Engine src/list/, fast path above.
Sweeps (2026-10-12 15:38Z): live latest 20 open issues (#19669 is the core leaf; no list component equivalent); A2A last 30 in all read-states (Euclid's #19669 claim is the complementary core; no surface claim); MC rationale sweep on the problem's nouns (6 results, no prior decision beyond D#19646); own-assignment sweep (4 open, none overlapping).
Context
Surface leaf of #19643 (graduated from D#19646 at
eaae5352…; pre-work review 6127438609). The surface splits into two one-PR leaves: this list component, and the header menu plugin that hosts it (the sibling leaf, filed next and blocked by this one). The list is separable because it is unit-testable in isolation over an options Store and has a second consumer,src/table/header/Button.mjs, so it lives insrc/list/, not undersrc/grid/. It consumes no API from the core leaf #19669: the host maps the core's{key, value, count}buckets into this list's options records.The Problem
A column's distinct values can number in the thousands (bigData's
numberNcolumns hold about 10,001 each; an id-like column one per record).list.Baserenders oneliper record (createItems,src/list/Base.mjs:623–645) andmenu.Listinherits that, so neither can host the picker; the accepted contract rejects full-cardinality rendering and one component per value. The picker must show checkbox rows with a label and a contextual count, keep every selected value visible and removable even at count 0 or when it no longer matches the search, narrow the options by a search string without touching the selection, admit at most 1,000 unselected search matches and say so honestly above that, and keep keyboard navigation logical across the buffered range.The Architectural Reality
Neo.list.Buffered(src/list/Buffered.mjs) keeps the list contract (ul/li, ListModel, Navigator, Store) with pooled slots and a mandatory positiveitemHeight(:142);createItemContent(:407–416) falls back tolist.Base's text hook when noitemConfigis set, which is the row-content hook the contract names ("no component-per-value layer"). Focus follows logical record identity across recycling (:16–20).Neo.list.Baseadds a checkbox per item withuseCheckBoxes(:174) and renders item text increateItemContent(:602);highlightFilterValuereads the list's own Store filter (:608), which is the options Store here, never the grid's.src/list/Chip.mjs,Color.mjs,Country.mjs; SCSS siblings inresources/scss/src/list/; unit siblings intest/playwright/unit/list/(Buffered.spec.mjs).list.Bufferedprojection, stable typed option keys, positive fixed item height, bounded viewport, multi-selection checkbox semantics, a 1,000-unselected-match cap with an honest refine message, selected values retained in the same projection, search and the cap never rewrite stored selection.Pre-Flight (structural fast-path): authoring
src/list/ValuePicker.mjsmatches the sibling pattern ofsrc/list/Chip.mjsandsrc/list/Color.mjsinsrc/list/(content-specialised list subclasses) on thesrc/list/Buffered.mjsbase; sibling-file-lift applies; no novel directory choice.src/grid/header/was considered and rejected becausesrc/table/header/Button.mjsis the second consumer. Map-maintenance: not needed (another instance of an existing role).The Fix
New
Neo.list.ValuePickerextendsNeo.list.Buffered:store: the host-owned options Store; Model fieldskey(typed identity, the Store'skeyProperty),value(the typed field value),label(display string),count(Number).selection_(reactive): the set of selected keys, a projection the host writes from the authoritative Store contribution; the list never mutates it on its own, it only reports toggles throughselectionChange.searchValue_(reactive): narrows the visible options through one ownedfilterByover selected keys and capped case-insensitive literal label matches on the options Store; selected options stay visible regardless of the match; the selection is untouched.maxMatchingOptions_(reactive, default 1000): the cap on unselected matching options admitted into the projection; above it the list shows the actual cohort size and a refine-search row instead of truncating silently; selected values never count against it.useCheckBoxes), label, count with tabular digits; count 0 rendered as such, never hidden.selectionChangeevent with{added, removed, selection}on a checkbox toggle, Space or Enter on the focused row; the host turns it into a filter write.itemHeightdefault set in the subclass; Home/End and arrow keys act on logical records across the buffer, not on mounted slots.resources/scss/src/list/ValuePicker.scss; unit spectest/playwright/unit/list/ValuePicker.spec.mjs(siblingBuffered.spec.mjs).Contract Ledger
Neo.list.ValuePickersrc/list/Buffered.mjsselection_searchValue_filterBy; selected keys exempt; safe text-node highlighting; selection unchangedmaxMatchingOptions_collection.Filtercontribution identity andStore.filtersproperty: 'label',ownerRole: 'value-picker:<id>'; selected union first N matching unselected keys. Authoritative filter replacement reinstates this predicate through the next filter refresh, preserving host filters. Replacement/destroy releases only the exact owned instanceStore.recordChangeand Buffered's mounted-row writerselectionChange{added, removed, selection}toggle intent for pointer, Space and Enter; host must echo authoritative statekey,value,label,countString(value)Navigator.subscribe.workerKeysKeyNavigationNavigator.subscribe.scrollIntoViewneonavigate.activeRecordIddata-record-idlistbox;optionrows usearia-selectedonly. Spacers/footer are presentational; overflow footer is exposed byaria-describedbyand removed below capValueslabel, host may supply root labelingDesign read (for the operator, before the PR)
A row reads left to right: checkbox, the value's label, then the count right-aligned in tabular digits and a muted colour. Selected rows look like any other row with a checked box; a selected value with count 0 keeps its row and shows
0. When the search narrows the options, unselected non-matches disappear and selected values stay in place. Above the cap, the last row is a non-selectable line in the muted colour: "1,240 values match, narrow the search". No Apply button: every toggle is immediate. Keyboard: arrows move, Space toggles, Home/End jump to the first and last logical option.Acceptance Criteria
Neo.list.ValuePickerrenders an options Store of 10,001 records with a bounded number of mounted rows (the Buffered pool), each row showing checkbox, label and count through the row-content hook; no component instance per option.selectionis a projection: setting it externally updates the checked rows without firingselectionChange; toggling a row firesselectionChangewith{added, removed, selection}and leaves theselectionconfig unchanged until the host writes it back.searchValuenarrows unselected options by case-insensitive substring onlabel, keeps every selected option visible, and never changesselection; clearing the search restores the admitted options.maxMatchingOptionsunselected matches, the list admits exactly that many, shows the actual cohort size in a refine row, and keeps all selected options; at or under the cap no refine row renders.test/playwright/unit/list/ValuePicker.spec.mjscovers AC1–AC6 and runs green in the unit tier;itemHeighthas a subclass default.src/grid,src/collectionorsrc/data; the options Store is the only Store the list touches.Out of Scope
Counting and filter ownership (#19669); the floating surface, the trigger, the search TextField UI, the Clear action and the wiring to the Store contribution (the host leaf); case-sensitive or regex search; an exclusion-set mode; the bigData journey and latency witness (host leaf).
Avoided Traps
menu.List: full-cardinalitylirendering, and its dismissal and selection behaviour do not transfer by swapping modules.itemConfig: the contract names the row-content hook.key; equal labels never merge.Related
Parent #19643 · core leaf #19669 (Euclid; not a dependency of this list) · host leaf (filed next, blocked by this one) · source D#19646 · siblings
src/list/Chip.mjs,test/playwright/unit/list/Buffered.spec.mjs.Decision Record: Not needed, as classified by D#19646. Decision Record impact: none. Structure-map: N/A — Engine
src/list/, fast path above.Sweeps (2026-10-12 15:38Z): live latest 20 open issues (#19669 is the core leaf; no list component equivalent); A2A last 30 in all read-states (Euclid's #19669 claim is the complementary core; no surface claim); MC rationale sweep on the problem's nouns (6 results, no prior decision beyond D#19646); own-assignment sweep (4 open, none overlapping).
Origin Session ID: a9e3a190-156a-4fdb-9ea0-a554fa796d87 Retrieval Hint: "list.ValuePicker buffered checkbox options counts search cap" · "D19646 Buffered projection 1000 unselected matches"