Adds SoftDelete/Trash across characters, chapters, locations, beats with a purge schedule and Trash page. Reworks the web client for keyboard-driven navigation (focus helpers, help overlay, keyboard.md doc). Moves the ChapterPage tag editor to the bottom of the page to match CharacterDetailPage.
3.1 KiB
Keyboard conventions
Novelly's web client is built to be driven entirely from the keyboard. New interactive components should follow these rules so the app stays consistent as it grows.
Escape cancels or closes — it never destroys already-saved work
In an editor that commits per field (a beat row, a chapter's title), Escape reverts only the field you're currently in and then closes the editor. Fields you already tabbed past and committed stay saved — Escape is honest about this, not a full undo. Anywhere a component can offer a true "discard everything" cancel (a create form that hasn't saved anything yet), do that instead.
Enter commits a single-line field and advances
Pressing Enter in a single-line field is equivalent to Tab: it commits the field's value and
moves focus to the next field. Shift+Enter moves to the previous field. This is what
AutoField (src/components/ui.tsx) does by default — reuse it rather than hand-rolling a
text input's key handling.
mod+Enter commits a multiline field or completes a record
A <textarea> needs plain Enter to insert a newline, so multiline fields commit on
mod+Enter (Cmd or Ctrl) instead. The same combo, handled at the row/form level, means "I'm
done with this record" — closing a beat row, submitting a question. This mirrors the app's
original convention in AgentPanel.tsx (mod+Enter sends a message).
Bare single letters create the primary thing on the page
n is the default create-hotkey across the app (new character, new chapter, new location). A
page with a second creatable thing uses a mnemonic instead (b for beat, q for question, a
for arc stage). Register these with useHotkey from the component that owns the create action,
so the shortcut is scoped to that page/section and unregisters when it unmounts — never
register a bare letter globally.
Creating something puts focus in its first editable field
A create action that leaves the user hunting for the thing they just made is a bug. Land focus
in the new item's first field (or, when a mutation's response id isn't the field's DOM node
yet, request focus for that id and let it land once the row/page actually renders — see the
focusRequestId/onAutoFocused pattern used for beats and arc stages).
Chip inputs commit on Enter, comma, or blur
TagEditor, LocationEditor, CharacterMultiSelect, and AliasEditor all add their draft
value to the list on Enter, comma, or losing focus. Follow the same shape for any new
chip-style input.
Destructive confirmations use ConfirmModal
Never use the native confirm()/alert() dialogs — they're not stylable, not consistent with
the rest of the app, and (depending on browser) can be genuinely awkward to dismiss from the
keyboard. Use ConfirmModal (src/components/ConfirmModal.tsx), which wraps Modal and gets
focus-trapping and Escape-to-close for free.
The exception, not the rule: allowInInputs
useHotkey shortcuts don't fire while a text field is focused, unless registered with
allowInInputs: true. Reserve that for shortcuts that make sense mid-typing (mod+Enter to
submit, Escape to close) — never a bare letter.