import type {} from '../../types/globals'; /* AbsoluteJS Error Overlay - branded, per-framework, modern styling */ import type { ErrorOverlayOptions } from '../../types/client'; import { OVERLAY_FADE_DURATION_MS } from './constants'; let errorOverlayElement: HTMLDivElement | null = null; let currentOverlayKind: 'compilation' | 'runtime' | null = null; // Runtime errors accumulate so a second uncaught error in the same tick // doesn't silently replace the first — the overlay shows nav buttons and // a "N of M" badge so you can step through them. const runtimeErrors: ErrorOverlayOptions[] = []; let activeRuntimeIndex = 0; let pendingCompilationOpts: ErrorOverlayOptions | null = null; // Tracks which queue renderOverlay should pull from. Driven by // showErrorOverlay BEFORE renderOverlay runs so the choice is unambiguous // (currentOverlayKind reflects what's currently mounted, not what's queued). let activeMode: 'runtime' | 'compilation' | null = null; const frameworkLabels: Record = { angular: 'Angular', assets: 'Assets', html: 'HTML', htmx: 'HTMX', react: 'React', svelte: 'Svelte', unknown: 'Unknown', vue: 'Vue' }; const frameworkColors: Record = { angular: '#dd0031', assets: '#563d7c', html: '#e34c26', htmx: '#1a365d', react: '#61dafb', svelte: '#ff3e00', unknown: '#94a3b8', vue: '#42b883' }; const removeOverlayElement = () => { if (errorOverlayElement && errorOverlayElement.parentNode) { errorOverlayElement.parentNode.removeChild(errorOverlayElement); } errorOverlayElement = null; currentOverlayKind = null; }; export const hideErrorOverlay = () => { const elm = errorOverlayElement; // Clearing on dismiss — if more errors arrive after this they get a // fresh overlay, otherwise stale entries accumulate forever. runtimeErrors.length = 0; activeRuntimeIndex = 0; pendingCompilationOpts = null; activeMode = null; if (!elm || !elm.parentNode) { removeOverlayElement(); return; } elm.style.transition = 'opacity 150ms ease-out'; elm.style.opacity = '0'; errorOverlayElement = null; currentOverlayKind = null; setTimeout(() => { if (elm.parentNode) elm.parentNode.removeChild(elm); }, OVERLAY_FADE_DURATION_MS); }; export const isRuntimeErrorOverlay = () => currentOverlayKind === 'runtime'; const sectionLabelStyle = 'font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.08em;color:#94a3b8;margin-bottom:8px;'; const codeBlockStyle = 'margin:0;padding:14px 18px;background:rgba(15,23,42,0.8);border-radius:10px;border:1px solid rgba(71,85,105,0.4);color:#cbd5e1;font-size:12.5px;line-height:1.55;overflow-x:auto;white-space:pre;font-family:inherit;'; const buildLocationSection = ( file: string | undefined, line: number | undefined, column: number | undefined, lineText: string | undefined ) => { if (!file && line === undefined && column === undefined && !lineText) { return null; } const locSection = document.createElement('div'); locSection.style.cssText = 'margin-bottom:20px;'; const locLabel = document.createElement('div'); locLabel.style.cssText = sectionLabelStyle; locLabel.textContent = 'Where'; locSection.appendChild(locLabel); const locParts: string[] = []; if (file) locParts.push(file); if (line !== undefined) locParts.push(String(line)); if (column !== undefined) locParts.push(String(column)); const loc = locParts.join(':') || 'Unknown location'; const locEl = document.createElement('div'); locEl.style.cssText = 'padding:12px 18px;background:rgba(71,85,105,0.3);border-radius:10px;border:1px solid rgba(71,85,105,0.4);color:#cbd5e1;font-size:13px;word-break:break-all;'; locEl.textContent = loc; locSection.appendChild(locEl); if (lineText) { const codeBlock = document.createElement('pre'); codeBlock.style.cssText = `${codeBlockStyle}margin-top:8px;`; codeBlock.textContent = lineText; locSection.appendChild(codeBlock); } return locSection; }; // Strip the leading `${ErrorName}: ${message}` line from a stack if it just // repeats what's already shown in the "What went wrong" panel — keeps the // stack panel focused on frames. const cleanStack = (message: string, stack: string) => { const firstNewline = stack.indexOf('\n'); if (firstNewline === -1) return stack; const head = stack.slice(0, firstNewline).trim(); if (head === message || head.endsWith(`: ${message}`)) { return stack.slice(firstNewline + 1).replace(/^\n+/, ''); } return stack; }; const buildStackSection = (stack: string | undefined, message: string) => { if (!stack) return null; const cleaned = cleanStack(message, stack); if (!cleaned.trim()) return null; const section = document.createElement('div'); section.style.cssText = 'margin-bottom:20px;'; const label = document.createElement('div'); label.style.cssText = sectionLabelStyle; label.textContent = 'Stack'; section.appendChild(label); const pre = document.createElement('pre'); pre.style.cssText = `${codeBlockStyle}max-height:300px;overflow-y:auto;`; pre.textContent = cleaned; section.appendChild(pre); return section; }; // Reads the live document for every