@webmcp-auto-ui/ui 2.5.26 → 2.5.28

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 (88) hide show
  1. package/README.md +2 -2
  2. package/package.json +15 -3
  3. package/src/agent/AgentConsole.svelte +1 -21
  4. package/src/agent/DataServersPanel.svelte +164 -0
  5. package/src/agent/LLMSelector.svelte +26 -8
  6. package/src/agent/ModelCacheManager.svelte +359 -0
  7. package/src/agent/{GemmaLoader.svelte → ModelLoader.svelte} +1 -1
  8. package/src/agent/SettingsPanel.svelte +16 -2
  9. package/src/index.ts +45 -31
  10. package/src/widgets/WidgetRenderer.svelte +118 -115
  11. package/src/widgets/export-widget.ts +28 -1
  12. package/src/widgets/helpers/safe-image.ts +78 -0
  13. package/src/widgets/notebook/.gitkeep +0 -0
  14. package/src/widgets/notebook/chart-renderer.ts +63 -0
  15. package/src/widgets/notebook/compact.ts +823 -0
  16. package/src/widgets/notebook/document.ts +1065 -0
  17. package/src/widgets/notebook/editorial.ts +936 -0
  18. package/src/widgets/notebook/executors/.gitkeep +1 -0
  19. package/src/widgets/notebook/executors/index.ts +4 -0
  20. package/src/widgets/notebook/executors/js-worker.ts +269 -0
  21. package/src/widgets/notebook/executors/sql.ts +206 -0
  22. package/src/widgets/notebook/import-modals.ts +553 -0
  23. package/src/widgets/notebook/left-pane.ts +249 -0
  24. package/src/widgets/notebook/prose.ts +280 -0
  25. package/src/widgets/notebook/recipe-browser.ts +350 -0
  26. package/src/widgets/notebook/recipes/compact.md +124 -0
  27. package/src/widgets/notebook/recipes/document.md +139 -0
  28. package/src/widgets/notebook/recipes/editorial.md +120 -0
  29. package/src/widgets/notebook/recipes/workspace.md +119 -0
  30. package/src/widgets/notebook/resource-extractor.ts +162 -0
  31. package/src/widgets/notebook/share-handlers.ts +222 -0
  32. package/src/widgets/notebook/shared.ts +1592 -0
  33. package/src/widgets/notebook/workspace.ts +852 -0
  34. package/src/widgets/rich/cards.ts +181 -0
  35. package/src/widgets/rich/carousel.ts +319 -0
  36. package/src/widgets/rich/chart-rich.ts +386 -0
  37. package/src/widgets/rich/d3.ts +503 -0
  38. package/src/widgets/rich/data-table.ts +342 -0
  39. package/src/widgets/rich/gallery.ts +350 -0
  40. package/src/widgets/rich/grid-data.ts +173 -0
  41. package/src/widgets/rich/hemicycle.ts +313 -0
  42. package/src/widgets/rich/js-sandbox.ts +106 -0
  43. package/src/widgets/rich/json-viewer.ts +202 -0
  44. package/src/widgets/rich/log.ts +143 -0
  45. package/src/widgets/rich/map.ts +218 -0
  46. package/src/widgets/rich/profile.ts +256 -0
  47. package/src/widgets/rich/sankey.ts +262 -0
  48. package/src/widgets/rich/stat-card.ts +125 -0
  49. package/src/widgets/rich/timeline.ts +179 -0
  50. package/src/widgets/rich/trombinoscope.ts +246 -0
  51. package/src/widgets/simple/actions.ts +89 -0
  52. package/src/widgets/simple/alert.ts +100 -0
  53. package/src/widgets/simple/chart.ts +189 -0
  54. package/src/widgets/simple/code.ts +79 -0
  55. package/src/widgets/simple/kv.ts +68 -0
  56. package/src/widgets/simple/list.ts +89 -0
  57. package/src/widgets/simple/stat.ts +58 -0
  58. package/src/widgets/simple/tags.ts +125 -0
  59. package/src/widgets/simple/text.ts +198 -0
  60. package/src/wm/FloatingLayout.svelte +2 -0
  61. package/src/wm/LinkIndicators.svelte +8 -15
  62. package/src/widgets/SafeImage.svelte +0 -76
  63. package/src/widgets/rich/Cards.svelte +0 -39
  64. package/src/widgets/rich/Carousel.svelte +0 -88
  65. package/src/widgets/rich/Chart.svelte +0 -142
  66. package/src/widgets/rich/D3Widget.svelte +0 -373
  67. package/src/widgets/rich/DataTable.svelte +0 -62
  68. package/src/widgets/rich/Gallery.svelte +0 -94
  69. package/src/widgets/rich/GridData.svelte +0 -44
  70. package/src/widgets/rich/Hemicycle.svelte +0 -78
  71. package/src/widgets/rich/JsSandbox.svelte +0 -51
  72. package/src/widgets/rich/JsonViewer.svelte +0 -42
  73. package/src/widgets/rich/LogViewer.svelte +0 -24
  74. package/src/widgets/rich/MapView.svelte +0 -140
  75. package/src/widgets/rich/ProfileCard.svelte +0 -59
  76. package/src/widgets/rich/Sankey.svelte +0 -38
  77. package/src/widgets/rich/StatCard.svelte +0 -35
  78. package/src/widgets/rich/Timeline.svelte +0 -43
  79. package/src/widgets/rich/Trombinoscope.svelte +0 -48
  80. package/src/widgets/simple/ActionsBlock.svelte +0 -15
  81. package/src/widgets/simple/AlertBlock.svelte +0 -11
  82. package/src/widgets/simple/ChartBlock.svelte +0 -21
  83. package/src/widgets/simple/CodeBlock.svelte +0 -11
  84. package/src/widgets/simple/KVBlock.svelte +0 -16
  85. package/src/widgets/simple/ListBlock.svelte +0 -17
  86. package/src/widgets/simple/StatBlock.svelte +0 -14
  87. package/src/widgets/simple/TagsBlock.svelte +0 -15
  88. package/src/widgets/simple/TextBlock.svelte +0 -122
