@webmcp-auto-ui/ui 2.5.28 → 2.5.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1065 +0,0 @@
1
- // @ts-nocheck
2
- // ---------------------------------------------------------------------------
3
- // notebook-document — collaborative doc layout (deepnote-like, serif prose)
4
- // Title + optional presence avatars, inline highlights, margin comments w/
5
- // editable body + replies, minimal cell chrome. Honest meta + real share.
6
- // ---------------------------------------------------------------------------
7
-
8
- import {
9
- createState, injectStyles, mountRunControls, mountHistoryPanel,
10
- setupDnD, deleteCellWithConfirm, restoreCellFromSnapshot, addCell,
11
- autosize, openShareModal, registerHistoryObserver,
12
- collectDataServers,
13
- createPublishControls, autoConnectFrontmatterServers,
14
- registerExecutor, logHistory, addImportedCells, fmtRelTime, uid,
15
- renderCellLogs,
16
- createRuntimeOverlay, bootstrapLiveRefresh, effectiveResult,
17
- cellRuntimeStatus, isReRunnable, lastRefreshedAt,
18
- type NotebookState, type NotebookCell, type CellResult, type RuntimeOverlay,
19
- } from './shared.js';
20
- import { renderChart } from './chart-renderer.js';
21
- import { dispatchShare } from './share-handlers.js';
22
- import { renderProse } from './prose.js';
23
- import {
24
- openAddMdModal, openAddRecipeModal,
25
- } from './import-modals.js';
26
- import {
27
- extractCellsFromRecipe, extractCellFromMarkdown,
28
- } from './resource-extractor.js';
29
- import { mountLeftPane } from './left-pane.js';
30
- import { callToolViaPostMessage, MultiMcpBridge } from '@webmcp-auto-ui/core';
31
-
32
- // ---------------------------------------------------------------------------
33
- // Comment types (extended: body editable + threaded replies)
34
- // ---------------------------------------------------------------------------
35
-
36
- interface CommentNode {
37
- who: string;
38
- when: string;
39
- body: string;
40
- replies?: CommentNode[];
41
- }
42
-
43
- interface PresenceEditor {
44
- id?: string;
45
- initial?: string;
46
- color?: string;
47
- }
48
-
49
- // ---------------------------------------------------------------------------
50
- // Executors — JS via new Function, SQL via MCP tool dispatch
51
- // ---------------------------------------------------------------------------
52
-
53
- function findSqlTool(data: Record<string, unknown>): { server: string; tool: string } | null {
54
- const servers = collectDataServers(data);
55
- const patterns = [/^.*query_sql$/i, /^(query|run|execute)(_sql)?$/i];
56
- for (const pat of patterns) {
57
- for (const srv of servers) {
58
- for (const t of srv.tools || []) {
59
- if (pat.test(t.name)) return { server: srv.name, tool: t.name };
60
- }
61
- }
62
- }
63
- return null;
64
- }
65
-
66
- function registerDocExecutors(state: NotebookState, data: Record<string, unknown>): void {
67
- // JS executor — runs in a new Function with access to state.scope
68
- registerExecutor(state, 'js', async (ctx): Promise<CellResult> => {
69
- const t0 = Date.now();
70
- try {
71
- const keys = Object.keys(ctx.scope);
72
- const vals = keys.map((k) => ctx.scope[k]);
73
- const src = ctx.cell.content || '';
74
- // Wrap in async function so `await` works inside user code
75
- const fn = new Function(...keys, `"use strict"; return (async () => { ${src} })();`);
76
- const out = await fn(...vals);
77
- const dur = Date.now() - t0;
78
- if (out === undefined || out === null) return { ok: true, kind: 'empty', durationMs: dur };
79
- if (Array.isArray(out) && out.length && typeof out[0] === 'object' && out[0] !== null) {
80
- const cols = Array.from(new Set(out.flatMap((r: any) => Object.keys(r))));
81
- return { ok: true, kind: 'table', rows: out as any, columns: cols, rowCount: out.length, durationMs: dur };
82
- }
83
- return { ok: true, kind: 'value', value: out, durationMs: dur };
84
- } catch (err: any) {
85
- return { ok: false, error: String(err?.message ?? err), errorKind: 'runtime', durationMs: Date.now() - t0 };
86
- }
87
- });
88
-
89
- // SQL executor — looks up a query_sql-like tool via regex and dispatches via postMessage
90
- registerExecutor(state, 'sql', async (ctx): Promise<CellResult> => {
91
- const t0 = Date.now();
92
- const hit = findSqlTool(data);
93
- if (!hit) {
94
- return { ok: false, error: 'No query_sql-like tool found on connected servers.', errorKind: 'schema', durationMs: Date.now() - t0 };
95
- }
96
- try {
97
- const res: any = await callToolViaPostMessage(hit.tool, { sql: ctx.cell.content });
98
- const dur = Date.now() - t0;
99
- // Unwrap MCP content array → text → JSON (aligned with executors/sql.ts)
100
- const text = res?.content?.find?.((c: any) => c && c.type === 'text')?.text ?? '';
101
- let parsed: any = null;
102
- if (text) {
103
- try { parsed = JSON.parse(text); } catch { parsed = null; }
104
- }
105
- // Error shape inside tool result
106
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && 'error' in parsed && parsed.error) {
107
- return { ok: false, error: String(parsed.error), errorKind: 'runtime', durationMs: dur };
108
- }
109
- // { rows, columns } shape
110
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && Array.isArray(parsed.rows)) {
111
- const rows = parsed.rows as Record<string, unknown>[];
112
- const columns = Array.isArray(parsed.columns) && parsed.columns.length
113
- ? parsed.columns
114
- : (rows.length && typeof rows[0] === 'object' ? Object.keys(rows[0]) : []);
115
- return { ok: true, kind: 'table', rows, columns, rowCount: rows.length, truncated: !!parsed.truncated, durationMs: dur };
116
- }
117
- // Array of row objects
118
- if (Array.isArray(parsed) && parsed.length && typeof parsed[0] === 'object') {
119
- const rows = parsed as Record<string, unknown>[];
120
- return { ok: true, kind: 'table', rows, columns: Object.keys(rows[0]), rowCount: rows.length, durationMs: dur };
121
- }
122
- if (parsed != null) return { ok: true, kind: 'value', value: parsed, durationMs: dur };
123
- if (!text) return { ok: true, kind: 'empty', durationMs: dur };
124
- return { ok: true, kind: 'value', value: text, durationMs: dur };
125
- } catch (err: any) {
126
- return { ok: false, error: String(err?.message ?? err), errorKind: 'runtime', durationMs: Date.now() - t0 };
127
- }
128
- });
129
- }
130
-
131
- // ---------------------------------------------------------------------------
132
- // Toast — tiny transient notification for share results
133
- // ---------------------------------------------------------------------------
134
-
135
- function toast(message: string): void {
136
- let host = document.getElementById('nb-toast-host') as HTMLElement | null;
137
- if (!host) {
138
- host = document.createElement('div');
139
- host.id = 'nb-toast-host';
140
- host.style.cssText = 'position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:2000;display:flex;flex-direction:column;gap:6px;pointer-events:none;';
141
- document.body.appendChild(host);
142
- }
143
- const el = document.createElement('div');
144
- el.textContent = message;
145
- el.style.cssText = 'background:var(--color-surface,#222);color:var(--color-text1,#eee);border:1px solid var(--color-border,#444);padding:8px 14px;border-radius:6px;font-family:var(--font-mono,monospace);font-size:11.5px;box-shadow:0 2px 12px rgba(0,0,0,.3);opacity:0;transition:opacity .15s;';
146
- host.appendChild(el);
147
- requestAnimationFrame(() => { el.style.opacity = '1'; });
148
- setTimeout(() => { el.style.opacity = '0'; setTimeout(() => el.remove(), 200); }, 2800);
149
- }
150
-
151
- // ---------------------------------------------------------------------------
152
- // Main render
153
- // ---------------------------------------------------------------------------
154
-
155
- export async function render(container: HTMLElement, data: Record<string, unknown>): Promise<() => void> {
156
- injectStyles();
157
- injectLayoutStyles();
158
-
159
- const state: NotebookState = createState({
160
- id: data.id as string,
161
- title: data.title as string ?? 'Untitled notebook',
162
- mode: (data.mode as any) ?? 'edit',
163
- cells: data.cells as any,
164
- });
165
-
166
- registerDocExecutors(state, data);
167
-
168
- // Forward-ref wrapper so helpers called before `rerender` is defined still work.
169
- let rerenderImpl: (() => void) | null = null;
170
- const rerenderSafe = () => { rerenderImpl?.(); };
171
- autoConnectFrontmatterServers(data, rerenderSafe);
172
-
173
- container.classList.add('nb-root');
174
- container.classList.toggle('nb-view-mode', state.mode === 'view');
175
-
176
- // Presence is opt-in: only shown when data.presence is explicitly provided.
177
- const presence = (data.presence as { editors?: PresenceEditor[]; count?: number } | undefined);
178
- const hasPresence = !!presence && Array.isArray(presence.editors) && presence.editors.length > 0;
179
-
180
- container.innerHTML = `
181
- <div class="nbd-shell">
182
- <div class="nbd-presence">
183
- ${hasPresence ? `
184
- <div class="nbd-avatars">
185
- ${presence!.editors!.slice(0, 3).map((e, i) => `
186
- <div class="nbd-av nbd-av${(i % 3) + 1}"${e.color ? ` style="background:${escapeAttr(e.color)}22;color:${escapeAttr(e.color)};"` : ''}>${escapeHtml((e.initial ?? '?').slice(0, 2).toUpperCase())}</div>
187
- `).join('')}
188
- </div>
189
- <span class="nbd-label">${presence!.count ?? presence!.editors!.length} editor${(presence!.count ?? presence!.editors!.length) > 1 ? 's' : ''} online</span>
190
- ` : ''}
191
- <label class="nbd-live-toggle" title="re-execute SQL cells against connected MCP servers when viewed">
192
- <input type="checkbox" class="nbd-live-checkbox" ${state.autoRun ? 'checked' : ''}>
193
- <span>Live data</span>
194
- </label>
195
- <div class="nb-mode-switch" style="margin-left:auto;">
196
- <button class="nb-mode-edit nb-on">edit</button>
197
- <button class="nb-mode-view">view</button>
198
- </div>
199
- <button class="nb-btn nbd-history-btn">⟲ history</button>
200
- <span class="nbd-publish-badge-slot"></span>
201
- </div>
202
- <div class="nbd-title-row">
203
- <input class="nbd-title nb-doc-title" value="${escapeAttr(state.title)}">
204
- <span class="nbd-live-badge-slot"></span>
205
- </div>
206
- <div class="nbd-meta">edited <span class="nbd-edited-rel">${fmtRelTime(state.lastEditAt)}</span> ago</div>
207
- <div class="nbd-empty-state-slot"></div>
208
- <div class="nb-history-panel nbd-history-panel"></div>
209
- <div class="nbd-cells"></div>
210
- <div class="nbd-footer">
211
- <button class="nb-btn nb-add-cell" data-add="md">+ text</button>
212
- <button class="nb-btn nb-add-cell" data-add="sql">+ sql</button>
213
- <button class="nb-btn nb-add-cell" data-add="js">+ code</button>
214
- <button class="nb-btn nbd-add-md-btn">+ md</button>
215
- <button class="nb-btn nbd-add-recipe-btn">+ recipe</button>
216
- <div class="nbd-spacer">
217
- <span class="nbd-publish-slot"></span>
218
- <span class="nbd-share-link nbd-share-btn">share</span>
219
- </div>
220
- </div>
221
- <div class="nbd-publish-footer-slot"></div>
222
- </div>`;
223
-
224
- const shell = container.querySelector('.nbd-shell') as HTMLElement;
225
- const cellsEl = shell.querySelector('.nbd-cells') as HTMLElement;
226
- const historyPanel = shell.querySelector('.nbd-history-panel') as HTMLElement;
227
- const editedRelEl = shell.querySelector('.nbd-edited-rel') as HTMLElement;
228
- const liveBadgeSlot = shell.querySelector('.nbd-live-badge-slot') as HTMLElement;
229
- const emptyStateSlot = shell.querySelector('.nbd-empty-state-slot') as HTMLElement;
230
- const liveCheckbox = shell.querySelector('.nbd-live-checkbox') as HTMLInputElement;
231
- const liveToggleLabel = shell.querySelector('.nbd-live-toggle') as HTMLElement;
232
-
233
- // Live mode (autoRun) — overlay holds ephemeral results; canonical cells.lastResult untouched.
234
- let overlay: RuntimeOverlay | null = null;
235
- let liveCleanup: (() => void) | null = null;
236
-
237
- // Track active cell (last one clicked or focused) so imports insert near cursor
238
- let activeIdx: number | null = null;
239
- function activeCellIdx(): number | null { return activeIdx; }
240
-
241
- function renderCells() {
242
- cellsEl.innerHTML = '';
243
- state.cells.forEach((cell, idx) => {
244
- const el = renderCell(cell, state, rerender, overlay);
245
- el.addEventListener('mousedown', () => { activeIdx = idx; }, true);
246
- cellsEl.appendChild(el);
247
- });
248
- }
249
-
250
- function updateMeta() {
251
- if (editedRelEl) editedRelEl.textContent = fmtRelTime(state.lastEditAt);
252
- }
253
-
254
- function renderLiveBadge() {
255
- if (!liveBadgeSlot) return;
256
- if (state.mode === 'view' && state.autoRun === true) {
257
- const ts = lastRefreshedAt(overlay);
258
- const rel = ts ? `refreshed ${fmtRelTime(ts)} ago` : 'connecting…';
259
- liveBadgeSlot.innerHTML = `<span class="nb-live-badge nbd-live-badge">● Live</span><span class="nbd-live-rel">${escapeHtml(rel)}</span>`;
260
- } else {
261
- liveBadgeSlot.innerHTML = '';
262
- }
263
- }
264
-
265
- function renderEmptyState() {
266
- if (!emptyStateSlot) return;
267
- const showEmpty = !!(
268
- state.autoRun
269
- && state.mode === 'view'
270
- && overlay
271
- && (overlay.error || (overlay.finishedAt && overlay.outputs.size === 0))
272
- );
273
- if (!showEmpty) { emptyStateSlot.innerHTML = ''; return; }
274
- const ts = state.lastEditAt;
275
- const rel = ts ? `${fmtRelTime(ts)} ago` : 'earlier';
276
- emptyStateSlot.innerHTML = `
277
- <div class="nb-empty-state nbd-empty-state-banner">
278
- <div class="nb-empty-icon">📡</div>
279
- <div class="nb-empty-title">Live mode active, but no data server is reachable.</div>
280
- <div class="nb-empty-desc">Showing snapshots from <time>${escapeHtml(rel)}</time>.</div>
281
- <button class="nb-btn nb-empty-retry">retry connection</button>
282
- </div>`;
283
- const retry = emptyStateSlot.querySelector('.nb-empty-retry') as HTMLButtonElement | null;
284
- if (retry) retry.addEventListener('click', () => { startLiveRefresh(); });
285
- }
286
-
287
- function updateLiveToggleVisibility() {
288
- if (!liveToggleLabel) return;
289
- liveToggleLabel.style.display = state.mode === 'edit' ? '' : 'none';
290
- }
291
-
292
- function teardownLive() {
293
- try { liveCleanup?.(); } catch { /* ignore */ }
294
- liveCleanup = null;
295
- overlay = null;
296
- }
297
-
298
- function startLiveRefresh() {
299
- teardownLive();
300
- if (!(state.autoRun && state.mode === 'view')) return;
301
- overlay = createRuntimeOverlay();
302
- liveCleanup = bootstrapLiveRefresh({
303
- state,
304
- data,
305
- overlay,
306
- MultiMcpBridgeCtor: MultiMcpBridge as any,
307
- onCellChange: (cellId) => {
308
- // Re-render only the affected cell to avoid full DOM churn
309
- const idx = state.cells.findIndex((c) => c.id === cellId);
310
- if (idx < 0) return;
311
- const oldEl = cellsEl.children[idx] as HTMLElement | undefined;
312
- if (!oldEl) return;
313
- const newEl = renderCell(state.cells[idx]!, state, rerender, overlay);
314
- newEl.addEventListener('mousedown', () => { activeIdx = idx; }, true);
315
- oldEl.replaceWith(newEl);
316
- },
317
- onTick: () => { renderLiveBadge(); renderEmptyState(); },
318
- });
319
- renderLiveBadge();
320
- renderEmptyState();
321
- }
322
-
323
- function rerender() {
324
- mountHistoryPanel(historyPanel, state, (snap) => { restoreCellFromSnapshot(state, snap); rerender(); });
325
- renderCells();
326
- updateMeta();
327
- renderLiveBadge();
328
- renderEmptyState();
329
- updateLiveToggleVisibility();
330
- }
331
- rerenderImpl = rerender;
332
-
333
- if (liveCheckbox) {
334
- liveCheckbox.addEventListener('change', () => {
335
- state.autoRun = liveCheckbox.checked;
336
- logHistory(state, 'edit', state.autoRun ? 'enabled live data' : 'disabled live data');
337
- // When toggled in edit mode the overlay isn't active yet; re-evaluate on view switch.
338
- if (state.mode === 'view') {
339
- if (state.autoRun) startLiveRefresh();
340
- else { teardownLive(); rerender(); }
341
- }
342
- });
343
- }
344
-
345
- shell.querySelectorAll<HTMLElement>('[data-add]').forEach((btn) => {
346
- btn.addEventListener('click', () => {
347
- const type = btn.dataset.add as any;
348
- addCell(state, type);
349
- rerender();
350
- });
351
- });
352
-
353
- (shell.querySelector('.nbd-add-md-btn') as HTMLElement).addEventListener('click', () => {
354
- openAddMdModal((content) => {
355
- addImportedCells(state, [extractCellFromMarkdown(content)], activeCellIdx());
356
- rerender();
357
- });
358
- });
359
-
360
- (shell.querySelector('.nbd-add-recipe-btn') as HTMLElement).addEventListener('click', () => {
361
- openAddRecipeModal({
362
- scope: 'data',
363
- mcpServers: (data?.servers as any[] | undefined)?.map((s: any) => ({ name: s.name, url: s.url })) ?? [],
364
- onPick: (recipe) => {
365
- addImportedCells(
366
- state,
367
- extractCellsFromRecipe(recipe.body ?? '', { title: recipe.name, description: recipe.description }),
368
- activeCellIdx(),
369
- );
370
- rerender();
371
- },
372
- });
373
- });
374
-
375
- (shell.querySelector('.nbd-history-btn') as HTMLElement).addEventListener('click', () => {
376
- historyPanel.classList.toggle('nb-open');
377
- });
378
- (shell.querySelector('.nbd-share-btn') as HTMLElement).addEventListener('click', () => {
379
- openShareModal(state, (fmt) => {
380
- dispatchShare(fmt, state, {
381
- container,
382
- onResult: (info: any) => toast(info?.message ?? 'shared'),
383
- });
384
- });
385
- });
386
- (shell.querySelector('.nbd-title') as HTMLInputElement).addEventListener('input', (e) => {
387
- state.title = (e.target as HTMLInputElement).value;
388
- logHistory(state, 'edit', 'renamed notebook');
389
- updateMeta();
390
- });
391
- const editBtn = shell.querySelector('.nb-mode-edit') as HTMLElement;
392
- const viewBtn = shell.querySelector('.nb-mode-view') as HTMLElement;
393
- editBtn.addEventListener('click', () => {
394
- state.mode = 'edit';
395
- container.classList.remove('nb-view-mode');
396
- editBtn.classList.add('nb-on'); viewBtn.classList.remove('nb-on');
397
- teardownLive();
398
- rerender();
399
- });
400
- viewBtn.addEventListener('click', () => {
401
- state.mode = 'view';
402
- container.classList.add('nb-view-mode');
403
- viewBtn.classList.add('nb-on'); editBtn.classList.remove('nb-on');
404
- if (state.autoRun) startLiveRefresh();
405
- rerender();
406
- });
407
-
408
- const publishCleanup = createPublishControls(state, {
409
- buttonSlot: shell.querySelector('.nbd-publish-slot') as HTMLElement,
410
- badgeSlot: shell.querySelector('.nbd-publish-badge-slot') as HTMLElement,
411
- footerSlot: shell.querySelector('.nbd-publish-footer-slot') as HTMLElement,
412
- onPublished: () => rerender(),
413
- });
414
-
415
- // Left pane — collapsed bookmarks-style panel (mounted into outer container,
416
- // not into shell, so it sits to the left of the document body)
417
- const leftHandle = mountLeftPane(container, state, collectDataServers(data), {
418
- onInjectCells: (cells) => {
419
- addImportedCells(state, cells, activeCellIdx());
420
- rerender();
421
- },
422
- });
423
-
424
- setupDnD(cellsEl, state, rerender);
425
- const unsubHistory = registerHistoryObserver(() => {
426
- mountHistoryPanel(historyPanel, state, (snap) => { restoreCellFromSnapshot(state, snap); rerender(); });
427
- updateMeta();
428
- });
429
-
430
- rerender();
431
-
432
- // Mount-time bootstrap: if published with autoRun + view, kick off live refresh.
433
- if (state.autoRun && state.mode === 'view') startLiveRefresh();
434
-
435
- return () => {
436
- unsubHistory();
437
- teardownLive();
438
- try { leftHandle.destroy(); } catch { /* ignore */ }
439
- try { publishCleanup?.(); } catch { /* ignore */ }
440
- };
441
- }
442
-
443
- // ---------------------------------------------------------------------------
444
- // Cell renderer
445
- // ---------------------------------------------------------------------------
446
-
447
- function renderCell(cell: NotebookCell, state: NotebookState, rerender: () => void, overlay: RuntimeOverlay | null): HTMLElement {
448
- const wrap = document.createElement('div');
449
- wrap.className = 'nb-cell-wrapper nbd-cell';
450
- wrap.dataset.id = cell.id;
451
-
452
- if (cell.type === 'md') {
453
- const handle = document.createElement('span');
454
- handle.className = 'nb-drag-handle nbd-md-handle';
455
- handle.draggable = true;
456
- handle.textContent = '⋮⋮';
457
- wrap.appendChild(handle);
458
-
459
- if (state.mode === 'view') {
460
- // View mode: render MD source as HTML (read-only).
461
- const p = document.createElement('div');
462
- p.className = 'nbd-prose nbd-prose-view';
463
- p.innerHTML = renderProse(cell.content || '');
464
- wrap.appendChild(p);
465
- } else {
466
- // Edit mode: split panel — MD source textarea (top) + live preview (bottom).
467
- // We keep `cell.content` as canonical markdown source so the roundtrip
468
- // is lossless (re-opening the notebook shows the same `###` the user typed).
469
- const edit = document.createElement('textarea');
470
- edit.className = 'nbd-prose-edit';
471
- edit.value = cell.content || '';
472
- edit.rows = 2;
473
- edit.placeholder = 'write prose (markdown)…';
474
- edit.spellcheck = true;
475
-
476
- const preview = document.createElement('div');
477
- preview.className = 'nbd-prose nbd-prose-view';
478
- preview.innerHTML = renderProse(cell.content || '');
479
-
480
- let proseDebounce: any = null;
481
- edit.addEventListener('input', () => {
482
- cell.content = edit.value;
483
- autosize(edit);
484
- preview.innerHTML = renderProse(cell.content || '');
485
- if (proseDebounce) clearTimeout(proseDebounce);
486
- proseDebounce = setTimeout(() => logHistory(state, 'edit', 'edited prose'), 400);
487
- });
488
- wrap.appendChild(edit);
489
- wrap.appendChild(preview);
490
- setTimeout(() => autosize(edit), 0);
491
- }
492
-
493
- const del = document.createElement('button');
494
- del.className = 'nb-icon-btn nb-danger nbd-del-abs';
495
- del.textContent = '✕';
496
- del.addEventListener('click', () =>
497
- deleteCellWithConfirm(state, cell, () => 'markdown block', rerender)
498
- );
499
- wrap.appendChild(del);
500
- return wrap;
501
- }
502
-
503
- const row = document.createElement('div');
504
- row.className = 'nbd-row' + (cell.comment ? '' : ' nbd-no-comment');
505
-
506
- const codeCell = document.createElement('div');
507
- codeCell.className = 'nb-code-cell nbd-code-cell';
508
-
509
- const head = document.createElement('div');
510
- head.className = 'nbd-cell-head';
511
- const showLiveBadges = state.mode === 'view' && state.autoRun === true;
512
- const rtStatus = cellRuntimeStatus(cell, overlay);
513
- let statusBadgeHtml = '';
514
- if (showLiveBadges) {
515
- if (rtStatus === 'running' || rtStatus === 'pending') {
516
- statusBadgeHtml = '<span class="nbd-status-badge nbd-status-running" title="re-running"><span class="nbd-spinner"></span> running</span>';
517
- } else if (rtStatus === 'stale') {
518
- statusBadgeHtml = '<span class="nbd-status-badge nbd-status-stale" title="couldn\'t refresh — showing snapshot">stale</span>';
519
- } else if (rtStatus === 'frozen' && !isReRunnable(cell)) {
520
- statusBadgeHtml = '<span class="nbd-status-badge nbd-status-frozen" title="not re-executed in live mode">frozen</span>';
521
- } else if (rtStatus === 'fresh') {
522
- statusBadgeHtml = '<span class="nbd-status-badge nbd-status-fresh" title="re-executed against live data">fresh</span>';
523
- }
524
- }
525
- head.innerHTML = `
526
- <span class="nb-drag-handle" draggable="true" title="drag">⋮⋮</span>
527
- <span class="nbd-run-controls"></span>
528
- <span class="${cell.type === 'sql' ? 'nbd-type-sql' : 'nbd-type-js'}">${cell.type}</span>
529
- ${statusBadgeHtml}
530
- <span class="nbd-meta-info">${cell.lastMs != null ? cell.lastMs + 'ms' : ''}</span>
531
- <div class="nbd-actions">
532
- <button class="nb-icon-btn nb-toggle-src">${cell.hideSource ? '▸ src' : '◂ src'}</button>
533
- <button class="nb-icon-btn nb-toggle-res">${cell.hideResult ? '▸ res' : '◂ res'}</button>
534
- ${!cell.comment ? '<button class="nb-icon-btn nbd-add-comment" title="add comment">+ note</button>' : ''}
535
- <button class="nb-icon-btn nb-danger nbd-del">✕</button>
536
- </div>`;
537
- codeCell.appendChild(head);
538
- mountRunControls(head.querySelector('.nbd-run-controls') as HTMLElement, cell, wrap, state, rerender);
539
-
540
- const body = document.createElement('div');
541
- body.className = 'nbd-code-body' + (cell.hideSource ? ' nbd-hidden' : '');
542
- const ta = document.createElement('textarea');
543
- ta.className = 'nb-code-edit';
544
- ta.value = cell.content;
545
- ta.rows = 1;
546
- ta.spellcheck = false;
547
- ta.addEventListener('input', () => { cell.content = ta.value; autosize(ta); cell.status = 'stale'; });
548
- body.appendChild(ta);
549
- codeCell.appendChild(body);
550
- setTimeout(() => autosize(ta), 0);
551
-
552
- if (!cell.hideResult) {
553
- // Live overlay takes precedence over frozen lastResult; canonical state untouched.
554
- const eff = effectiveResult(cell, overlay) ?? cell.lastResult;
555
- const res = renderResult(eff);
556
- if (res) codeCell.appendChild(res);
557
- }
558
-
559
- row.appendChild(codeCell);
560
-
561
- if (cell.comment) {
562
- row.appendChild(renderCommentColumn(cell, state, rerender));
563
- }
564
-
565
- (head.querySelector('.nb-toggle-src') as HTMLElement).addEventListener('click', () => { cell.hideSource = !cell.hideSource; rerender(); });
566
- (head.querySelector('.nb-toggle-res') as HTMLElement).addEventListener('click', () => { cell.hideResult = !cell.hideResult; rerender(); });
567
- const addCommentBtn = head.querySelector('.nbd-add-comment') as HTMLElement | null;
568
- if (addCommentBtn) {
569
- addCommentBtn.addEventListener('click', () => {
570
- (cell as any).comment = { who: 'you', when: 'now', body: 'write a note…', replies: [] } as CommentNode;
571
- logHistory(state, 'edit', 'added comment');
572
- rerender();
573
- });
574
- }
575
- (head.querySelector('.nbd-del') as HTMLElement).addEventListener('click', () =>
576
- deleteCellWithConfirm(state, cell, (c) => `${c.type} cell`, rerender)
577
- );
578
-
579
- wrap.appendChild(row);
580
- return wrap;
581
- }
582
-
583
- // ---------------------------------------------------------------------------
584
- // Result rendering — switch on cell.lastResult.kind
585
- // Document style: discrete tables, no ASCII bar charts.
586
- // ---------------------------------------------------------------------------
587
-
588
- function renderResult(res: CellResult | undefined): HTMLElement | null {
589
- if (!res) return null;
590
-
591
- const wrap = document.createElement('div');
592
- wrap.className = 'nbd-result-inline';
593
-
594
- // Logs panel — prepended, shared across all widgets
595
- const logsEl = renderCellLogs(res);
596
- if (logsEl) wrap.appendChild(logsEl);
597
-
598
- if (!res.ok) {
599
- wrap.classList.add('nbd-result-error');
600
- const errHost = document.createElement('div');
601
- errHost.innerHTML = `<div class="nbd-err-kind">${escapeHtml(res.errorKind ?? 'error')}</div><pre class="nbd-err-msg">${escapeHtml(res.error)}</pre>`;
602
- while (errHost.firstChild) wrap.appendChild(errHost.firstChild);
603
- return wrap;
604
- }
605
-
606
- if (res.kind === 'empty') {
607
- const empty = document.createElement('div');
608
- empty.className = 'nbd-empty-res';
609
- empty.textContent = '— no output —';
610
- wrap.appendChild(empty);
611
- return wrap;
612
- }
613
-
614
- if (res.kind === 'value') {
615
- const val = res.value;
616
- const pretty = typeof val === 'string' ? val : (() => {
617
- try { return JSON.stringify(val, null, 2); } catch { return String(val); }
618
- })();
619
- const pre = document.createElement('pre');
620
- pre.className = 'nbd-value-res';
621
- pre.textContent = pretty;
622
- wrap.appendChild(pre);
623
- return wrap;
624
- }
625
-
626
- if (res.kind === 'chart') {
627
- const chart = document.createElement('div');
628
- chart.className = 'nb-chart';
629
- wrap.appendChild(chart);
630
- renderChart(chart, res.spec).catch(() => { /* fallback handled internally */ });
631
- return wrap;
632
- }
633
-
634
- if (res.kind === 'table') {
635
- const cols = res.columns.length ? res.columns : (res.rows[0] ? Object.keys(res.rows[0]) : []);
636
- const preview = res.rows.slice(0, 12);
637
- const tbl = document.createElement('table');
638
- tbl.className = 'nbd-table';
639
- const thead = document.createElement('thead');
640
- thead.innerHTML = `<tr>${cols.map((c) => `<th>${escapeHtml(String(c))}</th>`).join('')}</tr>`;
641
- tbl.appendChild(thead);
642
- const tbody = document.createElement('tbody');
643
- tbody.innerHTML = preview.map((r) => `<tr>${cols.map((c) => `<td>${escapeHtml(formatCell((r as any)[c]))}</td>`).join('')}</tr>`).join('');
644
- tbl.appendChild(tbody);
645
- wrap.appendChild(tbl);
646
- if (res.rowCount > preview.length || res.truncated) {
647
- const foot = document.createElement('div');
648
- foot.className = 'nbd-table-foot';
649
- foot.textContent = `${res.rowCount} rows${res.truncated ? ' · truncated' : ''}`;
650
- wrap.appendChild(foot);
651
- }
652
- return wrap;
653
- }
654
-
655
- return null;
656
- }
657
-
658
- function formatCell(v: unknown): string {
659
- if (v == null) return '';
660
- if (typeof v === 'object') { try { return JSON.stringify(v); } catch { return String(v); } }
661
- return String(v);
662
- }
663
-
664
- // ---------------------------------------------------------------------------
665
- // Comments — editable body + threaded replies
666
- // ---------------------------------------------------------------------------
667
-
668
- function renderCommentColumn(cell: NotebookCell, state: NotebookState, rerender: () => void): HTMLElement {
669
- const col = document.createElement('div');
670
- col.className = 'nbd-comment';
671
-
672
- const root = (cell.comment as any as CommentNode);
673
- // Ensure replies array exists
674
- if (!Array.isArray(root.replies)) root.replies = [];
675
-
676
- col.appendChild(renderCommentNode(root, state, () => {
677
- logHistory(state, 'edit', 'edited comment');
678
- }));
679
-
680
- // Replies — rendered with indent
681
- const repliesWrap = document.createElement('div');
682
- repliesWrap.className = 'nbd-replies';
683
- root.replies!.forEach((reply) => {
684
- const rel = renderCommentNode(reply, state, () => logHistory(state, 'edit', 'edited reply'));
685
- rel.classList.add('nbd-reply');
686
- repliesWrap.appendChild(rel);
687
- });
688
- col.appendChild(repliesWrap);
689
-
690
- const replyBtn = document.createElement('button');
691
- replyBtn.className = 'nb-icon-btn nbd-reply-btn';
692
- replyBtn.textContent = '+ reply';
693
- replyBtn.addEventListener('click', () => {
694
- root.replies!.push({ who: 'you', when: 'now', body: 'write a reply…' });
695
- logHistory(state, 'edit', 'added reply');
696
- rerender();
697
- });
698
- col.appendChild(replyBtn);
699
-
700
- return col;
701
- }
702
-
703
- function renderCommentNode(
704
- node: CommentNode,
705
- _state: NotebookState,
706
- onEdit: () => void,
707
- ): HTMLElement {
708
- const el = document.createElement('div');
709
- el.className = 'nbd-comment-node';
710
- el.innerHTML = `
711
- <div class="nbd-comment-who">
712
- <div class="nbd-av-small">${escapeHtml((node.who || '?').slice(0, 2).toUpperCase())}</div>
713
- <span class="nbd-who-name">${escapeHtml(node.who || '')}</span>
714
- <span class="nbd-when">${escapeHtml(node.when || '')}</span>
715
- </div>
716
- <div class="nbd-comment-body" contenteditable="true" spellcheck="false"></div>`;
717
- const body = el.querySelector('.nbd-comment-body') as HTMLElement;
718
- body.textContent = node.body || '';
719
- let deb: any = null;
720
- body.addEventListener('input', () => {
721
- node.body = body.innerText;
722
- if (deb) clearTimeout(deb);
723
- deb = setTimeout(() => onEdit(), 300);
724
- });
725
- return el;
726
- }
727
-
728
- // ---------------------------------------------------------------------------
729
- // Helpers
730
- // ---------------------------------------------------------------------------
731
-
732
- function escapeHtml(s: string): string {
733
- return (s ?? '').toString().replace(/[&<>"']/g, (c) => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c]!));
734
- }
735
- function escapeAttr(s: string): string {
736
- return (s ?? '').toString().replace(/"/g, '&quot;');
737
- }
738
-
739
- // ---------------------------------------------------------------------------
740
- // Layout styles — serif "document" look
741
- // ---------------------------------------------------------------------------
742
-
743
- function injectLayoutStyles(): void {
744
- if (document.getElementById('nbd-styles')) return;
745
- const style = document.createElement('style');
746
- style.id = 'nbd-styles';
747
- style.textContent = `
748
- .nbd-shell {
749
- background: var(--color-surface);
750
- border: 1px solid var(--color-border);
751
- border-radius: 12px;
752
- padding: 28px 32px;
753
- font-family: Palatino, 'Palatino Linotype', Georgia, serif;
754
- }
755
- .nbd-presence { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
756
- .nbd-avatars { display: flex; }
757
- .nbd-av {
758
- width: 22px; height: 22px; border-radius: 50%;
759
- font-family: var(--font-mono, 'IBM Plex Mono', monospace);
760
- font-size: 9px; font-weight: 600;
761
- display: flex; align-items: center; justify-content: center;
762
- border: 2px solid var(--color-surface);
763
- }
764
- .nbd-av + .nbd-av { margin-left: -6px; }
765
- .nbd-av1 { background: rgba(124,109,250,0.25); color: var(--color-accent); }
766
- .nbd-av2 { background: rgba(62,207,178,0.22); color: var(--color-teal); }
767
- .nbd-av3 { background: rgba(240,160,80,0.22); color: var(--color-amber); }
768
- .nbd-label {
769
- font-family: var(--font-mono, 'IBM Plex Mono', monospace);
770
- font-size: 11px; color: var(--color-text2);
771
- }
772
- .nbd-title {
773
- font-family: Palatino, 'Palatino Linotype', Georgia, serif;
774
- font-size: 28px; font-weight: 600;
775
- letter-spacing: -0.01em; margin: 6px 0 2px;
776
- background: transparent; border: none; outline: none;
777
- color: var(--color-text1);
778
- width: 100%; padding: 2px 4px; border-radius: 3px;
779
- }
780
- .nbd-title:focus { background: var(--color-bg); }
781
- .nbd-meta {
782
- font-family: var(--font-mono, 'IBM Plex Mono', monospace);
783
- font-size: 11px; color: var(--color-text2);
784
- margin-bottom: 20px;
785
- }
786
- .nbd-history-panel { margin-bottom: 12px; }
787
-
788
- .nbd-cell { position: relative; margin-bottom: 18px; }
789
- .nbd-prose {
790
- font-family: Palatino, 'Palatino Linotype', Georgia, serif;
791
- font-size: 16px; line-height: 1.7;
792
- color: var(--color-text1);
793
- outline: none;
794
- padding: 4px 6px;
795
- border-radius: 3px;
796
- border: 1px dashed transparent;
797
- text-align: justify;
798
- }
799
- .nbd-prose:focus { border-color: var(--color-border); background: var(--color-bg); }
800
- .nbd-prose-edit {
801
- display: block; width: 100%;
802
- background: var(--color-bg);
803
- border: 1px dashed var(--color-border); border-radius: 4px;
804
- padding: 8px 10px; margin-bottom: 6px;
805
- font-family: var(--font-mono, 'IBM Plex Mono', monospace);
806
- font-size: 13px; line-height: 1.6;
807
- color: var(--color-text1);
808
- outline: none; resize: none; overflow: hidden;
809
- }
810
- .nbd-prose-edit:focus { border-color: var(--color-border2); border-style: solid; }
811
- .nbd-prose-view {
812
- margin-top: 2px;
813
- }
814
- .nbd-prose mark {
815
- background: rgba(240,160,80,0.18);
816
- color: var(--color-amber);
817
- padding: 0 4px; border-radius: 2px;
818
- }
819
- .nbd-prose h1, .nbd-prose h2, .nbd-prose h3 {
820
- font-family: Palatino, 'Palatino Linotype', Georgia, serif;
821
- text-align: left; margin: 1em 0 0.4em;
822
- }
823
- .nbd-md-handle { position: absolute; left: -20px; top: 6px; }
824
- .nbd-del-abs {
825
- position: absolute; top: 4px; right: 4px;
826
- opacity: 0; transition: opacity 0.15s;
827
- }
828
- .nbd-cell:hover .nbd-del-abs { opacity: 0.5; }
829
- .nbd-del-abs:hover { opacity: 1 !important; }
830
-
831
- .nbd-row {
832
- display: grid;
833
- grid-template-columns: 1fr 180px;
834
- gap: 16px; align-items: start;
835
- }
836
- .nbd-row.nbd-no-comment { grid-template-columns: 1fr; }
837
-
838
- .nbd-code-cell {
839
- border: 1px solid var(--color-border);
840
- border-radius: 8px; overflow: hidden;
841
- background: var(--color-bg);
842
- }
843
- .nbd-cell-head {
844
- padding: 7px 12px;
845
- display: flex; align-items: center; gap: 8px;
846
- border-bottom: 1px solid var(--color-border);
847
- font-family: var(--font-mono, 'IBM Plex Mono', monospace);
848
- font-size: 10.5px; color: var(--color-text2);
849
- }
850
- .nbd-type-sql { color: var(--color-accent); text-transform: uppercase; letter-spacing: 0.08em; font-size: 9.5px; }
851
- .nbd-type-js { color: var(--color-teal); text-transform: uppercase; letter-spacing: 0.08em; font-size: 9.5px; }
852
- .nbd-actions { margin-left: auto; display: flex; gap: 6px; }
853
- .nbd-code-body { padding: 11px 12px; }
854
- .nbd-hidden { display: none !important; }
855
-
856
- .nbd-result-inline {
857
- background: var(--color-surface2);
858
- padding: 10px 12px;
859
- border-top: 1px solid var(--color-border);
860
- font-family: Palatino, 'Palatino Linotype', Georgia, serif;
861
- font-size: 13px;
862
- color: var(--color-text1);
863
- }
864
- .nbd-result-inline.nbd-result-error {
865
- background: rgba(250,109,124,0.08);
866
- border-top-color: var(--color-accent2);
867
- }
868
- .nbd-err-kind {
869
- font-family: var(--font-mono, monospace);
870
- font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em;
871
- color: var(--color-accent2); margin-bottom: 4px;
872
- }
873
- .nbd-err-msg {
874
- font-family: var(--font-mono, monospace);
875
- font-size: 11.5px; color: var(--color-text1);
876
- white-space: pre-wrap; margin: 0;
877
- }
878
- .nbd-empty-res {
879
- font-style: italic; color: var(--color-text2); font-size: 12px;
880
- }
881
- .nbd-value-res {
882
- font-family: var(--font-mono, monospace);
883
- font-size: 11.5px; color: var(--color-text1);
884
- white-space: pre-wrap; margin: 0;
885
- }
886
- .nbd-chart-note {
887
- font-family: var(--font-mono, monospace);
888
- font-size: 11px; color: var(--color-text2);
889
- font-style: italic;
890
- }
891
- .nbd-table {
892
- width: 100%; border-collapse: collapse;
893
- font-variant-numeric: tabular-nums;
894
- font-family: Palatino, 'Palatino Linotype', Georgia, serif;
895
- font-size: 12.5px;
896
- }
897
- .nbd-table th {
898
- text-align: left; font-weight: 500; font-style: italic;
899
- color: var(--color-text2);
900
- border-bottom: 1px solid var(--color-border);
901
- padding: 4px 10px 4px 0;
902
- }
903
- .nbd-table td {
904
- padding: 3px 10px 3px 0;
905
- color: var(--color-text1);
906
- border-bottom: 1px solid rgba(160,160,184,0.08);
907
- }
908
- .nbd-table tr:last-child td { border-bottom: none; }
909
- .nbd-table-foot {
910
- font-family: var(--font-mono, monospace);
911
- font-size: 10px; color: var(--color-text2);
912
- margin-top: 6px; font-style: italic;
913
- }
914
-
915
- .nbd-comment {
916
- background: rgba(240,160,80,0.08);
917
- border-left: 2px solid var(--color-amber);
918
- border-radius: 0 6px 6px 0;
919
- padding: 10px 12px;
920
- font-size: 12px;
921
- display: flex; flex-direction: column; gap: 6px;
922
- }
923
- .nbd-comment-who { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
924
- .nbd-av-small {
925
- width: 15px; height: 15px; border-radius: 50%;
926
- background: rgba(240,160,80,0.25); color: var(--color-amber);
927
- font-family: var(--font-mono, 'IBM Plex Mono', monospace);
928
- font-size: 8px; font-weight: 600;
929
- display: flex; align-items: center; justify-content: center;
930
- }
931
- .nbd-who-name {
932
- font-family: var(--font-mono, 'IBM Plex Mono', monospace);
933
- font-size: 10.5px; color: var(--color-amber); font-weight: 500;
934
- }
935
- .nbd-when {
936
- margin-left: auto;
937
- font-family: var(--font-mono, 'IBM Plex Mono', monospace);
938
- font-size: 10px; color: var(--color-text2);
939
- }
940
- .nbd-comment-body {
941
- color: var(--color-text1); line-height: 1.5;
942
- outline: none; min-height: 1.2em;
943
- padding: 2px 4px; border-radius: 3px;
944
- border: 1px dashed transparent;
945
- font-family: Palatino, 'Palatino Linotype', Georgia, serif;
946
- }
947
- .nbd-comment-body:focus {
948
- border-color: rgba(240,160,80,0.3);
949
- background: rgba(255,255,255,0.02);
950
- }
951
- .nbd-replies {
952
- display: flex; flex-direction: column; gap: 6px;
953
- margin-left: 10px;
954
- border-left: 1px solid rgba(240,160,80,0.25);
955
- padding-left: 8px;
956
- }
957
- .nbd-reply { font-size: 11.5px; }
958
- .nbd-reply .nbd-comment-body { font-size: 11.5px; }
959
- .nbd-reply-btn {
960
- align-self: flex-start;
961
- font-size: 10.5px !important;
962
- color: var(--color-amber) !important;
963
- padding: 2px 6px !important;
964
- }
965
- .nbd-add-comment {
966
- color: var(--color-amber) !important;
967
- }
968
-
969
- .nbd-footer {
970
- display: flex; gap: 8px;
971
- padding-top: 14px; margin-top: 20px;
972
- border-top: 1px solid var(--color-border);
973
- align-items: center;
974
- }
975
- .nbd-spacer { margin-left: auto; }
976
- .nbd-share-link {
977
- font-family: var(--font-mono, 'IBM Plex Mono', monospace);
978
- font-size: 11px; color: var(--color-text2);
979
- cursor: pointer;
980
- padding: 5px 11px;
981
- border: 1px solid var(--color-border);
982
- border-radius: 6px;
983
- }
984
- .nbd-share-link:hover { color: var(--color-text1); border-color: var(--color-border2); }
985
- .nbd-publish-slot { display: inline-flex; }
986
- .nbd-publish-badge-slot { margin-left: 8px; }
987
- .nbd-publish-footer-slot { margin-top: 12px; padding: 0 16px; }
988
-
989
- /* Live mode (autoRun) */
990
- .nbd-live-toggle {
991
- display: inline-flex; align-items: center; gap: 6px;
992
- font-family: var(--font-mono, 'IBM Plex Mono', monospace);
993
- font-size: 11px; color: var(--color-text2);
994
- cursor: pointer;
995
- padding: 3px 8px;
996
- border-radius: 4px;
997
- }
998
- .nbd-live-toggle:hover { color: var(--color-text1); background: var(--color-bg); }
999
- .nbd-live-checkbox { margin: 0; cursor: pointer; }
1000
- .nbd-title-row { display: flex; align-items: center; gap: 12px; }
1001
- .nbd-title-row .nbd-title { flex: 1; }
1002
- .nbd-live-badge-slot { display: inline-flex; align-items: center; gap: 8px; }
1003
- .nb-live-badge.nbd-live-badge {
1004
- display: inline-flex; align-items: center;
1005
- padding: 2px 9px; border-radius: 999px;
1006
- background: rgba(62,207,178,0.15);
1007
- color: var(--color-teal, #3ecfb2);
1008
- border: 1px solid rgba(62,207,178,0.4);
1009
- font-family: var(--font-mono, 'IBM Plex Mono', monospace);
1010
- font-size: 10.5px; font-weight: 500; letter-spacing: 0.04em;
1011
- }
1012
- .nbd-live-rel {
1013
- font-family: var(--font-mono, 'IBM Plex Mono', monospace);
1014
- font-size: 10.5px; color: var(--color-text2);
1015
- }
1016
- .nbd-status-badge {
1017
- display: inline-flex; align-items: center; gap: 4px;
1018
- padding: 1px 7px; border-radius: 999px;
1019
- font-family: var(--font-mono, 'IBM Plex Mono', monospace);
1020
- font-size: 9.5px; font-weight: 500; letter-spacing: 0.04em;
1021
- text-transform: uppercase;
1022
- }
1023
- .nbd-status-running { background: rgba(124,109,250,0.15); color: var(--color-accent, #7c6dfa); }
1024
- .nbd-status-stale { background: rgba(240,160,80,0.18); color: var(--color-amber, #f0a050); border: 1px solid rgba(240,160,80,0.35); }
1025
- .nbd-status-frozen { background: transparent; color: var(--color-text2); border: 1px dashed var(--color-border); opacity: 0.7; }
1026
- .nbd-status-fresh { background: rgba(62,207,178,0.12); color: var(--color-teal, #3ecfb2); }
1027
- .nbd-spinner {
1028
- width: 8px; height: 8px; border-radius: 50%;
1029
- border: 1.5px solid currentColor; border-top-color: transparent;
1030
- animation: nbd-spin 0.7s linear infinite;
1031
- display: inline-block;
1032
- }
1033
- @keyframes nbd-spin { to { transform: rotate(360deg); } }
1034
-
1035
- .nb-empty-state.nbd-empty-state-banner {
1036
- display: grid;
1037
- grid-template-columns: auto 1fr auto;
1038
- grid-template-rows: auto auto;
1039
- gap: 4px 14px;
1040
- align-items: center;
1041
- margin: 8px 0 16px;
1042
- padding: 14px 18px;
1043
- border-radius: 8px;
1044
- background: rgba(240,160,80,0.08);
1045
- border: 1px solid rgba(240,160,80,0.35);
1046
- border-left: 3px solid var(--color-amber, #f0a050);
1047
- }
1048
- .nbd-empty-state-banner .nb-empty-icon {
1049
- grid-row: 1 / span 2;
1050
- font-size: 22px; line-height: 1;
1051
- }
1052
- .nbd-empty-state-banner .nb-empty-title {
1053
- font-family: var(--font-mono, 'IBM Plex Mono', monospace);
1054
- font-size: 12px; color: var(--color-amber, #f0a050); font-weight: 500;
1055
- }
1056
- .nbd-empty-state-banner .nb-empty-desc {
1057
- font-family: Palatino, Georgia, serif;
1058
- font-size: 12.5px; color: var(--color-text2);
1059
- }
1060
- .nbd-empty-state-banner .nb-empty-retry {
1061
- grid-row: 1 / span 2;
1062
- }
1063
- `;
1064
- document.head.appendChild(style);
1065
- }