beyond-simulator-web 0.1.2
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.
- package/LICENSE +232 -0
- package/README.md +68 -0
- package/dist/public/app.js +461 -0
- package/dist/public/editor.css +1 -0
- package/dist/public/editor.html +13 -0
- package/dist/public/editor.js +80 -0
- package/dist/public/index.html +125 -0
- package/dist/public/play-renderer.js +936 -0
- package/dist/public/play.css +14 -0
- package/dist/public/play.html +27 -0
- package/dist/public/play.js +300 -0
- package/dist/public/styles.css +291 -0
- package/dist/server.js +115 -0
- package/dist/worker.js +105 -0
- package/package.json +32 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
*{box-sizing:border-box}html,body{width:100%;height:100%;margin:0;overflow:hidden}
|
|
3
|
+
body{--bg:#0c1422;--panel:#101a2a;--panel2:#152238;--line:#263750;--text:#edf3ff;--muted:#8f9caf;--workspace:#111c2c;--input:#0d1726;--blue:#4382f3;color:var(--text);background:var(--bg);font:13px/1.45 Inter,"Microsoft YaHei UI","Microsoft YaHei",sans-serif}
|
|
4
|
+
body[data-theme=light]{--bg:#eef1f5;--panel:#fff;--panel2:#f3f5f8;--line:#d5dbe4;--text:#20252c;--muted:#68717e;--workspace:#d9dee6;--input:#fff}
|
|
5
|
+
button{font:inherit;color:inherit;cursor:pointer}button:disabled{opacity:.42;cursor:default}.app{height:100%;display:grid;grid-template-rows:56px minmax(0,1fr) 34px;background:var(--bg)}
|
|
6
|
+
.bar{display:grid;grid-template-columns:minmax(250px,1fr) auto minmax(360px,1fr);align-items:center;gap:12px;padding:0 14px;border-bottom:1px solid var(--line);background:linear-gradient(180deg,var(--panel2),var(--panel));box-shadow:0 1px 12px #0003;z-index:2}
|
|
7
|
+
.brand,.device,.view-tabs,.actions,.status{display:flex;align-items:center}.brand{gap:10px;min-width:0}.icon{width:30px;height:30px;display:grid;place-items:center;border:1px solid var(--line);border-radius:6px;background:var(--input);font-size:11px}.brand strong{font-size:16px}.device{gap:8px;margin-left:18px;color:var(--muted)}.device b{color:var(--text);font-weight:500}.view-tabs{height:36px;padding:3px;border:1px solid var(--line);border-radius:7px;background:var(--input)}.view-tabs button{min-width:100px;height:28px;padding:0 16px;border:0;border-radius:5px;background:transparent;color:var(--muted)}.view-tabs button.active{color:#fff;background:linear-gradient(#4c87fa,#376fd7);box-shadow:0 0 0 1px #5e90ef55}.actions{justify-content:flex-end;gap:7px}.actions button{height:34px;padding:0 12px;border:1px solid var(--line);border-radius:6px;background:var(--input)}.actions button:hover:not(:disabled){border-color:var(--blue)}.actions .stop{color:#f17884}.actions .primary{color:#fff;border-color:#3265d8;background:linear-gradient(#4c87fa,#376fd7)}.devsel,.playersel,.viewsel{height:30px;width:150px;margin-left:12px;padding:0 4px;border:1px solid var(--line);border-radius:6px;background:var(--input);color:var(--text);text-overflow:ellipsis}.playersel,.viewsel{width:108px}
|
|
8
|
+
.views{position:relative;min-width:0;min-height:0}.play-view{position:absolute;inset:0}.debug-view{position:absolute;inset:0;display:grid;grid-template-columns:minmax(280px,32%) minmax(420px,1fr);background:var(--bg)}.panel{min-height:0;background:color-mix(in srgb,var(--panel) 96%,transparent)}.tree-panel{display:grid;grid-template-rows:42px minmax(0,1fr) 42px minmax(160px,32%);border-right:1px solid var(--line)}.log-stack{min-height:0;display:grid;grid-template-rows:minmax(0,1fr) minmax(0,1fr) auto}.log-panel{display:grid;grid-template-rows:42px minmax(0,1fr);min-height:0}.log-panel + .log-panel{border-top:1px solid var(--line)}.case-panel{display:grid;grid-template-rows:42px auto;border-top:1px solid var(--line)}.case-body{display:grid;grid-template-columns:1fr auto auto;gap:8px;align-items:center;padding:10px 14px}.case-body input,.case-body textarea{width:100%;min-height:34px;padding:6px 8px;border:1px solid var(--line);border-radius:6px;background:var(--input);color:var(--text);font:12px/1.4 Consolas,"Microsoft YaHei UI",monospace}.case-body textarea{min-height:72px;resize:vertical}.vars{min-height:0;overflow:auto;padding:8px 10px}.var-row{display:grid;grid-template-columns:72px minmax(72px,1fr) 52px minmax(80px,1.2fr);gap:6px;align-items:center;padding:5px 4px;border-bottom:1px solid color-mix(in srgb,var(--line) 70%,transparent);font:12px/1.4 Consolas,"Microsoft YaHei UI",monospace}.var-row b{font-weight:650;color:var(--text)}.var-empty{padding:14px 8px;color:var(--muted)}.panel-head{display:flex;align-items:center;padding:0 14px;border-bottom:1px solid var(--line);font-weight:650}.panel-head small{margin-left:auto;color:var(--muted);font-weight:400}.tree{min-height:0;overflow:auto;padding:8px 7px}.log{min-height:0;overflow:auto;padding:12px 16px;white-space:pre-wrap;color:var(--muted);font:12px/1.8 Consolas,"Microsoft YaHei UI",monospace}.log:empty:before{content:"暂无日志。";color:var(--muted)}#log:empty:before{content:"暂无客户端脚本日志。Lua print / printerr 会显示在这里。"}#serverLog:empty:before{content:"暂无服务端日志。变量缺失、越界和信号投递记录在这里,不会混入脚本输出。"}
|
|
9
|
+
.workspace{position:absolute;inset:0;min-width:0;min-height:0;display:flex;align-items:center;justify-content:center;padding:28px;overflow:hidden;background-color:var(--workspace);background-image:radial-gradient(#ffffff0c 1px,transparent 1px),linear-gradient(135deg,#ffffff07 25%,transparent 25%,transparent 75%,#00000008 75%);background-size:4px 4px,24px 24px}.stage-shell{position:relative;filter:drop-shadow(0 14px 28px #0005)}.stage-shell.veiled{visibility:hidden}.loading{position:absolute;inset:0;z-index:3;display:flex;align-items:center;justify-content:center;gap:10px;color:var(--muted);background:var(--bg)}.loading .spin{width:14px;height:14px;border-radius:50%;border:2px solid #4382f338;border-top-color:var(--blue);animation:qxqy-spin .9s linear infinite}@keyframes qxqy-spin{to{transform:rotate(1turn)}}.stage-label{position:absolute;left:0;top:-23px;color:var(--muted);font-size:11px}.stage{position:relative;overflow:hidden;border:1px solid #4d7899;outline:1px solid #0005;background:radial-gradient(circle at 50% 45%,#123a6b,#061934 72%,#071426);touch-action:none;user-select:none}.fatal{position:absolute;inset:0;display:grid;place-items:center;padding:24px;color:#ef7784;background:color-mix(in srgb,var(--workspace) 88%,transparent)}.hidden{display:none!important}
|
|
10
|
+
.stage-canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
|
|
11
|
+
.status{gap:14px;padding:0 14px;border-top:1px solid var(--line);background:var(--panel);color:var(--muted);font-size:11px}.status .live{color:#62d398}.status .error{color:#f07480}.spacer{flex:1}.metric{padding-left:14px;border-left:1px solid var(--line)}
|
|
12
|
+
@media(max-width:1050px){.bar{grid-template-columns:1fr auto 1fr}.device{display:none}.devsel{width:118px;margin-left:8px}.view-tabs button{min-width:78px}.workspace{padding:20px}.debug-view{grid-template-columns:260px minmax(360px,1fr)}}
|
|
13
|
+
@media(max-width:760px){.debug-view{grid-template-columns:1fr}.tree-panel{display:none}.brand strong{font-size:14px}.brand .devsel{width:104px;margin-left:6px}.actions button{padding:0 9px}.actions #theme{display:none}.view-tabs button{min-width:64px;padding:0 10px}}
|
|
14
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>千星沙箱 · 独立试玩</title>
|
|
7
|
+
<link rel="stylesheet" href="/editor/play.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="app">
|
|
11
|
+
<header class="bar">
|
|
12
|
+
<div class="brand"><span class="icon">UI</span><strong>模拟器</strong><select id="deviceSel" class="devsel hidden" aria-label="切换设备画布" title="切换设备画布:以新画布重建运行时,GetUICanvasSize 与预设像素一致"></select><select id="playerCountSel" class="playersel hidden" aria-label="试玩人数" title="试玩人数:最多 8 人,每人一份独立客户端"></select><select id="playerViewSel" class="viewsel hidden" aria-label="当前玩家视角" title="切换当前玩家视角:输入、画面和 PlayerSelf 都跟随该玩家"></select><span class="device">设备 <b id="resolution">准备中…</b></span></div>
|
|
13
|
+
<div class="view-tabs" role="tablist" aria-label="试玩页面"><button class="active" data-view="play" role="tab">试玩</button><button data-view="log" role="tab">日志</button></div>
|
|
14
|
+
<div class="actions"><button id="pause">Ⅱ 暂停运行</button><button id="step" disabled title="暂停后可单帧调试">▷ 单帧调试</button><button class="stop" id="stop">■ 结束试玩</button><button class="primary hidden" id="restart">↻ 重新开始</button><button id="theme" title="切换亮暗主题">◐</button><button id="close">返回编辑</button></div>
|
|
15
|
+
</header>
|
|
16
|
+
<div class="views">
|
|
17
|
+
<main class="workspace play-view" id="workspace"><div class="stage-shell veiled" id="shell"><div class="stage-label" id="stageLabel"></div><div class="stage" id="stage"><canvas id="cv" class="stage-canvas"></canvas></div></div><div class="loading" id="loading"><span class="spin"></span>正在准备试玩…</div><div class="fatal hidden" id="fatal"></div></main>
|
|
18
|
+
<section class="debug-view hidden" id="logView">
|
|
19
|
+
<aside class="panel tree-panel"><div class="panel-head">运行时场景树 <small id="nodeCount">0 节点</small></div><div class="tree" id="sceneTree"></div><div class="panel-head">服务端变量 <small id="varCount">0 项</small></div><div class="vars" id="serverVars"></div></aside>
|
|
20
|
+
<div class="log-stack"><main class="panel log-panel"><div class="panel-head">客户端脚本日志 <small>最近 100 条</small></div><div class="log" id="log"></div></main><aside class="panel log-panel"><div class="panel-head">服务端日志 <small>变量 / 信号</small></div><div class="log" id="serverLog"></div></aside><section class="panel case-panel"><div class="panel-head">自动测用例 <small id="caseHint">先试玩,再保存回放</small></div><div class="case-body"><textarea id="caseAsserts" spellcheck="false" placeholder='断言 JSON 数组,例如 [{"kind":"log","contains":"clicked"}]'></textarea><button id="saveCase">保存用例</button><button id="runCase">回放用例</button></div></section></div>
|
|
21
|
+
</section>
|
|
22
|
+
</div>
|
|
23
|
+
<footer class="status"><span class="live" id="state">● 正在启动</span><span id="viewMode">页面:试玩</span><span class="spacer"></span><span class="metric" id="frame">Frame 0</span><span class="metric" id="time">0.00s</span></footer>
|
|
24
|
+
</div>
|
|
25
|
+
<script type="module" src="/editor/play.js"></script>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
|
|
2
|
+
import { PixiPlayRenderer, createPlaySession } from '/editor/play-renderer.js'
|
|
3
|
+
|
|
4
|
+
(() => {
|
|
5
|
+
'use strict'
|
|
6
|
+
const API = '/editor/api/play'
|
|
7
|
+
const sessionId = decodeURIComponent(location.hash.slice(1))
|
|
8
|
+
const $ = (id) => document.getElementById(id)
|
|
9
|
+
const stage = $('stage')
|
|
10
|
+
const cv = $('cv')
|
|
11
|
+
const renderer = new PixiPlayRenderer(cv)
|
|
12
|
+
const shell = $('shell')
|
|
13
|
+
const workspace = $('workspace')
|
|
14
|
+
const logView = $('logView')
|
|
15
|
+
let theme = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
16
|
+
let fittedCanvas = ''
|
|
17
|
+
|
|
18
|
+
function applyTheme() { document.body.dataset.theme = theme }
|
|
19
|
+
applyTheme()
|
|
20
|
+
|
|
21
|
+
function setView(view) {
|
|
22
|
+
const showLog = view === 'log'
|
|
23
|
+
workspace.classList.toggle('hidden', showLog)
|
|
24
|
+
logView.classList.toggle('hidden', !showLog)
|
|
25
|
+
$('viewMode').textContent = showLog ? '页面:日志' : '页面:试玩'
|
|
26
|
+
for (const button of document.querySelectorAll('[data-view]')) button.classList.toggle('active', button.dataset.view === view)
|
|
27
|
+
if (!showLog) requestAnimationFrame(() => fitStage(true))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function api(action, args = {}) {
|
|
31
|
+
const response = await fetch(API, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'content-type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ sessionId, action, args }),
|
|
35
|
+
})
|
|
36
|
+
const envelope = await response.json().catch(() => null)
|
|
37
|
+
if (!response.ok || !envelope || envelope.ok !== true) throw new Error(envelope?.error || '试玩请求失败')
|
|
38
|
+
return envelope.value
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderVars(rows) {
|
|
42
|
+
const host = $('serverVars')
|
|
43
|
+
host.replaceChildren()
|
|
44
|
+
const list = Array.isArray(rows) ? rows : []
|
|
45
|
+
$('varCount').textContent = list.length + ' 项'
|
|
46
|
+
if (!list.length) {
|
|
47
|
+
const empty = document.createElement('div')
|
|
48
|
+
empty.className = 'var-empty'
|
|
49
|
+
empty.textContent = '暂无自定义变量。脚本 Get/Set 或 serverSet 之后会出现在这里。'
|
|
50
|
+
host.appendChild(empty)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
for (const row of list) {
|
|
54
|
+
const el = document.createElement('div')
|
|
55
|
+
el.className = 'var-row'
|
|
56
|
+
const type = row.type || '—'
|
|
57
|
+
const value = row.value === undefined ? 'nil' : JSON.stringify(row.value)
|
|
58
|
+
const entityCell = document.createElement('span')
|
|
59
|
+
entityCell.textContent = row.entityLabel || row.entityType || ''
|
|
60
|
+
const nameCell = document.createElement('b')
|
|
61
|
+
nameCell.textContent = row.name || ''
|
|
62
|
+
const typeCell = document.createElement('span')
|
|
63
|
+
typeCell.textContent = type
|
|
64
|
+
const valueCell = document.createElement('span')
|
|
65
|
+
valueCell.textContent = value
|
|
66
|
+
el.append(entityCell, nameCell, typeCell, valueCell)
|
|
67
|
+
host.appendChild(el)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderTree(snapshot) {
|
|
72
|
+
const host = $('sceneTree')
|
|
73
|
+
host.replaceChildren()
|
|
74
|
+
const empty = document.createElement('div')
|
|
75
|
+
empty.className = 'var-empty'
|
|
76
|
+
empty.textContent = '完整运行树请用 Agent 的 inspect 快照。舞台按左树上方盖住下方绘制。'
|
|
77
|
+
host.appendChild(empty)
|
|
78
|
+
$('nodeCount').textContent = (snapshot?.scene?.count || snapshot?.paint?.length || 0) + ' 个可见图元'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fitStage(force = false) {
|
|
82
|
+
const snapshot = play.snapshot
|
|
83
|
+
if (!snapshot) return
|
|
84
|
+
const key = snapshot.canvasWidth + '×' + snapshot.canvasHeight
|
|
85
|
+
if (!force && (fittedCanvas === key || !Number.isFinite(snapshot.canvasWidth) || !Number.isFinite(snapshot.canvasHeight))) return
|
|
86
|
+
fittedCanvas = key
|
|
87
|
+
const maxW = Math.max(120, workspace.clientWidth - 48)
|
|
88
|
+
const maxH = Math.max(80, workspace.clientHeight - 48)
|
|
89
|
+
const scale = Math.min(maxW / snapshot.canvasWidth, maxH / snapshot.canvasHeight)
|
|
90
|
+
if (!Number.isFinite(scale) || scale <= 0) return
|
|
91
|
+
shell.style.width = (snapshot.canvasWidth * scale) + 'px'
|
|
92
|
+
shell.style.height = (snapshot.canvasHeight * scale) + 'px'
|
|
93
|
+
stage.style.width = '100%'
|
|
94
|
+
stage.style.height = '100%'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function revealStage() {
|
|
98
|
+
shell.classList.remove('veiled')
|
|
99
|
+
$('loading').classList.add('hidden')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function applyStatus(next) {
|
|
103
|
+
$('frame').textContent = 'Frame ' + (next.frame || 0)
|
|
104
|
+
$('time').textContent = Number(next.time || 0).toFixed(2) + 's'
|
|
105
|
+
$('pause').textContent = next.paused ? '▶ 继续运行' : 'Ⅱ 暂停运行'
|
|
106
|
+
$('pause').disabled = false
|
|
107
|
+
$('step').disabled = !next.paused
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function syncDeviceSelect(next) {
|
|
111
|
+
const sel = $('deviceSel')
|
|
112
|
+
const presets = Array.isArray(next.canvasPresets) ? next.canvasPresets : []
|
|
113
|
+
if (!presets.length || !next.canvasId) {
|
|
114
|
+
sel.replaceChildren()
|
|
115
|
+
sel.classList.add('hidden')
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
const stale = sel.options.length !== presets.length
|
|
119
|
+
|| [...sel.options].some((opt, i) => opt.value !== presets[i].id)
|
|
120
|
+
if (stale) {
|
|
121
|
+
sel.replaceChildren(...presets.map((preset) => {
|
|
122
|
+
const opt = document.createElement('option')
|
|
123
|
+
opt.value = preset.id
|
|
124
|
+
opt.textContent = preset.label || (preset.id + ' · ' + preset.width + '×' + preset.height)
|
|
125
|
+
return opt
|
|
126
|
+
}))
|
|
127
|
+
}
|
|
128
|
+
if (sel.value !== next.canvasId) sel.value = next.canvasId
|
|
129
|
+
sel.classList.remove('hidden')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function fillCountSelect(sel, count) {
|
|
133
|
+
const n = Math.max(1, Math.min(8, Number(count) || 1))
|
|
134
|
+
if (sel.options.length !== 8) {
|
|
135
|
+
sel.replaceChildren(...Array.from({ length: 8 }, (_, i) => {
|
|
136
|
+
const opt = document.createElement('option')
|
|
137
|
+
opt.value = String(i + 1)
|
|
138
|
+
opt.textContent = (i + 1) + ' 人'
|
|
139
|
+
return opt
|
|
140
|
+
}))
|
|
141
|
+
}
|
|
142
|
+
sel.value = String(n)
|
|
143
|
+
sel.classList.remove('hidden')
|
|
144
|
+
return n
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function syncPlayerSelects(next) {
|
|
148
|
+
const countSel = $('playerCountSel')
|
|
149
|
+
const viewSel = $('playerViewSel')
|
|
150
|
+
const count = fillCountSelect(countSel, next.playerCount || 1)
|
|
151
|
+
const players = Array.isArray(next.players) && next.players.length
|
|
152
|
+
? next.players
|
|
153
|
+
: Array.from({ length: count }, (_, i) => ({ index: i + 1, label: '玩家' + (i + 1) }))
|
|
154
|
+
const stale = viewSel.options.length !== players.length
|
|
155
|
+
|| [...viewSel.options].some((opt, i) => Number(opt.value) !== players[i].index)
|
|
156
|
+
if (stale) {
|
|
157
|
+
viewSel.replaceChildren(...players.map((player) => {
|
|
158
|
+
const opt = document.createElement('option')
|
|
159
|
+
opt.value = String(player.index)
|
|
160
|
+
opt.textContent = player.label || ('玩家' + player.index)
|
|
161
|
+
return opt
|
|
162
|
+
}))
|
|
163
|
+
}
|
|
164
|
+
const view = Number(next.viewPlayerIndex) || 1
|
|
165
|
+
if (viewSel.value !== String(view)) viewSel.value = String(view)
|
|
166
|
+
viewSel.classList.toggle('hidden', count < 2)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function renderChrome(next) {
|
|
170
|
+
$('fatal').classList.add('hidden')
|
|
171
|
+
fitStage()
|
|
172
|
+
syncDeviceSelect(next)
|
|
173
|
+
syncPlayerSelects(next)
|
|
174
|
+
$('resolution').textContent = next.canvasWidth + '×' + next.canvasHeight
|
|
175
|
+
const viewLabel = next.playerCount > 1 ? (' · 玩家' + (next.viewPlayerIndex || 1)) : ''
|
|
176
|
+
$('stageLabel').textContent = next.canvasWidth + '×' + next.canvasHeight + viewLabel + ' · 点击输入'
|
|
177
|
+
applyStatus(next)
|
|
178
|
+
$('stop').disabled = false
|
|
179
|
+
$('restart').classList.add('hidden')
|
|
180
|
+
$('state').textContent = next.mountError ? '⚠ 脚本挂载失败' : next.paused ? 'Ⅱ 已暂停' : '● 试玩运行中'
|
|
181
|
+
$('state').className = next.mountError ? 'error' : 'live'
|
|
182
|
+
renderTree(next)
|
|
183
|
+
renderVars(next.serverVars)
|
|
184
|
+
$('log').textContent = (next.logs || []).slice(-100).map((entry) => Number(entry.time || 0).toFixed(2).padStart(7, ' ') + ' [' + entry.level + '] ' + entry.text).join('\n')
|
|
185
|
+
$('serverLog').textContent = (next.serverLogs || next.server?.logs || []).slice(-100).map((entry) => Number(entry.time || 0).toFixed(2).padStart(7, ' ') + ' [' + entry.level + '] ' + entry.text).join('\n')
|
|
186
|
+
$('caseHint').textContent = (next.history && next.history.length ? ('已记录 ' + next.history.length + ' 步') : '先试玩,再保存回放')
|
|
187
|
+
if (next.mountError) setView('log')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function showError(error) {
|
|
191
|
+
$('loading').classList.add('hidden')
|
|
192
|
+
$('fatal').classList.remove('hidden')
|
|
193
|
+
$('fatal').textContent = error?.message || String(error)
|
|
194
|
+
$('state').className = 'error'
|
|
195
|
+
$('state').textContent = '⚠ 试玩错误'
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const play = createPlaySession({
|
|
199
|
+
renderer,
|
|
200
|
+
api,
|
|
201
|
+
onSnapshot: renderChrome,
|
|
202
|
+
onStatus: applyStatus,
|
|
203
|
+
onError: showError,
|
|
204
|
+
})
|
|
205
|
+
play.bindInput(stage, { keysOn: window })
|
|
206
|
+
|
|
207
|
+
$('step').addEventListener('click', () => void play.act('step', { dt: 1 / 30 }))
|
|
208
|
+
$('pause').addEventListener('click', () => void play.act(play.snapshot?.paused ? 'resume' : 'pause'))
|
|
209
|
+
$('stop').addEventListener('click', async () => {
|
|
210
|
+
if (!play.running) return
|
|
211
|
+
try { await play.stop() } catch (error) { showError(error); return }
|
|
212
|
+
$('state').textContent = '■ 试玩已结束,可重新开始或返回编辑'
|
|
213
|
+
$('state').className = ''
|
|
214
|
+
$('pause').disabled = true
|
|
215
|
+
$('step').disabled = true
|
|
216
|
+
$('stop').disabled = true
|
|
217
|
+
$('restart').classList.remove('hidden')
|
|
218
|
+
})
|
|
219
|
+
$('restart').addEventListener('click', async () => {
|
|
220
|
+
try {
|
|
221
|
+
fittedCanvas = ''
|
|
222
|
+
const keepArgs = {}
|
|
223
|
+
const snapshot = play.snapshot
|
|
224
|
+
if (snapshot?.canvasId) keepArgs.canvasId = snapshot.canvasId
|
|
225
|
+
if (snapshot?.playerCount) keepArgs.playerCount = snapshot.playerCount
|
|
226
|
+
if (snapshot?.viewPlayerIndex) keepArgs.viewPlayerIndex = snapshot.viewPlayerIndex
|
|
227
|
+
await play.start(keepArgs)
|
|
228
|
+
fitStage(true)
|
|
229
|
+
} catch (error) { showError(error) }
|
|
230
|
+
})
|
|
231
|
+
$('playerCountSel').addEventListener('change', async () => {
|
|
232
|
+
const playerCount = Number($('playerCountSel').value) || 1
|
|
233
|
+
try {
|
|
234
|
+
shell.classList.add('veiled')
|
|
235
|
+
$('loading').classList.remove('hidden')
|
|
236
|
+
fittedCanvas = ''
|
|
237
|
+
const args = { playerCount, viewPlayerIndex: 1 }
|
|
238
|
+
if (play.snapshot?.canvasId) args.canvasId = play.snapshot.canvasId
|
|
239
|
+
await play.start(args)
|
|
240
|
+
fitStage(true)
|
|
241
|
+
revealStage()
|
|
242
|
+
} catch (error) { showError(error) }
|
|
243
|
+
})
|
|
244
|
+
$('playerViewSel').addEventListener('change', async () => {
|
|
245
|
+
const playerIndex = Number($('playerViewSel').value) || 1
|
|
246
|
+
if (!play.snapshot || !play.running) return
|
|
247
|
+
try { await play.view({ playerIndex }) }
|
|
248
|
+
catch (error) { showError(error) }
|
|
249
|
+
})
|
|
250
|
+
$('deviceSel').addEventListener('change', async () => {
|
|
251
|
+
const canvasId = $('deviceSel').value
|
|
252
|
+
if (!canvasId) return
|
|
253
|
+
try {
|
|
254
|
+
shell.classList.add('veiled')
|
|
255
|
+
$('loading').classList.remove('hidden')
|
|
256
|
+
fittedCanvas = ''
|
|
257
|
+
await play.device({
|
|
258
|
+
canvasId,
|
|
259
|
+
playerCount: play.snapshot?.playerCount || 1,
|
|
260
|
+
viewPlayerIndex: play.snapshot?.viewPlayerIndex || 1,
|
|
261
|
+
})
|
|
262
|
+
fitStage(true)
|
|
263
|
+
revealStage()
|
|
264
|
+
} catch (error) { showError(error) }
|
|
265
|
+
})
|
|
266
|
+
$('close').addEventListener('click', async () => { if (play.running) await play.stop().catch(() => {}); window.close() })
|
|
267
|
+
$('theme').addEventListener('click', () => { theme = theme === 'dark' ? 'light' : 'dark'; applyTheme() })
|
|
268
|
+
let lastCase = null
|
|
269
|
+
function parseAsserts() {
|
|
270
|
+
const text = $('caseAsserts').value.trim()
|
|
271
|
+
if (!text) return []
|
|
272
|
+
const parsed = JSON.parse(text)
|
|
273
|
+
if (!Array.isArray(parsed)) throw new Error('断言必须是 JSON 数组')
|
|
274
|
+
return parsed
|
|
275
|
+
}
|
|
276
|
+
$('saveCase').addEventListener('click', async () => {
|
|
277
|
+
try {
|
|
278
|
+
lastCase = await api('saveCase', { name: 'play-case', asserts: parseAsserts() })
|
|
279
|
+
$('caseHint').textContent = '已保存 ' + (lastCase.events || []).length + ' 步 / ' + (lastCase.asserts || []).length + ' 条断言'
|
|
280
|
+
} catch (error) { showError(error) }
|
|
281
|
+
})
|
|
282
|
+
$('runCase').addEventListener('click', async () => {
|
|
283
|
+
try {
|
|
284
|
+
if (!lastCase) lastCase = await api('saveCase', { name: 'play-case', asserts: parseAsserts() })
|
|
285
|
+
const report = await api('runCase', { case: lastCase })
|
|
286
|
+
$('caseHint').textContent = report.passed ? '回放通过' : ('回放失败 @' + (report.failedAt ?? '?') + 's')
|
|
287
|
+
if (!report.passed) setView('log')
|
|
288
|
+
} catch (error) { showError(error) }
|
|
289
|
+
})
|
|
290
|
+
for (const button of document.querySelectorAll('[data-view]')) button.addEventListener('click', () => setView(button.dataset.view))
|
|
291
|
+
addEventListener('resize', () => fitStage(true))
|
|
292
|
+
addEventListener('pagehide', () => {
|
|
293
|
+
play.destroy()
|
|
294
|
+
fetch(API, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessionId, action: 'stop', args: {} }), keepalive: true }).catch(() => {})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
if (!sessionId) showError(new Error('缺少会话 ID'))
|
|
298
|
+
else play.start().then(() => setTimeout(revealStage, 0)).catch(showError)
|
|
299
|
+
})()
|
|
300
|
+
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--blue: #477cf0;
|
|
3
|
+
--danger: #e75a6a;
|
|
4
|
+
font-family: Inter, "Microsoft YaHei UI", "Microsoft YaHei", system-ui, sans-serif;
|
|
5
|
+
font-size: 13px;
|
|
6
|
+
color-scheme: dark;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
:root[data-theme="dark"] {
|
|
10
|
+
--bg: #20242e;
|
|
11
|
+
--panel: #181d27;
|
|
12
|
+
--panel-2: #202632;
|
|
13
|
+
--input: #151b25;
|
|
14
|
+
--line: #363e4d;
|
|
15
|
+
--line-soft: #2b3240;
|
|
16
|
+
--text: #edf1f7;
|
|
17
|
+
--muted: #929baa;
|
|
18
|
+
--quiet: #697484;
|
|
19
|
+
--hover: #252c37;
|
|
20
|
+
--workspace: #383d47;
|
|
21
|
+
--toolbar-a: #242a35;
|
|
22
|
+
--toolbar-b: #1e232d;
|
|
23
|
+
--status: #242a33;
|
|
24
|
+
--button: #1d2430;
|
|
25
|
+
--shadow: #0004;
|
|
26
|
+
color-scheme: dark;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
:root[data-theme="light"] {
|
|
30
|
+
--bg: #f5f6f8;
|
|
31
|
+
--panel: #fff;
|
|
32
|
+
--panel-2: #f4f6f8;
|
|
33
|
+
--input: #fff;
|
|
34
|
+
--line: #d6dbe3;
|
|
35
|
+
--line-soft: #e7e9ed;
|
|
36
|
+
--text: #20242a;
|
|
37
|
+
--muted: #69717d;
|
|
38
|
+
--quiet: #8c939d;
|
|
39
|
+
--hover: #eef1f5;
|
|
40
|
+
--workspace: #d9dde3;
|
|
41
|
+
--toolbar-a: #fff;
|
|
42
|
+
--toolbar-b: #f3f5f8;
|
|
43
|
+
--status: #f7f8fa;
|
|
44
|
+
--button: #f7f8fa;
|
|
45
|
+
--shadow: #18203318;
|
|
46
|
+
color-scheme: light;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
* { box-sizing: border-box; }
|
|
50
|
+
|
|
51
|
+
html, body { width: 100%; min-width: 320px; min-height: 100%; }
|
|
52
|
+
|
|
53
|
+
body {
|
|
54
|
+
margin: 0;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
background: var(--bg);
|
|
57
|
+
color: var(--text);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
button, select { font: inherit; color: inherit; }
|
|
61
|
+
|
|
62
|
+
button {
|
|
63
|
+
height: 32px;
|
|
64
|
+
padding: 0 10px;
|
|
65
|
+
border: 1px solid var(--line);
|
|
66
|
+
border-radius: 6px;
|
|
67
|
+
background: var(--button);
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
button:hover:not(:disabled) { border-color: #6689d8; background: var(--hover); }
|
|
72
|
+
button:active:not(:disabled) { transform: translateY(1px); }
|
|
73
|
+
button:disabled { cursor: default; opacity: .42; }
|
|
74
|
+
button.primary { border-color: #4779e6; color: #fff; background: linear-gradient(#5388fa, #376ad8); font-weight: 650; }
|
|
75
|
+
button.primary:hover:not(:disabled) { background: linear-gradient(#6092ff, #4073df); }
|
|
76
|
+
button.danger { border-color: color-mix(in srgb, var(--danger) 65%, var(--line)); color: var(--danger); }
|
|
77
|
+
|
|
78
|
+
select {
|
|
79
|
+
height: 32px;
|
|
80
|
+
min-width: 0;
|
|
81
|
+
padding: 0 28px 0 9px;
|
|
82
|
+
border: 1px solid var(--line);
|
|
83
|
+
border-radius: 6px;
|
|
84
|
+
outline: 0;
|
|
85
|
+
background: var(--input);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
button:focus-visible, select:focus-visible, .stage-shell:focus-visible, .tree-row:focus-visible {
|
|
89
|
+
outline: 2px solid color-mix(in srgb, var(--blue) 65%, transparent);
|
|
90
|
+
outline-offset: 1px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.topbar {
|
|
94
|
+
height: 50px;
|
|
95
|
+
display: grid;
|
|
96
|
+
grid-template-columns: minmax(190px, 1fr) minmax(300px, 1.6fr) minmax(190px, 1fr);
|
|
97
|
+
align-items: center;
|
|
98
|
+
gap: 14px;
|
|
99
|
+
padding: 0 10px;
|
|
100
|
+
border-bottom: 1px solid var(--line);
|
|
101
|
+
background: linear-gradient(var(--toolbar-a), var(--toolbar-b));
|
|
102
|
+
box-shadow: 0 1px 8px var(--shadow);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.brand { min-width: 0; display: flex; align-items: center; gap: 9px; }
|
|
106
|
+
.brand-mark {
|
|
107
|
+
width: 26px;
|
|
108
|
+
height: 26px;
|
|
109
|
+
display: grid;
|
|
110
|
+
place-items: center;
|
|
111
|
+
flex: 0 0 26px;
|
|
112
|
+
border: 1px solid #5688f4;
|
|
113
|
+
border-radius: 5px;
|
|
114
|
+
background: linear-gradient(145deg, #5388fa, #3565c6);
|
|
115
|
+
color: #fff;
|
|
116
|
+
box-shadow: inset 0 1px #ffffff30;
|
|
117
|
+
font-size: 9px;
|
|
118
|
+
font-weight: 800;
|
|
119
|
+
letter-spacing: -.02em;
|
|
120
|
+
}
|
|
121
|
+
.brand > div { min-width: 0; display: flex; align-items: baseline; gap: 8px; }
|
|
122
|
+
.brand strong { font-size: 13px; font-weight: 700; white-space: nowrap; }
|
|
123
|
+
.brand span:last-child { overflow: hidden; color: var(--muted); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
|
|
124
|
+
|
|
125
|
+
.project-picker { min-width: 0; display: flex; justify-content: center; align-items: center; gap: 7px; }
|
|
126
|
+
.project-picker label { color: var(--muted); font-size: 10px; white-space: nowrap; }
|
|
127
|
+
.project-picker select { width: min(430px, 100%); }
|
|
128
|
+
.icon-button { width: 32px; min-width: 32px; padding: 0; font-size: 16px; }
|
|
129
|
+
.topbar-status { min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 9px; }
|
|
130
|
+
.theme-button { font-size: 15px; }
|
|
131
|
+
|
|
132
|
+
.connection { overflow: hidden; color: var(--muted); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
|
|
133
|
+
.connection span { width: 7px; height: 7px; display: inline-block; margin-right: 6px; border-radius: 50%; background: #dea943; box-shadow: 0 0 0 3px color-mix(in srgb, #dea943 12%, transparent); }
|
|
134
|
+
.connection.online span { background: #62d398; box-shadow: 0 0 0 3px color-mix(in srgb, #62d398 12%, transparent); }
|
|
135
|
+
.connection.error { color: var(--danger); }
|
|
136
|
+
.connection.error span { background: var(--danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 12%, transparent); }
|
|
137
|
+
|
|
138
|
+
.app-shell {
|
|
139
|
+
height: calc(100vh - 50px);
|
|
140
|
+
display: grid;
|
|
141
|
+
grid-template-columns: clamp(220px, 17vw, 280px) minmax(360px, 1fr) clamp(310px, 22vw, 356px);
|
|
142
|
+
overflow: hidden;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.panel { background: var(--panel); }
|
|
146
|
+
.tree-panel, .info-panel { min-height: 0; overflow: hidden; }
|
|
147
|
+
.tree-panel { border-right: 1px solid var(--line); }
|
|
148
|
+
.info-panel { border-left: 1px solid var(--line); overflow-y: auto; }
|
|
149
|
+
|
|
150
|
+
.panel-heading {
|
|
151
|
+
height: 44px;
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
justify-content: space-between;
|
|
155
|
+
gap: 8px;
|
|
156
|
+
padding: 6px 10px;
|
|
157
|
+
border-bottom: 1px solid var(--line-soft);
|
|
158
|
+
}
|
|
159
|
+
.panel-heading > div { display: flex; align-items: baseline; gap: 7px; }
|
|
160
|
+
.panel-heading h2 { margin: 0; font-size: 13px; font-weight: 650; }
|
|
161
|
+
.eyebrow { color: var(--muted); font-size: 9px; letter-spacing: .1em; }
|
|
162
|
+
.count { min-width: 24px; padding: 2px 6px; border: 1px solid var(--line); border-radius: 9px; background: var(--input); color: var(--muted); text-align: center; font-size: 9px; }
|
|
163
|
+
|
|
164
|
+
.tree { height: calc(100% - 44px); overflow: auto; padding: 3px 6px 8px; scrollbar-color: color-mix(in srgb, var(--muted) 55%, transparent) transparent; }
|
|
165
|
+
.tree-row {
|
|
166
|
+
position: relative;
|
|
167
|
+
width: 100%;
|
|
168
|
+
min-height: 32px;
|
|
169
|
+
display: grid;
|
|
170
|
+
grid-template-columns: 19px minmax(0, 1fr) auto;
|
|
171
|
+
align-items: center;
|
|
172
|
+
gap: 6px;
|
|
173
|
+
padding-right: 7px;
|
|
174
|
+
border: 1px solid transparent;
|
|
175
|
+
border-radius: 4px;
|
|
176
|
+
background: transparent;
|
|
177
|
+
color: var(--text);
|
|
178
|
+
text-align: left;
|
|
179
|
+
}
|
|
180
|
+
.tree-row:hover { background: var(--hover); }
|
|
181
|
+
.tree-row.active { border-color: #4d7ee4; background: linear-gradient(90deg, #3569cf, #315a9f); color: #fff; }
|
|
182
|
+
.tree-icon { width: 19px; height: 19px; display: grid; place-items: center; border: 1px solid currentColor; border-radius: 3px; color: #77a5ff; font-size: 9px; }
|
|
183
|
+
.tree-row.active .tree-icon { color: #fff; }
|
|
184
|
+
.tree-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; }
|
|
185
|
+
.tree-kind { color: var(--muted); font-size: 9px; }
|
|
186
|
+
.tree-row.active .tree-kind { color: #dce7ff; }
|
|
187
|
+
.tree-empty { padding: 22px 10px; color: var(--muted); text-align: center; font-size: 11px; line-height: 1.6; }
|
|
188
|
+
|
|
189
|
+
.preview-panel { min-width: 0; min-height: 0; display: grid; grid-template-rows: 44px minmax(0, 1fr); overflow: hidden; background: var(--workspace); }
|
|
190
|
+
.preview-toolbar {
|
|
191
|
+
min-width: 0;
|
|
192
|
+
display: flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
gap: 9px;
|
|
195
|
+
padding: 5px 8px;
|
|
196
|
+
border-bottom: 1px solid var(--line);
|
|
197
|
+
background: linear-gradient(var(--toolbar-a), var(--toolbar-b));
|
|
198
|
+
overflow-x: auto;
|
|
199
|
+
}
|
|
200
|
+
.segmented { display: flex; flex: 0 0 auto; gap: 2px; padding: 2px; border: 1px solid var(--line); border-radius: 9px; background: var(--input); }
|
|
201
|
+
.segmented button { height: 26px; padding: 0 10px; border: 0; border-radius: 7px; background: transparent; color: var(--muted); font-size: 11px; font-weight: 600; }
|
|
202
|
+
.segmented button:hover:not(:disabled) { border: 0; color: var(--text); background: var(--hover); }
|
|
203
|
+
.segmented button.active { color: #fff; background: linear-gradient(#4f86ff, #3c6ed8); box-shadow: 0 1px 5px #1d4b9f44; }
|
|
204
|
+
.select-field { display: flex; align-items: center; gap: 5px; color: var(--muted); font-size: 10px; white-space: nowrap; }
|
|
205
|
+
.select-field select { height: 30px; font-size: 10px; }
|
|
206
|
+
.play-controls { margin-left: auto; display: none; align-items: center; gap: 4px; }
|
|
207
|
+
.play-controls.visible { display: flex; }
|
|
208
|
+
.play-controls button { height: 30px; padding: 0 8px; font-size: 10px; white-space: nowrap; }
|
|
209
|
+
|
|
210
|
+
.workspace { min-height: 0; display: grid; grid-template-rows: 48px minmax(0, 1fr) 30px; overflow: hidden; background: var(--workspace); }
|
|
211
|
+
.stage-meta { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 0 12px; border-bottom: 1px solid color-mix(in srgb, var(--line) 82%, transparent); background: color-mix(in srgb, var(--workspace) 86%, var(--panel)); }
|
|
212
|
+
.stage-meta > div:first-child { min-width: 0; display: flex; align-items: center; gap: 7px; }
|
|
213
|
+
.stage-meta span { color: var(--muted); font-size: 9px; }
|
|
214
|
+
.stage-meta strong { overflow: hidden; padding: 2px 7px; border: 1px solid var(--line); border-radius: 9px; background: var(--panel); font-size: 10px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
|
|
215
|
+
.stage-readout { display: flex; gap: 10px; white-space: nowrap; }
|
|
216
|
+
|
|
217
|
+
.stage-shell {
|
|
218
|
+
position: relative;
|
|
219
|
+
min-width: 0;
|
|
220
|
+
min-height: 0;
|
|
221
|
+
display: grid;
|
|
222
|
+
place-items: center;
|
|
223
|
+
margin: 0;
|
|
224
|
+
padding: 34px 22px 20px;
|
|
225
|
+
overflow: auto;
|
|
226
|
+
background-color: var(--workspace);
|
|
227
|
+
background-image: radial-gradient(#ffffff0b 1px, transparent 1px), linear-gradient(135deg, #ffffff09 25%, transparent 25%, transparent 75%, #00000008 75%);
|
|
228
|
+
background-size: 4px 4px, 24px 24px;
|
|
229
|
+
}
|
|
230
|
+
:root[data-theme="light"] .stage-shell { background-image: radial-gradient(#0000000c 1px, transparent 1px), linear-gradient(135deg, #ffffff32 25%, transparent 25%, transparent 75%, #00000008 75%); }
|
|
231
|
+
.stage-shell img, .stage-canvas { display: block; max-width: calc(100% - 24px); max-height: calc(100% - 24px); object-fit: contain; border: 1px solid #40b8cf; outline: 1px solid #1f2732b3; box-shadow: 0 12px 24px var(--shadow); user-select: none; }
|
|
232
|
+
.stage-canvas { width: auto; height: auto; touch-action: none; }
|
|
233
|
+
.stage-shell.playing .stage-canvas { cursor: crosshair; }
|
|
234
|
+
.stage-empty { max-width: 360px; display: grid; place-items: center; gap: 7px; padding: 24px; color: var(--muted); text-align: center; }
|
|
235
|
+
.stage-empty > span { color: #77a5ff; font-size: 28px; }
|
|
236
|
+
.stage-empty strong { color: var(--text); font-size: 13px; }
|
|
237
|
+
.stage-empty p { margin: 0; font-size: 10px; line-height: 1.6; }
|
|
238
|
+
.stage-loading { position: absolute; right: 10px; bottom: 10px; padding: 5px 8px; border: 1px solid var(--line); border-radius: 5px; background: var(--panel); color: var(--muted); font-size: 9px; box-shadow: 0 4px 14px var(--shadow); }
|
|
239
|
+
.stage-footer { min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 5px 10px; border-top: 1px solid var(--line); background: var(--status); color: var(--muted); font: 10px/1.4 ui-monospace, SFMono-Regular, Consolas, monospace; }
|
|
240
|
+
.stage-footer span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
241
|
+
|
|
242
|
+
.info-card, .log-card { margin: 6px 7px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel-2); overflow: hidden; }
|
|
243
|
+
.info-card { padding: 9px; }
|
|
244
|
+
.info-label { display: block; margin-bottom: 5px; color: var(--muted); font-size: 9px; }
|
|
245
|
+
.info-card > strong { font-size: 13px; font-weight: 650; }
|
|
246
|
+
.info-card dl { display: grid; grid-template-columns: 66px minmax(0, 1fr); gap: 6px 8px; margin: 9px 0 0; padding-top: 8px; border-top: 1px solid var(--line-soft); font-size: 10px; }
|
|
247
|
+
.info-card dt { color: var(--muted); }
|
|
248
|
+
.info-card dd { margin: 0; overflow: hidden; color: var(--text); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; text-overflow: ellipsis; white-space: nowrap; }
|
|
249
|
+
.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0; padding: 0; text-align: center; }
|
|
250
|
+
.metrics div { min-width: 0; display: grid; gap: 4px; padding: 9px 4px; border-right: 1px solid var(--line-soft); }
|
|
251
|
+
.metrics div:last-child { border-right: 0; }
|
|
252
|
+
.metrics span { color: var(--muted); font-size: 9px; }
|
|
253
|
+
.metrics strong { overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
|
254
|
+
.log-heading { height: 34px; display: flex; align-items: center; justify-content: space-between; padding: 0 9px; border-bottom: 1px solid var(--line-soft); color: var(--text); font-size: 10px; font-weight: 650; }
|
|
255
|
+
.log-heading button { height: 24px; padding: 0 6px; border: 0; background: transparent; color: var(--muted); font-size: 9px; }
|
|
256
|
+
.log-card pre { min-height: 130px; max-height: 34vh; margin: 0; padding: 9px; overflow: auto; color: var(--muted); background: var(--input); white-space: pre-wrap; word-break: break-word; font: 10px/1.55 ui-monospace, SFMono-Regular, Consolas, monospace; }
|
|
257
|
+
.watch-note { margin: 10px 11px 16px; padding: 7px 8px; border-left: 2px solid #477cf0; background: color-mix(in srgb, var(--blue) 8%, var(--panel-2)); color: var(--muted); font-size: 9px; line-height: 1.5; }
|
|
258
|
+
.watch-note span { color: #62d398; }
|
|
259
|
+
|
|
260
|
+
.toast { position: fixed; left: 50%; bottom: 18px; z-index: 20; max-width: min(520px, calc(100vw - 32px)); padding: 8px 11px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel-2); box-shadow: 0 12px 32px var(--shadow); color: var(--text); font-size: 10px; opacity: 0; pointer-events: none; transform: translate(-50%, 8px); transition: 160ms ease; }
|
|
261
|
+
.toast.visible { opacity: 1; transform: translate(-50%, 0); }
|
|
262
|
+
.toast.error { border-color: var(--danger); color: var(--danger); }
|
|
263
|
+
|
|
264
|
+
[hidden] { display: none !important; }
|
|
265
|
+
|
|
266
|
+
@media (max-width: 1180px) {
|
|
267
|
+
.app-shell { grid-template-columns: 200px minmax(300px, 1fr) 292px; }
|
|
268
|
+
.brand span:last-child, .select-field > span { display: none; }
|
|
269
|
+
.play-controls button { padding: 0 6px; }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@media (max-width: 860px) {
|
|
273
|
+
.topbar { grid-template-columns: 170px minmax(250px, 1fr) auto; }
|
|
274
|
+
.app-shell { grid-template-columns: 190px minmax(300px, 1fr); }
|
|
275
|
+
.info-panel { display: none; }
|
|
276
|
+
.connection { display: none; }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@media (max-width: 680px) {
|
|
280
|
+
body { overflow: auto; }
|
|
281
|
+
.topbar { height: auto; grid-template-columns: 1fr auto; padding: 8px; }
|
|
282
|
+
.project-picker { grid-column: 1 / -1; grid-row: 2; }
|
|
283
|
+
.project-picker label { display: none; }
|
|
284
|
+
.project-picker select { width: 100%; }
|
|
285
|
+
.app-shell { height: calc(100vh - 90px); grid-template-columns: minmax(0, 1fr); }
|
|
286
|
+
.tree-panel { display: none; }
|
|
287
|
+
.preview-toolbar { flex-wrap: wrap; height: auto; }
|
|
288
|
+
.preview-panel { grid-template-rows: auto minmax(0, 1fr); }
|
|
289
|
+
.play-controls { width: 100%; margin-left: 0; overflow-x: auto; }
|
|
290
|
+
.stage-meta { align-items: flex-start; flex-direction: column; justify-content: center; gap: 3px; }
|
|
291
|
+
}
|