opencroc 1.6.8 โ†’ 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.
Files changed (2) hide show
  1. package/dist/web/index.html +981 -367
  2. package/package.json +1 -1
@@ -1,375 +1,714 @@
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: #070b14;
10
- --bg-panel: #101829;
11
- --bg-panel-2: #0f1524;
12
- --bg-card: #19263f;
13
- --bg-soft: #233354;
14
- --accent: #53d2a6;
15
- --accent-dim: #2b8f73;
16
- --red: #f05f78;
17
- --orange: #f6ad55;
18
- --blue: #59a5ff;
19
- --purple: #b58cff;
20
- --text: #e7edf8;
21
- --text-dim: #8b9cc0;
22
- --text-subtle: #6a7da5;
23
- --pixel-border: 1px solid rgba(131, 156, 211, 0.22);
24
- --shadow-lg: 0 14px 30px rgba(0, 0, 0, 0.38);
25
- --shadow-md: 0 8px 20px rgba(0, 0, 0, 0.28);
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);
26
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
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
27
71
  * { margin:0; padding:0; box-sizing:border-box; }
28
72
  body {
29
- background:
30
- radial-gradient(circle at 20% 10%, rgba(83, 210, 166, 0.12), transparent 35%),
31
- radial-gradient(circle at 80% 20%, rgba(89, 165, 255, 0.12), transparent 35%),
32
- linear-gradient(180deg, #05070d 0%, var(--bg-dark) 55%, #060810 100%);
33
- color:var(--text);
34
- font-family:'Courier New','Consolas',monospace;
35
- overflow:hidden;
36
- height:100vh;
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;
37
80
  }
38
81
 
39
- .app { display:grid; grid-template-rows:68px 1fr 238px; grid-template-columns:260px 1fr 340px; height:100vh; gap:8px; padding:8px; }
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
+ }
40
128
 
129
+ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
130
+ Header
131
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
41
132
  .header {
42
- grid-column:1/-1;
43
- background:linear-gradient(180deg, rgba(16, 24, 41, 0.95), rgba(15, 21, 36, 0.9));
44
- border:var(--pixel-border);
45
- border-radius:12px;
46
- display:flex;
47
- align-items:center;
48
- padding:0 16px;
49
- gap:12px;
50
- box-shadow:var(--shadow-lg);
51
- backdrop-filter: blur(8px);
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);
52
142
  }
53
143
  .header .logo {
54
- font-size:26px;
55
- width:40px;
56
- height:40px;
57
- border-radius:10px;
58
- display:grid;
59
- place-items:center;
60
- background:linear-gradient(135deg, rgba(83,210,166,.2), rgba(89,165,255,.2));
61
- border:1px solid rgba(131,156,211,.26);
62
- }
63
- .header .title-wrap { display:flex; flex-direction:column; gap:2px; }
64
- .header h1 { font-size:15px; color:var(--accent); letter-spacing:.4px; }
65
- .header .subtitle { font-size:10px; color:var(--text-subtle); }
66
- .header .actions { display:flex; gap:6px; }
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 */
67
159
  .view-switch {
68
- display:flex;
69
- background:rgba(13,20,34,.78);
70
- border:1px solid rgba(131,156,211,.26);
71
- border-radius:10px;
72
- padding:2px;
73
- margin-right:4px;
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;
74
166
  }
75
167
  .view-switch button {
76
- background:transparent;
77
- color:var(--text-dim);
78
- border:none;
79
- font-family:inherit;
80
- font-size:10px;
81
- padding:5px 10px;
82
- border-radius:8px;
83
- cursor:pointer;
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;
84
178
  }
179
+ .view-switch button:hover { color: var(--text); background: var(--bg-hover); }
85
180
  .view-switch button.active {
86
- background:rgba(83,210,166,.18);
87
- color:var(--accent);
88
- border:1px solid rgba(83,210,166,.35);
181
+ background: var(--accent-bg);
182
+ color: var(--accent);
183
+ box-shadow: 0 0 0 1px var(--border-accent);
89
184
  }
185
+
186
+ /* Buttons */
90
187
  .btn {
91
- background:linear-gradient(180deg, rgba(43,143,115,.9), rgba(31,109,88,.9));
92
- color:#fff;
93
- border:1px solid rgba(83,210,166,.35);
94
- padding:6px 12px;
95
- font-family:inherit;
96
- font-size:11px;
97
- border-radius:8px;
98
- cursor:pointer;
99
- transition:all .2s;
100
- box-shadow:var(--shadow-md);
101
- }
102
- .btn:hover { background:linear-gradient(180deg, rgba(83,210,166,.95), rgba(43,143,115,.95)); transform:translateY(-1px); }
103
- .btn:active { transform:translateY(0); }
104
- .btn:disabled { opacity:.4; cursor:not-allowed; transform:none; }
105
- .btn.danger { background:linear-gradient(180deg, rgba(155,49,72,.9), rgba(127,34,55,.9)); border-color:rgba(240,95,120,.38); }
106
- .btn.danger:hover { background:linear-gradient(180deg, rgba(240,95,120,.95), rgba(163,57,80,.95)); }
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; }
107
207
  .mode-select {
108
- background:var(--bg-card);
109
- color:var(--text);
110
- border:1px solid rgba(131,156,211,.26);
111
- border-radius:8px;
112
- font-family:inherit;
113
- font-size:10px;
114
- padding:6px 8px;
115
- min-width:88px;
116
- }
117
- .header .stats { margin-left:auto; display:flex; gap:8px; font-size:10px; color:var(--text-dim); }
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; }
118
221
  .header .stats > div {
119
- background:rgba(25, 38, 63, 0.62);
120
- border:1px solid rgba(131,156,211,.2);
121
- border-radius:10px;
122
- padding:6px 8px;
123
- min-width:72px;
124
- text-align:center;
125
- }
126
- .header .stats span { display:block; color:var(--accent); font-weight:bold; font-size:12px; margin-top:2px; }
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; }
127
231
  .conn-dot {
128
- width:10px;
129
- height:10px;
130
- border-radius:50%;
131
- background:var(--red);
132
- box-shadow:0 0 0 3px rgba(240,95,120,.2);
133
- transition:background .3s, box-shadow .3s;
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);
134
248
  }
