anentrypoint-design 0.0.60 → 0.0.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anentrypoint-design",
3
- "version": "0.0.60",
3
+ "version": "0.0.62",
4
4
  "description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
5
5
  "type": "module",
6
6
  "main": "./dist/247420.js",
@@ -19,8 +19,10 @@
19
19
  "./desktop/theme.css": "./src/desktop/theme.css",
20
20
  "./desktop/wm.css": "./src/desktop/wm.css",
21
21
  "./desktop/launcher.css": "./src/desktop/launcher.css",
22
+ "./desktop/validate.css": "./src/desktop/validate.css",
22
23
  "./desktop/icons.js": "./src/desktop/icons.js",
23
24
  "./desktop/shell.js": "./src/desktop/shell.js",
25
+ "./desktop/freddie-dashboard.js": "./src/desktop/freddie-dashboard.js",
24
26
  "./page-html": {
25
27
  "import": "./src/page-html.js",
26
28
  "default": "./src/page-html.js"
@@ -0,0 +1,395 @@
1
+ const ROUTES = [
2
+ { path: 'projects', label: 'projects', glyph: '◆' },
3
+ { path: 'home', label: 'home', glyph: '⌂' },
4
+ { path: 'chat', label: 'chat', glyph: '⌨' },
5
+ { path: 'sessions', label: 'sessions', glyph: '✉' },
6
+ { path: 'agents', label: 'agents', glyph: '◈' },
7
+ { path: 'analytics', label: 'analytics', glyph: '◉' },
8
+ { path: 'models', label: 'models', glyph: '◎' },
9
+ { path: 'logs', label: 'logs', glyph: '☰' },
10
+ { path: 'cron', label: 'cron', glyph: '◷' },
11
+ { path: 'skills', label: 'skills', glyph: '◈' },
12
+ { path: 'config', label: 'config', glyph: '⚙' },
13
+ { path: 'env', label: 'keys', glyph: '⚿' },
14
+ { path: 'tools', label: 'tools', glyph: '⚒' },
15
+ { path: 'batch', label: 'batch', glyph: '⊞' },
16
+ { path: 'gateway', label: 'gateway', glyph: '⇌' },
17
+ ];
18
+
19
+ function el(tag, cls, attrs) {
20
+ const e = document.createElement(tag);
21
+ if (cls) e.className = cls;
22
+ if (attrs) for (const k of Object.keys(attrs)) {
23
+ if (k === 'on' && attrs.on) for (const ev of Object.keys(attrs.on)) e.addEventListener(ev, attrs.on[ev]);
24
+ else if (k === 'html') e.innerHTML = attrs.html;
25
+ else if (k === 'text') e.textContent = attrs.text;
26
+ else e.setAttribute(k, attrs[k]);
27
+ }
28
+ return e;
29
+ }
30
+
31
+ function ensureStyles() {
32
+ if (document.getElementById('freddie-dashboard-style')) return;
33
+ const s = document.createElement('style');
34
+ s.id = 'freddie-dashboard-style';
35
+ s.textContent = `
36
+ .fdash { display: flex; height: 100%; font-family: var(--ff-ui, Nunito, sans-serif); color: var(--ink, #1F1B16); }
37
+ .fdash-side { flex: 0 0 180px; background: var(--panel-1, #ECE6D5); border-right: 1px solid var(--panel-2, #DDD3BC); padding: 8px 0; overflow-y: auto; }
38
+ .fdash-nav { display: flex; flex-direction: column; gap: 2px; }
39
+ .fdash-nav button { text-align: left; padding: 6px 12px; background: transparent; color: inherit; border: 0; cursor: pointer; font-family: inherit; font-size: 12px; border-left: 4px solid transparent; }
40
+ .fdash-nav button:hover { background: var(--panel-hover, rgba(0,0,0,0.04)); }
41
+ .fdash-nav button.active { background: var(--panel-select, #C8E4CA); border-left-color: var(--panel-accent, #3F8A4A); font-weight: 600; }
42
+ .fdash-nav .glyph { display: inline-block; width: 16px; opacity: 0.7; font-family: var(--ff-mono, JetBrains Mono, monospace); }
43
+ .fdash-main { flex: 1; padding: 12px 16px; overflow: auto; min-width: 0; }
44
+ .fdash-h { font-size: 14px; font-weight: 700; margin: 0 0 8px; }
45
+ .fdash-kpi { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
46
+ .fdash-kpi .k { background: var(--panel-2, #DDD3BC); padding: 6px 12px; border-radius: var(--r-1, 6px); min-width: 80px; }
47
+ .fdash-kpi .k .v { font-size: 18px; font-weight: 700; font-family: var(--ff-mono, JetBrains Mono, monospace); }
48
+ .fdash-kpi .k .l { font-size: 10px; opacity: 0.7; text-transform: uppercase; }
49
+ .fdash-panel { background: var(--panel-1, #ECE6D5); border-radius: var(--r-1, 6px); padding: 8px 12px; margin-bottom: 8px; }
50
+ .fdash-panel h3 { font-size: 12px; font-weight: 700; margin: 0 0 6px; opacity: 0.8; text-transform: uppercase; }
51
+ .fdash-panel pre { font-family: var(--ff-mono, JetBrains Mono, monospace); font-size: 11px; white-space: pre-wrap; word-break: break-all; margin: 0; max-height: 280px; overflow: auto; }
52
+ .fdash-panel table { width: 100%; border-collapse: collapse; font-size: 12px; }
53
+ .fdash-panel th, .fdash-panel td { padding: 4px 8px; text-align: left; border-bottom: 1px solid var(--panel-2, #DDD3BC); }
54
+ .fdash-panel th { font-weight: 600; opacity: 0.7; font-size: 10px; text-transform: uppercase; }
55
+ .fdash-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px; }
56
+ .fdash-row .code { font-family: var(--ff-mono, JetBrains Mono, monospace); opacity: 0.6; }
57
+ .fdash-row .meta { margin-left: auto; opacity: 0.5; font-size: 11px; }
58
+ .fdash-form { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; margin-bottom: 8px; }
59
+ .fdash-form input, .fdash-form textarea, .fdash-form select { font: inherit; padding: 4px 8px; border: 1px solid var(--panel-2, #DDD3BC); border-radius: var(--r-1, 6px); background: var(--panel-0, #F5F0E4); color: inherit; }
60
+ .fdash-form button { padding: 4px 12px; background: var(--panel-accent, #3F8A4A); color: #fff; border: 0; border-radius: var(--r-1, 6px); cursor: pointer; }
61
+ .fdash-form button.danger { background: #c44; }
62
+ .fdash-chip { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; margin: 2px; }
63
+ .fdash-chip.ok { background: var(--panel-select, #C8E4CA); }
64
+ .fdash-chip.miss { background: var(--panel-2, #DDD3BC); opacity: 0.6; }
65
+ .fdash-empty { padding: 20px; text-align: center; opacity: 0.5; font-size: 12px; }
66
+ @media (max-width: 600px) { .fdash-side { flex: 0 0 56px; } .fdash-nav button .label { display: none; } }
67
+ `;
68
+ document.head.appendChild(s);
69
+ }
70
+
71
+ function kpi(items) {
72
+ const c = el('div', 'fdash-kpi');
73
+ for (const [v, l] of items) {
74
+ const k = el('div', 'k');
75
+ k.appendChild(el('div', 'v', { text: String(v) }));
76
+ k.appendChild(el('div', 'l', { text: String(l) }));
77
+ c.appendChild(k);
78
+ }
79
+ return c;
80
+ }
81
+
82
+ function panel(title, body, count) {
83
+ const p = el('div', 'fdash-panel');
84
+ const h = el('h3'); h.textContent = title + (count != null ? ' · ' + count : '');
85
+ p.appendChild(h);
86
+ if (body instanceof Node) p.appendChild(body);
87
+ else if (Array.isArray(body)) for (const n of body) if (n) p.appendChild(n);
88
+ else if (typeof body === 'string') { const pre = el('pre'); pre.textContent = body; p.appendChild(pre); }
89
+ return p;
90
+ }
91
+
92
+ function row(opts) {
93
+ const r = el('div', 'fdash-row');
94
+ if (opts.code) r.appendChild(el('span', 'code', { text: opts.code }));
95
+ r.appendChild(el('span', 'title', { text: opts.title || '' }));
96
+ if (opts.sub) r.appendChild(el('span', 'sub', { text: ' — ' + opts.sub }));
97
+ if (opts.meta) r.appendChild(el('span', 'meta', { text: opts.meta }));
98
+ return r;
99
+ }
100
+
101
+ function table(headers, rows) {
102
+ const t = el('table');
103
+ const thead = el('thead'); const trh = el('tr');
104
+ for (const h of headers) trh.appendChild(el('th', null, { text: h }));
105
+ thead.appendChild(trh); t.appendChild(thead);
106
+ const tb = el('tbody');
107
+ for (const r of rows) {
108
+ const tr = el('tr');
109
+ for (const c of r) tr.appendChild(el('td', null, { text: String(c) }));
110
+ tb.appendChild(tr);
111
+ }
112
+ t.appendChild(tb);
113
+ return t;
114
+ }
115
+
116
+ function pre(obj) { return el('pre', null, { text: typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2) }); }
117
+
118
+ export function createFreddieDashboard({ instance, bootHost }) {
119
+ ensureStyles();
120
+ const root = el('div', 'fdash');
121
+ const side = el('div', 'fdash-side');
122
+ const nav = el('div', 'fdash-nav');
123
+ const main = el('div', 'fdash-main');
124
+ side.appendChild(nav);
125
+ root.appendChild(side); root.appendChild(main);
126
+
127
+ let active = 'home';
128
+ let host = instance.host || null;
129
+
130
+ function setActive(p) {
131
+ active = p;
132
+ for (const b of nav.querySelectorAll('button')) b.classList.toggle('active', b.dataset.path === p);
133
+ render();
134
+ }
135
+
136
+ for (const r of ROUTES) {
137
+ const b = el('button', null, { 'data-path': r.path, on: { click: () => setActive(r.path) } });
138
+ b.appendChild(el('span', 'glyph', { text: r.glyph }));
139
+ b.appendChild(document.createTextNode(' '));
140
+ b.appendChild(el('span', 'label', { text: r.label }));
141
+ nav.appendChild(b);
142
+ }
143
+
144
+ async function ensureHost() {
145
+ if (host) return host;
146
+ if (typeof bootHost !== 'function') throw new Error('createFreddieDashboard: instance.host or bootHost required');
147
+ host = instance.host = await bootHost({ fs: instance.fs });
148
+ return host;
149
+ }
150
+
151
+ async function render() {
152
+ main.innerHTML = '';
153
+ main.appendChild(el('h2', 'fdash-h', { text: 'freddie · ' + instance.id + ' · ' + active }));
154
+ const h = await ensureHost();
155
+ const page = PAGES[active] || PAGES.home;
156
+ try {
157
+ const body = await page(h, instance);
158
+ const arr = Array.isArray(body) ? body : [body];
159
+ for (const n of arr) if (n) main.appendChild(n);
160
+ } catch (e) {
161
+ main.appendChild(panel('error', el('pre', null, { text: String(e && e.stack || e) })));
162
+ }
163
+ }
164
+
165
+ const PAGES = {
166
+ async projects(h) {
167
+ const list = h.pi.projects.list();
168
+ const active = h.pi.projects.active();
169
+ const form = el('form', 'fdash-form', { on: { submit: (ev) => {
170
+ ev.preventDefault();
171
+ try { h.pi.projects.create({ name: ev.target.elements.name.value, path: ev.target.elements.path.value }); render(); }
172
+ catch (e) { alert(e.message); }
173
+ } } });
174
+ form.appendChild(el('input', null, { name: 'name', placeholder: 'project name', required: 'true' }));
175
+ form.appendChild(el('input', null, { name: 'path', placeholder: '/path' }));
176
+ form.appendChild(el('button', null, { type: 'submit', text: 'add' }));
177
+ const rows = list.map(p => {
178
+ const r = el('div', 'fdash-row');
179
+ r.appendChild(el('span', 'code', { text: p.name === active?.name ? '●' : '○' }));
180
+ r.appendChild(el('span', null, { text: p.name + (p.name === active?.name ? ' (active)' : '') }));
181
+ r.appendChild(el('span', 'meta', { text: p.path }));
182
+ if (p.name !== 'default') {
183
+ const del = el('button', null, { type: 'button', text: 'remove', on: { click: () => { try { h.pi.projects.remove(p.name); render(); } catch (e) { alert(e.message); } } } });
184
+ r.appendChild(del);
185
+ }
186
+ if (p.name !== active?.name) {
187
+ const sw = el('button', null, { type: 'button', text: 'switch', on: { click: () => { h.pi.projects.setActive(p.name); render(); } } });
188
+ r.appendChild(sw);
189
+ }
190
+ return r;
191
+ });
192
+ return [
193
+ kpi([[list.length, 'projects'], [active?.name || '—', 'active'], [active?.path || '—', 'path']]),
194
+ panel('add a project', form),
195
+ panel('all projects', rows, list.length),
196
+ ];
197
+ },
198
+ async home(h) {
199
+ const sessions = await h.pi.sessions.list();
200
+ const tools = h.pi.tools.size;
201
+ const skills = h.pi.skills.size;
202
+ const health = h.pi.health();
203
+ return [
204
+ kpi([[sessions.length, 'sessions'], [tools, 'tools'], [skills, 'skills']]),
205
+ panel('quick start', table(['action', 'how'], [
206
+ ['open chat', "click 'chat' in sidebar"],
207
+ ['list tools', '/tools in chat or → tools tab'],
208
+ ['list skills', '/skills in chat or → skills tab'],
209
+ ['set api key', '→ keys tab → set ENV var'],
210
+ ])),
211
+ panel('host', table(['key', 'value'], Object.entries(health))),
212
+ ];
213
+ },
214
+ async chat(h, instance) {
215
+ const note = el('div', 'fdash-empty', { text: 'chat lives in its own thebird app — opening chat window…' });
216
+ try {
217
+ if (window.__debug?.shell?.openApp) window.__debug.shell.openApp('chat');
218
+ } catch {}
219
+ return [panel('chat', note), panel('cli surface', table(['command', 'description'], [...h.pi.cli.values()].map(c => [c.name, c.description])))];
220
+ },
221
+ async sessions(h) {
222
+ const list = await h.pi.sessions.list();
223
+ return [
224
+ kpi([[list.length, 'total sessions']]),
225
+ panel('recent sessions', list.length === 0
226
+ ? el('div', 'fdash-empty', { text: 'no sessions yet — start a chat' })
227
+ : table(['id', 'title', 'platform', 'model', 'turns'], list.map(s => [(s.id || '').slice(0, 8), s.title || '—', s.platform, s.model || '—', s.turn_count])), list.length),
228
+ ];
229
+ },
230
+ async agents(h) {
231
+ const a = await h.pi.agents();
232
+ return [
233
+ kpi([[a.count, 'active'], [a.turns, 'turns']]),
234
+ panel('overview', table(['key', 'value'], [
235
+ ['total turns', String(a.turns)],
236
+ ['active session', a.active || '(none)'],
237
+ ['last activity', a.last_activity ? new Date(a.last_activity).toLocaleString() : '—'],
238
+ ])),
239
+ ];
240
+ },
241
+ async analytics(h) {
242
+ const list = await h.pi.sessions.list();
243
+ const tools = [...h.pi.tools.values()];
244
+ const byPlatform = list.reduce((a, s) => { const k = s.platform || '?'; a[k] = (a[k] || 0) + 1; return a; }, {});
245
+ const byModel = list.reduce((a, s) => { const k = s.model || '?'; a[k] = (a[k] || 0) + 1; return a; }, {});
246
+ return [
247
+ kpi([[list.length, 'sessions'], [tools.length, 'tools']]),
248
+ panel('sessions by platform', Object.keys(byPlatform).length === 0 ? el('div', 'fdash-empty', { text: 'no data' }) : table(['platform', 'count'], Object.entries(byPlatform))),
249
+ panel('sessions by model', Object.keys(byModel).length === 0 ? el('div', 'fdash-empty', { text: 'no data' }) : table(['model', 'count'], Object.entries(byModel))),
250
+ panel('tools', table(['name', 'description'], tools.map(t => [t.name, (t.description || '').slice(0, 80)]))),
251
+ ];
252
+ },
253
+ async models(h) {
254
+ const cfg = h.pi.config.load();
255
+ const agent = cfg.agent || {};
256
+ const form = el('form', 'fdash-form', { on: { submit: (ev) => {
257
+ ev.preventDefault();
258
+ h.pi.config.saveValue('agent.provider', ev.target.elements.provider.value);
259
+ h.pi.config.saveValue('agent.model', ev.target.elements.model.value);
260
+ render();
261
+ } } });
262
+ form.appendChild(el('input', null, { name: 'provider', placeholder: 'provider', value: agent.provider || '' }));
263
+ form.appendChild(el('input', null, { name: 'model', placeholder: 'model id', value: agent.model || '' }));
264
+ form.appendChild(el('button', null, { type: 'submit', text: 'update' }));
265
+ return [
266
+ kpi([[agent.provider || '—', 'provider'], [agent.model || '—', 'model']]),
267
+ panel('active model', table(['key', 'value'], [
268
+ ['provider', agent.provider || '(unset)'],
269
+ ['model', agent.model || '(unset)'],
270
+ ['max_iterations', String(agent.max_iterations || '—')],
271
+ ])),
272
+ panel('change model', form),
273
+ ];
274
+ },
275
+ async logs(h) {
276
+ const dbg = h.pi.debug();
277
+ return [
278
+ panel('host debug snapshot', pre(dbg)),
279
+ ];
280
+ },
281
+ async cron(h) {
282
+ const list = await h.pi.cron.list();
283
+ const form = el('form', 'fdash-form', { on: { submit: async (ev) => {
284
+ ev.preventDefault();
285
+ try { await h.pi.cron.create({ cron: ev.target.elements.cron.value, prompt: ev.target.elements.prompt.value }); render(); }
286
+ catch (e) { alert(e.message); }
287
+ } } });
288
+ form.appendChild(el('input', null, { name: 'cron', placeholder: '* * * * *', required: 'true' }));
289
+ form.appendChild(el('input', null, { name: 'prompt', placeholder: 'prompt', required: 'true' }));
290
+ form.appendChild(el('button', null, { type: 'submit', text: 'create' }));
291
+ const tbl = list.length === 0
292
+ ? el('div', 'fdash-empty', { text: 'no cron jobs' })
293
+ : table(['id', 'cron', 'prompt', 'enabled'], list.map(j => [j.id, j.cron, (j.prompt || '').slice(0, 40), j.enabled ? 'yes' : 'no']));
294
+ return [
295
+ kpi([[list.length, 'cron jobs']]),
296
+ panel('add job', form),
297
+ panel('jobs', tbl, list.length),
298
+ ];
299
+ },
300
+ async skills(h) {
301
+ const list = [...h.pi.skills.values()];
302
+ const byCat = list.reduce((a, s) => { (a[s.category || 'other'] = a[s.category || 'other'] || []).push(s); return a; }, {});
303
+ const out = [kpi([[list.length, 'skills'], [Object.keys(byCat).length, 'categories']])];
304
+ for (const [cat, ss] of Object.entries(byCat)) {
305
+ out.push(panel(cat, table(['name', 'description'], ss.map(s => [s.shortName || s.name, (s.description || '').slice(0, 80)])), ss.length));
306
+ }
307
+ return out;
308
+ },
309
+ async config(h) {
310
+ const cfg = h.pi.config.load();
311
+ const profiles = h.pi.profiles.list();
312
+ const commands = h.pi.commands.list();
313
+ const form = el('form', 'fdash-form', { on: { submit: (ev) => {
314
+ ev.preventDefault();
315
+ let v = ev.target.elements.value.value;
316
+ try { v = JSON.parse(v); } catch {}
317
+ h.pi.config.saveValue(ev.target.elements.key.value, v);
318
+ render();
319
+ } } });
320
+ form.appendChild(el('input', null, { name: 'key', placeholder: 'dotted.key', required: 'true' }));
321
+ form.appendChild(el('input', null, { name: 'value', placeholder: 'value (json or string)', required: 'true' }));
322
+ form.appendChild(el('button', null, { type: 'submit', text: 'save' }));
323
+ return [
324
+ kpi([[profiles.length, 'profiles'], [commands.length, 'commands'], [cfg._config_version || 0, 'config version']]),
325
+ panel('set value', form),
326
+ panel('commands', table(['name', 'category', 'description'], commands.map(c => [c.name, c.category, c.description])), commands.length),
327
+ panel('active config', pre(cfg)),
328
+ ];
329
+ },
330
+ async env(h) {
331
+ const list = h.pi.env.list();
332
+ const setCount = list.filter(k => k.set).length;
333
+ const chips = el('div');
334
+ for (const k of list) {
335
+ const c = el('span', 'fdash-chip ' + (k.set ? 'ok' : 'miss'), { text: k.key + (k.set ? ' ✓' : '') });
336
+ c.addEventListener('click', () => {
337
+ const v = prompt('set ' + k.key + ' (empty to unset):');
338
+ if (v == null) return;
339
+ h.pi.env.set(k.key, v);
340
+ render();
341
+ });
342
+ chips.appendChild(c);
343
+ }
344
+ return [
345
+ kpi([[setCount, 'set'], [list.length - setCount, 'missing'], [list.length, 'total known']]),
346
+ panel('environment variables (click to set/unset)', chips),
347
+ ];
348
+ },
349
+ async tools(h) {
350
+ const list = [...h.pi.tools.values()];
351
+ return [
352
+ kpi([[list.length, 'tools']]),
353
+ panel('all tools', table(['name', 'description'], list.map(t => [t.name, (t.description || '').slice(0, 100)])), list.length),
354
+ ];
355
+ },
356
+ async batch(h) {
357
+ const out = el('div');
358
+ const form = el('form', 'fdash-form', { on: { submit: async (ev) => {
359
+ ev.preventDefault();
360
+ const prompts = ev.target.elements.prompts.value.split('\n').map(s => s.trim()).filter(Boolean);
361
+ if (!prompts.length) return;
362
+ out.textContent = 'running…';
363
+ try { const r = await h.pi.batch.run({ prompts, concurrency: Number(ev.target.elements.conc.value) || 4 });
364
+ out.innerHTML = ''; out.appendChild(pre(r));
365
+ } catch (e) { out.textContent = 'error: ' + (e.message || e); }
366
+ } } });
367
+ const ta = el('textarea', null, { name: 'prompts', rows: '5', placeholder: 'one prompt per line' });
368
+ form.appendChild(ta);
369
+ form.appendChild(el('input', null, { name: 'conc', type: 'number', value: '4' }));
370
+ form.appendChild(el('button', null, { type: 'submit', text: 'run' }));
371
+ return [
372
+ panel('run prompts', form),
373
+ panel('results', out),
374
+ ];
375
+ },
376
+ async gateway(h) {
377
+ const platforms = h.pi.gateway.platforms();
378
+ return [
379
+ kpi([[platforms.length, 'platforms'], [platforms.filter(p => p.enabled).length, 'active']]),
380
+ panel('platforms', table(['name', 'enabled', 'note'], platforms.map(p => [p.name, p.enabled ? 'yes' : 'no', p.note])), platforms.length),
381
+ ];
382
+ },
383
+ };
384
+
385
+ setActive('home');
386
+
387
+ if (typeof window !== 'undefined') {
388
+ window.__debug = window.__debug || {};
389
+ window.__debug.instances = window.__debug.instances || {};
390
+ window.__debug.instances[instance.id] = window.__debug.instances[instance.id] || {};
391
+ window.__debug.instances[instance.id].dashboard = { root, routes: ROUTES.map(r => r.path), setActive, get active() { return active; } };
392
+ }
393
+
394
+ return { node: root, dispose() {} };
395
+ }
@@ -40,7 +40,14 @@ html, body {
40
40
  padding: 0 12px;
41
41
  gap: 6px;
42
42
  box-shadow: none;
43
+ display: flex;
44
+ align-items: center;
45
+ flex-wrap: nowrap;
46
+ min-width: 0;
43
47
  }
