diff --git a/web/src/main.ts b/web/src/main.ts index 1b74ab31..1aa4fdc7 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -42,5 +42,30 @@ import "./unlinked_books"; import "./websocket"; import "./woodPanelingInit"; +// ─── cancelled-view-transition noise gate (plan round-4 item 9) ──── +// Two transition races are BENIGN BY DESIGN and reject their promises +// as unhandled rejections on every fast navigation: +// - `AbortError: Transition was skipped` — the browser's MPA view +// transitions (@view-transition { navigation: auto } in input.css) +// when a new navigation interrupts the running crossfade. +// - `{isFromCancelledTransition: true}` — Alpine's x-transition hide +// cancelled by a re-show before the out-phase finished. +// Both mean "an animation was superseded", nothing was lost; the knob +// matrix's zero-console-error tripwire depends on the console staying +// CLEAN, so only these two shapes are swallowed — real rejections +// still report. +window.addEventListener("unhandledrejection", (event) => { + const reason = event.reason as + | (Error & { isFromCancelledTransition?: boolean }) + | null + | undefined; + const cancelledTransition = + reason?.isFromCancelledTransition === true || + (reason?.name === "AbortError" && /transition was skipped/i.test(reason.message ?? "")); + if (cancelledTransition) { + event.preventDefault(); + } +}); + armAuthFormRedirects(); Alpine.start();