opencroc 1.6.7 โ†’ 1.6.9

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,150 +1,756 @@
1
1
  <!DOCTYPE html>
2
- <html lang="zh-CN">
2
+ <html lang="zh-CN" data-theme="dark">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>OpenCroc Studio ๐ŸŠ</title>
6
+ <title>OpenCroc Studio</title>
7
7
  <style>
8
+ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
9
+ Design System โ€” CSS Variables (borrowed from OpenClaw)
10
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
8
11
  :root {
9
- --bg-dark: #0a0a1a;
10
- --bg-panel: #12122a;
11
- --bg-card: #1a1a3e;
12
- --accent: #4ecca3;
13
- --accent-dim: #2a8a6a;
14
- --red: #e94560;
15
- --orange: #f39c12;
16
- --blue: #3498db;
17
- --purple: #9b59b6;
18
- --text: #e0e0e0;
19
- --text-dim: #888;
20
- --pixel-border: 2px solid #333;
12
+ --bg: #0a0f1a;
13
+ --bg-panel: #111827;
14
+ --bg-card: #1a2332;
15
+ --bg-hover: #243044;
16
+ --bg-soft: rgba(30, 41, 59, 0.5);
17
+ --accent: #34d399;
18
+ --accent-dim: #059669;
19
+ --accent-bg: rgba(52, 211, 153, 0.12);
20
+ --red: #f87171;
21
+ --red-bg: rgba(248, 113, 113, 0.12);
22
+ --orange: #fbbf24;
23
+ --orange-bg: rgba(251, 191, 36, 0.12);
24
+ --blue: #60a5fa;
25
+ --blue-bg: rgba(96, 165, 250, 0.12);
26
+ --purple: #a78bfa;
27
+ --purple-bg: rgba(167, 139, 250, 0.12);
28
+ --text: #f1f5f9;
29
+ --text-dim: #94a3b8;
30
+ --text-subtle:#64748b;
31
+ --border: rgba(148, 163, 184, 0.15);
32
+ --border-accent: rgba(52, 211, 153, 0.3);
33
+ --shadow-lg: 0 20px 40px rgba(0, 0, 0, 0.4);
34
+ --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.3);
35
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.2);
36
+ --radius-lg: 14px;
37
+ --radius-md: 10px;
38
+ --radius-sm: 6px;
39
+ --transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
21
40
  }
41
+ [data-theme="light"] {
42
+ --bg: #f1f5f9;
43
+ --bg-panel: #ffffff;
44
+ --bg-card: #f8fafc;
45
+ --bg-hover: #e2e8f0;
46
+ --bg-soft: rgba(241, 245, 249, 0.8);
47
+ --accent: #059669;
48
+ --accent-dim: #047857;
49
+ --accent-bg: rgba(5, 150, 105, 0.1);
50
+ --red: #dc2626;
51
+ --red-bg: rgba(220, 38, 38, 0.08);
52
+ --orange: #d97706;
53
+ --orange-bg: rgba(217, 119, 6, 0.08);
54
+ --blue: #2563eb;
55
+ --blue-bg: rgba(37, 99, 235, 0.08);
56
+ --purple: #7c3aed;
57
+ --purple-bg: rgba(124, 58, 237, 0.08);
58
+ --text: #0f172a;
59
+ --text-dim: #475569;
60
+ --text-subtle:#94a3b8;
61
+ --border: rgba(100, 116, 139, 0.2);
62
+ --border-accent: rgba(5, 150, 105, 0.35);
63
+ --shadow-lg: 0 20px 40px rgba(0, 0, 0, 0.08);
64
+ --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.06);
65
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.04);
66
+ }
67
+
68
+ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
69
+ Base Reset & Typography (system font stack)
70
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
22
71
  * { margin:0; padding:0; box-sizing:border-box; }