48
+ .os-menubar > *, .os-taskbar > * { flex-shrink: 0; }
49
+ .os-menubar .os-spacer { flex: 1 1 auto; min-width: 0; }
50
+ .os-menubar .os-tray { margin-left: auto; }
44
51
 
45
52
  .os-brand {
46
53
  color: var(--os-fg);
@@ -322,6 +329,7 @@ html, body {
322
329
  .app-pane.mono .row:hover { background: var(--panel-hover, var(--os-bg-2)); }
323
330
  .app-pane.mono .head { color: var(--os-accent); margin-bottom: 8px; padding-bottom: 6px; font-weight: 600; letter-spacing: 0.02em; }
324
331
  .app-pane.mono pre { background: var(--os-bg-2); padding: 10px 12px; margin: 8px 0 0 0; max-height: 220px; overflow: auto; white-space: pre-wrap; font: 11.5px var(--os-mono); border-radius: var(--r-1, 6px); color: var(--os-fg); border: none; }
332
+ .app-shell-pane { margin: 0; padding: 8px; font: 12px var(--os-mono); color: var(--os-fg); background: var(--os-bg-0); height: 100%; overflow: auto; white-space: pre-wrap; box-sizing: border-box; }
325
333
  .app-canvas { width: 100%; height: 100%; background: var(--os-bg-0); display: block; cursor: default; }
326
334
  .app-canvas.x-display { background: #0b0d10; }
327
335
  .app-frame { width: 100%; height: 100%; border: 0; background: var(--os-bg-0); }
@@ -0,0 +1,19 @@
1
+ body { margin: 0; padding: 24px; font-family: var(--os-font); background: var(--os-bg-0); color: var(--os-fg); }
2
+ h1 { margin: 0 0 8px 0; font-size: 22px; font-weight: 600; }
3
+ h3 { margin: 18px 0 6px 0; color: var(--os-fg-2); font-size: 13px; font-weight: 600; }
4
+ .sub { color: var(--os-fg-2); margin-bottom: 20px; }
5
+ table { border-collapse: collapse; width: 100%; max-width: 760px; }
6
+ td { padding: 6px 12px; border-bottom: 1px solid var(--os-bg-3); }
7
+ td.k { width: 280px; color: var(--os-fg-2); }
8
+ td.v { font-weight: 600; }
9
+ td.v.pass { color: var(--os-accent); }
10
+ td.v.fail { color: var(--os-red); }
11
+ td.v.pending { color: var(--os-amber); }
12
+ .all { margin-top: 16px; padding: 12px; background: var(--os-bg-1); max-width: 760px; border-radius: var(--os-radius-sm); }
13
+ .all.green { color: var(--os-accent); }
14
+ .all.red { color: var(--os-red); }
15
+ .err { color: var(--os-red); white-space: pre-wrap; margin-top: 12px; }
16
+ a { color: var(--os-accent); text-decoration: none; }
17
+ a:hover { color: var(--os-accent-2); }
18
+ iframe.osframe { position: absolute; left: -9999px; top: 0; width: 1280px; height: 900px; border: 0; }
19
+ iframe.osframe-phone { position: absolute; left: -9999px; top: 0; width: 414px; height: 720px; border: 0; }