135
- .conn-dot.on { background:var(--accent); box-shadow:0 0 0 3px rgba(83,210,166,.2); }
249
+ .theme-toggle:hover { background: var(--bg-hover); color: var(--text); }
250
+ .theme-toggle svg { width: 16px; height: 16px; }
136
251
 
252
+ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
253
+ Sidebar
254
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
137
255
  .sidebar {
138
- background:linear-gradient(180deg, rgba(16,24,41,.96), rgba(15,21,36,.92));
139
- border:var(--pixel-border);
140
- border-radius:12px;
141
- overflow-y:auto;
142
- padding:10px;
143
- box-shadow:var(--shadow-md);
144
- }
145
- .sidebar h3 { font-size:10px; text-transform:uppercase; color:var(--text-subtle); padding:10px 6px 6px; letter-spacing:1.1px; }
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; }
146
270
  .mod-item {
147
- padding:7px 10px;
148
- border-radius:8px;
149
- font-size:11px;
150
- cursor:pointer;
151
- display:flex;
152
- align-items:center;
153
- gap:6px;
154
- transition:background .15s, transform .15s;
155
- margin-bottom:3px;
156
- border:1px solid transparent;
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);
157
282
  }
158
283
  .mod-item:hover {
159
- background:rgba(35,51,84,.58);
160
- border-color:rgba(83,210,166,.2);
161
- transform:translateX(2px);
162
- }
163
- .dot { width:6px; height:6px; border-radius:50%; flex-shrink:0; }
164
- .dot.idle { background:var(--text-dim); }
165
- .dot.testing,.dot.working { background:var(--orange); animation:blink .7s infinite; }
166
- .dot.thinking { background:var(--blue); animation:blink 1s infinite; }
167
- .dot.passed,.dot.done { background:var(--accent); }
168
- .dot.failed,.dot.error { background:var(--red); }
169
- @keyframes blink { 50%{opacity:.3} }
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) } }
170
301
 
302
+ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
303
+ Main Content
304
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
171
305
  .main {
172
- position:relative;
173
- overflow:hidden;
174
- background:linear-gradient(180deg, rgba(16,24,41,.95), rgba(15,21,36,.92));
175
- border:var(--pixel-border);
176
- border-radius:12px;
177
- box-shadow:var(--shadow-lg);
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);
178
313
  }
179
314
  #graph-canvas { width:100%; height:100%; display:block; cursor:grab; }
180
- .view {
181
- position:absolute;
182
- inset:0;
183
- }
315
+ .view { position:absolute; inset:0; }
184
316
  .view.hidden { display:none; }
317
+
318
+ /* Pixel Office Stage */
185
319
  .pixel-stage {
186
- position:relative;
187
- width:100%;
188
- height:100%;
189
- overflow:hidden;
320
+ position: relative; width:100%; height:100%; overflow:hidden;
190
321
  background:
191
- linear-gradient(rgba(8,12,20,.2), rgba(8,12,20,.5)),
322
+ linear-gradient(rgba(8,12,20,.15), rgba(8,12,20,.4)),
192
323
  url('./assets/star/office_bg_small.webp') center/cover no-repeat;
193
324
  }
194
325
  .pixel-stage::before {
195
- content:'';
196
- position:absolute;
197
- inset:0;
326
+ content:''; position:absolute; inset:0;
198
327
  background-image:
199
- linear-gradient(rgba(111,141,194,.13) 1px, transparent 1px),
200
- linear-gradient(90deg, rgba(111,141,194,.13) 1px, transparent 1px);
201
- background-size: 28px 28px;
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;
202
331
  pointer-events:none;
203
332
  }
204
333
  .pixel-stage .asset {
205
- position:absolute;
206
- image-rendering:pixelated;
207
- z-index:2;
208
- filter: drop-shadow(0 6px 10px rgba(0,0,0,.35));
209
- opacity:.96;
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;
210
337
  }
211
338
  .pixel-stage .desk-asset { width:180px; bottom:90px; left:40px; }
212
339
  .pixel-stage .server-asset { width:110px; top:24px; right:34px; }
213
340
  .pixel-stage .coffee-asset { width:72px; top:100px; right:180px; }
214
- .pixel-stage .walls-asset { width:220px; top:10px; left:46%; transform:translateX(-50%); opacity:.45; }
215
- .pixel-agent-layer {
216
- position:absolute;
217
- inset:0;
218
- z-index:3;
219
- }
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; }
220
343
  .pixel-agent {
221
- position:absolute;
222
- width:44px;
223
- transition: left .55s ease, top .55s ease;
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;
224
352
  image-rendering:pixelated;
225
- filter: drop-shadow(0 5px 7px rgba(0,0,0,.36));
226
- }
227
- .pixel-agent.working,.pixel-agent.testing { animation: bob .45s infinite alternate; }
228
- .pixel-agent.thinking { animation: thinking 1s infinite; }
229
- .pixel-agent.error,.pixel-agent.failed { animation: shake .28s infinite; }
230
- .pixel-agent.done,.pixel-agent.passed { animation: pulse .85s infinite; }
231
- @keyframes bob { from { transform: translateY(0); } to { transform: translateY(-3px); } }
232
- @keyframes thinking { 0%,100%{transform:rotate(0)} 50%{transform:rotate(-3deg)} }
233
- @keyframes shake { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-2px)} 75%{transform:translateX(2px)} }
234
- @keyframes pulse { 0%,100%{transform:scale(1)} 50%{transform:scale(1.06)} }
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 */
235
370
  .pixel-label {
236
- position:absolute;
237
- transform:translate(-50%, -50%);
238
- background:rgba(6,10,18,.82);
239
- border:1px solid rgba(83,210,166,.35);
240
- border-radius:6px;
241
- padding:2px 6px;
242
- font-size:9px;
243
- color:#e9f3ff;
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);
244
378
  white-space:nowrap;
245
379
  z-index:4;
380
+ box-shadow: var(--shadow-sm);
381
+ backdrop-filter: blur(4px);
246
382
  }
247
- .pixel-kpis {
248
- position:absolute;
249
- left:14px;
250
- top:14px;
251
- display:flex;
252
- gap:8px;
253
- z-index:4;
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;
254
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; }
255
394
  .pixel-kpi {
256
- min-width:90px;
257
- background:rgba(10,16,28,.72);
258
- border:1px solid rgba(131,156,211,.24);
259
- border-radius:8px;
260
- padding:6px 8px;
261
- box-shadow:var(--shadow-md);
262
- }
263
- .pixel-kpi .t { font-size:9px; color:var(--text-subtle); }
264
- .pixel-kpi .v { font-size:12px; color:var(--accent); font-weight:bold; margin-top:2px; }
265
- .tooltip { position:absolute; background:rgba(25,38,63,.94); border:1px solid rgba(83,210,166,.35); border-radius:8px; padding:8px 12px; font-size:11px; pointer-events:none; z-index:100; display:none; max-width:280px; box-shadow:var(--shadow-md); }
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
+ }
266
439
  .tooltip.visible { display:block; }
440
+ .tooltip b { color: var(--accent); }
267
441
 
268
- .log-panel { background:linear-gradient(180deg, rgba(16,24,41,.96), rgba(15,21,36,.92)); border:var(--pixel-border); border-radius:12px; display:flex; flex-direction:column; overflow:hidden; box-shadow:var(--shadow-md); }
269
- .panel-tabs { display:flex; border-bottom:1px solid rgba(131,156,211,.18); background:rgba(25,38,63,.3); }
270
- .panel-tabs .tab { background:none; border:none; color:var(--text-dim); font-family:inherit; font-size:10px; padding:9px 12px; cursor:pointer; text-transform:uppercase; letter-spacing:1px; border-bottom:2px solid transparent; }
271
- .panel-tabs .tab.active { color:var(--accent); border-bottom-color:var(--accent); background:rgba(35,51,84,.45); }
272
- .panel-tabs .tab:hover { color:var(--text); }
273
- .file-list { flex:1; overflow-y:auto; padding:4px 8px; font-size:10px; }
274
- .file-item { padding:7px 8px; border-bottom:1px solid rgba(131,156,211,.12); cursor:pointer; border-radius:8px; transition:background .15s; }
275
- .file-item:hover { background:rgba(35,51,84,.55); }
276
- .file-item .fname { color:var(--accent); font-weight:bold; }
277
- .file-item .fmeta { color:var(--text-dim); font-size:9px; margin-top:2px; }
278
- .file-preview { position:fixed; top:60px; left:50%; transform:translateX(-50%); width:760px; max-height:82vh; background:var(--bg-panel); border:1px solid rgba(83,210,166,.35); border-radius:12px; z-index:200; display:none; flex-direction:column; box-shadow:var(--shadow-lg); }
279
- .file-preview.visible { display:flex; }
280
- .file-preview .fp-header { display:flex; justify-content:space-between; align-items:center; padding:10px 14px; border-bottom:1px solid rgba(131,156,211,.18); }
281
- .file-preview .fp-header h4 { font-size:12px; color:var(--accent); margin:0; }
282
- .file-preview .fp-close { background:none; border:none; color:var(--text-dim); font-size:18px; cursor:pointer; }
283
- .file-preview pre { flex:1; overflow:auto; padding:14px; margin:0; font-size:11px; line-height:1.5; color:var(--text); background:#0c1220; }
284
- .log-list { flex:1; overflow-y:auto; padding:4px 8px; font-size:10px; line-height:1.6; }
285
- .log-list .log-entry { padding:4px 2px; border-bottom:1px solid rgba(131,156,211,.08); word-break:break-all; }
286
- .log-list .log-entry.warn { color:var(--orange); }
287
- .log-list .log-entry.error { color:var(--red); }
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; }
288
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
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
289
540
  .office {
290
- grid-column:1/-1;
291
- background:
292
- linear-gradient(180deg, rgba(16,24,41,.96), rgba(15,21,36,.92));
293
- border:var(--pixel-border);
294
- border-radius:12px;
295
- display:flex;
296
- overflow-x:auto;
297
- padding:10px;
298
- gap:10px;
299
- box-shadow:var(--shadow-md);
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);
300
550
  }
