/* global AbortController, DOMParser, HTMLFormElement, HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, MutationObserver, Node, URL, URLSearchParams, clearTimeout, document, fetch, navigator, setTimeout, window */ (() => { const SELECTOR = "[data-playground]"; const DESIGN_DEFAULTS = { style: "default", palette: "violet", mode: "system", font: "jakarta", scale: "default", }; const htmlPolicy = window.trustedTypes?.createPolicy("wrnexus-playground", { createHTML: (value) => value, }); let request; let timer; // Preview props are refreshed by the server when a control changes. Avoid a // second parent-to-child prop write during hydration, which reconstructs // nested composite mounts without their server-rendered children. function preservePreviewProps(preview) { const root = Array.from(preview.children).find((node) => node.tagName !== "SCRIPT"); for (const attribute of Array.from(root?.attributes ?? [])) { if (attribute.name.startsWith("data-wrn-prop-bind-")) root.removeAttribute(attribute.name); } } document.querySelectorAll("[data-playground-preview]").forEach(preservePreviewProps); function storedDesign() { const root = document.documentElement.dataset; const preference = (name) => { const entry = document.cookie.split("; ").find((part) => part.startsWith(`wrn-ui-${name}=`)); return entry ? decodeURIComponent(entry.slice(entry.indexOf("=") + 1)) : ""; }; return { style: preference("style") || root.uiStyle || DESIGN_DEFAULTS.style, palette: preference("palette") || root.uiPalette || DESIGN_DEFAULTS.palette, mode: preference("mode") || root.uiMode || DESIGN_DEFAULTS.mode, font: preference("font") || root.uiFont || DESIGN_DEFAULTS.font, scale: preference("scale") || root.uiScale || DESIGN_DEFAULTS.scale, }; } function setCookie(name, value) { document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=31536000;samesite=lax`; } function applyDesign(settings, persist = true) { const root = document.documentElement; root.dataset.uiStyle = settings.style; root.dataset.uiPalette = settings.palette; root.dataset.uiMode = settings.mode; root.dataset.uiFont = settings.font; root.dataset.uiScale = settings.scale; const dark = settings.mode === "dark" || (settings.mode === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches); if (window.wrnTheme) window.wrnTheme.set(dark ? "dark" : "light"); else { const theme = dark ? "dark" : "light"; root.dataset.theme = theme; const stylesheet = document.querySelector("link[data-wrnexus-theme]"); if (stylesheet) { const href = stylesheet.getAttribute("href"); if (href) stylesheet.setAttribute("href", href.replace(/(\/theme\/)[^/]+\//, `$1${theme}/`)); } } for (const button of document.querySelectorAll("[data-design-mode]")) { button.setAttribute( "aria-pressed", String(button.dataset.designMode === (dark ? "dark" : "light")), ); } for (const control of document.querySelectorAll("[data-design-setting]")) { control.value = settings[control.dataset.designSetting] || ""; } if (persist) { setCookie("wrn-ui-style", settings.style); setCookie("wrn-ui-palette", settings.palette); setCookie("wrn-ui-mode", settings.mode); setCookie("wrn-ui-font", settings.font); setCookie("wrn-ui-scale", settings.scale); setCookie("wrn-theme", dark ? "dark" : "light"); } } let design = storedDesign(); applyDesign(design, false); function syncDirectory() { const directory = document.querySelector(".docs-directory"); if (!directory) return; let active; for (const link of directory.querySelectorAll("a[href]")) { const matches = new URL(link.href, window.location.origin).pathname.replace(/\/$/, "") === window.location.pathname.replace(/\/$/, ""); if (matches) { link.setAttribute("aria-current", "page"); active = link; } else link.removeAttribute("aria-current"); } if (active) { const bounds = directory.getBoundingClientRect(); const item = active.getBoundingClientRect(); if (item.top < bounds.top || item.bottom > bounds.bottom) directory.scrollTop += item.top - bounds.top - directory.clientHeight / 2; } } syncDirectory(); window.addEventListener("wrnexus:navigated", syncDirectory); window.addEventListener("popstate", syncDirectory); window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { if (design.mode === "system") applyDesign(design); }); function sourceAttribute(value) { return String(value) .replaceAll("&", "&") .replaceAll('"', """) .replaceAll("<", "<") .replaceAll(">", ">"); } function updateCode(playground) { const form = playground.querySelector("[data-playground-form]"); const code = playground.querySelector("[data-playground-code]"); if (!(form instanceof HTMLFormElement) || !code) return; const component = playground.getAttribute("data-playground-component") || "Component"; const publicTag = playground.getAttribute("data-playground-public-tag") === "true"; const hasSlot = playground.getAttribute("data-playground-has-slot") === "true"; const attributes = []; const declarations = []; for (const field of form.elements) { if ( !( field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement || field instanceof HTMLSelectElement ) || !field.name.startsWith("pg_") ) continue; const name = field.name.slice(3); if (field.hasAttribute("data-playground-json")) { try { const data = JSON.parse(field.value); declarations.push(` state ${name} = ${JSON.stringify(data, null, 2)}`); attributes.push(`${name}="{${name}}"`); continue; } catch { /* Keep invalid JSON visible for correction. */ } } if (field instanceof HTMLInputElement && field.type === "checkbox") { attributes.push(`${name}="${field.checked}"`); } else if (field.value !== "") { const value = field.hasAttribute("data-playground-json") ? `'${field.value.replaceAll("'", "\\'")}'` : `"${field.value.replaceAll('"', '\\"')}"`; attributes.push(`${name}=${value}`); } } const tag = publicTag ? component : "div"; const mount = publicTag ? [] : [`data-component="${sourceAttribute(component)}"`]; const lines = [...mount, ...attributes].map((attribute) => ` ${attribute}`).join("\n"); const opening = lines ? `<${tag}\n${lines}` : `<${tag}`; let source = hasSlot ? `${opening}>\n \n` : `${opening}\n/>`; if (declarations.length) source = `page ${component.replace(/[^A-Za-z0-9]/g, "")}Example {\n${declarations.join("\n")}\n view {\n${source}\n }\n}`; writeCode(code, source); } /* * The reactive runtime treats "{expr}" inside a text node as an * interpolation, so a sample like { "label": "Mon" } written with * textContent was evaluated and rendered as an empty string. Putting each * brace in its own means no single text node ever holds a {...} * pair -- the same markup the generator uses for the server-rendered code. */ function writeCode(code, source) { const fragment = document.createDocumentFragment(); for (const part of source.split(/([{}])/)) { if (!part) continue; if (part === "{" || part === "}") { const brace = document.createElement("span"); brace.textContent = part; fragment.append(brace); } else { fragment.append(document.createTextNode(part)); } } code.replaceChildren(fragment); } function valuesFrom(form) { const params = new URLSearchParams(); let valid = true; for (const field of form.elements) { if (!( field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement || field instanceof HTMLSelectElement )) continue; if (!field.name) continue; const error = field.closest(".playground-field")?.querySelector("[data-playground-error]"); if (field.hasAttribute("data-playground-json")) { try { JSON.parse(field.value); field.removeAttribute("aria-invalid"); if (error) error.textContent = ""; } catch { field.setAttribute("aria-invalid", "true"); if (error) error.textContent = "Enter valid JSON"; valid = false; continue; } } params.set(field.name, field.type === "checkbox" ? String(field.checked) : field.value); } return valid ? params : null; } async function update(playground) { const form = playground.querySelector("[data-playground-form]"); const preview = playground.querySelector("[data-playground-preview]"); const status = playground.querySelector("[data-playground-status]"); if (!(form instanceof HTMLFormElement) || !preview || !status) return; const params = valuesFrom(form); if (!params) { status.textContent = "Fix the highlighted JSON value."; return; } request?.abort(); request = new AbortController(); const url = new URL(window.location.href); for (const key of [...url.searchParams.keys()]) { if (key.startsWith("pg_")) url.searchParams.delete(key); } for (const [key, value] of params) url.searchParams.set(key, value); playground.setAttribute("data-updating", "true"); status.textContent = "Updating preview…"; try { const response = await fetch(url, { headers: { "x-wrnexus-playground": "1" }, signal: request.signal, }); if (!response.ok) throw new Error(`Preview request failed (${response.status})`); const responseHtml = await response.text(); const parsed = new DOMParser().parseFromString( htmlPolicy ? htmlPolicy.createHTML(responseHtml) : responseHtml, "text/html", ); const next = parsed.querySelector("[data-playground-preview]"); if (!next) throw new Error("Preview markup was not returned"); window.__wrnexusDisposeBehaviors?.(preview); preview.replaceChildren( ...Array.from( parsed.querySelectorAll('script[type="application/json"][data-wrn-controls]'), (node) => document.importNode(node, true), ), ...Array.from(next.childNodes, (node) => document.importNode(node, true)), ); preservePreviewProps(preview); window.__wrnexusHydrateScopes?.(preview); window.__wrnexusHydrateCsrFetches?.(preview); window.wrnTheme?.bind?.(document); bindPlaygroundEvents(document); status.textContent = "Preview updated"; } catch (error) { if (error?.name !== "AbortError") { status.textContent = error instanceof Error ? error.message : "Unable to update preview"; } } finally { playground.removeAttribute("data-updating"); } } const outputBindings = new WeakMap(); const legacyObservedOutputs = new Set(); function formatOutputPayload(payload) { if (payload === undefined) return "No output payload"; if (typeof payload === "string") return payload; try { return JSON.stringify(payload, null, 2); } catch { return String(payload); } } function renderOutput(owner, outputName, payload) { const log = owner.querySelector("[data-playground-event-log]"); if (!log) return; log.replaceChildren(); const heading = document.createElement("strong"); heading.textContent = `@${outputName}`; const output = document.createElement("pre"); output.textContent = formatOutputPayload(payload); log.append(heading, output); } function bindDirectOutput(host, owner, outputName) { let byName = outputBindings.get(host); if (!byName) { byName = new Map(); outputBindings.set(host, byName); } if (byName.has(outputName)) return; const registry = (host.__wrnexusOutputHandlers ??= new Map()); const handlers = (registry instanceof Map ? registry.get(outputName) : registry[outputName]) ?? new Set(); const handler = (payload) => renderOutput(owner, outputName, payload); handlers.add(handler); if (registry instanceof Map) registry.set(outputName, handlers); else registry[outputName] = handlers; byName.set(outputName, handler); } const initializedCode = new WeakSet(); function bindPlaygroundEvents(root = document) { for (const playground of root.querySelectorAll?.(SELECTOR) ?? []) { if (!initializedCode.has(playground)) { initializedCode.add(playground); updateCode(playground); } const outputNames = (playground.getAttribute("data-playground-events") || "") .split(",") .map((name) => name.trim()) .filter(Boolean); const host = playground.querySelector( "[data-playground-preview] [data-wrn-scope], [data-playground-preview] [data-ui-component], [data-playground-preview] [data-component]", ); for (const outputName of outputNames) { if (host) bindDirectOutput(host, playground, outputName); // Legacy CustomEvent observation is retained only for compatibility-mode components. if (legacyObservedOutputs.has(outputName)) continue; legacyObservedOutputs.add(outputName); document.addEventListener(outputName, (legacyEvent) => { const owner = legacyEvent.target?.closest?.(SELECTOR); if (!owner) return; const declaredOutputs = (owner.getAttribute("data-playground-events") || "").split(","); if (!declaredOutputs.includes(outputName)) return; renderOutput(owner, outputName, legacyEvent.detail); }); } } } bindPlaygroundEvents(); new MutationObserver(() => bindPlaygroundEvents(document)).observe( document.getElementById("app") || document.body, { childList: true, subtree: true }, ); function schedule(playground) { updateCode(playground); clearTimeout(timer); timer = setTimeout(() => update(playground), 180); } document.addEventListener("input", (event) => { const form = event.target.closest?.("[data-playground-form]"); const playground = form?.closest(SELECTOR); if (playground) schedule(playground); }); document.addEventListener("change", (event) => { const form = event.target.closest?.("[data-playground-form]"); const playground = form?.closest(SELECTOR); if (playground) schedule(playground); }); document.addEventListener("reset", (event) => { const playground = event.target.closest?.("[data-playground-form]")?.closest(SELECTOR); if (playground) setTimeout(() => { updateCode(playground); update(playground); }, 0); }); document.addEventListener("click", async (event) => { if (event.target.closest?.("[data-design-mode]")) { design.mode = event.target.closest("[data-design-mode]").dataset.designMode; applyDesign(design); return; } const designToggle = event.target.closest?.("[data-design-panel-toggle]"); if (designToggle) { document.documentElement.toggleAttribute("data-design-panel-open"); return; } if (event.target.closest?.("[data-design-reset]")) { design = { ...DESIGN_DEFAULTS }; applyDesign(design); return; } const copyTarget = event.target.closest?.("[data-copy-target]"); if (copyTarget) { const target = document.querySelector(copyTarget.dataset.copyTarget); if (target) { try { await navigator.clipboard.writeText(target.textContent || ""); copyTarget.textContent = "Copied"; } catch { copyTarget.textContent = "Select code to copy"; } } return; } const copyText = event.target.closest?.("[data-copy-text]"); if (copyText) { try { await navigator.clipboard.writeText(copyText.dataset.copyText || ""); copyText.setAttribute("data-copied", "true"); setTimeout(() => copyText.removeAttribute("data-copied"), 1200); } catch { // Clipboard access may be blocked in an insecure local context. } return; } const menuToggle = event.target.closest?.("[data-docs-menu-toggle]"); if (menuToggle) { document.documentElement.toggleAttribute("data-docs-menu-open"); return; } if (event.target.closest?.("[data-docs-component-link]")) { document.documentElement.removeAttribute("data-docs-menu-open"); } const copy = event.target.closest?.("[data-playground-copy]"); if (!copy) return; const playground = copy.closest(SELECTOR); const code = playground?.querySelector("[data-playground-code]"); const status = playground?.querySelector("[data-playground-status]"); if (!code || !status) return; try { await navigator.clipboard.writeText(code.textContent || ""); status.textContent = "Component code copied"; const label = copy.lastChild; if (label?.nodeType === Node.TEXT_NODE) label.textContent = "Copied"; setTimeout(() => { if (label?.nodeType === Node.TEXT_NODE) label.textContent = "Copy"; }, 1400); } catch { status.textContent = "Copy failed. Select the code and copy it manually."; } }); document.addEventListener("input", (event) => { const setting = event.target.closest?.("[data-design-setting]"); if (setting) { design = { ...design, [setting.dataset.designSetting]: setting.value }; applyDesign(design); return; } if (!event.target.matches?.("[data-docs-search]")) return; const query = event.target.value.trim().toLowerCase(); const links = [...document.querySelectorAll("[data-docs-component-link]")]; let visible = 0; for (const link of links) { const matches = !query || link.textContent.toLowerCase().includes(query); link.hidden = !matches; if (matches) visible++; } for (const group of document.querySelectorAll(".docs-directory-group")) { group.hidden = !group.querySelector("[data-docs-component-link]:not([hidden])"); } const empty = document.querySelector("[data-docs-empty]"); if (empty) empty.hidden = visible > 0; }); document.addEventListener("keydown", (event) => { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { event.preventDefault(); document.querySelector("[data-docs-search]")?.focus(); } }); })();