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
+35
View File
@@ -0,0 +1,35 @@
import { beforeEach, describe, expect, test } from 'vitest';
import { createSnippet } from '@core/snippet';
import { useNotificationStore } from '../stores/NotificationStore';
import { useSnippetStore } from '../stores/SnippetStore';
import { publishActiveSnippet } from './snippet-actions';
const T = new Date('2026-06-01T00:00:00Z');
const snippets = () => useSnippetStore.getState();
const toasts = () => useNotificationStore.getState().notifications;
beforeEach(() => {
useSnippetStore.getState().reset();
useNotificationStore.getState().clear();
});
describe('publishActiveSnippet', () => {
test('publishes the active draft and raises one success toast', () => {
snippets().hydrate([createSnippet({ id: 'a', now: T })], 'a');
snippets().updateDraft('{"published":1}');
publishActiveSnippet();
expect(snippets().snippets.find((s) => s.id === 'a')?.spec).toBe('{"published":1}');
expect(toasts()).toHaveLength(1);
expect(toasts()[0]).toMatchObject({ kind: 'success', title: 'Snippet published' });
});
test('is a silent no-op when no snippet is active', () => {
snippets().hydrate([], null);
publishActiveSnippet();
expect(toasts()).toHaveLength(0);
});
});
+28
View File
@@ -0,0 +1,28 @@
/**
* Snippet action helpers that pair a store mutation with its user-facing
* confirmation, so every trigger of an action behaves identically.
*
* `publishActiveSnippet` is the single publish-with-confirmation path: the
* Publish button (SpecEditor) and the Cmd/Ctrl+S shortcut (EventRouter) both go
* through it, so the success toast (spec §03D) fires no matter how the user
* published. Without this, Cmd+S published silently while the button toasted — a
* trigger-dependent inconsistency.
*/
import { notify } from '../stores/NotificationStore';
import { useSnippetStore } from '../stores/SnippetStore';
/**
* Publish the active snippet's draft and confirm it. No-op (no toast) when no
* snippet is active. The toast copy follows the council's rule (architecture 10
* → Toast copy): the title states the action, the message adds the consequence.
*/
export function publishActiveSnippet(): void {
if (!useSnippetStore.getState().activeSnippetId) return;
useSnippetStore.getState().publish();
notify({
kind: 'success',
title: 'Snippet published',
message: 'Your draft is now the published version.',
});
}