@@ -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"} |
+1
-1
| { | ||
| "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": [ |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
532698
1.12%5868
0.81%