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:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useSessionStore } from "../stores/session";
|
||||
import { useConfigStore } from "../stores/config";
|
||||
@@ -42,18 +42,18 @@ let searchTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const shortcuts = [
|
||||
{ label: "Command palette", keys: ["⌘/Ctrl", "K"] },
|
||||
{ label: "New note", keys: ["c"] },
|
||||
{ label: "New note (or just start typing)", keys: ["c"] },
|
||||
{ label: "Search", keys: ["/"] },
|
||||
{ label: "Go to Board", keys: ["g", "b"] },
|
||||
{ label: "Go to Graph", keys: ["g", "g"] },
|
||||
{ label: "Go to Reminders", keys: ["g", "r"] },
|
||||
{ label: "Go to Timeline", keys: ["g", "t"] },
|
||||
{ label: "Move card focus", keys: ["j", "k"] },
|
||||
{ label: "Open focused card", keys: ["Enter"] },
|
||||
{ label: "Browse cards", keys: ["↑", "↓", "←", "→"] },
|
||||
{ label: "Move / open focused card", keys: ["j", "k", "Enter"] },
|
||||
{ label: "Pin / archive / trash card", keys: ["#", "e", "x"] },
|
||||
{ label: "Finish & close note", keys: ["⌘/Ctrl", "Enter"] },
|
||||
{ label: "Save & new (composer)", keys: ["Shift", "Enter"] },
|
||||
{ label: "Close / back to Notes", keys: ["Esc"] },
|
||||
{ label: "Save & new (composing)", keys: ["Shift", "Enter"] },
|
||||
{ label: "Stop browsing / back to Notes", keys: ["Esc"] },
|
||||
{ label: "This help", keys: ["?"] },
|
||||
];
|
||||
|
||||
@@ -70,11 +70,14 @@ function focusSearch() {
|
||||
searchInput.value?.select();
|
||||
}
|
||||
|
||||
// `c` composes: jump to the board (which autofocuses its quick-add) or, if
|
||||
// already there, ask the board to reopen the composer.
|
||||
function compose() {
|
||||
if (route.name !== "board") void router.push("/");
|
||||
else ui.requestCompose();
|
||||
// New note (the "+ New" button and the `c` shortcut): ensure we're on the board,
|
||||
// then open the compose modal. Type-to-compose seeds it instead (see onKeydown).
|
||||
async function newNote() {
|
||||
if (route.name !== "board") {
|
||||
await router.push("/");
|
||||
await nextTick();
|
||||
}
|
||||
ui.requestCompose();
|
||||
}
|
||||
|
||||
// Global keyboard shortcuts, active only inside the authed shell. Ignored while
|
||||
@@ -137,7 +140,7 @@ function onKeydown(e: KeyboardEvent) {
|
||||
}
|
||||
if (e.key === "c") {
|
||||
e.preventDefault();
|
||||
compose();
|
||||
void newNote();
|
||||
return;
|
||||
}
|
||||
if (e.key === "?") {
|
||||
@@ -149,6 +152,13 @@ function onKeydown(e: KeyboardEvent) {
|
||||
gPending = true;
|
||||
clearTimeout(gTimer);
|
||||
gTimer = setTimeout(() => (gPending = false), 800);
|
||||
return;
|
||||
}
|
||||
// Type-to-compose: any OTHER single printable key, on the board with no card
|
||||
// focused (browse mode owns j/k/e/x/#), opens a new note seeded with that key.
|
||||
if (route.name === "board" && !ui.boardCardFocused && e.key.length === 1 && /\S/.test(e.key)) {
|
||||
e.preventDefault();
|
||||
ui.requestCompose(e.key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +250,14 @@ async function signOut() {
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-lg bg-brand px-2.5 py-1.5 text-sm font-semibold text-neutral-900 hover:brightness-95 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand"
|
||||
title="New note (or just start typing)"
|
||||
@click="newNote"
|
||||
>
|
||||
<Icon name="plus" /> <span class="hidden sm:inline">New</span>
|
||||
</button>
|
||||
<span class="hidden text-sm text-neutral-500 md:inline dark:text-neutral-400">{{
|
||||
session.user?.display_name
|
||||
}}</span>
|
||||
|
||||
@@ -18,8 +18,9 @@ import { LABEL_CHIP_CLASSES, type NoteColor } from "../notes/colors";
|
||||
// or null to compose a new one. In compose mode there is no note id until a draft is
|
||||
// persisted — on commit-with-content, or on the first rich action (label / image /
|
||||
// reminder / checklist) — so we never litter empty notes.
|
||||
const props = withDefaults(defineProps<{ note?: Note | null }>(), {
|
||||
const props = withDefaults(defineProps<{ note?: Note | null; initialBody?: string }>(), {
|
||||
note: null,
|
||||
initialBody: "",
|
||||
});
|
||||
const emit = defineEmits<{ (e: "close"): void; (e: "navigate", id: string): void }>();
|
||||
const notes = useNotesStore();
|
||||
@@ -28,7 +29,7 @@ const titles = useTitlesStore();
|
||||
|
||||
const noteId = ref<string | null>(props.note?.id ?? null);
|
||||
const title = ref(props.note?.title ?? "");
|
||||
const body = ref(props.note?.body ?? "");
|
||||
const body = ref(props.note?.body ?? props.initialBody);
|
||||
const color = ref<NoteColor>(props.note?.color ?? "default");
|
||||
const labelList = ref<NoteLabel[]>(props.note ? [...props.note.labels] : []);
|
||||
const createKind = ref<"text" | "list">("text"); // compose-only list toggle
|
||||
@@ -206,7 +207,10 @@ onMounted(async () => {
|
||||
void titles.load();
|
||||
void loadBacklinks();
|
||||
await nextTick();
|
||||
bodyInput.value?.focus();
|
||||
const el = bodyInput.value;
|
||||
el?.focus();
|
||||
// Put the caret after any seeded text (type-to-compose) so typing continues cleanly.
|
||||
if (el) el.selectionStart = el.selectionEnd = el.value.length;
|
||||
});
|
||||
|
||||
// ---- outgoing links (edit mode) ----
|
||||
|
||||
Reference in New Issue
Block a user