Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

30 KiB
Raw Permalink Blame History

PRD — Shared Keyboard Shortcuts System

  • Slug: 2026-05-18-keyboard-shortcuts-shared-system
  • Date: 2026-05-18
  • Status: Draft
  • Source: .ai/keyboard-shortcuts-shared-system-plan.md

Summary

Introduce a shared, client-side keyboard shortcut system for the MSP app. Features register stable semantic actions (e.g. page.save, global.search) rather than component-specific key handlers; the system resolves which key combination invokes which action, with scope/priority arbitration, user customization, a settings UI, a discoverable help dialog, and visible kbd hints. The system is correct and identical on macOS and Windows/Linux, including for a user who configures shortcuts on one OS and signs in from another. All user-facing text is internationalized in the app's eight production locales plus pseudo-locales.

Delivery covers all six phases of the source plan: engine, global migration, panels/drawers/record nav, editors/designers, user customization, and discoverability/accessibility — including the g-then-key sequence engine and visible hints.

Problem

Keyboard handling is scattered across ad hoc window/document listeners:

  • DefaultLayout.tsx (mod+l, mod+ArrowUp), SearchPalette.tsx (mod+k), AssetDashboardClient.tsx (mod+k — collides with global search), two DrawerContext files (Escape, Alt+Arrow), TicketNavigation.tsx (Alt+Arrow), useDesignerShortcuts.ts.
  • Conflicts are accidental (mod+k today). Editable-target filtering is inconsistent. There is no scope/priority model, no user customization, no central list of shortcuts. Alt+Arrow collides with browser Back/Forward on Windows/Linux. No matcher correctness across OS/keyboard layouts.

Goals

  • One shared registry + single delegated dispatcher under packages/ui.
  • Stable action IDs; bindings configurable and decoupled from handlers.
  • Scope (global/shell/page/panel/dialog/editor) + priority + active region arbitration; drawer/dialog/editor win over page over global.
  • Cross-platform correctness: dual matching (event.code for physical keys, event.key for character keys), mod resolved per device, per-platform defaults, international-layout safety.
  • Single-chord and multi-key sequence bindings (g then t).
  • User customization persisted as platform-neutral deltas via useUserPreference; localStorage + debounced server sync; validated per target platform on load.
  • Settings UI consistent with the rest of the app (settings tab shell, shared Card/Table/Switch/Button/ConfirmationDialog, react-hot-toast, handleError, LoadingIndicator).
  • Discoverable help dialog + aria-keyshortcuts + visible kbd hints.
  • Fully internationalized in en, fr, es, de, nl, it, pl, pt (+ xx/yy pseudo); passes validate-translations.cjs.
  • All existing AI/search/drawer/ticket/designer shortcuts keep working after migration.

Non-goals

  • Auth and client-portal screens (MSP /msp only for v1).
  • Tenant-level shortcut defaults / admin lock (kept as future open question).
  • Export/import of user override sets.
  • Replacing browser-owned combos we explicitly choose not to own (mod+r/reload, mod+f/find, mod+p/print, mod+w/mod+t/mod+n).
  • Migrating component-local widget key handling (DatePicker, SearchableSelect, TagInput, comboboxes, Radix internals) — these keep their local handlers; the system must not steal their keys.
  • Server-side keyboard logic (system is client-only).

Users and Primary Flows

Personas: Internal MSP technician (power user, keyboard-driven); occasional MSP user (discovers shortcuts via help/hints); accessibility user (screen reader

  • aria-keyshortcuts); cross-device user (configures on Mac laptop, uses Windows desktop).

