From 3ae68022a2705b42bc6e06332838068271247599 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 5 Sep 2026 18:12:20 -0700 Subject: [PATCH 01/27] =?UTF-8?q?docs:=20spec=20for=20components=20SP2=20?= =?UTF-8?q?=E2=80=94=20Listbox,=20and=20PretableSelect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A select-only combobox on the SP1 contract, built on a Listbox primitive extracted from the enum cell editor so the grid has one list implementation; the four native selects migrate, and their test drivers with them. Co-Authored-By: Claude Fable 5.1 --- ...2026-09-05-components-sp2-select-design.md | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-05-components-sp2-select-design.md diff --git a/docs/superpowers/specs/2026-09-05-components-sp2-select-design.md b/docs/superpowers/specs/2026-09-05-components-sp2-select-design.md new file mode 100644 index 00000000..65134fec --- /dev/null +++ b/docs/superpowers/specs/2026-09-05-components-sp2-select-design.md @@ -0,0 +1,242 @@ +# Components SP2: Listbox, and PretableSelect + +Date: 2026-09-05 +Status: approved, ready for planning + +## Purpose + +The second sub-project of the component kit (program and contract: +`2026-09-04-components-sp1-button-design.md`). Four native `` and not a + pointer-type hybrid: a kit-styled `button[role="combobox"]` trigger and a + portalled `role="listbox"` popover — the WAI-ARIA select-only pattern, and + the shape the enum cell editor already uses. Both halves are styleable; the + grid owns the keyboard and ARIA contract; existing test drivers migrate. +2. **Extract a shared `Listbox` from the enum cell editor; migrate the four + selects only.** The editor keeps its filtering `` trigger and renders + the same list. The group panel's chip listbox is out of scope. +3. **Data-driven options**: `options: readonly { value: string; label: +ReactNode; disabled?: boolean }[]`, `value: string`, `onChange(value)`. No + children API, no generic value type, no `renderOption` until a site needs it. +4. **Primitive shape**: a rendered `Listbox` component (the portalled list + both consumers render identically) plus a `useListboxKeys` hook (the + keyboard both triggers need). Not hook-only (two copies of the ARIA markup), + not a component that also owns the trigger (the two triggers differ). + +Inherited from SP1 without re-deciding: placement in +`packages/react/src/components/`; one `components` slot per type; the +placement prop is `site`; attributes + tokens + `className`/`style` passthrough +as the styling channel; kit base rules first in `grid.css` and state rules +last; every migrated site keeps its original `data-pretable-*` attribute; the +pixel must not move. + +## The primitive + +### `Listbox` (internal, `packages/react/src/components/listbox.tsx`) + +One responsibility: render a portalled list of options from data the trigger +owns. + +```ts +export interface ListboxOption { + readonly value: string; + readonly label: ReactNode; + readonly disabled?: boolean; +} + +interface ListboxProps { + /** The trigger's `aria-controls` target; option ids are `${id}-${index}`. */ + id: string; + options: readonly ListboxOption[]; + /** `aria-selected` — the committed value, or null. */ + value: string | null; + /** The roving highlight; -1 for none. Owned by the trigger via the hook. */ + activeIndex: number; + /** The trigger's rect; placed by `menuPopoverStyle`. */ + anchor: DOMRect; + "aria-label"?: string; + onSelect: (value: string) => void; + /** Outside pointerdown. No focus return: the click chose a new target. */ + onClose: () => void; +} +``` + +- Renders `