Skip to content

feat: hierarchical command palette - #185

Draft
seankmartin wants to merge 34 commits into
masterfrom
feat/command-palette
Draft

feat: hierarchical command palette#185
seankmartin wants to merge 34 commits into
masterfrom
feat/command-palette

Conversation

@seankmartin

Copy link
Copy Markdown
image

seankmartin and others added 30 commits June 7, 2026 18:28
pattern now follows default viewer_setup binding
also fixes the lifetime and binding locations to be more consistent with the default
viewer setup and the input event bindings to help panel
Also removes doc level palette key listener, this was designed for when inside a number
element for e.g. but not worth
also explicitly labels the command type as opposed to infer from optional properties
Extract the catalog — CommandCatalog, CommandCatalogContext,
collectActionBindings, the CommandPaletteEntry types, and the tool/label
helpers — from command_palette.ts into a new command_catalog.ts with no DOM
or CSS dependencies. command_palette.ts keeps the Overlay-based CommandPalette
UI and bindCommandPalette, importing the catalog and the stylesheet.

Previously, importing CommandCatalog for its enumeration transitively pulled
in command_palette.css and the Overlay class even when no palette was
rendered. Splitting the modules lets the catalog be consumed (and unit-tested)
without a DOM, and reused independently of the palette UI.

- command_catalog.spec.ts (renamed from command_palette.spec.ts) now imports
  from command_catalog.js, so the catalog tests no longer depend on the
  palette module.
- default_viewer_setup.ts imports CommandCatalog from command_catalog.js and
  bindCommandPalette from command_palette.js.

No behavioural change.
refactor: split CommandCatalog into a DOM-free command_catalog module
Grouping is a presentation concern: the command palette and the help
panel would reasonably group the same commands in different ways, so the
section a command belongs to belongs to whoever is presenting it.

A suggested binding on the command was only ever informational, and would
drift from whatever binding is actually installed. The shortcut a
consumer shows now always comes from the live input event bindings.
RenderedDataPanel registers its per-axis action listeners by iterating
AXES_NAMES, so declaring the matching commands from a second local list
of axis names left two places to keep in step. Import the same constant
and fold the move and rotate generators into one pass over it.
Behaviour had nowhere to live on a plain data record, so each consumer
re-derived it: the palette built the `action:<id>` CustomEvent itself,
and the catalog translated the registry's `type` discriminant into its
own `kind` discriminant to decide which branch to take.

A Command now owns its id, label, optional description and how it runs.
ActionCommand dispatches the DOM action, CallbackCommand runs a callback,
and both take a CommandContext rather than a bare target so that more
context (mouse position, originating layer) can be added later without
touching every implementation.

The registry stores instances and forwards each command's `changed`
signal, which replaces the per-command WatchableValue subscription that
backed the old `isAvailable`; that property is now a settable `enabled`
on the command itself. The catalog's ActionCommandEntry and CommandEntry
collapse into a single entry carrying the Command.
The registry lists the commands it was told about, and there is no way
to make that list complete: a viewer embedded in another application, or
driven from the Python integration, can bind an action without ever
registering a command for it. Enumerating only the registry dropped
those from the palette, which the previous catalog did show.

The catalog now enumerates the registry first, so a registered command
keeps its curated label and description, then adds an ActionCommand for
each keyboard-bound action the registry does not know, labelled from its
action id as before. Tool slots and layer-index actions stay excluded;
the catalog contributes its own entries for those.
The command ids in default_commands.ts have to match the action ids the
default input event bindings dispatch, and nothing checked that. A typo
in either direction is silent: a command whose id no action listens for
does nothing when invoked, and a bound action with no command loses its
label and description.

Assert both directions against the real binding maps, with the tool
slots and layer-index actions excluded as dynamic, and the three actions
that have no default binding listed explicitly.
Describe what each piece owns: a Command holds identity, presentation
and behaviour; the registry holds which commands exist; the catalog
turns that plus viewer state into an ordered list with shortcuts
attached; the palette renders it. Records why the registry cannot be
treated as the complete list of commands, and how a change flows from a
registration through to a re-render.
feat: Add CommandRegistry and default command descriptions.
also binds to that command palette in the default setup
The command catalog is now constructed by the viewer, so
default_viewer_setup no longer references CommandCatalog directly.
The leftover import fails lint:check with no-unused-vars.
fix: remove unused CommandCatalog import
Instead they are directly CallbackCommands
instead store all command actions flat in a catalog,
and leave the consumer (currently only palette)
to group commands based on the old grouping
information
since they can be used standalone
Existing signals for refreshing are kept, because there is interest in the catalog being
used outside of the palette - and because although unlikely, in theory a command could
change while the palette is open.

Rebuilding when opening is another way to help ensure the catalog is up to date in case
something was missed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants