picker

picker3d — choosing one of many. A select is right for five options and a scroll for five hundred; this is the other end. It composes controls that already exist — a table for the virtualized, filterable list, an inputField for the query, a select3d for the group — so there is no new machinery here, only the arrangement.

Demo

Five hundred and sixty-one names, in the shape a real model library has them: family_thing-variant. Type to narrow, or pick a family first.

import { panel3d, label3d, picker3d } from 'tosijs-3d'
import { elements } from 'tosijs'

const { div } = elements
const readout = div({ class: 'readout' }, 'nothing picked')

// Stand-in for a library catalogue — same naming shape, generated so the demo
// carries no 561-entry literal.
const FAMILIES = ['commercial', 'residential', 'roads', 'car', 'nature', 'furniture', 'industry', 'signage', 'debris']
const KINDS = ['building', 'block', 'tower', 'bend', 'straight', 'corner', 'crossing', 'barrier', 'lamp', 'bench', 'crate']
const options = []
for (const f of FAMILIES) {
  for (const k of KINDS) {
    for (const v of ['a', 'b', 'c', 'd', 'e']) {
      options.push({ value: `${f}_${k}-${v}`, label: `${k}-${v}`, group: f })
    }
  }
}

preview.append(
  panel3d({ width: 320 },
    label3d({ text: `library — ${options.length} models` }),
    picker3d({
      label: 'model',
      value: options[0].value,
      options,
      handleChange: (v) => { readout.textContent = `picked: ${v}` },
    })
  ),
  readout
)
.preview { padding: 12px; }
.readout {
  padding: 10px 2px;
  font-family: ui-monospace, monospace;
  color: #9aa4b2;
}

Why not a bigger popup

tosijs-3d-ensemble put it exactly, having landed 561 Kenney models into a palette built on select3d:

The original item asked for "a real popup select" because select3d is a cycler. With 561 entries that understates it: even a flat popup is a scroll, not a choice.

A list you scroll is a list you read. The fix is not more pixels, it is fewer candidates — so the popup's job is to get from five hundred to five before you look at anything.

Two levels, without a tree

Their content's own names give a natural taxonomy — commercial_building-a, roads_road-bend-barrier, car_debris-bolt — and the obvious response is a tree widget. This does it with a group chooser and a filter field instead, which reaches the same place with two controls you already know how to drive.

That is not only a simplicity argument. A tree needs a pointer to expand and collapse; two flat controls need a D-pad and a text field, both of which work from a controller in a headset. The tree would have been the version that only works flat.

Filtering is AND across terms, over the whole name

Typing commercial bend matches commercial_road-bend-a, because every space-separated term must appear somewhere in the group, the label or the value. One term over a concatenated string would make word ORDER matter, which is the thing nobody can guess: an author who thinks "the bend, in commercial" would type it that way and find nothing.

Selection survives filtering — that is table's rule, and it matters most here: a search box that discards what you had chosen is destructive rather than helpful.

It asks for a LAYER, and copes with being refused

A five-hundred-row list is bigger than most panels, and showPopup caps to the panel's own bounds — which on a short panel gives a list squeezed to nothing, placed over the control it belongs to. So this asks for showLayer (unbounded, above the panel) and falls back to a bounded popup sized to what is actually available. Same reasoning as the on-screen keyboard, and the same fallback.

Options

option
options string[], or {value, label?, group?}[]
value '' the current selection
label drawn to the left of the closed control
filter 'auto' 'auto' shows the query field above filterAbove options
filterAbove 12 where 'auto' starts showing it
groups 'auto' 'auto' shows the group chooser when there is more than one group
placeholder 'choose…' shown when nothing is selected
handleChange the chosen value; fires on pick, and the popup closes