Skip to content

feat: per-page command registration API for command palette - #1710

Merged
1nonlypiece merged 1 commit into
Commitlabs-Org:masterfrom
mikewheeleer:issue-918-command-palette-registration
Jul 30, 2026
Merged

feat: per-page command registration API for command palette#1710
1nonlypiece merged 1 commit into
Commitlabs-Org:masterfrom
mikewheeleer:issue-918-command-palette-registration

Conversation

@mikewheeleer

Copy link
Copy Markdown
Contributor

Summary

Context: as noted on #924/#968, this repo has a large amount of previously-deleted UI infrastructure from a prior incident. Neither CommandPalette nor CommandPaletteProvider exist currently -- both were among the deleted files (root layout.tsx already references <CommandPaletteProvider /> but doesn't compile, for unrelated reasons, same incident). Per maintainer guidance, building both fresh rather than resurrecting the old implementation, with the registration API this issue actually asks for from day one rather than a hard-coded list.

What this adds

  • CommandPaletteProvider -- holds open/close state, a small static navigation command set, and a registry of dynamically-registered commands. Merges both into the commands array exposed via context (deduped by id, dynamic registrations win on collision). Global Cmd/Ctrl+K toggles the palette.
  • useRegisterCommands(commands) -- registers on mount, unregisters on unmount.
  • CommandPalette -- the searchable list UI, built on the existing Dialog primitive (focus trap, Escape-to-close, inert background). Keyboard-navigable via the aria-activedescendant pattern (Up/Down/Enter on the input, no need to tab into the list); mouse click is a secondary affordance.
  • docs/COMMAND_REGISTRATION.md -- API reference, usage example, accessibility notes.

A real bug found and fixed while building this

My first pass made useRegisterCommands's effect depend on the commands array reference (the "obvious" design). Writing this PR's own tests, I hit an actual out-of-memory crash: a component that both calls useRegisterCommands([command]) (a fresh array literal each render) and reads commands back from useCommandPalette() enters an infinite loop -- register → state update → re-render → new array reference → re-register → ... This is an easy trap for real page code to fall into (e.g. a page showing "N commands available" alongside registering its own).

Fixed by re-registering only when the set of ids changes (a stable primitive key derived from sorted ids), not on the array reference. A non-memoized array is now safe to pass every render. Documented the trade-off (in-place content mutation on an unchanged id won't take effect -- give the command a new id, or memoize with useMemo, if that's needed) in the hook's docstring and in the doc file.

Testing

  • useRegisterCommands.test.tsx -- 8 tests: static defaults present, register-on-mount merges with static set, unregister-on-unmount, id-based dedupe (via direct registerCommands calls), multiple simultaneous registrants, manual registerCommands returns a scoped unregister fn, open/close/toggle, throws outside a provider.
  • CommandPalette.test.tsx -- 6 tests: closed renders nothing, lists static commands when open, filters by query, "no matches" state, Enter runs the highlighted command and closes, click runs a command.
  • All 14 new tests pass; confirmed via git stash A/B comparison that this diff adds zero new build errors and zero new test failures elsewhere (baseline: 22 failed test files / 37 failed tests / 488 passed, unchanged except my +14 passing).

Closes #918

Neither CommandPalette nor CommandPaletteProvider exist in this repo
currently (both were among the ~2,500 files deleted in a prior
incident; root layout.tsx already references <CommandPaletteProvider />
but doesn't compile for unrelated reasons). Building both fresh, with
the registration API this issue actually asks for, rather than a
hard-coded command list.

- CommandPaletteProvider: holds open/close state, a small static
  navigation command set, and a registry of dynamically-registered
  commands. Merges both (deduped by id, dynamic wins on collision)
  into the commands exposed via context. Global Cmd/Ctrl+K toggles it.
- useRegisterCommands(commands): registers on mount, unregisters on
  unmount. Re-registers only when the *set of ids* changes (not on
  every render) so passing a non-memoized array each render is safe --
  this avoids a register -> state update -> re-render -> re-register
  loop that an id-agnostic (array-reference-based) design hits in
  practice (found via an actual OOM crash while writing this PR's own
  tests). Trade-off documented in the docstring and in
  docs/COMMAND_REGISTRATION.md.
- CommandPalette: the searchable list UI, built on the existing Dialog
  primitive. Keyboard-navigable via aria-activedescendant (Up/Down/
  Enter on the input); mouse click as a secondary affordance.

Closes Commitlabs-Org#918
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

@mikewheeleer is attempting to deploy a commit to the 1nonly's projects Team on Vercel.

A member of the Team first needs to authorize it.

1nonlypiece pushed a commit that referenced this pull request Jul 30, 2026
Adds an accessible breadcrumb trail derived from the current route's
path segments, for nested routes like /commitments/[id] -- there's no
AppShellLayout to mount this in globally yet (see #921/#922's sibling
PRs #1710/#1711 for the same constraint), so it's a small standalone
component wired into the one real nested route in the app.

- Breadcrumbs derives its trail from usePathname(), hides itself on
  top-level routes (0-1 segments) where a trail adds no value, renders
  non-final segments as real <Link>s (keyboard-focusable in document
  order for free) and the final segment as aria-current="page" (never
  a link, never duplicates the page H1).
- currentLabel lets a page resolve its trailing id segment to a
  friendly label (e.g. "Balanced Commitment"); falls back to a
  truncated id for long opaque segments or a title-cased label for
  short/hyphenated ones.
- Wired into the commitment detail page, passing the commitment's type
  as the friendly label.

Closes #922
@1nonlypiece
1nonlypiece merged commit a0f134f into Commitlabs-Org:master Jul 30, 2026
2 of 12 checks passed
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.

Add a context-aware command registration API to CommandPaletteProvider for per-page actions

2 participants