@@ -0,0 +1,852 @@
1
+ // @ts-nocheck
2
+ // ---------------------------------------------------------------------------
3
+ // notebook-workspace — dense analyst workspace (hex-like)
4
+ // Header bar + sidebar (sources + cells nav) + main cells area.
5
+ // ---------------------------------------------------------------------------
6
+
7
+ import {
8
+ createState, injectStyles, mountRunControls, mountHistoryPanel,
9
+ setupDnD, deleteCellWithConfirm, restoreCellFromSnapshot, addCell,
10
+ autosize, openShareModal, registerHistoryObserver,
11
+ registerExecutor, addImportedCells,
12
+ collectDataServers, startRun, renderCellLogs,
13
+ createPublishControls, autoConnectFrontmatterServers,
14
+ createRuntimeOverlay, bootstrapLiveRefresh, effectiveResult,
15
+ cellRuntimeStatus, lastRefreshedAt, fmtRelTime,
16
+ type NotebookState, type NotebookCell, type CellResult, type RuntimeOverlay,
17
+ } from './shared.js';
18
+ import { renderChart } from './chart-renderer.js';
19
+ import { dispatchShare } from './share-handlers.js';
20
+ import { openAddMdModal, openAddRecipeModal } from './import-modals.js';
21
+ import { extractCellsFromRecipe, extractCellFromMarkdown } from './resource-extractor.js';
22
+ import { mountLeftPane } from './left-pane.js';
23
+ import { callToolViaPostMessage, MultiMcpBridge } from '@webmcp-auto-ui/core';
24
+
25
+ export async function render(container: HTMLElement, data: Record<string, unknown>): Promise<() => void> {
26
+ injectStyles();
27
+ injectLayoutStyles();
28
+
29
+ const state: NotebookState = createState({
30
+ id: data.id as string,
31
+ title: data.title as string ?? 'Untitled notebook',
32
+ mode: (data.mode as any) ?? 'edit',
33
+ cells: data.cells as any,
34
+ });
35
+
36
+ // Auto-connect frontmatter servers
37
+ autoConnectFrontmatterServers(data, () => rerender());
38
+
39
+ // Restore persisted title
40
+ try {
41
+ const saved = localStorage.getItem((state.id || 'nb') + ':title');
42
+ if (saved) state.title = saved;
43
+ } catch {}
44
+
45
+ // ---- Register executors (JS + SQL) ---------------------------------------
46
+ registerExecutor(state, 'js', async (ctx) => {
47
+ const started = Date.now();
48
+ try {
49
+ const scope = ctx.scope || {};
50
+ const keys = Object.keys(scope);
51
+ const vals = keys.map((k) => scope[k]);
52
+ // eslint-disable-next-line no-new-func
53
+ const fn = new Function(...keys, `"use strict";\nreturn (async () => { ${ctx.cell.content} })();`);
54
+ const out = await fn(...vals);
55
+ const durationMs = Date.now() - started;
56
+ if (out == null) return { ok: true, kind: 'empty', durationMs };
57
+ if (Array.isArray(out) && out.length && typeof out[0] === 'object') {
58
+ const columns = Array.from(new Set(out.flatMap((r: any) => Object.keys(r || {}))));
59
+ return { ok: true, kind: 'table', rows: out, columns, rowCount: out.length, durationMs };
60
+ }
61
+ return { ok: true, kind: 'value', value: out, durationMs };
62
+ } catch (err: any) {
63
+ return { ok: false, error: String(err?.message ?? err), errorKind: 'runtime', durationMs: Date.now() - started };
64
+ }
65
+ });
66
+
67
+ registerExecutor(state, 'sql', async (ctx) => {
68
+ const started = Date.now();
69
+ try {
70
+ const servers = collectDataServers(data);
71
+ let toolName: string | null = null;
72
+ for (const s of servers) {
73
+ const tools = s.tools || [];
74
+ let found = tools.find((t: any) => /.*query_sql$/i.test(t.name || ''));
75
+ if (!found) found = tools.find((t: any) => /^(query|run|execute)(_sql)?$/i.test(t.name || ''));
76
+ if (found) { toolName = found.name; break; }
77
+ }
78
+ if (!toolName) {
79
+ return { ok: false, error: 'No SQL tool found on connected servers', errorKind: 'schema', durationMs: Date.now() - started };
80
+ }
81
+ const res: any = await callToolViaPostMessage(toolName, { sql: ctx.cell.content });
82
+ const durationMs = Date.now() - started;
83
+ // Try to parse a result
84
+ let payload = res;
85
+ if (res && Array.isArray(res.content)) {
86
+ const textBlock = res.content.find((b: any) => b?.type === 'text');
87
+ if (textBlock?.text) {
88
+ try { payload = JSON.parse(textBlock.text); } catch { payload = textBlock.text; }
89
+ }
90
+ }
91
+ let rows: any[] = [];
92
+ if (Array.isArray(payload)) rows = payload;
93
+ else if (payload && Array.isArray(payload.rows)) rows = payload.rows;
94
+ else if (payload && Array.isArray(payload.results)) rows = payload.results;
95
+ if (!Array.isArray(rows) || rows.length === 0) {
96
+ return { ok: true, kind: 'empty', durationMs };
97
+ }
98
+ const columns = Array.from(new Set(rows.flatMap((r: any) => Object.keys(r || {}))));
99
+ return { ok: true, kind: 'table', rows, columns, rowCount: rows.length, durationMs };
100
+ } catch (err: any) {
101
+ return { ok: false, error: String(err?.message ?? err), errorKind: 'runtime', durationMs: Date.now() - started };
102
+ }
103
+ });
104
+
105
+ let activeCellId: string | null = state.cells.find((c) => c.type !== 'md')?.id ?? state.cells[0]?.id ?? null;
106
+ const activeCellIdx = (): number | null => {
107
+ const i = state.cells.findIndex((c) => c.id === activeCellId);
108
+ return i < 0 ? null : i;
109
+ };
110
+
111
+ container.classList.add('nb-root');
112
+ container.classList.toggle('nb-view-mode', state.mode === 'view');
113
+
114
+ const firstServer = collectDataServers(data)[0];
115
+ const sourceLabel = firstServer?.name ?? 'no source connected';
116
+
117
+ container.innerHTML = `
118
+ <div class="nbw-shell">
119
+ <div class="nbw-header">
120
+ <div class="nbw-logo"></div>
121
+ <input class="nbw-title-edit nb-title-edit" value="${escapeAttr(state.title)}">
122
+ <span class="nbw-live-badge-slot"></span>
123
+ <span class="nbw-publish-badge-slot"></span>
124
+ <div class="nbw-ctx">
125
+ <span class="nbw-source">${escapeAttr(sourceLabel)}</span>
126
+ <div class="nb-mode-switch">
127
+ <button class="nb-mode-edit nb-on">edit</button>
128
+ <button class="nb-mode-view">view</button>
129
+ </div>
130
+ <button class="nb-btn nbw-history-btn">⟲ history</button>
131
+ <button class="nb-btn nbw-runall-btn">run all</button>
132
+ <button class="nb-btn nbw-share-btn">share</button>
133
+ <label class="nbw-autorun-toggle" title="Re-run SQL cells live against connected servers when notebook is viewed">
134
+ <input type="checkbox" class="nbw-autorun-cb" ${state.autoRun ? 'checked' : ''}>
135
+ <span>live data</span>
136
+ </label>
137
+ <span class="nbw-publish-btn-slot"></span>
138
+ </div>
139
+ </div>
140
+ <div class="nbw-live-meta-slot"></div>
141
+ <div class="nbw-empty-slot"></div>
142
+ <div class="nb-history-panel nbw-history-panel"></div>
143
+ <div class="nbw-toast-slot"></div>
144
+ <div class="nbw-body">
145
+ <aside class="nbw-sidebar">
146
+ <div class="nbw-section">sources</div>
147
+ <div class="nbw-sources-list"></div>
148
+ <div class="nbw-section">cells</div>
149
+ <div class="nbw-cells-nav"></div>
150
+ <div class="nbw-add">
151
+ <button class="nb-btn nb-add-cell" data-add="md">+ md</button>
152
+ <button class="nb-btn nb-add-cell" data-add="sql">+ sql</button>
153
+ <button class="nb-btn nb-add-cell" data-add="js">+ js</button>
154
+ <button class="nb-btn nbw-import-md">+md</button>
155
+ <button class="nb-btn nbw-import-recipe">+recipe</button>
156
+ </div>
157
+ </aside>
158
+ <div class="nbw-cells"></div>
159
+ </div>
160
+ <div class="nbw-publish-footer-slot"></div>
161
+ </div>`;
162
+
163
+ const shell = container.querySelector('.nbw-shell') as HTMLElement;
164
+ const cellsEl = shell.querySelector('.nbw-cells') as HTMLElement;
165
+ const navEl = shell.querySelector('.nbw-cells-nav') as HTMLElement;
166
+ const historyPanel = shell.querySelector('.nbw-history-panel') as HTMLElement;
167
+ const sourcesListEl = shell.querySelector('.nbw-sources-list') as HTMLElement;
168
+ const toastSlot = shell.querySelector('.nbw-toast-slot') as HTMLElement;
169
+ const sourceEl = shell.querySelector('.nbw-source') as HTMLElement;
170
+ const liveBadgeSlot = shell.querySelector('.nbw-live-badge-slot') as HTMLElement;
171
+ const liveMetaSlot = shell.querySelector('.nbw-live-meta-slot') as HTMLElement;
172
+ const emptySlot = shell.querySelector('.nbw-empty-slot') as HTMLElement;
173
+ const autorunToggle = shell.querySelector('.nbw-autorun-toggle') as HTMLElement;
174
+ const autorunCb = shell.querySelector('.nbw-autorun-cb') as HTMLInputElement;
175
+
176
+ // ---- Live mode (autoRun) overlay --------------------------------------
177
+ let overlay: RuntimeOverlay | null = null;
178
+ let liveCleanup: (() => void) | null = null;
179
+
180
+ function startLive(): void {
181
+ stopLive();
182
+ overlay = createRuntimeOverlay();
183
+ liveCleanup = bootstrapLiveRefresh({
184
+ state,
185
+ data,
186
+ overlay,
187
+ MultiMcpBridgeCtor: MultiMcpBridge as any,
188
+ onCellChange: () => { renderCells(); renderLiveHeader(); },
189
+ onTick: () => { renderLiveHeader(); },
190
+ });
191
+ }
192
+ function stopLive(): void {
193
+ try { liveCleanup?.(); } catch {}
194
+ liveCleanup = null;
195
+ overlay = null;
196
+ }
197
+
198
+ function renderLiveHeader(): void {
199
+ // Live pill (only in view + autoRun)
200
+ const showLive = state.mode === 'view' && state.autoRun === true;
201
+ liveBadgeSlot.innerHTML = showLive ? '<span class="nb-live-badge">● Live</span>' : '';
202
+ if (showLive) {
203
+ const ts = lastRefreshedAt(overlay);
204
+ liveMetaSlot.innerHTML = ts
205
+ ? `<div class="nbw-live-meta">Refreshed <time datetime="${new Date(ts).toISOString()}">${escapeHtml(fmtRelTime(ts))}</time></div>`
206
+ : '<div class="nbw-live-meta">Refreshing…</div>';
207
+ } else {
208
+ liveMetaSlot.innerHTML = '';
209
+ }
210
+
211
+ // Empty-state banner
212
+ const showEmpty = !!(state.autoRun && state.mode === 'view' && overlay
213
+ && (overlay.error || (overlay.finishedAt && overlay.outputs.size === 0)));
214
+ if (showEmpty) {
215
+ const ts = lastRefreshedAt(overlay);
216
+ const snapWhen = ts ? fmtRelTime(ts) : 'last save';
217
+ emptySlot.innerHTML = `
218
+ <div class="nb-empty-state">
219
+ <div class="nb-empty-icon">📡</div>
220
+ <div class="nb-empty-body">
221
+ <div class="nb-empty-title">Live mode active, but no data server is reachable.</div>
222
+ <div class="nb-empty-desc">Showing snapshots from <time>${escapeHtml(snapWhen)}</time>.</div>
223
+ </div>
224
+ <button class="nb-btn nb-empty-retry">retry connection</button>
225
+ </div>`;
226
+ const retry = emptySlot.querySelector('.nb-empty-retry') as HTMLElement | null;
227
+ retry?.addEventListener('click', () => { startLive(); renderLiveHeader(); });
228
+ } else {
229
+ emptySlot.innerHTML = '';
230
+ }
231
+ }
232
+
233
+ function applyAutoRunUiVisibility(): void {
234
+ // Toggle visible only in edit mode
235
+ autorunToggle.style.display = state.mode === 'edit' ? '' : 'none';
236
+ }
237
+
238
+ autorunCb.addEventListener('change', () => {
239
+ state.autoRun = autorunCb.checked;
240
+ if (state.autoRun && state.mode === 'view') startLive();
241
+ else stopLive();
242
+ renderLiveHeader();
243
+ renderCells();
244
+ });
245
+
246
+ function rerenderSources() {
247
+ const servers = collectDataServers(data);
248
+ sourcesListEl.innerHTML = '';
249
+ if (servers.length === 0) {
250
+ const none = document.createElement('div');
251
+ none.className = 'nbw-item nbw-dim';
252
+ none.textContent = '◯ no source';
253
+ sourcesListEl.appendChild(none);
254
+ } else {
255
+ servers.forEach((srv: any) => {
256
+ const row = document.createElement('div');
257
+ row.className = 'nbw-sources-srv';
258
+ const dot = document.createElement('span');
259
+ dot.className = 'nbw-sources-srv-dot';
260
+ const name = document.createElement('span');
261
+ name.className = 'nbw-sources-srv-name';
262
+ name.textContent = srv.name;
263
+ const meta = document.createElement('span');
264
+ meta.className = 'nbw-sources-srv-meta';
265
+ const recipesN = Array.isArray(srv.recipes) ? srv.recipes.length : 0;
266
+ const toolsN = Array.isArray(srv.tools) ? srv.tools.length : 0;
267
+ meta.textContent = `(${recipesN} recipes, ${toolsN} tools)`;
268
+ row.appendChild(dot);
269
+ row.appendChild(name);
270
+ row.appendChild(meta);
271
+ sourcesListEl.appendChild(row);
272
+ });
273
+ }
274
+ const hint = document.createElement('div');
275
+ hint.className = 'nbw-item nbw-indent nbw-dim';
276
+ hint.textContent = 'manage data servers in the sidebar';
277
+ sourcesListEl.appendChild(hint);
278
+ }
279
+
280
+ function scrollToActive() {
281
+ if (!activeCellId) return;
282
+ const node = cellsEl.querySelector(`.nbw-cell[data-id="${activeCellId}"]`) as HTMLElement | null;
283
+ if (!node) return;
284
+ try { node.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch {}
285
+ const ta = node.querySelector('textarea') as HTMLTextAreaElement | null;
286
+ if (ta) setTimeout(() => { try { ta.focus(); } catch {} }, 250);
287
+ }
288
+
289
+ function renderCells() {
290
+ cellsEl.innerHTML = '';
291
+ navEl.innerHTML = '';
292
+ state.cells.forEach((cell, idx) => {
293
+ const navItem = document.createElement('div');
294
+ navItem.className = 'nbw-item' + (cell.id === activeCellId ? ' nbw-active' : '');
295
+ navItem.textContent = `${idx + 1} · ${cell.name || cell.type}`;
296
+ navItem.addEventListener('click', () => {
297
+ activeCellId = cell.id;
298
+ rerender();
299
+ scrollToActive();
300
+ });
301
+ navEl.appendChild(navItem);
302
+
303
+ cellsEl.appendChild(renderCell(cell, idx, state, rerender, overlay));
304
+ });
305
+ }
306
+
307
+ function rerender() {
308
+ mountHistoryPanel(historyPanel, state, (snap) => { restoreCellFromSnapshot(state, snap); rerender(); });
309
+ renderCells();
310
+ rerenderSources();
311
+ renderLiveHeader();
312
+ applyAutoRunUiVisibility();
313
+ // Update source label
314
+ const first = collectDataServers(data)[0];
315
+ sourceEl.textContent = first?.name ?? 'no source connected';
316
+ }
317
+
318
+ // Toast helper
319
+ function showToast(msg: string, href?: string): void {
320
+ const t = document.createElement('div');
321
+ t.className = 'nbw-toast';
322
+ if (href) {
323
+ t.innerHTML = `${escapeAttr(msg)} — <a href="${escapeAttr(href)}" target="_blank" rel="noopener">${escapeAttr(href)}</a>`;
324
+ } else {
325
+ t.textContent = msg;
326
+ }
327
+ toastSlot.appendChild(t);
328
+ setTimeout(() => { t.classList.add('nbw-toast-out'); }, 3500);
329
+ setTimeout(() => { try { t.remove(); } catch {} }, 4200);
330
+ }
331
+
332
+ // ---- Add-cell buttons -----------------------------------------------------
333
+ shell.querySelectorAll<HTMLElement>('[data-add]').forEach((btn) => {
334
+ btn.addEventListener('click', () => {
335
+ const type = btn.dataset.add as any;
336
+ const name = type === 'md' ? 'note' : type === 'sql' ? 'query_' + (state.cells.length + 1) : 'cell_' + (state.cells.length + 1);
337
+ const cell = addCell(state, type, { name });
338
+ activeCellId = cell.id;
339
+ rerender();
340
+ });
341
+ });
342
+
343
+ // ---- Import buttons (md / recipe) ----------------------------------------
344
+ (shell.querySelector('.nbw-import-md') as HTMLElement).addEventListener('click', () => {
345
+ openAddMdModal((content: string) => {
346
+ addImportedCells(state, [extractCellFromMarkdown(content)], activeCellIdx());
347
+ rerender();
348
+ });
349
+ });
350
+ (shell.querySelector('.nbw-import-recipe') as HTMLElement).addEventListener('click', () => {
351
+ openAddRecipeModal({
352
+ scope: 'data',
353
+ mcpServers: (Array.isArray(data?.servers) ? (data.servers as any[]) : []).map((s: any) => ({ name: s.name, url: s.url })),
354
+ onPick: (recipe: any) => {
355
+ addImportedCells(state, extractCellsFromRecipe(recipe.body ?? '', { title: recipe.name, description: recipe.description }), activeCellIdx());
356
+ rerender();
357
+ },
358
+ });
359
+ });
360
+
361
+ // ---- History panel toggle ------------------------------------------------
362
+ (shell.querySelector('.nbw-history-btn') as HTMLElement).addEventListener('click', () => {
363
+ historyPanel.classList.toggle('nb-open');
364
+ });
365
+
366
+ // ---- Share button --------------------------------------------------------
367
+ (shell.querySelector('.nbw-share-btn') as HTMLElement).addEventListener('click', () => {
368
+ openShareModal(state, async (fmt) => {
369
+ try {
370
+ await dispatchShare(fmt, state, {
371
+ container,
372
+ onResult: (result: any) => {
373
+ if (result?.shortUrl) showToast('shared', result.shortUrl);
374
+ else if (result?.fullUrl) showToast('shared', result.fullUrl);
375
+ else showToast('exported as ' + fmt);
376
+ },
377
+ });
378
+ } catch (err: any) {
379
+ showToast('share failed: ' + String(err?.message ?? err));
380
+ }
381
+ });
382
+ });
383
+
384
+ // ---- Run All -------------------------------------------------------------
385
+ (shell.querySelector('.nbw-runall-btn') as HTMLElement).addEventListener('click', async () => {
386
+ const runnable = state.cells.filter((c) => c.type !== 'md');
387
+ for (const cell of runnable) {
388
+ startRun(cell, state, rerender);
389
+ await waitForCell(cell);
390
+ }
391
+ });
392
+
393
+ function waitForCell(cell: NotebookCell): Promise<void> {
394
+ return new Promise((resolve) => {
395
+ const iv = setInterval(() => {
396
+ if (cell.runState !== 'running') {
397
+ clearInterval(iv);
398
+ resolve();
399
+ }
400
+ }, 100);
401
+ // Safety timeout (60s)
402
+ setTimeout(() => { clearInterval(iv); resolve(); }, 60000);
403
+ });
404
+ }
405
+
406
+ // ---- Publish controls (shared nb.hyperskills.net) ------------------------
407
+ const destroyPublish = createPublishControls(state, {
408
+ buttonSlot: shell.querySelector('.nbw-publish-btn-slot') as HTMLElement,
409
+ badgeSlot: shell.querySelector('.nbw-publish-badge-slot') as HTMLElement,
410
+ footerSlot: shell.querySelector('.nbw-publish-footer-slot') as HTMLElement,
411
+ toast: (msg: string) => showToast(msg),
412
+ });
413
+
414
+ // ---- Title edit (persisted with debounce) --------------------------------
415
+ const titleInput = shell.querySelector('.nbw-title-edit') as HTMLInputElement;
416
+ let titleTimer: any = null;
417
+ titleInput.addEventListener('input', (e) => {
418
+ const v = (e.target as HTMLInputElement).value;
419
+ state.title = v;
420
+ if (titleTimer) clearTimeout(titleTimer);
421
+ titleTimer = setTimeout(() => {
422
+ try { localStorage.setItem((state.id || 'nb') + ':title', v); } catch {}
423
+ }, 300);
424
+ });
425
+
426
+ // ---- Mode switch ---------------------------------------------------------
427
+ const editBtn = shell.querySelector('.nb-mode-edit') as HTMLElement;
428
+ const viewBtn = shell.querySelector('.nb-mode-view') as HTMLElement;
429
+ editBtn.addEventListener('click', () => {
430
+ state.mode = 'edit';
431
+ container.classList.remove('nb-view-mode');
432
+ editBtn.classList.add('nb-on'); viewBtn.classList.remove('nb-on');
433
+ stopLive();
434
+ rerender();
435
+ });
436
+ viewBtn.addEventListener('click', () => {
437
+ state.mode = 'view';
438
+ container.classList.add('nb-view-mode');
439
+ viewBtn.classList.add('nb-on'); editBtn.classList.remove('nb-on');
440
+ if (state.autoRun) startLive();
441
+ rerender();
442
+ });
443
+
444
+ // ---- Left pane (collapsed by default) ------------------------------------
445
+ let leftPaneHandle: any = null;
446
+ try {
447
+ leftPaneHandle = mountLeftPane(shell, state, collectDataServers(data), {
448
+ onInjectCells: (cells: NotebookCell[]) => {
449
+ addImportedCells(state, cells, activeCellIdx());
450
+ rerender();
451
+ },
452
+ });
453
+ } catch (err) {
454
+ // Left pane mount is non-critical; log and continue
455
+ console.warn('[notebook-workspace] left pane mount failed', err);
456
+ }
457
+
458
+ // ---- DnD + history observer ---------------------------------------------
459
+ setupDnD(cellsEl, state, rerender);
460
+ const unsubHistory = registerHistoryObserver(() => mountHistoryPanel(historyPanel, state, (snap) => { restoreCellFromSnapshot(state, snap); rerender(); }));
461
+
462
+ // ---- Canvas subscribe for sources sidebar live updates -------------------
463
+ let canvasUnsub: (() => void) | null = null;
464
+ try {
465
+ const canvasAny: any = (globalThis as any).__canvasVanilla || (globalThis as any).canvasVanilla;
466
+ if (canvasAny?.subscribe) canvasUnsub = canvasAny.subscribe(() => rerenderSources());
467
+ } catch {}
468
+
469
+ // ---- Live-mode bootstrap (view + autoRun at mount) ----------------------
470
+ if (state.autoRun && state.mode === 'view') {
471
+ startLive();
472
+ }
473
+
474
+ rerender();
475
+ return () => {
476
+ unsubHistory();
477
+ try { leftPaneHandle?.destroy?.(); } catch {}
478
+ try { canvasUnsub?.(); } catch {}
479
+ try { destroyPublish(); } catch {}
480
+ try { stopLive(); } catch {}
481
+ };
482
+ }
483
+
484
+ function renderCell(cell: NotebookCell, idx: number, state: NotebookState, rerender: () => void, overlay: RuntimeOverlay | null): HTMLElement {
485
+ const wrap = document.createElement('div');
486
+ wrap.className = 'nb-cell-wrapper nbw-cell';
487
+ wrap.dataset.id = cell.id;
488
+
489
+ const inner = document.createElement('div');
490
+ inner.className = 'nb-cell';
491
+
492
+ const head = document.createElement('div');
493
+ head.className = 'nbw-cell-head';
494
+ const isCode = cell.type !== 'md';
495
+
496
+ // Live-mode awareness
497
+ const liveActive = state.mode === 'view' && state.autoRun === true;
498
+ const rtStatus = cellRuntimeStatus(cell, overlay);
499
+ const liveResult = effectiveResult(cell, overlay);
500
+
501
+ // Compute meta info from effective (overlay-aware) result, no mutation
502
+ const metaInfo = computeMetaInfo(cell, liveResult);
503
+ const statusBadge = renderStatusBadge(rtStatus, liveActive);
504
+
505
+ head.innerHTML = `
506
+ <span class="nb-drag-handle" draggable="true" title="drag">⋮⋮</span>
507
+ ${isCode ? '<span class="nbw-run-controls"></span>' : ''}
508
+ <span class="nbw-type nbw-type-${cell.type}">${cell.type}</span>
509
+ <input class="nbw-cell-name-edit" value="${idx + 1} · ${escapeAttr(cell.name || '')}">
510
+ <div class="nbw-meta">
511
+ ${isCode && statusBadge ? statusBadge : ''}
512
+ ${isCode ? `<span class="nbw-meta-info">${escapeAttr(metaInfo)}</span>` : ''}
513
+ <button class="nb-icon-btn nb-toggle-src">${cell.hideSource ? '▸ src' : '◂ src'}</button>
514
+ ${isCode ? `<button class="nb-icon-btn nb-toggle-res">${cell.hideResult ? '▸ res' : '◂ res'}</button>` : ''}
515
+ <button class="nb-icon-btn nb-danger nbw-del">✕</button>
516
+ </div>`;
517
+ inner.appendChild(head);
518
+
519
+ if (isCode) {
520
+ mountRunControls(head.querySelector('.nbw-run-controls') as HTMLElement, cell, wrap, state, rerender);
521
+ }
522
+
523
+ const body = document.createElement('div');
524
+ body.className = 'nbw-cell-body' + (isCode ? ' nbw-code' : '') + (cell.hideSource ? ' nbw-hidden' : '');
525
+ const ta = document.createElement('textarea');
526
+ ta.className = isCode ? 'nb-code-edit' : 'nb-md-edit';
527
+ ta.value = cell.content;
528
+ ta.rows = 1;
529
+ ta.spellcheck = false;
530
+ ta.addEventListener('input', () => { cell.content = ta.value; autosize(ta); cell.status = 'stale'; });
531
+ body.appendChild(ta);
532
+ inner.appendChild(body);
533
+ setTimeout(() => autosize(ta), 0);
534
+
535
+ // ---- Result rendering (live overlay if available, else snapshot) ----
536
+ if (isCode && !cell.hideResult && liveResult) {
537
+ inner.appendChild(renderResult(liveResult));
538
+ }
539
+
540
+ (head.querySelector('.nb-toggle-src') as HTMLElement).addEventListener('click', () => { cell.hideSource = !cell.hideSource; rerender(); });
541
+ const togRes = head.querySelector('.nb-toggle-res') as HTMLElement | null;
542
+ if (togRes) togRes.addEventListener('click', () => { cell.hideResult = !cell.hideResult; rerender(); });
543
+ (head.querySelector('.nbw-del') as HTMLElement).addEventListener('click', () =>
544
+ deleteCellWithConfirm(state, cell, (c) => `${c.type} cell "${c.name}"`, rerender)
545
+ );
546
+ (head.querySelector('.nbw-cell-name-edit') as HTMLInputElement).addEventListener('input', (e) => {
547
+ const v = (e.target as HTMLInputElement).value;
548
+ const m = v.match(/^\d+\s*·\s*(.+)$/);
549
+ cell.name = m ? m[1] : v;
550
+ });
551
+
552
+ wrap.appendChild(inner);
553
+ return wrap;
554
+ }
555
+
556
+ function computeMetaInfo(cell: NotebookCell, effective?: CellResult | undefined): string {
557
+ const r = effective ?? cell.lastResult;
558
+ const ms = (r && (r as any).durationMs != null)
559
+ ? (r as any).durationMs + 'ms'
560
+ : (cell.lastMs != null ? cell.lastMs + 'ms' : '—');
561
+ if (!r) return ms + ' · (not run yet)';
562
+ if (!r.ok) return ms + ' · error';
563
+ if (r.kind === 'table') return ms + ' · ' + r.rowCount + ' rows';
564
+ if (r.kind === 'value') return ms + ' · value';
565
+ if (r.kind === 'chart') return ms + ' · chart';
566
+ return ms + ' · empty';
567
+ }
568
+
569
+ function renderStatusBadge(status: string, liveActive: boolean): string {
570
+ if (status === 'running') return '<span class="nbw-rt-badge nbw-rt-running" title="re-running"><span class="nbw-rt-spin"></span>running</span>';
571
+ if (status === 'stale') return '<span class="nbw-rt-badge nbw-rt-stale" title="live refresh failed; showing snapshot">stale</span>';
572
+ if (status === 'frozen' && liveActive) return '<span class="nbw-rt-badge nbw-rt-frozen" title="not re-run in live mode (snapshot)">frozen</span>';
573
+ if (status === 'fresh') return '<span class="nbw-rt-badge nbw-rt-fresh" title="freshly refreshed">fresh</span>';
574
+ return '';
575
+ }
576
+
577
+ function renderResult(res: CellResult): HTMLElement {
578
+ const wrap = document.createElement('div');
579
+ wrap.className = 'nbw-result';
580
+ const logsEl = renderCellLogs(res);
581
+ if (logsEl) wrap.appendChild(logsEl);
582
+ if (!res.ok) {
583
+ const err = document.createElement('div');
584
+ err.className = 'nbw-error';
585
+ err.textContent = '⚠ ' + (res.error || 'error');
586
+ wrap.appendChild(err);
587
+ return wrap;
588
+ }
589
+ if (res.kind === 'empty') {
590
+ const empty = document.createElement('div');
591
+ empty.className = 'nbw-empty';
592
+ empty.textContent = '(empty result)';
593
+ wrap.appendChild(empty);
594
+ return wrap;
595
+ }
596
+ if (res.kind === 'value') {
597
+ const txt = typeof res.value === 'object' ? JSON.stringify(res.value, null, 2) : String(res.value);
598
+ const pre = document.createElement('pre');
599
+ pre.className = 'nbw-value';
600
+ pre.textContent = txt;
601
+ wrap.appendChild(pre);
602
+ return wrap;
603
+ }
604
+ if (res.kind === 'chart') {
605
+ const chart = document.createElement('div');
606
+ chart.className = 'nb-chart';
607
+ wrap.appendChild(chart);
608
+ renderChart(chart, res.spec).catch(() => { /* fallback handled internally */ });
609
+ return wrap;
610
+ }
611
+ // table — use appendChild so we don't wipe the logs panel
612
+ const cols = res.columns;
613
+ const rows = res.rows.slice(0, 100);
614
+ const thead = cols.map((c) => `<th>${escapeHtml(c)}</th>`).join('');
615
+ const tbody = rows.map((r) => {
616
+ return '<tr>' + cols.map((c) => `<td>${escapeHtml(formatCell((r as any)[c]))}</td>`).join('') + '</tr>';
617
+ }).join('');
618
+ const tableHost = document.createElement('div');
619
+ tableHost.innerHTML = `<table class="nbw-result-table"><thead><tr>${thead}</tr></thead><tbody>${tbody}</tbody></table>`;
620
+ wrap.appendChild(tableHost.firstElementChild!);
621
+ if (res.rows.length > rows.length) {
622
+ const note = document.createElement('div');
623
+ note.className = 'nbw-empty';
624
+ note.textContent = `… ${res.rows.length - rows.length} more rows`;
625
+ wrap.appendChild(note);
626
+ }
627
+ return wrap;
628
+ }
629
+
630
+ function formatCell(v: unknown): string {
631
+ if (v == null) return '';
632
+ if (typeof v === 'object') return JSON.stringify(v);
633
+ return String(v);
634
+ }
635
+
636
+ function escapeHtml(s: string): string {
637
+ return (s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' } as any)[c]);
638
+ }
639
+
640
+ function escapeAttr(s: string): string {
641
+ return (s ?? '').replace(/"/g, '&quot;');
642
+ }
643
+
644
+ function injectLayoutStyles(): void {
645
+ if (document.getElementById('nbw-styles')) return;
646
+ const style = document.createElement('style');
647
+ style.id = 'nbw-styles';
648
+ style.textContent = `
649
+ .nbw-shell {
650
+ background: var(--color-surface); border: 1px solid var(--color-border);
651
+ border-radius: 12px; overflow: hidden; position: relative;
652
+ }
653
+ .nbw-header {
654
+ display: flex; align-items: center; padding: 10px 14px; gap: 12px;
655
+ border-bottom: 1px solid var(--color-border); background: var(--color-surface2);
656
+ }
657
+ .nbw-logo { width: 14px; height: 14px; background: var(--color-accent); border-radius: 3px; }
658
+ .nbw-title-edit {
659
+ font-size: 13px; font-weight: 500; color: var(--color-text1);
660
+ background: transparent; border: none; outline: none;
661
+ font-family: var(--font-sans, 'Syne', sans-serif);
662
+ width: 260px; padding: 2px 4px; border-radius: 3px;
663
+ }
664
+ .nbw-title-edit:focus { background: var(--color-bg); }
665
+ .nbw-ctx {
666
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
667
+ font-size: 11px; color: var(--color-text2);
668
+ margin-left: auto; display: flex; gap: 8px; align-items: center;
669
+ }
670
+ .nbw-history-panel { margin: 0 14px; }
671
+ .nbw-body { display: grid; grid-template-columns: 180px 1fr; min-height: 380px; }
672
+ .nbw-sidebar {
673
+ border-right: 1px solid var(--color-border);
674
+ background: var(--color-surface2); padding: 14px 12px; font-size: 12px;
675
+ }
676
+ .nbw-section {
677
+ color: var(--color-text2);
678
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
679
+ font-size: 10px; letter-spacing: 0.1em;
680
+ margin: 0 0 8px; text-transform: uppercase;
681
+ }
682
+ .nbw-section:not(:first-child) { margin-top: 16px; }
683
+ .nbw-item {
684
+ padding: 4px 6px; color: var(--color-text2);
685
+ border-radius: 4px; cursor: pointer;
686
+ display: flex; align-items: center; gap: 6px;
687
+ background: transparent; border: none;
688
+ font: inherit; text-align: left; width: 100%;
689
+ }
690
+ .nbw-item:hover { background: var(--color-surface); color: var(--color-text1); }
691
+ .nbw-item.nbw-indent { padding-left: 18px; }
692
+ .nbw-item.nbw-dim { opacity: 0.5; }
693
+ .nbw-item.nbw-active {
694
+ background: var(--color-surface); color: var(--color-text1);
695
+ border-left: 2px solid var(--color-accent); border-radius: 0 4px 4px 0;
696
+ }
697
+ .nbw-connect-btn { cursor: pointer; }
698
+ .nbw-connect-btn:hover { opacity: 1; }
699
+ .nbw-sources-srv { display: flex; align-items: center; gap: 8px; padding: 6px 0; font-size: 12px; }
700
+ .nbw-sources-srv-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--color-accent); }
701
+ .nbw-sources-srv-name { font-weight: 600; color: var(--color-text1); }
702
+ .nbw-sources-srv-meta { color: var(--color-text2); font-family: var(--font-mono, monospace); font-size: 10.5px; margin-left: auto; }
703
+ .nbw-add { margin-top: 10px; display: flex; gap: 4px; flex-wrap: wrap; }
704
+ .nbw-add .nb-btn { flex: 1 1 auto; font-size: 10px; padding: 3px 4px; }
705
+
706
+ .nbw-cells { display: flex; flex-direction: column; }
707
+ .nbw-cell .nb-cell { border-bottom: 1px solid var(--color-border); position: relative; }
708
+ .nbw-cell:last-child .nb-cell { border-bottom: none; }
709
+ .nbw-cell-head {
710
+ padding: 8px 16px;
711
+ display: flex; align-items: center; gap: 8px;
712
+ background: var(--color-surface2);
713
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
714
+ font-size: 10px; color: var(--color-text2);
715
+ letter-spacing: 0.06em;
716
+ }
717
+ .nbw-type {
718
+ padding: 1px 7px; border-radius: 3px; font-weight: 500;
719
+ text-transform: uppercase; letter-spacing: 0.08em; font-size: 9.5px;
720
+ }
721
+ .nbw-type-md { background: rgba(160,160,184,0.15); color: var(--color-text2); }
722
+ .nbw-type-sql { background: rgba(124,109,250,0.18); color: var(--color-accent); }
723
+ .nbw-type-js { background: rgba(62,207,178,0.15); color: var(--color-teal); }
724
+ .nbw-cell-name-edit {
725
+ background: transparent; border: none; outline: none;
726
+ color: var(--color-text1);
727
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
728
+ font-size: 10px; width: 140px;
729
+ }
730
+ .nbw-meta { margin-left: auto; display: flex; gap: 8px; align-items: center; }
731
+ .nbw-cell-body { padding: 14px 16px; font-size: 13.5px; line-height: 1.6; }
732
+ .nbw-cell-body.nbw-code {
733
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
734
+ font-size: 12.5px; line-height: 1.65;
735
+ }
736
+ .nbw-hidden { display: none !important; }
737
+
738
+ .nbw-result { border-top: 1px dashed var(--color-border); }
739
+ .nbw-result-table { width: 100%; border-collapse: collapse; font-size: 12px; }
740
+ .nbw-result-table thead tr { background: var(--color-surface2); }
741
+ .nbw-result-table th {
742
+ text-align: left; padding: 7px 16px; font-weight: 500;
743
+ color: var(--color-text2);
744
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
745
+ font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
746
+ border-top: 1px solid var(--color-border);
747
+ border-bottom: 1px solid var(--color-border);
748
+ }
749
+ .nbw-result-table td {
750
+ padding: 6px 16px; border-bottom: 1px solid var(--color-border);
751
+ font-variant-numeric: tabular-nums;
752
+ }
753
+ .nbw-result-table td:first-child { color: var(--color-text1); font-variant-numeric: normal; }
754
+ .nbw-share-bar { height: 8px; background: var(--color-accent); border-radius: 2px; }
755
+
756
+ .nbw-value {
757
+ margin: 0; padding: 12px 16px;
758
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
759
+ font-size: 12px; color: var(--color-text1);
760
+ background: var(--color-bg); white-space: pre-wrap; word-break: break-word;
761
+ }
762
+ .nbw-empty {
763
+ padding: 12px 16px; color: var(--color-text2);
764
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace); font-size: 11px;
765
+ font-style: italic;
766
+ }
767
+ .nbw-error {
768
+ padding: 10px 16px; color: #d66; background: rgba(220,80,80,0.08);
769
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace); font-size: 12px;
770
+ border-top: 1px solid rgba(220,80,80,0.3);
771
+ }
772
+
773
+ .nbw-toast-slot {
774
+ position: absolute; bottom: 14px; right: 14px; z-index: 50;
775
+ display: flex; flex-direction: column; gap: 6px; pointer-events: none;
776
+ }
777
+ .nbw-toast {
778
+ background: var(--color-surface2); color: var(--color-text1);
779
+ border: 1px solid var(--color-border); border-radius: 6px;
780
+ padding: 8px 12px; font-size: 11.5px;
781
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
782
+ pointer-events: auto; max-width: 420px;
783
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
784
+ transition: opacity 0.4s, transform 0.4s;
785
+ }
786
+ .nbw-toast a { color: var(--color-accent); text-decoration: underline; }
787
+ .nbw-toast-out { opacity: 0; transform: translateY(6px); }
788
+
789
+ /* ---- Live mode --------------------------------------------------------- */
790
+ .nb-live-badge {
791
+ display: inline-flex; align-items: center; gap: 4px;
792
+ background: rgba(62, 207, 130, 0.16); color: #2ea96b;
793
+ border: 1px solid rgba(62, 207, 130, 0.45);
794
+ padding: 1px 8px; border-radius: 999px;
795
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
796
+ font-size: 10px; letter-spacing: 0.06em; font-weight: 600;
797
+ text-transform: uppercase;
798
+ }
799
+ .nbw-live-meta {
800
+ padding: 4px 14px; color: var(--color-text2);
801
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
802
+ font-size: 10.5px; background: var(--color-surface2);
803
+ border-bottom: 1px solid var(--color-border);
804
+ }
805
+ .nbw-autorun-toggle {
806
+ display: inline-flex; align-items: center; gap: 5px;
807
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
808
+ font-size: 10.5px; color: var(--color-text2); cursor: pointer;
809
+ padding: 2px 6px; border-radius: 4px;
810
+ border: 1px solid var(--color-border);
811
+ }
812
+ .nbw-autorun-toggle:hover { color: var(--color-text1); }
813
+ .nbw-autorun-cb { accent-color: var(--color-accent); margin: 0; }
814
+ .nb-empty-state {
815
+ display: flex; align-items: center; gap: 12px;
816
+ margin: 10px 14px;
817
+ padding: 12px 14px;
818
+ background: rgba(245, 178, 53, 0.10);
819
+ border: 1px solid rgba(245, 178, 53, 0.45);
820
+ border-radius: 8px;
821
+ color: #a8741a;
822
+ font-size: 12.5px;
823
+ }
824
+ .nb-empty-icon { font-size: 22px; line-height: 1; }
825
+ .nb-empty-body { flex: 1; }
826
+ .nb-empty-title { font-weight: 600; color: #8a5e10; }
827
+ .nb-empty-desc {
828
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
829
+ font-size: 11px; opacity: 0.85; margin-top: 2px;
830
+ }
831
+ .nb-empty-retry { white-space: nowrap; }
832
+
833
+ .nbw-rt-badge {
834
+ display: inline-flex; align-items: center; gap: 4px;
835
+ padding: 1px 6px; border-radius: 3px;
836
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
837
+ font-size: 9.5px; font-weight: 600;
838
+ letter-spacing: 0.06em; text-transform: uppercase;
839
+ }
840
+ .nbw-rt-running { background: rgba(124,109,250,0.16); color: var(--color-accent); }
841
+ .nbw-rt-stale { background: rgba(245,178,53,0.18); color: #a8741a; }
842
+ .nbw-rt-frozen { background: rgba(160,160,184,0.14); color: var(--color-text2); opacity: 0.75; }
843
+ .nbw-rt-fresh { background: rgba(62,207,130,0.15); color: #2ea96b; }
844
+ .nbw-rt-spin {
845
+ width: 7px; height: 7px; border-radius: 50%;
846
+ border: 1.5px solid currentColor; border-right-color: transparent;
847
+ display: inline-block; animation: nbw-rt-spin 0.7s linear infinite;
848
+ }
849
+ @keyframes nbw-rt-spin { to { transform: rotate(360deg); } }
850
+ `;
851
+ document.head.appendChild(style);
852
+ }