23
- body { background:var(--bg-dark); color:var(--text); font-family:'Courier New','Consolas',monospace; overflow:hidden; height:100vh; }
24
-
25
- .app { display:grid; grid-template-rows:48px 1fr 220px; grid-template-columns:220px 1fr 280px; height:100vh; }
26
-
27
- .header { grid-column:1/-1; background:var(--bg-panel); border-bottom:var(--pixel-border); display:flex; align-items:center; padding:0 16px; gap:12px; }
28
- .header .logo { font-size:24px; }
29
- .header h1 { font-size:15px; color:var(--accent); }
30
- .header .actions { display:flex; gap:6px; }
31
- .btn { background:var(--accent-dim); color:#fff; border:none; padding:5px 12px; font-family:inherit; font-size:11px; border-radius:3px; cursor:pointer; transition:all .2s; }
32
- .btn:hover { background:var(--accent); transform:translateY(-1px); }
33
- .btn:active { transform:translateY(0); }
34
- .btn:disabled { opacity:.4; cursor:not-allowed; transform:none; }
35
- .btn.danger { background:#8b2035; }
36
- .btn.danger:hover { background:var(--red); }
37
- .mode-select { background:var(--bg-card); color:var(--text); border:1px solid #333; border-radius:3px; font-family:inherit; font-size:10px; padding:4px 6px; }
38
- .header .stats { margin-left:auto; display:flex; gap:14px; font-size:11px; color:var(--text-dim); }
39
- .header .stats span { color:var(--accent); font-weight:bold; }
40
- .conn-dot { width:8px; height:8px; border-radius:50%; background:var(--red); transition:background .3s; }
41
- .conn-dot.on { background:var(--accent); }
42
-
43
- .sidebar { background:var(--bg-panel); border-right:var(--pixel-border); overflow-y:auto; padding:8px; }
44
- .sidebar h3 { font-size:10px; text-transform:uppercase; color:var(--text-dim); padding:8px 4px 4px; letter-spacing:1px; }
45
- .mod-item { padding:5px 8px; border-radius:3px; font-size:11px; cursor:pointer; display:flex; align-items:center; gap:6px; transition:background .15s; }
46
- .mod-item:hover { background:var(--bg-card); }
47
- .dot { width:6px; height:6px; border-radius:50%; flex-shrink:0; }
48
- .dot.idle { background:var(--text-dim); }
49
- .dot.testing,.dot.working { background:var(--orange); animation:blink .7s infinite; }
50
- .dot.thinking { background:var(--blue); animation:blink 1s infinite; }
51
- .dot.passed,.dot.done { background:var(--accent); }
52
- .dot.failed,.dot.error { background:var(--red); }
53
- @keyframes blink { 50%{opacity:.3} }
54
-
55
- .main { position:relative; overflow:hidden; }
72
+ body {
73
+ background: var(--bg);
74
+ color: var(--text);
75
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans SC", "PingFang SC", sans-serif;
76
+ overflow: hidden;
77
+ height: 100vh;
78
+ line-height: 1.5;
79
+ -webkit-font-smoothing: antialiased;
80
+ }
81
+
82
+ /* Scrollbar styling */
83
+ ::-webkit-scrollbar { width: 5px; height: 5px; }
84
+ ::-webkit-scrollbar-track { background: transparent; }
85
+ ::-webkit-scrollbar-thumb { background: var(--text-subtle); border-radius: 4px; }
86
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
87
+
88
+ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
89
+ Layout Grid โ€” Responsive
90
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
91
+ .app {
92
+ display: grid;
93
+ grid-template-rows: 56px 1fr 220px;
94
+ grid-template-columns: 240px 1fr 320px;
95
+ grid-template-areas:
96
+ "header header header"
97
+ "sidebar main panel"
98
+ "office office office";
99
+ height: 100vh;
100
+ gap: 6px;
101
+ padding: 6px;
102
+ }
103
+ @media (max-width: 1200px) {
104
+ .app {
105
+ grid-template-columns: 200px 1fr 280px;
106
+ }
107
+ }
108
+ @media (max-width: 960px) {
109
+ .app {
110
+ grid-template-columns: 1fr;
111
+ grid-template-rows: 56px 1fr 200px;
112
+ grid-template-areas:
113
+ "header"
114
+ "main"
115
+ "office";
116
+ }
117
+ .sidebar, .log-panel { display: none; }
118
+ .sidebar.mobile-open, .log-panel.mobile-open {
119
+ display: flex;
120
+ position: fixed;
121
+ top: 62px; bottom: 0;
122
+ width: 280px;
123
+ z-index: 150;
124
+ }
125
+ .sidebar.mobile-open { left: 0; }
126
+ .log-panel.mobile-open { right: 0; }
127
+ }
128
+
129
+ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
130
+ Header
131
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
132
+ .header {
133
+ grid-area: header;
134
+ background: var(--bg-panel);
135
+ border: 1px solid var(--border);
136
+ border-radius: var(--radius-lg);
137
+ display: flex;
138
+ align-items: center;
139
+ padding: 0 14px;
140
+ gap: 10px;
141
+ box-shadow: var(--shadow-sm);
142
+ }
143
+ .header .logo {
144
+ width: 36px; height: 36px;
145
+ border-radius: var(--radius-md);
146
+ display: grid; place-items: center;
147
+ background: var(--accent-bg);
148
+ border: 1px solid var(--border-accent);
149
+ flex-shrink: 0;
150
+ }
151
+ .header .logo svg { width: 22px; height: 22px; }
152
+ .header .title-wrap { display:flex; flex-direction:column; gap:1px; min-width:0; }
153
+ .header h1 { font-size:14px; font-weight:700; color:var(--accent); letter-spacing:.3px; white-space:nowrap; }
154
+ .header .subtitle { font-size:10px; color:var(--text-subtle); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
155
+ .header .actions { display:flex; gap:5px; flex-shrink:0; }
156
+ .header .spacer { flex:1; }
157
+
158
+ /* View Switch */
159
+ .view-switch {
160
+ display: flex;
161
+ background: var(--bg-card);
162
+ border: 1px solid var(--border);
163
+ border-radius: var(--radius-md);
164
+ padding: 2px;
165
+ margin-right: 2px;
166
+ }
167
+ .view-switch button {
168
+ background: transparent;
169
+ color: var(--text-dim);
170
+ border: none;
171
+ font-family: inherit;
172
+ font-size: 11px;
173
+ padding: 5px 12px;
174
+ border-radius: var(--radius-sm);
175
+ cursor: pointer;
176
+ transition: all var(--transition);
177
+ display: flex; align-items: center; gap: 4px;
178
+ }
179
+ .view-switch button:hover { color: var(--text); background: var(--bg-hover); }
180
+ .view-switch button.active {
181
+ background: var(--accent-bg);
182
+ color: var(--accent);
183
+ box-shadow: 0 0 0 1px var(--border-accent);
184
+ }
185
+
186
+ /* Buttons */
187
+ .btn {
188
+ background: var(--accent-dim);
189
+ color: #fff;
190
+ border: 1px solid var(--border-accent);
191
+ padding: 5px 12px;
192
+ font-family: inherit;
193
+ font-size: 11px;
194
+ font-weight: 500;
195
+ border-radius: var(--radius-sm);
196
+ cursor: pointer;
197
+ transition: all var(--transition);
198
+ display: flex; align-items: center; gap: 4px;
199
+ white-space: nowrap;
200
+ }
201
+ .btn:hover { background: var(--accent); transform: translateY(-1px); box-shadow: var(--shadow-sm); }
202
+ .btn:active { transform: translateY(0); }
203
+ .btn:disabled { opacity: .35; cursor: not-allowed; transform: none; }
204
+ .btn.danger { background: #991b1b; border-color: rgba(248,113,113,.3); }
205
+ .btn.danger:hover { background: #b91c1c; }
206
+ .btn svg { width: 14px; height: 14px; }
207
+ .mode-select {
208
+ background: var(--bg-card);
209
+ color: var(--text);
210
+ border: 1px solid var(--border);
211
+ border-radius: var(--radius-sm);
212
+ font-family: inherit;
213
+ font-size: 11px;
214
+ padding: 5px 8px;
215
+ min-width: 80px;
216
+ cursor: pointer;
217
+ }
218
+
219
+ /* Stats */
220
+ .header .stats { display:flex; gap:6px; font-size:10px; color:var(--text-dim); flex-shrink:0; }
221
+ .header .stats > div {
222
+ background: var(--bg-card);
223
+ border: 1px solid var(--border);
224
+ border-radius: var(--radius-sm);
225
+ padding: 4px 8px;
226
+ min-width: 60px;
227
+ text-align: center;
228
+ }
229
+ .header .stats .label { font-size:9px; color:var(--text-subtle); text-transform:uppercase; letter-spacing:.5px; }
230
+ .header .stats .value { display:block; color:var(--accent); font-weight:700; font-size:13px; margin-top:1px; }
231
+ .conn-dot {
232
+ width: 10px; height: 10px;
233
+ border-radius: 50%;
234
+ background: var(--red);
235
+ box-shadow: 0 0 0 3px var(--red-bg);
236
+ transition: all .3s;
237
+ flex-shrink: 0;
238
+ }
239
+ .conn-dot.on { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg); animation: conn-pulse 2s infinite; }
240
+ @keyframes conn-pulse { 0%,100%{ box-shadow: 0 0 0 3px var(--accent-bg) } 50%{ box-shadow: 0 0 0 6px var(--accent-bg) } }
241
+
242
+ /* Theme toggle */
243
+ .theme-toggle {
244
+ background: none; border: 1px solid var(--border); color: var(--text-dim);
245
+ width: 32px; height: 32px; border-radius: var(--radius-sm);
246
+ display: grid; place-items: center; cursor: pointer;
247
+ transition: all var(--transition);
248
+ }
249
+ .theme-toggle:hover { background: var(--bg-hover); color: var(--text); }
250
+ .theme-toggle svg { width: 16px; height: 16px; }
251
+
252
+ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
253
+ Sidebar
254
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
255
+ .sidebar {
256
+ grid-area: sidebar;
257
+ background: var(--bg-panel);
258
+ border: 1px solid var(--border);
259
+ border-radius: var(--radius-lg);
260
+ overflow-y: auto;
261
+ padding: 10px;
262
+ box-shadow: var(--shadow-sm);
263
+ }
264
+ .sidebar h3 {
265
+ font-size: 10px; text-transform: uppercase; color: var(--text-subtle);
266
+ padding: 10px 4px 6px; letter-spacing: 1.2px; font-weight: 600;
267
+ display: flex; align-items: center; gap: 6px;
268
+ }
269
+ .sidebar h3 svg { width: 14px; height: 14px; opacity: 0.6; }
270
+ .mod-item {
271
+ padding: 7px 10px;
272
+ border-radius: var(--radius-sm);
273
+ font-size: 12px;
274
+ cursor: pointer;
275
+ display: flex;
276
+ align-items: center;
277
+ gap: 8px;
278
+ transition: all var(--transition);
279
+ margin-bottom: 2px;
280
+ border: 1px solid transparent;
281
+ color: var(--text-dim);
282
+ }
283
+ .mod-item:hover {
284
+ background: var(--bg-hover);
285
+ border-color: var(--border);
286
+ color: var(--text);
287
+ transform: translateX(2px);
288
+ }
289
+ .mod-count {
290
+ margin-left: auto; font-size: 9px; color: var(--text-subtle);
291
+ background: var(--bg-card); padding: 1px 6px; border-radius: 8px;
292
+ }
293
+ .dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; transition: background .3s; }
294
+ .dot.idle { background: var(--text-subtle); }
295
+ .dot.testing, .dot.working { background: var(--orange); animation: dot-pulse 1s infinite; }
296
+ .dot.thinking { background: var(--blue); animation: dot-pulse 1.4s infinite; }
297
+ .dot.passed, .dot.done { background: var(--accent); }
298
+ .dot.failed, .dot.error { background: var(--red); animation: dot-shake .3s infinite; }
299
+ @keyframes dot-pulse { 0%,100%{ opacity:1; transform:scale(1) } 50%{ opacity:.5; transform:scale(.85) } }
300
+ @keyframes dot-shake { 0%,100%{ transform:translateX(0) } 25%{ transform:translateX(-1px) } 75%{ transform:translateX(1px) } }
301
+
302
+ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
303
+ Main Content
304
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
305
+ .main {
306
+ grid-area: main;
307
+ position: relative;
308
+ overflow: hidden;
309
+ background: var(--bg-panel);
310
+ border: 1px solid var(--border);
311
+ border-radius: var(--radius-lg);
312
+ box-shadow: var(--shadow-md);
313
+ }
56
314
  #graph-canvas { width:100%; height:100%; display:block; cursor:grab; }
57
- .tooltip { position:absolute; background:var(--bg-card); border:1px solid var(--accent-dim); border-radius:4px; padding:8px 12px; font-size:11px; pointer-events:none; z-index:100; display:none; max-width:280px; }
315
+ .view { position:absolute; inset:0; }
316
+ .view.hidden { display:none; }
317
+
318
+ /* Pixel Office Stage */
319
+ .pixel-stage {
320
+ position: relative; width:100%; height:100%; overflow:hidden;
321
+ background:
322
+ linear-gradient(rgba(8,12,20,.15), rgba(8,12,20,.4)),
323
+ url('./assets/star/office_bg_small.webp') center/cover no-repeat;
324
+ }
325
+ .pixel-stage::before {
326
+ content:''; position:absolute; inset:0;
327
+ background-image:
328
+ linear-gradient(rgba(148,163,184,.08) 1px, transparent 1px),
329
+ linear-gradient(90deg, rgba(148,163,184,.08) 1px, transparent 1px);
330
+ background-size: 32px 32px;
331
+ pointer-events:none;
332
+ }
333
+ .pixel-stage .asset {
334
+ position:absolute; image-rendering:pixelated; z-index:2;
335
+ filter: drop-shadow(0 8px 16px rgba(0,0,0,.4));
336
+ transition: opacity .3s;
337
+ }
338
+ .pixel-stage .desk-asset { width:180px; bottom:90px; left:40px; }
339
+ .pixel-stage .server-asset { width:110px; top:24px; right:34px; }
340
+ .pixel-stage .coffee-asset { width:72px; top:100px; right:180px; }
341
+ .pixel-stage .walls-asset { width:220px; top:10px; left:46%; transform:translateX(-50%); opacity:.4; }
342
+ .pixel-agent-layer { position:absolute; inset:0; z-index:3; }
343
+ .pixel-agent {
344
+ position:absolute; width:44px;
345
+ transition: left .6s cubic-bezier(.4,0,.2,1), top .6s cubic-bezier(.4,0,.2,1);
346
+ image-rendering:pixelated;
347
+ filter: drop-shadow(0 6px 10px rgba(0,0,0,.4));
348
+ }
349
+ /* Sprite-canvas replaces static img for animated agents */
350
+ .pixel-sprite-canvas {
351
+ position:absolute; width:48px; height:48px;
352
+ image-rendering:pixelated;
353
+ transition: left .6s cubic-bezier(.4,0,.2,1), top .6s cubic-bezier(.4,0,.2,1);
354
+ filter: drop-shadow(0 6px 10px rgba(0,0,0,.4));
355
+ }
356
+ .pixel-agent.working,.pixel-agent.testing { animation: agent-bob .5s ease-in-out infinite alternate; }
357
+ .pixel-agent.thinking { animation: agent-think 1.2s ease-in-out infinite; }
358
+ .pixel-agent.error,.pixel-agent.failed { animation: agent-shake .25s ease-in-out infinite; }
359
+ .pixel-agent.done,.pixel-agent.passed { animation: agent-pulse .9s ease-in-out infinite; }
360
+ .pixel-sprite-canvas.working,.pixel-sprite-canvas.testing { animation: agent-bob .5s ease-in-out infinite alternate; }
361
+ .pixel-sprite-canvas.thinking { animation: agent-think 1.2s ease-in-out infinite; }
362
+ .pixel-sprite-canvas.error,.pixel-sprite-canvas.failed { animation: agent-shake .25s ease-in-out infinite; }
363
+ .pixel-sprite-canvas.done,.pixel-sprite-canvas.passed { animation: agent-pulse .9s ease-in-out infinite; }
364
+ @keyframes agent-bob { from{transform:translateY(0)} to{transform:translateY(-4px)} }
365
+ @keyframes agent-think { 0%,100%{transform:rotate(0) scale(1)} 50%{transform:rotate(-3deg) scale(1.02)} }
366
+ @keyframes agent-shake { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-3px)} 75%{transform:translateX(3px)} }
367
+ @keyframes agent-pulse { 0%,100%{transform:scale(1)} 50%{transform:scale(1.08)} }
368
+
369
+ /* Pixel Agent Label โ€” enhanced with status bubble */
370
+ .pixel-label {
371
+ position:absolute; transform:translate(-50%,-50%);
372
+ background: var(--bg-panel);
373
+ border: 1px solid var(--border-accent);
374
+ border-radius: var(--radius-sm);
375
+ padding: 3px 8px;
376
+ font-size: 10px;
377
+ color: var(--text);
378
+ white-space:nowrap;
379
+ z-index:4;
380
+ box-shadow: var(--shadow-sm);
381
+ backdrop-filter: blur(4px);
382
+ }
383
+ .pixel-label .status-tag {
384
+ display:inline-block; font-size:8px; padding:1px 4px; border-radius:4px; margin-left:4px;
385
+ font-weight:600; text-transform:uppercase; letter-spacing:.3px;
386
+ }
387
+ .pixel-label .status-tag.working { background:var(--orange-bg); color:var(--orange); }
388
+ .pixel-label .status-tag.thinking { background:var(--blue-bg); color:var(--blue); }
389
+ .pixel-label .status-tag.done { background:var(--accent-bg); color:var(--accent); }
390
+ .pixel-label .status-tag.error { background:var(--red-bg); color:var(--red); }
391
+
392
+ /* Pixel KPIs */
393
+ .pixel-kpis { position:absolute; left:14px; top:14px; display:flex; gap:6px; z-index:4; }
394
+ .pixel-kpi {
395
+ min-width:80px;
396
+ background: var(--bg-panel);
397
+ border: 1px solid var(--border);
398
+ border-radius: var(--radius-md);
399
+ padding: 8px 10px;
400
+ box-shadow: var(--shadow-md);
401
+ backdrop-filter: blur(8px);
402
+ }
403
+ .pixel-kpi .t { font-size:9px; color:var(--text-subtle); text-transform:uppercase; letter-spacing:.5px; }
404
+ .pixel-kpi .v { font-size:16px; color:var(--accent); font-weight:700; margin-top:2px; }
405
+ .pixel-kpi.errors .v { color:var(--red); }
406
+ .pixel-kpi.done .v { color:var(--accent); }
407
+
408
+ /* Bubble messages (Star-Office style) */
409
+ .pixel-bubble {
410
+ position:absolute; z-index:5; pointer-events:none;
411
+ background: var(--bg-panel);
412
+ border: 1px solid var(--border-accent);
413
+ border-radius: var(--radius-sm) var(--radius-sm) var(--radius-sm) 2px;
414
+ padding: 4px 10px;
415
+ font-size: 10px;
416
+ color: var(--text);
417
+ box-shadow: var(--shadow-sm);
418
+ animation: bubble-in .3s ease-out, bubble-out .3s ease-in 2.7s forwards;
419
+ white-space: nowrap;
420
+ }
421
+ @keyframes bubble-in { from{opacity:0;transform:translateY(6px) scale(.9)} to{opacity:1;transform:translateY(0) scale(1)} }
422
+ @keyframes bubble-out { from{opacity:1} to{opacity:0;transform:translateY(-4px)} }
423
+
424
+ /* Tooltip */
425
+ .tooltip {
426
+ position:absolute;
427
+ background: var(--bg-panel);
428
+ border: 1px solid var(--border-accent);
429
+ border-radius: var(--radius-md);
430
+ padding: 10px 14px;
431
+ font-size: 12px;
432
+ pointer-events:none;
433
+ z-index:100;
434
+ display:none;
435
+ max-width:300px;
436
+ box-shadow: var(--shadow-lg);
437
+ line-height: 1.6;
438
+ }
58
439
  .tooltip.visible { display:block; }
440
+ .tooltip b { color: var(--accent); }
59
441
 
60
- .log-panel { background:var(--bg-panel); border-left:var(--pixel-border); display:flex; flex-direction:column; overflow:hidden; }
61
- .panel-tabs { display:flex; border-bottom:1px solid #222; }
62
- .panel-tabs .tab { background:none; border:none; color:var(--text-dim); font-family:inherit; font-size:10px; padding:8px 12px; cursor:pointer; text-transform:uppercase; letter-spacing:1px; border-bottom:2px solid transparent; }
63
- .panel-tabs .tab.active { color:var(--accent); border-bottom-color:var(--accent); }
64
- .panel-tabs .tab:hover { color:var(--text); }
65
- .file-list { flex:1; overflow-y:auto; padding:4px 8px; font-size:10px; }
66
- .file-item { padding:6px 8px; border-bottom:1px solid #1a1a2e; cursor:pointer; border-radius:3px; }
67
- .file-item:hover { background:var(--bg-card); }
68
- .file-item .fname { color:var(--accent); font-weight:bold; }
69
- .file-item .fmeta { color:var(--text-dim); font-size:9px; margin-top:2px; }
70
- .file-preview { position:fixed; top:60px; left:50%; transform:translateX(-50%); width:700px; max-height:80vh; background:var(--bg-panel); border:2px solid var(--accent-dim); border-radius:8px; z-index:200; display:none; flex-direction:column; }
71
- .file-preview.visible { display:flex; }
72
- .file-preview .fp-header { display:flex; justify-content:space-between; align-items:center; padding:10px 14px; border-bottom:1px solid #333; }
73
- .file-preview .fp-header h4 { font-size:12px; color:var(--accent); margin:0; }
74
- .file-preview .fp-close { background:none; border:none; color:var(--text-dim); font-size:18px; cursor:pointer; }
75
- .file-preview pre { flex:1; overflow:auto; padding:12px; margin:0; font-size:11px; line-height:1.5; color:var(--text); background:#0d0d20; }
76
- .log-list { flex:1; overflow-y:auto; padding:4px 8px; font-size:10px; line-height:1.6; }
77
- .log-list .log-entry { padding:2px 0; border-bottom:1px solid #1a1a2e; word-break:break-all; }
78
- .log-list .log-entry.warn { color:var(--orange); }
79
- .log-list .log-entry.error { color:var(--red); }
80
-
81
- .office { grid-column:1/-1; background:var(--bg-panel); border-top:var(--pixel-border); display:flex; overflow-x:auto; padding:8px; gap:8px; }
82
- .desk { flex:0 0 180px; background:var(--bg-card); border:1px solid #333; border-radius:4px; padding:8px; display:flex; flex-direction:column; align-items:center; gap:2px; position:relative; overflow:hidden; }
83
- .desk .badge { position:absolute; top:4px; right:4px; width:8px; height:8px; border-radius:50%; }
84
- .desk .croc-sprite { font-size:40px; position:relative; z-index:1; }
85
- .desk.idle .croc-sprite { animation:croc-idle 3s infinite; }
86
- .desk.working .croc-sprite { animation:croc-work .5s infinite alternate; }
87
- .desk.thinking .croc-sprite { animation:croc-think 1.2s infinite; }
88
- .desk.done .croc-sprite { animation:croc-done .8s 1; }
89
- .desk.error .croc-sprite { animation:croc-error .3s 3; }
442
+ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
443
+ Right Panel (Log/Tests/Results/Reports)
444
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
445
+ .log-panel {
446
+ grid-area: panel;
447
+ background: var(--bg-panel);
448
+ border: 1px solid var(--border);
449
+ border-radius: var(--radius-lg);
450
+ display: flex; flex-direction: column;
451
+ overflow: hidden;
452
+ box-shadow: var(--shadow-sm);
453
+ }
454
+ .panel-tabs {
455
+ display: flex;
456
+ border-bottom: 1px solid var(--border);
457
+ background: var(--bg-card);
458
+ flex-shrink: 0;
459
+ }
460
+ .panel-tabs .tab {
461
+ background: none; border: none;
462
+ color: var(--text-dim);
463
+ font-family: inherit;
464
+ font-size: 11px;
465
+ padding: 10px 12px;
466
+ cursor: pointer;
467
+ border-bottom: 2px solid transparent;
468
+ transition: all var(--transition);
469
+ display: flex; align-items: center; gap: 4px;
470
+ }
471
+ .panel-tabs .tab:hover { color: var(--text); background: var(--bg-hover); }
472
+ .panel-tabs .tab.active { color: var(--accent); border-bottom-color: var(--accent); }
473
+ .panel-tabs .tab svg { width: 14px; height: 14px; }
474
+ .tab-badge {
475
+ display:inline-block; background:var(--accent); color:var(--bg);
476
+ border-radius:8px; padding:0 5px; font-size:9px; font-weight:700;
477
+ }
478
+ .tab-badge.alert { background:var(--red); color:#fff; }
479
+
480
+ .file-list { flex:1; overflow-y:auto; padding:6px 8px; font-size:11px; }
481
+ .file-item {
482
+ padding: 8px 10px;
483
+ border-bottom: 1px solid var(--border);
484
+ cursor: pointer;
485
+ border-radius: var(--radius-sm);
486
+ transition: all var(--transition);
487
+ margin-bottom:2px;
488
+ }
489
+ .file-item:hover { background: var(--bg-hover); }
490
+ .file-item .fname { color: var(--accent); font-weight: 600; font-size:12px; }
491
+ .file-item .fmeta { color: var(--text-dim); font-size: 10px; margin-top: 3px; }
492
+ .file-preview {
493
+ position: fixed; top: 50%; left: 50%;
494
+ transform: translate(-50%, -50%);
495
+ width: min(800px, 90vw);
496
+ max-height: 80vh;
497
+ background: var(--bg-panel);
498
+ border: 1px solid var(--border-accent);
499
+ border-radius: var(--radius-lg);
500
+ z-index: 200;
501
+ display: none; flex-direction: column;
502
+ box-shadow: var(--shadow-lg);
503
+ }
504
+ .file-preview.visible { display: flex; }
505
+ .file-preview .fp-header {
506
+ display: flex; justify-content: space-between; align-items: center;
507
+ padding: 12px 16px;
508
+ border-bottom: 1px solid var(--border);
509
+ }
510
+ .file-preview .fp-header h4 { font-size: 13px; color: var(--accent); font-weight: 600; }
511
+ .file-preview .fp-close {
512
+ background: none; border: none; color: var(--text-dim);
513
+ font-size: 20px; cursor: pointer; padding: 4px 8px; border-radius: var(--radius-sm);
514
+ transition: all var(--transition);
515
+ }
516
+ .file-preview .fp-close:hover { background: var(--bg-hover); color: var(--text); }
517
+ .file-preview pre {
518
+ flex:1; overflow:auto; padding:16px; margin:0;
519
+ font-size: 12px; line-height: 1.6;
520
+ font-family: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, monospace;
521
+ color: var(--text); background: var(--bg);
522
+ border-radius: 0 0 var(--radius-lg) var(--radius-lg);
523
+ }
524
+ .log-list { flex:1; overflow-y:auto; padding:6px 10px; font-size:11px; line-height:1.7; }
525
+ .log-list .log-entry {
526
+ padding: 4px 0;
527
+ border-bottom: 1px solid var(--border);
528
+ word-break: break-word;
529
+ font-family: "Cascadia Code", "Fira Code", Consolas, monospace;
530
+ font-size: 10px;
531
+ }
532
+ .log-list .log-entry .timestamp { color: var(--text-subtle); margin-right: 6px; }
533
+ .log-list .log-entry.warn { color: var(--orange); }
534
+ .log-list .log-entry.error { color: var(--red); }
535
+ .log-list .log-entry.info { color: var(--text-dim); }
536
+
537
+ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
538
+ Bottom: Croc Office (Agent Desks)
539
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
540
+ .office {
541
+ grid-area: office;
542
+ background: var(--bg-panel);
543
+ border: 1px solid var(--border);
544
+ border-radius: var(--radius-lg);
545
+ display: flex;
546
+ overflow-x: auto;
547
+ padding: 10px;
548
+ gap: 8px;
549
+ box-shadow: var(--shadow-sm);
550
+ }
551
+ .desk {
552
+ flex: 0 0 180px;
553
+ background: var(--bg-card);
554
+ border: 1px solid var(--border);
555
+ border-radius: var(--radius-md);
556
+ padding: 12px 10px;
557
+ display: flex;
558
+ flex-direction: column;
559
+ align-items: center;
560
+ gap: 4px;
561
+ position: relative;
562
+ overflow: hidden;
563
+ transition: all var(--transition);
564
+ box-shadow: var(--shadow-sm);
565
+ }
566
+ .desk:hover { border-color: var(--border-accent); transform: translateY(-2px); box-shadow: var(--shadow-md); }
567
+ .desk .badge { position:absolute; top:8px; right:8px; width:8px; height:8px; border-radius:50%; }
568
+
569
+ /* SVG Croc sprites replace emoji */
570
+ .desk .croc-sprite {
571
+ width: 48px; height: 48px;
572
+ position: relative; z-index: 1;
573
+ display: grid; place-items: center;
574
+ }
575
+ .desk .croc-sprite svg { width:44px; height:44px; }
576
+ .desk.idle .croc-sprite { animation: croc-idle 3s ease-in-out infinite; }
577
+ .desk.working .croc-sprite, .desk.testing .croc-sprite { animation: croc-work .5s ease-in-out infinite alternate; }
578
+ .desk.thinking .croc-sprite { animation: croc-think 1.2s ease-in-out infinite; }
579
+ .desk.done .croc-sprite, .desk.passed .croc-sprite { animation: croc-done .8s ease-out 1; }
580
+ .desk.error .croc-sprite, .desk.failed .croc-sprite { animation: croc-error .3s ease-in-out 3; }
90
581
  @keyframes croc-idle { 0%,90%,100%{transform:translateY(0)} 95%{transform:translateY(-3px)} }
91
- @keyframes croc-work { from{transform:translateY(0) rotate(-3deg)} to{transform:translateY(-5px) rotate(3deg)} }
92
- @keyframes croc-think { 0%,100%{transform:scale(1) rotate(0)} 25%{transform:scale(1.05) rotate(-2deg)} 75%{transform:scale(1.05) rotate(2deg)} }
93
- @keyframes croc-done { 0%{transform:scale(1)} 50%{transform:scale(1.2) translateY(-8px)} 100%{transform:scale(1)} }
94
- @keyframes croc-error { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-4px)} 75%{transform:translateX(4px)} }
95
- .desk .croc-name { font-size:11px; font-weight:bold; color:var(--accent); }
96
- .desk .croc-role { font-size:9px; color:var(--text-dim); text-transform:uppercase; }
97
- .desk .croc-task { font-size:9px; color:var(--orange); text-align:center; max-width:160px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-height:13px; }
98
- .desk .progress-bar { width:90%; height:3px; background:#222; border-radius:2px; margin-top:2px; overflow:hidden; }
99
- .desk .progress-bar .fill { height:100%; background:var(--accent); transition:width .3s; border-radius:2px; }
100
- .desk .desk-items { position:absolute; bottom:4px; right:8px; font-size:14px; opacity:.4; }
582
+ @keyframes croc-work { from{transform:translateY(0) rotate(-2deg)} to{transform:translateY(-5px) rotate(2deg)} }
583
+ @keyframes croc-think { 0%,100%{transform:scale(1) rotate(0)} 25%{transform:scale(1.04) rotate(-2deg)} 75%{transform:scale(1.04) rotate(2deg)} }
584
+ @keyframes croc-done { 0%{transform:scale(1)} 50%{transform:scale(1.15) translateY(-6px)} 100%{transform:scale(1)} }
585
+ @keyframes croc-error { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-3px)} 75%{transform:translateX(3px)} }
586
+ .desk .croc-name { font-size: 12px; font-weight: 600; color: var(--accent); }
587
+ .desk .croc-role {
588
+ font-size: 9px; color: var(--text-subtle); text-transform: uppercase;
589
+ letter-spacing: .5px; font-weight: 500;
590
+ }
591
+ .desk .croc-task {
592
+ font-size: 10px; color: var(--orange); text-align: center;
593
+ max-width: 160px; overflow: hidden; text-overflow: ellipsis;
594
+ white-space: nowrap; min-height: 14px;
595
+ }
596
+ .desk .progress-bar {
597
+ width: 90%; height: 3px; background: var(--bg-hover);
598
+ border-radius: 2px; margin-top: 3px; overflow: hidden;
599
+ }
600
+ .desk .progress-bar .fill {
601
+ height: 100%; background: linear-gradient(90deg, var(--accent-dim), var(--accent));
602
+ transition: width .4s cubic-bezier(.4,0,.2,1); border-radius: 2px;
603
+ }
604
+ .desk .desk-icon {
605
+ position: absolute; bottom: 6px; right: 8px; opacity: .25;
606
+ }
607
+ .desk .desk-icon svg { width: 16px; height: 16px; }
608
+
609
+ /* Loading skeleton */
610
+ .skeleton { background: linear-gradient(90deg, var(--bg-card) 25%, var(--bg-hover) 50%, var(--bg-card) 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; border-radius: var(--radius-sm); }
611
+ @keyframes shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
612
+
613
+ /* Typewriter cursor */
614
+ .typewriter-cursor {
615
+ display:inline-block; width:1px; height:1em; background:var(--accent);
616
+ margin-left:2px; vertical-align:text-bottom;
617
+ animation: cursor-blink .6s steps(1) infinite;
618
+ }
619
+ @keyframes cursor-blink { 0%,100%{opacity:1} 50%{opacity:0} }
620
+
621
+ /* Keyboard shortcuts legend */
622
+ .shortcut-legend {
623
+ position:fixed; bottom:12px; right:12px; z-index:90;
624
+ background:var(--bg-panel); border:1px solid var(--border);
625
+ border-radius:var(--radius-md); padding:10px 14px;
626
+ font-size:10px; color:var(--text-dim);
627
+ box-shadow:var(--shadow-md); opacity:0; pointer-events:none;
628
+ transition:opacity .2s;
629
+ display:grid; grid-template-columns:auto auto; gap:3px 12px;
630
+ }
631
+ .shortcut-legend.visible { opacity:1; pointer-events:auto; }
632
+ .shortcut-legend kbd {
633
+ display:inline-block; min-width:18px; text-align:center;
634
+ background:var(--bg-card); border:1px solid var(--border);
635
+ border-radius:3px; padding:1px 5px; font-family:inherit;
636
+ font-size:9px; font-weight:600; color:var(--text);
637
+ }
101
638
  </style>
102
639
  </head>
103
640
  <body>
104
641
  <div class="app">
105
642
  <header class="header">
106
- <div class="logo">๐ŸŠ</div>
107
- <h1>OpenCroc Studio</h1>
643
+ <div class="logo">
644
+ <svg viewBox="0 0 16 16" fill="none"><rect x="4" y="2" width="8" height="3" rx="1" fill="var(--accent)"/><rect x="3" y="5" width="10" height="6" rx="2" fill="var(--accent)" opacity=".8"/><rect x="5" y="11" width="2" height="2" fill="var(--accent)" opacity=".6"/><rect x="9" y="11" width="2" height="2" fill="var(--accent)" opacity=".6"/><rect x="5" y="6" width="2" height="2" rx="1" fill="var(--bg)"/><rect x="9" y="6" width="2" height="2" rx="1" fill="var(--bg)"/><rect x="6" y="9" width="4" height="1" fill="var(--bg)" opacity=".5"/></svg>
645
+ </div>
646
+ <div class="title-wrap">
647
+ <h1>OpenCroc Studio</h1>
648
+ <div class="subtitle">Pixel Ops Dashboard ยท Real-time Multi-Agent Runtime</div>
649
+ </div>
108
650
  <div class="actions">
109
- <button class="btn" id="btn-scan" title="Scan project">๐Ÿ” Scan</button>
110
- <button class="btn" id="btn-pipeline" title="Run full pipeline">โ–ถ Pipeline</button>
651
+ <div class="view-switch">
652
+ <button id="view-dashboard" class="active">
653
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><rect x="1" y="1" width="6" height="6" rx="1"/><rect x="9" y="1" width="6" height="3" rx="1"/><rect x="9" y="6" width="6" height="6" rx="1" opacity=".6"/><rect x="1" y="9" width="6" height="3" rx="1" opacity=".6"/></svg>
654
+ Dashboard
655
+ </button>
656
+ <button id="view-office">
657
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><rect x="2" y="8" width="12" height="6" rx="1"/><rect x="5" y="3" width="6" height="5" rx="1" opacity=".6"/><rect x="4" y="14" width="2" height="1"/><rect x="10" y="14" width="2" height="1"/></svg>
658
+ Pixel Office
659
+ </button>
660
+ </div>
661
+ <button class="btn" id="btn-scan" title="Scan project">
662
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="7" cy="7" r="4"/><line x1="10" y1="10" x2="14" y2="14"/></svg>
663
+ Scan
664
+ </button>
665
+ <button class="btn" id="btn-pipeline" title="Run full pipeline">
666
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><polygon points="4,2 14,8 4,14"/></svg>
667
+ Pipeline
668
+ </button>
111
669
  <select id="run-mode" class="mode-select" title="Test run mode">
112
670
  <option value="auto">Auto</option>
113
671
  <option value="reuse">Reuse</option>
114
672
  <option value="managed">Managed</option>
115
673
  </select>
116
- <button class="btn" id="btn-run-tests" title="Run generated tests" disabled>๐Ÿงช Run Tests</button>
117
- <button class="btn" id="btn-reports" title="Generate reports" disabled>๐Ÿ“Š Reports</button>
118
- <button class="btn danger" id="btn-reset" title="Reset agents">โน Reset</button>
674
+ <button class="btn" id="btn-run-tests" title="Run generated tests" disabled>
675
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 8l3 3 7-7"/></svg>
676
+ Tests
677
+ </button>
678
+ <button class="btn" id="btn-reports" title="Generate reports" disabled>
679
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="1" width="12" height="14" rx="2"/><line x1="5" y1="5" x2="11" y2="5"/><line x1="5" y1="8" x2="11" y2="8"/><line x1="5" y1="11" x2="9" y2="11"/></svg>
680
+ Reports
681
+ </button>
682
+ <button class="btn danger" id="btn-reset" title="Reset agents">
683
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><rect x="3" y="3" width="10" height="10" rx="2"/></svg>
684
+ Reset
685
+ </button>
119
686
  </div>
687
+ <div class="spacer"></div>
120
688
  <div class="stats">
121
- <div>Modules: <span id="s-mod">-</span></div>
122
- <div>Models: <span id="s-mdl">-</span></div>
123
- <div>APIs: <span id="s-api">-</span></div>
124
- <div>Tests: <span id="s-files">-</span></div>
125
- <div id="s-results-wrap" style="display:none">Results: <span id="s-results" style="color:var(--accent)">-</span></div>
689
+ <div><div class="label">Modules</div><span class="value" id="s-mod">-</span></div>
690
+ <div><div class="label">Models</div><span class="value" id="s-mdl">-</span></div>
691
+ <div><div class="label">APIs</div><span class="value" id="s-api">-</span></div>
692
+ <div><div class="label">Tests</div><span class="value" id="s-files">-</span></div>
693
+ <div id="s-results-wrap" style="display:none"><div class="label">Results</div><span class="value" id="s-results">-</span></div>
126
694
  </div>
695
+ <button class="theme-toggle" id="theme-toggle" title="Toggle theme">
696
+ <svg id="theme-icon-dark" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1a7 7 0 1 0 0 14A5 5 0 0 1 8 1z"/></svg>
697
+ <svg id="theme-icon-light" viewBox="0 0 16 16" fill="currentColor" style="display:none"><circle cx="8" cy="8" r="3"/><g stroke="currentColor" stroke-width="1.2"><line x1="8" y1="1" x2="8" y2="3"/><line x1="8" y1="13" x2="8" y2="15"/><line x1="1" y1="8" x2="3" y2="8"/><line x1="13" y1="8" x2="15" y2="8"/><line x1="3.05" y1="3.05" x2="4.46" y2="4.46"/><line x1="11.54" y1="11.54" x2="12.95" y2="12.95"/><line x1="3.05" y1="12.95" x2="4.46" y2="11.54"/><line x1="11.54" y1="4.46" x2="12.95" y2="3.05"/></g></svg>
698
+ </button>
127
699
  <div class="conn-dot" id="conn-dot" title="WebSocket"></div>
128
700
  </header>
129
701
 
130
702
  <aside class="sidebar">
131
- <h3>๐Ÿ“‚ Modules</h3>
703
+ <h3>
704
+ <svg viewBox="0 0 16 16" fill="currentColor"><path d="M1 2h14v2H1zm0 4h10v2H1zm0 4h12v2H1zm0 4h8v2H1z"/></svg>
705
+ Modules
706
+ </h3>
132
707
  <div id="mod-list"></div>
133
- <h3 style="margin-top:12px">๐ŸŠ Agents</h3>
708
+ <h3 style="margin-top:12px">
709
+ <svg viewBox="0 0 16 16" fill="var(--accent)"><rect x="4" y="2" width="8" height="3" rx="1"/><rect x="3" y="5" width="10" height="6" rx="2" opacity=".8"/><rect x="5" y="11" width="2" height="2" opacity=".6"/><rect x="9" y="11" width="2" height="2" opacity=".6"/></svg>
710
+ Agents
711
+ </h3>
134
712
  <div id="agent-sidebar"></div>
135
713
  </aside>
136
714
 
137
715
  <main class="main">
138
- <canvas id="graph-canvas"></canvas>
716
+ <div class="view" id="graph-view">
717
+ <canvas id="graph-canvas"></canvas>
718
+ </div>
719
+ <div class="view hidden" id="pixel-view">
720
+ <div class="pixel-stage">
721
+ <img class="asset desk-asset" src="./assets/star/desk-v3.webp" alt="desk">
722
+ <img class="asset server-asset" src="./assets/botreview/server.gif" alt="server">
723
+ <img class="asset coffee-asset" src="./assets/botreview/coffee-machine.gif" alt="coffee">
724
+ <img class="asset walls-asset" src="./assets/botreview/walls.png" alt="walls">
725
+ <div class="pixel-kpis">
726
+ <div class="pixel-kpi"><div class="t">Working</div><div class="v" id="kpi-working">0</div></div>
727
+ <div class="pixel-kpi errors"><div class="t">Errors</div><div class="v" id="kpi-errors">0</div></div>
728
+ <div class="pixel-kpi done"><div class="t">Done</div><div class="v" id="kpi-done">0</div></div>
729
+ </div>
730
+ <div class="pixel-agent-layer" id="pixel-agent-layer"></div>
731
+ </div>
732
+ </div>
139
733
  <div class="tooltip" id="tooltip"></div>
140
734
  </main>
141
735
 
142
736
  <div class="log-panel">
143
737
  <div class="panel-tabs">
144
- <button class="tab active" data-tab="log">๐Ÿ“‹ Log</button>
145
- <button class="tab" data-tab="files">๐Ÿ“„ Tests <span id="file-badge" style="display:none;background:var(--accent);color:#000;border-radius:8px;padding:0 5px;font-size:9px;margin-left:3px">0</span></button>
146
- <button class="tab" data-tab="results">๐Ÿงช Results <span id="result-badge" style="display:none;background:var(--accent);color:#000;border-radius:8px;padding:0 5px;font-size:9px;margin-left:3px">0</span></button>
147
- <button class="tab" data-tab="reports">๐Ÿ“Š Reports</button>
738
+ <button class="tab active" data-tab="log">
739
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><rect x="1" y="2" width="14" height="12" rx="2" opacity=".2"/><line x1="4" y1="5" x2="12" y2="5" stroke="currentColor" stroke-width="1.2"/><line x1="4" y1="8" x2="10" y2="8" stroke="currentColor" stroke-width="1.2"/><line x1="4" y1="11" x2="8" y2="11" stroke="currentColor" stroke-width="1.2"/></svg>
740
+ Log
741
+ </button>
742
+ <button class="tab" data-tab="files">
743
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><path d="M3 1h7l3 3v11H3V1z" opacity=".3"/><path d="M10 1v3h3" fill="none" stroke="currentColor" stroke-width="1"/></svg>
744
+ Tests <span id="file-badge" class="tab-badge" style="display:none">0</span>
745
+ </button>
746
+ <button class="tab" data-tab="results">
747
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3"><path d="M3 8l3 3 7-7"/></svg>
748
+ Results <span id="result-badge" class="tab-badge" style="display:none">0</span>
749
+ </button>
750
+ <button class="tab" data-tab="reports">
751
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" opacity=".7"><rect x="1" y="3" width="5" height="10" rx="1"/><rect x="7" y="1" width="8" height="14" rx="1"/></svg>
752
+ Reports
753
+ </button>
148
754
  </div>
149
755
  <div class="log-list" id="log-list"></div>
150
756
  <div class="file-list" id="file-list" style="display:none"></div>
@@ -160,14 +766,144 @@
160
766
  <pre id="fp-code"></pre>
161
767
  </div>
162
768
 
769
+ <!-- Keyboard shortcut legend -->
770
+ <div class="shortcut-legend" id="shortcut-legend">
771
+ <kbd>1</kbd><span>Dashboard</span>
772
+ <kbd>2</kbd><span>Pixel Office</span>
773
+ <kbd>S</kbd><span>Scan</span>
774
+ <kbd>P</kbd><span>Pipeline</span>
775
+ <kbd>T</kbd><span>Run Tests</span>
776
+ <kbd>R</kbd><span>Reports</span>
777
+ <kbd>X</kbd><span>Reset</span>
778
+ <kbd>D</kbd><span>Dark/Light</span>
779
+ <kbd>?</kbd><span>Show shortcuts</span>
780
+ <kbd>Esc</kbd><span>Close panel</span>
781
+ </div>
782
+
163
783
  <script>
784
+ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
785
+ SVG Icon Library (replaces emoji) โ€” inspired by OpenClaw PixelSvg
786
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
787
+ const ICONS = {
788
+ croc: `<svg viewBox="0 0 16 16" fill="none"><rect x="4" y="1" width="8" height="3" rx="1" fill="var(--accent)"/><rect x="3" y="4" width="10" height="7" rx="2" fill="var(--accent)" opacity=".85"/><rect x="5" y="5" width="2" height="2" rx="1" fill="var(--bg)"/><rect x="9" y="5" width="2" height="2" rx="1" fill="var(--bg)"/><rect x="6" y="8" width="4" height="1" fill="var(--bg)" opacity=".4"/><rect x="5" y="11" width="2" height="3" rx="1" fill="var(--accent)" opacity=".6"/><rect x="9" y="11" width="2" height="3" rx="1" fill="var(--accent)" opacity=".6"/></svg>`,
789
+ parser: `<svg viewBox="0 0 16 16" fill="none" stroke="var(--blue)" stroke-width="1.3"><rect x="2" y="2" width="12" height="12" rx="2"/><line x1="5" y1="6" x2="11" y2="6"/><line x1="5" y1="9" x2="9" y2="9"/></svg>`,
790
+ analyzer: `<svg viewBox="0 0 16 16" fill="none" stroke="var(--purple)" stroke-width="1.3"><rect x="1" y="11" width="3" height="4"/><rect x="5" y="7" width="3" height="8"/><rect x="9" y="3" width="3" height="12"/><rect x="13" y="1" width="2" height="14"/></svg>`,
791
+ tester: `<svg viewBox="0 0 16 16" fill="none" stroke="var(--accent)" stroke-width="1.3"><path d="M6 2v4l-3 8h10l-3-8V2"/><line x1="4" y1="2" x2="12" y2="2"/></svg>`,
792
+ healer: `<svg viewBox="0 0 16 16" fill="none" stroke="var(--orange)" stroke-width="1.5"><path d="M5 3l-2 5h4l-2 5 7-7h-4l2-3z"/></svg>`,
793
+ planner: `<svg viewBox="0 0 16 16" fill="none" stroke="var(--blue)" stroke-width="1.3"><rect x="2" y="1" width="12" height="14" rx="2"/><line x1="5" y1="5" x2="11" y2="5"/><line x1="5" y1="8" x2="11" y2="8"/><line x1="5" y1="11" x2="8" y2="11"/></svg>`,
794
+ reporter: `<svg viewBox="0 0 16 16" fill="none" stroke="var(--accent)" stroke-width="1.3"><rect x="1" y="2" width="14" height="12" rx="2"/><polyline points="4,10 6,6 8,9 10,4 12,7"/></svg>`,
795
+ };
796
+ const ROLE_ICONS = { parser:ICONS.parser, analyzer:ICONS.analyzer, tester:ICONS.tester, healer:ICONS.healer, planner:ICONS.planner, reporter:ICONS.reporter };
797
+
798
+ /* Bubble messages (Star-Office style) */
799
+ const BUBBLE_TEXTS = {
800
+ working: ['ๆญฃๅœจๆ‰ง่กŒ...', 'ๅฟซไบ†ๅฟซไบ†', 'ๅค„็†ไธญ...', 'ๅŠ ๆฒน ๐Ÿ’ช'],
801
+ testing: ['่ท‘ๆต‹่ฏ•ไธญ...', '้ชŒ่ฏ API...', '็ญ‰็ป“ๆžœ...'],
802
+ thinking: ['่ฎฉๆˆ‘ๆƒณๆƒณ...', 'ๅˆ†ๆžไธญ...', 'ๆŽจ็†...', '๐Ÿค”'],
803
+ error: ['ๅ‡บ้”™ไบ†!', 'ไฟฎๅคไธญ...', '็ณŸ็ณ•...', 'ๆŽ’ๆŸฅ้—ฎ้ข˜...'],
804
+ idle: ['ๆ‘ธ้ฑผไธญ~', '็ญ‰ไปปๅŠก...', 'โ˜• ๅ–ๅ’–ๅ•ก', 'zzZ'],
805
+ done: ['ๆžๅฎš!', 'ๅฎŒๆˆ โœ“', 'ไธ‹ไธ€ไธช!'],
806
+ passed: ['ๅ…จ็ปฟ โœ“', 'ๆต‹่ฏ•้€š่ฟ‡!'],
807
+ failed: ['ๆœ‰ๅคฑ่ดฅ...', '้œ€่ฆไฟฎๅค'],
808
+ };
809
+
164
810
  const S = {
165
811
  project:null, graph:{nodes:[],edges:[]}, agents:[], ws:null,
166
812
  pan:{x:0,y:0}, zoom:1, dragging:false, dragStart:{x:0,y:0},
167
813
  nodePos:new Map(), hoveredNode:null, running:false, _userPanned:false,
168
- generatedFiles:[], testMetrics:null, testQuality:null, reports:[], runMode:'auto'
814
+ generatedFiles:[], testMetrics:null, testQuality:null, reports:[], runMode:'auto',
815
+ currentView:'dashboard', theme:'dark', bubbleTimers:new Map(),
816
+ spriteSheets:new Map(), spriteAnimFrames:new Map(), spriteAnimTimers:new Map()
817
+ };
818
+
819
+ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
820
+ Spritesheet Animation Engine (inspired by Star-Office Phaser)
821
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
822
+ const SPRITE_CONFIG = {
823
+ // Each character sprite: image src, frame grid, frame count, FPS per status
824
+ chars: [
825
+ { key:'char_0', src:'./assets/botreview/char_0.png', cols:1, rows:1, total:1 },
826
+ { key:'char_1', src:'./assets/botreview/char_1.png', cols:1, rows:1, total:1 },
827
+ { key:'char_2', src:'./assets/botreview/char_2.png', cols:1, rows:1, total:1 },
828
+ ],
829
+ // FPS by status โ€” active states get faster animation
830
+ fps: { idle:2, working:8, testing:8, thinking:4, error:10, failed:10, done:4, passed:4 },
169
831
  };
170
832
 
833
+ // Preload sprite images
834
+ function preloadSprites(){
835
+ SPRITE_CONFIG.chars.forEach(cfg=>{
836
+ if(S.spriteSheets.has(cfg.key)) return;
837
+ const img=new Image(); img.src=cfg.src;
838
+ img.onload=()=>{ S.spriteSheets.set(cfg.key, {img,cols:cfg.cols,rows:cfg.rows,total:cfg.total}); };
839
+ });
840
+ }
841
+
842
+ // Render a single frame of a sprite to a canvas element
843
+ function renderSpriteFrame(canvas, spriteKey, frameIdx){
844
+ const sheet=S.spriteSheets.get(spriteKey);
845
+ if(!sheet||!canvas) return;
846
+ const ctx=canvas.getContext('2d');
847
+ const fw=sheet.img.naturalWidth/sheet.cols;
848
+ const fh=sheet.img.naturalHeight/sheet.rows;
849
+ const col=frameIdx%sheet.cols;
850
+ const row=Math.floor(frameIdx/sheet.cols);
851
+ ctx.clearRect(0,0,canvas.width,canvas.height);
852
+ ctx.imageSmoothingEnabled=false; // pixel-art crisp
853
+ ctx.drawImage(sheet.img, col*fw,row*fh,fw,fh, 0,0,canvas.width,canvas.height);
854
+ }
855
+
856
+ // Start/manage sprite animation loop for an agent
857
+ function startSpriteAnim(canvasId, spriteKey, status){
858
+ // Stop existing timer
859
+ if(S.spriteAnimTimers.has(canvasId)){
860
+ clearInterval(S.spriteAnimTimers.get(canvasId));
861
+ }
862
+ const sheet=S.spriteSheets.get(spriteKey);
863
+ if(!sheet) return;
864
+ const fps=SPRITE_CONFIG.fps[status]||SPRITE_CONFIG.fps.idle;
865
+ let frame=S.spriteAnimFrames.get(canvasId)||0;
866
+ const canvas=document.getElementById(canvasId);
867
+ if(!canvas) return;
868
+ renderSpriteFrame(canvas, spriteKey, frame);
869
+ if(sheet.total<=1){ S.spriteAnimTimers.delete(canvasId); return; }
870
+ const timer=setInterval(()=>{
871
+ frame=(frame+1)%sheet.total;
872
+ S.spriteAnimFrames.set(canvasId, frame);
873
+ const c=document.getElementById(canvasId);
874
+ if(c) renderSpriteFrame(c, spriteKey, frame);
875
+ else { clearInterval(timer); S.spriteAnimTimers.delete(canvasId); }
876
+ }, 1000/fps);
877
+ S.spriteAnimTimers.set(canvasId, timer);
878
+ }
879
+
880
+ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
881
+ Typewriter Effect (inspired by Star-Office)
882
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
883
+ function typewriterLog(msg, level, charDelay){
884
+ level=level||'info';
885
+ charDelay=charDelay||35;
886
+ const el=document.getElementById('log-list');
887
+ const d=document.createElement('div'); d.className='log-entry '+level;
888
+ const ts=document.createElement('span'); ts.className='timestamp';
889
+ ts.textContent=new Date().toLocaleTimeString();
890
+ d.appendChild(ts);
891
+ const textSpan=document.createElement('span');
892
+ const cursor=document.createElement('span'); cursor.className='typewriter-cursor';
893
+ d.appendChild(textSpan); d.appendChild(cursor);
894
+ el.appendChild(d); el.scrollTop=el.scrollHeight;
895
+ let i=0;
896
+ const interval=setInterval(()=>{
897
+ if(i<msg.length){
898
+ textSpan.textContent+=msg[i]; i++;
899
+ el.scrollTop=el.scrollHeight;
900
+ }else{
901
+ clearInterval(interval); cursor.remove();
902
+ }
903
+ }, charDelay);
904
+ return d;
905
+ }
906
+
171
907
  async function fetchProject(){
172
908
  try{
173
909
  const r=await fetch('/api/project'); S.project=await r.json();
@@ -177,23 +913,23 @@ async function fetchProject(){
177
913
  }
178
914
  async function doScan(){
179
915
  if(S.running)return; S.running=true; updateBtns();
180
- addLog('๐Ÿ” Starting scan...');
916
+ addLog('๐Ÿ” Starting scan...','info',true);
181
917
  try{await fetch('/api/scan',{method:'POST'});}
182
918
  catch(e){addLog('Scan failed: '+e.message,'error');S.running=false;updateBtns();}
183
919
  }
184
920
  async function doPipeline(){
185
921
  if(S.running)return; S.running=true; updateBtns();
186
- addLog('โ–ถ Starting pipeline...');
922
+ addLog('โ–ถ Starting pipeline...','info',true);
187
923
  try{await fetch('/api/pipeline',{method:'POST'});}
188
924
  catch(e){addLog('Pipeline failed: '+e.message,'error');S.running=false;updateBtns();}
189
925
  }
190
926
  async function doReset(){
191
927
  try{await fetch('/api/reset',{method:'POST'});}catch(e){addLog('Reset failed','error');}
192
- S.running=false; updateBtns(); addLog('โน Agents reset');
928
+ S.running=false; updateBtns(); addLog('โน Agents reset','info',true);
193
929
  }
194
930
  async function doRunTests(){
195
931
  if(S.running)return; S.running=true; updateBtns();
196
- addLog('๐Ÿงช Starting test execution ('+S.runMode+')...');
932
+ addLog('๐Ÿงช Starting test execution ('+S.runMode+')...','info',true);
197
933
  try{
198
934
  const r=await fetch('/api/run-tests',{
199
935
  method:'POST',
@@ -209,7 +945,7 @@ async function doRunTests(){
209
945
  }
210
946
  async function doReports(){
211
947
  if(S.running)return; S.running=true; updateBtns();
212
- addLog('๐Ÿ“Š Generating reports...');
948
+ addLog('๐Ÿ“Š Generating reports...','info',true);
213
949
  try{await fetch('/api/reports/generate',{method:'POST'});}
214
950
  catch(e){addLog('Report gen failed: '+e.message,'error');S.running=false;updateBtns();}
215
951
  }
@@ -229,7 +965,7 @@ function connectWS(){
229
965
  try{
230
966
  const m=JSON.parse(e.data);
231
967
  if(m.type==='agent:update'&&Array.isArray(m.payload)){
232
- S.agents=m.payload; renderOffice(); renderAgentSB();
968
+ S.agents=m.payload; renderOffice(); renderAgentSB(); renderPixelOffice();
233
969
  }else if(m.type==='graph:update'){
234
970
  S.graph=m.payload; layoutGraph(); renderCanvas(); renderModList(); updateStats();
235
971
  }else if(m.type==='log'){
@@ -242,8 +978,8 @@ function connectWS(){
242
978
  badge.textContent=S.generatedFiles.length;badge.style.display='inline';
243
979
  }else if(m.type==='pipeline:complete'){
244
980
  S.running=false; updateBtns();
245
- if(m.payload.status==='success') addLog('โœ… Pipeline complete!');
246
- else addLog('โŒ Pipeline failed: '+(m.payload.error||''),'error');
981
+ if(m.payload.status==='success') addLog('โœ… Pipeline complete!','info',true);
982
+ else addLog('โŒ Pipeline failed: '+(m.payload.error||''),'error',true);
247
983
  setTimeout(fetchProject,500);
248
984
  }else if(m.type==='test:complete'){
249
985
  S.running=false; S.testMetrics=m.payload.metrics||null; S.testQuality=m.payload.quality||null; updateBtns(); renderResults();
@@ -258,7 +994,7 @@ function connectWS(){
258
994
  }
259
995
  const rb=document.getElementById('result-badge');
260
996
  rb.textContent=m.payload.total||0; rb.style.display='inline';
261
- rb.style.background=(met&&met.failed>0)?'var(--red)':'var(--accent)';
997
+ rb.className='tab-badge'+((met&&met.failed>0)?' alert':'');
262
998
  }else if(m.type==='reports:generated'){
263
999
  S.running=false; S.reports=m.payload||[]; updateBtns(); renderReports();
264
1000
  addLog('๐Ÿ“Š '+S.reports.length+' reports generated');
@@ -268,11 +1004,17 @@ function connectWS(){
268
1004
  S.ws.onclose=()=>{document.getElementById('conn-dot').classList.remove('on');setTimeout(connectWS,3000);};
269
1005
  }
270
1006
 
271
- function addLog(msg,level){
1007
+ function addLog(msg,level,useTypewriter){
272
1008
  level=level||'info';
1009
+ // Use typewriter for important messages (pipeline, scan, reset, test)
1010
+ if(useTypewriter){
1011
+ typewriterLog(msg,level,30);
1012
+ return;
1013
+ }
273
1014
  const el=document.getElementById('log-list');
274
1015
  const d=document.createElement('div'); d.className='log-entry '+level;
275
- d.textContent='['+new Date().toLocaleTimeString()+'] '+msg;
1016
+ const ts=document.createElement('span'); ts.className='timestamp'; ts.textContent=new Date().toLocaleTimeString();
1017
+ d.appendChild(ts); d.appendChild(document.createTextNode(msg));
276
1018
  el.appendChild(d); el.scrollTop=el.scrollHeight;
277
1019
  }
278
1020
  function updateStats(){
@@ -364,79 +1106,81 @@ function renderCanvas(){
364
1106
  canvas.width=canvas.clientWidth*dpr; canvas.height=canvas.clientHeight*dpr;
365
1107
  ctx.scale(dpr,dpr);
366
1108
  const w=canvas.clientWidth,h=canvas.clientHeight;
367
- ctx.clearRect(0,0,w,h); ctx.save(); ctx.translate(S.pan.x,S.pan.y); ctx.scale(S.zoom,S.zoom);
1109
+ const isDark=S.theme==='dark';
1110
+ ctx.clearRect(0,0,w,h);
1111
+ // Background fill
1112
+ ctx.fillStyle=isDark?'#0a0f1a':'#f8fafc';
1113
+ ctx.fillRect(0,0,w,h);
1114
+ ctx.save(); ctx.translate(S.pan.x,S.pan.y); ctx.scale(S.zoom,S.zoom);
368
1115
  // Subtle grid
369
- ctx.strokeStyle='#151530';ctx.lineWidth=.5;
1116
+ ctx.strokeStyle=isDark?'rgba(148,163,184,.06)':'rgba(100,116,139,.08)';ctx.lineWidth=.5;
370
1117
  const gridStep=40;
371
1118
  for(let x=-2000;x<4000;x+=gridStep){ctx.beginPath();ctx.moveTo(x,-2000);ctx.lineTo(x,4000);ctx.stroke();}
372
1119
  for(let y=-2000;y<4000;y+=gridStep){ctx.beginPath();ctx.moveTo(-2000,y);ctx.lineTo(4000,y);ctx.stroke();}
373
1120
 
374
1121
  const edges=S.graph.edges||[],nodes=S.graph.nodes||[];
375
- const largeGraph=nodes.length>80;
376
1122
 
377
1123
  // Draw module cluster backgrounds
378
1124
  if(S.modMeta){
379
1125
  for(const[name,m] of S.modMeta){
380
1126
  ctx.beginPath(); ctx.arc(m.cx,m.cy,m.radius,0,Math.PI*2);
381
- ctx.fillStyle=m.color+'10'; ctx.fill();
382
- ctx.strokeStyle=m.color+'30'; ctx.lineWidth=2; ctx.setLineDash([6,4]); ctx.stroke(); ctx.setLineDash([]);
383
- // Module label at top of cluster
384
- ctx.font='bold 13px "Courier New"'; ctx.fillStyle=m.color+'cc'; ctx.textAlign='center'; ctx.textBaseline='bottom';
385
- ctx.fillText(name+' ('+m.count+')',m.cx,m.cy-m.radius-6);
1127
+ ctx.fillStyle=m.color+(isDark?'0a':'08'); ctx.fill();
1128
+ ctx.strokeStyle=m.color+(isDark?'25':'20'); ctx.lineWidth=1.5; ctx.setLineDash([6,4]); ctx.stroke(); ctx.setLineDash([]);
1129
+ ctx.font='600 12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1130
+ ctx.fillStyle=m.color+(isDark?'cc':'aa'); ctx.textAlign='center'; ctx.textBaseline='bottom';
1131
+ ctx.fillText(name+' ('+m.count+')',m.cx,m.cy-m.radius-8);
386
1132
  }
387
1133
  }
388
1134
 
389
- // Draw "uses" edges with curved lines + gradient
1135
+ // Draw edges with curved lines + gradient
390
1136
  for(const e of edges){
391
- if(e.relation==='contains') continue; // never draw contains
1137
+ if(e.relation==='contains') continue;
392
1138
  const s=S.nodePos.get(e.source),t=S.nodePos.get(e.target);if(!s||!t)continue;
393
- const dx=t.x-s.x,dy=t.y-s.y,dist=Math.sqrt(dx*dx+dy*dy);
394
- // Curved edge via quadratic bezier, perpendicular offset
1139
+ const dx=t.x-s.x,dy=t.y-s.y;
395
1140
  const mx=(s.x+t.x)/2+dy*0.15, my=(s.y+t.y)/2-dx*0.15;
396
1141
  const grad=ctx.createLinearGradient(s.x,s.y,t.x,t.y);
397
- grad.addColorStop(0,'rgba(233,69,96,.5)'); // controller (red)
398
- grad.addColorStop(1,'rgba(78,204,163,.5)'); // model (green)
399
- ctx.strokeStyle=grad;ctx.lineWidth=2;
1142
+ grad.addColorStop(0,isDark?'rgba(248,113,113,.35)':'rgba(220,38,38,.25)');
1143
+ grad.addColorStop(1,isDark?'rgba(52,211,153,.35)':'rgba(5,150,105,.25)');
1144
+ ctx.strokeStyle=grad;ctx.lineWidth=1.5;
400
1145
  ctx.beginPath();ctx.moveTo(s.x,s.y);ctx.quadraticCurveTo(mx,my,t.x,t.y);ctx.stroke();
401
- // Arrowhead
402
1146
  const t2=0.95,at2x=(1-t2)*(1-t2)*s.x+2*(1-t2)*t2*mx+t2*t2*t.x,at2y=(1-t2)*(1-t2)*s.y+2*(1-t2)*t2*my+t2*t2*t.y;
403
- const a=Math.atan2(t.y-at2y,t.x-at2x),al=8;
404
- ctx.fillStyle='rgba(78,204,163,.6)';ctx.beginPath();ctx.moveTo(t.x,t.y);
1147
+ const a=Math.atan2(t.y-at2y,t.x-at2x),al=7;
1148
+ ctx.fillStyle=isDark?'rgba(52,211,153,.5)':'rgba(5,150,105,.4)';ctx.beginPath();ctx.moveTo(t.x,t.y);
405
1149
  ctx.lineTo(t.x-al*Math.cos(a-.4),t.y-al*Math.sin(a-.4));
406
1150
  ctx.lineTo(t.x-al*Math.cos(a+.4),t.y-al*Math.sin(a+.4));ctx.closePath();ctx.fill();
407
1151
  }
408
1152
 
409
1153
  // Draw nodes
410
- const tc={model:'#4ecca3',controller:'#e94560',api:'#f39c12',dto:'#3498db',module:'#9b59b6'};
411
- const sc={idle:'#444',testing:'#f39c12',passed:'#4ecca3',failed:'#e94560'};
1154
+ const tc={model:'#34d399',controller:'#f87171',api:'#fbbf24',dto:'#60a5fa',module:'#a78bfa'};
1155
+ const sc={idle:isDark?'#475569':'#94a3b8',testing:'#fbbf24',passed:'#34d399',failed:'#f87171'};
412
1156
  for(const n of nodes){
413
- if(n.type==='module') continue; // drawn as cluster bg instead
1157
+ if(n.type==='module') continue;
414
1158
  const p=S.nodePos.get(n.id);if(!p)continue;
415
- const sz=10,c=tc[n.type]||'#888',ol=sc[n.status]||'#444',hov=S.hoveredNode===n.id;
416
- // Glow for active status
417
- if(n.status==='testing'){ctx.shadowColor=sc.testing;ctx.shadowBlur=14;}
418
- else if(n.status==='passed'){ctx.shadowColor=sc.passed;ctx.shadowBlur=10;}
419
- else if(n.status==='failed'){ctx.shadowColor=sc.failed;ctx.shadowBlur=12;}
420
- // Node circle instead of rect for cleaner look
1159
+ const sz=9,c=tc[n.type]||'#888',ol=sc[n.status]||sc.idle,hov=S.hoveredNode===n.id;
1160
+ if(n.status==='testing'){ctx.shadowColor=sc.testing;ctx.shadowBlur=12;}
1161
+ else if(n.status==='passed'){ctx.shadowColor=sc.passed;ctx.shadowBlur=8;}
1162
+ else if(n.status==='failed'){ctx.shadowColor=sc.failed;ctx.shadowBlur=10;}
421
1163
  ctx.beginPath();ctx.arc(p.x,p.y,sz,0,Math.PI*2);
422
1164
  ctx.fillStyle=c;ctx.fill();
423
1165
  ctx.shadowBlur=0;
424
- ctx.strokeStyle=hov?'#fff':ol;ctx.lineWidth=hov?3:1.5;ctx.stroke();
425
- // Icon
426
- ctx.font=(sz)+'px serif';ctx.textAlign='center';ctx.textBaseline='middle';
427
- ctx.fillText(n.type==='model'?'๐Ÿ“ฆ':'๐ŸŽฎ',p.x,p.y);
428
- // Label โ€” show at higher zoom or on hover
1166
+ ctx.strokeStyle=hov?(isDark?'#fff':'#0f172a'):ol;ctx.lineWidth=hov?2.5:1.2;ctx.stroke();
1167
+ // Label
429
1168
  if(S.zoom>0.4||hov){
430
- ctx.font='9px "Courier New"';ctx.fillStyle=hov?'#fff':'#aaa';ctx.textAlign='center';ctx.textBaseline='top';
431
- ctx.fillText((n.label||n.id.split(':').pop()).substring(0,18),p.x,p.y+sz+3);
1169
+ ctx.font='500 9px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1170
+ ctx.fillStyle=hov?(isDark?'#fff':'#0f172a'):(isDark?'#94a3b8':'#64748b');
1171
+ ctx.textAlign='center';ctx.textBaseline='top';
1172
+ ctx.fillText((n.label||n.id.split(':').pop()).substring(0,18),p.x,p.y+sz+4);
432
1173
  }
433
1174
  }
434
1175
  ctx.restore();
435
1176
 
436
1177
  // HUD legend
437
- ctx.font='10px "Courier New"';ctx.textAlign='left';
438
- const leg=[['๐Ÿ“ฆ Model','#4ecca3'],['๐ŸŽฎ Controller','#e94560'],['โ”€โ”€โ”€ uses','rgba(200,100,130,.8)']];
439
- for(let i=0;i<leg.length;i++){ctx.fillStyle=leg[i][1];ctx.fillText(leg[i][0],10,h-40+i*14);}
1178
+ ctx.font='500 10px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';ctx.textAlign='left';
1179
+ const legBg=isDark?'rgba(10,15,26,.8)':'rgba(255,255,255,.85)';
1180
+ ctx.fillStyle=legBg; ctx.beginPath(); ctx.roundRect(6,h-56,120,48,6); ctx.fill();
1181
+ ctx.strokeStyle=isDark?'rgba(148,163,184,.15)':'rgba(100,116,139,.2)';ctx.lineWidth=1;ctx.stroke();
1182
+ const leg=[['โ— Model','#34d399'],['โ— Controller','#f87171'],['โ”€ uses',isDark?'rgba(200,150,160,.7)':'rgba(150,80,100,.6)']];
1183
+ for(let i=0;i<leg.length;i++){ctx.fillStyle=leg[i][1];ctx.fillText(leg[i][0],14,h-42+i*14);}
440
1184
  }
441
1185
 
442
1186
  function setupCanvas(){
@@ -467,12 +1211,11 @@ function updateAll(){
467
1211
  document.getElementById('s-mdl').textContent=S.project.stats?.models||0;
468
1212
  document.getElementById('s-api').textContent=S.project.stats?.endpoints||0;
469
1213
  }
470
- renderModList();renderOffice();renderAgentSB();renderCanvas();
1214
+ renderModList();renderOffice();renderAgentSB();renderPixelOffice();renderCanvas();
471
1215
  }
472
1216
  function renderModList(){
473
1217
  const el=document.getElementById('mod-list'),mods=S.graph.nodes.filter(n=>n.type==='module');
474
- if(!mods.length){el.innerHTML='<div style="padding:8px;color:#555;font-size:10px">No modules found</div>';return;}
475
- // Sort by member count descending
1218
+ if(!mods.length){el.innerHTML='<div style="padding:12px;color:var(--text-subtle);font-size:11px">No modules found</div>';return;}
476
1219
  const sorted=mods.sort((a,b)=>{
477
1220
  const ca=S.modMeta&&S.modMeta.get(a.label)?S.modMeta.get(a.label).count:0;
478
1221
  const cb=S.modMeta&&S.modMeta.get(b.label)?S.modMeta.get(b.label).count:0;
@@ -481,10 +1224,10 @@ function renderModList(){
481
1224
  el.innerHTML=sorted.map(m=>{
482
1225
  const meta=S.modMeta&&S.modMeta.get(m.label);
483
1226
  const cnt=meta?meta.count:'';
484
- const col=meta?meta.color:'#888';
485
- return '<div class="mod-item" data-mod="'+esc(m.label)+'" style="border-left:3px solid '+col+';padding-left:6px">'
1227
+ const col=meta?meta.color:'var(--text-subtle)';
1228
+ return '<div class="mod-item" data-mod="'+esc(m.label)+'" style="border-left:3px solid '+col+'">'
486
1229
  +'<div class="dot '+m.status+'"></div>'
487
- +esc(m.label)+' <span style="color:#555;font-size:9px">('+cnt+')</span></div>';
1230
+ +esc(m.label)+'<span class="mod-count">'+cnt+'</span></div>';
488
1231
  }).join('');
489
1232
  // Click to navigate
490
1233
  el.querySelectorAll('.mod-item').forEach(item=>{
@@ -503,21 +1246,116 @@ function renderModList(){
503
1246
  }
504
1247
  function renderAgentSB(){
505
1248
  document.getElementById('agent-sidebar').innerHTML=S.agents.map(a=>
506
- '<div class="mod-item"><div class="dot '+a.status+'"></div>'+esc(a.name)+' <span style="color:#555;font-size:9px">'+a.status+'</span></div>'
1249
+ '<div class="mod-item"><div class="dot '+a.status+'"></div>'+esc(a.name)+'<span class="mod-count">'+a.status+'</span></div>'
507
1250
  ).join('');
508
1251
  }
509
1252
  function renderOffice(){
510
- const dd={parser:'๐Ÿ’ป',analyzer:'๐Ÿ“Š',tester:'๐Ÿงช',healer:'๐Ÿ”ง',planner:'๐Ÿ“‹',reporter:'๐Ÿ“'};
511
1253
  document.getElementById('croc-office').innerHTML=S.agents.map(a=>{
512
1254
  const prog=typeof a.progress==='number'?a.progress:0;
1255
+ const roleIcon=ROLE_ICONS[a.role]||ICONS.croc;
513
1256
  return '<div class="desk '+a.status+'"><div class="badge dot '+a.status+'"></div>'+
514
- '<div class="croc-sprite">๐ŸŠ</div><div class="croc-name">'+esc(a.name)+'</div>'+
1257
+ '<div class="croc-sprite">'+ICONS.croc+'</div><div class="croc-name">'+esc(a.name)+'</div>'+
515
1258
  '<div class="croc-role">'+esc(a.role)+'</div>'+
516
1259
  '<div class="croc-task">'+(a.currentTask?esc(a.currentTask):'')+'</div>'+
517
1260
  '<div class="progress-bar"><div class="fill" style="width:'+prog+'%"></div></div>'+
518
- '<div class="desk-items">'+(dd[a.role]||'')+'</div></div>';
1261
+ '<div class="desk-icon">'+roleIcon+'</div></div>';
519
1262
  }).join('');
520
1263
  }
1264
+ function renderPixelOffice(){
1265
+ const el=document.getElementById('pixel-agent-layer');
1266
+ const stageWidth=(document.getElementById('pixel-view').clientWidth||900)-80;
1267
+ const stageHeight=(document.getElementById('pixel-view').clientHeight||520)-120;
1268
+ const presets=[
1269
+ {x:.18,y:.64},{x:.3,y:.66},{x:.42,y:.62},{x:.56,y:.6},{x:.72,y:.63},{x:.82,y:.52},
1270
+ {x:.22,y:.45},{x:.48,y:.42},{x:.66,y:.4},{x:.78,y:.72}
1271
+ ];
1272
+ const roles=['char_0','char_1','char_2'];
1273
+ const working=S.agents.filter(a=>a.status==='working'||a.status==='testing').length;
1274
+ const errors=S.agents.filter(a=>a.status==='error'||a.status==='failed').length;
1275
+ const done=S.agents.filter(a=>a.status==='done'||a.status==='passed').length;
1276
+ document.getElementById('kpi-working').textContent=String(working);
1277
+ document.getElementById('kpi-errors').textContent=String(errors);
1278
+ document.getElementById('kpi-done').textContent=String(done);
1279
+ if(!S.agents.length){el.innerHTML='';return;}
1280
+
1281
+ // Stop all existing sprite timers
1282
+ for(const [id,timer] of S.spriteAnimTimers){ clearInterval(timer); }
1283
+ S.spriteAnimTimers.clear();
1284
+
1285
+ el.innerHTML=S.agents.map((a,i)=>{
1286
+ const p=presets[i%presets.length];
1287
+ const x=Math.max(16,Math.round(stageWidth*p.x));
1288
+ const y=Math.max(20,Math.round(stageHeight*p.y));
1289
+ const roleSprite=roles[i%roles.length];
1290
+ const labelY=Math.max(16,y-12);
1291
+ const statusClass=(['working','testing'].includes(a.status)?'working':
1292
+ ['done','passed'].includes(a.status)?'done':
1293
+ ['error','failed'].includes(a.status)?'error':
1294
+ a.status==='thinking'?'thinking':'');
1295
+ const canvasId='sprite-'+i;
1296
+ // Use canvas element for spritesheet-based rendering
1297
+ return '<canvas id="'+canvasId+'" class="pixel-sprite-canvas '+a.status+'" width="48" height="48" style="left:'+x+'px;top:'+y+'px"></canvas>'
1298
+ +'<div class="pixel-label" style="left:'+(x+24)+'px;top:'+labelY+'px">'+esc(a.name)
1299
+ +(statusClass?'<span class="status-tag '+statusClass+'">'+esc(a.status)+'</span>':'')+'</div>';
1300
+ }).join('');
1301
+
1302
+ // Start sprite animations for each agent
1303
+ S.agents.forEach((a,i)=>{
1304
+ const roleSprite=roles[i%roles.length];
1305
+ const canvasId='sprite-'+i;
1306
+ // Delay to ensure DOM is ready
1307
+ requestAnimationFrame(()=>startSpriteAnim(canvasId, roleSprite, a.status));
1308
+ });
1309
+
1310
+ // Trigger bubbles for active agents (Star-Office style)
1311
+ scheduleBubbles();
1312
+ }
1313
+ function scheduleBubbles(){
1314
+ S.agents.forEach((a,i)=>{
1315
+ if(S.bubbleTimers.has(a.name)) return;
1316
+ const texts=BUBBLE_TEXTS[a.status]||BUBBLE_TEXTS.idle;
1317
+ const show=()=>{
1318
+ const el=document.getElementById('pixel-agent-layer');
1319
+ if(!el||S.currentView!=='office') return;
1320
+ const presets=[
1321
+ {x:.18,y:.64},{x:.3,y:.66},{x:.42,y:.62},{x:.56,y:.6},{x:.72,y:.63},{x:.82,y:.52},
1322
+ {x:.22,y:.45},{x:.48,y:.42},{x:.66,y:.4},{x:.78,y:.72}
1323
+ ];
1324
+ const stageW=(document.getElementById('pixel-view').clientWidth||900)-80;
1325
+ const stageH=(document.getElementById('pixel-view').clientHeight||520)-120;
1326
+ const p=presets[i%presets.length];
1327
+ const x=Math.max(16,Math.round(stageW*p.x));
1328
+ const y=Math.max(20,Math.round(stageH*p.y));
1329
+ const bubble=document.createElement('div');
1330
+ bubble.className='pixel-bubble';
1331
+ bubble.textContent=texts[Math.floor(Math.random()*texts.length)];
1332
+ bubble.style.left=(x+50)+'px'; bubble.style.top=(y-20)+'px';
1333
+ el.appendChild(bubble);
1334
+ setTimeout(()=>bubble.remove(),3000);
1335
+ };
1336
+ const interval=setInterval(show, 6000+Math.random()*8000);
1337
+ S.bubbleTimers.set(a.name, interval);
1338
+ setTimeout(show, 1000+Math.random()*3000);
1339
+ });
1340
+ // Clear timers for agents that no longer exist
1341
+ for(const [name,timer] of S.bubbleTimers){
1342
+ if(!S.agents.find(a=>a.name===name)){clearInterval(timer);S.bubbleTimers.delete(name);}
1343
+ }
1344
+ }
1345
+ function setView(view){
1346
+ S.currentView=view;
1347
+ const dashboard=view==='dashboard';
1348
+ document.getElementById('graph-view').classList.toggle('hidden',!dashboard);
1349
+ document.getElementById('pixel-view').classList.toggle('hidden',dashboard);
1350
+ document.getElementById('view-dashboard').classList.toggle('active',dashboard);
1351
+ document.getElementById('view-office').classList.toggle('active',!dashboard);
1352
+ document.getElementById('tooltip').classList.remove('visible');
1353
+ if(dashboard){
1354
+ setTimeout(()=>{layoutGraph();renderCanvas();},0);
1355
+ }else{
1356
+ renderPixelOffice();
1357
+ }
1358
+ }
521
1359
  function esc(s){return s?s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'):'';}
522
1360
 
523
1361
  document.getElementById('btn-scan').addEventListener('click',doScan);
@@ -526,6 +1364,8 @@ document.getElementById('btn-reset').addEventListener('click',doReset);
526
1364
  document.getElementById('btn-run-tests').addEventListener('click',doRunTests);
527
1365
  document.getElementById('btn-reports').addEventListener('click',doReports);
528
1366
  document.getElementById('run-mode').addEventListener('change',e=>{S.runMode=e.target.value;});
1367
+ document.getElementById('view-dashboard').addEventListener('click',()=>setView('dashboard'));
1368
+ document.getElementById('view-office').addEventListener('click',()=>setView('office'));
529
1369
 
530
1370
  // Tab switching
531
1371
  document.querySelectorAll('.panel-tabs .tab').forEach(tab=>{
@@ -543,7 +1383,7 @@ document.querySelectorAll('.panel-tabs .tab').forEach(tab=>{
543
1383
  // File list rendering
544
1384
  function renderFileList(){
545
1385
  const el=document.getElementById('file-list');
546
- if(!S.generatedFiles.length){el.innerHTML='<div style="padding:12px;color:#555;font-size:10px">No test files generated yet. Run Pipeline first.</div>';return;}
1386
+ if(!S.generatedFiles.length){el.innerHTML='<div style="padding:12px;color:var(--text-subtle);font-size:11px">No test files generated yet. Run Pipeline first.</div>';return;}
547
1387
  el.innerHTML=S.generatedFiles.map((f,i)=>
548
1388
  '<div class="file-item" data-idx="'+i+'"><div class="fname">'+esc(f.filePath.split('/').pop()||f.filePath)+'</div>'+
549
1389
  '<div class="fmeta">'+esc(f.module)+' / '+esc(f.chain)+' โ€” '+f.lines+' lines</div></div>'
@@ -572,12 +1412,12 @@ document.addEventListener('keydown',e=>{
572
1412
  // Test Results rendering
573
1413
  function renderResults(){
574
1414
  const el=document.getElementById('results-panel');
575
- if(!S.testMetrics&&!S.testQuality){el.innerHTML='<div style="padding:12px;color:#555;font-size:10px">No test results yet. Run Tests first.</div>';return;}
1415
+ if(!S.testMetrics&&!S.testQuality){el.innerHTML='<div style="padding:12px;color:var(--text-subtle);font-size:11px">No test results yet. Run Tests first.</div>';return;}
576
1416
  const q=S.testQuality;
577
1417
  if(!S.testMetrics&&q){
578
1418
  el.innerHTML='<div style="padding:10px">'
579
1419
  +'<div style="font-size:13px;font-weight:bold;margin-bottom:8px">๐Ÿงช Test Execution Results</div>'
580
- +'<div style="padding:10px;border-radius:4px;background:#2d0a12;color:var(--red);font-size:10px;margin-bottom:10px">Setup failed before test execution.</div>'
1420
+ +'<div style="padding:10px;border-radius:var(--radius-sm);background:var(--red-bg);color:var(--red);font-size:11px;margin-bottom:10px">Setup failed before test execution.</div>'
581
1421
  +'<div style="font-size:11px;margin-bottom:6px">Gate Level: <span style="color:'+(q.level==='fail'?'var(--red)':q.level==='warn'?'var(--orange)':'var(--accent)')+';font-weight:bold">'+q.level.toUpperCase()+'</span></div>'
582
1422
  +'<div style="font-size:10px;color:var(--text-dim)">Auth: '+q.authStatus+' | Backend: '+q.backendStatus+'</div>'
583
1423
  +'<div style="font-size:10px;color:var(--text-dim);margin-top:4px">Reasons: '+(q.reasons&&q.reasons.length?q.reasons.join(', '):'-')+'</div>'
@@ -590,15 +1430,15 @@ function renderResults(){
590
1430
  let qualityHtml='';
591
1431
  if(q){
592
1432
  const gateColor=q.level==='fail'?'var(--red)':q.level==='warn'?'var(--orange)':'var(--accent)';
593
- qualityHtml='<div style="margin-top:12px;padding-top:10px;border-top:1px solid #2a2a45">'
1433
+ qualityHtml='<div style="margin-top:12px;padding-top:10px;border-top:1px solid var(--border)">'
594
1434
  +'<div style="font-size:12px;font-weight:bold;margin-bottom:8px">๐Ÿงญ Execution Quality</div>'
595
- +'<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:10px">'
596
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Gate: <span style="color:'+gateColor+';font-weight:bold">'+q.level.toUpperCase()+'</span></div>'
597
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Setup Fail: '+q.setupFail+'</div>'
598
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Skip Ratio: '+Math.round((q.skipRatio||0)*100)+'%</div>'
599
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Auth Fail Ratio: '+Math.round((q.authFailRatio||0)*100)+'%</div>'
600
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Effective Rate: '+Math.round((q.effectiveExecutionRate||0)*100)+'%</div>'
601
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Auth/Backend: '+q.authStatus+' / '+q.backendStatus+'</div>'
1435
+ +'<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:11px">'
1436
+ +'<div style="background:var(--bg-card);padding:8px;border-radius:var(--radius-sm);border:1px solid var(--border)">Gate: <span style="color:'+gateColor+';font-weight:600">'+q.level.toUpperCase()+'</span></div>'
1437
+ +'<div style="background:var(--bg-card);padding:8px;border-radius:var(--radius-sm);border:1px solid var(--border)">Setup Fail: '+q.setupFail+'</div>'
1438
+ +'<div style="background:var(--bg-card);padding:8px;border-radius:var(--radius-sm);border:1px solid var(--border)">Skip Ratio: '+Math.round((q.skipRatio||0)*100)+'%</div>'
1439
+ +'<div style="background:var(--bg-card);padding:8px;border-radius:var(--radius-sm);border:1px solid var(--border)">Auth Fail Ratio: '+Math.round((q.authFailRatio||0)*100)+'%</div>'
1440
+ +'<div style="background:var(--bg-card);padding:8px;border-radius:var(--radius-sm);border:1px solid var(--border)">Effective Rate: '+Math.round((q.effectiveExecutionRate||0)*100)+'%</div>'
1441
+ +'<div style="background:var(--bg-card);padding:8px;border-radius:var(--radius-sm);border:1px solid var(--border)">Auth/Backend: '+q.authStatus+' / '+q.backendStatus+'</div>'
602
1442
  +'</div>'
603
1443
  +'<div style="font-size:10px;color:var(--text-dim);margin-top:6px">Reasons: '+(q.reasons&&q.reasons.length?q.reasons.join(', '):'-')+'</div>'
604
1444
  +'</div>';
@@ -606,12 +1446,12 @@ function renderResults(){
606
1446
  el.innerHTML='<div style="padding:10px">'
607
1447
  +'<div style="font-size:13px;font-weight:bold;margin-bottom:8px">๐Ÿงช Test Execution Results</div>'
608
1448
  +'<div style="display:flex;gap:12px;margin-bottom:10px">'
609
- +'<div style="flex:1;text-align:center;background:#0d2818;padding:8px;border-radius:4px"><div style="font-size:20px;color:var(--accent)">'+m.passed+'</div><div style="font-size:9px;color:var(--text-dim)">PASSED</div></div>'
610
- +'<div style="flex:1;text-align:center;background:#2d0a12;padding:8px;border-radius:4px"><div style="font-size:20px;color:var(--red)">'+m.failed+'</div><div style="font-size:9px;color:var(--text-dim)">FAILED</div></div>'
611
- +'<div style="flex:1;text-align:center;background:#1a1a2e;padding:8px;border-radius:4px"><div style="font-size:20px;color:var(--orange)">'+m.skipped+'</div><div style="font-size:9px;color:var(--text-dim)">SKIPPED</div></div>'
612
- +'<div style="flex:1;text-align:center;background:#1a1a2e;padding:8px;border-radius:4px"><div style="font-size:20px;color:var(--blue)">'+m.timedOut+'</div><div style="font-size:9px;color:var(--text-dim)">TIMEOUT</div></div>'
1449
+ +'<div style="flex:1;text-align:center;background:var(--accent-bg);padding:10px;border-radius:var(--radius-sm);border:1px solid var(--border)"><div style="font-size:22px;font-weight:700;color:var(--accent)">'+m.passed+'</div><div style="font-size:9px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;margin-top:2px">PASSED</div></div>'
1450
+ +'<div style="flex:1;text-align:center;background:var(--red-bg);padding:10px;border-radius:var(--radius-sm);border:1px solid var(--border)"><div style="font-size:22px;font-weight:700;color:var(--red)">'+m.failed+'</div><div style="font-size:9px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;margin-top:2px">FAILED</div></div>'
1451
+ +'<div style="flex:1;text-align:center;background:var(--orange-bg);padding:10px;border-radius:var(--radius-sm);border:1px solid var(--border)"><div style="font-size:22px;font-weight:700;color:var(--orange)">'+m.skipped+'</div><div style="font-size:9px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;margin-top:2px">SKIPPED</div></div>'
1452
+ +'<div style="flex:1;text-align:center;background:var(--blue-bg);padding:10px;border-radius:var(--radius-sm);border:1px solid var(--border)"><div style="font-size:22px;font-weight:700;color:var(--blue)">'+m.timedOut+'</div><div style="font-size:9px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;margin-top:2px">TIMEOUT</div></div>'
613
1453
  +'</div>'
614
- +'<div style="background:#222;border-radius:3px;height:8px;overflow:hidden">'
1454
+ +'<div style="background:var(--bg-hover);border-radius:4px;height:8px;overflow:hidden">'
615
1455
  +'<div style="height:100%;width:'+passRate+'%;background:'+barColor+';transition:width .5s"></div></div>'
616
1456
  +'<div style="text-align:center;font-size:10px;color:var(--text-dim);margin-top:4px">Pass Rate: '+passRate+'% ('+total+' total)</div>'
617
1457
  +qualityHtml
@@ -621,7 +1461,7 @@ function renderResults(){
621
1461
  // Reports rendering
622
1462
  function renderReports(){
623
1463
  const el=document.getElementById('reports-panel');
624
- if(!S.reports.length){el.innerHTML='<div style="padding:12px;color:#555;font-size:10px">No reports generated yet. Click Reports to generate.</div>';return;}
1464
+ if(!S.reports.length){el.innerHTML='<div style="padding:12px;color:var(--text-subtle);font-size:11px">No reports generated yet. Click Reports to generate.</div>';return;}
625
1465
  el.innerHTML='<div style="padding:10px"><div style="font-size:13px;font-weight:bold;margin-bottom:8px">๐Ÿ“Š Generated Reports</div>'
626
1466
  +S.reports.map(r=>{
627
1467
  const icon=r.format==='html'?'๐ŸŒ':r.format==='json'?'๐Ÿ“‹':'๐Ÿ“';
@@ -649,8 +1489,80 @@ function renderReports(){
649
1489
  });
650
1490
  }
651
1491
 
652
- (async()=>{setupCanvas();await fetchProject();connectWS();addLog('๐ŸŠ OpenCroc Studio ready');
653
- window.addEventListener('resize',()=>{layoutGraph();renderCanvas();});})();
1492
+ // Theme toggle (borrowed from OpenClaw)
1493
+ function toggleTheme(){
1494
+ S.theme=S.theme==='dark'?'light':'dark';
1495
+ document.documentElement.setAttribute('data-theme',S.theme);
1496
+ localStorage.setItem('opencroc-theme',S.theme);
1497
+ document.getElementById('theme-icon-dark').style.display=S.theme==='dark'?'':'none';
1498
+ document.getElementById('theme-icon-light').style.display=S.theme==='light'?'':'none';
1499
+ renderCanvas();
1500
+ }
1501
+ document.getElementById('theme-toggle').addEventListener('click',toggleTheme);
1502
+
1503
+ // Restore saved theme
1504
+ (function initTheme(){
1505
+ const saved=localStorage.getItem('opencroc-theme');
1506
+ if(saved&&saved==='light'){S.theme='light';document.documentElement.setAttribute('data-theme','light');
1507
+ document.getElementById('theme-icon-dark').style.display='none';
1508
+ document.getElementById('theme-icon-light').style.display='';
1509
+ }
1510
+ })();
1511
+
1512
+ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1513
+ Keyboard Shortcuts System
1514
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
1515
+ let shortcutLegendTimer=null;
1516
+ function showShortcutLegend(){
1517
+ const el=document.getElementById('shortcut-legend');
1518
+ el.classList.add('visible');
1519
+ if(shortcutLegendTimer) clearTimeout(shortcutLegendTimer);
1520
+ shortcutLegendTimer=setTimeout(()=>el.classList.remove('visible'), 4000);
1521
+ }
1522
+
1523
+ document.addEventListener('keydown',e=>{
1524
+ // Don't capture when typing in inputs/selects
1525
+ const tag=e.target.tagName.toLowerCase();
1526
+ if(tag==='input'||tag==='textarea'||tag==='select') return;
1527
+
1528
+ const key=e.key.toLowerCase();
1529
+
1530
+ // Escape โ€” close file preview, close shortcut legend
1531
+ if(e.key==='Escape'){
1532
+ document.getElementById('file-preview').classList.remove('visible');
1533
+ document.getElementById('shortcut-legend').classList.remove('visible');
1534
+ return;
1535
+ }
1536
+ // ? โ€” Show shortcut legend
1537
+ if(key==='?'||e.key==='/'&&e.shiftKey){
1538
+ e.preventDefault(); showShortcutLegend(); return;
1539
+ }
1540
+ // 1 โ€” Dashboard view
1541
+ if(key==='1'){ e.preventDefault(); setView('dashboard'); return; }
1542
+ // 2 โ€” Pixel Office view
1543
+ if(key==='2'){ e.preventDefault(); setView('office'); return; }
1544
+ // S โ€” Scan
1545
+ if(key==='s'&&!e.ctrlKey&&!e.metaKey){ e.preventDefault(); doScan(); return; }
1546
+ // P โ€” Pipeline
1547
+ if(key==='p'&&!e.ctrlKey&&!e.metaKey){ e.preventDefault(); doPipeline(); return; }
1548
+ // T โ€” Tests
1549
+ if(key==='t'&&!e.ctrlKey&&!e.metaKey){ e.preventDefault(); doRunTests(); return; }
1550
+ // R โ€” Reports
1551
+ if(key==='r'&&!e.ctrlKey&&!e.metaKey){ e.preventDefault(); doReports(); return; }
1552
+ // X โ€” Reset
1553
+ if(key==='x'&&!e.ctrlKey&&!e.metaKey){ e.preventDefault(); doReset(); return; }
1554
+ // D โ€” Dark/Light toggle
1555
+ if(key==='d'&&!e.ctrlKey&&!e.metaKey){ e.preventDefault(); toggleTheme(); return; }
1556
+ });
1557
+
1558
+ (async()=>{
1559
+ preloadSprites();
1560
+ setupCanvas();
1561
+ await fetchProject();
1562
+ connectWS();
1563
+ addLog('OpenCroc Studio ready โ€” press ? for shortcuts','info',true);
1564
+ window.addEventListener('resize',()=>{layoutGraph();renderCanvas();renderPixelOffice();});
1565
+ })();
654
1566
  </script>
655
1567
  </body>
656
1568
  </html>