interface IdentityContext { viewerId?: string; userId?: string; accountId?: string; language?: string; } interface TargetingRule { pathContains?: string | string[]; pathEquals?: string; queryParam?: string; selector?: string; eventName?: string; delayMs?: number; } interface EmbedOptions { width?: number | string; height?: number | string; host?: string; identity?: IdentityContext; variables?: Record; } interface MountOptions extends EmbedOptions { mode?: "inline" | "modal"; target?: string | Element; autoShow?: boolean; targeting?: TargetingRule; closeOnBackdrop?: boolean; zIndex?: number; } interface BootConfig extends MountOptions { demoId: string; } interface MountedDemo { iframe: HTMLIFrameElement; container: HTMLElement; show: () => void; hide: () => void; destroy: () => void; } function resolveContainer(target: string | Element | undefined): Element | null { if (!target) return null; if (typeof target === "string") return document.querySelector(target); return target; } function normalizeHost(host?: string): string { const fallback = "https://showrunner.dev"; const value = (host ?? fallback).trim(); return value.endsWith("/") ? value.slice(0, -1) : value; } function toCssSize(value: number | string | undefined, fallback: string): string { if (value == null) return fallback; if (typeof value === "number") return `${value}px`; const trimmed = value.trim(); return trimmed || fallback; } function randomId(prefix: string): string { const random = Math.random().toString(36).slice(2, 10); const stamp = Date.now().toString(36).slice(-4); return `${prefix}_${random}${stamp}`; } function encodeVars( vars: Record | undefined ): string | null { if (!vars) return null; const compact = Object.entries(vars).reduce>((acc, [key, raw]) => { if (raw == null) return acc; const safeKey = key.trim(); if (!safeKey) return acc; acc[safeKey] = String(raw); return acc; }, {}); if (Object.keys(compact).length === 0) return null; return JSON.stringify(compact); } function buildDemoUrl(demoId: string, options: EmbedOptions = {}): string { const host = normalizeHost(options.host); const url = new URL(`${host}/embed/${encodeURIComponent(demoId)}`); const identity = options.identity; const viewerId = identity?.viewerId?.trim() || randomId("viewer"); url.searchParams.set("vid", viewerId); if (identity?.userId?.trim()) { url.searchParams.set("uid", identity.userId.trim()); } if (identity?.accountId?.trim()) { url.searchParams.set("aid", identity.accountId.trim()); } if (identity?.language?.trim()) { url.searchParams.set("lang", identity.language.trim()); } const encoded = encodeVars(options.variables); if (encoded) { url.searchParams.set("vars", encoded); } return url.toString(); } function createIframe(demoId: string, options: EmbedOptions = {}): HTMLIFrameElement { const iframe = document.createElement("iframe"); iframe.src = buildDemoUrl(demoId, options); iframe.loading = "lazy"; iframe.allowFullscreen = true; iframe.title = `ShowRunner demo ${demoId}`; iframe.width = String(options.width ?? 960); iframe.height = String(options.height ?? 600); iframe.style.width = toCssSize(options.width, "100%"); iframe.style.height = toCssSize(options.height, "100%"); iframe.style.maxWidth = "100%"; iframe.style.border = "0"; iframe.style.borderRadius = "12px"; iframe.style.background = "#070b16"; iframe.style.boxShadow = "0 18px 42px rgba(0,0,0,0.32)"; return iframe; } function matchesPath(rule: TargetingRule): boolean { const current = window.location.pathname; if (rule.pathEquals && current !== rule.pathEquals) { return false; } if (rule.pathContains) { const required = Array.isArray(rule.pathContains) ? rule.pathContains : [rule.pathContains]; if (!required.some((candidate) => candidate && current.includes(candidate))) { return false; } } return true; } function matchesQuery(rule: TargetingRule): boolean { if (!rule.queryParam) return true; return new URLSearchParams(window.location.search).has(rule.queryParam); } function waitForSelector(selector: string, onReady: () => void): () => void { const immediate = document.querySelector(selector); if (immediate) { onReady(); return () => {}; } const observer = new MutationObserver(() => { const node = document.querySelector(selector); if (!node) return; observer.disconnect(); onReady(); }); observer.observe(document.documentElement, { childList: true, subtree: true, }); const timeout = window.setTimeout(() => { observer.disconnect(); }, 30000); return () => { window.clearTimeout(timeout); observer.disconnect(); }; } function embed( demoId: string, target: string | Element, options: EmbedOptions = {} ): HTMLIFrameElement { const container = resolveContainer(target); if (!container) { throw new Error("ShowRunner embed target not found"); } const iframe = createIframe(demoId, options); container.innerHTML = ""; container.appendChild(iframe); return iframe; } function mount(demoId: string, options: MountOptions = {}): MountedDemo { const mode = options.mode ?? "modal"; const closeOnBackdrop = options.closeOnBackdrop !== false; const zIndex = Number.isFinite(options.zIndex) ? Number(options.zIndex) : 2147482000; const iframe = createIframe(demoId, options); const root = document.createElement("div"); root.dataset.showrunnerDemo = demoId; if (mode === "inline") { root.style.display = "none"; root.style.width = toCssSize(options.width, "100%"); root.style.height = toCssSize(options.height, "620px"); root.appendChild(iframe); const container = resolveContainer(options.target) ?? document.body; container.appendChild(root); const show = () => { root.style.display = "block"; }; const hide = () => { root.style.display = "none"; }; const destroy = () => { root.remove(); }; setupTargeting(options, show); if (options.autoShow !== false && !options.targeting) { show(); } return { iframe, container: root, show, hide, destroy }; } // Modal mode root.style.position = "fixed"; root.style.inset = "0"; root.style.display = "none"; root.style.alignItems = "center"; root.style.justifyContent = "center"; root.style.background = "rgba(8,11,20,0.68)"; root.style.backdropFilter = "blur(2px)"; root.style.padding = "20px"; root.style.zIndex = String(zIndex); const panel = document.createElement("div"); panel.style.position = "relative"; panel.style.width = toCssSize(options.width, "min(1100px, 100%)"); panel.style.height = toCssSize(options.height, "min(78vh, 760px)"); panel.style.maxWidth = "100%"; panel.style.borderRadius = "14px"; panel.style.border = "1px solid rgba(255,255,255,0.18)"; panel.style.background = "#0a1020"; panel.style.padding = "10px"; panel.style.boxShadow = "0 28px 70px rgba(0,0,0,0.45)"; panel.appendChild(iframe); const closeButton = document.createElement("button"); closeButton.type = "button"; closeButton.textContent = "Close"; closeButton.style.position = "absolute"; closeButton.style.top = "12px"; closeButton.style.right = "12px"; closeButton.style.minHeight = "30px"; closeButton.style.padding = "0 10px"; closeButton.style.borderRadius = "8px"; closeButton.style.border = "1px solid rgba(255,255,255,0.24)"; closeButton.style.background = "rgba(7,11,20,0.86)"; closeButton.style.color = "#dce6ff"; closeButton.style.cursor = "pointer"; panel.appendChild(closeButton); root.appendChild(panel); document.body.appendChild(root); const show = () => { root.style.display = "flex"; }; const hide = () => { root.style.display = "none"; }; const destroy = () => { root.remove(); }; closeButton.addEventListener("click", hide); if (closeOnBackdrop) { root.addEventListener("click", (event) => { if (event.target === root) { hide(); } }); } setupTargeting(options, show); if (options.autoShow !== false && !options.targeting) { show(); } return { iframe, container: root, show, hide, destroy }; } function setupTargeting(options: MountOptions, show: () => void): void { if (options.autoShow === false) { return; } const rule = options.targeting; if (!rule) { return; } if (!matchesPath(rule) || !matchesQuery(rule)) { return; } const triggerShow = () => { const delay = Number(rule.delayMs ?? 0); if (delay > 0) { window.setTimeout(show, delay); return; } show(); }; if (rule.eventName) { const eventName = rule.eventName; const handler = () => { triggerShow(); window.removeEventListener(eventName, handler as EventListener); }; window.addEventListener(eventName, handler as EventListener, { once: true }); return; } if (rule.selector) { const cleanup = waitForSelector(rule.selector, triggerShow); window.addEventListener( "beforeunload", () => { cleanup(); }, { once: true } ); return; } triggerShow(); } function boot(config: BootConfig | BootConfig[]): MountedDemo[] { const entries = Array.isArray(config) ? config : [config]; return entries.map((entry) => { const { demoId, ...options } = entry; return mount(demoId, options); }); } const api = { embed, mount, boot }; (window as Window & { ShowRunner?: typeof api }).ShowRunner = api; export { boot, embed, mount }; export type { BootConfig, EmbedOptions, IdentityContext, MountedDemo, MountOptions, TargetingRule };