@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.
- package/README.md +2 -2
- package/package.json +15 -3
- package/src/agent/AgentConsole.svelte +1 -21
- package/src/agent/DataServersPanel.svelte +164 -0
- package/src/agent/LLMSelector.svelte +26 -8
- package/src/agent/ModelCacheManager.svelte +359 -0
- package/src/agent/{GemmaLoader.svelte → ModelLoader.svelte} +1 -1
- package/src/agent/SettingsPanel.svelte +16 -2
- package/src/index.ts +45 -31
- package/src/widgets/WidgetRenderer.svelte +118 -115
- package/src/widgets/export-widget.ts +28 -1
- package/src/widgets/helpers/safe-image.ts +78 -0
- package/src/widgets/notebook/.gitkeep +0 -0
- package/src/widgets/notebook/chart-renderer.ts +63 -0
- package/src/widgets/notebook/compact.ts +823 -0
- package/src/widgets/notebook/document.ts +1065 -0
- package/src/widgets/notebook/editorial.ts +936 -0
- package/src/widgets/notebook/executors/.gitkeep +1 -0
- package/src/widgets/notebook/executors/index.ts +4 -0
- package/src/widgets/notebook/executors/js-worker.ts +269 -0
- package/src/widgets/notebook/executors/sql.ts +206 -0
- package/src/widgets/notebook/import-modals.ts +553 -0
- package/src/widgets/notebook/left-pane.ts +249 -0
- package/src/widgets/notebook/prose.ts +280 -0
- package/src/widgets/notebook/recipe-browser.ts +350 -0
- package/src/widgets/notebook/recipes/compact.md +124 -0
- package/src/widgets/notebook/recipes/document.md +139 -0
- package/src/widgets/notebook/recipes/editorial.md +120 -0
- package/src/widgets/notebook/recipes/workspace.md +119 -0
- package/src/widgets/notebook/resource-extractor.ts +162 -0
- package/src/widgets/notebook/share-handlers.ts +222 -0
- package/src/widgets/notebook/shared.ts +1592 -0
- package/src/widgets/notebook/workspace.ts +852 -0
- package/src/widgets/rich/cards.ts +181 -0
- package/src/widgets/rich/carousel.ts +319 -0
- package/src/widgets/rich/chart-rich.ts +386 -0
- package/src/widgets/rich/d3.ts +503 -0
- package/src/widgets/rich/data-table.ts +342 -0
- package/src/widgets/rich/gallery.ts +350 -0
- package/src/widgets/rich/grid-data.ts +173 -0
- package/src/widgets/rich/hemicycle.ts +313 -0
- package/src/widgets/rich/js-sandbox.ts +106 -0
- package/src/widgets/rich/json-viewer.ts +202 -0
- package/src/widgets/rich/log.ts +143 -0
- package/src/widgets/rich/map.ts +218 -0
- package/src/widgets/rich/profile.ts +256 -0
- package/src/widgets/rich/sankey.ts +262 -0
- package/src/widgets/rich/stat-card.ts +125 -0
- package/src/widgets/rich/timeline.ts +179 -0
- package/src/widgets/rich/trombinoscope.ts +246 -0
- package/src/widgets/simple/actions.ts +89 -0
- package/src/widgets/simple/alert.ts +100 -0
- package/src/widgets/simple/chart.ts +189 -0
- package/src/widgets/simple/code.ts +79 -0
- package/src/widgets/simple/kv.ts +68 -0
- package/src/widgets/simple/list.ts +89 -0
- package/src/widgets/simple/stat.ts +58 -0
- package/src/widgets/simple/tags.ts +125 -0
- package/src/widgets/simple/text.ts +198 -0
- package/src/wm/FloatingLayout.svelte +2 -0
- package/src/wm/LinkIndicators.svelte +8 -15
- package/src/widgets/SafeImage.svelte +0 -76
- package/src/widgets/rich/Cards.svelte +0 -39
- package/src/widgets/rich/Carousel.svelte +0 -88
- package/src/widgets/rich/Chart.svelte +0 -142
- package/src/widgets/rich/D3Widget.svelte +0 -373
- package/src/widgets/rich/DataTable.svelte +0 -62
- package/src/widgets/rich/Gallery.svelte +0 -94
- package/src/widgets/rich/GridData.svelte +0 -44
- package/src/widgets/rich/Hemicycle.svelte +0 -78
- package/src/widgets/rich/JsSandbox.svelte +0 -51
- package/src/widgets/rich/JsonViewer.svelte +0 -42
- package/src/widgets/rich/LogViewer.svelte +0 -24
- package/src/widgets/rich/MapView.svelte +0 -140
- package/src/widgets/rich/ProfileCard.svelte +0 -59
- package/src/widgets/rich/Sankey.svelte +0 -38
- package/src/widgets/rich/StatCard.svelte +0 -35
- package/src/widgets/rich/Timeline.svelte +0 -43
- package/src/widgets/rich/Trombinoscope.svelte +0 -48
- package/src/widgets/simple/ActionsBlock.svelte +0 -15
- package/src/widgets/simple/AlertBlock.svelte +0 -11
- package/src/widgets/simple/ChartBlock.svelte +0 -21
- package/src/widgets/simple/CodeBlock.svelte +0 -11
- package/src/widgets/simple/KVBlock.svelte +0 -16
- package/src/widgets/simple/ListBlock.svelte +0 -17
- package/src/widgets/simple/StatBlock.svelte +0 -14
- package/src/widgets/simple/TagsBlock.svelte +0 -15
- package/src/widgets/simple/TextBlock.svelte +0 -122
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// notebook-editorial — publication-ready layout (observable-like)
|
|
4
|
+
// Serif prose + cells in a single ordered list, all drag-and-droppable together.
|
|
5
|
+
// Cells alternate freely: md (prose paragraph) / sql / js cells share the flow.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createState, injectStyles, mountRunControls, mountHistoryPanel,
|
|
10
|
+
setupDnD, deleteCellWithConfirm, restoreCellFromSnapshot, addCell,
|
|
11
|
+
addImportedCells, registerExecutor, collectDataServers,
|
|
12
|
+
autosize, openShareModal, registerHistoryObserver,
|
|
13
|
+
renderCellLogs,
|
|
14
|
+
createPublishControls, autoConnectFrontmatterServers,
|
|
15
|
+
createRuntimeOverlay, effectiveResult, cellRuntimeStatus,
|
|
16
|
+
lastRefreshedAt, bootstrapLiveRefresh, fmtRelTime,
|
|
17
|
+
type NotebookState, type NotebookCell, type CellResult, type CellExecContext,
|
|
18
|
+
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 { openAddMdModal, openAddRecipeModal } from './import-modals.js';
|
|
24
|
+
import { extractCellsFromRecipe, extractCellFromMarkdown } from './resource-extractor.js';
|
|
25
|
+
import { mountLeftPane } from './left-pane.js';
|
|
26
|
+
import { callToolViaPostMessage, MultiMcpBridge } from '@webmcp-auto-ui/core';
|
|
27
|
+
|
|
28
|
+
export async function render(container: HTMLElement, data: Record<string, unknown>): Promise<() => void> {
|
|
29
|
+
injectStyles();
|
|
30
|
+
injectLayoutStyles();
|
|
31
|
+
|
|
32
|
+
const state: NotebookState = createState({
|
|
33
|
+
id: data.id as string,
|
|
34
|
+
title: data.title as string ?? 'Untitled notebook',
|
|
35
|
+
mode: (data.mode as any) ?? 'edit',
|
|
36
|
+
cells: data.cells as any,
|
|
37
|
+
kicker: (data.kicker as string) ?? undefined,
|
|
38
|
+
autoRun: (data as any).autoRun === true,
|
|
39
|
+
});
|
|
40
|
+
if (!state.kicker) state.kicker = (data.kicker as string) ?? 'untitled';
|
|
41
|
+
|
|
42
|
+
// Live mode runtime overlay (created lazily). Never mutates state.
|
|
43
|
+
let overlay: RuntimeOverlay | null = null;
|
|
44
|
+
let liveCleanup: (() => void) | null = null;
|
|
45
|
+
|
|
46
|
+
// --- register executors -------------------------------------------------
|
|
47
|
+
registerExecutor(state, 'js', jsExecutor);
|
|
48
|
+
registerExecutor(state, 'sql', makeSqlExecutor(data));
|
|
49
|
+
|
|
50
|
+
container.classList.add('nb-root');
|
|
51
|
+
container.classList.toggle('nb-view-mode', state.mode === 'view');
|
|
52
|
+
|
|
53
|
+
container.innerHTML = `
|
|
54
|
+
<div class="nbe-outer">
|
|
55
|
+
<div class="nbe-leftpane-slot"></div>
|
|
56
|
+
<div class="nbe-shell">
|
|
57
|
+
<div class="nbe-kicker">
|
|
58
|
+
<input class="nbe-kicker-input" value="${escapeAttr(state.kicker || '')}" placeholder="kicker…">
|
|
59
|
+
<span class="nbe-live-toggle-slot"></span>
|
|
60
|
+
<div class="nb-mode-switch" style="margin-left:auto;">
|
|
61
|
+
<button class="nb-mode-edit nb-on">edit</button>
|
|
62
|
+
<button class="nb-mode-view">view</button>
|
|
63
|
+
</div>
|
|
64
|
+
<button class="nb-btn nbe-history-btn">⟲ history</button>
|
|
65
|
+
<span class="nbe-publish-badge-slot"></span>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="nbe-title-row">
|
|
68
|
+
<input class="nbe-title nb-ed-title" value="${escapeAttr(state.title)}">
|
|
69
|
+
<span class="nbe-live-badge-slot"></span>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="nbe-empty-state-slot"></div>
|
|
72
|
+
<div class="nb-history-panel nbe-history-panel"></div>
|
|
73
|
+
<div class="nbe-cells"></div>
|
|
74
|
+
<div class="nbe-footer">
|
|
75
|
+
<button class="nb-btn nb-add-cell" data-add="md">+ prose</button>
|
|
76
|
+
<button class="nb-btn nb-add-cell" data-add="sql">+ sql</button>
|
|
77
|
+
<button class="nb-btn nb-add-cell" data-add="js">+ chart</button>
|
|
78
|
+
<button class="nb-btn nb-add-cell" data-add-modal="md">+ md</button>
|
|
79
|
+
<button class="nb-btn nb-add-cell" data-add-modal="recipe">+ recipe</button>
|
|
80
|
+
<span class="nbe-share-btn" title="Share">share</span>
|
|
81
|
+
<span class="nbe-publish-slot"></span>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="nbe-publish-footer-slot"></div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>`;
|
|
86
|
+
|
|
87
|
+
const shell = container.querySelector('.nbe-shell') as HTMLElement;
|
|
88
|
+
const leftPaneHost = container.querySelector('.nbe-leftpane-slot') as HTMLElement;
|
|
89
|
+
const cellsEl = shell.querySelector('.nbe-cells') as HTMLElement;
|
|
90
|
+
const historyPanel = shell.querySelector('.nbe-history-panel') as HTMLElement;
|
|
91
|
+
|
|
92
|
+
let lastActiveIdx: number | null = null;
|
|
93
|
+
function activeCellIdx(): number | null {
|
|
94
|
+
if (lastActiveIdx != null && lastActiveIdx >= 0 && lastActiveIdx < state.cells.length) {
|
|
95
|
+
return lastActiveIdx;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function renderCells() {
|
|
101
|
+
cellsEl.innerHTML = '';
|
|
102
|
+
state.cells.forEach((cell, idx) => {
|
|
103
|
+
const node = renderCell(cell, state, overlay, rerender);
|
|
104
|
+
node.addEventListener('focusin', () => { lastActiveIdx = idx; });
|
|
105
|
+
cellsEl.appendChild(node);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function renderLiveToggle() {
|
|
110
|
+
const slot = shell.querySelector('.nbe-live-toggle-slot') as HTMLElement;
|
|
111
|
+
if (state.mode === 'edit') {
|
|
112
|
+
const checked = state.autoRun === true ? 'checked' : '';
|
|
113
|
+
slot.innerHTML = `<label class="nbe-live-toggle" title="Re-execute SQL cells against connected servers when this notebook is opened in view mode."><input type="checkbox" ${checked} />Live data</label>`;
|
|
114
|
+
const cb = slot.querySelector('input[type=checkbox]') as HTMLInputElement;
|
|
115
|
+
cb.addEventListener('change', () => {
|
|
116
|
+
state.autoRun = cb.checked;
|
|
117
|
+
rerender();
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
slot.innerHTML = '';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function renderLiveBadge() {
|
|
125
|
+
const slot = shell.querySelector('.nbe-live-badge-slot') as HTMLElement;
|
|
126
|
+
if (state.mode === 'view' && state.autoRun === true) {
|
|
127
|
+
const refreshedAt = lastRefreshedAt(overlay);
|
|
128
|
+
const refreshedTxt = refreshedAt
|
|
129
|
+
? `Refreshed ${escapeHtml(fmtRelTime(refreshedAt))} ago`
|
|
130
|
+
: (overlay?.startedAt && !overlay?.finishedAt ? 'Refreshing…' : '');
|
|
131
|
+
slot.innerHTML = `<span class="nb-live-badge">● Live</span>${refreshedTxt ? `<span class="nbe-refreshed-at">${refreshedTxt}</span>` : ''}`;
|
|
132
|
+
} else {
|
|
133
|
+
slot.innerHTML = '';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renderEmptyState() {
|
|
138
|
+
const slot = shell.querySelector('.nbe-empty-state-slot') as HTMLElement;
|
|
139
|
+
const showBanner = state.autoRun === true && state.mode === 'view' && overlay
|
|
140
|
+
&& (overlay.error || (overlay.finishedAt !== null && overlay.outputs.size === 0));
|
|
141
|
+
if (!showBanner) {
|
|
142
|
+
slot.innerHTML = '';
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const snapTs = state.lastEditAt ? fmtRelTime(state.lastEditAt) : '—';
|
|
146
|
+
slot.innerHTML = `
|
|
147
|
+
<div class="nb-empty-state">
|
|
148
|
+
<div class="nb-empty-icon">📡</div>
|
|
149
|
+
<div class="nb-empty-body">
|
|
150
|
+
<div class="nb-empty-title">Live mode active, but no data server is reachable.</div>
|
|
151
|
+
<div class="nb-empty-desc">Showing snapshots from <time>${escapeHtml(snapTs)} ago</time>.</div>
|
|
152
|
+
</div>
|
|
153
|
+
<button class="nb-btn nb-empty-retry">retry connection</button>
|
|
154
|
+
</div>
|
|
155
|
+
`;
|
|
156
|
+
(slot.querySelector('.nb-empty-retry') as HTMLElement).addEventListener('click', () => {
|
|
157
|
+
bootstrapLive();
|
|
158
|
+
rerender();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function bootstrapLive() {
|
|
163
|
+
liveCleanup?.();
|
|
164
|
+
liveCleanup = null;
|
|
165
|
+
overlay = createRuntimeOverlay();
|
|
166
|
+
liveCleanup = bootstrapLiveRefresh({
|
|
167
|
+
state,
|
|
168
|
+
data,
|
|
169
|
+
overlay,
|
|
170
|
+
MultiMcpBridgeCtor: MultiMcpBridge as any,
|
|
171
|
+
onCellChange: (cellId) => {
|
|
172
|
+
const node = cellsEl.querySelector(`[data-id="${cellId}"]`) as HTMLElement | null;
|
|
173
|
+
if (!node) { renderCells(); return; }
|
|
174
|
+
const idx = state.cells.findIndex((c) => c.id === cellId);
|
|
175
|
+
if (idx < 0) return;
|
|
176
|
+
const fresh = renderCell(state.cells[idx], state, overlay, rerender);
|
|
177
|
+
fresh.addEventListener('focusin', () => { lastActiveIdx = idx; });
|
|
178
|
+
node.replaceWith(fresh);
|
|
179
|
+
},
|
|
180
|
+
onTick: () => {
|
|
181
|
+
renderLiveBadge();
|
|
182
|
+
renderEmptyState();
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function rerender() {
|
|
188
|
+
mountHistoryPanel(historyPanel, state, (snap) => { restoreCellFromSnapshot(state, snap); rerender(); });
|
|
189
|
+
renderLiveToggle();
|
|
190
|
+
renderLiveBadge();
|
|
191
|
+
renderEmptyState();
|
|
192
|
+
renderCells();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Toolbar: direct add (prose/sql/js)
|
|
196
|
+
shell.querySelectorAll<HTMLElement>('[data-add]').forEach((btn) => {
|
|
197
|
+
btn.addEventListener('click', () => {
|
|
198
|
+
const type = btn.dataset.add as any;
|
|
199
|
+
addCell(state, type);
|
|
200
|
+
rerender();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Toolbar: modal add (md / recipe)
|
|
205
|
+
shell.querySelectorAll<HTMLElement>('[data-add-modal]').forEach((btn) => {
|
|
206
|
+
btn.addEventListener('click', () => {
|
|
207
|
+
const which = btn.dataset.addModal;
|
|
208
|
+
if (which === 'md') {
|
|
209
|
+
openAddMdModal((content) => {
|
|
210
|
+
const cell = extractCellFromMarkdown(content);
|
|
211
|
+
addImportedCells(state, [cell], activeCellIdx());
|
|
212
|
+
rerender();
|
|
213
|
+
});
|
|
214
|
+
} else if (which === 'recipe') {
|
|
215
|
+
const mcpServers = (Array.isArray((data as any)?.servers) ? (data as any).servers : [])
|
|
216
|
+
.map((s: any) => ({ name: String(s?.name ?? ''), url: s?.url ? String(s.url) : undefined }))
|
|
217
|
+
.filter((s: any) => s.name);
|
|
218
|
+
openAddRecipeModal({
|
|
219
|
+
mcpServers,
|
|
220
|
+
scope: 'data',
|
|
221
|
+
onPick: (recipe) => {
|
|
222
|
+
const cells = extractCellsFromRecipe(recipe.body ?? '', {
|
|
223
|
+
title: recipe.name, description: recipe.description,
|
|
224
|
+
});
|
|
225
|
+
addImportedCells(state, cells, activeCellIdx());
|
|
226
|
+
rerender();
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
(shell.querySelector('.nbe-history-btn') as HTMLElement).addEventListener('click', () => {
|
|
234
|
+
historyPanel.classList.toggle('nb-open');
|
|
235
|
+
});
|
|
236
|
+
(shell.querySelector('.nbe-share-btn') as HTMLElement).addEventListener('click', () => {
|
|
237
|
+
openShareModal(state, (fmt) => {
|
|
238
|
+
dispatchShare(fmt, state, {
|
|
239
|
+
container,
|
|
240
|
+
onResult: (info) => toast(container, formatShareToast(info)),
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
const publishCleanup = createPublishControls(state, {
|
|
245
|
+
buttonSlot: shell.querySelector('.nbe-publish-slot') as HTMLElement,
|
|
246
|
+
badgeSlot: shell.querySelector('.nbe-publish-badge-slot') as HTMLElement,
|
|
247
|
+
footerSlot: shell.querySelector('.nbe-publish-footer-slot') as HTMLElement,
|
|
248
|
+
onPublished: () => rerender(),
|
|
249
|
+
});
|
|
250
|
+
(shell.querySelector('.nbe-kicker-input') as HTMLInputElement).addEventListener('input', (e) => {
|
|
251
|
+
state.kicker = (e.target as HTMLInputElement).value;
|
|
252
|
+
state.lastEditAt = Date.now();
|
|
253
|
+
});
|
|
254
|
+
(shell.querySelector('.nbe-title') as HTMLInputElement).addEventListener('input', (e) => {
|
|
255
|
+
state.title = (e.target as HTMLInputElement).value;
|
|
256
|
+
state.lastEditAt = Date.now();
|
|
257
|
+
});
|
|
258
|
+
const editBtn = shell.querySelector('.nb-mode-edit') as HTMLElement;
|
|
259
|
+
const viewBtn = shell.querySelector('.nb-mode-view') as HTMLElement;
|
|
260
|
+
editBtn.addEventListener('click', () => {
|
|
261
|
+
state.mode = 'edit';
|
|
262
|
+
container.classList.remove('nb-view-mode');
|
|
263
|
+
editBtn.classList.add('nb-on'); viewBtn.classList.remove('nb-on');
|
|
264
|
+
// Leaving view: stop live refresh and clear overlay so frozen snapshots show.
|
|
265
|
+
liveCleanup?.(); liveCleanup = null; overlay = null;
|
|
266
|
+
rerender();
|
|
267
|
+
});
|
|
268
|
+
viewBtn.addEventListener('click', () => {
|
|
269
|
+
state.mode = 'view';
|
|
270
|
+
container.classList.add('nb-view-mode');
|
|
271
|
+
viewBtn.classList.add('nb-on'); editBtn.classList.remove('nb-on');
|
|
272
|
+
if (state.autoRun === true) bootstrapLive();
|
|
273
|
+
rerender();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Left pane (collapsed by default)
|
|
277
|
+
const pane = mountLeftPane(leftPaneHost, state, collectDataServers(data), {
|
|
278
|
+
onInjectCells: (cells) => {
|
|
279
|
+
addImportedCells(state, cells, activeCellIdx());
|
|
280
|
+
rerender();
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Auto-connect data servers declared in the recipe frontmatter (data.servers)
|
|
285
|
+
autoConnectFrontmatterServers(data, () => pane.setServers(collectDataServers(data)));
|
|
286
|
+
|
|
287
|
+
// Keep pane servers in sync with canvas changes
|
|
288
|
+
let canvasUnsub: (() => void) | null = null;
|
|
289
|
+
try {
|
|
290
|
+
const canvasAny: any = (globalThis as any).__canvasVanilla || (globalThis as any).canvasVanilla;
|
|
291
|
+
if (canvasAny?.subscribe) {
|
|
292
|
+
canvasUnsub = canvasAny.subscribe(() => pane.setServers(collectDataServers(data)));
|
|
293
|
+
}
|
|
294
|
+
} catch { /* ignore */ }
|
|
295
|
+
|
|
296
|
+
setupDnD(cellsEl, state, rerender);
|
|
297
|
+
const unsubHistory = registerHistoryObserver(() => mountHistoryPanel(historyPanel, state, (snap) => { restoreCellFromSnapshot(state, snap); rerender(); }));
|
|
298
|
+
|
|
299
|
+
rerender();
|
|
300
|
+
|
|
301
|
+
// Mount-time bootstrap: view + autoRun → start live refresh.
|
|
302
|
+
if (state.autoRun === true && state.mode === 'view') {
|
|
303
|
+
bootstrapLive();
|
|
304
|
+
rerender();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return () => {
|
|
308
|
+
unsubHistory();
|
|
309
|
+
canvasUnsub?.();
|
|
310
|
+
pane.destroy();
|
|
311
|
+
publishCleanup();
|
|
312
|
+
liveCleanup?.();
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Executors (same pattern as compact/workspace/document agents)
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
async function jsExecutor(ctx: CellExecContext): Promise<CellResult> {
|
|
321
|
+
const start = Date.now();
|
|
322
|
+
const { cell, scope } = ctx;
|
|
323
|
+
try {
|
|
324
|
+
const keys = Object.keys(scope);
|
|
325
|
+
const values = keys.map((k) => scope[k]);
|
|
326
|
+
const src = cell.content.trim();
|
|
327
|
+
const body = /^\s*(return|var|let|const|function|class|if|for|while|\/\/|\/\*)/.test(src)
|
|
328
|
+
? src
|
|
329
|
+
: `return (async () => { return (${src}); })();`;
|
|
330
|
+
// eslint-disable-next-line no-new-func
|
|
331
|
+
const fn = new Function(...keys, body);
|
|
332
|
+
let result = fn(...values);
|
|
333
|
+
if (result && typeof result.then === 'function') result = await result;
|
|
334
|
+
const durationMs = Date.now() - start;
|
|
335
|
+
|
|
336
|
+
if (result === undefined || result === null) return { ok: true, kind: 'empty', durationMs };
|
|
337
|
+
if (Array.isArray(result)) {
|
|
338
|
+
const rows = result.filter((r) => r && typeof r === 'object') as Record<string, unknown>[];
|
|
339
|
+
const columns = rows.length ? Array.from(new Set(rows.flatMap((r) => Object.keys(r)))) : [];
|
|
340
|
+
return { ok: true, kind: 'table', rows, columns, rowCount: rows.length, durationMs };
|
|
341
|
+
}
|
|
342
|
+
if (result && typeof result === 'object') {
|
|
343
|
+
const r: any = result;
|
|
344
|
+
if (r.data || r.marks || r.mark || r.$schema) {
|
|
345
|
+
return { ok: true, kind: 'chart', spec: result, durationMs };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return { ok: true, kind: 'value', value: result, durationMs };
|
|
349
|
+
} catch (err: any) {
|
|
350
|
+
return { ok: false, error: String(err?.message ?? err), errorKind: 'runtime', durationMs: Date.now() - start };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function makeSqlExecutor(data: Record<string, unknown>) {
|
|
355
|
+
return async function sqlExecutor(ctx: CellExecContext): Promise<CellResult> {
|
|
356
|
+
const start = Date.now();
|
|
357
|
+
const sql = ctx.cell.content;
|
|
358
|
+
const servers = collectDataServers(data);
|
|
359
|
+
const candidates: string[] = [];
|
|
360
|
+
for (const srv of servers) {
|
|
361
|
+
for (const t of srv.tools ?? []) candidates.push(t.name);
|
|
362
|
+
}
|
|
363
|
+
const precise = candidates.find((n) => /^.*query_sql$/i.test(n));
|
|
364
|
+
const loose = precise ?? candidates.find((n) => /^(query|run|execute)(_sql)?$/i.test(n));
|
|
365
|
+
const toolName = precise ?? loose;
|
|
366
|
+
if (!toolName) {
|
|
367
|
+
return {
|
|
368
|
+
ok: false,
|
|
369
|
+
error: 'No SQL tool available on connected servers (looked for *query_sql or query/run/execute).',
|
|
370
|
+
errorKind: 'schema',
|
|
371
|
+
durationMs: Date.now() - start,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
const res: any = await callToolViaPostMessage(toolName, { sql });
|
|
376
|
+
const text = res?.content?.find?.((c: any) => c.type === 'text')?.text ?? '';
|
|
377
|
+
const durationMs = Date.now() - start;
|
|
378
|
+
let parsed: any = null;
|
|
379
|
+
try { parsed = JSON.parse(text); } catch { /* not JSON */ }
|
|
380
|
+
if (parsed) {
|
|
381
|
+
const rows: any[] = Array.isArray(parsed) ? parsed
|
|
382
|
+
: Array.isArray(parsed?.rows) ? parsed.rows
|
|
383
|
+
: Array.isArray(parsed?.data) ? parsed.data
|
|
384
|
+
: Array.isArray(parsed?.results) ? parsed.results
|
|
385
|
+
: [];
|
|
386
|
+
if (rows.length && rows.every((r) => r && typeof r === 'object')) {
|
|
387
|
+
const columns = Array.isArray(parsed?.columns)
|
|
388
|
+
? parsed.columns.map(String)
|
|
389
|
+
: Array.from(new Set(rows.flatMap((r) => Object.keys(r))));
|
|
390
|
+
return { ok: true, kind: 'table', rows, columns, rowCount: rows.length, durationMs };
|
|
391
|
+
}
|
|
392
|
+
return { ok: true, kind: 'value', value: parsed, durationMs };
|
|
393
|
+
}
|
|
394
|
+
if (!text) return { ok: true, kind: 'empty', durationMs };
|
|
395
|
+
return { ok: true, kind: 'value', value: text, durationMs };
|
|
396
|
+
} catch (err: any) {
|
|
397
|
+
return { ok: false, error: String(err?.message ?? err), errorKind: 'runtime', durationMs: Date.now() - start };
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
// Cell rendering — prose + code share the unified flow, same DnD handle
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
function renderCell(cell: NotebookCell, state: NotebookState, overlay: RuntimeOverlay | null, rerender: () => void): HTMLElement {
|
|
407
|
+
const wrap = document.createElement('div');
|
|
408
|
+
wrap.className = 'nb-cell-wrapper nbe-cell';
|
|
409
|
+
wrap.dataset.id = cell.id;
|
|
410
|
+
|
|
411
|
+
const handle = document.createElement('span');
|
|
412
|
+
handle.className = 'nb-drag-handle nbe-handle';
|
|
413
|
+
handle.draggable = true;
|
|
414
|
+
handle.textContent = '⋮⋮';
|
|
415
|
+
wrap.appendChild(handle);
|
|
416
|
+
|
|
417
|
+
const del = document.createElement('button');
|
|
418
|
+
del.className = 'nb-icon-btn nb-danger nbe-del-abs';
|
|
419
|
+
del.textContent = '✕';
|
|
420
|
+
del.addEventListener('click', () =>
|
|
421
|
+
deleteCellWithConfirm(state, cell, (c) => c.type === 'md' ? 'prose paragraph' : `${c.type} cell`, rerender)
|
|
422
|
+
);
|
|
423
|
+
wrap.appendChild(del);
|
|
424
|
+
|
|
425
|
+
if (cell.type === 'md') {
|
|
426
|
+
if (state.mode === 'view') {
|
|
427
|
+
const rendered = document.createElement('div');
|
|
428
|
+
rendered.className = 'nbe-prose nbe-prose-render';
|
|
429
|
+
rendered.innerHTML = renderProse(cell.content || '');
|
|
430
|
+
wrap.appendChild(rendered);
|
|
431
|
+
} else {
|
|
432
|
+
// Editor: plain textarea (markdown source) + rendered preview below
|
|
433
|
+
const editor = document.createElement('textarea');
|
|
434
|
+
editor.className = 'nbe-prose-edit';
|
|
435
|
+
editor.value = cell.content || '';
|
|
436
|
+
editor.rows = 2;
|
|
437
|
+
editor.placeholder = 'write prose (markdown)…';
|
|
438
|
+
editor.spellcheck = true;
|
|
439
|
+
editor.addEventListener('input', () => {
|
|
440
|
+
cell.content = editor.value;
|
|
441
|
+
autosize(editor);
|
|
442
|
+
preview.innerHTML = renderProse(cell.content || '');
|
|
443
|
+
});
|
|
444
|
+
const preview = document.createElement('div');
|
|
445
|
+
preview.className = 'nbe-prose nbe-prose-render';
|
|
446
|
+
preview.innerHTML = renderProse(cell.content || '');
|
|
447
|
+
wrap.appendChild(editor);
|
|
448
|
+
wrap.appendChild(preview);
|
|
449
|
+
requestAnimationFrame(() => requestAnimationFrame(() => autosize(editor)));
|
|
450
|
+
}
|
|
451
|
+
return wrap;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Code cell: header with run controls FIRST, then code body, then optional output.
|
|
455
|
+
const codeCell = document.createElement('div');
|
|
456
|
+
codeCell.className = 'nb-code-cell nbe-code-cell';
|
|
457
|
+
|
|
458
|
+
const head = document.createElement('div');
|
|
459
|
+
head.className = 'nbe-cell-head';
|
|
460
|
+
const rtStatus = cellRuntimeStatus(cell, overlay);
|
|
461
|
+
const showLive = state.autoRun === true && state.mode === 'view';
|
|
462
|
+
let liveBadge = '';
|
|
463
|
+
if (showLive) {
|
|
464
|
+
if (rtStatus === 'running') {
|
|
465
|
+
liveBadge = `<span class="nbe-cell-badge nbe-cell-running" title="re-executing"><span class="nbe-spinner"></span>running</span>`;
|
|
466
|
+
} else if (rtStatus === 'stale') {
|
|
467
|
+
liveBadge = `<span class="nbe-cell-badge nbe-cell-stale" title="last live refresh failed">stale</span>`;
|
|
468
|
+
} else if (rtStatus === 'frozen') {
|
|
469
|
+
liveBadge = `<span class="nbe-cell-badge nbe-cell-frozen" title="JS cells are not re-executed in live mode">frozen</span>`;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
head.innerHTML = `
|
|
473
|
+
<span class="nbe-run-controls"></span>
|
|
474
|
+
<span class="nbe-type-${cell.type}">${cell.type}</span>
|
|
475
|
+
<span class="nbe-meta-info">${escapeHtml(metaInfoFor(cell, overlay))}</span>
|
|
476
|
+
${liveBadge}
|
|
477
|
+
<div class="nbe-actions">
|
|
478
|
+
<button class="nb-icon-btn nb-toggle-src">${cell.hideSource ? '▸ src' : '◂ src'}</button>
|
|
479
|
+
<button class="nb-icon-btn nb-toggle-res">${cell.hideResult ? '▸ res' : '◂ res'}</button>
|
|
480
|
+
</div>`;
|
|
481
|
+
codeCell.appendChild(head);
|
|
482
|
+
mountRunControls(head.querySelector('.nbe-run-controls') as HTMLElement, cell, wrap, state, rerender);
|
|
483
|
+
|
|
484
|
+
const body = document.createElement('div');
|
|
485
|
+
body.className = 'nbe-code-body' + (cell.hideSource ? ' nbe-hidden' : '');
|
|
486
|
+
const ta = document.createElement('textarea');
|
|
487
|
+
ta.className = 'nb-code-edit';
|
|
488
|
+
ta.value = cell.content;
|
|
489
|
+
ta.rows = 1;
|
|
490
|
+
ta.spellcheck = false;
|
|
491
|
+
ta.addEventListener('input', () => { cell.content = ta.value; autosize(ta); cell.status = 'stale'; });
|
|
492
|
+
body.appendChild(ta);
|
|
493
|
+
codeCell.appendChild(body);
|
|
494
|
+
requestAnimationFrame(() => requestAnimationFrame(() => autosize(ta)));
|
|
495
|
+
|
|
496
|
+
if (!cell.hideResult) {
|
|
497
|
+
const res = document.createElement('div');
|
|
498
|
+
res.className = 'nbe-result';
|
|
499
|
+
renderResultInto(res, cell, overlay);
|
|
500
|
+
codeCell.appendChild(res);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
wrap.appendChild(codeCell);
|
|
504
|
+
|
|
505
|
+
(head.querySelector('.nb-toggle-src') as HTMLElement).addEventListener('click', () => { cell.hideSource = !cell.hideSource; rerender(); });
|
|
506
|
+
(head.querySelector('.nb-toggle-res') as HTMLElement).addEventListener('click', () => { cell.hideResult = !cell.hideResult; rerender(); });
|
|
507
|
+
|
|
508
|
+
return wrap;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ---------------------------------------------------------------------------
|
|
512
|
+
// Result rendering — editorial flavour (serif headers, mono cells, discreet)
|
|
513
|
+
// ---------------------------------------------------------------------------
|
|
514
|
+
|
|
515
|
+
function metaInfoFor(cell: NotebookCell, overlay: RuntimeOverlay | null): string {
|
|
516
|
+
const r = effectiveResult(cell, overlay) ?? cell.lastResult;
|
|
517
|
+
if (!r) {
|
|
518
|
+
if (cell.lastMs != null) return formatMs(cell.lastMs);
|
|
519
|
+
return cell.status === 'stale' ? 'stale' : '';
|
|
520
|
+
}
|
|
521
|
+
const parts: string[] = [];
|
|
522
|
+
if (!r.ok) parts.push('error');
|
|
523
|
+
else if (r.kind === 'table') parts.push(`${r.rowCount} row${r.rowCount === 1 ? '' : 's'}`);
|
|
524
|
+
else if (r.kind === 'value') parts.push(typeof r.value === 'object' && r.value !== null ? 'object' : typeof r.value);
|
|
525
|
+
else if (r.kind === 'chart') parts.push('chart');
|
|
526
|
+
else parts.push('empty');
|
|
527
|
+
if (r.durationMs != null) parts.push(formatMs(r.durationMs));
|
|
528
|
+
return parts.join(' · ');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function formatMs(ms: number): string {
|
|
532
|
+
if (ms < 1000) return ms + 'ms';
|
|
533
|
+
return (ms / 1000).toFixed(2) + 's';
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function renderResultInto(el: HTMLElement, cell: NotebookCell, overlay: RuntimeOverlay | null): void {
|
|
537
|
+
const r = effectiveResult(cell, overlay) ?? cell.lastResult;
|
|
538
|
+
el.innerHTML = '';
|
|
539
|
+
if (!r) {
|
|
540
|
+
el.innerHTML = `<div class="nbe-result-empty">press ▶ to run</div>`;
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
// Logs panel (shared across all widgets), prepended above the main result
|
|
544
|
+
const logsEl = renderCellLogs(r);
|
|
545
|
+
if (logsEl) el.appendChild(logsEl);
|
|
546
|
+
if (!r.ok) {
|
|
547
|
+
const err = document.createElement('div');
|
|
548
|
+
err.className = 'nbe-result-error';
|
|
549
|
+
err.textContent = r.error || 'error';
|
|
550
|
+
el.appendChild(err);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (r.kind === 'empty') {
|
|
554
|
+
const empty = document.createElement('div');
|
|
555
|
+
empty.className = 'nbe-result-empty';
|
|
556
|
+
empty.textContent = '(no output)';
|
|
557
|
+
el.appendChild(empty);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (r.kind === 'value') {
|
|
561
|
+
const pre = document.createElement('pre');
|
|
562
|
+
pre.className = 'nbe-result-pre';
|
|
563
|
+
pre.textContent = safeJson(r.value);
|
|
564
|
+
el.appendChild(pre);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (r.kind === 'chart') {
|
|
568
|
+
const chart = document.createElement('div');
|
|
569
|
+
chart.className = 'nb-chart';
|
|
570
|
+
el.appendChild(chart);
|
|
571
|
+
renderChart(chart, r.spec).catch(() => { /* fallback handled internally */ });
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
// table — editorial style: serif header row, mono cells, minimal chrome.
|
|
575
|
+
const cols = r.columns && r.columns.length ? r.columns
|
|
576
|
+
: (r.rows[0] ? Object.keys(r.rows[0]) : []);
|
|
577
|
+
const maxRows = 40;
|
|
578
|
+
const shown = r.rows.slice(0, maxRows);
|
|
579
|
+
const thead = `<tr>${cols.map((c) => `<th>${escapeHtml(String(c))}</th>`).join('')}</tr>`;
|
|
580
|
+
const tbody = shown.map((row) => {
|
|
581
|
+
return `<tr>${cols.map((c) => {
|
|
582
|
+
const v = (row as any)[c];
|
|
583
|
+
const cellStr = v == null ? '' : typeof v === 'object' ? safeJson(v) : String(v);
|
|
584
|
+
return `<td>${escapeHtml(cellStr)}</td>`;
|
|
585
|
+
}).join('')}</tr>`;
|
|
586
|
+
}).join('');
|
|
587
|
+
const trunc = r.rows.length > maxRows
|
|
588
|
+
? `<div class="nbe-result-trunc">showing ${maxRows} of ${r.rowCount}</div>`
|
|
589
|
+
: '';
|
|
590
|
+
// appendChild so we don't overwrite the logs panel prepended above
|
|
591
|
+
const host = document.createElement('div');
|
|
592
|
+
host.innerHTML = `<div class="nbe-result-table-wrap"><table class="nbe-result-table"><thead>${thead}</thead><tbody>${tbody}</tbody></table></div>${trunc}`;
|
|
593
|
+
while (host.firstChild) el.appendChild(host.firstChild);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function safeJson(v: unknown): string {
|
|
597
|
+
try { return JSON.stringify(v, null, 2); } catch { return String(v); }
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
// Toast
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
|
|
604
|
+
function toast(container: HTMLElement, msg: string, isError = false): void {
|
|
605
|
+
const t = document.createElement('div');
|
|
606
|
+
t.className = 'nbe-toast' + (isError ? ' nbe-toast-error' : '');
|
|
607
|
+
t.textContent = msg;
|
|
608
|
+
container.appendChild(t);
|
|
609
|
+
requestAnimationFrame(() => t.classList.add('nbe-toast-in'));
|
|
610
|
+
setTimeout(() => {
|
|
611
|
+
t.classList.remove('nbe-toast-in');
|
|
612
|
+
setTimeout(() => t.remove(), 250);
|
|
613
|
+
}, 3200);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function formatShareToast(info: any): string {
|
|
617
|
+
if (!info) return 'share ready';
|
|
618
|
+
if (info.kind === 'hyperskill') {
|
|
619
|
+
const url = info.shortUrl || info.fullUrl || '';
|
|
620
|
+
return url ? `hyperskill link copied · ${url.slice(0, 48)}…` : 'hyperskill link ready';
|
|
621
|
+
}
|
|
622
|
+
if (info.kind === 'markdown' || info.kind === 'md') return 'markdown downloaded';
|
|
623
|
+
if (info.kind === 'json') return 'json downloaded';
|
|
624
|
+
if (info.kind === 'png') return 'png downloaded';
|
|
625
|
+
return 'shared';
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ---------------------------------------------------------------------------
|
|
629
|
+
// Helpers
|
|
630
|
+
// ---------------------------------------------------------------------------
|
|
631
|
+
|
|
632
|
+
function escapeHtml(s: string): string {
|
|
633
|
+
return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]!));
|
|
634
|
+
}
|
|
635
|
+
function escapeAttr(s: string): string {
|
|
636
|
+
return String(s ?? '').replace(/"/g, '"');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function injectLayoutStyles(): void {
|
|
640
|
+
if (document.getElementById('nbe-styles')) return;
|
|
641
|
+
const style = document.createElement('style');
|
|
642
|
+
style.id = 'nbe-styles';
|
|
643
|
+
style.textContent = `
|
|
644
|
+
.nbe-outer {
|
|
645
|
+
display: flex; align-items: flex-start; gap: 8px;
|
|
646
|
+
}
|
|
647
|
+
.nbe-leftpane-slot { flex-shrink: 0; }
|
|
648
|
+
.nbe-shell {
|
|
649
|
+
flex: 1; min-width: 0;
|
|
650
|
+
background: var(--color-surface);
|
|
651
|
+
border: 1px solid var(--color-border);
|
|
652
|
+
border-radius: 12px;
|
|
653
|
+
padding: 36px 44px;
|
|
654
|
+
}
|
|
655
|
+
.nbe-kicker {
|
|
656
|
+
display: flex; align-items: center; gap: 8px;
|
|
657
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
658
|
+
font-size: 11px; color: var(--color-text2);
|
|
659
|
+
letter-spacing: 0.1em; text-transform: uppercase;
|
|
660
|
+
margin-bottom: 14px;
|
|
661
|
+
}
|
|
662
|
+
.nbe-kicker-input {
|
|
663
|
+
flex: 0 0 auto; min-width: 120px;
|
|
664
|
+
background: transparent; border: 1px dashed transparent;
|
|
665
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
666
|
+
font-size: 11px; color: var(--color-text2);
|
|
667
|
+
letter-spacing: 0.1em; text-transform: uppercase;
|
|
668
|
+
padding: 2px 4px; border-radius: 3px; outline: none;
|
|
669
|
+
}
|
|
670
|
+
.nbe-kicker-input:focus { border-color: var(--color-border); background: var(--color-bg); color: var(--color-text1); }
|
|
671
|
+
.nb-root.nb-view-mode .nbe-kicker-input { pointer-events: none; }
|
|
672
|
+
.nbe-title {
|
|
673
|
+
font-family: 'EB Garamond', Georgia, serif;
|
|
674
|
+
font-size: 30px; font-weight: 500;
|
|
675
|
+
margin: 0 0 12px;
|
|
676
|
+
letter-spacing: -0.01em; line-height: 1.2;
|
|
677
|
+
background: transparent; border: none; outline: none;
|
|
678
|
+
color: var(--color-text1);
|
|
679
|
+
width: 100%; padding: 2px 4px; border-radius: 3px;
|
|
680
|
+
}
|
|
681
|
+
.nbe-title:focus { background: var(--color-bg); }
|
|
682
|
+
.nbe-history-panel { margin: 0 0 14px; }
|
|
683
|
+
|
|
684
|
+
.nbe-cells { display: flex; flex-direction: column; gap: 14px; }
|
|
685
|
+
.nbe-cell { position: relative; padding-left: 28px; }
|
|
686
|
+
.nbe-handle { position: absolute; left: 0; top: 6px; }
|
|
687
|
+
.nbe-del-abs {
|
|
688
|
+
position: absolute; top: 4px; right: 4px;
|
|
689
|
+
opacity: 0; transition: opacity 0.15s;
|
|
690
|
+
}
|
|
691
|
+
.nbe-cell:hover .nbe-del-abs { opacity: 0.5; }
|
|
692
|
+
.nbe-del-abs:hover { opacity: 1 !important; }
|
|
693
|
+
|
|
694
|
+
.nbe-prose {
|
|
695
|
+
font-family: 'EB Garamond', Georgia, serif;
|
|
696
|
+
font-size: 17px; line-height: 1.7;
|
|
697
|
+
color: var(--color-text1);
|
|
698
|
+
max-width: 620px;
|
|
699
|
+
padding: 2px 4px;
|
|
700
|
+
border-radius: 3px;
|
|
701
|
+
}
|
|
702
|
+
.nbe-prose-render h1, .nbe-prose-render h2, .nbe-prose-render h3,
|
|
703
|
+
.nbe-prose-render h4, .nbe-prose-render h5, .nbe-prose-render h6 {
|
|
704
|
+
font-family: 'EB Garamond', Georgia, serif;
|
|
705
|
+
font-weight: 600; letter-spacing: -0.01em;
|
|
706
|
+
margin: 0.6em 0 0.3em;
|
|
707
|
+
}
|
|
708
|
+
.nbe-prose-render h1 { font-size: 1.4em; }
|
|
709
|
+
.nbe-prose-render h2 { font-size: 1.25em; }
|
|
710
|
+
.nbe-prose-render h3 { font-size: 1.12em; }
|
|
711
|
+
.nbe-prose-render p { margin: 0.5em 0; }
|
|
712
|
+
.nbe-prose-render ul, .nbe-prose-render ol { margin: 0.5em 0; padding-left: 1.4em; }
|
|
713
|
+
.nbe-prose-render blockquote {
|
|
714
|
+
border-left: 3px solid var(--color-border);
|
|
715
|
+
padding-left: 12px; margin: 0.6em 0;
|
|
716
|
+
color: var(--color-text2); font-style: italic;
|
|
717
|
+
}
|
|
718
|
+
.nbe-prose-render code {
|
|
719
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
720
|
+
font-size: 0.82em;
|
|
721
|
+
background: var(--color-surface2);
|
|
722
|
+
padding: 1px 6px; border-radius: 3px;
|
|
723
|
+
color: var(--color-accent);
|
|
724
|
+
}
|
|
725
|
+
.nbe-prose-render mark {
|
|
726
|
+
background: rgba(240,160,80,0.18);
|
|
727
|
+
color: var(--color-amber);
|
|
728
|
+
padding: 0 4px; border-radius: 2px;
|
|
729
|
+
}
|
|
730
|
+
.nbe-prose-edit {
|
|
731
|
+
display: block; width: 100%; max-width: 620px;
|
|
732
|
+
background: var(--color-bg);
|
|
733
|
+
border: 1px dashed var(--color-border); border-radius: 4px;
|
|
734
|
+
padding: 8px 10px; margin-bottom: 6px;
|
|
735
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
736
|
+
font-size: 12.5px; line-height: 1.6;
|
|
737
|
+
color: var(--color-text1);
|
|
738
|
+
outline: none; resize: none; overflow: hidden;
|
|
739
|
+
}
|
|
740
|
+
.nbe-prose-edit:focus { border-color: var(--color-border2); border-style: solid; }
|
|
741
|
+
|
|
742
|
+
.nbe-code-cell {
|
|
743
|
+
background: var(--color-surface2);
|
|
744
|
+
border: 1px solid var(--color-border);
|
|
745
|
+
border-radius: 8px;
|
|
746
|
+
overflow: hidden;
|
|
747
|
+
}
|
|
748
|
+
.nbe-cell-head {
|
|
749
|
+
display: flex; align-items: center; gap: 8px;
|
|
750
|
+
padding: 7px 12px;
|
|
751
|
+
border-bottom: 1px solid var(--color-border);
|
|
752
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
753
|
+
font-size: 10px; color: var(--color-text2);
|
|
754
|
+
letter-spacing: 0.06em;
|
|
755
|
+
}
|
|
756
|
+
.nbe-type-sql { color: var(--color-accent); text-transform: uppercase; letter-spacing: 0.08em; }
|
|
757
|
+
.nbe-type-js { color: var(--color-teal); text-transform: uppercase; letter-spacing: 0.08em; }
|
|
758
|
+
.nbe-meta-info { margin-right: auto; color: var(--color-text2); }
|
|
759
|
+
.nbe-actions { display: flex; gap: 4px; }
|
|
760
|
+
.nbe-code-body { padding: 14px 16px; }
|
|
761
|
+
.nbe-hidden { display: none !important; }
|
|
762
|
+
|
|
763
|
+
.nbe-result {
|
|
764
|
+
background: var(--color-bg);
|
|
765
|
+
border-top: 1px solid var(--color-border);
|
|
766
|
+
padding: 12px 16px;
|
|
767
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
768
|
+
font-size: 12px; color: var(--color-text1);
|
|
769
|
+
}
|
|
770
|
+
.nbe-result-empty {
|
|
771
|
+
color: var(--color-text2); font-style: italic; font-size: 11.5px;
|
|
772
|
+
}
|
|
773
|
+
.nbe-result-error {
|
|
774
|
+
color: var(--color-accent2); white-space: pre-wrap; font-size: 12px;
|
|
775
|
+
}
|
|
776
|
+
.nbe-result-label {
|
|
777
|
+
font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
|
|
778
|
+
color: var(--color-text2); margin-bottom: 6px;
|
|
779
|
+
}
|
|
780
|
+
.nbe-result-pre {
|
|
781
|
+
margin: 0; padding: 8px 10px;
|
|
782
|
+
background: var(--color-surface2); border-radius: 4px;
|
|
783
|
+
font-size: 11.5px; overflow: auto; max-height: 260px;
|
|
784
|
+
color: var(--color-text1);
|
|
785
|
+
}
|
|
786
|
+
.nbe-result-table-wrap { overflow: auto; max-height: 320px; }
|
|
787
|
+
.nbe-result-table {
|
|
788
|
+
width: 100%; border-collapse: collapse;
|
|
789
|
+
font-variant-numeric: tabular-nums;
|
|
790
|
+
}
|
|
791
|
+
.nbe-result-table thead th {
|
|
792
|
+
font-family: 'EB Garamond', Georgia, serif;
|
|
793
|
+
font-size: 12.5px; font-weight: 600;
|
|
794
|
+
letter-spacing: 0.02em;
|
|
795
|
+
color: var(--color-text2);
|
|
796
|
+
text-align: left;
|
|
797
|
+
padding: 6px 10px;
|
|
798
|
+
border-bottom: 1px solid var(--color-border);
|
|
799
|
+
background: transparent;
|
|
800
|
+
position: sticky; top: 0;
|
|
801
|
+
}
|
|
802
|
+
.nbe-result-table tbody td {
|
|
803
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
804
|
+
font-size: 11.5px;
|
|
805
|
+
color: var(--color-text1);
|
|
806
|
+
padding: 4px 10px;
|
|
807
|
+
border-bottom: 1px solid var(--color-border);
|
|
808
|
+
white-space: nowrap; max-width: 320px; overflow: hidden; text-overflow: ellipsis;
|
|
809
|
+
}
|
|
810
|
+
.nbe-result-table tbody tr:last-child td { border-bottom: none; }
|
|
811
|
+
.nbe-result-trunc {
|
|
812
|
+
margin-top: 6px; padding: 4px 2px;
|
|
813
|
+
color: var(--color-text2); font-size: 10.5px; font-style: italic;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
.nbe-footer {
|
|
817
|
+
display: flex; gap: 8px; flex-wrap: wrap;
|
|
818
|
+
padding-top: 16px; margin-top: 24px;
|
|
819
|
+
border-top: 1px solid var(--color-border);
|
|
820
|
+
align-items: center;
|
|
821
|
+
}
|
|
822
|
+
.nbe-share-btn {
|
|
823
|
+
margin-left: auto;
|
|
824
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
825
|
+
font-size: 11px; color: var(--color-text2);
|
|
826
|
+
cursor: pointer;
|
|
827
|
+
padding: 5px 10px;
|
|
828
|
+
}
|
|
829
|
+
.nbe-share-btn:hover { color: var(--color-accent); }
|
|
830
|
+
.nbe-publish-btn {
|
|
831
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
832
|
+
font-size: 11px;
|
|
833
|
+
}
|
|
834
|
+
.nbe-publish-btn[data-state="published"] { color: var(--color-accent); }
|
|
835
|
+
|
|
836
|
+
/* Toast */
|
|
837
|
+
.nbe-toast {
|
|
838
|
+
position: fixed; bottom: 24px; left: 50%;
|
|
839
|
+
transform: translateX(-50%) translateY(8px);
|
|
840
|
+
background: var(--color-surface2); color: var(--color-text1);
|
|
841
|
+
border: 1px solid var(--color-border); border-radius: 8px;
|
|
842
|
+
padding: 8px 14px;
|
|
843
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
844
|
+
font-size: 11.5px;
|
|
845
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.2);
|
|
846
|
+
opacity: 0; transition: opacity 0.25s, transform 0.25s;
|
|
847
|
+
z-index: 1005; pointer-events: none;
|
|
848
|
+
max-width: 480px;
|
|
849
|
+
}
|
|
850
|
+
.nbe-toast.nbe-toast-in { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
851
|
+
.nbe-toast.nbe-toast-error { color: var(--color-accent2); border-color: var(--color-accent2); }
|
|
852
|
+
|
|
853
|
+
/* Live mode — discreet toggle in header (edit mode only) */
|
|
854
|
+
.nbe-live-toggle {
|
|
855
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
856
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
857
|
+
font-size: 10.5px; color: var(--color-text2);
|
|
858
|
+
letter-spacing: 0.06em; text-transform: uppercase;
|
|
859
|
+
cursor: pointer; user-select: none;
|
|
860
|
+
padding: 2px 7px; border: 1px solid var(--color-border); border-radius: 4px;
|
|
861
|
+
}
|
|
862
|
+
.nbe-live-toggle:hover { color: var(--color-text1); border-color: var(--color-border2); }
|
|
863
|
+
.nbe-live-toggle input { margin: 0; cursor: pointer; }
|
|
864
|
+
|
|
865
|
+
/* Title row + Live badge (view mode + autoRun) */
|
|
866
|
+
.nbe-title-row { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
|
|
867
|
+
.nbe-title-row .nbe-title { flex: 1; min-width: 0; }
|
|
868
|
+
.nbe-live-badge-slot { display: inline-flex; align-items: center; gap: 8px; }
|
|
869
|
+
.nbe-refreshed-at {
|
|
870
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
871
|
+
font-size: 10.5px; color: var(--color-text2);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/* Shared "● Live" pill */
|
|
875
|
+
.nb-live-badge {
|
|
876
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
877
|
+
padding: 2px 7px; border-radius: 999px;
|
|
878
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
879
|
+
font-size: 10px; font-weight: 500; letter-spacing: 0.04em;
|
|
880
|
+
background: rgba(46, 160, 67, 0.12);
|
|
881
|
+
color: #2ea043; border: 1px solid rgba(46, 160, 67, 0.35);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/* Per-cell live badges (in cell head) */
|
|
885
|
+
.nbe-cell-badge {
|
|
886
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
887
|
+
padding: 1px 6px; border-radius: 999px;
|
|
888
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
889
|
+
font-size: 9.5px; letter-spacing: 0.04em;
|
|
890
|
+
border: 1px solid transparent;
|
|
891
|
+
margin-left: 4px;
|
|
892
|
+
}
|
|
893
|
+
.nbe-cell-running {
|
|
894
|
+
background: rgba(46, 160, 67, 0.10); color: #2ea043;
|
|
895
|
+
border-color: rgba(46, 160, 67, 0.30);
|
|
896
|
+
}
|
|
897
|
+
.nbe-cell-stale {
|
|
898
|
+
background: rgba(210, 153, 34, 0.12); color: var(--color-amber, #d29922);
|
|
899
|
+
border-color: rgba(210, 153, 34, 0.35);
|
|
900
|
+
}
|
|
901
|
+
.nbe-cell-frozen {
|
|
902
|
+
background: var(--color-surface2); color: var(--color-text2);
|
|
903
|
+
border-color: var(--color-border);
|
|
904
|
+
opacity: 0.75;
|
|
905
|
+
}
|
|
906
|
+
.nbe-spinner {
|
|
907
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
908
|
+
border: 1.5px solid currentColor; border-right-color: transparent;
|
|
909
|
+
display: inline-block; animation: nbe-spin 0.8s linear infinite;
|
|
910
|
+
}
|
|
911
|
+
@keyframes nbe-spin { to { transform: rotate(360deg); } }
|
|
912
|
+
|
|
913
|
+
/* Empty-state banner */
|
|
914
|
+
.nb-empty-state {
|
|
915
|
+
display: flex; align-items: center; gap: 12px;
|
|
916
|
+
padding: 12px 14px; margin: 12px 0 14px;
|
|
917
|
+
background: rgba(210, 153, 34, 0.08);
|
|
918
|
+
border: 1px solid rgba(210, 153, 34, 0.40);
|
|
919
|
+
border-radius: 8px;
|
|
920
|
+
color: var(--color-amber, #d29922);
|
|
921
|
+
}
|
|
922
|
+
.nb-empty-icon { font-size: 22px; line-height: 1; }
|
|
923
|
+
.nb-empty-body { flex: 1; min-width: 0; }
|
|
924
|
+
.nb-empty-title {
|
|
925
|
+
font-family: 'EB Garamond', Georgia, serif;
|
|
926
|
+
font-weight: 600; font-size: 14px; color: var(--color-text1);
|
|
927
|
+
}
|
|
928
|
+
.nb-empty-desc {
|
|
929
|
+
margin-top: 2px;
|
|
930
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
931
|
+
font-size: 11px; color: var(--color-text2);
|
|
932
|
+
}
|
|
933
|
+
.nb-empty-retry { white-space: nowrap; }
|
|
934
|
+
`;
|
|
935
|
+
document.head.appendChild(style);
|
|
936
|
+
}
|