301
551
  .desk {
302
- flex:0 0 190px;
303
- background:linear-gradient(180deg, rgba(25,38,63,.95), rgba(22,33,54,.9));
304
- border:1px solid rgba(131,156,211,.22);
305
- border-radius:10px;
306
- padding:10px;
307
- display:flex;
308
- flex-direction:column;
309
- align-items:center;
310
- gap:3px;
311
- position:relative;
312
- overflow:hidden;
313
- box-shadow:0 8px 18px rgba(0,0,0,.25);
314
- }
315
- .desk .badge { position:absolute; top:4px; right:4px; width:8px; height:8px; border-radius:50%; }
316
- .desk .croc-sprite { font-size:40px; position:relative; z-index:1; }
317
- .desk.idle .croc-sprite { animation:croc-idle 3s infinite; }
318
- .desk.working .croc-sprite { animation:croc-work .5s infinite alternate; }
319
- .desk.thinking .croc-sprite { animation:croc-think 1.2s infinite; }
320
- .desk.done .croc-sprite { animation:croc-done .8s 1; }
321
- .desk.error .croc-sprite { animation:croc-error .3s 3; }
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; }
322
581
  @keyframes croc-idle { 0%,90%,100%{transform:translateY(0)} 95%{transform:translateY(-3px)} }
323
- @keyframes croc-work { from{transform:translateY(0) rotate(-3deg)} to{transform:translateY(-5px) rotate(3deg)} }
324
- @keyframes croc-think { 0%,100%{transform:scale(1) rotate(0)} 25%{transform:scale(1.05) rotate(-2deg)} 75%{transform:scale(1.05) rotate(2deg)} }
325
- @keyframes croc-done { 0%{transform:scale(1)} 50%{transform:scale(1.2) translateY(-8px)} 100%{transform:scale(1)} }
326
- @keyframes croc-error { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-4px)} 75%{transform:translateX(4px)} }
327
- .desk .croc-name { font-size:11px; font-weight:bold; color:var(--accent); }
328
- .desk .croc-role { font-size:9px; color:var(--text-dim); text-transform:uppercase; }
329
- .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; }
330
- .desk .progress-bar { width:90%; height:3px; background:#222; border-radius:2px; margin-top:2px; overflow:hidden; }
331
- .desk .progress-bar .fill { height:100%; background:var(--accent); transition:width .3s; border-radius:2px; }
332
- .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
+ }
333
638
  </style>
334
639
  </head>
335
640
  <body>
336
641
  <div class="app">
337
642
  <header class="header">
338
- <div class="logo">๐ŸŠ</div>
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>
339
646
  <div class="title-wrap">
340
647
  <h1>OpenCroc Studio</h1>
341
648
  <div class="subtitle">Pixel Ops Dashboard ยท Real-time Multi-Agent Runtime</div>
342
649
  </div>
343
650
  <div class="actions">
344
651
  <div class="view-switch">
345
- <button id="view-dashboard" class="active">Dashboard</button>
346
- <button id="view-office">Pixel Office</button>
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>
347
660
  </div>
348
- <button class="btn" id="btn-scan" title="Scan project">๐Ÿ” Scan</button>
349
- <button class="btn" id="btn-pipeline" title="Run full pipeline">โ–ถ Pipeline</button>
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>
350
669
  <select id="run-mode" class="mode-select" title="Test run mode">
351
670
  <option value="auto">Auto</option>
352
671
  <option value="reuse">Reuse</option>
353
672
  <option value="managed">Managed</option>
354
673
  </select>
355
- <button class="btn" id="btn-run-tests" title="Run generated tests" disabled>๐Ÿงช Run Tests</button>
356
- <button class="btn" id="btn-reports" title="Generate reports" disabled>๐Ÿ“Š Reports</button>
357
- <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>
358
686
  </div>
687
+ <div class="spacer"></div>
359
688
  <div class="stats">
360
- <div>Modules <span id="s-mod">-</span></div>
361
- <div>Models <span id="s-mdl">-</span></div>
362
- <div>APIs <span id="s-api">-</span></div>
363
- <div>Tests <span id="s-files">-</span></div>
364
- <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>
365
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>
366
699
  <div class="conn-dot" id="conn-dot" title="WebSocket"></div>
367
700
  </header>
368
701
 
369
702
  <aside class="sidebar">
370
- <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>
371
707
  <div id="mod-list"></div>
372
- <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>
373
712
  <div id="agent-sidebar"></div>
