Capture UX: "+ New" header button + type-to-compose (two-mode board keys)
Live-pass feedback: the wide "Take a note…" bar showed through behind the
compose modal and felt redundant. Per operator choice, remove the bar and
make capture header-button + keyboard driven.
- Removed the board's inline "Take a note…" trigger bar.
- AppShell gains a "+ New" header button (next to search) — navigates to the
board if needed, then opens the compose modal. The `c` shortcut now does
the same (unified with newNote()).
- Type-to-compose: on the board with no card focused, any other single
printable key opens a new note SEEDED with that key (AppShell onKeydown
fall-through, after the reserved / c ? g shortcuts). Seed travels via
ui.composeSeed → NoteEditor's new `initialBody` prop; caret placed at end.
- Two-mode board keyboard (BoardView): RESTING = arrows enter browse, letters
type-to-compose; BROWSING (a card focused) = j/k move, e/x/# act, Enter
opens, Esc exits to resting. ui.boardCardFocused tells the global handler
to stand down while browsing so it doesn't swallow card keys.
- Updated the shortcuts help + the empty-state copy ("Hit + New — or just
start typing").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
@@ -74,22 +74,32 @@ function moveFocus(delta: number) {
|
||||
focusedIndex.value < 0 ? 0 : Math.min(Math.max(focusedIndex.value + delta, 0), count - 1);
|
||||
}
|
||||
|
||||
// Two-mode board keyboard. RESTING (no card focused): letters type-to-compose,
|
||||
// handled globally in AppShell; here only the ARROWS enter browse mode. BROWSING (a
|
||||
// card focused): j/k move, e/x/# act, Enter opens, Esc exits back to resting.
|
||||
function onBoardKey(e: KeyboardEvent) {
|
||||
if (editing.value) return; // the editor modal owns the keyboard while open
|
||||
if (editing.value || composing.value) return; // a modal owns the keyboard
|
||||
const el = e.target as HTMLElement | null;
|
||||
if (el && (["INPUT", "TEXTAREA", "BUTTON", "A"].includes(el.tagName) || el.isContentEditable)) return;
|
||||
if (el && (["INPUT", "TEXTAREA", "BUTTON", "A", "SELECT"].includes(el.tagName) || el.isContentEditable)) return;
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
const key = e.key;
|
||||
if (key === "j" || key === "ArrowDown" || key === "ArrowRight") {
|
||||
const browsing = focusedIndex.value >= 0;
|
||||
if (key === "ArrowDown" || key === "ArrowRight" || (browsing && key === "j")) {
|
||||
e.preventDefault();
|
||||
moveFocus(1);
|
||||
return;
|
||||
}
|
||||
if (key === "k" || key === "ArrowUp" || key === "ArrowLeft") {
|
||||
if (key === "ArrowUp" || key === "ArrowLeft" || (browsing && key === "k")) {
|
||||
e.preventDefault();
|
||||
moveFocus(-1);
|
||||
return;
|
||||
}
|
||||
if (!browsing) return; // resting → let AppShell's type-to-compose take the key
|
||||
if (key === "Escape") {
|
||||
e.preventDefault();
|
||||
focusedIndex.value = -1; // leave browse mode, back to type-to-capture
|
||||
return;
|
||||
}
|
||||
const note = orderedNotes.value[focusedIndex.value];
|
||||
if (!note) return;
|
||||
if (key === "Enter") {
|
||||
@@ -107,6 +117,9 @@ function onBoardKey(e: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// Publish browse-mode state so AppShell's type-to-compose stands down while browsing.
|
||||
watch(focusedIndex, (i) => (ui.boardCardFocused = i >= 0));
|
||||
|
||||
// Keep the selection valid as the list changes / the view switches.
|
||||
watch(
|
||||
() => orderedNotes.value.length,
|
||||
@@ -122,7 +135,7 @@ const emptyState = computed(() => {
|
||||
if (currentView.value === "archived")
|
||||
return { title: "Nothing archived", subtitle: "Archived notes are tucked away here." };
|
||||
if (currentLabel.value) return { title: "No notes with this label", subtitle: "Tag a note to see it here." };
|
||||
return { title: "No notes yet", subtitle: "Capture your first thought in the box above." };
|
||||
return { title: "No notes yet", subtitle: "Hit + New — or just start typing — to capture a thought." };
|
||||
});
|
||||
|
||||
async function reload() {
|
||||
@@ -138,7 +151,10 @@ onMounted(() => {
|
||||
void reload();
|
||||
window.addEventListener("keydown", onBoardKey);
|
||||
});
|
||||
onBeforeUnmount(() => window.removeEventListener("keydown", onBoardKey));
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("keydown", onBoardKey);
|
||||
ui.boardCardFocused = false;
|
||||
});
|
||||
watch([currentView, currentLabel, facetKey], reload);
|
||||
|
||||
function openEditor(note: Note) {
|
||||
@@ -183,14 +199,6 @@ async function onDrop(target: Note) {
|
||||
|
||||
<template>
|
||||
<div class="mx-auto w-full max-w-6xl px-4 py-6">
|
||||
<button
|
||||
v-if="isMainBoard"
|
||||
type="button"
|
||||
class="mx-auto mb-6 block w-full max-w-xl rounded-xl border border-neutral-200 bg-white px-4 py-3 text-left text-sm text-neutral-500 shadow-sm transition hover:shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-400"
|
||||
@click="composing = true"
|
||||
>
|
||||
Take a note…
|
||||
</button>
|
||||
<FilterBar v-if="isMainBoard" />
|
||||
|
||||
<div v-if="notes.loading" class="py-24 text-center text-sm text-neutral-400">Loading…</div>
|
||||
@@ -267,6 +275,11 @@ async function onDrop(target: Note) {
|
||||
</div>
|
||||
|
||||
<template v-if="composing || editing">
|
||||
<NoteEditor :note="editing" @close="closeEditor" @navigate="onNavigate" />
|
||||
<NoteEditor
|
||||
:note="editing"
|
||||
:initial-body="composing ? ui.composeSeed : ''"
|
||||
@close="closeEditor"
|
||||
@navigate="onNavigate"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user