Sign In

uxloom

Package Overview
Dependencies
Maintainers
1
Versions
21
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

uxloom - npm Package Compare versions

Comparing version
0.10.0
to
0.10.1
+1
-1
dist/preview-template.d.ts

@@ -20,3 +20,3 @@ /**

*/
export declare const PREVIEW_TEMPLATE = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>UXLoom preview</title>\n<style>\n :root {\n --bg: #eceeec; --panel: #f8f9f8; --ink: #2a2e2a; --dim: #6b706b;\n --line: #d2d6d2; --block: #ffffff; --blockline: #c4c9c4;\n --accent: #2f6b52; --warn: #b45309; --err: #b04338;\n }\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font: 14px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n background: var(--bg); color: var(--ink); height: 100vh; display: flex; }\n button { font: inherit; cursor: pointer; background: none; border: none; color: inherit; }\n\n /* sidebar */\n aside { width: 250px; flex-shrink: 0; background: var(--panel); border-right: 1px solid var(--line);\n overflow-y: auto; padding: 14px; }\n aside h1 { font-size: 15px; margin-bottom: 2px; }\n aside .sub { color: var(--dim); font-size: 12px; margin-bottom: 14px; }\n aside h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--dim);\n margin: 14px 0 6px; }\n .jname { font-weight: 600; font-size: 13px; margin-top: 8px; }\n .jplat { color: var(--dim); font-weight: 400; font-size: 11px; margin-left: 5px; }\n .jstate, .snav { display: block; width: 100%; text-align: left; padding: 3px 8px; border-radius: 6px;\n font-size: 13px; color: var(--ink); }\n .jstate:hover, .snav:hover { background: #e6e9e6; }\n .jstate.on, .snav.on { background: var(--accent); color: #fff; }\n .jstate small { color: inherit; opacity: .65; }\n .final-flag { opacity: .6; font-size: 11px; }\n\n /* main */\n main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }\n .bar { display: flex; align-items: center; gap: 8px; padding: 10px 16px;\n border-bottom: 1px solid var(--line); background: var(--panel); flex-wrap: wrap; }\n .bar .grow { flex: 1; }\n .chip { font-size: 12px; color: var(--dim); border: 1px solid var(--line); border-radius: 999px;\n padding: 2px 10px; }\n .chip.live { border-color: var(--accent); color: var(--accent); }\n .vp, .tab { padding: 4px 10px; border-radius: 6px; font-size: 13px; border: 1px solid transparent; }\n .vp.on { background: var(--ink); color: #fff; }\n .tab { border-color: var(--line); }\n .tab.on { background: var(--accent); color: #fff; border-color: var(--accent); }\n .tab.undesigned { border-style: dashed; color: var(--dim); }\n .tabs { display: flex; gap: 6px; padding: 10px 16px 0; flex-wrap: wrap; }\n\n .stage { flex: 1; overflow: auto; display: flex; align-items: flex-start; justify-content: center;\n padding: 26px; }\n .stage.commenting .screen { cursor: crosshair; }\n .frame { background: #fff; border: 1px solid var(--line); border-radius: 12px;\n box-shadow: 0 8px 30px rgba(0,0,0,.08); overflow: hidden; flex-shrink: 0; }\n .frame.desktop { width: 960px; } .frame.tablet { width: 640px; } .frame.mobile { width: 390px; }\n .chrome { display: flex; align-items: center; gap: 6px; padding: 8px 12px; background: #eef0ee;\n border-bottom: 1px solid var(--line); font-size: 11px; color: var(--dim); }\n .dot { width: 9px; height: 9px; border-radius: 50%; background: #cfd3cf; }\n .chrome .url { flex: 1; background: #fff; border: 1px solid var(--line); border-radius: 6px;\n padding: 2px 10px; text-align: center; }\n .notch { flex: 1; text-align: center; }\n .screen { position: relative; padding: 14px; min-height: 420px; display: flex;\n flex-direction: column; gap: 10px;\n background: var(--mock-bg, transparent);\n color: var(--mock-text, var(--ink));\n font-family: var(--mock-font, inherit); }\n\n /* wireframe blocks \u2014 themed via --mock-* custom properties when the\n project declares tokens; grayscale defaults otherwise */\n .b { border: 1.5px solid var(--blockline); border-radius: var(--mock-radius, 8px);\n background: var(--block); padding: 10px 12px; position: relative; }\n .b .lab { font-size: 12px; color: var(--mock-muted, var(--dim)); }\n .b-header, .b-nav, .b-footer { background: #f1f3f1; display: flex; gap: 10px; align-items: center; }\n .b-nav .pill, .b-header .pill { width: 54px; height: 8px; border-radius: 4px; background: var(--blockline); }\n .b-hero { min-height: 90px; display: flex; align-items: center; justify-content: center; }\n .b-hero .copy { font-size: 17px; font-weight: 600; text-align: center; }\n .b-text .ln { height: 8px; border-radius: 4px; background: #e0e3e0; margin: 7px 0; }\n .b-text .ln:last-child { width: 60%; }\n .copy { font-size: 13px; white-space: pre-wrap; }\n .b-image { min-height: 80px;\n background: repeating-linear-gradient(45deg, #f4f5f4, #f4f5f4 8px, #e8eae8 8px, #e8eae8 9px);\n display: flex; align-items: center; justify-content: center; }\n .b-button { display: inline-block; background: var(--mock-accent, var(--ink)); color: #fff;\n border-radius: var(--mock-radius, 8px);\n padding: 9px 18px; font-size: 13px; align-self: flex-start; border: none; }\n .b-field { background: var(--block); } .b-field .inp { height: 30px; border: 1.5px solid var(--blockline);\n border-radius: 6px; margin-top: 5px; background: #fdfdfd; }\n .row { display: flex; gap: 10px; align-items: center; border: 1.5px solid var(--blockline);\n border-radius: var(--mock-radius, 8px); padding: 9px 12px; background: var(--block); }\n .row .av { width: 26px; height: 26px; border-radius: 50%; background: #e0e3e0; flex-shrink: 0; }\n .row .ln { height: 8px; border-radius: 4px; background: #e0e3e0; flex: 1; }\n .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }\n .tbl { border: 1.5px solid var(--blockline); border-radius: var(--mock-radius, 8px);\n overflow: hidden; background: var(--block); }\n .tbl .tr { display: flex; border-top: 1px solid #e4e7e4; }\n .tbl .tr:first-child { border-top: none; background: #f1f3f1; }\n .tbl .td { flex: 1; padding: 8px; } .tbl .td .ln { height: 8px; border-radius: 4px; background: #e0e3e0; }\n .tbl .td.hd { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em;\n color: var(--mock-muted, var(--dim)); }\n .srcchip { display: inline-block; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 10px; color: var(--mock-muted, var(--dim)); border: 1px solid var(--blockline);\n border-radius: 4px; padding: 0 5px; margin-top: 5px; align-self: flex-start; }\n .srcchip + .srcchip { margin-left: 4px; }\n .kids { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }\n\n /* state treatments */\n .skel .b, .skel .row, .skel .tbl { border-color: #e4e7e4; }\n .skel .ln, .skel .pill, .skel .av, .skel .inp, .skel .b-image, .skel .b-button, .skel .copy {\n background: linear-gradient(90deg, #ececec 25%, #f7f7f7 50%, #ececec 75%);\n background-size: 200% 100%; animation: shimmer 1.4s infinite; color: transparent; border-color: transparent; }\n @keyframes shimmer { to { background-position: -200% 0; } }\n @media (prefers-reduced-motion: reduce) { .skel * { animation: none !important; } }\n .banner { border: 1.5px solid var(--err); background: #fdf3f2; color: var(--err);\n border-radius: 8px; padding: 10px 12px; font-size: 13px; }\n .dimmed { opacity: .35; pointer-events: none; }\n .emptybox { border: 2px dashed var(--blockline); border-radius: 10px; padding: 34px 16px;\n text-align: center; color: var(--mock-muted, var(--dim)); }\n .overlay { position: absolute; inset: 0; background: rgba(42,46,42,.35); display: flex;\n align-items: center; justify-content: center; border-radius: 0 0 10px 10px; }\n .modal { background: var(--block); border-radius: var(--mock-radius, 12px); padding: 18px;\n width: min(85%, 340px); box-shadow: 0 12px 40px rgba(0,0,0,.25); }\n .modal h4 { font-size: 13px; margin-bottom: 8px; }\n\n /* events + meta */\n .meta { border-top: 1px solid var(--line); background: var(--panel); padding: 10px 16px;\n display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }\n .meta .lbl { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--dim); }\n .ev { border: 1px solid var(--accent); color: var(--accent); border-radius: 999px;\n padding: 3px 12px; font-size: 12.5px; }\n .ev:hover { background: var(--accent); color: #fff; }\n .ev small { opacity: .7; }\n .ev .g { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10.5px;\n opacity: .85; margin-left: 4px; }\n .note { font-size: 12px; color: var(--warn); }\n .datameta { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px;\n color: var(--dim); }\n .err-load { margin: auto; color: var(--err); }\n\n /* rationale (the \"why\" evidence panel, R20) */\n .whypanel { width: 340px; flex-shrink: 0; background: var(--panel); border-left: 1px solid var(--line);\n overflow-y: auto; padding: 14px 16px; outline: none; }\n .whytop { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }\n .whytitle { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--dim); }\n .whyclose { color: var(--dim); font-size: 14px; padding: 2px 6px; border-radius: 6px; }\n .whyclose:hover, .whyclose:focus-visible { background: #e6e9e6; color: var(--ink); }\n .rat-scope { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px;\n color: var(--dim); margin-bottom: 2px; }\n .rat-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }\n .rat-decision { font-size: 14px; font-weight: 600; }\n .rat-conf { display: inline-block; font-size: 10.5px; border-radius: 999px; padding: 1px 9px;\n border: 1px solid var(--line); color: var(--dim); white-space: nowrap; }\n .rat-conf.medium { border-color: var(--accent); color: var(--accent); }\n .rat-conf.high { background: var(--accent); border-color: var(--accent); color: #fff; }\n .rat-reason { font-size: 12.5px; margin: 6px 0 10px; }\n .rat-alts { width: 100%; border-collapse: collapse; font-size: 11.5px; margin: 8px 0 10px; }\n .rat-alts caption { text-align: left; font-size: 10.5px; text-transform: uppercase;\n letter-spacing: .05em; color: var(--dim); margin-bottom: 4px; }\n .rat-alts th, .rat-alts td { border: 1px solid var(--line); padding: 4px 6px; text-align: left;\n vertical-align: top; }\n .rat-alts th { background: #f1f3f1; font-size: 10px; text-transform: uppercase;\n letter-spacing: .05em; color: var(--dim); }\n .rat-opt { font-weight: 600; }\n .rat-pros, .rat-cons { list-style: disc; padding-left: 14px; margin: 0; }\n .rat-pros li { color: #2e7d4f; }\n .rat-cons li { color: var(--err); }\n .rat-src { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-top: 4px; }\n .rat-lbl { font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--dim); }\n .rat-src a { color: var(--accent); font-size: 12px; }\n .whydetails { border-top: 1px solid var(--line); margin-top: 12px; padding-top: 10px; }\n .whydetails summary { cursor: pointer; font-size: 12px; font-weight: 600; margin-bottom: 6px; }\n .whymark { margin-left: 5px; font-size: 11px; opacity: .55; }\n\n /* comment mode */\n .cbtn { border: 1px solid var(--line); border-radius: 999px; padding: 3px 12px; font-size: 12.5px; }\n .cbtn.on { background: var(--accent); color: #fff; border-color: var(--accent); }\n .cbtn .cnt { display: inline-block; margin-left: 6px; min-width: 17px; text-align: center;\n border-radius: 999px; background: var(--accent); color: #fff; font-size: 11px; padding: 0 4px; }\n .cbtn.on .cnt { background: #fff; color: var(--accent); }\n .pinwrap { position: absolute; transform: translate(-50%, -50%); z-index: 5; }\n .pin { width: 20px; height: 20px; border-radius: 50%; background: var(--accent); color: #fff;\n font-size: 11px; line-height: 20px; text-align: center; padding: 0;\n box-shadow: 0 2px 6px rgba(0,0,0,.35); }\n .pinpop { display: none; position: absolute; top: 24px; left: -4px; background: #fff;\n color: var(--ink); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px;\n width: 210px; font-size: 12.5px; box-shadow: 0 8px 24px rgba(0,0,0,.2); z-index: 6;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif; }\n .pinwrap:hover .pinpop, .pinwrap.open .pinpop, .pinwrap:focus-within .pinpop { display: block; }\n .pinpop .resolve { margin-top: 6px; border: 1px solid var(--accent); color: var(--accent);\n border-radius: 6px; padding: 2px 10px; font-size: 12px; }\n .pinpop .resolve:hover { background: var(--accent); color: #fff; }\n .cform { position: absolute; z-index: 7; background: #fff; border: 1px solid var(--line);\n border-radius: 8px; padding: 8px; box-shadow: 0 8px 24px rgba(0,0,0,.25); width: 230px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif; }\n .cform input { width: 100%; font: inherit; font-size: 12.5px; padding: 4px 6px;\n border: 1px solid var(--line); border-radius: 6px; }\n .cform .acts { display: flex; gap: 6px; margin-top: 6px; justify-content: flex-end; }\n .cform .acts button { font-size: 12px; border: 1px solid var(--line); border-radius: 6px; padding: 2px 10px; }\n .cform .acts .save { background: var(--accent); color: #fff; border-color: var(--accent); }\n .pin.assigned { background: var(--warn); }\n .pinpop .acts { display: flex; gap: 6px; margin-top: 6px; }\n .pinpop .assign { margin-top: 6px; border: 1px solid var(--warn); color: var(--warn);\n border-radius: 6px; padding: 2px 10px; font-size: 12px; }\n .pinpop .assign:hover { background: var(--warn); color: #fff; }\n .pinpop .asgchip { display: inline-block; margin-top: 6px; font-size: 11px; color: var(--warn);\n border: 1px solid var(--warn); border-radius: 999px; padding: 0 8px; }\n .cnt.asg { background: var(--warn); }\n .cbtn.on .cnt.asg { background: var(--warn); color: #fff; }\n .atoast { position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%); z-index: 40;\n background: var(--ink); color: #fff; border-radius: 10px; padding: 10px 14px;\n box-shadow: 0 8px 24px rgba(0,0,0,.35); font-size: 12.5px; max-width: 560px;\n display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }\n .atoast code { font-size: 11.5px; background: rgba(255,255,255,.12); border-radius: 6px; padding: 2px 6px; }\n .atoast button { border: 1px solid rgba(255,255,255,.4); border-radius: 6px; padding: 2px 10px;\n font-size: 12px; color: #fff; }\n .atoast button:hover { background: rgba(255,255,255,.15); }\n\n /* edit mode */\n .ewrap { position: relative; }\n .etools { display: none; position: absolute; top: -12px; right: 8px; gap: 2px; z-index: 4;\n background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 1px 3px;\n box-shadow: 0 2px 8px rgba(0,0,0,.15);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif; }\n .ewrap:hover > .etools, .ewrap:focus-within > .etools { display: flex; }\n .etools button { font-size: 12px; line-height: 1.4; padding: 1px 6px; border-radius: 4px; }\n .etools button:hover, .etools button:focus-visible { background: #eef0ee; }\n .eedit input { width: 100%; font: inherit; font-size: 13px; padding: 4px 6px; margin-top: 4px;\n border: 1px solid var(--accent); border-radius: 6px; }\n .tokpanel { width: 196px; flex-shrink: 0; background: var(--panel); border: 1px solid var(--line);\n border-radius: 10px; padding: 10px 12px; margin-right: 18px; font-size: 12px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n color: var(--ink); }\n .tokpanel h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--dim);\n margin-bottom: 6px; }\n .tokpanel label { display: flex; align-items: center; justify-content: space-between; gap: 8px;\n margin: 6px 0; }\n .tokpanel input[type=color] { width: 44px; height: 24px; padding: 0; border: 1px solid var(--line);\n border-radius: 4px; background: none; cursor: pointer; }\n .tokpanel input[type=number], .tokpanel input[type=text] { width: 96px; font: inherit; font-size: 12px;\n padding: 2px 5px; border: 1px solid var(--line); border-radius: 4px; }\n .addblock { border: 1.5px dashed var(--blockline); border-radius: 8px; padding: 8px; width: 100%;\n text-align: center; color: var(--dim); font-size: 12.5px; }\n .addblock:hover, .addblock:focus-visible { border-color: var(--accent); color: var(--accent); }\n .palette { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }\n .palette button { border: 1px solid var(--line); border-radius: 6px; padding: 3px 9px; font-size: 12px;\n background: #fff; }\n .palette button:hover, .palette button:focus-visible { border-color: var(--accent); color: var(--accent); }\n .toast { position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%); z-index: 20;\n display: flex; gap: 10px; align-items: center; background: var(--err); color: #fff;\n border-radius: 8px; padding: 8px 14px; font-size: 13px; max-width: 70vw;\n box-shadow: 0 6px 20px rgba(0,0,0,.3); }\n .toast button { color: #fff; font-size: 14px; }\n</style>\n</head>\n<body>\n<aside id=\"side\"></aside>\n<main>\n <div class=\"bar\" id=\"bar\"></div>\n <div class=\"tabs\" id=\"tabs\"></div>\n <div class=\"stage\" id=\"stage\"></div>\n <div class=\"meta\" id=\"meta\"></div>\n</main>\n<div id=\"why\" class=\"whypanel\" role=\"complementary\" aria-label=\"Design rationale\" tabindex=\"-1\" hidden></div>\n<script>\n\"use strict\";\nvar STATIC_INFO = null;\n/*__UXLOOM_STATIC__*/\nvar data = null, comments = [], sel = { screen: null, state: \"default\" }, viewport = \"desktop\";\nvar commentMode = false;\nvar whyOpen = false;\n/* assigned by the edit-mode section below; stripped from static exports */\nvar EDIT = null;\n\nfunction h(tag, cls, text) {\n var el = document.createElement(tag);\n if (cls) el.className = cls;\n if (text != null) el.textContent = text;\n return el;\n}\nfunction screenById(id) { return (data.screens || []).find(function (s) { return s.id === id; }); }\nfunction splitTarget(ref) {\n var i = ref.indexOf(\"#\");\n return i < 0 ? { state: ref } : { state: ref.slice(0, i), screenState: ref.slice(i + 1) };\n}\n/* an \"on\" value is either \"target\" or { target, guard?, roles? } */\nfunction normOn(v) { return typeof v === \"string\" ? { target: v } : (v || {}); }\nfunction pick(screenId, stateId) {\n sel = { screen: screenId, state: stateId || \"default\" };\n render();\n}\nfunction postJson(url, payload) {\n return fetch(url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(payload)\n }).then(function (r) {\n if (!r.ok) throw new Error(\"HTTP \" + r.status);\n return r.json();\n });\n}\n\n/* -------- theme: apply project tokens; grayscale when absent -------- */\nfunction applyTheme() {\n var r = document.documentElement.style;\n [\"--accent\", \"--mock-accent\", \"--mock-bg\", \"--block\", \"--mock-text\", \"--mock-muted\",\n \"--mock-radius\", \"--mock-font\"].forEach(function (p) { r.removeProperty(p); });\n var t = data && data.tokens;\n if (!t) return;\n var c = t.colors || {};\n if (c.accent) { r.setProperty(\"--accent\", c.accent); r.setProperty(\"--mock-accent\", c.accent); }\n if (c.bg) r.setProperty(\"--mock-bg\", c.bg);\n if (c.surface) r.setProperty(\"--block\", c.surface);\n if (c.text) r.setProperty(\"--mock-text\", c.text);\n if (c.muted) r.setProperty(\"--mock-muted\", c.muted);\n if (typeof t.radius === \"number\") r.setProperty(\"--mock-radius\", t.radius + \"px\");\n if (t.font) r.setProperty(\"--mock-font\", t.font);\n}\n\n/* ---------- derive blocks when a screen declares no layout ---------- */\nfunction autoBlocks(screen) {\n var blocks = [{ type: \"header\", label: screen.id }];\n if (screen.intent) blocks.push({ type: \"text\", label: screen.intent });\n var hasList = false;\n (screen.components || []).forEach(function (c) {\n var s = (c.semantic || \"\").toLowerCase();\n /* carry interaction intent (R12) through so derived blocks chip it */\n function derived(type, label) {\n return { type: type, label: label, validation: c.validation, motion: c.motion };\n }\n if (s.indexOf(\"button\") === 0) blocks.push(derived(\"button\", c.label && c.label.en || c.semantic));\n else if (s.indexOf(\"input\") === 0 || s.indexOf(\"field\") === 0)\n blocks.push(derived(\"field\", c.label && c.label.en || c.semantic));\n else if (s.indexOf(\"list\") === 0 || s.indexOf(\"table\") === 0) { blocks.push(derived(\"list\", c.semantic)); hasList = true; }\n else if (s.indexOf(\"nav\") === 0) blocks.splice(1, 0, derived(\"nav\", c.semantic));\n else blocks.push(derived(\"card\", c.semantic));\n });\n if (!hasList) blocks.push({ type: \"list\", label: \"Content\", count: 3 });\n return blocks;\n}\n\n/* -------- R12 chips: validation / sort / filter / motion intent ------ */\nfunction metaChip(text, title) {\n var s = h(\"span\", \"srcchip\", text);\n if (title) s.title = title;\n return s;\n}\n/* validation/motion live on components; blocks derived by autoBlocks carry\n them directly, explicit field/button blocks match a component by label */\nfunction blockMeta(b, screen) {\n var v = b.validation, m = b.motion;\n if ((b.type === \"field\" || b.type === \"button\") && screen && screen.components && (!v || !m)) {\n var match = screen.components.find(function (c) {\n return b.label && ((c.label && c.label.en === b.label) || c.semantic === b.label);\n });\n if (match) { if (!v) v = match.validation; if (!m) m = match.motion; }\n }\n return { validation: v, motion: m };\n}\nfunction appendMetaChips(el, b, screen) {\n var meta = blockMeta(b, screen);\n if (meta.validation) {\n if (meta.validation.required) el.appendChild(metaChip(\"required\", meta.validation.message));\n if (meta.validation.pattern)\n el.appendChild(metaChip(\"pattern: \" + meta.validation.pattern, meta.validation.message));\n }\n if (b.type === \"table\" || b.type === \"list\") {\n if (b.sort && b.sort.length) el.appendChild(metaChip(\"sort: \" + b.sort.join(\", \")));\n if (b.filter && b.filter.length) el.appendChild(metaChip(\"filter: \" + b.filter.join(\", \")));\n }\n if (meta.motion === \"decorative\") el.appendChild(metaChip(\"motion: decorative\"));\n}\n\nfunction renderBlock(b, screen) {\n var el, i, j, n = b.count || 3;\n switch (b.type) {\n case \"list\":\n el = h(\"div\", \"kids\");\n for (i = 0; i < n; i++) { var r = h(\"div\", \"row\"); r.appendChild(h(\"div\", \"av\")); r.appendChild(h(\"div\", \"ln\")); el.appendChild(r); }\n break;\n case \"card\":\n el = h(\"div\", \"cards\");\n for (i = 0; i < (b.count || 2); i++) { var c = h(\"div\", \"b\"); c.appendChild(h(\"div\", \"lab\", b.label || \"Card\")); c.appendChild(h(\"div\", \"b-text\")).appendChild(h(\"div\", \"ln\")); el.appendChild(c); }\n break;\n case \"table\":\n el = h(\"div\", \"tbl\");\n var cols = (b.columns && b.columns.length) ? b.columns : null;\n var ncol = cols ? cols.length : 3;\n var hr = h(\"div\", \"tr\");\n for (j = 0; j < ncol; j++) {\n var hd = h(\"div\", \"td\" + (cols ? \" hd\" : \"\"));\n if (cols) hd.textContent = cols[j]; else hd.appendChild(h(\"div\", \"ln\"));\n hr.appendChild(hd);\n }\n el.appendChild(hr);\n for (i = 0; i < n; i++) { var tr = h(\"div\", \"tr\"); for (j = 0; j < ncol; j++) tr.appendChild(h(\"div\", \"td\")).appendChild(h(\"div\", \"ln\")); el.appendChild(tr); }\n break;\n case \"button\": el = h(\"button\", \"b-button\", b.label || \"Action\"); break;\n case \"field\": el = h(\"div\", \"b b-field\"); el.appendChild(h(\"div\", \"lab\", b.label || \"Field\")); el.appendChild(h(\"div\", \"inp\")); break;\n case \"text\":\n el = h(\"div\", \"b b-text\"); if (b.label) el.appendChild(h(\"div\", \"lab\", b.label));\n if (b.copy) { el.appendChild(h(\"div\", \"copy\", b.copy)); }\n else { el.appendChild(h(\"div\", \"ln\")); el.appendChild(h(\"div\", \"ln\")); }\n break;\n case \"image\": el = h(\"div\", \"b b-image\"); el.appendChild(h(\"span\", \"lab\", b.label || \"image\")); break;\n case \"hero\":\n el = h(\"div\", \"b b-hero\");\n if (b.copy) el.appendChild(h(\"span\", \"copy\", b.copy));\n else el.appendChild(h(\"span\", \"lab\", b.label || \"Hero\"));\n break;\n case \"header\": case \"nav\": case \"footer\":\n el = h(\"div\", \"b b-\" + b.type); el.appendChild(h(\"span\", \"lab\", b.label || b.type));\n el.appendChild(h(\"span\", \"pill\")); el.appendChild(h(\"span\", \"pill\")); break;\n case \"form\":\n el = h(\"div\", \"b\"); el.appendChild(h(\"div\", \"lab\", b.label || \"Form\"));\n var kk = h(\"div\", \"kids\"); (b.children || [{ type: \"field\" }, { type: \"field\" }, { type: \"button\", label: \"Submit\" }]).forEach(function (ch) { kk.appendChild(renderBlock(ch, screen)); }); el.appendChild(kk); break;\n default:\n el = h(\"div\", \"b\"); el.appendChild(h(\"div\", \"lab\", b.label || b.type));\n }\n if (b.children && b.type !== \"form\") {\n var kids = h(\"div\", \"kids\");\n b.children.forEach(function (ch) { kids.appendChild(renderBlock(ch, screen)); });\n el.appendChild(kids);\n }\n if (b.source) el.appendChild(h(\"span\", \"srcchip\", b.source));\n appendMetaChips(el, b, screen);\n return el;\n}\n\nfunction renderScreenBody(screen, stateId) {\n var body = h(\"div\", \"screen\");\n body.setAttribute(\"data-ux-screen\", screen.id);\n body.setAttribute(\"data-ux-state\", stateId);\n var blocks = (screen.layout && screen.layout.blocks) || autoBlocks(screen);\n var isError = stateId.indexOf(\"error\") === 0;\n var baseline = stateId === \"default\" || stateId === \"empty\" || stateId === \"loading\";\n /* editing needs an explicit layout: block indexes must map 1:1 to file */\n var editing = EDIT && EDIT.on && !STATIC_INFO && screen.layout && screen.layout.blocks;\n var content = h(\"div\", stateId === \"loading\" ? \"skel\" : \"\");\n content.style.display = \"flex\"; content.style.flexDirection = \"column\"; content.style.gap = \"10px\";\n\n if (stateId === \"empty\") {\n blocks.filter(function (b) { return [\"header\", \"nav\"].indexOf(b.type) >= 0; })\n .forEach(function (b) { content.appendChild(renderBlock(b, screen)); });\n var eb = h(\"div\", \"emptybox\");\n eb.appendChild(h(\"div\", null, \"Nothing here yet\"));\n eb.appendChild(h(\"small\", null, screen.intent || \"Empty state \u2014 first-run guidance goes here\"));\n content.appendChild(eb);\n } else {\n blocks.forEach(function (b, i) {\n var el = renderBlock(b, screen);\n /* comment pins anchor to explicit-layout blocks only (file-stable indexes) */\n if (screen.layout && screen.layout.blocks) {\n el.setAttribute(\"data-bi\", String(i));\n el.setAttribute(\"data-bt\", b.type);\n if (b.label) el.setAttribute(\"data-bl\", b.label);\n }\n content.appendChild(editing ? EDIT.wrap(el, b, i, screen) : el);\n });\n }\n\n if (isError) {\n body.appendChild(h(\"div\", \"banner\", \"\\u26a0 \" + stateId + \" \u2014 what went wrong and how to fix it\"));\n content.className += \" dimmed\";\n }\n body.appendChild(content);\n if (editing && stateId !== \"empty\") body.appendChild(EDIT.addButton(screen));\n\n if (!baseline && !isError) { // custom states render as overlays over the default\n var ov = h(\"div\", \"overlay\"), mo = h(\"div\", \"modal\");\n mo.appendChild(h(\"h4\", null, stateId));\n mo.appendChild(h(\"div\", \"b-text\")).appendChild(h(\"div\", \"ln\"));\n mo.appendChild(h(\"button\", \"b-button\", \"Confirm\"));\n ov.appendChild(mo); body.appendChild(ov);\n }\n return body;\n}\n\n/* --------------------------- comment mode --------------------------- */\n/* effective lifecycle status (RFC 0006): legacy comments have no status */\nfunction cstatus(c) {\n if (c.resolved) return \"resolved\";\n return c.status === \"assigned\" ? \"assigned\" : \"open\";\n}\n\nfunction openCommentsFor(screenId) {\n return comments.filter(function (c) { return cstatus(c) !== \"resolved\" && c.screen === screenId; });\n}\n\nvar AGENT_PROMPT = \"Address the assigned UXLoom comments (comments_list \\u2192 comment_context \\u2192 fix \\u2192 comment_resolve).\";\n\n/* after \"\\u2192 agent\": tell the reviewer the exact line to hand their agent */\nfunction showAssignToast() {\n var old = document.querySelector(\".atoast\");\n if (old) old.remove();\n var t = h(\"div\", \"atoast\");\n t.setAttribute(\"role\", \"status\");\n t.appendChild(h(\"span\", null, \"Assigned. Tell your agent:\"));\n var code = h(\"code\", null, AGENT_PROMPT);\n t.appendChild(code);\n var cp = h(\"button\", null, \"Copy\");\n cp.setAttribute(\"aria-label\", \"Copy the agent prompt\");\n cp.onclick = function () {\n var done = function () { cp.textContent = \"Copied\"; };\n /* clipboard API may be unavailable on plain http \u2014 the visible text is the fallback */\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(AGENT_PROMPT).then(done).catch(done);\n } else done();\n };\n var x = h(\"button\", null, \"\\u00d7\");\n x.setAttribute(\"aria-label\", \"Dismiss\");\n x.onclick = function () { t.remove(); };\n t.appendChild(cp); t.appendChild(x);\n document.body.appendChild(t);\n setTimeout(function () { if (t.parentNode) t.remove(); }, 15000);\n}\n\nfunction attachComments(body, screen) {\n var mine = comments.filter(function (c) {\n return cstatus(c) !== \"resolved\" && c.screen === screen.id && c.state === sel.state;\n });\n mine.forEach(function (c, i) {\n var assigned = cstatus(c) === \"assigned\";\n var w = h(\"div\", \"pinwrap\");\n w.style.left = c.x + \"%\"; w.style.top = c.y + \"%\";\n var p = h(\"button\", \"pin\" + (assigned ? \" assigned\" : \"\"), String(i + 1));\n p.setAttribute(\"aria-label\", \"Comment \" + (i + 1) + (assigned ? \" (assigned to agent)\" : \"\") + \": \" + c.text);\n p.onclick = function (e) { e.stopPropagation(); w.classList.toggle(\"open\"); };\n var pop = h(\"div\", \"pinpop\");\n pop.appendChild(h(\"div\", null, c.text));\n if (assigned) pop.appendChild(h(\"span\", \"asgchip\", \"\\u2192 assigned to agent\"));\n var acts = h(\"div\", \"acts\");\n var rb = h(\"button\", \"resolve\", \"Resolve\");\n rb.onclick = function (e) {\n e.stopPropagation();\n postJson(\"/comments/resolve\", { id: c.id }).then(function () {\n c.resolved = true; c.status = \"resolved\"; render();\n }).catch(function () {});\n };\n acts.appendChild(rb);\n if (!assigned) {\n var ab = h(\"button\", \"assign\", \"\\u2192 agent\");\n ab.setAttribute(\"aria-label\", \"Assign to agent\");\n ab.title = \"Hand this comment to the agent: it reads the pin, the block it points at, and the screen contract\";\n ab.onclick = function (e) {\n e.stopPropagation();\n postJson(\"/comments/assign\", { id: c.id }).then(function (u) {\n c.status = \"assigned\"; c.assignedAt = u.assignedAt;\n showAssignToast(); render();\n }).catch(function () {});\n };\n acts.appendChild(ab);\n }\n pop.appendChild(acts);\n pop.onclick = function (e) { e.stopPropagation(); };\n w.appendChild(p); w.appendChild(pop);\n body.appendChild(w);\n });\n if (commentMode) body.addEventListener(\"click\", function (e) { placePin(e, body, screen); });\n}\n\nfunction placePin(e, body, screen) {\n var existing = body.querySelector(\".cform\");\n if (existing) { existing.remove(); return; }\n if (e.target.closest && e.target.closest(\".pinwrap\")) return;\n var rect = body.getBoundingClientRect();\n var x = Math.round((e.clientX - rect.left) / rect.width * 1000) / 10;\n var y = Math.round((e.clientY - rect.top) / rect.height * 1000) / 10;\n /* anchor the pin to the layout block it lands on (explicit layouts only \u2014\n indexes must map 1:1 to the project file, same rule as edit mode) */\n var anchor = null;\n var be = e.target.closest ? e.target.closest(\"[data-bi]\") : null;\n if (be && body.contains(be)) {\n anchor = { index: +be.getAttribute(\"data-bi\"), type: be.getAttribute(\"data-bt\") };\n if (be.getAttribute(\"data-bl\")) anchor.label = be.getAttribute(\"data-bl\");\n }\n var form = h(\"div\", \"cform\");\n form.style.left = Math.min(Math.max(x, 2), 68) + \"%\";\n form.style.top = Math.min(Math.max(y, 2), 88) + \"%\";\n var inp = h(\"input\");\n inp.setAttribute(\"type\", \"text\");\n inp.setAttribute(\"placeholder\", \"Leave a comment\\u2026\");\n inp.setAttribute(\"aria-label\", \"Comment text\");\n var save = h(\"button\", \"save\", \"Add\"), cancel = h(\"button\", null, \"Cancel\");\n function submit() {\n var text = inp.value.trim();\n if (!text) return;\n var payload = { screen: screen.id, state: sel.state, x: x, y: y, text: text };\n if (anchor) payload.block = anchor;\n postJson(\"/comments\", payload)\n .then(function (c) { comments.push(c); render(); })\n .catch(function () { form.remove(); });\n }\n save.onclick = function (ev) { ev.stopPropagation(); submit(); };\n cancel.onclick = function (ev) { ev.stopPropagation(); form.remove(); };\n inp.onkeydown = function (ev) {\n if (ev.key === \"Enter\") submit();\n if (ev.key === \"Escape\") form.remove();\n };\n form.onclick = function (ev) { ev.stopPropagation(); };\n var acts = h(\"div\", \"acts\"); acts.appendChild(cancel); acts.appendChild(save);\n form.appendChild(inp); form.appendChild(acts);\n body.appendChild(form);\n inp.focus();\n}\n\n/* ------------------------------ chrome ------------------------------ */\nfunction chromeFor(vp) {\n var c = h(\"div\", \"chrome\");\n var native = (data.platforms || []).some(function (p) { return p === \"ios\" || p === \"android\"; });\n if (vp === \"mobile\" && native && (data.platforms || []).indexOf(\"web\") < 0 && (data.platforms || []).indexOf(\"mweb\") < 0) {\n c.appendChild(h(\"span\", \"notch\", \"9:41 \\u2014 \" + data.platforms.join(\" / \")));\n } else if (vp === \"mobile\") {\n c.appendChild(h(\"span\", \"notch\", \"9:41 \\u2014 mobile web\"));\n } else {\n c.appendChild(h(\"span\", \"dot\")); c.appendChild(h(\"span\", \"dot\")); c.appendChild(h(\"span\", \"dot\"));\n c.appendChild(h(\"span\", \"url\", (data.name || \"app\") + \".example.com\"));\n }\n return c;\n}\n\n/* ----------- R20: design rationale \u2014 the \"why\" evidence panel ----------- */\n/* the toggle is visible only when the current screen OR the project carries\n rationale; journeys surface theirs via the panel and a muted meta chip */\nfunction hasWhyContent(screen) {\n return !!((screen && screen.rationale) || (data && data.rationale));\n}\nfunction journeysForScreen(screenId) {\n return (data.journeys || []).filter(function (j) {\n return Object.keys(j.states).some(function (sn) { return j.states[sn].screen === screenId; });\n });\n}\nfunction hostOf(u) {\n try { return new URL(u).hostname || u; } catch (e) { return u; }\n}\n/* one rationale rendered as evidence: decision, confidence, reasoning,\n the alternatives that were argued over, and the sources it leans on.\n Every field is optional \u2014 show what exists, never invent the rest. */\nfunction ratBody(r) {\n var box = h(\"div\", \"rat\");\n var head = h(\"div\", \"rat-head\");\n head.appendChild(h(\"h3\", \"rat-decision\", r.decision || \"(no decision recorded)\"));\n if (r.confidence === \"low\" || r.confidence === \"medium\" || r.confidence === \"high\")\n head.appendChild(h(\"span\", \"rat-conf \" + r.confidence, r.confidence + \" confidence\"));\n box.appendChild(head);\n if (r.reasoning) box.appendChild(h(\"p\", \"rat-reason\", r.reasoning));\n if (r.alternatives && r.alternatives.length) {\n var tbl = h(\"table\", \"rat-alts\");\n tbl.appendChild(h(\"caption\", null, \"Alternatives considered\"));\n var thr = h(\"tr\");\n [\"Option\", \"Pros\", \"Cons\"].forEach(function (t) { thr.appendChild(h(\"th\", null, t)); });\n tbl.appendChild(thr);\n r.alternatives.forEach(function (a) {\n var tr = h(\"tr\");\n tr.appendChild(h(\"td\", \"rat-opt\", a.option || \"\"));\n var ptd = h(\"td\"), pul = h(\"ul\", \"rat-pros\");\n (a.pros || []).forEach(function (p) { pul.appendChild(h(\"li\", null, p)); });\n ptd.appendChild(pul); tr.appendChild(ptd);\n var ctd = h(\"td\"), cul = h(\"ul\", \"rat-cons\");\n (a.cons || []).forEach(function (c) { cul.appendChild(h(\"li\", null, c)); });\n ctd.appendChild(cul); tr.appendChild(ctd);\n tbl.appendChild(tr);\n });\n box.appendChild(tbl);\n }\n if (r.sources && r.sources.length) {\n var src = h(\"div\", \"rat-src\");\n src.appendChild(h(\"span\", \"rat-lbl\", \"Sources\"));\n r.sources.forEach(function (u) {\n var a = h(\"a\", null, hostOf(u));\n a.setAttribute(\"href\", u);\n a.setAttribute(\"target\", \"_blank\");\n a.setAttribute(\"rel\", \"noopener\");\n a.title = u;\n src.appendChild(a);\n });\n box.appendChild(src);\n }\n return box;\n}\nfunction closeWhy() {\n whyOpen = false;\n render();\n var t = document.getElementById(\"whytoggle\");\n if (t) t.focus();\n}\nfunction renderWhy(screen) {\n var panel = document.getElementById(\"why\");\n panel.innerHTML = \"\";\n if (!hasWhyContent(screen)) whyOpen = false;\n panel.hidden = !whyOpen;\n if (!whyOpen) return;\n var top = h(\"div\", \"whytop\");\n top.appendChild(h(\"h2\", \"whytitle\", \"Design rationale\"));\n var x = h(\"button\", \"whyclose\", \"\\u2715\");\n x.setAttribute(\"aria-label\", \"Close rationale panel\");\n x.onclick = function () { closeWhy(); };\n top.appendChild(x);\n panel.appendChild(top);\n if (screen && screen.rationale) {\n panel.appendChild(h(\"div\", \"rat-scope\", screen.id));\n panel.appendChild(ratBody(screen.rationale));\n }\n if (data && data.rationale) {\n var pd = h(\"details\", \"whydetails\");\n pd.appendChild(h(\"summary\", null, \"Product direction\"));\n pd.appendChild(ratBody(data.rationale));\n panel.appendChild(pd);\n }\n if (screen) journeysForScreen(screen.id).forEach(function (j) {\n if (!j.rationale) return;\n var jd = h(\"details\", \"whydetails\");\n jd.appendChild(h(\"summary\", null, \"Flow rationale \\u00b7 \" + j.id));\n jd.appendChild(ratBody(j.rationale));\n panel.appendChild(jd);\n });\n}\n\n/* ------------------------------ render ------------------------------ */\nfunction render() {\n if (!data) return;\n if (!sel.screen && data.screens && data.screens.length) sel.screen = data.screens[0].id;\n var screen = screenById(sel.screen);\n\n // sidebar\n var side = document.getElementById(\"side\"); side.innerHTML = \"\";\n side.appendChild(h(\"h1\", null, \"UXLoom preview\"));\n side.appendChild(h(\"div\", \"sub\", data.name + \" \\u00b7 \" + (data.platforms || []).join(\", \")));\n side.appendChild(h(\"h2\", null, \"Journeys\"));\n (data.journeys || []).forEach(function (j) {\n var jn = h(\"div\", \"jname\");\n jn.appendChild(document.createTextNode(j.id));\n if (j.platforms && j.platforms.length) jn.appendChild(h(\"small\", \"jplat\", j.platforms.join(\" \\u00b7 \")));\n side.appendChild(jn);\n Object.keys(j.states).forEach(function (stateName) {\n var js = j.states[stateName];\n var b = h(\"button\", \"jstate\" + (screen && js.screen === screen.id ? \" on\" : \"\"));\n b.appendChild(document.createTextNode(stateName + \" \"));\n var sm = h(\"small\", null, \"\\u2192 \" + js.screen + (js.final ? \" \\u2713\" : \"\"));\n b.appendChild(sm);\n b.onclick = function () { pick(js.screen, \"default\"); };\n side.appendChild(b);\n });\n });\n side.appendChild(h(\"h2\", null, \"Screens\"));\n (data.screens || []).forEach(function (s) {\n var b = h(\"button\", \"snav\" + (screen && s.id === screen.id ? \" on\" : \"\"));\n b.appendChild(document.createTextNode(s.id));\n if (s.rationale) {\n var wm = h(\"span\", \"whymark\", \"\\u24d8\");\n wm.title = \"has design rationale\";\n b.appendChild(wm);\n }\n b.onclick = function () { pick(s.id, \"default\"); };\n side.appendChild(b);\n });\n\n // bar\n var bar = document.getElementById(\"bar\"); bar.innerHTML = \"\";\n [\"desktop\", \"tablet\", \"mobile\"].forEach(function (vp) {\n var b = h(\"button\", \"vp\" + (viewport === vp ? \" on\" : \"\"), vp);\n b.onclick = function () { viewport = vp; render(); };\n bar.appendChild(b);\n });\n bar.appendChild(h(\"div\", \"grow\"));\n if (screen) {\n var req = screen.requiredStates.length;\n var des = screen.requiredStates.filter(function (s) { return screen.designedStates.indexOf(s) >= 0; }).length;\n bar.appendChild(h(\"span\", \"chip\", \"coverage \" + des + \"/\" + req));\n }\n if (!STATIC_INFO && screen) {\n var unresolved = openCommentsFor(screen.id);\n var asgCount = unresolved.filter(function (c) { return cstatus(c) === \"assigned\"; }).length;\n var openCount = unresolved.length - asgCount;\n var cb = h(\"button\", \"cbtn\" + (commentMode ? \" on\" : \"\"));\n cb.appendChild(document.createTextNode(\"\\ud83d\\udcac comment\"));\n if (openCount) cb.appendChild(h(\"span\", \"cnt\", String(openCount)));\n if (asgCount) {\n var asgChip = h(\"span\", \"cnt asg\", asgCount + \"\\u2192\");\n asgChip.setAttribute(\"aria-label\", asgCount + \" assigned to agent\");\n cb.appendChild(asgChip);\n }\n cb.setAttribute(\"aria-pressed\", commentMode ? \"true\" : \"false\");\n cb.title = \"Comment mode: click the mock to leave a pinned note\" +\n (asgCount ? \" \\u00b7 \" + asgCount + \" assigned to agent\" : \"\");\n cb.onclick = function () {\n commentMode = !commentMode;\n if (commentMode && EDIT) EDIT.on = false; /* modes are exclusive */\n render();\n };\n bar.appendChild(cb);\n if (EDIT) bar.appendChild(EDIT.toggleButton());\n }\n if (hasWhyContent(screen)) {\n var wb = h(\"button\", \"cbtn\" + (whyOpen ? \" on\" : \"\"), \"\\u24d8 why\");\n wb.id = \"whytoggle\";\n wb.setAttribute(\"aria-pressed\", whyOpen ? \"true\" : \"false\");\n wb.setAttribute(\"aria-controls\", \"why\");\n wb.title = \"Why: the design rationale behind this screen\";\n wb.onclick = function () {\n whyOpen = !whyOpen;\n render();\n if (whyOpen) document.getElementById(\"why\").focus();\n };\n bar.appendChild(wb);\n }\n if (STATIC_INFO) bar.appendChild(h(\"span\", \"chip\", \"static export \\u00b7 \" + STATIC_INFO.generated));\n else bar.appendChild(h(\"span\", \"chip live\", \"\\u25cf live\"));\n\n // tabs\n var tabs = document.getElementById(\"tabs\"); tabs.innerHTML = \"\";\n if (screen) screen.requiredStates.forEach(function (st) {\n var designed = screen.designedStates.indexOf(st) >= 0;\n var t = h(\"button\", \"tab\" + (sel.state === st ? \" on\" : \"\") + (designed ? \"\" : \" undesigned\"), st + (designed ? \"\" : \" \\u25cb\"));\n t.title = designed ? st : st + \" \u2014 contracted but not designed yet\";\n t.onclick = function () { sel.state = st; render(); };\n tabs.appendChild(t);\n });\n\n // stage\n var stage = document.getElementById(\"stage\"); stage.innerHTML = \"\";\n stage.className = \"stage\" + (commentMode && !STATIC_INFO ? \" commenting\" : \"\");\n if (!screen) { stage.appendChild(h(\"div\", \"err-load\", \"No screens in the project yet \u2014 ask your agent to design some.\")); }\n else {\n if (EDIT && EDIT.on && !STATIC_INFO) stage.appendChild(EDIT.tokenPanel());\n var frame = h(\"div\", \"frame \" + viewport);\n frame.appendChild(chromeFor(viewport));\n var body = renderScreenBody(screen, sel.state);\n if (!STATIC_INFO) attachComments(body, screen);\n frame.appendChild(body);\n stage.appendChild(frame);\n }\n\n // meta: outgoing events for journey states showing this screen + exemptions\n var meta = document.getElementById(\"meta\"); meta.innerHTML = \"\";\n if (screen) {\n var events = [];\n (data.journeys || []).forEach(function (j) {\n Object.keys(j.states).forEach(function (sn) {\n var js = j.states[sn];\n if (js.screen !== screen.id) return;\n Object.keys(js.on || {}).forEach(function (ev) {\n var o = normOn(js.on[ev]);\n if (!o.target) return;\n events.push({ ev: ev, target: o.target, guard: o.guard, roles: o.roles, journey: j.id });\n });\n });\n });\n if (events.length) {\n meta.appendChild(h(\"span\", \"lbl\", \"Events\"));\n events.forEach(function (e) {\n var t = splitTarget(e.target);\n var jj = (data.journeys || []).find(function (x) { return x.id === e.journey; });\n var targetState = jj && jj.states[t.state];\n var b = h(\"button\", \"ev\");\n b.appendChild(document.createTextNode(e.ev + \" \"));\n b.appendChild(h(\"small\", null, \"\\u2192 \" + (targetState ? targetState.screen : t.state) + (t.screenState ? \"#\" + t.screenState : \"\")));\n if (e.guard) b.appendChild(h(\"span\", \"g\", \"[if \" + e.guard + \"]\"));\n if (e.roles && e.roles.length) b.appendChild(h(\"span\", \"g\", \"[\" + e.roles.join(\"|\") + \"]\"));\n b.onclick = function () { if (targetState) pick(targetState.screen, t.screenState || \"default\"); };\n meta.appendChild(b);\n });\n }\n if (screen.data) {\n var fields = Object.keys(screen.data);\n if (fields.length) meta.appendChild(h(\"span\", \"datameta\",\n \"data: \" + fields.map(function (k) { return k + \": \" + screen.data[k]; }).join(\", \")));\n }\n (screen.exemptions || []).forEach(function (ex) {\n meta.appendChild(h(\"span\", \"note\", \"exempt \" + ex.state + \": \" + ex.reason));\n });\n journeysForScreen(screen.id).forEach(function (j) {\n if (j.rationale && j.rationale.decision)\n meta.appendChild(h(\"span\", \"chip flowchip\", \"flow: \" + j.rationale.decision));\n });\n }\n\n renderWhy(screen);\n}\n\nfunction boot(p) {\n data = p;\n if (sel.screen && !screenById(sel.screen)) sel = { screen: null, state: \"default\" };\n var hasDesktop = (data.platforms || []).indexOf(\"web\") >= 0;\n if (!hasDesktop && viewport === \"desktop\") viewport = \"mobile\";\n applyTheme();\n render();\n}\n\nfunction load() {\n if (STATIC_INFO) { boot(STATIC_INFO.data); return; }\n Promise.all([\n fetch(\"/project\").then(function (r) { return r.json(); }),\n fetch(\"/comments\").then(function (r) { return r.json(); }).catch(function () { return { comments: [] }; })\n ]).then(function (rs) {\n var p = rs[0];\n if (p && p.error) throw new Error(p.error);\n comments = (rs[1] && rs[1].comments) || [];\n boot(p);\n }).catch(function (e) {\n document.getElementById(\"stage\").innerHTML = \"\";\n document.getElementById(\"stage\").appendChild(h(\"div\", \"err-load\", \"Cannot load project: \" + e.message));\n });\n}\ndocument.addEventListener(\"keydown\", function (e) {\n if (e.key === \"Escape\" && whyOpen) closeWhy();\n});\n/*__UXLOOM_EDIT__*/\n/* ------------- structured edit mode (live preview only) ------------- */\nEDIT = (function () {\n var api = { on: false };\n var BLOCK_TYPES = [\"header\", \"nav\", \"hero\", \"text\", \"list\", \"card\", \"form\",\n \"field\", \"button\", \"image\", \"table\", \"footer\", \"custom\"];\n\n function toast(msg) {\n var old = document.querySelector(\".toast\");\n if (old) old.remove();\n var t = h(\"div\", \"toast\");\n t.setAttribute(\"role\", \"alert\");\n t.appendChild(h(\"span\", null, msg));\n var x = h(\"button\", null, \"\\u2715\");\n x.setAttribute(\"aria-label\", \"Dismiss\");\n x.onclick = function () { t.remove(); };\n t.appendChild(x);\n document.body.appendChild(t);\n }\n\n /* POST an op; the file write wakes the SSE watcher, which re-renders\n every viewer \u2014 success needs no local handling at all */\n function postEdit(payload) {\n fetch(\"/edit\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(payload)\n }).then(function (r) {\n return r.json().catch(function () { return {}; }).then(function (j) {\n if (!r.ok) throw new Error(j.error || (\"HTTP \" + r.status));\n });\n }).catch(function (e) { toast(e.message); });\n }\n\n api.toggleButton = function () {\n var b = h(\"button\", \"cbtn\" + (api.on ? \" on\" : \"\"), \"\\u270e edit\");\n b.setAttribute(\"aria-pressed\", api.on ? \"true\" : \"false\");\n b.title = \"Edit mode: tokens, copy, and blocks \\u2014 writes the project file\";\n b.onclick = function () {\n api.on = !api.on;\n if (api.on) commentMode = false; /* modes are exclusive */\n render();\n };\n return b;\n };\n\n /* 3-digit hex \u2192 6-digit (color inputs only accept #rrggbb) */\n function hex6(v, fallback) {\n if (typeof v !== \"string\") return fallback;\n var m = v.match(/^#([0-9a-fA-F]{3})$/);\n if (m) return \"#\" + m[1].charAt(0) + m[1].charAt(0) + m[1].charAt(1) + m[1].charAt(1) +\n m[1].charAt(2) + m[1].charAt(2);\n return /^#[0-9a-fA-F]{6}$/.test(v) ? v : fallback;\n }\n\n api.tokenPanel = function () {\n var p = h(\"div\", \"tokpanel\");\n p.appendChild(h(\"h3\", null, \"Design tokens\"));\n var t = (data && data.tokens) || {}, c = t.colors || {};\n [[\"accent\", \"#2f6b52\"], [\"bg\", \"#ffffff\"], [\"surface\", \"#ffffff\"],\n [\"text\", \"#2a2e2a\"], [\"muted\", \"#6b706b\"]].forEach(function (def) {\n var name = def[0];\n var lab = h(\"label\");\n lab.appendChild(h(\"span\", null, name));\n var inp = h(\"input\");\n inp.setAttribute(\"type\", \"color\");\n inp.value = hex6(c[name], def[1]);\n inp.setAttribute(\"aria-label\", \"Token color \" + name);\n inp.onchange = function () {\n postEdit({ op: \"set-token\", path: \"colors.\" + name, value: inp.value });\n };\n lab.appendChild(inp);\n p.appendChild(lab);\n });\n var rl = h(\"label\");\n rl.appendChild(h(\"span\", null, \"radius\"));\n var ri = h(\"input\");\n ri.setAttribute(\"type\", \"number\");\n ri.setAttribute(\"min\", \"0\"); ri.setAttribute(\"max\", \"32\"); ri.setAttribute(\"step\", \"1\");\n ri.value = typeof t.radius === \"number\" ? String(t.radius) : \"8\";\n ri.setAttribute(\"aria-label\", \"Token radius in px\");\n ri.onchange = function () {\n var n = Number(ri.value);\n if (isFinite(n)) postEdit({ op: \"set-token\", path: \"radius\", value: n });\n };\n rl.appendChild(ri); p.appendChild(rl);\n var fl = h(\"label\");\n fl.appendChild(h(\"span\", null, \"font\"));\n var fi = h(\"input\");\n fi.setAttribute(\"type\", \"text\");\n fi.value = t.font || \"\";\n fi.setAttribute(\"placeholder\", \"Inter, sans-serif\");\n fi.setAttribute(\"aria-label\", \"Token font stack\");\n fi.onchange = function () {\n if (fi.value.trim()) postEdit({ op: \"set-token\", path: \"font\", value: fi.value.trim() });\n };\n fl.appendChild(fi); p.appendChild(fl);\n return p;\n };\n\n function toolButton(txt, label, fn) {\n var x = h(\"button\", null, txt);\n x.setAttribute(\"aria-label\", label);\n x.title = label;\n x.onclick = function (e) { e.stopPropagation(); fn(); };\n return x;\n }\n\n function inlineEdit(w, b, i, screen) {\n var old = w.querySelector(\".eedit\");\n if (old) { old.remove(); return; }\n var isCopy = b.type === \"text\" || b.type === \"hero\";\n var box = h(\"div\", \"eedit\");\n var inp = h(\"input\");\n inp.setAttribute(\"type\", \"text\");\n inp.value = isCopy ? (b.copy || \"\") : (b.label || \"\");\n inp.setAttribute(\"aria-label\", (isCopy ? \"Copy\" : \"Label\") + \" for \" + b.type + \" block \" + (i + 1) +\n \" \\u2014 Enter saves, Escape cancels\");\n inp.onkeydown = function (ev) {\n if (ev.key === \"Enter\") {\n if (isCopy) postEdit({ op: \"set-copy\", screen: screen.id, blockIndex: i, copy: inp.value });\n else postEdit({ op: \"set-label\", screen: screen.id, blockIndex: i, label: inp.value });\n box.remove();\n }\n if (ev.key === \"Escape\") box.remove();\n };\n box.appendChild(inp);\n w.appendChild(box);\n inp.focus();\n inp.select();\n }\n\n /* hover/focus toolbar on each top-level block of an explicit layout */\n api.wrap = function (el, b, i, screen) {\n var total = screen.layout.blocks.length;\n var w = h(\"div\", \"ewrap\");\n w.appendChild(el);\n var tools = h(\"div\", \"etools\");\n if (i > 0) tools.appendChild(toolButton(\"\\u2191\", \"Move \" + b.type + \" block up\", function () {\n postEdit({ op: \"move-block\", screen: screen.id, from: i, to: i - 1 });\n }));\n if (i < total - 1) tools.appendChild(toolButton(\"\\u2193\", \"Move \" + b.type + \" block down\", function () {\n postEdit({ op: \"move-block\", screen: screen.id, from: i, to: i + 1 });\n }));\n if ([\"text\", \"hero\", \"button\", \"field\"].indexOf(b.type) >= 0) {\n var what = b.type === \"text\" || b.type === \"hero\" ? \"copy\" : \"label\";\n tools.appendChild(toolButton(\"\\u270e\", \"Edit \" + b.type + \" \" + what, function () {\n inlineEdit(w, b, i, screen);\n }));\n }\n tools.appendChild(toolButton(\"\\u2715\", \"Remove \" + b.type + \" block\", function () {\n if (window.confirm(\"Remove this \" + b.type + \" block?\"))\n postEdit({ op: \"remove-block\", screen: screen.id, blockIndex: i });\n }));\n w.appendChild(tools);\n return w;\n };\n\n api.addButton = function (screen) {\n var wrap = h(\"div\");\n var pal = null;\n var btn = h(\"button\", \"addblock\", \"+ add block\");\n btn.setAttribute(\"aria-expanded\", \"false\");\n btn.onclick = function () {\n if (pal) { pal.remove(); pal = null; btn.setAttribute(\"aria-expanded\", \"false\"); return; }\n pal = h(\"div\", \"palette\");\n pal.setAttribute(\"role\", \"menu\");\n BLOCK_TYPES.forEach(function (t) {\n var tb = h(\"button\", null, t);\n tb.setAttribute(\"role\", \"menuitem\");\n tb.onclick = function () {\n postEdit({ op: \"add-block\", screen: screen.id, index: screen.layout.blocks.length, block: { type: t } });\n pal.remove(); pal = null;\n btn.setAttribute(\"aria-expanded\", \"false\");\n };\n pal.appendChild(tb);\n });\n wrap.appendChild(pal);\n btn.setAttribute(\"aria-expanded\", \"true\");\n pal.firstChild.focus();\n };\n wrap.appendChild(btn);\n return wrap;\n };\n\n return api;\n})();\n/*__UXLOOM_EDIT_END__*/\n/*__UXLOOM_LIVE__*/\nnew EventSource(\"/events\").onmessage = function () { load(); };\n/*__UXLOOM_LIVE_END__*/\nload();\n</script>\n</body>\n</html>";
export declare const PREVIEW_TEMPLATE = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>UXLoom preview</title>\n<style>\n :root {\n --bg: #eceeec; --panel: #f8f9f8; --ink: #2a2e2a; --dim: #6b706b;\n --line: #d2d6d2; --block: #ffffff; --blockline: #c4c9c4;\n --accent: #2f6b52; --warn: #b45309; --err: #b04338;\n }\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font: 14px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n background: var(--bg); color: var(--ink); height: 100vh; display: flex; }\n button { font: inherit; cursor: pointer; background: none; border: none; color: inherit; }\n\n /* sidebar */\n aside { width: 250px; flex-shrink: 0; background: var(--panel); border-right: 1px solid var(--line);\n overflow-y: auto; padding: 14px; }\n aside h1 { font-size: 15px; margin-bottom: 2px; }\n aside .sub { color: var(--dim); font-size: 12px; margin-bottom: 14px; }\n aside h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--dim);\n margin: 14px 0 6px; }\n .jname { font-weight: 600; font-size: 13px; margin-top: 8px; }\n .jplat { color: var(--dim); font-weight: 400; font-size: 11px; margin-left: 5px; }\n .jstate, .snav { display: block; width: 100%; text-align: left; padding: 3px 8px; border-radius: 6px;\n font-size: 13px; color: var(--ink); }\n .jstate:hover, .snav:hover { background: #e6e9e6; }\n .jstate.on, .snav.on { background: var(--accent); color: #fff; }\n .jstate small { color: inherit; opacity: .65; }\n .final-flag { opacity: .6; font-size: 11px; }\n\n /* main */\n main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }\n .bar { display: flex; align-items: center; gap: 8px; padding: 10px 16px;\n border-bottom: 1px solid var(--line); background: var(--panel); flex-wrap: wrap; }\n .bar .grow { flex: 1; }\n .chip { font-size: 12px; color: var(--dim); border: 1px solid var(--line); border-radius: 999px;\n padding: 2px 10px; }\n .chip.live { border-color: var(--accent); color: var(--accent); }\n .vp, .tab { padding: 4px 10px; border-radius: 6px; font-size: 13px; border: 1px solid transparent; }\n .vp.on { background: var(--ink); color: #fff; }\n .tab { border-color: var(--line); }\n .tab.on { background: var(--accent); color: #fff; border-color: var(--accent); }\n .tab.undesigned { border-style: dashed; color: var(--dim); }\n .tabs { display: flex; gap: 6px; padding: 10px 16px 0; flex-wrap: wrap; }\n\n .stage { flex: 1; overflow: auto; display: flex; align-items: flex-start; justify-content: center;\n padding: 26px; }\n .stage.commenting .screen { cursor: crosshair; }\n .frame { background: #fff; border: 1px solid var(--line); border-radius: 12px;\n box-shadow: 0 8px 30px rgba(0,0,0,.08); overflow: hidden; flex-shrink: 0; }\n .frame.desktop { width: 960px; } .frame.tablet { width: 640px; }\n .chrome { display: flex; align-items: center; gap: 6px; padding: 8px 12px; background: #eef0ee;\n border-bottom: 1px solid var(--line); font-size: 11px; color: var(--dim); }\n .dot { width: 9px; height: 9px; border-radius: 50%; background: #cfd3cf; }\n .chrome .url { flex: 1; background: #fff; border: 1px solid var(--line); border-radius: 6px;\n padding: 2px 10px; text-align: center; }\n .notch { flex: 1; text-align: center; }\n\n /* mobile & tablet: render as an actual device so the viewport reads as one at a glance */\n .frame.mobile, .frame.tablet { border: 14px solid #0c0c0e; background: #0c0c0e;\n box-shadow: 0 26px 64px rgba(0,0,0,.30); position: relative; }\n .frame.mobile { width: 372px; border-radius: 52px; }\n .frame.tablet { width: 720px; border-width: 16px; border-radius: 34px; }\n .frame.mobile .chrome, .frame.tablet .chrome { background: var(--mock-bg, #fff);\n color: var(--mock-text, var(--ink)); border-bottom: none;\n justify-content: space-between; padding: 12px 24px 5px;\n font-weight: 600; font-size: 12px; }\n .frame.mobile .screen { min-height: 560px; }\n .frame.tablet .screen { min-height: 500px; }\n /* phone dynamic island vs tablet camera dot */\n .frame.mobile .island { position: absolute; top: 11px; left: 50%; transform: translateX(-50%);\n width: 108px; height: 26px; background: #000; border-radius: 14px; z-index: 4;\n box-shadow: 0 0 0 1px rgba(128,128,128,.25); }\n .frame.tablet .cam { position: absolute; top: 8px; left: 50%; transform: translateX(-50%);\n width: 7px; height: 7px; border-radius: 50%; background: #23232a; z-index: 4;\n box-shadow: 0 0 0 2px rgba(255,255,255,.06); }\n .st-right { display: inline-flex; align-items: center; gap: 7px; }\n .batt { display: inline-block; width: 22px; height: 11px; border: 1.5px solid currentColor;\n border-radius: 3px; position: relative; opacity: .9; }\n .batt::after { content: \"\"; position: absolute; right: -3px; top: 3px; width: 2px; height: 5px;\n background: currentColor; border-radius: 0 1px 1px 0; }\n .batt::before { content: \"\"; position: absolute; left: 1.5px; top: 1.5px; bottom: 1.5px;\n width: 68%; background: currentColor; border-radius: 1px; }\n .frame.mobile .home-ind, .frame.tablet .home-ind { background: var(--mock-bg, #fff);\n color: var(--mock-text, var(--ink)); display: flex;\n justify-content: center; align-items: center; height: 24px; }\n .frame.mobile .home-ind i { width: 128px; height: 5px; border-radius: 3px; background: currentColor; opacity: .32; }\n .frame.tablet .home-ind i { width: 180px; height: 5px; border-radius: 3px; background: currentColor; opacity: .3; }\n .screen { position: relative; padding: 14px; min-height: 420px; display: flex;\n flex-direction: column; gap: 10px;\n background: var(--mock-bg, transparent);\n color: var(--mock-text, var(--ink));\n font-family: var(--mock-font, inherit); }\n\n /* wireframe blocks \u2014 themed via --mock-* custom properties when the\n project declares tokens; grayscale defaults otherwise */\n .b { border: 1.5px solid var(--blockline); border-radius: var(--mock-radius, 8px);\n background: var(--block); padding: 10px 12px; position: relative; }\n .b .lab { font-size: 12px; color: var(--mock-muted, var(--dim)); }\n .b-header, .b-nav, .b-footer { background: #f1f3f1; display: flex; gap: 10px; align-items: center; }\n .b-nav .pill, .b-header .pill { width: 54px; height: 8px; border-radius: 4px; background: var(--blockline); }\n .b-hero { min-height: 90px; display: flex; align-items: center; justify-content: center; }\n .b-hero .copy { font-size: 17px; font-weight: 600; text-align: center; }\n .b-text .ln { height: 8px; border-radius: 4px; background: #e0e3e0; margin: 7px 0; }\n .b-text .ln:last-child { width: 60%; }\n .copy { font-size: 13px; white-space: pre-wrap; }\n .b-image { min-height: 80px;\n background: repeating-linear-gradient(45deg, #f4f5f4, #f4f5f4 8px, #e8eae8 8px, #e8eae8 9px);\n display: flex; align-items: center; justify-content: center; }\n .b-button { display: inline-block; background: var(--mock-accent, var(--ink)); color: #fff;\n border-radius: var(--mock-radius, 8px);\n padding: 9px 18px; font-size: 13px; align-self: flex-start; border: none; }\n .b-field { background: var(--block); } .b-field .inp { height: 30px; border: 1.5px solid var(--blockline);\n border-radius: 6px; margin-top: 5px; background: #fdfdfd; }\n .row { display: flex; gap: 10px; align-items: center; border: 1.5px solid var(--blockline);\n border-radius: var(--mock-radius, 8px); padding: 9px 12px; background: var(--block); }\n .row .av { width: 26px; height: 26px; border-radius: 50%; background: #e0e3e0; flex-shrink: 0; }\n .row .ln { height: 8px; border-radius: 4px; background: #e0e3e0; flex: 1; }\n .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }\n .tbl { border: 1.5px solid var(--blockline); border-radius: var(--mock-radius, 8px);\n overflow: hidden; background: var(--block); }\n .tbl .tr { display: flex; border-top: 1px solid #e4e7e4; }\n .tbl .tr:first-child { border-top: none; background: #f1f3f1; }\n .tbl .td { flex: 1; padding: 8px; } .tbl .td .ln { height: 8px; border-radius: 4px; background: #e0e3e0; }\n .tbl .td.hd { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em;\n color: var(--mock-muted, var(--dim)); }\n .srcchip { display: inline-block; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 10px; color: var(--mock-muted, var(--dim)); border: 1px solid var(--blockline);\n border-radius: 4px; padding: 0 5px; margin-top: 5px; align-self: flex-start; }\n .srcchip + .srcchip { margin-left: 4px; }\n .kids { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }\n\n /* state treatments */\n .skel .b, .skel .row, .skel .tbl { border-color: #e4e7e4; }\n .skel .ln, .skel .pill, .skel .av, .skel .inp, .skel .b-image, .skel .b-button, .skel .copy {\n background: linear-gradient(90deg, #ececec 25%, #f7f7f7 50%, #ececec 75%);\n background-size: 200% 100%; animation: shimmer 1.4s infinite; color: transparent; border-color: transparent; }\n @keyframes shimmer { to { background-position: -200% 0; } }\n @media (prefers-reduced-motion: reduce) { .skel * { animation: none !important; } }\n .banner { border: 1.5px solid var(--err); background: #fdf3f2; color: var(--err);\n border-radius: 8px; padding: 10px 12px; font-size: 13px; }\n .dimmed { opacity: .35; pointer-events: none; }\n .emptybox { border: 2px dashed var(--blockline); border-radius: 10px; padding: 34px 16px;\n text-align: center; color: var(--mock-muted, var(--dim)); }\n .overlay { position: absolute; inset: 0; background: rgba(42,46,42,.35); display: flex;\n align-items: center; justify-content: center; border-radius: 0 0 10px 10px; }\n .modal { background: var(--block); border-radius: var(--mock-radius, 12px); padding: 18px;\n width: min(85%, 340px); box-shadow: 0 12px 40px rgba(0,0,0,.25); }\n .modal h4 { font-size: 13px; margin-bottom: 8px; }\n\n /* events + meta */\n .meta { border-top: 1px solid var(--line); background: var(--panel); padding: 10px 16px;\n display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }\n .meta .lbl { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--dim); }\n .ev { border: 1px solid var(--accent); color: var(--accent); border-radius: 999px;\n padding: 3px 12px; font-size: 12.5px; }\n .ev:hover { background: var(--accent); color: #fff; }\n .ev small { opacity: .7; }\n .ev .g { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10.5px;\n opacity: .85; margin-left: 4px; }\n .note { font-size: 12px; color: var(--warn); }\n .datameta { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px;\n color: var(--dim); }\n .err-load { margin: auto; color: var(--err); }\n\n /* rationale (the \"why\" evidence panel, R20) */\n .whypanel { width: 340px; flex-shrink: 0; background: var(--panel); border-left: 1px solid var(--line);\n overflow-y: auto; padding: 14px 16px; outline: none; }\n .whytop { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }\n .whytitle { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--dim); }\n .whyclose { color: var(--dim); font-size: 14px; padding: 2px 6px; border-radius: 6px; }\n .whyclose:hover, .whyclose:focus-visible { background: #e6e9e6; color: var(--ink); }\n .rat-scope { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px;\n color: var(--dim); margin-bottom: 2px; }\n .rat-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }\n .rat-decision { font-size: 14px; font-weight: 600; }\n .rat-conf { display: inline-block; font-size: 10.5px; border-radius: 999px; padding: 1px 9px;\n border: 1px solid var(--line); color: var(--dim); white-space: nowrap; }\n .rat-conf.medium { border-color: var(--accent); color: var(--accent); }\n .rat-conf.high { background: var(--accent); border-color: var(--accent); color: #fff; }\n .rat-reason { font-size: 12.5px; margin: 6px 0 10px; }\n .rat-alts { width: 100%; border-collapse: collapse; font-size: 11.5px; margin: 8px 0 10px; }\n .rat-alts caption { text-align: left; font-size: 10.5px; text-transform: uppercase;\n letter-spacing: .05em; color: var(--dim); margin-bottom: 4px; }\n .rat-alts th, .rat-alts td { border: 1px solid var(--line); padding: 4px 6px; text-align: left;\n vertical-align: top; }\n .rat-alts th { background: #f1f3f1; font-size: 10px; text-transform: uppercase;\n letter-spacing: .05em; color: var(--dim); }\n .rat-opt { font-weight: 600; }\n .rat-pros, .rat-cons { list-style: disc; padding-left: 14px; margin: 0; }\n .rat-pros li { color: #2e7d4f; }\n .rat-cons li { color: var(--err); }\n .rat-src { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-top: 4px; }\n .rat-lbl { font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--dim); }\n .rat-src a { color: var(--accent); font-size: 12px; }\n .whydetails { border-top: 1px solid var(--line); margin-top: 12px; padding-top: 10px; }\n .whydetails summary { cursor: pointer; font-size: 12px; font-weight: 600; margin-bottom: 6px; }\n .whymark { margin-left: 5px; font-size: 11px; opacity: .55; }\n\n /* comment mode */\n .cbtn { border: 1px solid var(--line); border-radius: 999px; padding: 3px 12px; font-size: 12.5px; }\n .cbtn.on { background: var(--accent); color: #fff; border-color: var(--accent); }\n .cbtn .cnt { display: inline-block; margin-left: 6px; min-width: 17px; text-align: center;\n border-radius: 999px; background: var(--accent); color: #fff; font-size: 11px; padding: 0 4px; }\n .cbtn.on .cnt { background: #fff; color: var(--accent); }\n .pinwrap { position: absolute; transform: translate(-50%, -50%); z-index: 5; }\n .pin { width: 20px; height: 20px; border-radius: 50%; background: var(--accent); color: #fff;\n font-size: 11px; line-height: 20px; text-align: center; padding: 0;\n box-shadow: 0 2px 6px rgba(0,0,0,.35); }\n .pinpop { display: none; position: absolute; top: 24px; left: -4px; background: #fff;\n color: var(--ink); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px;\n width: 210px; font-size: 12.5px; box-shadow: 0 8px 24px rgba(0,0,0,.2); z-index: 6;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif; }\n .pinwrap:hover .pinpop, .pinwrap.open .pinpop, .pinwrap:focus-within .pinpop { display: block; }\n .pinpop .resolve { margin-top: 6px; border: 1px solid var(--accent); color: var(--accent);\n border-radius: 6px; padding: 2px 10px; font-size: 12px; }\n .pinpop .resolve:hover { background: var(--accent); color: #fff; }\n .cform { position: absolute; z-index: 7; background: #fff; border: 1px solid var(--line);\n border-radius: 8px; padding: 8px; box-shadow: 0 8px 24px rgba(0,0,0,.25); width: 230px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif; }\n .cform input { width: 100%; font: inherit; font-size: 12.5px; padding: 4px 6px;\n border: 1px solid var(--line); border-radius: 6px; }\n .cform .acts { display: flex; gap: 6px; margin-top: 6px; justify-content: flex-end; }\n .cform .acts button { font-size: 12px; border: 1px solid var(--line); border-radius: 6px; padding: 2px 10px; }\n .cform .acts .save { background: var(--accent); color: #fff; border-color: var(--accent); }\n .pin.assigned { background: var(--warn); }\n .pinpop .acts { display: flex; gap: 6px; margin-top: 6px; }\n .pinpop .assign { margin-top: 6px; border: 1px solid var(--warn); color: var(--warn);\n border-radius: 6px; padding: 2px 10px; font-size: 12px; }\n .pinpop .assign:hover { background: var(--warn); color: #fff; }\n .pinpop .asgchip { display: inline-block; margin-top: 6px; font-size: 11px; color: var(--warn);\n border: 1px solid var(--warn); border-radius: 999px; padding: 0 8px; }\n .cnt.asg { background: var(--warn); }\n .cbtn.on .cnt.asg { background: var(--warn); color: #fff; }\n .atoast { position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%); z-index: 40;\n background: var(--ink); color: #fff; border-radius: 10px; padding: 10px 14px;\n box-shadow: 0 8px 24px rgba(0,0,0,.35); font-size: 12.5px; max-width: 560px;\n display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }\n .atoast code { font-size: 11.5px; background: rgba(255,255,255,.12); border-radius: 6px; padding: 2px 6px; }\n .atoast button { border: 1px solid rgba(255,255,255,.4); border-radius: 6px; padding: 2px 10px;\n font-size: 12px; color: #fff; }\n .atoast button:hover { background: rgba(255,255,255,.15); }\n\n /* edit mode */\n .ewrap { position: relative; }\n .etools { display: none; position: absolute; top: -12px; right: 8px; gap: 2px; z-index: 4;\n background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 1px 3px;\n box-shadow: 0 2px 8px rgba(0,0,0,.15);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif; }\n .ewrap:hover > .etools, .ewrap:focus-within > .etools { display: flex; }\n .etools button { font-size: 12px; line-height: 1.4; padding: 1px 6px; border-radius: 4px; }\n .etools button:hover, .etools button:focus-visible { background: #eef0ee; }\n .eedit input { width: 100%; font: inherit; font-size: 13px; padding: 4px 6px; margin-top: 4px;\n border: 1px solid var(--accent); border-radius: 6px; }\n .tokpanel { width: 196px; flex-shrink: 0; background: var(--panel); border: 1px solid var(--line);\n border-radius: 10px; padding: 10px 12px; margin-right: 18px; font-size: 12px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n color: var(--ink); }\n .tokpanel h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--dim);\n margin-bottom: 6px; }\n .tokpanel label { display: flex; align-items: center; justify-content: space-between; gap: 8px;\n margin: 6px 0; }\n .tokpanel input[type=color] { width: 44px; height: 24px; padding: 0; border: 1px solid var(--line);\n border-radius: 4px; background: none; cursor: pointer; }\n .tokpanel input[type=number], .tokpanel input[type=text] { width: 96px; font: inherit; font-size: 12px;\n padding: 2px 5px; border: 1px solid var(--line); border-radius: 4px; }\n .addblock { border: 1.5px dashed var(--blockline); border-radius: 8px; padding: 8px; width: 100%;\n text-align: center; color: var(--dim); font-size: 12.5px; }\n .addblock:hover, .addblock:focus-visible { border-color: var(--accent); color: var(--accent); }\n .palette { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }\n .palette button { border: 1px solid var(--line); border-radius: 6px; padding: 3px 9px; font-size: 12px;\n background: #fff; }\n .palette button:hover, .palette button:focus-visible { border-color: var(--accent); color: var(--accent); }\n .toast { position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%); z-index: 20;\n display: flex; gap: 10px; align-items: center; background: var(--err); color: #fff;\n border-radius: 8px; padding: 8px 14px; font-size: 13px; max-width: 70vw;\n box-shadow: 0 6px 20px rgba(0,0,0,.3); }\n .toast button { color: #fff; font-size: 14px; }\n</style>\n</head>\n<body>\n<aside id=\"side\"></aside>\n<main>\n <div class=\"bar\" id=\"bar\"></div>\n <div class=\"tabs\" id=\"tabs\"></div>\n <div class=\"stage\" id=\"stage\"></div>\n <div class=\"meta\" id=\"meta\"></div>\n</main>\n<div id=\"why\" class=\"whypanel\" role=\"complementary\" aria-label=\"Design rationale\" tabindex=\"-1\" hidden></div>\n<script>\n\"use strict\";\nvar STATIC_INFO = null;\n/*__UXLOOM_STATIC__*/\nvar data = null, comments = [], sel = { screen: null, state: \"default\" }, viewport = \"desktop\";\nvar commentMode = false;\nvar whyOpen = false;\n/* assigned by the edit-mode section below; stripped from static exports */\nvar EDIT = null;\n\nfunction h(tag, cls, text) {\n var el = document.createElement(tag);\n if (cls) el.className = cls;\n if (text != null) el.textContent = text;\n return el;\n}\nfunction screenById(id) { return (data.screens || []).find(function (s) { return s.id === id; }); }\nfunction splitTarget(ref) {\n var i = ref.indexOf(\"#\");\n return i < 0 ? { state: ref } : { state: ref.slice(0, i), screenState: ref.slice(i + 1) };\n}\n/* an \"on\" value is either \"target\" or { target, guard?, roles? } */\nfunction normOn(v) { return typeof v === \"string\" ? { target: v } : (v || {}); }\nfunction pick(screenId, stateId) {\n sel = { screen: screenId, state: stateId || \"default\" };\n render();\n}\nfunction postJson(url, payload) {\n return fetch(url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(payload)\n }).then(function (r) {\n if (!r.ok) throw new Error(\"HTTP \" + r.status);\n return r.json();\n });\n}\n\n/* -------- theme: apply project tokens; grayscale when absent -------- */\nfunction applyTheme() {\n var r = document.documentElement.style;\n [\"--accent\", \"--mock-accent\", \"--mock-bg\", \"--block\", \"--mock-text\", \"--mock-muted\",\n \"--mock-radius\", \"--mock-font\"].forEach(function (p) { r.removeProperty(p); });\n var t = data && data.tokens;\n if (!t) return;\n var c = t.colors || {};\n if (c.accent) { r.setProperty(\"--accent\", c.accent); r.setProperty(\"--mock-accent\", c.accent); }\n if (c.bg) r.setProperty(\"--mock-bg\", c.bg);\n if (c.surface) r.setProperty(\"--block\", c.surface);\n if (c.text) r.setProperty(\"--mock-text\", c.text);\n if (c.muted) r.setProperty(\"--mock-muted\", c.muted);\n if (typeof t.radius === \"number\") r.setProperty(\"--mock-radius\", t.radius + \"px\");\n if (t.font) r.setProperty(\"--mock-font\", t.font);\n}\n\n/* ---------- derive blocks when a screen declares no layout ---------- */\nfunction autoBlocks(screen) {\n var blocks = [{ type: \"header\", label: screen.id }];\n if (screen.intent) blocks.push({ type: \"text\", label: screen.intent });\n var hasList = false;\n (screen.components || []).forEach(function (c) {\n var s = (c.semantic || \"\").toLowerCase();\n /* carry interaction intent (R12) through so derived blocks chip it */\n function derived(type, label) {\n return { type: type, label: label, validation: c.validation, motion: c.motion };\n }\n if (s.indexOf(\"button\") === 0) blocks.push(derived(\"button\", c.label && c.label.en || c.semantic));\n else if (s.indexOf(\"input\") === 0 || s.indexOf(\"field\") === 0)\n blocks.push(derived(\"field\", c.label && c.label.en || c.semantic));\n else if (s.indexOf(\"list\") === 0 || s.indexOf(\"table\") === 0) { blocks.push(derived(\"list\", c.semantic)); hasList = true; }\n else if (s.indexOf(\"nav\") === 0) blocks.splice(1, 0, derived(\"nav\", c.semantic));\n else blocks.push(derived(\"card\", c.semantic));\n });\n if (!hasList) blocks.push({ type: \"list\", label: \"Content\", count: 3 });\n return blocks;\n}\n\n/* -------- R12 chips: validation / sort / filter / motion intent ------ */\nfunction metaChip(text, title) {\n var s = h(\"span\", \"srcchip\", text);\n if (title) s.title = title;\n return s;\n}\n/* validation/motion live on components; blocks derived by autoBlocks carry\n them directly, explicit field/button blocks match a component by label */\nfunction blockMeta(b, screen) {\n var v = b.validation, m = b.motion;\n if ((b.type === \"field\" || b.type === \"button\") && screen && screen.components && (!v || !m)) {\n var match = screen.components.find(function (c) {\n return b.label && ((c.label && c.label.en === b.label) || c.semantic === b.label);\n });\n if (match) { if (!v) v = match.validation; if (!m) m = match.motion; }\n }\n return { validation: v, motion: m };\n}\nfunction appendMetaChips(el, b, screen) {\n var meta = blockMeta(b, screen);\n if (meta.validation) {\n if (meta.validation.required) el.appendChild(metaChip(\"required\", meta.validation.message));\n if (meta.validation.pattern)\n el.appendChild(metaChip(\"pattern: \" + meta.validation.pattern, meta.validation.message));\n }\n if (b.type === \"table\" || b.type === \"list\") {\n if (b.sort && b.sort.length) el.appendChild(metaChip(\"sort: \" + b.sort.join(\", \")));\n if (b.filter && b.filter.length) el.appendChild(metaChip(\"filter: \" + b.filter.join(\", \")));\n }\n if (meta.motion === \"decorative\") el.appendChild(metaChip(\"motion: decorative\"));\n}\n\nfunction renderBlock(b, screen) {\n var el, i, j, n = b.count || 3;\n switch (b.type) {\n case \"list\":\n el = h(\"div\", \"kids\");\n for (i = 0; i < n; i++) { var r = h(\"div\", \"row\"); r.appendChild(h(\"div\", \"av\")); r.appendChild(h(\"div\", \"ln\")); el.appendChild(r); }\n break;\n case \"card\":\n el = h(\"div\", \"cards\");\n for (i = 0; i < (b.count || 2); i++) { var c = h(\"div\", \"b\"); c.appendChild(h(\"div\", \"lab\", b.label || \"Card\")); c.appendChild(h(\"div\", \"b-text\")).appendChild(h(\"div\", \"ln\")); el.appendChild(c); }\n break;\n case \"table\":\n el = h(\"div\", \"tbl\");\n var cols = (b.columns && b.columns.length) ? b.columns : null;\n var ncol = cols ? cols.length : 3;\n var hr = h(\"div\", \"tr\");\n for (j = 0; j < ncol; j++) {\n var hd = h(\"div\", \"td\" + (cols ? \" hd\" : \"\"));\n if (cols) hd.textContent = cols[j]; else hd.appendChild(h(\"div\", \"ln\"));\n hr.appendChild(hd);\n }\n el.appendChild(hr);\n for (i = 0; i < n; i++) { var tr = h(\"div\", \"tr\"); for (j = 0; j < ncol; j++) tr.appendChild(h(\"div\", \"td\")).appendChild(h(\"div\", \"ln\")); el.appendChild(tr); }\n break;\n case \"button\": el = h(\"button\", \"b-button\", b.label || \"Action\"); break;\n case \"field\": el = h(\"div\", \"b b-field\"); el.appendChild(h(\"div\", \"lab\", b.label || \"Field\")); el.appendChild(h(\"div\", \"inp\")); break;\n case \"text\":\n el = h(\"div\", \"b b-text\"); if (b.label) el.appendChild(h(\"div\", \"lab\", b.label));\n if (b.copy) { el.appendChild(h(\"div\", \"copy\", b.copy)); }\n else { el.appendChild(h(\"div\", \"ln\")); el.appendChild(h(\"div\", \"ln\")); }\n break;\n case \"image\": el = h(\"div\", \"b b-image\"); el.appendChild(h(\"span\", \"lab\", b.label || \"image\")); break;\n case \"hero\":\n el = h(\"div\", \"b b-hero\");\n if (b.copy) el.appendChild(h(\"span\", \"copy\", b.copy));\n else el.appendChild(h(\"span\", \"lab\", b.label || \"Hero\"));\n break;\n case \"header\": case \"nav\": case \"footer\":\n el = h(\"div\", \"b b-\" + b.type); el.appendChild(h(\"span\", \"lab\", b.label || b.type));\n el.appendChild(h(\"span\", \"pill\")); el.appendChild(h(\"span\", \"pill\")); break;\n case \"form\":\n el = h(\"div\", \"b\"); el.appendChild(h(\"div\", \"lab\", b.label || \"Form\"));\n var kk = h(\"div\", \"kids\"); (b.children || [{ type: \"field\" }, { type: \"field\" }, { type: \"button\", label: \"Submit\" }]).forEach(function (ch) { kk.appendChild(renderBlock(ch, screen)); }); el.appendChild(kk); break;\n default:\n el = h(\"div\", \"b\"); el.appendChild(h(\"div\", \"lab\", b.label || b.type));\n }\n if (b.children && b.type !== \"form\") {\n var kids = h(\"div\", \"kids\");\n b.children.forEach(function (ch) { kids.appendChild(renderBlock(ch, screen)); });\n el.appendChild(kids);\n }\n if (b.source) el.appendChild(h(\"span\", \"srcchip\", b.source));\n appendMetaChips(el, b, screen);\n return el;\n}\n\nfunction renderScreenBody(screen, stateId) {\n var body = h(\"div\", \"screen\");\n body.setAttribute(\"data-ux-screen\", screen.id);\n body.setAttribute(\"data-ux-state\", stateId);\n var blocks = (screen.layout && screen.layout.blocks) || autoBlocks(screen);\n var isError = stateId.indexOf(\"error\") === 0;\n var baseline = stateId === \"default\" || stateId === \"empty\" || stateId === \"loading\";\n /* editing needs an explicit layout: block indexes must map 1:1 to file */\n var editing = EDIT && EDIT.on && !STATIC_INFO && screen.layout && screen.layout.blocks;\n var content = h(\"div\", stateId === \"loading\" ? \"skel\" : \"\");\n content.style.display = \"flex\"; content.style.flexDirection = \"column\"; content.style.gap = \"10px\";\n\n if (stateId === \"empty\") {\n blocks.filter(function (b) { return [\"header\", \"nav\"].indexOf(b.type) >= 0; })\n .forEach(function (b) { content.appendChild(renderBlock(b, screen)); });\n var eb = h(\"div\", \"emptybox\");\n eb.appendChild(h(\"div\", null, \"Nothing here yet\"));\n eb.appendChild(h(\"small\", null, screen.intent || \"Empty state \u2014 first-run guidance goes here\"));\n content.appendChild(eb);\n } else {\n blocks.forEach(function (b, i) {\n var el = renderBlock(b, screen);\n /* comment pins anchor to explicit-layout blocks only (file-stable indexes) */\n if (screen.layout && screen.layout.blocks) {\n el.setAttribute(\"data-bi\", String(i));\n el.setAttribute(\"data-bt\", b.type);\n if (b.label) el.setAttribute(\"data-bl\", b.label);\n }\n content.appendChild(editing ? EDIT.wrap(el, b, i, screen) : el);\n });\n }\n\n if (isError) {\n body.appendChild(h(\"div\", \"banner\", \"\\u26a0 \" + stateId + \" \u2014 what went wrong and how to fix it\"));\n content.className += \" dimmed\";\n }\n body.appendChild(content);\n if (editing && stateId !== \"empty\") body.appendChild(EDIT.addButton(screen));\n\n if (!baseline && !isError) { // custom states render as overlays over the default\n var ov = h(\"div\", \"overlay\"), mo = h(\"div\", \"modal\");\n mo.appendChild(h(\"h4\", null, stateId));\n mo.appendChild(h(\"div\", \"b-text\")).appendChild(h(\"div\", \"ln\"));\n mo.appendChild(h(\"button\", \"b-button\", \"Confirm\"));\n ov.appendChild(mo); body.appendChild(ov);\n }\n return body;\n}\n\n/* --------------------------- comment mode --------------------------- */\n/* effective lifecycle status (RFC 0006): legacy comments have no status */\nfunction cstatus(c) {\n if (c.resolved) return \"resolved\";\n return c.status === \"assigned\" ? \"assigned\" : \"open\";\n}\n\nfunction openCommentsFor(screenId) {\n return comments.filter(function (c) { return cstatus(c) !== \"resolved\" && c.screen === screenId; });\n}\n\nvar AGENT_PROMPT = \"Address the assigned UXLoom comments (comments_list \\u2192 comment_context \\u2192 fix \\u2192 comment_resolve).\";\n\n/* after \"\\u2192 agent\": tell the reviewer the exact line to hand their agent */\nfunction showAssignToast() {\n var old = document.querySelector(\".atoast\");\n if (old) old.remove();\n var t = h(\"div\", \"atoast\");\n t.setAttribute(\"role\", \"status\");\n t.appendChild(h(\"span\", null, \"Assigned. Tell your agent:\"));\n var code = h(\"code\", null, AGENT_PROMPT);\n t.appendChild(code);\n var cp = h(\"button\", null, \"Copy\");\n cp.setAttribute(\"aria-label\", \"Copy the agent prompt\");\n cp.onclick = function () {\n var done = function () { cp.textContent = \"Copied\"; };\n /* clipboard API may be unavailable on plain http \u2014 the visible text is the fallback */\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(AGENT_PROMPT).then(done).catch(done);\n } else done();\n };\n var x = h(\"button\", null, \"\\u00d7\");\n x.setAttribute(\"aria-label\", \"Dismiss\");\n x.onclick = function () { t.remove(); };\n t.appendChild(cp); t.appendChild(x);\n document.body.appendChild(t);\n setTimeout(function () { if (t.parentNode) t.remove(); }, 15000);\n}\n\nfunction attachComments(body, screen) {\n var mine = comments.filter(function (c) {\n return cstatus(c) !== \"resolved\" && c.screen === screen.id && c.state === sel.state;\n });\n mine.forEach(function (c, i) {\n var assigned = cstatus(c) === \"assigned\";\n var w = h(\"div\", \"pinwrap\");\n w.style.left = c.x + \"%\"; w.style.top = c.y + \"%\";\n var p = h(\"button\", \"pin\" + (assigned ? \" assigned\" : \"\"), String(i + 1));\n p.setAttribute(\"aria-label\", \"Comment \" + (i + 1) + (assigned ? \" (assigned to agent)\" : \"\") + \": \" + c.text);\n p.onclick = function (e) { e.stopPropagation(); w.classList.toggle(\"open\"); };\n var pop = h(\"div\", \"pinpop\");\n pop.appendChild(h(\"div\", null, c.text));\n if (assigned) pop.appendChild(h(\"span\", \"asgchip\", \"\\u2192 assigned to agent\"));\n var acts = h(\"div\", \"acts\");\n var rb = h(\"button\", \"resolve\", \"Resolve\");\n rb.onclick = function (e) {\n e.stopPropagation();\n postJson(\"/comments/resolve\", { id: c.id }).then(function () {\n c.resolved = true; c.status = \"resolved\"; render();\n }).catch(function () {});\n };\n acts.appendChild(rb);\n if (!assigned) {\n var ab = h(\"button\", \"assign\", \"\\u2192 agent\");\n ab.setAttribute(\"aria-label\", \"Assign to agent\");\n ab.title = \"Hand this comment to the agent: it reads the pin, the block it points at, and the screen contract\";\n ab.onclick = function (e) {\n e.stopPropagation();\n postJson(\"/comments/assign\", { id: c.id }).then(function (u) {\n c.status = \"assigned\"; c.assignedAt = u.assignedAt;\n showAssignToast(); render();\n }).catch(function () {});\n };\n acts.appendChild(ab);\n }\n pop.appendChild(acts);\n pop.onclick = function (e) { e.stopPropagation(); };\n w.appendChild(p); w.appendChild(pop);\n body.appendChild(w);\n });\n if (commentMode) body.addEventListener(\"click\", function (e) { placePin(e, body, screen); });\n}\n\nfunction placePin(e, body, screen) {\n var existing = body.querySelector(\".cform\");\n if (existing) { existing.remove(); return; }\n if (e.target.closest && e.target.closest(\".pinwrap\")) return;\n var rect = body.getBoundingClientRect();\n var x = Math.round((e.clientX - rect.left) / rect.width * 1000) / 10;\n var y = Math.round((e.clientY - rect.top) / rect.height * 1000) / 10;\n /* anchor the pin to the layout block it lands on (explicit layouts only \u2014\n indexes must map 1:1 to the project file, same rule as edit mode) */\n var anchor = null;\n var be = e.target.closest ? e.target.closest(\"[data-bi]\") : null;\n if (be && body.contains(be)) {\n anchor = { index: +be.getAttribute(\"data-bi\"), type: be.getAttribute(\"data-bt\") };\n if (be.getAttribute(\"data-bl\")) anchor.label = be.getAttribute(\"data-bl\");\n }\n var form = h(\"div\", \"cform\");\n form.style.left = Math.min(Math.max(x, 2), 68) + \"%\";\n form.style.top = Math.min(Math.max(y, 2), 88) + \"%\";\n var inp = h(\"input\");\n inp.setAttribute(\"type\", \"text\");\n inp.setAttribute(\"placeholder\", \"Leave a comment\\u2026\");\n inp.setAttribute(\"aria-label\", \"Comment text\");\n var save = h(\"button\", \"save\", \"Add\"), cancel = h(\"button\", null, \"Cancel\");\n function submit() {\n var text = inp.value.trim();\n if (!text) return;\n var payload = { screen: screen.id, state: sel.state, x: x, y: y, text: text };\n if (anchor) payload.block = anchor;\n postJson(\"/comments\", payload)\n .then(function (c) { comments.push(c); render(); })\n .catch(function () { form.remove(); });\n }\n save.onclick = function (ev) { ev.stopPropagation(); submit(); };\n cancel.onclick = function (ev) { ev.stopPropagation(); form.remove(); };\n inp.onkeydown = function (ev) {\n if (ev.key === \"Enter\") submit();\n if (ev.key === \"Escape\") form.remove();\n };\n form.onclick = function (ev) { ev.stopPropagation(); };\n var acts = h(\"div\", \"acts\"); acts.appendChild(cancel); acts.appendChild(save);\n form.appendChild(inp); form.appendChild(acts);\n body.appendChild(form);\n inp.focus();\n}\n\n/* ------------------------------ chrome ------------------------------ */\nfunction chromeFor(vp) {\n var c = h(\"div\", \"chrome\");\n var native = (data.platforms || []).some(function (p) { return p === \"ios\" || p === \"android\"; });\n if (vp === \"mobile\" || vp === \"tablet\") {\n // iOS-style status bar over a device bezel (see .frame.mobile/.tablet):\n // time, a phone dynamic-island pill or tablet camera dot, and signal +\n // battery. The device frame itself tells the reviewer which viewport this\n // is \u2014 no textual label needed.\n c.appendChild(h(\"span\", \"st-time\", \"9:41\"));\n c.appendChild(h(\"div\", vp === \"mobile\" ? \"island\" : \"cam\"));\n var right = h(\"span\", \"st-right\");\n right.appendChild(h(\"span\", \"st-sig\", native ? data.platforms.join(\" / \") : (vp === \"tablet\" ? \"Wi-Fi\" : \"5G\")));\n right.appendChild(h(\"span\", \"batt\"));\n c.appendChild(right);\n } else {\n c.appendChild(h(\"span\", \"dot\")); c.appendChild(h(\"span\", \"dot\")); c.appendChild(h(\"span\", \"dot\"));\n c.appendChild(h(\"span\", \"url\", (data.name || \"app\") + \".example.com\"));\n }\n return c;\n}\n\n/* ----------- R20: design rationale \u2014 the \"why\" evidence panel ----------- */\n/* the toggle is visible only when the current screen OR the project carries\n rationale; journeys surface theirs via the panel and a muted meta chip */\nfunction hasWhyContent(screen) {\n return !!((screen && screen.rationale) || (data && data.rationale));\n}\nfunction journeysForScreen(screenId) {\n return (data.journeys || []).filter(function (j) {\n return Object.keys(j.states).some(function (sn) { return j.states[sn].screen === screenId; });\n });\n}\nfunction hostOf(u) {\n try { return new URL(u).hostname || u; } catch (e) { return u; }\n}\n/* one rationale rendered as evidence: decision, confidence, reasoning,\n the alternatives that were argued over, and the sources it leans on.\n Every field is optional \u2014 show what exists, never invent the rest. */\nfunction ratBody(r) {\n var box = h(\"div\", \"rat\");\n var head = h(\"div\", \"rat-head\");\n head.appendChild(h(\"h3\", \"rat-decision\", r.decision || \"(no decision recorded)\"));\n if (r.confidence === \"low\" || r.confidence === \"medium\" || r.confidence === \"high\")\n head.appendChild(h(\"span\", \"rat-conf \" + r.confidence, r.confidence + \" confidence\"));\n box.appendChild(head);\n if (r.reasoning) box.appendChild(h(\"p\", \"rat-reason\", r.reasoning));\n if (r.alternatives && r.alternatives.length) {\n var tbl = h(\"table\", \"rat-alts\");\n tbl.appendChild(h(\"caption\", null, \"Alternatives considered\"));\n var thr = h(\"tr\");\n [\"Option\", \"Pros\", \"Cons\"].forEach(function (t) { thr.appendChild(h(\"th\", null, t)); });\n tbl.appendChild(thr);\n r.alternatives.forEach(function (a) {\n var tr = h(\"tr\");\n tr.appendChild(h(\"td\", \"rat-opt\", a.option || \"\"));\n var ptd = h(\"td\"), pul = h(\"ul\", \"rat-pros\");\n (a.pros || []).forEach(function (p) { pul.appendChild(h(\"li\", null, p)); });\n ptd.appendChild(pul); tr.appendChild(ptd);\n var ctd = h(\"td\"), cul = h(\"ul\", \"rat-cons\");\n (a.cons || []).forEach(function (c) { cul.appendChild(h(\"li\", null, c)); });\n ctd.appendChild(cul); tr.appendChild(ctd);\n tbl.appendChild(tr);\n });\n box.appendChild(tbl);\n }\n if (r.sources && r.sources.length) {\n var src = h(\"div\", \"rat-src\");\n src.appendChild(h(\"span\", \"rat-lbl\", \"Sources\"));\n r.sources.forEach(function (u) {\n var a = h(\"a\", null, hostOf(u));\n a.setAttribute(\"href\", u);\n a.setAttribute(\"target\", \"_blank\");\n a.setAttribute(\"rel\", \"noopener\");\n a.title = u;\n src.appendChild(a);\n });\n box.appendChild(src);\n }\n return box;\n}\nfunction closeWhy() {\n whyOpen = false;\n render();\n var t = document.getElementById(\"whytoggle\");\n if (t) t.focus();\n}\nfunction renderWhy(screen) {\n var panel = document.getElementById(\"why\");\n panel.innerHTML = \"\";\n if (!hasWhyContent(screen)) whyOpen = false;\n panel.hidden = !whyOpen;\n if (!whyOpen) return;\n var top = h(\"div\", \"whytop\");\n top.appendChild(h(\"h2\", \"whytitle\", \"Design rationale\"));\n var x = h(\"button\", \"whyclose\", \"\\u2715\");\n x.setAttribute(\"aria-label\", \"Close rationale panel\");\n x.onclick = function () { closeWhy(); };\n top.appendChild(x);\n panel.appendChild(top);\n if (screen && screen.rationale) {\n panel.appendChild(h(\"div\", \"rat-scope\", screen.id));\n panel.appendChild(ratBody(screen.rationale));\n }\n if (data && data.rationale) {\n var pd = h(\"details\", \"whydetails\");\n pd.appendChild(h(\"summary\", null, \"Product direction\"));\n pd.appendChild(ratBody(data.rationale));\n panel.appendChild(pd);\n }\n if (screen) journeysForScreen(screen.id).forEach(function (j) {\n if (!j.rationale) return;\n var jd = h(\"details\", \"whydetails\");\n jd.appendChild(h(\"summary\", null, \"Flow rationale \\u00b7 \" + j.id));\n jd.appendChild(ratBody(j.rationale));\n panel.appendChild(jd);\n });\n}\n\n/* ------------------------------ render ------------------------------ */\nfunction render() {\n if (!data) return;\n if (!sel.screen && data.screens && data.screens.length) sel.screen = data.screens[0].id;\n var screen = screenById(sel.screen);\n\n // sidebar\n var side = document.getElementById(\"side\"); side.innerHTML = \"\";\n side.appendChild(h(\"h1\", null, \"UXLoom preview\"));\n side.appendChild(h(\"div\", \"sub\", data.name + \" \\u00b7 \" + (data.platforms || []).join(\", \")));\n side.appendChild(h(\"h2\", null, \"Journeys\"));\n (data.journeys || []).forEach(function (j) {\n var jn = h(\"div\", \"jname\");\n jn.appendChild(document.createTextNode(j.id));\n if (j.platforms && j.platforms.length) jn.appendChild(h(\"small\", \"jplat\", j.platforms.join(\" \\u00b7 \")));\n side.appendChild(jn);\n Object.keys(j.states).forEach(function (stateName) {\n var js = j.states[stateName];\n var b = h(\"button\", \"jstate\" + (screen && js.screen === screen.id ? \" on\" : \"\"));\n b.appendChild(document.createTextNode(stateName + \" \"));\n var sm = h(\"small\", null, \"\\u2192 \" + js.screen + (js.final ? \" \\u2713\" : \"\"));\n b.appendChild(sm);\n b.onclick = function () { pick(js.screen, \"default\"); };\n side.appendChild(b);\n });\n });\n side.appendChild(h(\"h2\", null, \"Screens\"));\n (data.screens || []).forEach(function (s) {\n var b = h(\"button\", \"snav\" + (screen && s.id === screen.id ? \" on\" : \"\"));\n b.appendChild(document.createTextNode(s.id));\n if (s.rationale) {\n var wm = h(\"span\", \"whymark\", \"\\u24d8\");\n wm.title = \"has design rationale\";\n b.appendChild(wm);\n }\n b.onclick = function () { pick(s.id, \"default\"); };\n side.appendChild(b);\n });\n\n // bar\n var bar = document.getElementById(\"bar\"); bar.innerHTML = \"\";\n [\"desktop\", \"tablet\", \"mobile\"].forEach(function (vp) {\n var b = h(\"button\", \"vp\" + (viewport === vp ? \" on\" : \"\"), vp);\n b.onclick = function () { viewport = vp; render(); };\n bar.appendChild(b);\n });\n bar.appendChild(h(\"div\", \"grow\"));\n if (screen) {\n var req = screen.requiredStates.length;\n var des = screen.requiredStates.filter(function (s) { return screen.designedStates.indexOf(s) >= 0; }).length;\n bar.appendChild(h(\"span\", \"chip\", \"coverage \" + des + \"/\" + req));\n }\n if (!STATIC_INFO && screen) {\n var unresolved = openCommentsFor(screen.id);\n var asgCount = unresolved.filter(function (c) { return cstatus(c) === \"assigned\"; }).length;\n var openCount = unresolved.length - asgCount;\n var cb = h(\"button\", \"cbtn\" + (commentMode ? \" on\" : \"\"));\n cb.appendChild(document.createTextNode(\"\\ud83d\\udcac comment\"));\n if (openCount) cb.appendChild(h(\"span\", \"cnt\", String(openCount)));\n if (asgCount) {\n var asgChip = h(\"span\", \"cnt asg\", asgCount + \"\\u2192\");\n asgChip.setAttribute(\"aria-label\", asgCount + \" assigned to agent\");\n cb.appendChild(asgChip);\n }\n cb.setAttribute(\"aria-pressed\", commentMode ? \"true\" : \"false\");\n cb.title = \"Comment mode: click the mock to leave a pinned note\" +\n (asgCount ? \" \\u00b7 \" + asgCount + \" assigned to agent\" : \"\");\n cb.onclick = function () {\n commentMode = !commentMode;\n if (commentMode && EDIT) EDIT.on = false; /* modes are exclusive */\n render();\n };\n bar.appendChild(cb);\n if (EDIT) bar.appendChild(EDIT.toggleButton());\n }\n if (hasWhyContent(screen)) {\n var wb = h(\"button\", \"cbtn\" + (whyOpen ? \" on\" : \"\"), \"\\u24d8 why\");\n wb.id = \"whytoggle\";\n wb.setAttribute(\"aria-pressed\", whyOpen ? \"true\" : \"false\");\n wb.setAttribute(\"aria-controls\", \"why\");\n wb.title = \"Why: the design rationale behind this screen\";\n wb.onclick = function () {\n whyOpen = !whyOpen;\n render();\n if (whyOpen) document.getElementById(\"why\").focus();\n };\n bar.appendChild(wb);\n }\n if (STATIC_INFO) bar.appendChild(h(\"span\", \"chip\", \"static export \\u00b7 \" + STATIC_INFO.generated));\n else bar.appendChild(h(\"span\", \"chip live\", \"\\u25cf live\"));\n\n // tabs\n var tabs = document.getElementById(\"tabs\"); tabs.innerHTML = \"\";\n if (screen) screen.requiredStates.forEach(function (st) {\n var designed = screen.designedStates.indexOf(st) >= 0;\n var t = h(\"button\", \"tab\" + (sel.state === st ? \" on\" : \"\") + (designed ? \"\" : \" undesigned\"), st + (designed ? \"\" : \" \\u25cb\"));\n t.title = designed ? st : st + \" \u2014 contracted but not designed yet\";\n t.onclick = function () { sel.state = st; render(); };\n tabs.appendChild(t);\n });\n\n // stage\n var stage = document.getElementById(\"stage\"); stage.innerHTML = \"\";\n stage.className = \"stage\" + (commentMode && !STATIC_INFO ? \" commenting\" : \"\");\n if (!screen) { stage.appendChild(h(\"div\", \"err-load\", \"No screens in the project yet \u2014 ask your agent to design some.\")); }\n else {\n if (EDIT && EDIT.on && !STATIC_INFO) stage.appendChild(EDIT.tokenPanel());\n var frame = h(\"div\", \"frame \" + viewport);\n frame.appendChild(chromeFor(viewport));\n var body = renderScreenBody(screen, sel.state);\n if (!STATIC_INFO) attachComments(body, screen);\n frame.appendChild(body);\n if (viewport === \"mobile\" || viewport === \"tablet\") { var hi = h(\"div\", \"home-ind\"); hi.appendChild(h(\"i\")); frame.appendChild(hi); }\n stage.appendChild(frame);\n }\n\n // meta: outgoing events for journey states showing this screen + exemptions\n var meta = document.getElementById(\"meta\"); meta.innerHTML = \"\";\n if (screen) {\n var events = [];\n (data.journeys || []).forEach(function (j) {\n Object.keys(j.states).forEach(function (sn) {\n var js = j.states[sn];\n if (js.screen !== screen.id) return;\n Object.keys(js.on || {}).forEach(function (ev) {\n var o = normOn(js.on[ev]);\n if (!o.target) return;\n events.push({ ev: ev, target: o.target, guard: o.guard, roles: o.roles, journey: j.id });\n });\n });\n });\n if (events.length) {\n meta.appendChild(h(\"span\", \"lbl\", \"Events\"));\n events.forEach(function (e) {\n var t = splitTarget(e.target);\n var jj = (data.journeys || []).find(function (x) { return x.id === e.journey; });\n var targetState = jj && jj.states[t.state];\n var b = h(\"button\", \"ev\");\n b.appendChild(document.createTextNode(e.ev + \" \"));\n b.appendChild(h(\"small\", null, \"\\u2192 \" + (targetState ? targetState.screen : t.state) + (t.screenState ? \"#\" + t.screenState : \"\")));\n if (e.guard) b.appendChild(h(\"span\", \"g\", \"[if \" + e.guard + \"]\"));\n if (e.roles && e.roles.length) b.appendChild(h(\"span\", \"g\", \"[\" + e.roles.join(\"|\") + \"]\"));\n b.onclick = function () { if (targetState) pick(targetState.screen, t.screenState || \"default\"); };\n meta.appendChild(b);\n });\n }\n if (screen.data) {\n var fields = Object.keys(screen.data);\n if (fields.length) meta.appendChild(h(\"span\", \"datameta\",\n \"data: \" + fields.map(function (k) { return k + \": \" + screen.data[k]; }).join(\", \")));\n }\n (screen.exemptions || []).forEach(function (ex) {\n meta.appendChild(h(\"span\", \"note\", \"exempt \" + ex.state + \": \" + ex.reason));\n });\n journeysForScreen(screen.id).forEach(function (j) {\n if (j.rationale && j.rationale.decision)\n meta.appendChild(h(\"span\", \"chip flowchip\", \"flow: \" + j.rationale.decision));\n });\n }\n\n renderWhy(screen);\n}\n\nfunction boot(p) {\n data = p;\n if (sel.screen && !screenById(sel.screen)) sel = { screen: null, state: \"default\" };\n var hasDesktop = (data.platforms || []).indexOf(\"web\") >= 0;\n if (!hasDesktop && viewport === \"desktop\") viewport = \"mobile\";\n applyTheme();\n render();\n}\n\nfunction load() {\n if (STATIC_INFO) { boot(STATIC_INFO.data); return; }\n Promise.all([\n fetch(\"/project\").then(function (r) { return r.json(); }),\n fetch(\"/comments\").then(function (r) { return r.json(); }).catch(function () { return { comments: [] }; })\n ]).then(function (rs) {\n var p = rs[0];\n if (p && p.error) throw new Error(p.error);\n comments = (rs[1] && rs[1].comments) || [];\n boot(p);\n }).catch(function (e) {\n document.getElementById(\"stage\").innerHTML = \"\";\n document.getElementById(\"stage\").appendChild(h(\"div\", \"err-load\", \"Cannot load project: \" + e.message));\n });\n}\ndocument.addEventListener(\"keydown\", function (e) {\n if (e.key === \"Escape\" && whyOpen) closeWhy();\n});\n/*__UXLOOM_EDIT__*/\n/* ------------- structured edit mode (live preview only) ------------- */\nEDIT = (function () {\n var api = { on: false };\n var BLOCK_TYPES = [\"header\", \"nav\", \"hero\", \"text\", \"list\", \"card\", \"form\",\n \"field\", \"button\", \"image\", \"table\", \"footer\", \"custom\"];\n\n function toast(msg) {\n var old = document.querySelector(\".toast\");\n if (old) old.remove();\n var t = h(\"div\", \"toast\");\n t.setAttribute(\"role\", \"alert\");\n t.appendChild(h(\"span\", null, msg));\n var x = h(\"button\", null, \"\\u2715\");\n x.setAttribute(\"aria-label\", \"Dismiss\");\n x.onclick = function () { t.remove(); };\n t.appendChild(x);\n document.body.appendChild(t);\n }\n\n /* POST an op; the file write wakes the SSE watcher, which re-renders\n every viewer \u2014 success needs no local handling at all */\n function postEdit(payload) {\n fetch(\"/edit\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(payload)\n }).then(function (r) {\n return r.json().catch(function () { return {}; }).then(function (j) {\n if (!r.ok) throw new Error(j.error || (\"HTTP \" + r.status));\n });\n }).catch(function (e) { toast(e.message); });\n }\n\n api.toggleButton = function () {\n var b = h(\"button\", \"cbtn\" + (api.on ? \" on\" : \"\"), \"\\u270e edit\");\n b.setAttribute(\"aria-pressed\", api.on ? \"true\" : \"false\");\n b.title = \"Edit mode: tokens, copy, and blocks \\u2014 writes the project file\";\n b.onclick = function () {\n api.on = !api.on;\n if (api.on) commentMode = false; /* modes are exclusive */\n render();\n };\n return b;\n };\n\n /* 3-digit hex \u2192 6-digit (color inputs only accept #rrggbb) */\n function hex6(v, fallback) {\n if (typeof v !== \"string\") return fallback;\n var m = v.match(/^#([0-9a-fA-F]{3})$/);\n if (m) return \"#\" + m[1].charAt(0) + m[1].charAt(0) + m[1].charAt(1) + m[1].charAt(1) +\n m[1].charAt(2) + m[1].charAt(2);\n return /^#[0-9a-fA-F]{6}$/.test(v) ? v : fallback;\n }\n\n api.tokenPanel = function () {\n var p = h(\"div\", \"tokpanel\");\n p.appendChild(h(\"h3\", null, \"Design tokens\"));\n var t = (data && data.tokens) || {}, c = t.colors || {};\n [[\"accent\", \"#2f6b52\"], [\"bg\", \"#ffffff\"], [\"surface\", \"#ffffff\"],\n [\"text\", \"#2a2e2a\"], [\"muted\", \"#6b706b\"]].forEach(function (def) {\n var name = def[0];\n var lab = h(\"label\");\n lab.appendChild(h(\"span\", null, name));\n var inp = h(\"input\");\n inp.setAttribute(\"type\", \"color\");\n inp.value = hex6(c[name], def[1]);\n inp.setAttribute(\"aria-label\", \"Token color \" + name);\n inp.onchange = function () {\n postEdit({ op: \"set-token\", path: \"colors.\" + name, value: inp.value });\n };\n lab.appendChild(inp);\n p.appendChild(lab);\n });\n var rl = h(\"label\");\n rl.appendChild(h(\"span\", null, \"radius\"));\n var ri = h(\"input\");\n ri.setAttribute(\"type\", \"number\");\n ri.setAttribute(\"min\", \"0\"); ri.setAttribute(\"max\", \"32\"); ri.setAttribute(\"step\", \"1\");\n ri.value = typeof t.radius === \"number\" ? String(t.radius) : \"8\";\n ri.setAttribute(\"aria-label\", \"Token radius in px\");\n ri.onchange = function () {\n var n = Number(ri.value);\n if (isFinite(n)) postEdit({ op: \"set-token\", path: \"radius\", value: n });\n };\n rl.appendChild(ri); p.appendChild(rl);\n var fl = h(\"label\");\n fl.appendChild(h(\"span\", null, \"font\"));\n var fi = h(\"input\");\n fi.setAttribute(\"type\", \"text\");\n fi.value = t.font || \"\";\n fi.setAttribute(\"placeholder\", \"Inter, sans-serif\");\n fi.setAttribute(\"aria-label\", \"Token font stack\");\n fi.onchange = function () {\n if (fi.value.trim()) postEdit({ op: \"set-token\", path: \"font\", value: fi.value.trim() });\n };\n fl.appendChild(fi); p.appendChild(fl);\n return p;\n };\n\n function toolButton(txt, label, fn) {\n var x = h(\"button\", null, txt);\n x.setAttribute(\"aria-label\", label);\n x.title = label;\n x.onclick = function (e) { e.stopPropagation(); fn(); };\n return x;\n }\n\n function inlineEdit(w, b, i, screen) {\n var old = w.querySelector(\".eedit\");\n if (old) { old.remove(); return; }\n var isCopy = b.type === \"text\" || b.type === \"hero\";\n var box = h(\"div\", \"eedit\");\n var inp = h(\"input\");\n inp.setAttribute(\"type\", \"text\");\n inp.value = isCopy ? (b.copy || \"\") : (b.label || \"\");\n inp.setAttribute(\"aria-label\", (isCopy ? \"Copy\" : \"Label\") + \" for \" + b.type + \" block \" + (i + 1) +\n \" \\u2014 Enter saves, Escape cancels\");\n inp.onkeydown = function (ev) {\n if (ev.key === \"Enter\") {\n if (isCopy) postEdit({ op: \"set-copy\", screen: screen.id, blockIndex: i, copy: inp.value });\n else postEdit({ op: \"set-label\", screen: screen.id, blockIndex: i, label: inp.value });\n box.remove();\n }\n if (ev.key === \"Escape\") box.remove();\n };\n box.appendChild(inp);\n w.appendChild(box);\n inp.focus();\n inp.select();\n }\n\n /* hover/focus toolbar on each top-level block of an explicit layout */\n api.wrap = function (el, b, i, screen) {\n var total = screen.layout.blocks.length;\n var w = h(\"div\", \"ewrap\");\n w.appendChild(el);\n var tools = h(\"div\", \"etools\");\n if (i > 0) tools.appendChild(toolButton(\"\\u2191\", \"Move \" + b.type + \" block up\", function () {\n postEdit({ op: \"move-block\", screen: screen.id, from: i, to: i - 1 });\n }));\n if (i < total - 1) tools.appendChild(toolButton(\"\\u2193\", \"Move \" + b.type + \" block down\", function () {\n postEdit({ op: \"move-block\", screen: screen.id, from: i, to: i + 1 });\n }));\n if ([\"text\", \"hero\", \"button\", \"field\"].indexOf(b.type) >= 0) {\n var what = b.type === \"text\" || b.type === \"hero\" ? \"copy\" : \"label\";\n tools.appendChild(toolButton(\"\\u270e\", \"Edit \" + b.type + \" \" + what, function () {\n inlineEdit(w, b, i, screen);\n }));\n }\n tools.appendChild(toolButton(\"\\u2715\", \"Remove \" + b.type + \" block\", function () {\n if (window.confirm(\"Remove this \" + b.type + \" block?\"))\n postEdit({ op: \"remove-block\", screen: screen.id, blockIndex: i });\n }));\n w.appendChild(tools);\n return w;\n };\n\n api.addButton = function (screen) {\n var wrap = h(\"div\");\n var pal = null;\n var btn = h(\"button\", \"addblock\", \"+ add block\");\n btn.setAttribute(\"aria-expanded\", \"false\");\n btn.onclick = function () {\n if (pal) { pal.remove(); pal = null; btn.setAttribute(\"aria-expanded\", \"false\"); return; }\n pal = h(\"div\", \"palette\");\n pal.setAttribute(\"role\", \"menu\");\n BLOCK_TYPES.forEach(function (t) {\n var tb = h(\"button\", null, t);\n tb.setAttribute(\"role\", \"menuitem\");\n tb.onclick = function () {\n postEdit({ op: \"add-block\", screen: screen.id, index: screen.layout.blocks.length, block: { type: t } });\n pal.remove(); pal = null;\n btn.setAttribute(\"aria-expanded\", \"false\");\n };\n pal.appendChild(tb);\n });\n wrap.appendChild(pal);\n btn.setAttribute(\"aria-expanded\", \"true\");\n pal.firstChild.focus();\n };\n wrap.appendChild(btn);\n return wrap;\n };\n\n return api;\n})();\n/*__UXLOOM_EDIT_END__*/\n/*__UXLOOM_LIVE__*/\nnew EventSource(\"/events\").onmessage = function () { load(); };\n/*__UXLOOM_LIVE_END__*/\nload();\n</script>\n</body>\n</html>";
/**

@@ -23,0 +23,0 @@ * Turn the live template into one self-contained HTML file: project data

@@ -73,3 +73,3 @@ /**

box-shadow: 0 8px 30px rgba(0,0,0,.08); overflow: hidden; flex-shrink: 0; }
.frame.desktop { width: 960px; } .frame.tablet { width: 640px; } .frame.mobile { width: 390px; }
.frame.desktop { width: 960px; } .frame.tablet { width: 640px; }
.chrome { display: flex; align-items: center; gap: 6px; padding: 8px 12px; background: #eef0ee;

@@ -81,2 +81,33 @@ border-bottom: 1px solid var(--line); font-size: 11px; color: var(--dim); }

.notch { flex: 1; text-align: center; }
/* mobile & tablet: render as an actual device so the viewport reads as one at a glance */
.frame.mobile, .frame.tablet { border: 14px solid #0c0c0e; background: #0c0c0e;
box-shadow: 0 26px 64px rgba(0,0,0,.30); position: relative; }
.frame.mobile { width: 372px; border-radius: 52px; }
.frame.tablet { width: 720px; border-width: 16px; border-radius: 34px; }
.frame.mobile .chrome, .frame.tablet .chrome { background: var(--mock-bg, #fff);
color: var(--mock-text, var(--ink)); border-bottom: none;
justify-content: space-between; padding: 12px 24px 5px;
font-weight: 600; font-size: 12px; }
.frame.mobile .screen { min-height: 560px; }
.frame.tablet .screen { min-height: 500px; }
/* phone dynamic island vs tablet camera dot */
.frame.mobile .island { position: absolute; top: 11px; left: 50%; transform: translateX(-50%);
width: 108px; height: 26px; background: #000; border-radius: 14px; z-index: 4;
box-shadow: 0 0 0 1px rgba(128,128,128,.25); }
.frame.tablet .cam { position: absolute; top: 8px; left: 50%; transform: translateX(-50%);
width: 7px; height: 7px; border-radius: 50%; background: #23232a; z-index: 4;
box-shadow: 0 0 0 2px rgba(255,255,255,.06); }
.st-right { display: inline-flex; align-items: center; gap: 7px; }
.batt { display: inline-block; width: 22px; height: 11px; border: 1.5px solid currentColor;
border-radius: 3px; position: relative; opacity: .9; }
.batt::after { content: ""; position: absolute; right: -3px; top: 3px; width: 2px; height: 5px;
background: currentColor; border-radius: 0 1px 1px 0; }
.batt::before { content: ""; position: absolute; left: 1.5px; top: 1.5px; bottom: 1.5px;
width: 68%; background: currentColor; border-radius: 1px; }
.frame.mobile .home-ind, .frame.tablet .home-ind { background: var(--mock-bg, #fff);
color: var(--mock-text, var(--ink)); display: flex;
justify-content: center; align-items: center; height: 24px; }
.frame.mobile .home-ind i { width: 128px; height: 5px; border-radius: 3px; background: currentColor; opacity: .32; }
.frame.tablet .home-ind i { width: 180px; height: 5px; border-radius: 3px; background: currentColor; opacity: .3; }
.screen { position: relative; padding: 14px; min-height: 420px; display: flex;

@@ -629,6 +660,13 @@ flex-direction: column; gap: 10px;

var native = (data.platforms || []).some(function (p) { return p === "ios" || p === "android"; });
if (vp === "mobile" && native && (data.platforms || []).indexOf("web") < 0 && (data.platforms || []).indexOf("mweb") < 0) {
c.appendChild(h("span", "notch", "9:41 \\u2014 " + data.platforms.join(" / ")));
} else if (vp === "mobile") {
c.appendChild(h("span", "notch", "9:41 \\u2014 mobile web"));
if (vp === "mobile" || vp === "tablet") {
// iOS-style status bar over a device bezel (see .frame.mobile/.tablet):
// time, a phone dynamic-island pill or tablet camera dot, and signal +
// battery. The device frame itself tells the reviewer which viewport this
// is — no textual label needed.
c.appendChild(h("span", "st-time", "9:41"));
c.appendChild(h("div", vp === "mobile" ? "island" : "cam"));
var right = h("span", "st-right");
right.appendChild(h("span", "st-sig", native ? data.platforms.join(" / ") : (vp === "tablet" ? "Wi-Fi" : "5G")));
right.appendChild(h("span", "batt"));
c.appendChild(right);
} else {

@@ -850,2 +888,3 @@ c.appendChild(h("span", "dot")); c.appendChild(h("span", "dot")); c.appendChild(h("span", "dot"));

frame.appendChild(body);
if (viewport === "mobile" || viewport === "tablet") { var hi = h("div", "home-ind"); hi.appendChild(h("i")); frame.appendChild(hi); }
stage.appendChild(frame);

@@ -852,0 +891,0 @@ }

@@ -1,1 +0,1 @@

{"version":3,"file":"preview-template.js","sourceRoot":"","sources":["../src/preview-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA0kCxB,CAAC;AAET,MAAM,aAAa,GAAG,uBAAuB,CAAC;AAC9C,MAAM,UAAU,GAAG,qBAAqB,CAAC;AACzC,MAAM,QAAQ,GAAG,yBAAyB,CAAC;AAC3C,MAAM,UAAU,GAAG,qBAAqB,CAAC;AACzC,MAAM,QAAQ,GAAG,yBAAyB,CAAC;AAE3C,SAAS,YAAY,CAAC,IAAY,EAAE,WAAmB,EAAE,SAAiB;IACxE,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACxC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACpC,OAAO,KAAK,IAAI,CAAC,IAAI,GAAG,GAAG,KAAK;QAC9B,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC;QAC3D,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,WAAmB;IAClD,uEAAuE;IACvE,sEAAsE;IACtE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAClF,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACxD,MAAM,SAAS,GACb,6BAA6B,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,UAAU,GAAG,QAAQ,GAAG,KAAK,CAAC;IAC5F,IAAI,IAAI,GAAG,gBAAgB,CAAC,OAAO,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IAC9D,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IAChD,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IAChD,OAAO,IAAI,CAAC;AACd,CAAC"}
{"version":3,"file":"preview-template.js","sourceRoot":"","sources":["../src/preview-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAinCxB,CAAC;AAET,MAAM,aAAa,GAAG,uBAAuB,CAAC;AAC9C,MAAM,UAAU,GAAG,qBAAqB,CAAC;AACzC,MAAM,QAAQ,GAAG,yBAAyB,CAAC;AAC3C,MAAM,UAAU,GAAG,qBAAqB,CAAC;AACzC,MAAM,QAAQ,GAAG,yBAAyB,CAAC;AAE3C,SAAS,YAAY,CAAC,IAAY,EAAE,WAAmB,EAAE,SAAiB;IACxE,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACxC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACpC,OAAO,KAAK,IAAI,CAAC,IAAI,GAAG,GAAG,KAAK;QAC9B,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC;QAC3D,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,WAAmB;IAClD,uEAAuE;IACvE,sEAAsE;IACtE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAClF,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACxD,MAAM,SAAS,GACb,6BAA6B,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,UAAU,GAAG,QAAQ,GAAG,KAAK,CAAC;IAC5F,IAAI,IAAI,GAAG,gBAAgB,CAAC,OAAO,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IAC9D,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IAChD,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IAChD,OAAO,IAAI,CAAC;AACd,CAAC"}
{
"name": "uxloom",
"version": "0.10.0",
"version": "0.10.1",
"description": "UXLoom — agent-native UI/UX design validation via MCP. Your generator gave you 6 screens; UXLoom proves you're missing 9 states.",

@@ -5,0 +5,0 @@ "keywords": [