Add global keyboard EventRouter and unify publish (M6, §01D)

One module owns the document keydown listener and dispatches the shortcut map
(Cmd/Ctrl+Shift+N / +K / +S / +, / Escape), platform-aware, via the single-source
focus-utils interactive-context gate. Escape and Cmd/Ctrl+S run before the gate so
save works while editing; the rest are suppressed mid-typing. Publish + its toast
move into services/snippet-actions so the button and Cmd/Ctrl+S behave identically.
Removes the ad-hoc keydown handler from App and Monaco's Cmd+S command.
This commit is contained in:
2026-06-07 17:18:55 +03:00
parent f365258fcc
commit 807d3c8e3c
12 changed files with 401 additions and 48 deletions
+22 -12
View File
@@ -320,7 +320,17 @@ function onKeyDown(e: KeyboardEvent): void {
const mod = isMac ? e.metaKey : e.ctrlKey;
// --- Shortcuts: never fire while typing in an input or Monaco ----------
// Cmd/Ctrl + S -> publish current draft. Checked BEFORE the interactive-context
// gate: it is the canonical "save" shortcut (spec §01D mandates it override the
// browser default), and publishing happens *while editing the draft in Monaco* —
// gating it behind "not typing" would defeat its purpose.
if (mod && !e.shiftKey && e.key.toLowerCase() === 's') {
e.preventDefault(); // override the browser "save page" dialog
useSnippetStore.getState().publishDraft();
return;
}
// --- Remaining shortcuts: never fire while typing in an input or Monaco ----
if (isInInteractiveContext()) return;
// Cmd/Ctrl + Shift + N -> new snippet
@@ -336,16 +346,12 @@ function onKeyDown(e: KeyboardEvent): void {
toggleDatasets();
return;
}
// Cmd/Ctrl + S -> publish current draft
if (mod && e.key.toLowerCase() === 's') {
e.preventDefault(); // override the browser "save page" dialog
useSnippetStore.getState().publishDraft();
return;
}
// Cmd/Ctrl + , -> settings (through the coordinator: snapshot + URL sync)
// Cmd/Ctrl + , -> open the editor settings popover. Settings are distributed to
// per-pane disclosure popovers, not a modal (spec §07), so this opens the editor
// cluster (openSettingsPopover) — there is no 'settings' modal to open.
if (mod && e.key === ',') {
e.preventDefault();
openModal('settings');
openSettingsPopover('editor-settings');
return;
}
}
@@ -392,9 +398,13 @@ by inserting a rung at the right priority — never by sprinkling
`e.preventDefault()` so Cmd/Ctrl+S does not trigger "save page", Cmd/Ctrl+K
does not focus the browser search bar, etc.
Note the asymmetry: **Escape is checked before the interactive-context gate**
(you want Escape to dismiss a modal even while focus is in the editor), whereas
all other shortcuts are checked **after** the gate (so they don't fire mid-typing).
Note the asymmetry: **Escape and Cmd/Ctrl+S are checked before the
interactive-context gate.** Escape, so it dismisses a modal even while focus is in
the editor; Cmd/Ctrl+S, because publishing the draft is something you do _while_
editing it — gating "save" behind "not typing" would defeat it (and it must
override the browser's "save page" regardless). The remaining shortcuts (new
snippet, toggle Datasets, settings) are checked **after** the gate, so they never
fire mid-typing or steal a key the editor wants (e.g. Monaco's own Cmd+K chord).
### 2.2 The single-source helper: `orchestration/focus-utils.ts`