// ── Dev-mode error overlay ─────────────────────────────────────────────────────
//
// A full-screen, Vite/Next-style overlay for unexpected errors thrown by a Flow action. The
// server attaches the error's message + stack + action to the error frame ONLY under the dev
// worker (never in production), so the mere presence of a stack means "show the overlay". Purely
// presentational + dismissible; the component's patch still applies underneath, so dismissing
// returns you to a live page.
export interface OverlayError {
message: string;
name?: string | undefined;
stack?: string | undefined;
action?: string | undefined;
component?: string | undefined;
}
const OVERLAY_ID = "flow-error-overlay";
function _esc(s: unknown): string {
return String(s ?? "").replace(
/[&<>"']/g,
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]!,
);
}
/** Render each stack line, dimming framework/node_modules frames so app frames stand out. @internal */
export function _formatStack(stack: string): string {
return stack
.split("\n")
.slice(1) // first line is "Name: message", already shown as the heading
.map((line) => {
const dim = /node_modules|node:internal|\/flow\/src\//.test(line);
return `
${_esc(line.trim())}
`;
})
.join("");
}
/** Show (or replace) the dev error overlay for `err`. Idempotent — re-showing replaces content. */
export function showErrorOverlay(err: OverlayError): void {
if (typeof document === "undefined") return;
hideErrorOverlay();
const root = document.createElement("div");
root.id = OVERLAY_ID;
const shadow = root.attachShadow({ mode: "open" });
const where = [
err.action ? `action ${_esc(err.action)}` : "",
err.component ? `in ${_esc(err.component)}` : "",
]
.filter(Boolean)
.join(" ");
shadow.innerHTML = `
${_esc(err.name || "Error")}
${where ? `
${where}
` : ""}
${_esc(err.message)}
${err.stack ? `
${_formatStack(err.stack)}
` : ""}
`;
document.body.appendChild(root);
const back = shadow.querySelector(".flow-eo-back")!;
back.addEventListener("click", (e) => {
// Dismiss on backdrop click or the Dismiss button; keep clicks inside the card.
if (e.target === back || (e.target as HTMLElement).closest("[data-eo-dismiss]"))
hideErrorOverlay();
});
document.addEventListener("keydown", _onKey);
}
function _onKey(e: KeyboardEvent): void {
if (e.key === "Escape") hideErrorOverlay();
}
/** Remove the overlay if present. */
export function hideErrorOverlay(): void {
if (typeof document === "undefined") return;
document.getElementById(OVERLAY_ID)?.remove();
document.removeEventListener("keydown", _onKey);
}