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"