374
713
  </aside>
375
714
 
@@ -385,8 +724,8 @@
385
724
  <img class="asset walls-asset" src="./assets/botreview/walls.png" alt="walls">
386
725
  <div class="pixel-kpis">
387
726
  <div class="pixel-kpi"><div class="t">Working</div><div class="v" id="kpi-working">0</div></div>
388
- <div class="pixel-kpi"><div class="t">Errors</div><div class="v" id="kpi-errors">0</div></div>
389
- <div class="pixel-kpi"><div class="t">Done</div><div class="v" id="kpi-done">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>
390
729
  </div>
391
730
  <div class="pixel-agent-layer" id="pixel-agent-layer"></div>
392
731
  </div>
@@ -396,10 +735,22 @@
396
735
 
397
736
  <div class="log-panel">
398
737
  <div class="panel-tabs">
399
- <button class="tab active" data-tab="log">๐Ÿ“‹ Log</button>
400
- <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>
401
- <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>
402
- <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>
403
754
  </div>
404
755
  <div class="log-list" id="log-list"></div>
405
756
  <div class="file-list" id="file-list" style="display:none"></div>
@@ -415,15 +766,144 @@
415
766
  <pre id="fp-code"></pre>
416
767
  </div>
417
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
+
418
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
+
419
810
  const S = {
420
811
  project:null, graph:{nodes:[],edges:[]}, agents:[], ws:null,
421
812
  pan:{x:0,y:0}, zoom:1, dragging:false, dragStart:{x:0,y:0},
422
813
  nodePos:new Map(), hoveredNode:null, running:false, _userPanned:false,
423
814
  generatedFiles:[], testMetrics:null, testQuality:null, reports:[], runMode:'auto',
424
- currentView:'dashboard'
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 },
425
831
  };
