diff --git a/templates/header.templ b/templates/header.templ
index 3f2ff19e..050010dc 100644
--- a/templates/header.templ
+++ b/templates/header.templ
@@ -352,4 +352,24 @@ templ SidebarSignIn(currentPath string) {
}
+
+
+
+
+
+
+
+
+
+
+
}
diff --git a/web/src/admin.ts b/web/src/admin.ts
index 5d830f2b..1353d9a2 100644
--- a/web/src/admin.ts
+++ b/web/src/admin.ts
@@ -1,3 +1,4 @@
+import { confirmDialog } from "./confirm";
import { Alpine } from "./alpine";
import { showToast } from "./toast";
@@ -423,9 +424,9 @@ async function purgeArchivedItems(): Promise {
if (!token) return;
if (
- !window.confirm(
+ !(await confirmDialog(
"Permanently delete all archived items? Their reading progress, notes, and highlights will be lost.",
- )
+ ))
) {
return;
}
@@ -475,9 +476,9 @@ async function deleteArchivedItem(id: string): Promise {
const token = requireToken();
if (!token) return;
if (
- !window.confirm(
+ !(await confirmDialog(
"Permanently delete this item? Its reading progress, notes, and highlights will be lost.",
- )
+ ))
) {
return;
}
@@ -504,9 +505,9 @@ async function mergeArchivedItem(sourceId: string, targetId: string): Promise {
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;
}
diff --git a/web/src/confirm.ts b/web/src/confirm.ts
new file mode 100644
index 00000000..cd1b4244
--- /dev/null
+++ b/web/src/confirm.ts
@@ -0,0 +1,80 @@
+// Global confirm-dialog replacement (issue #5): ONE styled modal
+// answers every hx-confirm sitewide (via the htmx:confirm event) and
+// exposes a promise API for the legacy JS call sites. No more native
+// window.confirm anywhere.
+
+let activeResolve: ((ok: boolean) => void) | null = null;
+
+function modalElements(): { overlay: HTMLElement | null; message: HTMLElement | null } {
+ return {
+ overlay: document.getElementById("confirm-modal"),
+ message: document.getElementById("confirm-modal-message"),
+ };
+}
+
+function hide(ok: boolean): void {
+ const { overlay } = modalElements();
+ if (overlay) overlay.classList.add("hidden");
+ const resolve = activeResolve;
+ activeResolve = null;
+ if (resolve) resolve(ok);
+}
+
+/**
+ * confirmDialog shows the global modal and resolves with the answer.
+ * Safe to call from any module (import this instead of window.confirm).
+ */
+export function confirmDialog(message: string): Promise {
+ const { overlay, message: messageEl } = modalElements();
+ if (!overlay || !messageEl) {
+ // Header missing (bare layouts): degrade to the native dialog
+ // rather than silently swallowing the confirmation.
+ return Promise.resolve(window.confirm(message));
+ }
+ messageEl.textContent = message;
+ overlay.classList.remove("hidden");
+ return new Promise((resolve) => {
+ activeResolve = resolve;
+ });
+}
+
+// Wire the static buttons once.
+function wireButtons(): void {
+ const { overlay } = modalElements();
+ const accept = document.getElementById("confirm-modal-accept");
+ const dismiss = document.getElementById("confirm-modal-cancel");
+ if (accept && !accept.hasAttribute("data-wired")) {
+ accept.setAttribute("data-wired", "1");
+ accept.addEventListener("click", () => hide(true));
+ }
+ if (dismiss && !dismiss.hasAttribute("data-wired")) {
+ dismiss.setAttribute("data-wired", "1");
+ dismiss.addEventListener("click", () => hide(false));
+ }
+ // Backdrop click cancels.
+ if (overlay && !overlay.hasAttribute("data-wired")) {
+ overlay.setAttribute("data-wired", "1");
+ overlay.addEventListener("click", (e) => {
+ if (e.target === overlay) hide(false);
+ });
+ }
+}
+
+// htmx:confirm interceptor: every element with hx-confirm gets the
+// modal instead of the browser dialog; confirming issues the request.
+// (Canonical htmx pattern — see htmx docs, "confirming with a modal".)
+document.body.addEventListener("htmx:confirm", function (evt) {
+ const custom = evt as CustomEvent<{
+ element: HTMLElement;
+ issueRequest: (override?: boolean) => void;
+ }>;
+ const elt = custom.detail.element;
+ if (!elt) return;
+ const question = elt.getAttribute("hx-confirm");
+ if (question === null) return;
+ evt.preventDefault();
+ wireButtons();
+ confirmDialog(question).then((ok) => {
+ if (ok) custom.detail.issueRequest(true);
+ });
+});
diff --git a/web/src/main.ts b/web/src/main.ts
index a93db44b..1b74ab31 100644
--- a/web/src/main.ts
+++ b/web/src/main.ts
@@ -25,6 +25,7 @@ import "./login";
import "./password_validation";
import "./processing_issues";
import "./podcasts";
+import "./confirm";
import "./profile";
import "./profile-modal";
import "./register";