feat(ui): global confirm modal — browser dialogs retired sitewide (issues #5/#4/#7)

ONE styled modal answers every hx-confirm in the app: a body-level
htmx:confirm interceptor (web/src/confirm.ts, canonical htmx pattern)
preventDefaults and shows the dialog, issuing the request on Confirm.
Nine hx-confirm call sites upgrade without touching them. The modal
lives in the header partial so every page has it; confirmDialog() is
exported for JS call sites and degrades to the native dialog if the
header is absent (bare layouts).

Migrated the five window.confirm sites (admin.ts ×3, book-detail.ts ×2)
to the same promise API.

Podcast feed detail (issues #4/#7): the unsubscribe copy is
role-aware — non-admins get 'Unsubscribe from this feed?' with silent
retention (file state is admin knowledge); an admin who is the LAST
subscriber gets a three-choice modal instead: Unsubscribe (files kept,
the D6 default — sliding-window feeds make purge unrecoverable) /
Remove feed + files / Cancel. Admins with co-subscribers keep the
files-note confirm.
This commit is contained in:
John O'Keefe committed 2026-10-29 20:12:30 -04:00
1 parent 30766020c8
commit acbcc61ae4
5 files changed
+113 -10

No files matched your search

+5 -4
View File
@@ -1,3 +1,4 @@
import { confirmDialog } from "./confirm";
import { Alpine } from "./alpine";
import { showToast } from "./toast";
import { searchTagSuggestions, type TagSuggestion } from "./tag-dropdown";
@@ -531,9 +532,9 @@ Alpine.data("bookDetail", () => {
async purgeDeletedAnnotation(annotationType: string, annotationId: string) {
if (
!confirm(
!(await confirmDialog(
"Permanently delete this annotation? This cannot be undone and it will not reappear on any device.",
)
))
) {
return;
}
@@ -1002,9 +1003,9 @@ Alpine.data("bookDetail", () => {
async resetMetadata() {
if (this.resetting) return;
if (
!window.confirm(
!(await confirmDialog(
"Reset all metadata to scanned defaults? Manual edits and custom covers will be discarded.",
)
))
) {
return;
}