426
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
+
427
907
  async function fetchProject(){
428
908
  try{
429
909
  const r=await fetch('/api/project'); S.project=await r.json();
@@ -433,23 +913,23 @@ async function fetchProject(){
433
913
  }
434
914
  async function doScan(){
435
915
  if(S.running)return; S.running=true; updateBtns();
436
- addLog('๐Ÿ” Starting scan...');
916
+ addLog('๐Ÿ” Starting scan...','info',true);
437
917
  try{await fetch('/api/scan',{method:'POST'});}
438
918
  catch(e){addLog('Scan failed: '+e.message,'error');S.running=false;updateBtns();}
439
919
  }
440
920
  async function doPipeline(){
441
921
  if(S.running)return; S.running=true; updateBtns();
442
- addLog('โ–ถ Starting pipeline...');
922
+ addLog('โ–ถ Starting pipeline...','info',true);
443
923
  try{await fetch('/api/pipeline',{method:'POST'});}
444
924
  catch(e){addLog('Pipeline failed: '+e.message,'error');S.running=false;updateBtns();}
445
925
  }
446
926
  async function doReset(){
447
927
  try{await fetch('/api/reset',{method:'POST'});}catch(e){addLog('Reset failed','error');}
448
- S.running=false; updateBtns(); addLog('โน Agents reset');
928
+ S.running=false; updateBtns(); addLog('โน Agents reset','info',true);
449
929
  }
450
930
  async function doRunTests(){
451
931
  if(S.running)return; S.running=true; updateBtns();
452
- addLog('๐Ÿงช Starting test execution ('+S.runMode+')...');
932
+ addLog('๐Ÿงช Starting test execution ('+S.runMode+')...','info',true);
453
933
  try{
454
934
  const r=await fetch('/api/run-tests',{
455
935
  method:'POST',
@@ -465,7 +945,7 @@ async function doRunTests(){
465
945
  }
466
946
  async function doReports(){
467
947
  if(S.running)return; S.running=true; updateBtns();
468
- addLog('๐Ÿ“Š Generating reports...');
948
+ addLog('๐Ÿ“Š Generating reports...','info',true);
469
949
  try{await fetch('/api/reports/generate',{method:'POST'});}
470
950
  catch(e){addLog('Report gen failed: '+e.message,'error');S.running=false;updateBtns();}
471
951
  }
@@ -498,8 +978,8 @@ function connectWS(){
498
978
  badge.textContent=S.generatedFiles.length;badge.style.display='inline';
499
979
  }else if(m.type==='pipeline:complete'){
500
980
  S.running=false; updateBtns();
501
- if(m.payload.status==='success') addLog('โœ… Pipeline complete!');
502
- 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);
503
983
  setTimeout(fetchProject,500);
504
984
  }else if(m.type==='test:complete'){
505
985
  S.running=false; S.testMetrics=m.payload.metrics||null; S.testQuality=m.payload.quality||null; updateBtns(); renderResults();
@@ -514,7 +994,7 @@ function connectWS(){
514
994
  }
515
995
  const rb=document.getElementById('result-badge');
516
996
  rb.textContent=m.payload.total||0; rb.style.display='inline';
517
- rb.style.background=(met&&met.failed>0)?'var(--red)':'var(--accent)';
997
+ rb.className='tab-badge'+((met&&met.failed>0)?' alert':'');
518
998
  }else if(m.type==='reports:generated'){
519
999
  S.running=false; S.reports=m.payload||[]; updateBtns(); renderReports();
520
1000
  addLog('๐Ÿ“Š '+S.reports.length+' reports generated');
@@ -524,11 +1004,17 @@ function connectWS(){
524
1004
  S.ws.onclose=()=>{document.getElementById('conn-dot').classList.remove('on');setTimeout(connectWS,3000);};
525
1005
  }
526
1006
 
527
- function addLog(msg,level){
1007
+ function addLog(msg,level,useTypewriter){
528
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
+ }
529
1014
  const el=document.getElementById('log-list');
530
1015
  const d=document.createElement('div'); d.className='log-entry '+level;
531
- 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));
532
1018
  el.appendChild(d); el.scrollTop=el.scrollHeight;
533
1019
  }
534
1020
  function updateStats(){
@@ -620,79 +1106,81 @@ function renderCanvas(){
620
1106
  canvas.width=canvas.clientWidth*dpr; canvas.height=canvas.clientHeight*dpr;
621
1107
  ctx.scale(dpr,dpr);
622
1108
  const w=canvas.clientWidth,h=canvas.clientHeight;
623
- 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);
624
1115
  // Subtle grid
625
- ctx.strokeStyle='#151530';ctx.lineWidth=.5;
1116
+ ctx.strokeStyle=isDark?'rgba(148,163,184,.06)':'rgba(100,116,139,.08)';ctx.lineWidth=.5;
626
1117
  const gridStep=40;
627
1118
  for(let x=-2000;x<4000;x+=gridStep){ctx.beginPath();ctx.moveTo(x,-2000);ctx.lineTo(x,4000);ctx.stroke();}
628
1119
  for(let y=-2000;y<4000;y+=gridStep){ctx.beginPath();ctx.moveTo(-2000,y);ctx.lineTo(4000,y);ctx.stroke();}
629
1120
 
630
1121
  const edges=S.graph.edges||[],nodes=S.graph.nodes||[];
631
- const largeGraph=nodes.length>80;
632
1122
 
633
1123
  // Draw module cluster backgrounds
634
1124
  if(S.modMeta){
635
1125
  for(const[name,m] of S.modMeta){
636
1126
  ctx.beginPath(); ctx.arc(m.cx,m.cy,m.radius,0,Math.PI*2);
637
- ctx.fillStyle=m.color+'10'; ctx.fill();
638
- ctx.strokeStyle=m.color+'30'; ctx.lineWidth=2; ctx.setLineDash([6,4]); ctx.stroke(); ctx.setLineDash([]);
639
- // Module label at top of cluster
640
- ctx.font='bold 13px "Courier New"'; ctx.fillStyle=m.color+'cc'; ctx.textAlign='center'; ctx.textBaseline='bottom';
641
- 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);
642
1132
  }
643
1133
  }
644
1134
 
645
- // Draw "uses" edges with curved lines + gradient
1135
+ // Draw edges with curved lines + gradient
646
1136
  for(const e of edges){
647
- if(e.relation==='contains') continue; // never draw contains
1137
+ if(e.relation==='contains') continue;
648
1138
  const s=S.nodePos.get(e.source),t=S.nodePos.get(e.target);if(!s||!t)continue;
649
- const dx=t.x-s.x,dy=t.y-s.y,dist=Math.sqrt(dx*dx+dy*dy);
650
- // Curved edge via quadratic bezier, perpendicular offset
1139
+ const dx=t.x-s.x,dy=t.y-s.y;
651
1140
  const mx=(s.x+t.x)/2+dy*0.15, my=(s.y+t.y)/2-dx*0.15;
652
1141
  const grad=ctx.createLinearGradient(s.x,s.y,t.x,t.y);
653
- grad.addColorStop(0,'rgba(233,69,96,.5)'); // controller (red)
654
- grad.addColorStop(1,'rgba(78,204,163,.5)'); // model (green)
655
- 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;
656
1145
  ctx.beginPath();ctx.moveTo(s.x,s.y);ctx.quadraticCurveTo(mx,my,t.x,t.y);ctx.stroke();
657
- // Arrowhead
658
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;
659
- const a=Math.atan2(t.y-at2y,t.x-at2x),al=8;
660
- 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);
661
1149
  ctx.lineTo(t.x-al*Math.cos(a-.4),t.y-al*Math.sin(a-.4));
662
1150
  ctx.lineTo(t.x-al*Math.cos(a+.4),t.y-al*Math.sin(a+.4));ctx.closePath();ctx.fill();
663
1151
  }
664
1152
 
665
1153
  // Draw nodes
666
- const tc={model:'#4ecca3',controller:'#e94560',api:'#f39c12',dto:'#3498db',module:'#9b59b6'};
667
- 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'};
668
1156
  for(const n of nodes){
669
- if(n.type==='module') continue; // drawn as cluster bg instead
1157
+ if(n.type==='module') continue;
670
1158
  const p=S.nodePos.get(n.id);if(!p)continue;
671
- const sz=10,c=tc[n.type]||'#888',ol=sc[n.status]||'#444',hov=S.hoveredNode===n.id;
672
- // Glow for active status
673
- if(n.status==='testing'){ctx.shadowColor=sc.testing;ctx.shadowBlur=14;}
674
- else if(n.status==='passed'){ctx.shadowColor=sc.passed;ctx.shadowBlur=10;}
675
- else if(n.status==='failed'){ctx.shadowColor=sc.failed;ctx.shadowBlur=12;}
676
- // 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;}
677
1163
  ctx.beginPath();ctx.arc(p.x,p.y,sz,0,Math.PI*2);
678
1164
  ctx.fillStyle=c;ctx.fill();
679
1165
  ctx.shadowBlur=0;
680
- ctx.strokeStyle=hov?'#fff':ol;ctx.lineWidth=hov?3:1.5;ctx.stroke();
681
- // Icon
682
- ctx.font=(sz)+'px serif';ctx.textAlign='center';ctx.textBaseline='middle';
683
- ctx.fillText(n.type==='model'?'๐Ÿ“ฆ':'๐ŸŽฎ',p.x,p.y);
684
- // 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
685
1168
  if(S.zoom>0.4||hov){
686
- ctx.font='9px "Courier New"';ctx.fillStyle=hov?'#fff':'#aaa';ctx.textAlign='center';ctx.textBaseline='top';
687
- 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);
688
1173
  }
689
1174
  }
690
1175
  ctx.restore();
691
1176
 
692
1177
  // HUD legend
693
- ctx.font='10px "Courier New"';ctx.textAlign='left';
694
- const leg=[['๐Ÿ“ฆ Model','#4ecca3'],['๐ŸŽฎ Controller','#e94560'],['โ”€โ”€โ”€ uses','rgba(200,100,130,.8)']];
695
- 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);}
696
1184
  }