Primary flows:

  1. Use a default shortcut. User presses mod+k; global search focuses. The resolved combo shows in the help dialog and as a kbd hint on the control.
  2. Discover shortcuts. User presses ? (or opens it from a menu) → read-only help dialog lists active shortcuts grouped by group/scope, resolved for the current OS, custom bindings flagged.
  3. Sequence navigation. User presses g then t in a non-editable page region → routes to Tickets; the chord buffer times out if the second key is late.
  4. Customize. Settings → Keyboard Shortcuts tab: list of actions with action label, group, current effective binding, default; user rebinds via key capture, clears a custom binding, disables an action, resets one, resets all. Conflicts are detected (platform-aware) before save.
  5. Scope arbitration. A detail drawer is open over a ticket list; [/] move between adjacent records (drawer/panel scope), not list rows; Escape closes the drawer and integrates with Radix, not a competing listener.
  6. Cross-device. User rebinds global.search to mod+j on macOS; signs in on Windows; it resolves to Ctrl+J automatically (neutral mod+j delta). A binding hostile on the new OS surfaces a non-blocking advisory.

UX / UI Notes

Settings panel lives as a new tab in the existing settings shell (server/src/components/settings/SettingsPage.tsx), added to baseTabContent with { id: 'keyboard-shortcuts', label: t(...), icon: KeyboardIcon, content }, wrapped in the standard Card/CardHeader/CardContent. Component: server/src/components/settings/general/KeyboardShortcutsSettings.tsx.

  • Layout mirrors existing list-settings panels (e.g. ExperimentalFeaturesSettings.tsx, GeneralSettings.tsx): a Table (@alga-psa/ui/components/Table) grouped by action group, columns: Action (label + description), Scope, Default, Effective binding, Enabled (Switch), Reset (Button variant="ghost" size="sm" with RotateCcw).
  • Rebind uses a capture affordance (button → "press keys" inline capture) that records event.code for code-kind and event.key for char-kind, matching the matcher. Conflicts shown inline before commit; replacing an existing binding requires explicit confirm (ConfirmationDialog).
  • "Reset all" uses ConfirmationDialog (destructive intent).
  • Buttons use standard variants (default primary, outline secondary, ghost icon, destructive reset-all). Every interactive element has an id.
  • Feedback: react-hot-toast success + handleError(error, t(...)) on failure, consistent with other settings panels. LoadingIndicator while loading.
  • Saving model: immediate, debounced via useUserPreference (no explicit Save button), consistent with preference-backed panels; UnsavedChangesProvider not required since there is no staged form.

Help dialog uses the shared Dialog (@alga-psa/ui/components/Dialog), read-only, grouped sections, bindings rendered via a shared <Kbd> component that displays platform glyphs (⌘/⌥/⇧/⌃ on macOS; Ctrl/Alt/Shift on others), resolved after mount to avoid hydration mismatch.

Visible hints: a shared <Kbd> / ShortcutHint component renders next to key actions (e.g. search field, primary buttons, menu items) and in tooltips; controls also get aria-keyshortcuts with a value derived from the effective binding via a dedicated mapping (its format differs from the internal syntax).

i18n: new namespace msp/keyboard-shortcuts for action labels, group names, help dialog, and settings panel chrome; settings-tab label added to msp/settings. All keys added to en first, then fr/es/de/nl/it/pl/pt; pseudo xx/yy regenerated; ROUTE_NAMESPACES updated so the namespace preloads for /msp. No raw user-facing strings; action labels are i18n keys passed at registration (labelKey/groupKey).

Requirements

Functional Requirements

