From 47974f62ee29339bbbf2959afd2553ca3f68c346 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 22 Jul 2026 14:53:18 -0400 Subject: [PATCH] M7: editor keyboard + one-click card toolbars (live-pass fixes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the unified-editor live pass (task 1922): - Esc now returns to the base Notes board from any view (AppShell); an open editor consumes Esc first (its handler stops propagation) so it closes before navigating. - Ctrl/Cmd+Enter = finish & close in BOTH frames — it already closed the modal editor; now it also commits+collapses the board composer (matches the email/chat 'send' convention). Shift+Enter still saves & starts a new note. - Fix: the inline composer committed on document mousedown, collapsing and shifting the board before a click landed — so clicking another card's pin/archive/trash took two clicks. Commit on click (bubble phase) instead; the target's action fires first, then the composer closes. Pure frontend. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm --- frontend/src/components/AppShell.vue | 10 ++++++++-- frontend/src/components/NoteEditor.vue | 15 ++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/AppShell.vue b/frontend/src/components/AppShell.vue index b7f8741..e8cf5af 100644 --- a/frontend/src/components/AppShell.vue +++ b/frontend/src/components/AppShell.vue @@ -36,9 +36,9 @@ const shortcuts = [ { label: "Move card focus", keys: ["j", "k"] }, { label: "Open focused card", keys: ["Enter"] }, { label: "Pin / archive / trash card", keys: ["#", "e", "x"] }, - { label: "Save & close (editor)", keys: ["⌘/Ctrl", "Enter"] }, + { label: "Finish & close note", keys: ["⌘/Ctrl", "Enter"] }, { label: "Save & new (composer)", keys: ["Shift", "Enter"] }, - { label: "Dismiss / close", keys: ["Esc"] }, + { label: "Close / back to Notes", keys: ["Esc"] }, { label: "This help", keys: ["?"] }, ]; @@ -84,6 +84,12 @@ function onKeydown(e: KeyboardEvent) { drawer.value = false; return; } + // Esc with nothing open = back to the base Notes board. (An open editor gets Esc + // first via its own handler, which stops propagation so it doesn't also navigate.) + if (e.key === "Escape") { + if (route.name !== "board") void router.push("/"); + return; + } if (e.metaKey || e.ctrlKey || e.altKey) return; if (gPending) { gPending = false; diff --git a/frontend/src/components/NoteEditor.vue b/frontend/src/components/NoteEditor.vue index cf92c7d..66a3e64 100644 --- a/frontend/src/components/NoteEditor.vue +++ b/frontend/src/components/NoteEditor.vue @@ -192,12 +192,17 @@ function onEsc(): void { else void close(); } function onMetaEnter(): void { - if (!props.inline) void close(); + // Ctrl/Cmd+Enter = finish & close, in both frames (matches email/chat "send"). + if (props.inline) void commitInline(); + else void close(); } function onBackdropMousedown(): void { if (!props.inline) void close(); } -function onDocMousedown(e: MouseEvent): void { +function onDocClick(e: MouseEvent): void { + // Commit the inline composer on an OUTSIDE *click* (bubble phase, not mousedown): + // the clicked target's own handler fires first, so clicking another card's toolbar + // performs its action, THEN the composer collapses — no wasted first click. if (props.inline && expanded.value && root.value && !root.value.contains(e.target as Node)) { void commitInline(); } @@ -230,14 +235,14 @@ onMounted(async () => { void titles.load(); void loadBacklinks(); if (props.inline) { - document.addEventListener("mousedown", onDocMousedown); + document.addEventListener("click", onDocClick); if (props.autofocus) void open(); } else { await nextTick(); bodyInput.value?.focus(); } }); -onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown)); +onBeforeUnmount(() => document.removeEventListener("click", onDocClick)); // ---- outgoing links (edit mode) ---- const outgoingLinks = computed(() => { @@ -489,7 +494,7 @@ defineExpose({ open }); " :role="inline ? undefined : 'dialog'" :aria-modal="inline ? undefined : 'true'" - @keydown.esc="onEsc" + @keydown.esc.stop="onEsc" @keydown.enter.meta.prevent="onMetaEnter" @keydown.enter.ctrl.prevent="onMetaEnter" @paste="onPaste"