Files
James Wampler aca26588f9
CI / build-and-push (push) Failing after 31s
CI / deploy (push) Has been skipped
Add soft delete + trash, keyboard-first web overhaul, move chapter tags to bottom
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.
2026-08-20 16:39:09 -07:00

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.