Engine (Phase 1)

  • FR1. parseBinding(str) parses modifiers + a key token, classifying the token as code (letters/digits/named keys/brackets) or char (glyphs like ?). Supports literal ctrl/meta (not remapped) plus mod.
  • FR2. parseSequence(str) parses space-separated chords into an ordered sequence (e.g. "g t"), each element a single chord from FR1.
  • FR3. matchEvent(event, descriptor, platform) dual matches: code-kind → event.code + exact modifier set; char-kind → event.key produced char (Shift implied by glyph), with mod/alt still required if specified.
  • FR4. mod resolves to Meta on macOS, Ctrl elsewhere, at runtime per device. Platform detected client-only (post-mount), never during SSR.
  • FR5. Provider installs exactly one capture-phase keydown listener on document. Registration hooks only mutate the registry.
  • FR6. useShortcutAction(def) registers/unregisters an action on mount/unmount; def carries id, labelKey, groupKey, defaultBindings: { mac, other }, scope, priority, enabled, allowInEditable, handler, optional sequence flag.
  • FR7. useShortcutScope(scope) pushes/pops a ref-counted active scope; cleared on route change.
  • FR8. Dispatch: skip if event.defaultPrevented; match enabled actions; filter by active scopes, editable-target rule, and active-region rule for selection/single-key actions; pick highest priority; tie → most-local active scope; still tied → report conflict (dev/settings), never silent registration order. preventDefault() only when an enabled action actually handles it.
  • FR9. Editable-target suppression for input/textarea/select/ contenteditable/role=textbox/role=combobox/editor roots unless allowInEditable.
  • FR10. Active-region: selection.* actions fire only when a registered roving-focus region is active. Single-letter page actions (page.create/global.quickCreate) are not region-gated — they fire whenever their page scope is active and the target is non-editable, so the affordance works on page load without first focusing the list.
  • FR11. Sequence dispatch: a chord buffer accumulates keys; resolves a matching sequence; resets on timeout (configurable, default ~1s), on a non-matching key, on scope/route change, or in editable targets.
  • FR12. Radix Escape integration: dialog.cancel/panel.close coordinate with Radix onEscapeKeyDown + ModalityContext/InsideDialogContext; no competing global Escape listener while a Radix modal owns Escape.

Migrations (Phases 24)

  • FR13. Provider mounted in MspLayoutClient.tsx wrapping DefaultLayout and AlgaDeskMspShell; not on auth/client-portal.
  • FR14. global.search migrated from SearchPalette.tsx; global.toggleChat and ai.quickAsk from DefaultLayout.tsx (preserving aiAssistantAvailable gating); new global.openShortcuts (help) and global.quickCreate (QuickCreateDialog).
  • FR15. mod+k resolution: stays global search; asset palette becomes page-scoped assets.commandPalette with a distinct default binding; AssetDashboardClient.tsx window listener removed.
  • FR16. Both DrawerContext files migrated together; panel.close (Escape, Radix-integrated), drawer.historyBack/Forward.
  • FR17. TicketNavigation.tsx adjacent-record nav migrated to record.previous/record.next defaulting to [/] (Alt+Arrow only as an opt-in alternate); drawer/panel scope wins over ticket page scope.
  • FR18. Invoice designer (useDesignerShortcuts.ts), workflow designer, and rich-text local shortcuts migrated/wrapped into editor scope (high priority, per-platform redo, careful allowInEditable); BlockNote retains its own internal undo/redo.

Customization (Phase 5)

  • FR19. Overrides persisted via useUserPreference under key keyboard_shortcuts_v1: delta-only, platform-neutral syntax, versioned, with a disabled list; localStorage + debounced server sync.
  • FR20. Resolution = userOverride[id] ?? platformDefault(id, platform); a user-set value equal to the current platform default is dropped, not frozen.
  • FR21. On load, overrides validated against the current platform; hostile/ reserved combos surface a non-blocking advisory; never silently rewritten.
  • FR22. Versioned blob with a migration function (v1 → vN).
  • FR23. Settings panel: list actions grouped by group; show label, description, scope, default, effective binding (resolved for device); rebind via key capture; clear custom; disable; reset one; reset all (confirm); inline platform-aware conflict detection before commit.

Discoverability / Accessibility (Phase 6)

  • FR24. Help dialog opened by global.openShortcuts: read-only, active shortcuts only, grouped by group/scope, custom flagged, disabled hidden, bindings resolved per device.
  • FR25. <Kbd>/ShortcutHint shared component renders platform glyphs; visible hints added to key controls (search field, primary action buttons, relevant menu items) and tooltips.
  • FR26. aria-keyshortcuts set on instrumented controls via a mapping layer that converts the effective binding to the ARIA value format.

i18n (cross-cutting)

  • FR27. New msp/keyboard-shortcuts namespace; settings-tab label in msp/settings. Action labelKey/groupKey are i18n keys from Phase 1.
  • FR28. English keys authored first; fr/es/de/nl/it/pl/pt translated; pseudo xx/yy regenerated; validate-translations.cjs passes; ROUTE_NAMESPACES updated so the namespace preloads for /msp.
  • FR29. Platform glyph labels (⌘/Ctrl etc.) and ARIA shortcut text are localized/handled per locale; no hardcoded user-facing strings anywhere in the system, settings, or help.

Architecture boundary (dependency direction)

  • FR30. The engine in packages/ui/src/keyboard-shortcuts persists only through a ShortcutStorage adapter interface and must not import @alga-psa/user-composition or any feature package (@alga-psa/tickets|billing|assets|projects|clients|scheduling|...). The useUserPreference-backed adapter is implemented in the MSP wrapper (server/user-composition layer) and injected into the provider. A CI-runnable dependency-boundary guard enforces no forbidden imports and no new circular dependency vs .github/known-cycles.json (consistent with .github/workflows/circular-deps.yml and eslint-plugin-custom-rules).

Page-scoped create/save (commitGroup page-actions)

  • FR31. page.create is a page-scoped action defaulting to c (non-editable; fires on page scope, not active-region gated). mod+n is offered only as a user-configurable alternate with a documented caveat: Ctrl/⌘+N is a browser "new window" accelerator and is not reliably interceptable (especially Chrome on Windows), so c is the working default. Each page with a Create/New control registers page.create (via a usePageCreateShortcut helper) wired to that page's create dialog; global.quickCreate has its own distinct default binding n (the shared c default caused a silent unresolved dispatch conflict) and opens the multi-type QuickCreateDialog; it is the create affordance on pages with no page.create registered and coexists with page.create (c) elsewhere. page.save (mod+s, preventDefault browser save) is registered by pages with a primary Save. Both are suppressed in editable targets and when a dialog/drawer owns scope.

Create-dialog keyboard usability (commitGroup dialog-a11y)

  • FR32. Every page's create dialog is fully operable without a mouse: focus the first field on open and restore focus to the invoker on close; a focus trap; mod+Enter submit (textarea/BlockNote-safe); Escape cancel (Radix-integrated); all controls (selects, pickers, comboboxes, toggles) keyboard-reachable; correct role=dialog/aria-modal/aria-labelledby.

Command palette / Spotlight (commitGroup command-palette)

  • FR33. A keyboard-driven command palette opened by mod+k (this resolves and supersedes the prior open decision — mod+k no longer just focuses the sidebar search; the palette's default free-text mode includes record search so existing behavior is preserved/enhanced; the asset palette stays rescoped to mod+shift+k). Results merge navigation destinations (menuConfig), runnable registered shortcut actions, and record search, ranked with recents/frequency. The query grammar follows the TeamCity search model (see Appendix). The parser is a pure, UI-decoupled, unit-tested module obeying the FR30 boundary. Fully accessible (combobox/listbox roles, aria-activedescendant, live result count), i18n in all 8 locales + pseudo, with in-palette syntax help linked from the global help dialog.

End-to-end customization wiring (commitGroup customization-wiring)

  • FR34. The persistence/override layer must be functionally connected, not only unit-tested in isolation. The provider is the single source of preference state: MspLayoutClient injects the useUserPreference-backed ShortcutStorage; the provider loads PersistedShortcuts, dispatch resolves bindings via resolveActionBindings (override → platform default), merges the persisted disabled list, and exposes resolved bindings + mutators via context; display.tsx hints/aria-keyshortcuts and the settings panel all read from that one source. An integration test is required asserting: rebind in settings → the new combo dispatches, the old default stops, the settings "Effective" column, ShortcutHint, and aria-keyshortcuts all reflect it; disable stops dispatch and hides from help; reset live-updates. Unit/contract green on isolated pieces is not sufficient to mark FR19/FR20/FR23/FR26 or F043/F140 done.

Gap-analysis remediation (commitGroup gap-hardening)

These three requirements came out of a post-implementation review of the branch. They are distinct from FR34 (which covers only the persistence/dispatch wiring) and address structural issues that let the review-discovered bugs ship "green".

  • FR35. Catalog is the single source of truth for action metadata. scope, priority, defaultBindings, labelKey, groupKey, sequence, and allowInEditable live only in catalog.ts. Registration sites build actions through a catalog-derived factory (e.g. createShortcutAction(id, handler, { enabled? }) / useCatalogShortcut) supplying only id + handler (+ optional runtime enabled); hand-authored metadata literals are removed from SearchPalette, DefaultLayout, both DrawerContext files, TicketNavigation, AssetDashboardClient, and useDesignerShortcuts. A guard (unit + the boundary script) fails when a registered/used action id is absent from the catalog or its scope/priority/sequence/bindings diverge from the catalog entry. This resolves the current divergence where useDesignerShortcuts hardcodes priority: 60 while every other site omits priority (runtime 0), contradicting catalog.ts DEFAULT_PRIORITY — so the priorities the settings UI/help show are not the priorities dispatch uses.

  • FR36. Active-region gating is enforced, not nominal. DefaultLayout must not call useShortcutActiveRegion(true) unconditionally. An active region is registered only by genuine roving-focus list/selection containers (via a shared region wrapper/hook applied to the real list views). As a result selection.* actions (j/k/Enter) are inert on arbitrary non-editable focus anywhere in /msp and fire only when such a region is focused/active. Single-letter page actions (page.create c, global.quickCreate n) are not region-gated: they are page-wide affordances guarded by scope eligibility and editable-target suppression only, so they work on page load without first focusing the list (the earlier active-region requirement made c/n dead until the list was clicked into — rejected as a UX regression). This turns FR10 from documented intent into enforced behavior.

  • FR37. Behavioral test coverage replaces source greps. The readFileSync + toContain *.contract.test.ts suites (global-migration, panels-drawers, editors, persistence-bridge, settings-ui, regression, i18n) are replaced/augmented with tests that assert observable dispatch/registration/resolution behavior rather than source-string presence; a thin source-presence smoke is kept only where the behavior genuinely cannot be simulated. A meta test-guard flags any new *.contract.test.ts that only greps source without a behavioral assertion, so "green CI" cannot again mean "the feature was never exercised".

Non-functional Requirements

  • One global listener; O(registered actions) match per keydown; no measurable input latency.
  • No SSR/hydration mismatch (platform + bindings resolved post-mount).
  • packages/ui keeps the engine dependency-light; preference wiring lives in the MSP wrapper/user-composition layer.
  • No regression to component-local widget key handling.
  • Accessibility: screen readers announce shortcuts via aria-keyshortcuts.

Shortcuts UI redesign + Profile placement (commitGroup shortcuts-ui-redesign)

These requirements supersede the original list-Table settings panel (KeyboardShortcutsSettings.tsx, now deleted) with the design-handoff visual keyboard cheatsheet, and correct its placement so users can actually reach it. Sourced from ~/Downloads/design_handoff_keyboard_shortcuts/ (variation-c) and manual-test feedback that the prior tab never appeared in the sidebar.

  • FR38. Placement: Profile, not Settings. The keyboard-shortcuts pane is a CustomTabs sub-tab under /msp/profile (UserProfile.tsx tabContent), matching the per-user-preference convention (Profile / Security / SSO / API Keys / Notifications). The old SettingsPage.tsx baseTabContent entry and KeyboardShortcutsSettings.tsx are removed. keyboard-shortcuts is added to BASE_PROFILE_TABS in packages/integrations/src/lib/calendarAvailability.ts so /msp/profile?tab=keyboard-shortcuts deep-links work. The Sidebar settings-mode menu is unaffected (the pane is not a Settings tab).

  • FR39. Visual keyboard panel. KeyboardShortcutsPanel.tsx recreates the design handoff (variation-c) using product CSS variables and Radix primitives, driven by the real catalog + provider single source (no parallel data): full keyboard grid (KB_ROWS), layer toggle (Plain / ⌘ Mod / ⇧ Shift / ⌘⇧) with per-layer counts, category-tinted bound keys with conflict dots, hover/selection KeyDetail strip (action name + scope chip + Modified badge + description + BindingDisplay + Rebind/Cancel + Enabled switch + per-action Reset), right-rail chord list with search and Reset all to {profile} footer, real keydown capture wired through useKeyboardShortcutPreferences, Copy cheatsheet (print window), ConfirmationDialog for reset-all and the prompt-to-reassign conflict UX (the new binding takes over, the previous owner is left unbound), react-hot-toast + handleError parity, every interactive element has an id. Bindings whose key isn't on the keyboard (?, Escape, Delete, sequences) route to the chord rail rather than silently disappearing (improvement over the prototype).

  • FR40. Profile presets in the engine single source. PersistedShortcuts gains profile: string (schema v2, with a v1→v2 migration that defaults to 'default' and preserves a valid stored profile). SHORTCUT_PROFILES ships default, vim, emacs with parser-valid neutral single-chord deltas keyed by real catalog ids — multi-chord Emacs sequences are deliberately not assigned to non-sequence actions (they would silently never dispatch). Resolution order is user override → active-profile delta → platform default, applied inside resolveActionBindings so dispatch, hints, ARIA, and the panel all read the same effective binding. setActionBindingsDelta drops an override equal to the profile baseline (not raw factory), so per-action reset returns to the active profile's baseline. The provider exposes profile + setProfile. Override scope stays per-account only (useUserPreference); the prototype's per-device split is intentionally deferred. Vim/Emacs delta maps are best-guess pending team confirmation.

Data / API / Integrations

  • Persistence: engine exposes a ShortcutStorage adapter interface; the concrete adapter is useUserPreference-backed (packages/user-composition/src/hooks/useUserPreference.ts), key keyboard_shortcuts_v1, localStorage + debounced server sync, unauthenticated path supported. The adapter and its @alga-psa/user-composition import live in the MSP wrapper (server), injected into the provider — never imported into packages/ui. No new tables/endpoints (reuses existing user_preferences).
  • Stored shape: { "version": 1, "bindings": { "<id>": ["<neutral-binding>"...] }, "disabled": ["<id>"...] } storing only deltas in neutral syntax (mod, logical tokens).
  • i18n integration: useTranslation('msp/keyboard-shortcuts') (@alga-psa/ui/lib/i18n/client); locale files under server/public/locales/{lang}/msp/keyboard-shortcuts.json; namespace added to ROUTE_NAMESPACES in packages/core/src/lib/i18n/config.ts.
  • Settings shell: server/src/components/settings/SettingsPage.tsx baseTabContent; product-tab allowlist (settingsProductTabs.ts) reviewed (no change expected for MSP-only).

Security / Permissions

  • No new privileged operations. Shortcuts only invoke handlers the user can already trigger via UI; enabled honors existing gating (e.g. aiAssistantAvailable). Preference reads/writes are user-scoped via existing useUserPreference server actions.

Observability

  • None beyond dev-time conflict reporting in the registry/settings UI (no new metrics/logging per scope philosophy).

Rollout / Migration

  • Phased per source plan: P1 engine → P2 global → P3 panels/drawers/record → P4 editors → P5 customization → P6 discoverability/a11y.
  • Each migrated handler is removed only after its action is registered and regression-verified, so behavior is preserved at every step.
  • mod+k decision is settled (global search; asset palette rescoped) and is a hard prerequisite for P2 completion.
  • Preference blob is versioned with a migration function for forward changes.

Open Questions

  • Tenant-level defaults / admin lock (deferred; not in scope).
  • Export/import of overrides (deferred).
  • Whether client portal adopts the system later (deferred).
  • Final default binding for assets.commandPalette after rescoping (implemented: mod+shift+k).
  • (Resolved) mod+k = the command palette / Spotlight (FR33), not just sidebar search; record search lives inside the palette.
  • $ magic-keyword set and the exact field-alias list are finalized during command-palette implementation (Appendix is the starting spec).

Acceptance Criteria (Definition of Done)

  • All handlers from the inventory are registered through the shared system; one delegated capture listener; component-local widget handlers untouched.
  • Matching correct on macOS and Windows/Linux incl. alt+letter, international layouts, mod resolution; respects event.defaultPrevented.
  • Single-chord and g-sequence bindings work; sequence buffer times out/resets correctly and is suppressed in editable targets.
  • Scope/priority/active-region prevent global/page/panel/dialog/editor conflicts; drawer wins over page; Escape integrates with Radix.
  • mod+k conflict removed; asset palette rescoped and functional.
  • packages/ui/src/keyboard-shortcuts has zero imports of @alga-psa/user-composition or feature packages; persistence flows through the injected ShortcutStorage adapter; npx nx graph shows no new cycle vs .github/known-cycles.json; the dependency-boundary guard passes.
  • Existing AI/search/drawer/ticket/designer shortcuts preserved post-migration (regression + Playwright verified).
  • User can rebind/clear/disable/reset; overrides persist as neutral deltas locally and sync to server; Mac→Windows cross-device resolves correctly with advisories for hostile combos.
  • Settings UI is visually/structurally consistent with existing settings panels (shared components, variants, toast/error handling, ids, loading state).
  • Help dialog lists active shortcuts resolved per device; aria-keyshortcuts and visible kbd hints present on instrumented controls.
  • All user-facing strings internationalized in 8 locales + pseudo; validate-translations.cjs passes; namespace preloads via ROUTE_NAMESPACES.
  • No SSR/hydration mismatch.
  • page.create (c) opens the current page's create dialog on every page with a Create control; page.save (mod+s) saves on pages with a Save; both suppressed while typing / when a dialog owns scope.
  • Every page's create dialog is completable end-to-end with the keyboard only.
  • mod+k opens the command palette; field-scoped syntax + operators per the Appendix work; results span nav + actions + records; fully keyboard/SR accessible; palette module passes the FR30 dependency-boundary guard.
  • Customization is wired end-to-end: a rebind/disable/reset made in settings immediately changes what the dispatcher fires and what hints/aria show, from a single provider-owned source; the FR34 integration test passes.

Appendix: Command Palette Syntax (TeamCity-derived)

Adapted from JetBrains TeamCity search (https://www.jetbrains.com/help/teamcity/search.html).

  • Field-scoped: field:value with short aliases. Initial registry: ticket:/t:, client:/c:, contact:, project:/p:, asset:/a:, user:/u: (and @name), nav: (and /path), action: (and >cmd). Record-id sigil #1234. Final alias list confirmed in implementation.
  • Operators: double-quoted phrases (status:"in progress"); - / NOT exclusion; * and ? wildcards (no leading *); fuzzy term~; prefix-match by default; OR is the default between unscoped terms; AND only within the same field scope (tag:a AND tag:b).
  • Magic keywords: $-prefixed, abbreviable to first syllable ($mine/$m, $recent/$rec, $open) — mirrors TeamCity $pinned/$p.
  • Sigils (leading): > run action/command, # open by record id, @ people, / navigation.
  • No field given: free-text fuzzy/prefix across nav + actions + records, ranked with recents/frequency.