Add soft delete + trash, keyboard-first web overhaul, move chapter tags to bottom
CI / build-and-push (push) Failing after 31s
CI / deploy (push) Has been skipped

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.
This commit is contained in:
James Wampler
2026-08-20 16:39:09 -07:00
parent 7df1fffdca
commit aca26588f9
59 changed files with 2913 additions and 170 deletions
+60
View File
@@ -0,0 +1,60 @@
# 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.