697
1185
 
698
1186
  function setupCanvas(){
@@ -727,8 +1215,7 @@ function updateAll(){
727
1215
  }
728
1216
  function renderModList(){
729
1217
  const el=document.getElementById('mod-list'),mods=S.graph.nodes.filter(n=>n.type==='module');
730
- if(!mods.length){el.innerHTML='<div style="padding:8px;color:#555;font-size:10px">No modules found</div>';return;}
731
- // 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;}
732
1219
  const sorted=mods.sort((a,b)=>{
733
1220
  const ca=S.modMeta&&S.modMeta.get(a.label)?S.modMeta.get(a.label).count:0;
734
1221
  const cb=S.modMeta&&S.modMeta.get(b.label)?S.modMeta.get(b.label).count:0;
@@ -737,10 +1224,10 @@ function renderModList(){
737
1224
  el.innerHTML=sorted.map(m=>{
738
1225
  const meta=S.modMeta&&S.modMeta.get(m.label);
739
1226
  const cnt=meta?meta.count:'';
740
- const col=meta?meta.color:'#888';
741
- 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+'">'
742
1229
  +'<div class="dot '+m.status+'"></div>'
743
- +esc(m.label)+' <span style="color:#555;font-size:9px">('+cnt+')</span></div>';
1230
+ +esc(m.label)+'<span class="mod-count">'+cnt+'</span></div>';
744
1231
  }).join('');
745
1232
  // Click to navigate
746
1233
  el.querySelectorAll('.mod-item').forEach(item=>{
@@ -759,19 +1246,19 @@ function renderModList(){
759
1246
  }
760
1247
  function renderAgentSB(){
761
1248
  document.getElementById('agent-sidebar').innerHTML=S.agents.map(a=>
762
- '<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>'
763
1250
  ).join('');
764
1251
  }
765
1252
  function renderOffice(){
766
- const dd={parser:'๐Ÿ’ป',analyzer:'๐Ÿ“Š',tester:'๐Ÿงช',healer:'๐Ÿ”ง',planner:'๐Ÿ“‹',reporter:'๐Ÿ“'};
767
1253
  document.getElementById('croc-office').innerHTML=S.agents.map(a=>{
768
1254
  const prog=typeof a.progress==='number'?a.progress:0;
1255
+ const roleIcon=ROLE_ICONS[a.role]||ICONS.croc;
769
1256
  return '<div class="desk '+a.status+'"><div class="badge dot '+a.status+'"></div>'+
770
- '<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>'+
771
1258
  '<div class="croc-role">'+esc(a.role)+'</div>'+
772
1259
  '<div class="croc-task">'+(a.currentTask?esc(a.currentTask):'')+'</div>'+
773
1260
  '<div class="progress-bar"><div class="fill" style="width:'+prog+'%"></div></div>'+
774
- '<div class="desk-items">'+(dd[a.role]||'')+'</div></div>';
1261
+ '<div class="desk-icon">'+roleIcon+'</div></div>';
775
1262
  }).join('');
776
1263
  }
777
1264
  function renderPixelOffice(){
@@ -790,15 +1277,70 @@ function renderPixelOffice(){
790
1277
  document.getElementById('kpi-errors').textContent=String(errors);
791
1278
  document.getElementById('kpi-done').textContent=String(done);
792
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
+
793
1285
  el.innerHTML=S.agents.map((a,i)=>{
794
1286
  const p=presets[i%presets.length];
795
1287
  const x=Math.max(16,Math.round(stageWidth*p.x));
796
1288
  const y=Math.max(20,Math.round(stageHeight*p.y));
797
1289
  const roleSprite=roles[i%roles.length];
798
1290
  const labelY=Math.max(16,y-12);
799
- return '<img class="pixel-agent '+a.status+'" src="./assets/botreview/'+roleSprite+'.png" style="left:'+x+'px;top:'+y+'px" alt="'+esc(a.name)+'" />'
800
- +'<div class="pixel-label" style="left:'+(x+22)+'px;top:'+labelY+'px">'+esc(a.name)+' ยท '+esc(a.status)+'</div>';
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>';
801
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
+ }
802
1344
  }
803
1345
  function setView(view){
804
1346
  S.currentView=view;
@@ -841,7 +1383,7 @@ document.querySelectorAll('.panel-tabs .tab').forEach(tab=>{
841
1383
  // File list rendering
842
1384
  function renderFileList(){
843
1385
  const el=document.getElementById('file-list');
844
- 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;}
845
1387
  el.innerHTML=S.generatedFiles.map((f,i)=>
846
1388
  '<div class="file-item" data-idx="'+i+'"><div class="fname">'+esc(f.filePath.split('/').pop()||f.filePath)+'</div>'+
847
1389
  '<div class="fmeta">'+esc(f.module)+' / '+esc(f.chain)+' โ€” '+f.lines+' lines</div></div>'
@@ -870,12 +1412,12 @@ document.addEventListener('keydown',e=>{
870
1412
  // Test Results rendering
871
1413
  function renderResults(){
872
1414
  const el=document.getElementById('results-panel');
873
- 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;}
874
1416
  const q=S.testQuality;
875
1417
  if(!S.testMetrics&&q){
876
1418
  el.innerHTML='<div style="padding:10px">'
877
1419
  +'<div style="font-size:13px;font-weight:bold;margin-bottom:8px">๐Ÿงช Test Execution Results</div>'
878
- +'<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>'
879
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>'
880
1422
  +'<div style="font-size:10px;color:var(--text-dim)">Auth: '+q.authStatus+' | Backend: '+q.backendStatus+'</div>'
881
1423
  +'<div style="font-size:10px;color:var(--text-dim);margin-top:4px">Reasons: '+(q.reasons&&q.reasons.length?q.reasons.join(', '):'-')+'</div>'
@@ -888,15 +1430,15 @@ function renderResults(){
888
1430
  let qualityHtml='';
889
1431
  if(q){
890
1432
  const gateColor=q.level==='fail'?'var(--red)':q.level==='warn'?'var(--orange)':'var(--accent)';
891
- 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)">'
892
1434
  +'<div style="font-size:12px;font-weight:bold;margin-bottom:8px">๐Ÿงญ Execution Quality</div>'
893
- +'<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:10px">'
894
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Gate: <span style="color:'+gateColor+';font-weight:bold">'+q.level.toUpperCase()+'</span></div>'
895
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Setup Fail: '+q.setupFail+'</div>'
896
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Skip Ratio: '+Math.round((q.skipRatio||0)*100)+'%</div>'
897
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Auth Fail Ratio: '+Math.round((q.authFailRatio||0)*100)+'%</div>'
898
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Effective Rate: '+Math.round((q.effectiveExecutionRate||0)*100)+'%</div>'
899
- +'<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>'
900
1442
  +'</div>'
901
1443
  +'<div style="font-size:10px;color:var(--text-dim);margin-top:6px">Reasons: '+(q.reasons&&q.reasons.length?q.reasons.join(', '):'-')+'</div>'
902
1444
  +'</div>';
@@ -904,12 +1446,12 @@ function renderResults(){
904
1446
  el.innerHTML='<div style="padding:10px">'
905
1447
  +'<div style="font-size:13px;font-weight:bold;margin-bottom:8px">๐Ÿงช Test Execution Results</div>'
906
1448
  +'<div style="display:flex;gap:12px;margin-bottom:10px">'
907
- +'<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>'
908
- +'<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>'
909
- +'<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>'
910
- +'<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>'
911
1453
  +'</div>'
912
- +'<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">'
913
1455
  +'<div style="height:100%;width:'+passRate+'%;background:'+barColor+';transition:width .5s"></div></div>'
914
1456
  +'<div style="text-align:center;font-size:10px;color:var(--text-dim);margin-top:4px">Pass Rate: '+passRate+'% ('+total+' total)</div>'
915
1457
  +qualityHtml
@@ -919,7 +1461,7 @@ function renderResults(){
919
1461
  // Reports rendering
920
1462
  function renderReports(){
921
1463
  const el=document.getElementById('reports-panel');
922
- 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;}
923
1465
  el.innerHTML='<div style="padding:10px"><div style="font-size:13px;font-weight:bold;margin-bottom:8px">๐Ÿ“Š Generated Reports</div>'
924
1466
  +S.reports.map(r=>{
925
1467
  const icon=r.format==='html'?'๐ŸŒ':r.format==='json'?'๐Ÿ“‹':'๐Ÿ“';
@@ -947,8 +1489,80 @@ function renderReports(){
947
1489
  });
948
1490
  }
949
1491
 
950
- (async()=>{setupCanvas();await fetchProject();connectWS();addLog('๐ŸŠ OpenCroc Studio ready');
951
- window.addEventListener('resize',()=>{layoutGraph();renderCanvas();renderPixelOffice();});})();
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
+ })();
952
1566
  </script>
953
1567
  </body>
954
1568
  </html>