@scenetest/vite-plugin 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +0,0 @@
1
- /**
2
- * Auto-generated dev panel script
3
- * DO NOT EDIT - this file is generated by scripts/bundle-dev-panel.mjs
4
- *
5
- * To modify the dev panel, edit files in src/dev-panel/ and run:
6
- * pnpm run build:dev-panel
7
- */
8
- export declare const devPanelScript = "\"use strict\";\n(() => {\n // src/dev-panel/state.ts\n var assertions = [];\n var groups = [];\n var assertionHistory = /* @__PURE__ */ new Map();\n var locationGroups = /* @__PURE__ */ new Map();\n var passCount = 0;\n var failCount = 0;\n function incrementPassCount() {\n passCount++;\n }\n function incrementFailCount() {\n failCount++;\n }\n var panel = null;\n var listEl = null;\n var fullscreenWindow = null;\n var filter = \"all\";\n var collapsedMode = true;\n var viewMode = \"grouped\";\n var sequenceLocationKey = null;\n function setPanel(el) {\n panel = el;\n }\n function setListEl(el) {\n listEl = el;\n }\n function setFullscreenWindow(win) {\n fullscreenWindow = win;\n }\n function setFilter(newFilter) {\n filter = newFilter;\n }\n function setViewMode(mode) {\n viewMode = mode;\n if (mode !== \"sequence\") {\n sequenceLocationKey = null;\n }\n }\n function setSequenceLocation(key) {\n sequenceLocationKey = key;\n if (key !== null) {\n viewMode = \"sequence\";\n }\n }\n function getLocationKey(result) {\n if (!result.location) return null;\n return `${result.location.file}:${result.location.line}`;\n }\n function trackLocationGroup(result, index) {\n const key = getLocationKey(result);\n if (!key || !result.location) return;\n let group = locationGroups.get(key);\n if (!group) {\n group = {\n key,\n location: result.location,\n description: result.description,\n entries: [],\n lastResult: result.result,\n lastTimestamp: result.timestamp\n };\n locationGroups.set(key, group);\n }\n group.description = result.description;\n group.lastResult = result.result;\n group.lastTimestamp = result.timestamp;\n group.entries.push({\n result: result.result,\n timestamp: result.timestamp,\n index,\n description: result.description,\n context: result.context\n });\n }\n var GROUP_THRESHOLD_MS = 50;\n var pendingGroup = null;\n var groupTimeout = null;\n function setPendingGroup(group) {\n pendingGroup = group;\n }\n function setGroupTimeout(timeout) {\n groupTimeout = timeout;\n }\n function clearAll() {\n assertions.length = 0;\n groups.length = 0;\n assertionHistory.clear();\n locationGroups.clear();\n passCount = 0;\n failCount = 0;\n pendingGroup = null;\n sequenceLocationKey = null;\n if (groupTimeout) {\n clearTimeout(groupTimeout);\n groupTimeout = null;\n }\n }\n\n // src/dev-panel/history.ts\n function trackAssertion(result, index) {\n const key = result.description;\n if (!assertionHistory.has(key)) {\n assertionHistory.set(key, []);\n }\n const history = assertionHistory.get(key);\n history.push({ result: result.result, timestamp: result.timestamp, index });\n return history;\n }\n function getHistoryStats(description, currentIndex) {\n const history = assertionHistory.get(description);\n if (!history || history.length <= 1) return null;\n let priorPassed = 0;\n let priorFailed = 0;\n let afterPassed = 0;\n let afterFailed = 0;\n for (const entry of history) {\n if (entry.index < currentIndex) {\n if (entry.result) priorPassed++;\n else priorFailed++;\n } else if (entry.index > currentIndex) {\n if (entry.result) afterPassed++;\n else afterFailed++;\n }\n }\n const total = history.length - 1;\n if (total === 0) return null;\n return { priorPassed, priorFailed, afterPassed, afterFailed, total };\n }\n function formatHistorySummary(stats) {\n if (!stats) return \"\";\n const parts = [];\n const priorTotal = stats.priorPassed + stats.priorFailed;\n const afterTotal = stats.afterPassed + stats.afterFailed;\n if (priorTotal > 0) {\n if (stats.priorFailed === 0) {\n parts.push(`${priorTotal} prior \\u2713`);\n } else if (stats.priorPassed === 0) {\n parts.push(`${priorTotal} prior \\u2717`);\n } else {\n parts.push(`${priorTotal} prior (${stats.priorPassed}\\u2713 ${stats.priorFailed}\\u2717)`);\n }\n }\n if (afterTotal > 0) {\n if (stats.afterFailed === 0) {\n parts.push(`${afterTotal} after \\u2713`);\n } else if (stats.afterPassed === 0) {\n parts.push(`${afterTotal} after \\u2717`);\n } else {\n parts.push(`${afterTotal} after (${stats.afterPassed}\\u2713 ${stats.afterFailed}\\u2717)`);\n }\n }\n return parts.length > 0 ? `(${parts.join(\", \")})` : \"\";\n }\n\n // src/dev-panel/utils.ts\n function escapeHtml(str) {\n return str.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n }\n function formatContext(ctx) {\n if (!ctx) return \"\";\n try {\n return JSON.stringify(ctx, null, 2);\n } catch {\n return String(ctx);\n }\n }\n function formatLocation(loc) {\n if (!loc) return \"\";\n const file = loc.file.replace(/^.*\\/src\\//, \"src/\");\n return `${file}:${loc.line}${loc.column ? \":\" + loc.column : \"\"}`;\n }\n function formatTime(ts) {\n const d = new Date(ts);\n return d.toLocaleTimeString(\"en-US\", { hour12: false }) + \".\" + String(d.getMilliseconds()).padStart(3, \"0\");\n }\n function openInEditor(loc) {\n if (!loc) return;\n fetch(`/__open-in-editor?file=${encodeURIComponent(loc.file)}&line=${loc.line}${loc.column ? \"&column=\" + loc.column : \"\"}`).catch(() => {\n window.open(`vscode://file${loc.file}:${loc.line}${loc.column ? \":\" + loc.column : \"\"}`);\n });\n }\n function filterItems(items) {\n if (filter === \"all\") return items;\n if (filter === \"fails\") return items.filter((a) => !a.result);\n if (filter === \"passes\") return items.filter((a) => a.result);\n return items;\n }\n function getGroupStats(items) {\n let passCount2 = 0;\n let failCount2 = 0;\n for (const item of items) {\n if (item.result) passCount2++;\n else failCount2++;\n }\n return { passCount: passCount2, failCount: failCount2 };\n }\n function formatLocationShort(loc) {\n if (!loc) return \"\";\n const file = loc.file.split(\"/\").pop() || loc.file;\n return `${file}:${loc.line}`;\n }\n\n // src/dev-panel/render.ts\n function renderPanelItem(a, groupId) {\n const titleAttr = a.context ? escapeHtml(formatContext(a.context)) : a.location ? escapeHtml(formatLocation(a.location)) : \"\";\n return `\n <div class=\"scenetest-item ${a.result ? \"pass\" : \"fail\"}\"\n onclick=\"if(window.__scenetest_openFullscreenToGroup)window.__scenetest_openFullscreenToGroup(${groupId})\"\n title=\"${titleAttr}\">\n <span class=\"scenetest-icon\">${a.result ? \"\\u2713\" : \"\\u2717\"}</span>\n <div class=\"scenetest-content\">\n <div class=\"scenetest-desc${a.type === \"fail\" && a.result ? \" negated\" : \"\"}\">${escapeHtml(a.description)}</div>\n ${a.location ? `<div class=\"scenetest-location\">${escapeHtml(formatLocation(a.location))}</div>` : \"\"}\n </div>\n </div>\n `;\n }\n function renderPanelGroup(g) {\n const stats = getGroupStats(g.items);\n return `\n <div class=\"scenetest-group${g.collapsed ? \" collapsed\" : \"\"}\" data-group-id=\"${g.id}\">\n <div class=\"scenetest-group-header\" onclick=\"this.parentElement.classList.toggle('collapsed')\">\n <div class=\"scenetest-group-summary\">\n <span class=\"scenetest-group-time\">${formatTime(g.timestamp)}</span>\n <div class=\"scenetest-group-stats\">\n <span class=\"scenetest-group-stat pass\">\\u2713${stats.passCount}</span>\n <span class=\"scenetest-group-stat ${stats.failCount > 0 ? \"fail\" : \"zero\"}\">\\u2717${stats.failCount}</span>\n </div>\n </div>\n <span class=\"scenetest-group-toggle\">\\u25BC</span>\n </div>\n <div class=\"scenetest-group-items\">\n ${g.items.map((a) => renderPanelItem(a, g.id)).join(\"\")}\n </div>\n </div>\n `;\n }\n function renderFullscreenItem(a) {\n const histStats = getHistoryStats(a.description, a._index ?? 0);\n const histSummary = formatHistorySummary(histStats);\n const locJson = a.location ? JSON.stringify(a.location).replace(/\"/g, \"&quot;\") : \"null\";\n return `\n <div class=\"item ${a.result ? \"pass\" : \"fail\"}\">\n <span class=\"icon\">${a.result ? \"\\u2713\" : \"\\u2717\"}</span>\n <div class=\"content\">\n <div class=\"desc${a.type === \"fail\" && a.result ? \" negated\" : \"\"}\">${escapeHtml(a.description)}</div>\n ${a.location ? `<div class=\"location\" onclick=\"window.opener && window.opener.__scenetest_openInEditor && window.opener.__scenetest_openInEditor(${locJson})\">${escapeHtml(formatLocation(a.location))}</div>` : \"\"}\n ${histSummary ? `<div class=\"history\">${histSummary}</div>` : \"\"}\n ${a.context ? `<div class=\"context\">${escapeHtml(formatContext(a.context))}</div>` : \"\"}\n ${a.stack && !a.context ? `<div class=\"stack\">${escapeHtml(a.stack.split(\"\\n\").slice(0, 3).join(\"\\n\"))}</div>` : \"\"}\n </div>\n </div>\n `;\n }\n function renderFullscreenGroup(g) {\n const stats = getGroupStats(g.items);\n return `\n <div class=\"group\" data-group-id=\"${g.id}\">\n <div class=\"group-header\" onclick=\"this.parentElement.classList.toggle('collapsed')\">\n <div class=\"group-info\">\n <span class=\"group-time\">${formatTime(g.timestamp)}</span>\n <div class=\"group-stats\">\n ${stats.passCount > 0 ? `<span class=\"group-stat pass\">\\u2713 ${stats.passCount}</span>` : \"\"}\n ${stats.failCount > 0 ? `<span class=\"group-stat fail\">\\u2717 ${stats.failCount}</span>` : \"\"}\n </div>\n <span style=\"color: #6a6a8a\">${g.items.length} assertion${g.items.length === 1 ? \"\" : \"s\"}</span>\n </div>\n <span class=\"group-toggle\">\\u25BC</span>\n </div>\n <div class=\"group-items\">\n ${g.items.map((a) => renderFullscreenItem(a)).join(\"\")}\n </div>\n </div>\n `;\n }\n function renderLocationRow(group) {\n const passCount2 = group.entries.filter((e) => e.result).length;\n const failCount2 = group.entries.filter((e) => !e.result).length;\n const total = group.entries.length;\n const keyJson = JSON.stringify(group.key).replace(/\"/g, \"&quot;\");\n const recentEntries = group.entries.slice(-10);\n const dots = recentEntries.map((e) => {\n if (e.result) {\n return `<span class=\"status-dot pass\" title=\"${formatTime(e.timestamp)}\"></span>`;\n } else {\n return `<span class=\"status-dot fail\" title=\"${formatTime(e.timestamp)}\"><span class=\"dot-x\">\\u2717</span></span>`;\n }\n }).join(\"\");\n const hasAnyFails = failCount2 > 0;\n const lastFailed = !group.lastResult;\n const statusClass = lastFailed ? \"last-fail\" : hasAnyFails ? \"has-fails\" : \"all-pass\";\n const stateIcon = lastFailed ? '<span class=\"state-icon fail\">\\u2717</span>' : hasAnyFails ? '<span class=\"state-icon warn\">\\u26A0</span>' : '<span class=\"state-icon pass\">\\u2713</span>';\n return `\n <div class=\"location-row ${statusClass}\" data-location-key=\"${keyJson}\">\n ${stateIcon}\n <div class=\"location-main\" onclick=\"window.opener ? window.opener.__scenetest_showSequence && window.opener.__scenetest_showSequence(${keyJson}) : window.__scenetest_showSequence && window.__scenetest_showSequence(${keyJson})\">\n <div class=\"location-info\">\n <span class=\"location-file\">${escapeHtml(formatLocationShort(group.location))}</span>\n <span class=\"location-desc\">${escapeHtml(group.description)}</span>\n </div>\n <div class=\"location-stats\">\n <div class=\"status-dots\">${dots}</div>\n <span class=\"location-count\">${total} run${total === 1 ? \"\" : \"s\"}</span>\n <div class=\"location-summary\">\n ${passCount2 > 0 ? `<span class=\"stat pass\">\\u2713${passCount2}</span>` : \"\"}\n ${failCount2 > 0 ? `<span class=\"stat fail\">\\u2717${failCount2}</span>` : \"\"}\n </div>\n </div>\n </div>\n <div class=\"location-actions\">\n <button class=\"loc-btn\" onclick=\"event.stopPropagation(); window.opener ? window.opener.__scenetest_openInEditor && window.opener.__scenetest_openInEditor(${JSON.stringify(group.location).replace(/\"/g, \"&quot;\")}) : window.__scenetest_openInEditor && window.__scenetest_openInEditor(${JSON.stringify(group.location).replace(/\"/g, \"&quot;\")})\" title=\"Open in editor\">\\u270E</button>\n </div>\n </div>\n `;\n }\n function renderSequenceEntry(entry, location, isFirst = false, isLast = false) {\n return `\n <div class=\"sequence-entry ${entry.result ? \"pass\" : \"fail\"}\">\n <div class=\"timeline-track\">\n <div class=\"timeline-line ${isFirst ? \"first\" : \"\"} ${isLast ? \"last\" : \"\"}\"></div>\n <div class=\"timeline-dot ${entry.result ? \"pass\" : \"fail\"}\">\n ${entry.result ? \"\" : '<span class=\"dot-x\">\\u2717</span>'}\n </div>\n </div>\n <div class=\"content\">\n <div class=\"sequence-time\">${formatTime(entry.timestamp)}</div>\n <div class=\"desc\">${escapeHtml(entry.description)}</div>\n ${entry.context ? `<div class=\"context\">${escapeHtml(formatContext(entry.context))}</div>` : \"\"}\n </div>\n </div>\n `;\n }\n function renderSequenceHeader(group) {\n const locJson = JSON.stringify(group.location).replace(/\"/g, \"&quot;\");\n const passCount2 = group.entries.filter((e) => e.result).length;\n const failCount2 = group.entries.filter((e) => !e.result).length;\n return `\n <div class=\"sequence-header\">\n <div class=\"sequence-location\">\n <span class=\"sequence-file\" onclick=\"window.opener ? window.opener.__scenetest_openInEditor && window.opener.__scenetest_openInEditor(${locJson}) : window.__scenetest_openInEditor && window.__scenetest_openInEditor(${locJson})\">${escapeHtml(formatLocation(group.location))}</span>\n </div>\n <div class=\"sequence-summary\">\n <span class=\"sequence-total\">${group.entries.length} run${group.entries.length === 1 ? \"\" : \"s\"}</span>\n <div class=\"sequence-stats\">\n ${passCount2 > 0 ? `<span class=\"stat pass\">\\u2713 ${passCount2}</span>` : \"\"}\n ${failCount2 > 0 ? `<span class=\"stat fail\">\\u2717 ${failCount2}</span>` : \"\"}\n </div>\n </div>\n </div>\n `;\n }\n\n // src/dev-panel/styles.ts\n var panelStyles = `\n#scenetest-panel {\n position: fixed;\n bottom: 16px;\n right: 16px;\n width: 400px;\n max-height: 450px;\n background: #1a1a2e;\n border: 1px solid #4a4a6a;\n border-radius: 8px;\n font-family: ui-monospace, monospace;\n font-size: 12px;\n color: #e0e0e0;\n z-index: 999999;\n box-shadow: 0 4px 24px rgba(0,0,0,0.4);\n display: flex;\n flex-direction: column;\n}\n#scenetest-panel.collapsed {\n max-height: none;\n width: auto;\n}\n#scenetest-panel.collapsed #scenetest-list,\n#scenetest-panel.collapsed #scenetest-actions {\n display: none;\n}\n#scenetest-header {\n padding: 10px 12px;\n background: #252542;\n border-radius: 8px 8px 0 0;\n display: flex;\n justify-content: space-between;\n align-items: center;\n cursor: pointer;\n user-select: none;\n border-bottom: 1px solid #4a4a6a;\n}\n#scenetest-header:hover {\n background: #2a2a4a;\n}\n#scenetest-title {\n font-weight: 600;\n color: #a0a0ff;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.scenetest-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n border-radius: 50%;\n background: #a0a0ff;\n font-size: 12px;\n filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));\n}\n.scenetest-icon span {\n filter: drop-shadow(0px 0px 4px #ffffff);\n}\n#scenetest-counts {\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.scenetest-count {\n padding: 2px 8px;\n border-radius: 4px;\n font-weight: 500;\n cursor: pointer;\n transition: opacity 0.15s;\n}\n.scenetest-count:hover {\n opacity: 0.8;\n}\n.scenetest-count.pass {\n background: #1a3a1a;\n color: #4ade80;\n}\n.scenetest-count.fail {\n background: #3a1a1a;\n color: #f87171;\n}\n.scenetest-count.active {\n outline: 2px solid currentColor;\n outline-offset: 1px;\n}\n#scenetest-actions {\n display: flex;\n gap: 8px;\n padding: 6px 12px;\n background: #202038;\n border-bottom: 1px solid #3a3a5a;\n flex-wrap: wrap;\n align-items: center;\n}\n.scenetest-btn-group {\n display: flex;\n border: 1px solid #4a4a6a;\n border-radius: 4px;\n overflow: hidden;\n}\n.scenetest-btn-group .scenetest-btn {\n border: none;\n border-radius: 0;\n border-right: 1px solid #4a4a6a;\n}\n.scenetest-btn-group .scenetest-btn:last-child {\n border-right: none;\n}\n.scenetest-btn {\n background: none;\n border: 1px solid #4a4a6a;\n color: #a0a0a0;\n padding: 4px 10px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 11px;\n font-family: inherit;\n transition: all 0.15s;\n}\n.scenetest-btn:hover {\n background: #3a3a5a;\n color: #e0e0e0;\n}\n.scenetest-btn.active {\n background: #4a4a6a;\n color: #fff;\n}\n.scenetest-separator {\n width: 1px;\n height: 16px;\n background: #3a3a5a;\n}\n#scenetest-list {\n overflow-y: auto;\n max-height: 340px;\n padding: 8px 0;\n}\n.scenetest-group {\n margin: 4px 8px;\n border: 1px solid #3a3a5a;\n border-radius: 6px;\n overflow: hidden;\n}\n.scenetest-group-header {\n padding: 6px 10px;\n background: #252542;\n display: flex;\n justify-content: space-between;\n align-items: center;\n cursor: pointer;\n font-size: 11px;\n}\n.scenetest-group-header:hover {\n background: #2a2a4a;\n}\n.scenetest-group-summary {\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.scenetest-group-time {\n color: #6a6a8a;\n}\n.scenetest-group-stats {\n display: flex;\n gap: 6px;\n}\n.scenetest-group-stat {\n font-size: 10px;\n padding: 1px 5px;\n border-radius: 3px;\n}\n.scenetest-group-stat.pass {\n background: #1a3a1a;\n color: #4ade80;\n}\n.scenetest-group-stat.fail {\n background: #3a1a1a;\n color: #f87171;\n}\n.scenetest-group-stat.zero {\n background: #2a2a3a;\n color: #6a6a8a;\n}\n.scenetest-group-items {\n border-top: 1px solid #3a3a5a;\n}\n.scenetest-group.collapsed .scenetest-group-items {\n display: none;\n}\n.scenetest-group-toggle {\n color: #6a6a8a;\n font-size: 10px;\n}\n.scenetest-item {\n padding: 6px 10px;\n border-bottom: 1px solid #2a2a4a;\n display: flex;\n gap: 8px;\n align-items: flex-start;\n cursor: pointer;\n position: relative;\n}\n.scenetest-item:hover {\n background: #252545;\n}\n.scenetest-item:last-child {\n border-bottom: none;\n}\n.scenetest-item.pass .scenetest-icon {\n color: #4ade80;\n}\n.scenetest-item.fail .scenetest-icon {\n color: #f87171;\n}\n.scenetest-icon {\n flex-shrink: 0;\n width: 14px;\n text-align: center;\n}\n.scenetest-content {\n flex: 1;\n min-width: 0;\n}\n.scenetest-desc {\n word-break: break-word;\n}\n.scenetest-item.fail .scenetest-desc {\n color: #f87171;\n}\n.scenetest-desc.negated {\n text-decoration: line-through;\n opacity: 0.7;\n}\n.scenetest-location {\n font-size: 9px;\n color: #6a6a8a;\n margin-top: 2px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.scenetest-location:hover {\n color: #a0a0ff;\n text-decoration: underline;\n}\n.scenetest-context {\n font-size: 10px;\n color: #8a8aaa;\n margin-top: 3px;\n padding: 4px 6px;\n background: #12122a;\n border-radius: 3px;\n font-family: ui-monospace, monospace;\n max-height: 60px;\n overflow: auto;\n white-space: pre-wrap;\n word-break: break-all;\n}\n.scenetest-time {\n color: #6a6a8a;\n flex-shrink: 0;\n font-size: 10px;\n}\n#scenetest-empty {\n padding: 20px;\n text-align: center;\n color: #6a6a8a;\n}\n.scenetest-ungrouped {\n padding: 4px 8px;\n}\n.scenetest-history {\n font-size: 9px;\n color: #8a8aaa;\n margin-top: 2px;\n font-style: italic;\n}\n`;\n var fullscreenStyles = `\n* { box-sizing: border-box; }\nbody {\n margin: 0;\n padding: 0;\n background: #0f0f1a;\n color: #e0e0e0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;\n font-size: 13px;\n}\n#header {\n position: sticky;\n top: 0;\n background: #1a1a2e;\n border-bottom: 1px solid #4a4a6a;\n padding: 16px 24px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n z-index: 100;\n flex-wrap: wrap;\n gap: 12px;\n}\n#title {\n font-size: 18px;\n font-weight: 600;\n color: #a0a0ff;\n display: flex;\n align-items: center;\n gap: 10px;\n}\n#title .icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n border-radius: 50%;\n background: #a0a0ff;\n font-size: 18px;\n filter: drop-shadow(0 2px 6px rgba(0,0,0,0.3));\n}\n#title .icon span {\n filter: drop-shadow(0px 0px 5px #ffffff);\n}\n#controls {\n display: flex;\n gap: 12px;\n align-items: center;\n flex-wrap: wrap;\n}\n#counts {\n display: flex;\n gap: 12px;\n align-items: center;\n}\n.count {\n padding: 4px 12px;\n border-radius: 6px;\n font-weight: 600;\n font-size: 14px;\n}\n.count.pass {\n background: #1a3a1a;\n color: #4ade80;\n}\n.count.fail {\n background: #3a1a1a;\n color: #f87171;\n}\n.btn {\n background: #252542;\n border: 1px solid #4a4a6a;\n color: #a0a0a0;\n padding: 8px 16px;\n border-radius: 6px;\n cursor: pointer;\n font-size: 13px;\n font-family: inherit;\n transition: all 0.15s;\n}\n.btn:hover {\n background: #3a3a5a;\n color: #e0e0e0;\n}\n.btn.active {\n background: #4a4a6a;\n color: #fff;\n}\n#filters {\n display: flex;\n gap: 0;\n}\n.btn-group {\n display: flex;\n border: 1px solid #4a4a6a;\n border-radius: 6px;\n overflow: hidden;\n}\n.btn-group .btn {\n border: none;\n border-radius: 0;\n border-right: 1px solid #4a4a6a;\n}\n.btn-group .btn:last-child {\n border-right: none;\n}\n.separator {\n width: 1px;\n height: 24px;\n background: #4a4a6a;\n}\n#list {\n padding: 16px;\n}\n.ungrouped-list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.group {\n margin-bottom: 16px;\n border: 1px solid #3a3a5a;\n border-radius: 8px;\n overflow: hidden;\n}\n.group-header {\n padding: 12px 16px;\n background: #1a1a2e;\n display: flex;\n justify-content: space-between;\n align-items: center;\n cursor: pointer;\n}\n.group-header:hover {\n background: #252542;\n}\n.group-info {\n display: flex;\n gap: 16px;\n align-items: center;\n}\n.group-time {\n color: #a0a0ff;\n font-weight: 500;\n}\n.group-stats {\n display: flex;\n gap: 8px;\n}\n.group-stat {\n padding: 2px 8px;\n border-radius: 4px;\n font-size: 12px;\n}\n.group-stat.pass {\n background: #1a3a1a;\n color: #4ade80;\n}\n.group-stat.fail {\n background: #3a1a1a;\n color: #f87171;\n}\n.group-toggle {\n color: #6a6a8a;\n font-size: 12px;\n}\n.group-items {\n border-top: 1px solid #3a3a5a;\n}\n.group.collapsed .group-items {\n display: none;\n}\n.item {\n padding: 10px 16px;\n background: #12121f;\n display: flex;\n gap: 12px;\n align-items: flex-start;\n border-bottom: 1px solid #2a2a4a;\n}\n.item:last-child {\n border-bottom: none;\n}\n.item.fail {\n background: #1a1212;\n}\n.icon {\n font-size: 14px;\n width: 18px;\n text-align: center;\n flex-shrink: 0;\n}\n.item.pass .icon { color: #4ade80; }\n.item.fail .icon { color: #f87171; }\n.content {\n flex: 1;\n}\n.desc {\n margin-bottom: 4px;\n word-break: break-word;\n}\n.item.fail .desc {\n color: #f87171;\n}\n.desc.negated {\n text-decoration: line-through;\n opacity: 0.7;\n}\n.location {\n font-size: 11px;\n color: #6a6a8a;\n margin-top: 4px;\n cursor: pointer;\n}\n.location:hover {\n color: #a0a0ff;\n text-decoration: underline;\n}\n.context {\n margin-top: 8px;\n padding: 8px;\n background: #0a0a12;\n border-radius: 4px;\n font-size: 11px;\n color: #a0a0c0;\n white-space: pre-wrap;\n word-break: break-all;\n max-height: 100px;\n overflow: auto;\n}\n.meta {\n font-size: 11px;\n color: #6a6a8a;\n}\n.stack {\n margin-top: 8px;\n padding: 8px;\n background: #0a0a12;\n border-radius: 4px;\n font-size: 11px;\n color: #8a8a9a;\n white-space: pre-wrap;\n word-break: break-all;\n}\n.history {\n font-size: 11px;\n color: #8a8aaa;\n margin-top: 4px;\n font-style: italic;\n}\n#empty {\n text-align: center;\n padding: 60px 20px;\n color: #6a6a8a;\n}\n#empty-icon {\n font-size: 48px;\n margin-bottom: 16px;\n}\n.group.highlighted {\n box-shadow: 0 0 0 3px #a0a0ff, 0 0 20px rgba(160, 160, 255, 0.4);\n}\n\n/* View mode toggle */\n#view-modes {\n display: flex;\n gap: 0;\n}\n\n/* Location view styles */\n.location-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 16px;\n background: #12121f;\n border: 1px solid #3a3a5a;\n border-radius: 8px;\n margin-bottom: 8px;\n cursor: pointer;\n transition: all 0.15s;\n}\n.location-row:hover {\n background: #1a1a2e;\n border-color: #4a4a6a;\n}\n.location-row.all-pass {\n border-left: 3px solid #4ade80;\n}\n.location-row.has-fails {\n border-left: 3px solid #f59e0b;\n}\n.location-row.last-fail {\n border-left: 3px solid #f87171;\n background: #1a1212;\n}\n.location-main {\n flex: 1;\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 16px;\n}\n.location-info {\n display: flex;\n flex-direction: column;\n gap: 4px;\n min-width: 0;\n}\n.location-file {\n font-size: 12px;\n color: #a0a0ff;\n font-weight: 500;\n}\n.location-desc {\n font-size: 13px;\n color: #e0e0e0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.location-stats {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-shrink: 0;\n}\n/* Current state icon - prominent indicator on the left */\n.state-icon {\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n font-weight: bold;\n border-radius: 50%;\n flex-shrink: 0;\n margin-right: 12px;\n}\n.state-icon.pass {\n background: #1a3a1a;\n color: #4ade80;\n}\n.state-icon.warn {\n background: #3a2a0a;\n color: #f59e0b;\n}\n.state-icon.fail {\n background: #3a1a1a;\n color: #f87171;\n}\n\n.status-dots {\n display: flex;\n gap: 4px;\n align-items: center;\n}\n.status-dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n transition: transform 0.15s;\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n.status-dot.pass {\n background: #4ade80;\n}\n.status-dot.fail {\n background: #f87171;\n}\n/* Accessibility: X marker on failure dots for colorblind users */\n.status-dot .dot-x {\n position: absolute;\n color: white;\n font-size: 9px;\n font-weight: bold;\n line-height: 1;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n text-shadow: 0 0 2px rgba(0,0,0,0.5);\n}\n.status-dot:last-child {\n animation: pulse-dot 0.5s ease-out;\n}\n@keyframes pulse-dot {\n 0% { transform: scale(1.5); }\n 100% { transform: scale(1); }\n}\n.location-count {\n font-size: 11px;\n color: #6a6a8a;\n}\n.location-summary {\n display: flex;\n gap: 6px;\n}\n.location-summary .stat {\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 11px;\n}\n.location-summary .stat.pass {\n background: #1a3a1a;\n color: #4ade80;\n}\n.location-summary .stat.fail {\n background: #3a1a1a;\n color: #f87171;\n}\n.location-actions {\n margin-left: 12px;\n}\n.loc-btn {\n background: #252542;\n border: 1px solid #4a4a6a;\n color: #a0a0a0;\n padding: 4px 8px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 12px;\n}\n.loc-btn:hover {\n background: #3a3a5a;\n color: #e0e0e0;\n}\n\n/* Sequence view styles */\n.sequence-header {\n background: #1a1a2e;\n border: 1px solid #4a4a6a;\n border-radius: 8px;\n padding: 16px;\n margin-bottom: 8px;\n}\n.sequence-location {\n margin-bottom: 8px;\n}\n.sequence-file {\n color: #a0a0ff;\n font-size: 14px;\n cursor: pointer;\n}\n.sequence-file:hover {\n text-decoration: underline;\n}\n.sequence-summary {\n display: flex;\n align-items: center;\n gap: 16px;\n}\n.sequence-total {\n color: #6a6a8a;\n font-size: 12px;\n}\n.sequence-stats {\n display: flex;\n gap: 8px;\n}\n.sequence-stats .stat {\n padding: 2px 8px;\n border-radius: 4px;\n font-size: 12px;\n}\n.sequence-stats .stat.pass {\n background: #1a3a1a;\n color: #4ade80;\n}\n.sequence-stats .stat.fail {\n background: #3a1a1a;\n color: #f87171;\n}\n\n/* Direction hint */\n.sequence-direction-hint {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 16px;\n margin-bottom: 8px;\n color: #6a6a8a;\n font-size: 11px;\n background: #0f0f1a;\n border-radius: 4px;\n}\n.direction-arrow {\n color: #a0a0ff;\n font-size: 14px;\n}\n\n/* Timeline track for sequence entries */\n.sequence-list {\n position: relative;\n padding-left: 8px;\n}\n.sequence-entry {\n padding: 12px 16px 12px 0;\n background: #12121f;\n display: flex;\n gap: 0;\n align-items: stretch;\n border: 1px solid #3a3a5a;\n border-radius: 6px;\n margin-bottom: 0;\n margin-left: 20px;\n position: relative;\n}\n.sequence-entry.fail {\n background: #1a1212;\n border-color: #4a2a2a;\n}\n\n/* Timeline track with connecting line */\n.timeline-track {\n width: 40px;\n display: flex;\n flex-direction: column;\n align-items: center;\n position: relative;\n flex-shrink: 0;\n}\n.timeline-line {\n position: absolute;\n left: 50%;\n transform: translateX(-50%);\n width: 2px;\n background: #3a3a5a;\n top: -8px;\n bottom: -8px;\n}\n.timeline-line.first {\n top: 50%;\n}\n.timeline-line.last {\n bottom: 50%;\n}\n.timeline-line.first.last {\n display: none;\n}\n.timeline-dot {\n width: 14px;\n height: 14px;\n border-radius: 50%;\n position: relative;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-top: 4px;\n}\n.timeline-dot.pass {\n background: #4ade80;\n box-shadow: 0 0 0 3px #1a3a1a;\n}\n.timeline-dot.fail {\n background: #f87171;\n box-shadow: 0 0 0 3px #3a1a1a;\n}\n.timeline-dot .dot-x {\n color: white;\n font-size: 10px;\n font-weight: bold;\n line-height: 1;\n text-shadow: 0 0 2px rgba(0,0,0,0.5);\n}\n\n.sequence-entry .content {\n flex: 1;\n padding-left: 8px;\n}\n.sequence-time {\n font-size: 11px;\n color: #6a6a8a;\n margin-bottom: 4px;\n}\n\n/* End label */\n.sequence-end-label {\n margin-left: 20px;\n padding: 8px 16px 8px 40px;\n color: #6a6a8a;\n font-size: 11px;\n position: relative;\n}\n.sequence-end-label::before {\n content: '';\n position: absolute;\n left: 28px;\n top: 0;\n width: 2px;\n height: 8px;\n background: #3a3a5a;\n}\n\n.back-btn {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 16px;\n color: #a0a0ff;\n cursor: pointer;\n font-size: 13px;\n}\n.back-btn:hover {\n text-decoration: underline;\n}\n`;\n\n // src/dev-panel/fullscreen.ts\n function getFullscreenHTML() {\n return `\n <!DOCTYPE html>\n <html>\n <head>\n <title>scenetest - Inline Assertions</title>\n <style>${fullscreenStyles}</style>\n </head>\n <body>\n <div id=\"header\">\n <span id=\"title\"><span class=\"icon\"><span>\\u{1F3AC}</span></span>scenetest</span>\n <div id=\"controls\">\n <div id=\"counts\">\n <span class=\"count pass\" id=\"pass-count\">\\u2713 0</span>\n <span class=\"count fail\" id=\"fail-count\">\\u2717 0</span>\n </div>\n <div id=\"view-modes\" class=\"btn-group\">\n <button class=\"btn active\" id=\"view-grouped\" title=\"Group by time\">Timeline</button>\n <button class=\"btn\" id=\"view-byLocation\" title=\"Group by code location\">By Location</button>\n </div>\n <div id=\"filters\" class=\"btn-group\">\n <button class=\"btn active\" id=\"filter-all\">All</button>\n <button class=\"btn\" id=\"filter-fails\">Errors</button>\n <button class=\"btn\" id=\"filter-passes\">Passes</button>\n </div>\n <span class=\"separator\"></span>\n <button class=\"btn\" id=\"scenetest-clear-full\">Clear</button>\n </div>\n </div>\n <div id=\"list\">\n <div id=\"empty\">\n <div id=\"empty-icon\">\\u{1F3AC}</div>\n <div>Interact with your app to see inline assertions appear here...</div>\n </div>\n </div>\n </body>\n </html>\n `;\n }\n function setFullscreenFilter(newFilter) {\n setFilter(newFilter);\n if (panel) {\n panel.querySelector(\"#scenetest-filter-all\")?.classList.toggle(\"active\", filter === \"all\");\n panel.querySelector(\"#scenetest-filter-fails\")?.classList.toggle(\"active\", filter === \"fails\");\n panel.querySelector(\"#scenetest-pass\")?.classList.toggle(\"active\", filter === \"passes\");\n panel.querySelector(\"#scenetest-fail\")?.classList.toggle(\"active\", filter === \"fails\");\n }\n updatePanel();\n updateFullscreenWindow();\n }\n function setFullscreenViewMode(newMode) {\n setViewMode(newMode);\n updateFullscreenWindow();\n }\n function showSequence(locationKey) {\n setSequenceLocation(locationKey);\n updateFullscreenWindow();\n }\n var scrollToGroupId = null;\n function openFullscreen(groupId) {\n scrollToGroupId = groupId ?? null;\n if (fullscreenWindow && !fullscreenWindow.closed) {\n fullscreenWindow.focus();\n if (scrollToGroupId !== null) {\n scrollToGroup(scrollToGroupId);\n }\n return;\n }\n const win = window.open(\"\", \"scenetest-fullscreen\", \"width=900,height=700\");\n if (!win) {\n alert(\"Please allow popups for this site to use fullscreen mode.\");\n return;\n }\n setFullscreenWindow(win);\n win.document.write(getFullscreenHTML());\n win.document.close();\n const doc = win.document;\n doc.getElementById(\"scenetest-clear-full\")?.addEventListener(\"click\", () => {\n clearAll();\n updatePanel();\n updateFullscreenWindow();\n });\n doc.getElementById(\"filter-all\")?.addEventListener(\"click\", () => {\n setFullscreenFilter(\"all\");\n });\n doc.getElementById(\"filter-fails\")?.addEventListener(\"click\", () => {\n setFullscreenFilter(\"fails\");\n });\n doc.getElementById(\"filter-passes\")?.addEventListener(\"click\", () => {\n setFullscreenFilter(\"passes\");\n });\n doc.getElementById(\"view-grouped\")?.addEventListener(\"click\", () => {\n setFullscreenViewMode(\"grouped\");\n });\n doc.getElementById(\"view-byLocation\")?.addEventListener(\"click\", () => {\n setFullscreenViewMode(\"byLocation\");\n });\n updateFullscreenWindow();\n if (scrollToGroupId !== null) {\n scrollToGroup(scrollToGroupId);\n scrollToGroupId = null;\n }\n }\n function scrollToGroup(groupId) {\n if (!fullscreenWindow || fullscreenWindow.closed) return;\n const doc = fullscreenWindow.document;\n const groupEl = doc.querySelector(`[data-group-id=\"${groupId}\"]`);\n if (groupEl) {\n groupEl.classList.remove(\"collapsed\");\n groupEl.scrollIntoView({ behavior: \"auto\", block: \"start\" });\n const prevHighlighted = doc.querySelector(\".group.highlighted\");\n if (prevHighlighted) prevHighlighted.classList.remove(\"highlighted\");\n groupEl.classList.add(\"highlighted\");\n }\n }\n function openFullscreenToGroup(groupId) {\n openFullscreen(groupId);\n }\n function updateFullscreenWindow() {\n if (!fullscreenWindow || fullscreenWindow.closed) return;\n const doc = fullscreenWindow.document;\n const passCountEl = doc.getElementById(\"pass-count\");\n const failCountEl = doc.getElementById(\"fail-count\");\n if (passCountEl) passCountEl.textContent = `\\u2713 ${passCount}`;\n if (failCountEl) failCountEl.textContent = `\\u2717 ${failCount}`;\n doc.getElementById(\"filter-all\")?.classList.toggle(\"active\", filter === \"all\");\n doc.getElementById(\"filter-fails\")?.classList.toggle(\"active\", filter === \"fails\");\n doc.getElementById(\"filter-passes\")?.classList.toggle(\"active\", filter === \"passes\");\n const isSequenceView = viewMode === \"sequence\";\n doc.getElementById(\"view-grouped\")?.classList.toggle(\"active\", viewMode === \"grouped\");\n doc.getElementById(\"view-byLocation\")?.classList.toggle(\"active\", viewMode === \"byLocation\" || isSequenceView);\n const listEl2 = doc.getElementById(\"list\");\n if (!listEl2) return;\n if (viewMode === \"sequence\" && sequenceLocationKey) {\n renderSequenceView(doc, listEl2);\n return;\n }\n if (viewMode === \"byLocation\") {\n renderByLocationView(doc, listEl2);\n return;\n }\n renderGroupedView(doc, listEl2);\n }\n function renderGroupedView(doc, listEl2) {\n const scrollTop = doc.documentElement.scrollTop || doc.body.scrollTop;\n const isScrolled = scrollTop > 50;\n let anchorGroupId = null;\n let anchorOffset = 0;\n if (isScrolled) {\n const groupEls = listEl2.querySelectorAll(\"[data-group-id]\");\n for (const el of groupEls) {\n const rect = el.getBoundingClientRect();\n if (rect.top >= -rect.height / 2) {\n anchorGroupId = parseInt(el.getAttribute(\"data-group-id\") || \"\", 10);\n anchorOffset = rect.top;\n break;\n }\n }\n }\n const filteredGroups = groups.map((g) => ({\n ...g,\n items: filterItems(g.items)\n })).filter((g) => g.items.length > 0);\n if (filteredGroups.length === 0) {\n const icon = filter === \"fails\" ? \"\\u2713\" : \"\\u{1F3AC}\";\n const message = filter === \"fails\" ? \"No errors! All assertions passed.\" : \"Interact with your app to see inline assertions appear here...\";\n listEl2.innerHTML = `\n <div id=\"empty\">\n <div id=\"empty-icon\">${icon}</div>\n <div>${message}</div>\n </div>\n `;\n return;\n }\n listEl2.innerHTML = filteredGroups.map((g) => renderFullscreenGroup(g)).reverse().join(\"\");\n if (anchorGroupId !== null) {\n const anchorEl = listEl2.querySelector(`[data-group-id=\"${anchorGroupId}\"]`);\n if (anchorEl) {\n const newRect = anchorEl.getBoundingClientRect();\n const scrollAdjustment = newRect.top - anchorOffset;\n doc.documentElement.scrollTop += scrollAdjustment;\n doc.body.scrollTop += scrollAdjustment;\n }\n }\n }\n function renderByLocationView(_doc, listEl2) {\n const locations = Array.from(locationGroups.values()).sort((a, b) => b.lastTimestamp - a.lastTimestamp);\n let filteredLocations = locations;\n if (filter === \"fails\") {\n filteredLocations = locations.filter((loc) => loc.entries.some((e) => !e.result));\n } else if (filter === \"passes\") {\n filteredLocations = locations.filter((loc) => loc.entries.some((e) => e.result));\n }\n if (filteredLocations.length === 0) {\n const icon = filter === \"fails\" ? \"\\u2713\" : \"\\u{1F3AC}\";\n const message = filter === \"fails\" ? \"No errors! All assertions passed.\" : \"Interact with your app to see inline assertions appear here...\";\n listEl2.innerHTML = `\n <div id=\"empty\">\n <div id=\"empty-icon\">${icon}</div>\n <div>${message}</div>\n </div>\n `;\n return;\n }\n listEl2.innerHTML = `\n <div class=\"location-list\">\n ${filteredLocations.map((loc) => renderLocationRow(loc)).join(\"\")}\n </div>\n `;\n }\n function renderSequenceView(_doc, listEl2) {\n const group = locationGroups.get(sequenceLocationKey);\n if (!group) {\n setViewMode(\"byLocation\");\n renderByLocationView(_doc, listEl2);\n return;\n }\n let entries = group.entries;\n if (filter === \"fails\") {\n entries = entries.filter((e) => !e.result);\n } else if (filter === \"passes\") {\n entries = entries.filter((e) => e.result);\n }\n const backHandler = `window.opener ? window.opener.__scenetest_setViewMode && window.opener.__scenetest_setViewMode('byLocation') : window.__scenetest_setViewMode && window.__scenetest_setViewMode('byLocation')`;\n const reversedEntries = entries.slice().reverse();\n const entryCount = reversedEntries.length;\n listEl2.innerHTML = `\n <div class=\"back-btn\" onclick=\"${backHandler}\">\\u2190 Back to all locations</div>\n ${renderSequenceHeader(group)}\n <div class=\"sequence-direction-hint\">\n <span class=\"direction-arrow\">\\u2191</span> Most recent at top\n </div>\n <div class=\"sequence-list\">\n ${reversedEntries.map((entry, i) => {\n const isFirst = i === 0;\n const isLast = i === entryCount - 1;\n return renderSequenceEntry(entry, group.location, isFirst, isLast);\n }).join(\"\")}\n </div>\n ${entryCount > 1 ? '<div class=\"sequence-end-label\">First event</div>' : \"\"}\n `;\n }\n\n // src/dev-panel/panel.ts\n function getPanelHTML() {\n return `\n <style>${panelStyles}</style>\n <div id=\"scenetest-header\">\n <span id=\"scenetest-title\"><span class=\"scenetest-icon\"><span>\\u{1F3AC}</span></span>scenetest</span>\n <span id=\"scenetest-counts\">\n <span class=\"scenetest-count pass\" id=\"scenetest-pass\" title=\"Click to filter passes\">\\u2713 0</span>\n <span class=\"scenetest-count fail\" id=\"scenetest-fail\" title=\"Click to filter failures\">\\u2717 0</span>\n </span>\n </div>\n <div id=\"scenetest-actions\">\n <div class=\"scenetest-btn-group\">\n <button class=\"scenetest-btn active\" id=\"scenetest-filter-all\">all</button>\n <button class=\"scenetest-btn\" id=\"scenetest-filter-fails\">errors</button>\n </div>\n <span class=\"scenetest-separator\"></span>\n <button class=\"scenetest-btn\" id=\"scenetest-fullscreen\">fullscreen</button>\n <button class=\"scenetest-btn\" id=\"scenetest-clear\">clear</button>\n </div>\n <div id=\"scenetest-list\">\n <div id=\"scenetest-empty\">Click around to see inline assertions...</div>\n </div>\n `;\n }\n function handleSetFilter(newFilter) {\n setFilter(newFilter);\n panel?.querySelector(\"#scenetest-filter-all\")?.classList.toggle(\"active\", filter === \"all\");\n panel?.querySelector(\"#scenetest-filter-fails\")?.classList.toggle(\"active\", filter === \"fails\");\n panel?.querySelector(\"#scenetest-pass\")?.classList.toggle(\"active\", filter === \"passes\");\n panel?.querySelector(\"#scenetest-fail\")?.classList.toggle(\"active\", filter === \"fails\");\n updatePanel();\n updateFullscreenWindow();\n }\n function createPanel() {\n const panelEl = document.createElement(\"div\");\n panelEl.id = \"scenetest-panel\";\n panelEl.innerHTML = getPanelHTML();\n document.body.appendChild(panelEl);\n setPanel(panelEl);\n setListEl(panelEl.querySelector(\"#scenetest-list\"));\n panelEl.querySelector(\"#scenetest-header\")?.addEventListener(\"click\", (e) => {\n const target = e.target;\n if (target.classList.contains(\"scenetest-count\")) return;\n panelEl.classList.toggle(\"collapsed\");\n });\n panelEl.querySelector(\"#scenetest-pass\")?.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n handleSetFilter(filter === \"passes\" ? \"all\" : \"passes\");\n });\n panelEl.querySelector(\"#scenetest-fail\")?.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n handleSetFilter(filter === \"fails\" ? \"all\" : \"fails\");\n });\n panelEl.querySelector(\"#scenetest-filter-all\")?.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n handleSetFilter(\"all\");\n });\n panelEl.querySelector(\"#scenetest-filter-fails\")?.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n handleSetFilter(\"fails\");\n });\n panelEl.querySelector(\"#scenetest-clear\")?.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n clearAll();\n updatePanel();\n updateFullscreenWindow();\n });\n panelEl.querySelector(\"#scenetest-fullscreen\")?.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n openFullscreen();\n });\n }\n function updatePanel() {\n if (!panel || !listEl) return;\n const passEl = panel.querySelector(\"#scenetest-pass\");\n const failEl = panel.querySelector(\"#scenetest-fail\");\n if (passEl) passEl.textContent = `\\u2713 ${passCount}`;\n if (failEl) failEl.textContent = `\\u2717 ${failCount}`;\n const filteredGroups = groups.map((g) => ({\n ...g,\n items: filterItems(g.items)\n })).filter((g) => g.items.length > 0);\n if (filteredGroups.length === 0) {\n const message = filter === \"fails\" ? \"No errors! All assertions passed.\" : \"Click around to see inline assertions...\";\n listEl.innerHTML = `<div id=\"scenetest-empty\">${message}</div>`;\n return;\n }\n listEl.innerHTML = filteredGroups.map((g) => renderPanelGroup(g)).reverse().join(\"\");\n }\n\n // src/dev-panel/index.ts\n if (!window.__scenetest_panel) {\n let addToGroup = function(result) {\n const now = Date.now();\n if (pendingGroup && now - pendingGroup.timestamp < GROUP_THRESHOLD_MS) {\n pendingGroup.items.push(result);\n } else {\n const newGroup = {\n id: groups.length,\n timestamp: now,\n items: [result],\n collapsed: collapsedMode\n // Use collapsedMode to determine initial state\n };\n setPendingGroup(newGroup);\n groups.push(newGroup);\n }\n if (groupTimeout) clearTimeout(groupTimeout);\n setGroupTimeout(\n setTimeout(() => {\n setPendingGroup(null);\n updatePanel();\n updateFullscreenWindow();\n }, GROUP_THRESHOLD_MS)\n );\n };\n addToGroup2 = addToGroup;\n window.__scenetest_panel = true;\n window.__scenetest_openInEditor = openInEditor;\n window.__scenetest_openFullscreenToGroup = openFullscreenToGroup;\n window.__scenetest_showSequence = showSequence;\n window.__scenetest_setViewMode = (mode) => {\n setViewMode(mode);\n updateFullscreenWindow();\n };\n const existingReport = window.__scenetest_report;\n window.__scenetest_report = function(result) {\n if (existingReport) {\n try {\n existingReport(result);\n } catch {\n }\n }\n const index = assertions.length;\n result._index = index;\n assertions.push(result);\n trackAssertion(result, index);\n trackLocationGroup(result, index);\n if (result.result) {\n incrementPassCount();\n } else {\n incrementFailCount();\n }\n addToGroup(result);\n if (!panel && document.body) {\n createPanel();\n }\n updatePanel();\n updateFullscreenWindow();\n const icon = result.result ? \"\\u2713\" : \"\\u2717\";\n const style = result.result ? \"color: #4ade80\" : \"color: #f87171\";\n console.log(`%c${icon} [scenetest] ${result.description}`, style);\n };\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", createPanel);\n } else {\n createPanel();\n }\n }\n var addToGroup2;\n})();\n";
9
- //# sourceMappingURL=dev-panel.generated.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dev-panel.generated.d.ts","sourceRoot":"","sources":["../src/dev-panel.generated.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,cAAc,wx9CAAwx9C,CAAC"}
@@ -1,9 +0,0 @@
1
- /**
2
- * Auto-generated dev panel script
3
- * DO NOT EDIT - this file is generated by scripts/bundle-dev-panel.mjs
4
- *
5
- * To modify the dev panel, edit files in src/dev-panel/ and run:
6
- * pnpm run build:dev-panel
7
- */
8
- export const devPanelScript = "\"use strict\";\n(() => {\n // src/dev-panel/state.ts\n var assertions = [];\n var groups = [];\n var assertionHistory = /* @__PURE__ */ new Map();\n var locationGroups = /* @__PURE__ */ new Map();\n var passCount = 0;\n var failCount = 0;\n function incrementPassCount() {\n passCount++;\n }\n function incrementFailCount() {\n failCount++;\n }\n var panel = null;\n var listEl = null;\n var fullscreenWindow = null;\n var filter = \"all\";\n var collapsedMode = true;\n var viewMode = \"grouped\";\n var sequenceLocationKey = null;\n function setPanel(el) {\n panel = el;\n }\n function setListEl(el) {\n listEl = el;\n }\n function setFullscreenWindow(win) {\n fullscreenWindow = win;\n }\n function setFilter(newFilter) {\n filter = newFilter;\n }\n function setViewMode(mode) {\n viewMode = mode;\n if (mode !== \"sequence\") {\n sequenceLocationKey = null;\n }\n }\n function setSequenceLocation(key) {\n sequenceLocationKey = key;\n if (key !== null) {\n viewMode = \"sequence\";\n }\n }\n function getLocationKey(result) {\n if (!result.location) return null;\n return `${result.location.file}:${result.location.line}`;\n }\n function trackLocationGroup(result, index) {\n const key = getLocationKey(result);\n if (!key || !result.location) return;\n let group = locationGroups.get(key);\n if (!group) {\n group = {\n key,\n location: result.location,\n description: result.description,\n entries: [],\n lastResult: result.result,\n lastTimestamp: result.timestamp\n };\n locationGroups.set(key, group);\n }\n group.description = result.description;\n group.lastResult = result.result;\n group.lastTimestamp = result.timestamp;\n group.entries.push({\n result: result.result,\n timestamp: result.timestamp,\n index,\n description: result.description,\n context: result.context\n });\n }\n var GROUP_THRESHOLD_MS = 50;\n var pendingGroup = null;\n var groupTimeout = null;\n function setPendingGroup(group) {\n pendingGroup = group;\n }\n function setGroupTimeout(timeout) {\n groupTimeout = timeout;\n }\n function clearAll() {\n assertions.length = 0;\n groups.length = 0;\n assertionHistory.clear();\n locationGroups.clear();\n passCount = 0;\n failCount = 0;\n pendingGroup = null;\n sequenceLocationKey = null;\n if (groupTimeout) {\n clearTimeout(groupTimeout);\n groupTimeout = null;\n }\n }\n\n // src/dev-panel/history.ts\n function trackAssertion(result, index) {\n const key = result.description;\n if (!assertionHistory.has(key)) {\n assertionHistory.set(key, []);\n }\n const history = assertionHistory.get(key);\n history.push({ result: result.result, timestamp: result.timestamp, index });\n return history;\n }\n function getHistoryStats(description, currentIndex) {\n const history = assertionHistory.get(description);\n if (!history || history.length <= 1) return null;\n let priorPassed = 0;\n let priorFailed = 0;\n let afterPassed = 0;\n let afterFailed = 0;\n for (const entry of history) {\n if (entry.index < currentIndex) {\n if (entry.result) priorPassed++;\n else priorFailed++;\n } else if (entry.index > currentIndex) {\n if (entry.result) afterPassed++;\n else afterFailed++;\n }\n }\n const total = history.length - 1;\n if (total === 0) return null;\n return { priorPassed, priorFailed, afterPassed, afterFailed, total };\n }\n function formatHistorySummary(stats) {\n if (!stats) return \"\";\n const parts = [];\n const priorTotal = stats.priorPassed + stats.priorFailed;\n const afterTotal = stats.afterPassed + stats.afterFailed;\n if (priorTotal > 0) {\n if (stats.priorFailed === 0) {\n parts.push(`${priorTotal} prior \\u2713`);\n } else if (stats.priorPassed === 0) {\n parts.push(`${priorTotal} prior \\u2717`);\n } else {\n parts.push(`${priorTotal} prior (${stats.priorPassed}\\u2713 ${stats.priorFailed}\\u2717)`);\n }\n }\n if (afterTotal > 0) {\n if (stats.afterFailed === 0) {\n parts.push(`${afterTotal} after \\u2713`);\n } else if (stats.afterPassed === 0) {\n parts.push(`${afterTotal} after \\u2717`);\n } else {\n parts.push(`${afterTotal} after (${stats.afterPassed}\\u2713 ${stats.afterFailed}\\u2717)`);\n }\n }\n return parts.length > 0 ? `(${parts.join(\", \")})` : \"\";\n }\n\n // src/dev-panel/utils.ts\n function escapeHtml(str) {\n return str.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n }\n function formatContext(ctx) {\n if (!ctx) return \"\";\n try {\n return JSON.stringify(ctx, null, 2);\n } catch {\n return String(ctx);\n }\n }\n function formatLocation(loc) {\n if (!loc) return \"\";\n const file = loc.file.replace(/^.*\\/src\\//, \"src/\");\n return `${file}:${loc.line}${loc.column ? \":\" + loc.column : \"\"}`;\n }\n function formatTime(ts) {\n const d = new Date(ts);\n return d.toLocaleTimeString(\"en-US\", { hour12: false }) + \".\" + String(d.getMilliseconds()).padStart(3, \"0\");\n }\n function openInEditor(loc) {\n if (!loc) return;\n fetch(`/__open-in-editor?file=${encodeURIComponent(loc.file)}&line=${loc.line}${loc.column ? \"&column=\" + loc.column : \"\"}`).catch(() => {\n window.open(`vscode://file${loc.file}:${loc.line}${loc.column ? \":\" + loc.column : \"\"}`);\n });\n }\n function filterItems(items) {\n if (filter === \"all\") return items;\n if (filter === \"fails\") return items.filter((a) => !a.result);\n if (filter === \"passes\") return items.filter((a) => a.result);\n return items;\n }\n function getGroupStats(items) {\n let passCount2 = 0;\n let failCount2 = 0;\n for (const item of items) {\n if (item.result) passCount2++;\n else failCount2++;\n }\n return { passCount: passCount2, failCount: failCount2 };\n }\n function formatLocationShort(loc) {\n if (!loc) return \"\";\n const file = loc.file.split(\"/\").pop() || loc.file;\n return `${file}:${loc.line}`;\n }\n\n // src/dev-panel/render.ts\n function renderPanelItem(a, groupId) {\n const titleAttr = a.context ? escapeHtml(formatContext(a.context)) : a.location ? escapeHtml(formatLocation(a.location)) : \"\";\n return `\n <div class=\"scenetest-item ${a.result ? \"pass\" : \"fail\"}\"\n onclick=\"if(window.__scenetest_openFullscreenToGroup)window.__scenetest_openFullscreenToGroup(${groupId})\"\n title=\"${titleAttr}\">\n <span class=\"scenetest-icon\">${a.result ? \"\\u2713\" : \"\\u2717\"}</span>\n <div class=\"scenetest-content\">\n <div class=\"scenetest-desc${a.type === \"fail\" && a.result ? \" negated\" : \"\"}\">${escapeHtml(a.description)}</div>\n ${a.location ? `<div class=\"scenetest-location\">${escapeHtml(formatLocation(a.location))}</div>` : \"\"}\n </div>\n </div>\n `;\n }\n function renderPanelGroup(g) {\n const stats = getGroupStats(g.items);\n return `\n <div class=\"scenetest-group${g.collapsed ? \" collapsed\" : \"\"}\" data-group-id=\"${g.id}\">\n <div class=\"scenetest-group-header\" onclick=\"this.parentElement.classList.toggle('collapsed')\">\n <div class=\"scenetest-group-summary\">\n <span class=\"scenetest-group-time\">${formatTime(g.timestamp)}</span>\n <div class=\"scenetest-group-stats\">\n <span class=\"scenetest-group-stat pass\">\\u2713${stats.passCount}</span>\n <span class=\"scenetest-group-stat ${stats.failCount > 0 ? \"fail\" : \"zero\"}\">\\u2717${stats.failCount}</span>\n </div>\n </div>\n <span class=\"scenetest-group-toggle\">\\u25BC</span>\n </div>\n <div class=\"scenetest-group-items\">\n ${g.items.map((a) => renderPanelItem(a, g.id)).join(\"\")}\n </div>\n </div>\n `;\n }\n function renderFullscreenItem(a) {\n const histStats = getHistoryStats(a.description, a._index ?? 0);\n const histSummary = formatHistorySummary(histStats);\n const locJson = a.location ? JSON.stringify(a.location).replace(/\"/g, \"&quot;\") : \"null\";\n return `\n <div class=\"item ${a.result ? \"pass\" : \"fail\"}\">\n <span class=\"icon\">${a.result ? \"\\u2713\" : \"\\u2717\"}</span>\n <div class=\"content\">\n <div class=\"desc${a.type === \"fail\" && a.result ? \" negated\" : \"\"}\">${escapeHtml(a.description)}</div>\n ${a.location ? `<div class=\"location\" onclick=\"window.opener && window.opener.__scenetest_openInEditor && window.opener.__scenetest_openInEditor(${locJson})\">${escapeHtml(formatLocation(a.location))}</div>` : \"\"}\n ${histSummary ? `<div class=\"history\">${histSummary}</div>` : \"\"}\n ${a.context ? `<div class=\"context\">${escapeHtml(formatContext(a.context))}</div>` : \"\"}\n ${a.stack && !a.context ? `<div class=\"stack\">${escapeHtml(a.stack.split(\"\\n\").slice(0, 3).join(\"\\n\"))}</div>` : \"\"}\n </div>\n </div>\n `;\n }\n function renderFullscreenGroup(g) {\n const stats = getGroupStats(g.items);\n return `\n <div class=\"group\" data-group-id=\"${g.id}\">\n <div class=\"group-header\" onclick=\"this.parentElement.classList.toggle('collapsed')\">\n <div class=\"group-info\">\n <span class=\"group-time\">${formatTime(g.timestamp)}</span>\n <div class=\"group-stats\">\n ${stats.passCount > 0 ? `<span class=\"group-stat pass\">\\u2713 ${stats.passCount}</span>` : \"\"}\n ${stats.failCount > 0 ? `<span class=\"group-stat fail\">\\u2717 ${stats.failCount}</span>` : \"\"}\n </div>\n <span style=\"color: #6a6a8a\">${g.items.length} assertion${g.items.length === 1 ? \"\" : \"s\"}</span>\n </div>\n <span class=\"group-toggle\">\\u25BC</span>\n </div>\n <div class=\"group-items\">\n ${g.items.map((a) => renderFullscreenItem(a)).join(\"\")}\n </div>\n </div>\n `;\n }\n function renderLocationRow(group) {\n const passCount2 = group.entries.filter((e) => e.result).length;\n const failCount2 = group.entries.filter((e) => !e.result).length;\n const total = group.entries.length;\n const keyJson = JSON.stringify(group.key).replace(/\"/g, \"&quot;\");\n const recentEntries = group.entries.slice(-10);\n const dots = recentEntries.map((e) => {\n if (e.result) {\n return `<span class=\"status-dot pass\" title=\"${formatTime(e.timestamp)}\"></span>`;\n } else {\n return `<span class=\"status-dot fail\" title=\"${formatTime(e.timestamp)}\"><span class=\"dot-x\">\\u2717</span></span>`;\n }\n }).join(\"\");\n const hasAnyFails = failCount2 > 0;\n const lastFailed = !group.lastResult;\n const statusClass = lastFailed ? \"last-fail\" : hasAnyFails ? \"has-fails\" : \"all-pass\";\n const stateIcon = lastFailed ? '<span class=\"state-icon fail\">\\u2717</span>' : hasAnyFails ? '<span class=\"state-icon warn\">\\u26A0</span>' : '<span class=\"state-icon pass\">\\u2713</span>';\n return `\n <div class=\"location-row ${statusClass}\" data-location-key=\"${keyJson}\">\n ${stateIcon}\n <div class=\"location-main\" onclick=\"window.opener ? window.opener.__scenetest_showSequence && window.opener.__scenetest_showSequence(${keyJson}) : window.__scenetest_showSequence && window.__scenetest_showSequence(${keyJson})\">\n <div class=\"location-info\">\n <span class=\"location-file\">${escapeHtml(formatLocationShort(group.location))}</span>\n <span class=\"location-desc\">${escapeHtml(group.description)}</span>\n </div>\n <div class=\"location-stats\">\n <div class=\"status-dots\">${dots}</div>\n <span class=\"location-count\">${total} run${total === 1 ? \"\" : \"s\"}</span>\n <div class=\"location-summary\">\n ${passCount2 > 0 ? `<span class=\"stat pass\">\\u2713${passCount2}</span>` : \"\"}\n ${failCount2 > 0 ? `<span class=\"stat fail\">\\u2717${failCount2}</span>` : \"\"}\n </div>\n </div>\n </div>\n <div class=\"location-actions\">\n <button class=\"loc-btn\" onclick=\"event.stopPropagation(); window.opener ? window.opener.__scenetest_openInEditor && window.opener.__scenetest_openInEditor(${JSON.stringify(group.location).replace(/\"/g, \"&quot;\")}) : window.__scenetest_openInEditor && window.__scenetest_openInEditor(${JSON.stringify(group.location).replace(/\"/g, \"&quot;\")})\" title=\"Open in editor\">\\u270E</button>\n </div>\n </div>\n `;\n }\n function renderSequenceEntry(entry, location, isFirst = false, isLast = false) {\n return `\n <div class=\"sequence-entry ${entry.result ? \"pass\" : \"fail\"}\">\n <div class=\"timeline-track\">\n <div class=\"timeline-line ${isFirst ? \"first\" : \"\"} ${isLast ? \"last\" : \"\"}\"></div>\n <div class=\"timeline-dot ${entry.result ? \"pass\" : \"fail\"}\">\n ${entry.result ? \"\" : '<span class=\"dot-x\">\\u2717</span>'}\n </div>\n </div>\n <div class=\"content\">\n <div class=\"sequence-time\">${formatTime(entry.timestamp)}</div>\n <div class=\"desc\">${escapeHtml(entry.description)}</div>\n ${entry.context ? `<div class=\"context\">${escapeHtml(formatContext(entry.context))}</div>` : \"\"}\n </div>\n </div>\n `;\n }\n function renderSequenceHeader(group) {\n const locJson = JSON.stringify(group.location).replace(/\"/g, \"&quot;\");\n const passCount2 = group.entries.filter((e) => e.result).length;\n const failCount2 = group.entries.filter((e) => !e.result).length;\n return `\n <div class=\"sequence-header\">\n <div class=\"sequence-location\">\n <span class=\"sequence-file\" onclick=\"window.opener ? window.opener.__scenetest_openInEditor && window.opener.__scenetest_openInEditor(${locJson}) : window.__scenetest_openInEditor && window.__scenetest_openInEditor(${locJson})\">${escapeHtml(formatLocation(group.location))}</span>\n </div>\n <div class=\"sequence-summary\">\n <span class=\"sequence-total\">${group.entries.length} run${group.entries.length === 1 ? \"\" : \"s\"}</span>\n <div class=\"sequence-stats\">\n ${passCount2 > 0 ? `<span class=\"stat pass\">\\u2713 ${passCount2}</span>` : \"\"}\n ${failCount2 > 0 ? `<span class=\"stat fail\">\\u2717 ${failCount2}</span>` : \"\"}\n </div>\n </div>\n </div>\n `;\n }\n\n // src/dev-panel/styles.ts\n var panelStyles = `\n#scenetest-panel {\n position: fixed;\n bottom: 16px;\n right: 16px;\n width: 400px;\n max-height: 450px;\n background: #1a1a2e;\n border: 1px solid #4a4a6a;\n border-radius: 8px;\n font-family: ui-monospace, monospace;\n font-size: 12px;\n color: #e0e0e0;\n z-index: 999999;\n box-shadow: 0 4px 24px rgba(0,0,0,0.4);\n display: flex;\n flex-direction: column;\n}\n#scenetest-panel.collapsed {\n max-height: none;\n width: auto;\n}\n#scenetest-panel.collapsed #scenetest-list,\n#scenetest-panel.collapsed #scenetest-actions {\n display: none;\n}\n#scenetest-header {\n padding: 10px 12px;\n background: #252542;\n border-radius: 8px 8px 0 0;\n display: flex;\n justify-content: space-between;\n align-items: center;\n cursor: pointer;\n user-select: none;\n border-bottom: 1px solid #4a4a6a;\n}\n#scenetest-header:hover {\n background: #2a2a4a;\n}\n#scenetest-title {\n font-weight: 600;\n color: #a0a0ff;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.scenetest-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n border-radius: 50%;\n background: #a0a0ff;\n font-size: 12px;\n filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));\n}\n.scenetest-icon span {\n filter: drop-shadow(0px 0px 4px #ffffff);\n}\n#scenetest-counts {\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.scenetest-count {\n padding: 2px 8px;\n border-radius: 4px;\n font-weight: 500;\n cursor: pointer;\n transition: opacity 0.15s;\n}\n.scenetest-count:hover {\n opacity: 0.8;\n}\n.scenetest-count.pass {\n background: #1a3a1a;\n color: #4ade80;\n}\n.scenetest-count.fail {\n background: #3a1a1a;\n color: #f87171;\n}\n.scenetest-count.active {\n outline: 2px solid currentColor;\n outline-offset: 1px;\n}\n#scenetest-actions {\n display: flex;\n gap: 8px;\n padding: 6px 12px;\n background: #202038;\n border-bottom: 1px solid #3a3a5a;\n flex-wrap: wrap;\n align-items: center;\n}\n.scenetest-btn-group {\n display: flex;\n border: 1px solid #4a4a6a;\n border-radius: 4px;\n overflow: hidden;\n}\n.scenetest-btn-group .scenetest-btn {\n border: none;\n border-radius: 0;\n border-right: 1px solid #4a4a6a;\n}\n.scenetest-btn-group .scenetest-btn:last-child {\n border-right: none;\n}\n.scenetest-btn {\n background: none;\n border: 1px solid #4a4a6a;\n color: #a0a0a0;\n padding: 4px 10px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 11px;\n font-family: inherit;\n transition: all 0.15s;\n}\n.scenetest-btn:hover {\n background: #3a3a5a;\n color: #e0e0e0;\n}\n.scenetest-btn.active {\n background: #4a4a6a;\n color: #fff;\n}\n.scenetest-separator {\n width: 1px;\n height: 16px;\n background: #3a3a5a;\n}\n#scenetest-list {\n overflow-y: auto;\n max-height: 340px;\n padding: 8px 0;\n}\n.scenetest-group {\n margin: 4px 8px;\n border: 1px solid #3a3a5a;\n border-radius: 6px;\n overflow: hidden;\n}\n.scenetest-group-header {\n padding: 6px 10px;\n background: #252542;\n display: flex;\n justify-content: space-between;\n align-items: center;\n cursor: pointer;\n font-size: 11px;\n}\n.scenetest-group-header:hover {\n background: #2a2a4a;\n}\n.scenetest-group-summary {\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.scenetest-group-time {\n color: #6a6a8a;\n}\n.scenetest-group-stats {\n display: flex;\n gap: 6px;\n}\n.scenetest-group-stat {\n font-size: 10px;\n padding: 1px 5px;\n border-radius: 3px;\n}\n.scenetest-group-stat.pass {\n background: #1a3a1a;\n color: #4ade80;\n}\n.scenetest-group-stat.fail {\n background: #3a1a1a;\n color: #f87171;\n}\n.scenetest-group-stat.zero {\n background: #2a2a3a;\n color: #6a6a8a;\n}\n.scenetest-group-items {\n border-top: 1px solid #3a3a5a;\n}\n.scenetest-group.collapsed .scenetest-group-items {\n display: none;\n}\n.scenetest-group-toggle {\n color: #6a6a8a;\n font-size: 10px;\n}\n.scenetest-item {\n padding: 6px 10px;\n border-bottom: 1px solid #2a2a4a;\n display: flex;\n gap: 8px;\n align-items: flex-start;\n cursor: pointer;\n position: relative;\n}\n.scenetest-item:hover {\n background: #252545;\n}\n.scenetest-item:last-child {\n border-bottom: none;\n}\n.scenetest-item.pass .scenetest-icon {\n color: #4ade80;\n}\n.scenetest-item.fail .scenetest-icon {\n color: #f87171;\n}\n.scenetest-icon {\n flex-shrink: 0;\n width: 14px;\n text-align: center;\n}\n.scenetest-content {\n flex: 1;\n min-width: 0;\n}\n.scenetest-desc {\n word-break: break-word;\n}\n.scenetest-item.fail .scenetest-desc {\n color: #f87171;\n}\n.scenetest-desc.negated {\n text-decoration: line-through;\n opacity: 0.7;\n}\n.scenetest-location {\n font-size: 9px;\n color: #6a6a8a;\n margin-top: 2px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.scenetest-location:hover {\n color: #a0a0ff;\n text-decoration: underline;\n}\n.scenetest-context {\n font-size: 10px;\n color: #8a8aaa;\n margin-top: 3px;\n padding: 4px 6px;\n background: #12122a;\n border-radius: 3px;\n font-family: ui-monospace, monospace;\n max-height: 60px;\n overflow: auto;\n white-space: pre-wrap;\n word-break: break-all;\n}\n.scenetest-time {\n color: #6a6a8a;\n flex-shrink: 0;\n font-size: 10px;\n}\n#scenetest-empty {\n padding: 20px;\n text-align: center;\n color: #6a6a8a;\n}\n.scenetest-ungrouped {\n padding: 4px 8px;\n}\n.scenetest-history {\n font-size: 9px;\n color: #8a8aaa;\n margin-top: 2px;\n font-style: italic;\n}\n`;\n var fullscreenStyles = `\n* { box-sizing: border-box; }\nbody {\n margin: 0;\n padding: 0;\n background: #0f0f1a;\n color: #e0e0e0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;\n font-size: 13px;\n}\n#header {\n position: sticky;\n top: 0;\n background: #1a1a2e;\n border-bottom: 1px solid #4a4a6a;\n padding: 16px 24px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n z-index: 100;\n flex-wrap: wrap;\n gap: 12px;\n}\n#title {\n font-size: 18px;\n font-weight: 600;\n color: #a0a0ff;\n display: flex;\n align-items: center;\n gap: 10px;\n}\n#title .icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n border-radius: 50%;\n background: #a0a0ff;\n font-size: 18px;\n filter: drop-shadow(0 2px 6px rgba(0,0,0,0.3));\n}\n#title .icon span {\n filter: drop-shadow(0px 0px 5px #ffffff);\n}\n#controls {\n display: flex;\n gap: 12px;\n align-items: center;\n flex-wrap: wrap;\n}\n#counts {\n display: flex;\n gap: 12px;\n align-items: center;\n}\n.count {\n padding: 4px 12px;\n border-radius: 6px;\n font-weight: 600;\n font-size: 14px;\n}\n.count.pass {\n background: #1a3a1a;\n color: #4ade80;\n}\n.count.fail {\n background: #3a1a1a;\n color: #f87171;\n}\n.btn {\n background: #252542;\n border: 1px solid #4a4a6a;\n color: #a0a0a0;\n padding: 8px 16px;\n border-radius: 6px;\n cursor: pointer;\n font-size: 13px;\n font-family: inherit;\n transition: all 0.15s;\n}\n.btn:hover {\n background: #3a3a5a;\n color: #e0e0e0;\n}\n.btn.active {\n background: #4a4a6a;\n color: #fff;\n}\n#filters {\n display: flex;\n gap: 0;\n}\n.btn-group {\n display: flex;\n border: 1px solid #4a4a6a;\n border-radius: 6px;\n overflow: hidden;\n}\n.btn-group .btn {\n border: none;\n border-radius: 0;\n border-right: 1px solid #4a4a6a;\n}\n.btn-group .btn:last-child {\n border-right: none;\n}\n.separator {\n width: 1px;\n height: 24px;\n background: #4a4a6a;\n}\n#list {\n padding: 16px;\n}\n.ungrouped-list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.group {\n margin-bottom: 16px;\n border: 1px solid #3a3a5a;\n border-radius: 8px;\n overflow: hidden;\n}\n.group-header {\n padding: 12px 16px;\n background: #1a1a2e;\n display: flex;\n justify-content: space-between;\n align-items: center;\n cursor: pointer;\n}\n.group-header:hover {\n background: #252542;\n}\n.group-info {\n display: flex;\n gap: 16px;\n align-items: center;\n}\n.group-time {\n color: #a0a0ff;\n font-weight: 500;\n}\n.group-stats {\n display: flex;\n gap: 8px;\n}\n.group-stat {\n padding: 2px 8px;\n border-radius: 4px;\n font-size: 12px;\n}\n.group-stat.pass {\n background: #1a3a1a;\n color: #4ade80;\n}\n.group-stat.fail {\n background: #3a1a1a;\n color: #f87171;\n}\n.group-toggle {\n color: #6a6a8a;\n font-size: 12px;\n}\n.group-items {\n border-top: 1px solid #3a3a5a;\n}\n.group.collapsed .group-items {\n display: none;\n}\n.item {\n padding: 10px 16px;\n background: #12121f;\n display: flex;\n gap: 12px;\n align-items: flex-start;\n border-bottom: 1px solid #2a2a4a;\n}\n.item:last-child {\n border-bottom: none;\n}\n.item.fail {\n background: #1a1212;\n}\n.icon {\n font-size: 14px;\n width: 18px;\n text-align: center;\n flex-shrink: 0;\n}\n.item.pass .icon { color: #4ade80; }\n.item.fail .icon { color: #f87171; }\n.content {\n flex: 1;\n}\n.desc {\n margin-bottom: 4px;\n word-break: break-word;\n}\n.item.fail .desc {\n color: #f87171;\n}\n.desc.negated {\n text-decoration: line-through;\n opacity: 0.7;\n}\n.location {\n font-size: 11px;\n color: #6a6a8a;\n margin-top: 4px;\n cursor: pointer;\n}\n.location:hover {\n color: #a0a0ff;\n text-decoration: underline;\n}\n.context {\n margin-top: 8px;\n padding: 8px;\n background: #0a0a12;\n border-radius: 4px;\n font-size: 11px;\n color: #a0a0c0;\n white-space: pre-wrap;\n word-break: break-all;\n max-height: 100px;\n overflow: auto;\n}\n.meta {\n font-size: 11px;\n color: #6a6a8a;\n}\n.stack {\n margin-top: 8px;\n padding: 8px;\n background: #0a0a12;\n border-radius: 4px;\n font-size: 11px;\n color: #8a8a9a;\n white-space: pre-wrap;\n word-break: break-all;\n}\n.history {\n font-size: 11px;\n color: #8a8aaa;\n margin-top: 4px;\n font-style: italic;\n}\n#empty {\n text-align: center;\n padding: 60px 20px;\n color: #6a6a8a;\n}\n#empty-icon {\n font-size: 48px;\n margin-bottom: 16px;\n}\n.group.highlighted {\n box-shadow: 0 0 0 3px #a0a0ff, 0 0 20px rgba(160, 160, 255, 0.4);\n}\n\n/* View mode toggle */\n#view-modes {\n display: flex;\n gap: 0;\n}\n\n/* Location view styles */\n.location-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 16px;\n background: #12121f;\n border: 1px solid #3a3a5a;\n border-radius: 8px;\n margin-bottom: 8px;\n cursor: pointer;\n transition: all 0.15s;\n}\n.location-row:hover {\n background: #1a1a2e;\n border-color: #4a4a6a;\n}\n.location-row.all-pass {\n border-left: 3px solid #4ade80;\n}\n.location-row.has-fails {\n border-left: 3px solid #f59e0b;\n}\n.location-row.last-fail {\n border-left: 3px solid #f87171;\n background: #1a1212;\n}\n.location-main {\n flex: 1;\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 16px;\n}\n.location-info {\n display: flex;\n flex-direction: column;\n gap: 4px;\n min-width: 0;\n}\n.location-file {\n font-size: 12px;\n color: #a0a0ff;\n font-weight: 500;\n}\n.location-desc {\n font-size: 13px;\n color: #e0e0e0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.location-stats {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-shrink: 0;\n}\n/* Current state icon - prominent indicator on the left */\n.state-icon {\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n font-weight: bold;\n border-radius: 50%;\n flex-shrink: 0;\n margin-right: 12px;\n}\n.state-icon.pass {\n background: #1a3a1a;\n color: #4ade80;\n}\n.state-icon.warn {\n background: #3a2a0a;\n color: #f59e0b;\n}\n.state-icon.fail {\n background: #3a1a1a;\n color: #f87171;\n}\n\n.status-dots {\n display: flex;\n gap: 4px;\n align-items: center;\n}\n.status-dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n transition: transform 0.15s;\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n.status-dot.pass {\n background: #4ade80;\n}\n.status-dot.fail {\n background: #f87171;\n}\n/* Accessibility: X marker on failure dots for colorblind users */\n.status-dot .dot-x {\n position: absolute;\n color: white;\n font-size: 9px;\n font-weight: bold;\n line-height: 1;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n text-shadow: 0 0 2px rgba(0,0,0,0.5);\n}\n.status-dot:last-child {\n animation: pulse-dot 0.5s ease-out;\n}\n@keyframes pulse-dot {\n 0% { transform: scale(1.5); }\n 100% { transform: scale(1); }\n}\n.location-count {\n font-size: 11px;\n color: #6a6a8a;\n}\n.location-summary {\n display: flex;\n gap: 6px;\n}\n.location-summary .stat {\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 11px;\n}\n.location-summary .stat.pass {\n background: #1a3a1a;\n color: #4ade80;\n}\n.location-summary .stat.fail {\n background: #3a1a1a;\n color: #f87171;\n}\n.location-actions {\n margin-left: 12px;\n}\n.loc-btn {\n background: #252542;\n border: 1px solid #4a4a6a;\n color: #a0a0a0;\n padding: 4px 8px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 12px;\n}\n.loc-btn:hover {\n background: #3a3a5a;\n color: #e0e0e0;\n}\n\n/* Sequence view styles */\n.sequence-header {\n background: #1a1a2e;\n border: 1px solid #4a4a6a;\n border-radius: 8px;\n padding: 16px;\n margin-bottom: 8px;\n}\n.sequence-location {\n margin-bottom: 8px;\n}\n.sequence-file {\n color: #a0a0ff;\n font-size: 14px;\n cursor: pointer;\n}\n.sequence-file:hover {\n text-decoration: underline;\n}\n.sequence-summary {\n display: flex;\n align-items: center;\n gap: 16px;\n}\n.sequence-total {\n color: #6a6a8a;\n font-size: 12px;\n}\n.sequence-stats {\n display: flex;\n gap: 8px;\n}\n.sequence-stats .stat {\n padding: 2px 8px;\n border-radius: 4px;\n font-size: 12px;\n}\n.sequence-stats .stat.pass {\n background: #1a3a1a;\n color: #4ade80;\n}\n.sequence-stats .stat.fail {\n background: #3a1a1a;\n color: #f87171;\n}\n\n/* Direction hint */\n.sequence-direction-hint {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 16px;\n margin-bottom: 8px;\n color: #6a6a8a;\n font-size: 11px;\n background: #0f0f1a;\n border-radius: 4px;\n}\n.direction-arrow {\n color: #a0a0ff;\n font-size: 14px;\n}\n\n/* Timeline track for sequence entries */\n.sequence-list {\n position: relative;\n padding-left: 8px;\n}\n.sequence-entry {\n padding: 12px 16px 12px 0;\n background: #12121f;\n display: flex;\n gap: 0;\n align-items: stretch;\n border: 1px solid #3a3a5a;\n border-radius: 6px;\n margin-bottom: 0;\n margin-left: 20px;\n position: relative;\n}\n.sequence-entry.fail {\n background: #1a1212;\n border-color: #4a2a2a;\n}\n\n/* Timeline track with connecting line */\n.timeline-track {\n width: 40px;\n display: flex;\n flex-direction: column;\n align-items: center;\n position: relative;\n flex-shrink: 0;\n}\n.timeline-line {\n position: absolute;\n left: 50%;\n transform: translateX(-50%);\n width: 2px;\n background: #3a3a5a;\n top: -8px;\n bottom: -8px;\n}\n.timeline-line.first {\n top: 50%;\n}\n.timeline-line.last {\n bottom: 50%;\n}\n.timeline-line.first.last {\n display: none;\n}\n.timeline-dot {\n width: 14px;\n height: 14px;\n border-radius: 50%;\n position: relative;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-top: 4px;\n}\n.timeline-dot.pass {\n background: #4ade80;\n box-shadow: 0 0 0 3px #1a3a1a;\n}\n.timeline-dot.fail {\n background: #f87171;\n box-shadow: 0 0 0 3px #3a1a1a;\n}\n.timeline-dot .dot-x {\n color: white;\n font-size: 10px;\n font-weight: bold;\n line-height: 1;\n text-shadow: 0 0 2px rgba(0,0,0,0.5);\n}\n\n.sequence-entry .content {\n flex: 1;\n padding-left: 8px;\n}\n.sequence-time {\n font-size: 11px;\n color: #6a6a8a;\n margin-bottom: 4px;\n}\n\n/* End label */\n.sequence-end-label {\n margin-left: 20px;\n padding: 8px 16px 8px 40px;\n color: #6a6a8a;\n font-size: 11px;\n position: relative;\n}\n.sequence-end-label::before {\n content: '';\n position: absolute;\n left: 28px;\n top: 0;\n width: 2px;\n height: 8px;\n background: #3a3a5a;\n}\n\n.back-btn {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 16px;\n color: #a0a0ff;\n cursor: pointer;\n font-size: 13px;\n}\n.back-btn:hover {\n text-decoration: underline;\n}\n`;\n\n // src/dev-panel/fullscreen.ts\n function getFullscreenHTML() {\n return `\n <!DOCTYPE html>\n <html>\n <head>\n <title>scenetest - Inline Assertions</title>\n <style>${fullscreenStyles}</style>\n </head>\n <body>\n <div id=\"header\">\n <span id=\"title\"><span class=\"icon\"><span>\\u{1F3AC}</span></span>scenetest</span>\n <div id=\"controls\">\n <div id=\"counts\">\n <span class=\"count pass\" id=\"pass-count\">\\u2713 0</span>\n <span class=\"count fail\" id=\"fail-count\">\\u2717 0</span>\n </div>\n <div id=\"view-modes\" class=\"btn-group\">\n <button class=\"btn active\" id=\"view-grouped\" title=\"Group by time\">Timeline</button>\n <button class=\"btn\" id=\"view-byLocation\" title=\"Group by code location\">By Location</button>\n </div>\n <div id=\"filters\" class=\"btn-group\">\n <button class=\"btn active\" id=\"filter-all\">All</button>\n <button class=\"btn\" id=\"filter-fails\">Errors</button>\n <button class=\"btn\" id=\"filter-passes\">Passes</button>\n </div>\n <span class=\"separator\"></span>\n <button class=\"btn\" id=\"scenetest-clear-full\">Clear</button>\n </div>\n </div>\n <div id=\"list\">\n <div id=\"empty\">\n <div id=\"empty-icon\">\\u{1F3AC}</div>\n <div>Interact with your app to see inline assertions appear here...</div>\n </div>\n </div>\n </body>\n </html>\n `;\n }\n function setFullscreenFilter(newFilter) {\n setFilter(newFilter);\n if (panel) {\n panel.querySelector(\"#scenetest-filter-all\")?.classList.toggle(\"active\", filter === \"all\");\n panel.querySelector(\"#scenetest-filter-fails\")?.classList.toggle(\"active\", filter === \"fails\");\n panel.querySelector(\"#scenetest-pass\")?.classList.toggle(\"active\", filter === \"passes\");\n panel.querySelector(\"#scenetest-fail\")?.classList.toggle(\"active\", filter === \"fails\");\n }\n updatePanel();\n updateFullscreenWindow();\n }\n function setFullscreenViewMode(newMode) {\n setViewMode(newMode);\n updateFullscreenWindow();\n }\n function showSequence(locationKey) {\n setSequenceLocation(locationKey);\n updateFullscreenWindow();\n }\n var scrollToGroupId = null;\n function openFullscreen(groupId) {\n scrollToGroupId = groupId ?? null;\n if (fullscreenWindow && !fullscreenWindow.closed) {\n fullscreenWindow.focus();\n if (scrollToGroupId !== null) {\n scrollToGroup(scrollToGroupId);\n }\n return;\n }\n const win = window.open(\"\", \"scenetest-fullscreen\", \"width=900,height=700\");\n if (!win) {\n alert(\"Please allow popups for this site to use fullscreen mode.\");\n return;\n }\n setFullscreenWindow(win);\n win.document.write(getFullscreenHTML());\n win.document.close();\n const doc = win.document;\n doc.getElementById(\"scenetest-clear-full\")?.addEventListener(\"click\", () => {\n clearAll();\n updatePanel();\n updateFullscreenWindow();\n });\n doc.getElementById(\"filter-all\")?.addEventListener(\"click\", () => {\n setFullscreenFilter(\"all\");\n });\n doc.getElementById(\"filter-fails\")?.addEventListener(\"click\", () => {\n setFullscreenFilter(\"fails\");\n });\n doc.getElementById(\"filter-passes\")?.addEventListener(\"click\", () => {\n setFullscreenFilter(\"passes\");\n });\n doc.getElementById(\"view-grouped\")?.addEventListener(\"click\", () => {\n setFullscreenViewMode(\"grouped\");\n });\n doc.getElementById(\"view-byLocation\")?.addEventListener(\"click\", () => {\n setFullscreenViewMode(\"byLocation\");\n });\n updateFullscreenWindow();\n if (scrollToGroupId !== null) {\n scrollToGroup(scrollToGroupId);\n scrollToGroupId = null;\n }\n }\n function scrollToGroup(groupId) {\n if (!fullscreenWindow || fullscreenWindow.closed) return;\n const doc = fullscreenWindow.document;\n const groupEl = doc.querySelector(`[data-group-id=\"${groupId}\"]`);\n if (groupEl) {\n groupEl.classList.remove(\"collapsed\");\n groupEl.scrollIntoView({ behavior: \"auto\", block: \"start\" });\n const prevHighlighted = doc.querySelector(\".group.highlighted\");\n if (prevHighlighted) prevHighlighted.classList.remove(\"highlighted\");\n groupEl.classList.add(\"highlighted\");\n }\n }\n function openFullscreenToGroup(groupId) {\n openFullscreen(groupId);\n }\n function updateFullscreenWindow() {\n if (!fullscreenWindow || fullscreenWindow.closed) return;\n const doc = fullscreenWindow.document;\n const passCountEl = doc.getElementById(\"pass-count\");\n const failCountEl = doc.getElementById(\"fail-count\");\n if (passCountEl) passCountEl.textContent = `\\u2713 ${passCount}`;\n if (failCountEl) failCountEl.textContent = `\\u2717 ${failCount}`;\n doc.getElementById(\"filter-all\")?.classList.toggle(\"active\", filter === \"all\");\n doc.getElementById(\"filter-fails\")?.classList.toggle(\"active\", filter === \"fails\");\n doc.getElementById(\"filter-passes\")?.classList.toggle(\"active\", filter === \"passes\");\n const isSequenceView = viewMode === \"sequence\";\n doc.getElementById(\"view-grouped\")?.classList.toggle(\"active\", viewMode === \"grouped\");\n doc.getElementById(\"view-byLocation\")?.classList.toggle(\"active\", viewMode === \"byLocation\" || isSequenceView);\n const listEl2 = doc.getElementById(\"list\");\n if (!listEl2) return;\n if (viewMode === \"sequence\" && sequenceLocationKey) {\n renderSequenceView(doc, listEl2);\n return;\n }\n if (viewMode === \"byLocation\") {\n renderByLocationView(doc, listEl2);\n return;\n }\n renderGroupedView(doc, listEl2);\n }\n function renderGroupedView(doc, listEl2) {\n const scrollTop = doc.documentElement.scrollTop || doc.body.scrollTop;\n const isScrolled = scrollTop > 50;\n let anchorGroupId = null;\n let anchorOffset = 0;\n if (isScrolled) {\n const groupEls = listEl2.querySelectorAll(\"[data-group-id]\");\n for (const el of groupEls) {\n const rect = el.getBoundingClientRect();\n if (rect.top >= -rect.height / 2) {\n anchorGroupId = parseInt(el.getAttribute(\"data-group-id\") || \"\", 10);\n anchorOffset = rect.top;\n break;\n }\n }\n }\n const filteredGroups = groups.map((g) => ({\n ...g,\n items: filterItems(g.items)\n })).filter((g) => g.items.length > 0);\n if (filteredGroups.length === 0) {\n const icon = filter === \"fails\" ? \"\\u2713\" : \"\\u{1F3AC}\";\n const message = filter === \"fails\" ? \"No errors! All assertions passed.\" : \"Interact with your app to see inline assertions appear here...\";\n listEl2.innerHTML = `\n <div id=\"empty\">\n <div id=\"empty-icon\">${icon}</div>\n <div>${message}</div>\n </div>\n `;\n return;\n }\n listEl2.innerHTML = filteredGroups.map((g) => renderFullscreenGroup(g)).reverse().join(\"\");\n if (anchorGroupId !== null) {\n const anchorEl = listEl2.querySelector(`[data-group-id=\"${anchorGroupId}\"]`);\n if (anchorEl) {\n const newRect = anchorEl.getBoundingClientRect();\n const scrollAdjustment = newRect.top - anchorOffset;\n doc.documentElement.scrollTop += scrollAdjustment;\n doc.body.scrollTop += scrollAdjustment;\n }\n }\n }\n function renderByLocationView(_doc, listEl2) {\n const locations = Array.from(locationGroups.values()).sort((a, b) => b.lastTimestamp - a.lastTimestamp);\n let filteredLocations = locations;\n if (filter === \"fails\") {\n filteredLocations = locations.filter((loc) => loc.entries.some((e) => !e.result));\n } else if (filter === \"passes\") {\n filteredLocations = locations.filter((loc) => loc.entries.some((e) => e.result));\n }\n if (filteredLocations.length === 0) {\n const icon = filter === \"fails\" ? \"\\u2713\" : \"\\u{1F3AC}\";\n const message = filter === \"fails\" ? \"No errors! All assertions passed.\" : \"Interact with your app to see inline assertions appear here...\";\n listEl2.innerHTML = `\n <div id=\"empty\">\n <div id=\"empty-icon\">${icon}</div>\n <div>${message}</div>\n </div>\n `;\n return;\n }\n listEl2.innerHTML = `\n <div class=\"location-list\">\n ${filteredLocations.map((loc) => renderLocationRow(loc)).join(\"\")}\n </div>\n `;\n }\n function renderSequenceView(_doc, listEl2) {\n const group = locationGroups.get(sequenceLocationKey);\n if (!group) {\n setViewMode(\"byLocation\");\n renderByLocationView(_doc, listEl2);\n return;\n }\n let entries = group.entries;\n if (filter === \"fails\") {\n entries = entries.filter((e) => !e.result);\n } else if (filter === \"passes\") {\n entries = entries.filter((e) => e.result);\n }\n const backHandler = `window.opener ? window.opener.__scenetest_setViewMode && window.opener.__scenetest_setViewMode('byLocation') : window.__scenetest_setViewMode && window.__scenetest_setViewMode('byLocation')`;\n const reversedEntries = entries.slice().reverse();\n const entryCount = reversedEntries.length;\n listEl2.innerHTML = `\n <div class=\"back-btn\" onclick=\"${backHandler}\">\\u2190 Back to all locations</div>\n ${renderSequenceHeader(group)}\n <div class=\"sequence-direction-hint\">\n <span class=\"direction-arrow\">\\u2191</span> Most recent at top\n </div>\n <div class=\"sequence-list\">\n ${reversedEntries.map((entry, i) => {\n const isFirst = i === 0;\n const isLast = i === entryCount - 1;\n return renderSequenceEntry(entry, group.location, isFirst, isLast);\n }).join(\"\")}\n </div>\n ${entryCount > 1 ? '<div class=\"sequence-end-label\">First event</div>' : \"\"}\n `;\n }\n\n // src/dev-panel/panel.ts\n function getPanelHTML() {\n return `\n <style>${panelStyles}</style>\n <div id=\"scenetest-header\">\n <span id=\"scenetest-title\"><span class=\"scenetest-icon\"><span>\\u{1F3AC}</span></span>scenetest</span>\n <span id=\"scenetest-counts\">\n <span class=\"scenetest-count pass\" id=\"scenetest-pass\" title=\"Click to filter passes\">\\u2713 0</span>\n <span class=\"scenetest-count fail\" id=\"scenetest-fail\" title=\"Click to filter failures\">\\u2717 0</span>\n </span>\n </div>\n <div id=\"scenetest-actions\">\n <div class=\"scenetest-btn-group\">\n <button class=\"scenetest-btn active\" id=\"scenetest-filter-all\">all</button>\n <button class=\"scenetest-btn\" id=\"scenetest-filter-fails\">errors</button>\n </div>\n <span class=\"scenetest-separator\"></span>\n <button class=\"scenetest-btn\" id=\"scenetest-fullscreen\">fullscreen</button>\n <button class=\"scenetest-btn\" id=\"scenetest-clear\">clear</button>\n </div>\n <div id=\"scenetest-list\">\n <div id=\"scenetest-empty\">Click around to see inline assertions...</div>\n </div>\n `;\n }\n function handleSetFilter(newFilter) {\n setFilter(newFilter);\n panel?.querySelector(\"#scenetest-filter-all\")?.classList.toggle(\"active\", filter === \"all\");\n panel?.querySelector(\"#scenetest-filter-fails\")?.classList.toggle(\"active\", filter === \"fails\");\n panel?.querySelector(\"#scenetest-pass\")?.classList.toggle(\"active\", filter === \"passes\");\n panel?.querySelector(\"#scenetest-fail\")?.classList.toggle(\"active\", filter === \"fails\");\n updatePanel();\n updateFullscreenWindow();\n }\n function createPanel() {\n const panelEl = document.createElement(\"div\");\n panelEl.id = \"scenetest-panel\";\n panelEl.innerHTML = getPanelHTML();\n document.body.appendChild(panelEl);\n setPanel(panelEl);\n setListEl(panelEl.querySelector(\"#scenetest-list\"));\n panelEl.querySelector(\"#scenetest-header\")?.addEventListener(\"click\", (e) => {\n const target = e.target;\n if (target.classList.contains(\"scenetest-count\")) return;\n panelEl.classList.toggle(\"collapsed\");\n });\n panelEl.querySelector(\"#scenetest-pass\")?.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n handleSetFilter(filter === \"passes\" ? \"all\" : \"passes\");\n });\n panelEl.querySelector(\"#scenetest-fail\")?.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n handleSetFilter(filter === \"fails\" ? \"all\" : \"fails\");\n });\n panelEl.querySelector(\"#scenetest-filter-all\")?.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n handleSetFilter(\"all\");\n });\n panelEl.querySelector(\"#scenetest-filter-fails\")?.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n handleSetFilter(\"fails\");\n });\n panelEl.querySelector(\"#scenetest-clear\")?.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n clearAll();\n updatePanel();\n updateFullscreenWindow();\n });\n panelEl.querySelector(\"#scenetest-fullscreen\")?.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n openFullscreen();\n });\n }\n function updatePanel() {\n if (!panel || !listEl) return;\n const passEl = panel.querySelector(\"#scenetest-pass\");\n const failEl = panel.querySelector(\"#scenetest-fail\");\n if (passEl) passEl.textContent = `\\u2713 ${passCount}`;\n if (failEl) failEl.textContent = `\\u2717 ${failCount}`;\n const filteredGroups = groups.map((g) => ({\n ...g,\n items: filterItems(g.items)\n })).filter((g) => g.items.length > 0);\n if (filteredGroups.length === 0) {\n const message = filter === \"fails\" ? \"No errors! All assertions passed.\" : \"Click around to see inline assertions...\";\n listEl.innerHTML = `<div id=\"scenetest-empty\">${message}</div>`;\n return;\n }\n listEl.innerHTML = filteredGroups.map((g) => renderPanelGroup(g)).reverse().join(\"\");\n }\n\n // src/dev-panel/index.ts\n if (!window.__scenetest_panel) {\n let addToGroup = function(result) {\n const now = Date.now();\n if (pendingGroup && now - pendingGroup.timestamp < GROUP_THRESHOLD_MS) {\n pendingGroup.items.push(result);\n } else {\n const newGroup = {\n id: groups.length,\n timestamp: now,\n items: [result],\n collapsed: collapsedMode\n // Use collapsedMode to determine initial state\n };\n setPendingGroup(newGroup);\n groups.push(newGroup);\n }\n if (groupTimeout) clearTimeout(groupTimeout);\n setGroupTimeout(\n setTimeout(() => {\n setPendingGroup(null);\n updatePanel();\n updateFullscreenWindow();\n }, GROUP_THRESHOLD_MS)\n );\n };\n addToGroup2 = addToGroup;\n window.__scenetest_panel = true;\n window.__scenetest_openInEditor = openInEditor;\n window.__scenetest_openFullscreenToGroup = openFullscreenToGroup;\n window.__scenetest_showSequence = showSequence;\n window.__scenetest_setViewMode = (mode) => {\n setViewMode(mode);\n updateFullscreenWindow();\n };\n const existingReport = window.__scenetest_report;\n window.__scenetest_report = function(result) {\n if (existingReport) {\n try {\n existingReport(result);\n } catch {\n }\n }\n const index = assertions.length;\n result._index = index;\n assertions.push(result);\n trackAssertion(result, index);\n trackLocationGroup(result, index);\n if (result.result) {\n incrementPassCount();\n } else {\n incrementFailCount();\n }\n addToGroup(result);\n if (!panel && document.body) {\n createPanel();\n }\n updatePanel();\n updateFullscreenWindow();\n const icon = result.result ? \"\\u2713\" : \"\\u2717\";\n const style = result.result ? \"color: #4ade80\" : \"color: #f87171\";\n console.log(`%c${icon} [scenetest] ${result.description}`, style);\n };\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", createPanel);\n } else {\n createPanel();\n }\n }\n var addToGroup2;\n})();\n";
9
- //# sourceMappingURL=dev-panel.generated.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dev-panel.generated.js","sourceRoot":"","sources":["../src/dev-panel.generated.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,qx9CAAqx9C,CAAC"}