Skip to content

fresh-ui: no command-palette primitive (⌘K); Combobox is not sufficient #1689

Description

@rickylabs

Summary

@netscript/fresh-ui ships no command-palette primitive. A ⌘K surface is now
standard furniture in this class of app, and building one correctly is more
subtle than it looks — I have just done it by hand in eis-chat and would
rather contribute it back than have the next app repeat the work.

What is missing

Combobox is close but not sufficient. It is a headless roving-keys input
over a listbox, which gives you the input and the keyboard, but a command
palette additionally needs:

  • grouped results with headings, where arrow keys traverse the flattened
    order across group boundaries;
  • filtering and ranking owned by the consumerCombobox does not filter,
    which is correct, but every consumer then reimplements the same fuzzy scorer;
  • a virtual active row via aria-activedescendant with rows as
    role="option" that are not tab stops, so the surface has exactly one
    focusable element;
  • mousemove-driven hover rather than mouseenter, or the keyboard
    highlight is stolen whenever the list scrolls under a stationary pointer;
  • an empty state that is a peer of the groups rather than a listbox child;
  • Escape as a two-stage retreat (clear the query, then close).

Composing Dialog + Combobox also currently inherits #1688.

Proposed shape

A headless useCommandPalette plus a thin Command.* namespace mirroring the
parts that have become conventional (Root, Input, List, Empty, Group,
Item, Separator, Shortcut), with:

  • items: readonly {id, group}[] in display order, and the hook owning
    activeIndex, wrap-around, Home/End/PageUp/PageDown, and
    scrollIntoView({block: 'nearest'});
  • getInputProps() emitting role="combobox", aria-expanded,
    aria-controls, aria-activedescendant, aria-autocomplete="list";
  • getItemProps(index) emitting role="option", aria-selected, the option
    id, and the mousemove handler;
  • ranking left to the consumer, with an optional exported subsequence scorer.

Prior art to copy from

eis-chat's implementation, which is tested and measured:
https://github.com/rickylabs/eis-chat/pull/255

  • lib/commands/score.ts — fuzzy subsequence scorer (DP over query x text)
    with word-start / acronym / consecutive-run bonuses and gap penalties,
    returning match ranges for highlighting. 10 unit tests.
  • lib/commands/search.ts — grouping, per-group browse caps, group ordering by
    best member, and a "breadcrumb only when it is not an echo of its own group
    heading" rule.
  • lib/commands/registry.ts — provider registry so features contribute
    commands without editing the palette.
  • components/ui/command-palette.tsx — the a11y wiring described above.

Happy to raise a PR extracting the headless half if the shape looks right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions