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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
645 lines
46 KiB
Markdown
645 lines
46 KiB
Markdown
# Scratchpad — Shared Keyboard Shortcuts System
|
||
|
||
- Plan slug: `2026-05-18-keyboard-shortcuts-shared-system`
|
||
- Created: `2026-05-18`
|
||
- Source design doc: `.ai/keyboard-shortcuts-shared-system-plan.md`
|
||
|
||
## What This Is
|
||
|
||
Working memory for the shared keyboard shortcuts effort. Append discoveries and
|
||
decisions; update earlier notes when a decision changes.
|
||
|
||
## Decisions
|
||
|
||
- (2026-05-18) Scope = **all 6 phases** incl. chord/sequence engine and visible
|
||
`kbd` hints + `aria-keyshortcuts`. (User answer.)
|
||
- (2026-05-18) i18n = **English + all 8 production languages** (en, fr, es, de,
|
||
nl, it, pl, pt) translated in this plan, + pseudo xx/yy regenerated, +
|
||
`validate-translations.cjs` must pass. (User answer.)
|
||
- (2026-05-18) `mod+k` = **stays global search**; asset command palette
|
||
rescoped to page-scoped `assets.commandPalette` with a non-`mod+k` default.
|
||
(User answer; settles the source-doc open question, hard prereq for P2.)
|
||
- (2026-05-18) Matcher = **dual matching**: code-kind tokens → `event.code` +
|
||
exact modifier set; char-kind tokens (e.g. `?`) → `event.key` produced char.
|
||
Rationale: fixes macOS Option-as-dead-key (`event.key` becomes `Dead`/`˜`)
|
||
and international layouts (Cyrillic/German `mod+s`).
|
||
- (2026-05-18) Persist **deltas only in platform-neutral syntax**; resolve
|
||
`mod`/platform per device at runtime; drop a user value that equals the
|
||
current platform default. Never persist resolved (`cmd+`/`ctrl+`) bindings.
|
||
- (2026-05-18) **Respect `event.defaultPrevented`** (reversed from original
|
||
source draft) so the global system does not double-fire against ~40
|
||
component-local arrow/Enter handlers.
|
||
- (2026-05-18) Engine lives in `packages/ui/src/keyboard-shortcuts/*`, kept
|
||
dependency-light; preference wiring lives in the MSP wrapper via
|
||
`useUserPreference` (`@alga-psa/user-composition/hooks`).
|
||
- (2026-05-19) **Dependency-boundary guard (group `architecture-guard`,
|
||
F005-F007/T012-T014, placed right after `scaffold` so it lands before
|
||
`persistence`).** Engine persists ONLY via a `ShortcutStorage` interface
|
||
(F005) + default in-memory adapter (F006); provider takes an injected
|
||
storage (F043); the `useUserPreference` adapter lives in `server` and is
|
||
injected (F140 reworded). Rationale: importing `useUserPreference` into
|
||
`packages/ui` creates `ui → user-composition → ui`, a NEW cycle that fails
|
||
`.github/workflows/circular-deps.yml` (not in `known-cycles.json`). Guard
|
||
(F007) + T012 (no forbidden imports) + T013 (`nx graph` no new cycle)
|
||
enforce it. Repo also has `eslint-plugin-custom-rules/`
|
||
`no-feature-to-feature-imports` (CI `error`).
|
||
- (2026-05-18) i18n namespace = new `msp/keyboard-shortcuts` (action labels,
|
||
groups, help, settings-panel chrome). Settings **tab label** goes in existing
|
||
`msp/settings`. Action `labelKey`/`groupKey` are i18n keys from Phase 1
|
||
(no raw strings, no Phase-6 retrofit).
|
||
- (2026-05-18) Settings UI = new tab in `SettingsPage.tsx` `baseTabContent`,
|
||
component `KeyboardShortcutsSettings.tsx`, preference-backed (immediate
|
||
debounced save, no explicit Save button), shared components only.
|
||
- (2026-05-18) Commit policy = **one commit per `commitGroup`** (not per item).
|
||
Every feature/test carries a `commitGroup`; 15 groups: scaffold, parser,
|
||
matcher, registry, sequence, radix-escape, action-catalog, global-migration,
|
||
panels-drawers, editors, persistence, settings-ui, help-a11y, i18n,
|
||
regression → ≈15 commits. Commit a group only when all its items are
|
||
implemented:true. Message form: `<type>(<group>): summary [ids]`.
|
||
- (2026-05-19) Added 3 net-new groups from manual-test feedback:
|
||
`page-actions` (page-scoped `page.create`/`page.save` registered per page;
|
||
default create = **`c`**, NOT `mod+n` — Ctrl/⌘+N is a browser new-window
|
||
accelerator not reliably interceptable, esp. Chrome/Windows; `mod+n` is an
|
||
opt-in alternate with a caveat), `dialog-a11y` (every page's create dialog
|
||
keyboard-only: focus-on-open, trap, `mod+Enter` submit, Escape, SR roles),
|
||
`command-palette` (Spotlight on **`mod+k`** — resolves the old open
|
||
decision; record search now lives inside the palette; asset palette stays
|
||
`mod+shift+k`). Palette grammar = TeamCity-derived (see PRD Appendix:
|
||
field:value + aliases, quoted phrases, `-`/NOT, `*`/`?`, prefix+OR default,
|
||
AND in-field, `$`magic, sigils `> # @ /`). Parser is a pure module under the
|
||
same FR30 boundary guard. F300-F353 / T300-T349.
|
||
- (2026-05-19) **Code review caught a severe wiring gap: customization is
|
||
cosmetic.** Provider accepts `storage?`/`disabledActionIds?` but
|
||
`MspLayoutClient` passes neither (falls back to in-memory). Dispatch
|
||
(`collectSingleChord/Sequence`) resolves from `normalizeDefaultBindings`,
|
||
never `resolveActionBindings`; `display.tsx` uses
|
||
`getDefaultBindingsForPlatform` only. So a rebind persists + shows in the
|
||
settings Effective column but does nothing; old default still fires;
|
||
disabled list never reaches dispatch; hints/aria stay on defaults.
|
||
**Root cause: no integration test.** Every piece had a green unit/contract
|
||
test in isolation; the architecture-guard checked the *import* boundary but
|
||
nothing checked the *functional* wiring → F043/F140/F142/F146/F185/F186
|
||
were flipped true on unit-green. Corrective: reopened those 6
|
||
(commitGroup→`customization-wiring`), added F360-F366 (inject storage at
|
||
mount; provider = single source; dispatch via resolveActionBindings; merge
|
||
disabled; context resolver; settings consumes context) + T360-T364
|
||
including **T360, the end-to-end test that would have caught it**. FR34
|
||
added: unit-green is NOT sufficient to mark FR19/20/23/26 done.
|
||
- **Process lesson:** any "engine + adapter + UI" feature needs one
|
||
integration test across the seam before its features can be marked done;
|
||
isolated unit tests passing is a false signal.
|
||
|
||
## Discoveries / Constraints
|
||
|
||
- (2026-05-19) GAP found in manual test: `catalog.ts` defined
|
||
`navigation.goTickets/goAssets/goClients` (sequence `g t/a/c`) but **no
|
||
component registered handlers** → `g t` was a silent no-op. Source plan had
|
||
nav as "illustrative"; `sequence` group built the engine only. Fixed:
|
||
registered the 3 nav actions in `DefaultLayout.tsx` with `router.push`
|
||
(`navigation` commitGroup, F095 impl, T095 e2e pending).
|
||
- (2026-05-18) ~50 files use `keydown`/`onKeyDown`; only window/document-level
|
||
handlers are in scope. Component-local widget handlers (DatePicker,
|
||
SearchableSelect, TagInput, comboboxes, Radix internals) stay as-is.
|
||
- (2026-05-18) **Two** DrawerContexts: `server/src/context/DrawerContext.tsx`
|
||
AND `packages/ui/src/context/DrawerContext.tsx` — migrate together or they
|
||
fight.
|
||
- (2026-05-18) `Alt+ArrowLeft/Right` (TicketNavigation, DrawerContext) = browser
|
||
Back/Forward on Windows/Linux → change record nav default to `[`/`]`.
|
||
- (2026-05-18) `DefaultLayout.tsx:215/229` `mod+l`/`mod+ArrowUp` gated on
|
||
`aiAssistantAvailable` (early-return before `preventDefault`) — preserve via
|
||
the registry `enabled` flag.
|
||
- (2026-05-18) `AssetDashboardClient.tsx:285` window `mod+k` listener = the
|
||
conflict source; remove on rescope.
|
||
- (2026-05-18) Radix Dialog/Drawer Escape is managed via `onEscapeKeyDown` +
|
||
`stopPropagation` and `ModalityContext`/`InsideDialogContext` nesting fix
|
||
(see global memory). Escape actions must integrate, not add a competing
|
||
window listener.
|
||
- (2026-05-18) Settings shell: `server/src/components/settings/SettingsPage.tsx`
|
||
→ `CustomTabs` + `UnsavedChangesProvider`; tabs are a hard-coded
|
||
`baseTabContent` array (`{id,label,icon,content}`); add a tab there.
|
||
- (2026-05-18) Settings panel analogs:
|
||
`server/src/components/settings/general/ExperimentalFeaturesSettings.tsx`
|
||
(toggle list), `GeneralSettings.tsx` (Table of items with controls).
|
||
- (2026-05-18) Shared component import paths:
|
||
`@alga-psa/ui/components/{Card,Table,Button,Switch,Dialog,ConfirmationDialog,LoadingIndicator,Alert,Input,Label,CustomSelect}`;
|
||
feedback via `react-hot-toast` + `@alga-psa/ui/lib/errorHandling#handleError`.
|
||
Button variants: `default|destructive|outline|ghost|soft`; every interactive
|
||
element needs an `id`.
|
||
- (2026-05-18) i18n: `i18next` + `react-i18next`. Client hook
|
||
`useTranslation('namespace')` from `@alga-psa/ui/lib/i18n/client`; server
|
||
`getServerTranslation`/`getServerLocale` from
|
||
`@alga-psa/ui/lib/i18n/serverOnly`. Locale files
|
||
`server/public/locales/{lang}/{namespace}.json` (nested JSON, dot keys).
|
||
Languages: en, fr, es, de, nl, it, pl, pt (+ xx, yy pseudo).
|
||
- (2026-05-18) i18n workflow: add EN keys → mirror to 7 langs → regenerate
|
||
pseudo (`node scripts/generate-pseudo-locales.cjs`) → validate
|
||
(`node scripts/validate-translations.cjs`) → register namespace in
|
||
`ROUTE_NAMESPACES` (`packages/core/src/lib/i18n/config.ts`).
|
||
- (2026-05-18) `useUserPreference` already does default→localStorage→server,
|
||
500ms debounce, hydration-mismatch avoidance, `skipServerFetch`/
|
||
`isUserLoggedIn` unauth path — it is THE persistence impl, not re-invented.
|
||
- (2026-05-18) Plan-folder convention: `docs/plans/YYYY-MM-DD-<slug>/` with
|
||
PRD.md/features.json/tests.json/SCRATCHPAD.md (matches
|
||
`docs/plans/2026-05-13-threaded-comments/`).
|
||
|
||
## Commands / Runbooks
|
||
|
||
- Pseudo-locales: `node scripts/generate-pseudo-locales.cjs`
|
||
- Validate translations: `node scripts/validate-translations.cjs`
|
||
- Pseudo QA: switch locale to `xx` (expect `11111…`) or `yy` (`55555…`) to
|
||
confirm all strings are extracted.
|
||
|
||
## Links / References
|
||
|
||
- Source: `.ai/keyboard-shortcuts-shared-system-plan.md`
|
||
- Engine target: `packages/ui/src/keyboard-shortcuts/*`
|
||
- Settings shell: `server/src/components/settings/SettingsPage.tsx`
|
||
- Persistence: `packages/user-composition/src/hooks/useUserPreference.ts`
|
||
- i18n config: `packages/core/src/lib/i18n/config.ts`
|
||
- Locale root: `server/public/locales/`
|
||
- Global memory: Radix Dialog nesting / ModalityContext escape fix.
|
||
|
||
## Work Log
|
||
|
||
- (2026-05-19) Completed scaffold group F001-F004/T026. Added
|
||
`packages/ui/src/keyboard-shortcuts/` with a barrel, core public types
|
||
(`ShortcutAction`, scopes, binding descriptors, parse result, persisted
|
||
shortcut blob), and client-only platform detection helpers. Added package
|
||
exports and a tsup entry for `@alga-psa/ui/keyboard-shortcuts`. Detection
|
||
now returns `null` when `window` is absent so SSR/node paths do not inspect
|
||
`navigator`; `useClientPlatform` resolves after mount.
|
||
- (2026-05-19) Verification for scaffold:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/platform.test.ts`
|
||
from `packages/ui` passed (3 tests).
|
||
- (2026-05-19) Completed parser group F010-F019/T001-T011. Added
|
||
`parser.ts` with typed-result `parseBinding` and `parseSequence`.
|
||
Supported physical code tokens for letters, digits, F1-F12, named navigation
|
||
keys, and brackets; glyphs like `?` parse as character tokens. Modifier
|
||
normalization is deterministic (`mod`, `ctrl`, `meta`, `alt`, `shift`), and
|
||
literal `ctrl`/`meta` remain distinct from neutral `mod`.
|
||
- (2026-05-19) Verification for parser:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/parser.test.ts`
|
||
from `packages/ui` passed (27 tests).
|
||
- (2026-05-19) Completed matcher group F020-F025/T020-T025. Added
|
||
`matcher.ts` with platform-time `mod` resolution, exact modifier matching for
|
||
code tokens, produced-character matching for char tokens with Shift ignored
|
||
only as the glyph-producing modifier, and AltGraph protection. Tests cover
|
||
mac/other `mod`, macOS Option dead-key behavior, international-layout
|
||
physical-code matching, and AltGr false-positive prevention.
|
||
- (2026-05-19) Verification for matcher:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/matcher.test.ts`
|
||
from `packages/ui` passed (6 tests).
|
||
- (2026-05-19) Completed registry group F030-F042/T030-T044/T234.
|
||
Added `ShortcutRegistry`, `normalizeDefaultBindings`, and a client
|
||
`KeyboardShortcutsProvider` with one capture-phase document listener.
|
||
Hooks now register actions, scopes, and active regions without adding
|
||
listeners. Dispatch skips `defaultPrevented`, honors enabled/disabled gates,
|
||
suppresses editable targets unless opted in, filters by active scopes, uses
|
||
priority then most-local active scope, and reports residual ties through
|
||
`onConflict`.
|
||
- (2026-05-19) Route-change clearing is represented in the UI package by a
|
||
provider `routeKey` prop so MSP can pass pathname later without coupling the
|
||
package to Next routing. Active-region gating covers `selection.*` and
|
||
unmodified single-letter page actions.
|
||
- (2026-05-19) Verification for registry:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts` from
|
||
`packages/ui` passed (57 tests). `npx tsc --noEmit -p packages/ui/tsconfig.json`
|
||
passed.
|
||
- (2026-05-19) PRD/checklist was updated outside my prior commits with new
|
||
FR30 dependency-boundary requirements and a late F043 storage-adapter item.
|
||
Moved that late item from the already-committed `registry` group to a new
|
||
`architecture-boundary` group to preserve the one-commit-per-group rule, and
|
||
added F044/T045-T047 to cover the boundary guard and CI path.
|
||
- (2026-05-19) Completed architecture-boundary group F043-F044/T045-T047.
|
||
Added `ShortcutStorage`, default in-memory storage, provider injection via
|
||
`storage` prop/context, and `useShortcutStorage`. Added
|
||
`scripts/guard-keyboard-shortcuts-boundary.mjs` plus a package script and
|
||
circular-deps workflow step. The guard blocks imports from
|
||
`@alga-psa/user-composition` and feature packages in
|
||
`packages/ui/src/keyboard-shortcuts`, and delegates graph checks to
|
||
`scripts/check-circular-deps.mjs --baseline .github/known-cycles.json` when
|
||
`--graph` is supplied.
|
||
- (2026-05-19) Verification for architecture-boundary:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/storage.test.tsx`
|
||
from `packages/ui` passed (2 tests);
|
||
`node --test scripts/tests/guard-keyboard-shortcuts-boundary.test.mjs`
|
||
passed (2 tests); `npm run guard:keyboard-shortcuts-boundary` passed;
|
||
`npx tsc --noEmit -p packages/ui/tsconfig.json` passed.
|
||
- (2026-05-19) Completed sequence group F050-F054/T050-T054. The provider now
|
||
evaluates sequence actions through the same delegated listener, with a
|
||
configurable `sequenceTimeoutMs` (default 1000ms). Sequence buffers reset on
|
||
timeout, non-match, scope push/pop, route change, and editable targets.
|
||
Prefixes do not block single-chord actions; a full sequence prevents default
|
||
only when its handler reports handled.
|
||
- (2026-05-19) Verification for sequence:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/sequence.test.tsx src/keyboard-shortcuts/provider.test.tsx`
|
||
from `packages/ui` passed (24 tests);
|
||
`npx tsc --noEmit -p packages/ui/tsconfig.json` passed.
|
||
- (2026-05-19) Completed radix-escape group F060-F062/T060-T062. Added a
|
||
ref-counted Radix Escape owner bridge (`useRadixEscapeOwner`) and wired
|
||
shared `Dialog`/`Drawer` to mark ownership while open. The provider skips
|
||
Escape dispatch while any Radix owner exists, so document-capture shortcuts
|
||
cannot race Radix `onEscapeKeyDown` or nested `stopPropagation`. When no
|
||
Radix owner exists, `panel.close`/Escape actions still dispatch normally.
|
||
- (2026-05-19) Verification for radix-escape:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/escape.test.tsx src/keyboard-shortcuts/provider.test.tsx`
|
||
from `packages/ui` passed (22 tests);
|
||
`npx tsc --noEmit -p packages/ui/tsconfig.json` passed.
|
||
- (2026-05-19) Completed action-catalog group F070-F071/T070-T071. Added
|
||
`catalog.ts` with stable metadata entries for global, AI, page, selection,
|
||
navigation sequence, assets, dialog/panel/drawer, record, and editor actions.
|
||
Each entry carries `labelKey`/`groupKey`, scope, priority, defaults, and
|
||
flags like `sequence`/`allowInEditable`; `editor.redo` resolves to
|
||
`mod+shift+z` on mac and `ctrl+y`/`ctrl+shift+z` elsewhere.
|
||
- (2026-05-19) Verification for action-catalog:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/catalog.test.ts`
|
||
from `packages/ui` passed (2 tests);
|
||
`npx tsc --noEmit -p packages/ui/tsconfig.json` passed.
|
||
- (2026-05-19) Completed global-migration group F080-F090/T080-T089/T230.
|
||
Mounted `KeyboardShortcutsProvider` in `MspLayoutClient` around the MSP
|
||
product shells (not auth/client portal). Migrated `global.search` out of
|
||
`SearchPalette`'s window listener; migrated `global.toggleChat` and
|
||
`ai.quickAsk` out of `DefaultLayout`'s window listener while preserving
|
||
`aiAssistantAvailable` gates; registered `global.openShortcuts` and
|
||
`global.quickCreate`; and rescoped the assets palette to page-scoped
|
||
`assets.commandPalette` on `mod+shift+k`.
|
||
- (2026-05-19) Temporary note: `global.openShortcuts` currently opens a minimal
|
||
placeholder dialog in `DefaultLayout`; the full shared help dialog remains
|
||
owned by the later `help-a11y` group. `global.quickCreate` opens the ticket
|
||
quick-create dialog by default.
|
||
- (2026-05-19) Verification for global-migration:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/global-migration.contract.test.ts`
|
||
from `packages/ui` passed (5 tests);
|
||
`npx tsc --noEmit -p packages/ui/tsconfig.json` passed;
|
||
`npx tsc --noEmit -p packages/assets/tsconfig.json` passed;
|
||
`npm run typecheck --workspace server` passed. Attempting direct server
|
||
layout Vitest files hit an existing Vitest 4/coverage-v8 3.2.4 harness error
|
||
before tests executed (`Cannot read properties of undefined (reading
|
||
'fetchCache')`).
|
||
- (2026-05-19) Completed panels-drawers group F100-F108/T100-T107. Both
|
||
`server/src/context/DrawerContext.tsx` and
|
||
`packages/ui/src/context/DrawerContext.tsx` now register `panel.close`,
|
||
`drawer.historyBack`, and `drawer.historyForward`; both push `panel` scope
|
||
only while open. `TicketNavigation` now registers `record.previous`/`next`
|
||
on `[`/`]` and removed its `Alt+Arrow` window listener. The catalog keeps
|
||
`Alt+ArrowLeft/Right` only in `OPTIONAL_ALTERNATE_BINDINGS`.
|
||
- (2026-05-19) Verification for panels-drawers:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/panels-drawers.contract.test.ts src/keyboard-shortcuts/provider.test.tsx src/keyboard-shortcuts/catalog.test.ts`
|
||
from `packages/ui` passed (25 tests);
|
||
`npx tsc --noEmit -p packages/ui/tsconfig.json` passed;
|
||
`npx tsc --noEmit -p packages/tickets/tsconfig.json` passed;
|
||
`npm run typecheck --workspace server` passed.
|
||
- (2026-05-19) Completed editors group F120-F124/T120-T124. Invoice designer
|
||
`useDesignerShortcuts.ts` now pushes `editor` scope and registers undo, redo,
|
||
delete selection, cancel, and arrow-move actions instead of a window keydown
|
||
listener. `TextEditor` pushes editor scope and marks the BlockNote root with
|
||
`data-keyboard-shortcuts-editor-root="true"` but does not register undo/redo,
|
||
so BlockNote internal editing shortcuts remain local. Search did not find a
|
||
separate workflow-designer window-level shortcut hook to migrate in this
|
||
group.
|
||
- (2026-05-19) Verification for editors:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/editors.contract.test.ts src/keyboard-shortcuts/catalog.test.ts`
|
||
from `packages/ui` passed (5 tests);
|
||
`npx tsc --noEmit -p packages/ui/tsconfig.json` passed;
|
||
`npx tsc --noEmit -p packages/billing/tsconfig.json` passed.
|
||
- (2026-05-19) Completed persistence group F140-F147/T140-T150. Added
|
||
dependency-free preference utilities under `packages/ui` for
|
||
`keyboard_shortcuts_v1`, version migration, neutral delta storage,
|
||
override/default resolution, drop-equals-default, disabled action ids, and
|
||
hostile/reserved combo advisories. Added the MSP bridge hook
|
||
`server/src/hooks/useKeyboardShortcutPreferenceStorage.ts`, which uses
|
||
`useUserPreference` with localStorage and `skipServerFetch` support.
|
||
- (2026-05-19) Verification for persistence:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/preferences.test.ts src/keyboard-shortcuts/persistence-bridge.contract.test.ts`
|
||
from `packages/ui` passed (6 tests);
|
||
`npx tsc --noEmit -p packages/ui/tsconfig.json` passed;
|
||
`npm run typecheck --workspace server` passed.
|
||
- (2026-05-19) Completed settings-ui group F160-F172/T160-T174. Added
|
||
`KeyboardShortcutsSettings` as a settings tab with shared Card/Table/Switch/
|
||
Button/LoadingIndicator/ConfirmationDialog components. The panel lists
|
||
catalog actions by group, captures bindings inline, writes preference deltas
|
||
immediately through `useKeyboardShortcutPreferenceStorage`, supports disable,
|
||
reset one, reset all, and conflict confirmation, and uses toast/handleError.
|
||
- (2026-05-19) Verification for settings-ui:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/settings-ui.contract.test.ts`
|
||
from `packages/ui` passed (3 tests);
|
||
`npm run typecheck --workspace server` passed.
|
||
- (2026-05-19) Completed help-a11y group F180-F187/T180-T187. Added shared
|
||
`Kbd`, `ShortcutHint`, shortcut formatting, `aria-keyshortcuts` conversion,
|
||
and `ShortcutHelpDialog`. The search input now exposes `aria-keyshortcuts`
|
||
and a visible hint, and `global.openShortcuts` renders the shared help
|
||
dialog instead of the temporary placeholder.
|
||
- (2026-05-19) Verification for help-a11y:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/display.test.tsx`
|
||
from `packages/ui` passed (3 tests);
|
||
`npx tsc --noEmit -p packages/ui/tsconfig.json` passed;
|
||
`npm run typecheck --workspace server` passed.
|
||
|
||
## Open Questions
|
||
|
||
- Final `assets.commandPalette` default binding after rescope (confirm in P2).
|
||
- Whether any `editor` action needs `allowInEditable` exceptions beyond
|
||
designer canvases (confirm in P4 against `useDesignerShortcuts.ts`).
|
||
- Exact set of controls to receive visible `kbd` hints / `aria-keyshortcuts`
|
||
(curate during P6 — avoid hint clutter).
|
||
|
||
## 2026-05-19 — i18n group implementation
|
||
- F200: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- F201: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- F202: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- F203: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- F204: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- F205: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- F206: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- F207: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- F208: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- F209: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- F210: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- F211: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- F212: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- F213: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- F214: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- F215: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- F216: Implemented in commitGroup i18n. Added the keyboard-shortcuts namespace, settings tab key, route preload, pseudo-locale generation support, and localized platform/advisory/help/settings/action strings as applicable.
|
||
- T200: Verified in commitGroup i18n via the keyboard shortcuts i18n contract test plus pseudo-locale generation and validate-translations.cjs.
|
||
- T201: Verified in commitGroup i18n via the keyboard shortcuts i18n contract test plus pseudo-locale generation and validate-translations.cjs.
|
||
- T202: Verified in commitGroup i18n via the keyboard shortcuts i18n contract test plus pseudo-locale generation and validate-translations.cjs.
|
||
- T203: Verified in commitGroup i18n via the keyboard shortcuts i18n contract test plus pseudo-locale generation and validate-translations.cjs.
|
||
- T204: Verified in commitGroup i18n via the keyboard shortcuts i18n contract test plus pseudo-locale generation and validate-translations.cjs.
|
||
- T205: Verified in commitGroup i18n via the keyboard shortcuts i18n contract test plus pseudo-locale generation and validate-translations.cjs.
|
||
- T206: Verified in commitGroup i18n via the keyboard shortcuts i18n contract test plus pseudo-locale generation and validate-translations.cjs.
|
||
- T207: Verified in commitGroup i18n via the keyboard shortcuts i18n contract test plus pseudo-locale generation and validate-translations.cjs.
|
||
- T208: Verified in commitGroup i18n via the keyboard shortcuts i18n contract test plus pseudo-locale generation and validate-translations.cjs.
|
||
- T209: Verified in commitGroup i18n via the keyboard shortcuts i18n contract test plus pseudo-locale generation and validate-translations.cjs.
|
||
- Checks: `node scripts/validate-translations.cjs` passed with pre-existing Polish plural warnings only; `npx vitest run --config vitest.config.ts src/keyboard-shortcuts/i18n.contract.test.ts src/keyboard-shortcuts/display.test.tsx` passed; `npx tsc --noEmit -p packages/ui/tsconfig.json` passed.
|
||
- Additional check: `npm run typecheck --workspace server` passed for i18n wiring.
|
||
|
||
## 2026-05-19 — architecture-guard checklist reconciliation
|
||
- F005: Verified existing ShortcutStorage adapter/default memory storage/dependency guard implementation and marked complete under the current architecture-guard checklist ids.
|
||
- F006: Verified existing ShortcutStorage adapter/default memory storage/dependency guard implementation and marked complete under the current architecture-guard checklist ids.
|
||
- F007: Verified existing ShortcutStorage adapter/default memory storage/dependency guard implementation and marked complete under the current architecture-guard checklist ids.
|
||
- T012: Verified with guard/storage tests and Nx graph baseline check; marked complete under the current architecture-guard checklist ids.
|
||
- T013: Verified with guard/storage tests and Nx graph baseline check; marked complete under the current architecture-guard checklist ids.
|
||
- T014: Verified with guard/storage tests and Nx graph baseline check; marked complete under the current architecture-guard checklist ids.
|
||
- Checks: `node --test scripts/tests/guard-keyboard-shortcuts-boundary.test.mjs`, `npx vitest run --config vitest.config.ts src/keyboard-shortcuts/storage.test.tsx`, and `npx nx graph --file=/tmp/project-graph.json && node scripts/guard-keyboard-shortcuts-boundary.mjs --graph /tmp/project-graph.json` passed.
|
||
|
||
## 2026-05-19 — regression group implementation
|
||
- F230: Added final regression contract coverage asserting migrated legacy shortcut listeners remain removed after replacement actions are present.
|
||
- F231: Added SSR/client-only regression coverage for platform-sensitive shortcut components and navigator access ordering.
|
||
- F232: Added regression coverage proving DatePicker, SearchableSelect, AsyncSearchableSelect, TagInput, and TagInputInline keep local onKeyDown handling and do not register shared shortcut actions.
|
||
- T231: Covered by `regression.contract.test.ts` widget-local assertions.
|
||
- T232: Covered by `regression.contract.test.ts` client-only/platform assertions plus existing `platform.test.ts`.
|
||
- T233: Covered by `regression.contract.test.ts` migrated legacy-listener assertions plus phase contract tests.
|
||
- Checks: `npx vitest run --config vitest.config.ts src/keyboard-shortcuts/regression.contract.test.ts src/keyboard-shortcuts/global-migration.contract.test.ts src/keyboard-shortcuts/panels-drawers.contract.test.ts src/keyboard-shortcuts/editors.contract.test.ts src/keyboard-shortcuts/platform.test.ts` passed; `npx tsc --noEmit -p packages/ui/tsconfig.json` passed.
|
||
|
||
## 2026-05-19 — Gap analysis (post-implementation review) → commitGroup `gap-hardening`
|
||
|
||
A full end-to-end read of the branch surfaced structural gaps. Finding #1 was
|
||
already captured by the prior `customization-wiring` group (F360-F366 /
|
||
T360-T364, FR34) — verified, not re-added. The remaining three findings are
|
||
added as **F367-F373 / T365-T373 under commitGroup `gap-hardening`**
|
||
(FR35/FR36/FR37). The loop should not commit this group until all of its
|
||
items are `implemented: true` (one-commit-per-group rule).
|
||
|
||
- (FR35, F367-F369/T365-T366) **Catalog is not the source of truth.**
|
||
`catalog.ts` carries scope/priority/bindings, but registration sites
|
||
hand-author `ShortcutAction` literals. Concrete divergence: only
|
||
`useDesignerShortcuts` sets `priority: 60`; `DefaultLayout`,
|
||
both `DrawerContext`, `SearchPalette`, `TicketNavigation`,
|
||
`AssetDashboardClient` omit `priority` so the provider uses `?? 0`,
|
||
contradicting `catalog.ts` `DEFAULT_PRIORITY` (panel 40 / page 20 / editor
|
||
60). The settings UI/help show catalog priorities; dispatch uses different
|
||
ones. Fix = a catalog-derived factory (id + handler only) + a drift guard
|
||
(unit + extend `guard-keyboard-shortcuts-boundary.mjs`). This is distinct
|
||
from FR30 (import boundary), which the guard already enforces correctly.
|
||
|
||
- (FR36, F370-F371/T367-T368) **Active-region gating is a no-op.**
|
||
`DefaultLayout` calls `useShortcutActiveRegion(true)` unconditionally, so
|
||
`activeRegionsRef` is always non-empty while the MSP shell is mounted →
|
||
`requiresActiveRegion()` never gates anything. `global.quickCreate` (`c`),
|
||
`selection.next` (`j`), `selection.previous` (`k`) fire on any non-editable
|
||
focus app-wide, the opposite of FR10's intent. Fix = register an active
|
||
region only from genuine roving-focus list/selection containers via a
|
||
shared wrapper/hook.
|
||
|
||
- (FR37, F372-F373/T369-T373) **Contract tests are source greps, not
|
||
behavioral.** Every `*.contract.test.ts` (global-migration, panels-drawers,
|
||
editors, persistence-bridge, settings-ui, regression, i18n) is
|
||
`readFileSync` + `.toContain('...')`. They assert code *exists*, never that
|
||
it *works* — which is why findings #1-#3 shipped "green". `T360-T364`
|
||
already add behavioral coverage for the customization path; `gap-hardening`
|
||
converts the remaining grep suites to behavioral and adds a meta test-guard
|
||
so a new grep-only contract test fails CI.
|
||
|
||
- Uncommitted working tree at review time: `DefaultLayout.tsx` carries the
|
||
F095 nav handlers (legit, not yet committed); `features.json`/`tests.json`
|
||
have incidental `\uXXXX` escape churn from a JSON re-serializer (the new
|
||
`gap-hardening` items are authored in plain ASCII to avoid adding to it).
|
||
These are pre-existing and out of scope for `gap-hardening` — flag for the
|
||
branch owner, do not bundle into this group's commit.
|
||
|
||
## 2026-05-19 — navigation group verification
|
||
- T095: Added `packages/ui/src/keyboard-shortcuts/navigation.test.tsx` to
|
||
exercise the catalogued `navigation.goTickets`/`goAssets`/`goClients`
|
||
sequence actions through `KeyboardShortcutsProvider`. The test verifies
|
||
`g t`, `g a`, and `g c` dispatch to their handlers and that the same
|
||
sequence is suppressed while typing in an input.
|
||
|
||
## 2026-05-19 — customization-wiring group implementation
|
||
- F043/F140/F142/F146/F185/F186/F360-F366: Connected the preference adapter
|
||
end-to-end. `MspLayoutClient` now injects `useKeyboardShortcutPreferenceStorage`
|
||
into `KeyboardShortcutsProvider`; the provider loads the persisted blob,
|
||
keeps it as reactive context state, resolves dispatch through
|
||
`resolveActionBindings`, merges `preferences.disabled`, and exposes resolved
|
||
bindings plus set/disable/reset mutators. `ShortcutHint`, `useAriaKeyShortcuts`,
|
||
`ShortcutHelpDialog`, and `KeyboardShortcutsSettings` now read the same
|
||
provider-owned source instead of separate defaults/preference hooks.
|
||
- T360-T363: Added
|
||
`packages/ui/src/keyboard-shortcuts/customization-wiring.test.tsx`. The test
|
||
covers rebind -> new combo dispatches and old default stops, Effective +
|
||
`ShortcutHint` + `aria-keyshortcuts` update, disable stops dispatch and hides
|
||
help, reset-one/reset-all live-update, and persisted storage loads before
|
||
dispatch.
|
||
- T364/checks: `npx nx graph --file=/tmp/project-graph.json && node
|
||
scripts/guard-keyboard-shortcuts-boundary.mjs --graph /tmp/project-graph.json`
|
||
passed. Focused tests passed:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/customization-wiring.test.tsx src/keyboard-shortcuts/provider.test.tsx src/keyboard-shortcuts/storage.test.tsx src/keyboard-shortcuts/display.test.tsx src/keyboard-shortcuts/settings-ui.contract.test.ts src/keyboard-shortcuts/persistence-bridge.contract.test.ts src/keyboard-shortcuts/global-migration.contract.test.ts`.
|
||
Type checks passed: `npx tsc --noEmit -p packages/ui/tsconfig.json` and
|
||
`npm run typecheck --workspace server`.
|
||
|
||
## 2026-05-19 — page-actions group implementation
|
||
- F300-F302/F310: Added `page.create` to the catalog with default `c`, kept
|
||
`mod+n` only as an optional alternate with a browser-owned new-window caveat,
|
||
and added shared `usePageCreateShortcut`/`usePageSaveShortcut` helpers in
|
||
`packages/ui/src/keyboard-shortcuts/page-actions.ts`. Page-scoped actions are
|
||
now suppressed when panel/dialog/editor scope is active, in addition to the
|
||
existing editable-target suppression.
|
||
- F303-F308: Wired page create shortcuts to existing create controls:
|
||
`TicketingDashboard` (ticket), `Clients` (client), `Contacts` (contact),
|
||
`InteractionsFeed` (interaction), `Projects` (project), and
|
||
`AssetDashboardClient` (asset).
|
||
- F309: Wired page save to primary editable Save controls found in this pass:
|
||
`ClientDetails` (`handleSave`) and ticket `TicketInfo`
|
||
(`handleSaveChanges`). I did not find a route-level project-detail Save
|
||
equivalent; project detail appears to save scoped phase/task edits through
|
||
local controls.
|
||
- F311/T304: Added `actions.page.create` label/description to en/fr/es/de/nl/it/pl/pt
|
||
and regenerated xx/yy pseudo-locales.
|
||
- Tests/checks: Added `page-actions.test.tsx` for catalog/default/optional
|
||
alternate behavior, create/save dispatch, editable suppression, and
|
||
panel-scope suppression. Added `page-actions.source.test.ts` to smoke the
|
||
page component wiring. Passed:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/page-actions.test.tsx src/keyboard-shortcuts/page-actions.source.test.ts src/keyboard-shortcuts/provider.test.tsx src/keyboard-shortcuts/catalog.test.ts src/keyboard-shortcuts/i18n.contract.test.ts`;
|
||
`node scripts/validate-translations.cjs` (pre-existing Polish plural
|
||
warnings only); `npx tsc --noEmit -p packages/ui/tsconfig.json`;
|
||
`npx tsc --noEmit -p packages/tickets/tsconfig.json`;
|
||
`npx tsc --noEmit -p packages/clients/tsconfig.json`;
|
||
`npx tsc --noEmit -p packages/projects/tsconfig.json`;
|
||
`npx tsc --noEmit -p packages/assets/tsconfig.json`; and
|
||
`npm run typecheck --workspace server`.
|
||
|
||
## 2026-05-19 — dialog-a11y group implementation
|
||
- F320-F324/F330: Moved the shared create-dialog keyboard contract into
|
||
`packages/ui/src/components/Dialog.tsx`: store/restore invoker focus,
|
||
focus the first focusable field on open when no custom `onOpenAutoFocus` is
|
||
supplied, keep Radix modal focus trapping enabled, and submit the first form
|
||
on Ctrl/Cmd+Enter even from textarea/editor-like fields. Escape remains
|
||
routed through the existing Radix owner bridge.
|
||
- F325-F329: Removed `disableFocusTrap` from Create Ticket, Create Client,
|
||
Create Contact, Create Project, and Create Asset dialogs. Create Interaction
|
||
was already on the focus-trapped shared Dialog path.
|
||
- T320-T326: Added `dialog-a11y.test.tsx` for first-field focus, invoker focus
|
||
restore, Escape close, and mod+Enter form submit. Added
|
||
`dialog-a11y.source.test.ts` to smoke that all create dialogs use shared
|
||
Dialog, keep forms, and no longer opt out of focus trapping.
|
||
- Checks passed:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/dialog-a11y.test.tsx src/keyboard-shortcuts/dialog-a11y.source.test.ts src/keyboard-shortcuts/escape.test.tsx`;
|
||
`npx tsc --noEmit -p packages/ui/tsconfig.json`;
|
||
`npx tsc --noEmit -p packages/tickets/tsconfig.json`;
|
||
`npx tsc --noEmit -p packages/clients/tsconfig.json`;
|
||
`npx tsc --noEmit -p packages/projects/tsconfig.json`;
|
||
`npx tsc --noEmit -p packages/assets/tsconfig.json`; and
|
||
`npm run typecheck --workspace server`.
|
||
|
||
## 2026-05-19 — command-palette group implementation
|
||
- F340-F353: Implemented the command palette overlay opened by `global.search`
|
||
(`mod+k`) from `SearchPalette`. The overlay merges navigation menu entries,
|
||
catalog shortcut actions, and existing record typeahead search; supports
|
||
keyboard navigation/activation, Escape close, localStorage frequency boosts,
|
||
accessible combobox/listbox roles, live result counts, visible `mod+k` hint,
|
||
and in-palette syntax help.
|
||
- F343-F347: Added pure UI-decoupled
|
||
`packages/ui/src/keyboard-shortcuts/command-palette-query.ts` parser with
|
||
field aliases, sigils, quoted phrases, exclusion, wildcards, fuzzy suffixes,
|
||
OR/AND metadata, and `$mine`/`$recent`/`$open` aliases.
|
||
- F350/F352: Added command palette chrome/syntax/type labels across
|
||
en/fr/es/de/nl/it/pl/pt, regenerated pseudo locales, and added a shortcut
|
||
help-dialog link to the palette syntax affordance.
|
||
- T340-T349: Covered by parser unit tests, command palette source wiring smoke,
|
||
translation validation, server/UI typecheck, and the existing keyboard
|
||
shortcut dependency-boundary/graph guard.
|
||
- Checks passed:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts/command-palette-query.test.ts src/keyboard-shortcuts/command-palette.source.test.ts`;
|
||
`npx tsc --noEmit -p packages/ui/tsconfig.json`;
|
||
`npm run typecheck --workspace server`;
|
||
`node scripts/generate-pseudo-locales.cjs`;
|
||
`node scripts/validate-translations.cjs` (pre-existing Polish plural
|
||
warnings only); and
|
||
`npx nx graph --file=/tmp/project-graph.json && node scripts/guard-keyboard-shortcuts-boundary.mjs --graph /tmp/project-graph.json`.
|
||
|
||
## 2026-05-19 — gap-hardening group implementation
|
||
- F367-F369: Added catalog-derived `createShortcutAction` and
|
||
`useCatalogShortcut`, then migrated SearchPalette, DefaultLayout, both
|
||
DrawerContexts, TicketNavigation, AssetDashboardClient, and invoice designer
|
||
shortcuts away from hand-authored metadata. The boundary guard now also
|
||
checks these registration sites for hand-authored shortcut metadata and
|
||
unknown catalog IDs.
|
||
- F370-F371: Removed the unconditional `useShortcutActiveRegion(true)` from
|
||
`DefaultLayout`. Added shared `ShortcutActiveRegion`, which registers an
|
||
active region only while focus is within the real list region, and applied it
|
||
to ticket/client/contact/interaction/project/asset list surfaces.
|
||
- F372-F373: Added behavioral gap-hardening coverage for global, panel, editor,
|
||
active-region, and settings preference behavior; added a contract-test guard
|
||
so grep-only contract tests must carry behavioral coverage. Existing source
|
||
smoke tests were updated to assert the catalog-hook wiring rather than stale
|
||
metadata literals.
|
||
- Checks passed:
|
||
`npx vitest run --config vitest.config.ts src/keyboard-shortcuts`;
|
||
`node --test scripts/tests/guard-keyboard-shortcuts-contract-tests.test.mjs scripts/tests/guard-keyboard-shortcuts-boundary.test.mjs`;
|
||
`node scripts/guard-keyboard-shortcuts-contract-tests.mjs`;
|
||
`npx tsc --noEmit -p packages/ui/tsconfig.json`;
|
||
`npx tsc --noEmit -p packages/tickets/tsconfig.json`;
|
||
`npx tsc --noEmit -p packages/clients/tsconfig.json`;
|
||
`npx tsc --noEmit -p packages/assets/tsconfig.json`;
|
||
`npx tsc --noEmit -p packages/projects/tsconfig.json`;
|
||
`npx tsc --noEmit -p packages/billing/tsconfig.json`;
|
||
`npm run typecheck --workspace server`; and
|
||
`npx nx graph --file=/tmp/project-graph.json && node scripts/guard-keyboard-shortcuts-boundary.mjs --graph /tmp/project-graph.json && node scripts/guard-keyboard-shortcuts-contract-tests.mjs`.
|
||
|
||
## 2026-05-20 — shortcuts-ui-redesign (Profile move + visual keyboard cheatsheet)
|
||
|
||
Built directly (not via loop) because the work is high-fidelity visual +
|
||
judgment-heavy and the loop has no signal for either. Source: design handoff in
|
||
`~/Downloads/design_handoff_keyboard_shortcuts/` (variation-c is canonical).
|
||
|
||
- Root cause of "no nav tab" turned out NOT to be the AlgaDesk allowlist
|
||
(user is on full PSA). The real cause: `SettingsPage.tsx` does not render
|
||
the tab strip itself — the settings nav is driven by the **Sidebar in
|
||
settings mode** (`Sidebar.tsx:111`, `DefaultLayout.tsx:52`), a separate
|
||
curated menu that never got a `keyboard-shortcuts` entry. The content
|
||
route worked, but there was nothing to click. `settings-ui.contract.test.ts`
|
||
was a grep that only asserted the `id`/`icon`/`<Component/>` strings were
|
||
present in SettingsPage source, never that the nav surfaced it — another
|
||
FR37-class miss the gap-hardening group didn't cover (it converted engine
|
||
contract tests, not the server-side settings one).
|
||
- Decision: move to **Profile** sub-tab (matches per-user preference
|
||
convention; uses real `<CustomTabs tabs={tabContent}>` so the tab strip
|
||
appears immediately; sidesteps any allowlist). Added entry to
|
||
`UserProfile.tsx tabContent`, deleted `KeyboardShortcutsSettings.tsx` and
|
||
its `SettingsPage` registration + `Keyboard` lucide import. Added
|
||
`'keyboard-shortcuts'` to `BASE_PROFILE_TABS` (`calendarAvailability.ts`)
|
||
so `/msp/profile?tab=keyboard-shortcuts` deep-links.
|
||
- Engine: extended `PersistedShortcuts` to v2 with `profile: string` and a
|
||
v1→v2 migration; `SHORTCUT_PROFILES` ships `default` / `vim` / `emacs`
|
||
with parser-valid neutral single-chord deltas keyed by real catalog ids.
|
||
**Multi-chord emacs sequences (`mod+x mod+s`) were deliberately NOT
|
||
assigned to non-sequence actions** (page.save etc.) — they would parse-fail
|
||
and silently never dispatch. Resolution = user override → profile delta →
|
||
platform default, inside `resolveActionBindings` so dispatch + hints +
|
||
ARIA + panel all read the same effective binding. `setActionBindingsDelta`
|
||
now drops overrides equal to the **profile baseline**, so per-action reset
|
||
returns to the active profile (not raw factory). Provider exposes
|
||
`profile` + `setProfile`; `useKeyboardShortcutPreferences` includes them.
|
||
Vim/Emacs deltas are best-guess pending team confirmation (open Q in handoff).
|
||
- Panel: `server/src/components/keyboard-shortcuts/KeyboardShortcutsPanel.tsx`
|
||
recreates variation-c using product CSS vars (`--color-primary-*`,
|
||
`--color-card`, `--radius-md`, `--font-mono` — globals.css already defines
|
||
these) + Radix-based `ConfirmationDialog`/`Switch`/`LoadingIndicator`. Real
|
||
`keydown` capture via window-level capture-phase listener with
|
||
`stopImmediatePropagation` so the provider dispatch doesn't double-fire
|
||
during rebind. Conflict UX = **prompt-to-reassign** (the new binding takes
|
||
over, the previous owner is left unbound) per user decision. Override
|
||
scope = **per-account only** (`useUserPreference`); per-device deferred.
|
||
Unmappable keys (`?`, `Escape`, `Delete`, sequences) route to the chord
|
||
rail instead of disappearing — improvement over the prototype.
|
||
- Tests: added `profiles.test.ts` (7 behavioral cases for the profile layer).
|
||
Rewrote `settings-ui.contract.test.ts` to assert the new placement + panel
|
||
wiring (with `@behavioralCoverage` linking the real behavioral suites).
|
||
Updated `i18n.contract.test.ts` to assert `profile.tabs.keyboardShortcuts`
|
||
in `en/msp/profile.json` and that `SettingsPage` no longer references the
|
||
removed tab. Added the key to all production+pseudo locales for parity
|
||
and ran `generate-pseudo-locales.cjs` + `validate-translations.cjs`
|
||
(0 errors, 8 pre-existing Polish plural warnings).
|
||
- Verification: `npx vitest run --config vitest.config.ts src/keyboard-shortcuts`
|
||
→ 29 files / 137 tests pass; `npx tsc --noEmit -p packages/ui/tsconfig.json`
|
||
clean; `npm run typecheck --workspace server` clean;
|
||
`node scripts/guard-keyboard-shortcuts-boundary.mjs` OK; meta contract-test
|
||
guard OK; `node scripts/validate-translations.cjs` PASSED.
|
||
- Follow-up (`F386/T386`, implemented:false): translate panel chrome strings
|
||
(`settings.*`, `profiles.*`, `legend.*`, `settings.chords.*`,
|
||
`settings.conflict.*`, `settings.actions.*`) into the 7 non-EN production
|
||
locales; today the panel falls back via `t(..., { defaultValue })` outside
|
||
EN. Per-device override scope from the handoff is also deferred.
|
||
- Visual QA: not driven here. Browser/conduit QA pending with the user; the
|
||
prototype's `prototype/index.html` is the side-by-side reference.
|