From aca26588f9c029b988ddf53da949d4e445ec9528 Mon Sep 17 00:00:00 2001 From: James Wampler Date: Thu, 20 Aug 2026 16:39:09 -0700 Subject: [PATCH] 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. --- CLAUDE.md | 1 + docs/keyboard.md | 60 + src/Novelly.Api/Activity/ActivityEvent.cs | 3 +- src/Novelly.Api/Beats/Beat.cs | 1 + src/Novelly.Api/Beats/BeatContracts.cs | 2 +- src/Novelly.Api/Beats/BeatService.cs | 4 +- src/Novelly.Api/Chapters/Chapter.cs | 4 +- src/Novelly.Api/Chapters/ChapterContracts.cs | 4 +- src/Novelly.Api/Chapters/ChapterEndpoints.cs | 2 +- src/Novelly.Api/Chapters/ChapterService.cs | 14 +- src/Novelly.Api/Characters/Character.cs | 7 +- .../Characters/CharacterArcService.cs | 2 +- .../Characters/CharacterArcStage.cs | 1 + .../Characters/CharacterContracts.cs | 58 +- .../Characters/CharacterEndpoints.cs | 2 +- .../Characters/CharacterService.cs | 6 +- .../Common/NovellyServiceRegistration.cs | 6 + src/Novelly.Api/Common/SoftDelete.cs | 6 + .../20260820214543_AddSoftDelete.Designer.cs | 1299 +++++++++++++++++ .../20260820214543_AddSoftDelete.cs | 69 + .../Migrations/NovelDbContextModelSnapshot.cs | 12 +- src/Novelly.Api/Locations/Location.cs | 7 +- .../Locations/LocationEndpoints.cs | 2 +- src/Novelly.Api/Locations/LocationService.cs | 4 +- src/Novelly.Api/Program.cs | 4 +- .../Questions/OpenQuestionContracts.cs | 42 +- src/Novelly.Api/Trash/TrashContracts.cs | 9 + src/Novelly.Api/Trash/TrashEndpoints.cs | 38 + src/Novelly.Api/Trash/TrashEntityKind.cs | 8 + src/Novelly.Api/Trash/TrashOptions.cs | 10 + src/Novelly.Api/Trash/TrashPurge.cs | 57 + src/Novelly.Api/Trash/TrashPurgeRunner.cs | 84 ++ src/Novelly.Api/Trash/TrashPurgeSchedule.cs | 11 + src/Novelly.Api/Trash/TrashService.cs | 186 +++ src/Novelly.Api/appsettings.json | 5 + src/Novelly.Mcp/Tools/LocationTools.cs | 3 +- src/Novelly.ServiceDefaults/Extensions.cs | 5 - src/Novelly.Web/src/App.tsx | 2 + src/Novelly.Web/src/api/hooks.ts | 54 + src/Novelly.Web/src/api/types.ts | 13 + .../src/components/CharacterArc.tsx | 40 +- .../src/components/OpenQuestions.tsx | 81 +- src/Novelly.Web/src/components/icons.tsx | 11 + src/Novelly.Web/src/components/ui.tsx | 61 +- src/Novelly.Web/src/keyboard/HelpOverlay.tsx | 21 +- src/Novelly.Web/src/keyboard/focus.ts | 21 + src/Novelly.Web/src/pages/ChapterPage.tsx | 134 +- src/Novelly.Web/src/pages/ChaptersPage.tsx | 22 +- .../src/pages/CharacterDetailPage.tsx | 5 +- src/Novelly.Web/src/pages/LocationsPage.tsx | 95 +- src/Novelly.Web/src/pages/NovelLayout.tsx | 5 +- src/Novelly.Web/src/pages/TrashPage.tsx | 127 ++ tests/Novelly.Api.Tests/CharacterArcTests.cs | 9 +- .../CharacterServiceTests.cs | 10 +- .../Novelly.Api.Tests/LocationServiceTests.cs | 2 +- tests/Novelly.Api.Tests/OpenQuestionTests.cs | 6 +- tests/Novelly.Api.Tests/ServiceTestFixture.cs | 8 + tests/Novelly.Api.Tests/TrashPurgeTests.cs | 69 + tests/Novelly.Api.Tests/TrashServiceTests.cs | 249 ++++ 59 files changed, 2913 insertions(+), 170 deletions(-) create mode 100644 docs/keyboard.md create mode 100644 src/Novelly.Api/Common/SoftDelete.cs create mode 100644 src/Novelly.Api/Data/Migrations/20260820214543_AddSoftDelete.Designer.cs create mode 100644 src/Novelly.Api/Data/Migrations/20260820214543_AddSoftDelete.cs create mode 100644 src/Novelly.Api/Trash/TrashContracts.cs create mode 100644 src/Novelly.Api/Trash/TrashEndpoints.cs create mode 100644 src/Novelly.Api/Trash/TrashEntityKind.cs create mode 100644 src/Novelly.Api/Trash/TrashOptions.cs create mode 100644 src/Novelly.Api/Trash/TrashPurge.cs create mode 100644 src/Novelly.Api/Trash/TrashPurgeRunner.cs create mode 100644 src/Novelly.Api/Trash/TrashPurgeSchedule.cs create mode 100644 src/Novelly.Api/Trash/TrashService.cs create mode 100644 src/Novelly.Web/src/keyboard/focus.ts create mode 100644 src/Novelly.Web/src/pages/TrashPage.tsx create mode 100644 tests/Novelly.Api.Tests/TrashPurgeTests.cs create mode 100644 tests/Novelly.Api.Tests/TrashServiceTests.cs diff --git a/CLAUDE.md b/CLAUDE.md index 871fe82..02e8c67 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,6 +59,7 @@ Serilog console via `AddSerilog` (not `UseSerilog` — keeps OTel provider for A - `PATCH` requests partial: null field = leave alone, empty string = clear. Keep new update endpoints consistent with `Patch.Apply`. - Enums cross wire as names, never ordinals - All frontend components should have an id attribute that identifies them uniquely. +- Web client is keyboard-first: read `docs/keyboard.md` before adding any interactive UI (forms, editable rows, create flows). ## Testing diff --git a/docs/keyboard.md b/docs/keyboard.md new file mode 100644 index 0000000..f038abb --- /dev/null +++ b/docs/keyboard.md @@ -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 `