@webmcp-auto-ui/agent 2.5.27 → 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/package.json +10 -2
- package/src/autoui-server.ts +63 -75
- package/src/index.ts +7 -2
- package/src/loop.ts +48 -21
- package/src/providers/factory.ts +15 -1
- package/src/providers/hawk-models.ts +22 -0
- package/src/providers/hawk.ts +181 -0
- package/src/providers/transformers.worker.ts +5 -32
- package/src/recipes/_generated.ts +81 -17
- package/src/recipes/notebook-playbook.md +81 -17
- package/src/server/hawkProxy.ts +54 -0
- package/src/server/index.ts +2 -0
- package/src/util/opfs-cache.ts +101 -2
- package/src/util/storage-inventory.ts +195 -0
- package/src/notebook-widgets/compact.ts +0 -312
- package/src/notebook-widgets/document.ts +0 -372
- package/src/notebook-widgets/editorial.ts +0 -348
- package/src/notebook-widgets/recipes/compact.md +0 -104
- package/src/notebook-widgets/recipes/document.md +0 -100
- package/src/notebook-widgets/recipes/editorial.md +0 -104
- package/src/notebook-widgets/recipes/workspace.md +0 -94
- package/src/notebook-widgets/shared.ts +0 -1064
- package/src/notebook-widgets/workspace.ts +0 -328
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* storage-inventory — enumerate OPFS (outside webmcp-models), Cache Storage
|
|
3
|
+
* API and IndexedDB entries. Best-effort: any per-entry failure is swallowed.
|
|
4
|
+
*/
|
|
5
|
+
import { walkDirectoryStats } from './opfs-cache.js';
|
|
6
|
+
|
|
7
|
+
export type StorageSource = 'opfs' | 'cache-storage' | 'indexeddb';
|
|
8
|
+
|
|
9
|
+
export interface StorageEntry {
|
|
10
|
+
source: StorageSource;
|
|
11
|
+
key: string;
|
|
12
|
+
size: number;
|
|
13
|
+
sizeKnown: boolean;
|
|
14
|
+
itemCount: number;
|
|
15
|
+
lastModified: number;
|
|
16
|
+
modelLike: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MODEL_HINTS = [
|
|
20
|
+
'huggingface', 'hf-', 'hf_', 'gemma', 'litert', 'onnx',
|
|
21
|
+
'qwen', 'mistral', 'llama', 'transformers', 'tokenizer', 'mediapipe',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const BLOB_SIZE_LIMIT = 50 * 1024 * 1024; // 50 MB — skip blob() fallback above this
|
|
25
|
+
|
|
26
|
+
function isModelLike(key: string): boolean {
|
|
27
|
+
const k = key.toLowerCase();
|
|
28
|
+
return MODEL_HINTS.some((h) => k.includes(h));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** OPFS entries outside the `webmcp-models` directory (which has its own UI). */
|
|
32
|
+
async function listOpfsEntries(): Promise<StorageEntry[]> {
|
|
33
|
+
const out: StorageEntry[] = [];
|
|
34
|
+
try {
|
|
35
|
+
if (!navigator.storage?.getDirectory) return out;
|
|
36
|
+
const root = await navigator.storage.getDirectory();
|
|
37
|
+
const iter = root as unknown as { entries: () => AsyncIterable<[string, FileSystemHandle]> };
|
|
38
|
+
for await (const [name, handle] of iter.entries()) {
|
|
39
|
+
if (name === 'webmcp-models') continue;
|
|
40
|
+
if (handle.kind !== 'directory') continue;
|
|
41
|
+
try {
|
|
42
|
+
const stats = await walkDirectoryStats(handle as FileSystemDirectoryHandle);
|
|
43
|
+
out.push({
|
|
44
|
+
source: 'opfs',
|
|
45
|
+
key: name,
|
|
46
|
+
size: stats.size,
|
|
47
|
+
sizeKnown: true,
|
|
48
|
+
itemCount: stats.fileCount,
|
|
49
|
+
lastModified: stats.lastModified,
|
|
50
|
+
modelLike: isModelLike(name),
|
|
51
|
+
});
|
|
52
|
+
} catch { /* skip entry */ }
|
|
53
|
+
}
|
|
54
|
+
} catch { /* OPFS unavailable */ }
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function measureResponse(response: Response): Promise<{ size: number; sizeKnown: boolean; lastModified: number }> {
|
|
59
|
+
let size = 0;
|
|
60
|
+
let sizeKnown = false;
|
|
61
|
+
let lastModified = 0;
|
|
62
|
+
try {
|
|
63
|
+
const cl = response.headers.get('content-length');
|
|
64
|
+
const parsed = cl !== null ? parseInt(cl, 10) : NaN;
|
|
65
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
66
|
+
size = parsed;
|
|
67
|
+
sizeKnown = true;
|
|
68
|
+
} else {
|
|
69
|
+
// Fallback: blob() — but only for small responses to avoid GB memory hits.
|
|
70
|
+
try {
|
|
71
|
+
const blob = await response.clone().blob();
|
|
72
|
+
if (blob.size < BLOB_SIZE_LIMIT) {
|
|
73
|
+
size = blob.size;
|
|
74
|
+
sizeKnown = true;
|
|
75
|
+
}
|
|
76
|
+
} catch { /* blob failed */ }
|
|
77
|
+
}
|
|
78
|
+
const dateHdr = response.headers.get('date');
|
|
79
|
+
if (dateHdr) {
|
|
80
|
+
const t = new Date(dateHdr).getTime();
|
|
81
|
+
if (Number.isFinite(t)) lastModified = t;
|
|
82
|
+
}
|
|
83
|
+
} catch { /* header access failed */ }
|
|
84
|
+
return { size, sizeKnown, lastModified };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function listCacheStorageEntries(): Promise<StorageEntry[]> {
|
|
88
|
+
const out: StorageEntry[] = [];
|
|
89
|
+
try {
|
|
90
|
+
if (typeof caches === 'undefined' || !caches.keys) return out;
|
|
91
|
+
const names = await caches.keys();
|
|
92
|
+
for (const name of names) {
|
|
93
|
+
try {
|
|
94
|
+
const cache = await caches.open(name);
|
|
95
|
+
const requests = await cache.keys();
|
|
96
|
+
let totalSize = 0;
|
|
97
|
+
let anyUnknown = false;
|
|
98
|
+
let lastModified = 0;
|
|
99
|
+
for (const req of requests) {
|
|
100
|
+
try {
|
|
101
|
+
const resp = await cache.match(req);
|
|
102
|
+
if (!resp) continue;
|
|
103
|
+
const m = await measureResponse(resp);
|
|
104
|
+
if (m.sizeKnown) totalSize += m.size;
|
|
105
|
+
else anyUnknown = true;
|
|
106
|
+
if (m.lastModified > lastModified) lastModified = m.lastModified;
|
|
107
|
+
} catch { /* per-request skip */ }
|
|
108
|
+
}
|
|
109
|
+
out.push({
|
|
110
|
+
source: 'cache-storage',
|
|
111
|
+
key: name,
|
|
112
|
+
size: totalSize,
|
|
113
|
+
sizeKnown: !anyUnknown,
|
|
114
|
+
itemCount: requests.length,
|
|
115
|
+
lastModified,
|
|
116
|
+
modelLike: isModelLike(name),
|
|
117
|
+
});
|
|
118
|
+
} catch { /* skip cache */ }
|
|
119
|
+
}
|
|
120
|
+
} catch { /* Cache API unavailable */ }
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function listIndexedDbEntries(): Promise<StorageEntry[]> {
|
|
125
|
+
const out: StorageEntry[] = [];
|
|
126
|
+
try {
|
|
127
|
+
const idb = indexedDB as IDBFactory & { databases?: () => Promise<Array<{ name?: string; version?: number }>> };
|
|
128
|
+
if (typeof idb.databases !== 'function') return out;
|
|
129
|
+
const dbs = await idb.databases();
|
|
130
|
+
for (const db of dbs) {
|
|
131
|
+
if (!db.name) continue;
|
|
132
|
+
out.push({
|
|
133
|
+
source: 'indexeddb',
|
|
134
|
+
key: db.name,
|
|
135
|
+
size: 0,
|
|
136
|
+
sizeKnown: false,
|
|
137
|
+
itemCount: db.version ?? 0,
|
|
138
|
+
lastModified: 0,
|
|
139
|
+
modelLike: isModelLike(db.name),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
} catch { /* IDB listing unsupported (e.g. older Safari) */ }
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Enumerate all Chrome-visible caches — OPFS (minus webmcp-models), Cache Storage, IndexedDB. */
|
|
147
|
+
export async function listAllStorage(): Promise<StorageEntry[]> {
|
|
148
|
+
const [opfs, cacheStorage, idb] = await Promise.all([
|
|
149
|
+
listOpfsEntries(),
|
|
150
|
+
listCacheStorageEntries(),
|
|
151
|
+
listIndexedDbEntries(),
|
|
152
|
+
]);
|
|
153
|
+
return [...opfs, ...cacheStorage, ...idb];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function deleteIdb(name: string): Promise<void> {
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
try {
|
|
159
|
+
const req = indexedDB.deleteDatabase(name);
|
|
160
|
+
req.onsuccess = () => resolve();
|
|
161
|
+
req.onerror = () => resolve();
|
|
162
|
+
req.onblocked = () => resolve();
|
|
163
|
+
} catch { resolve(); }
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Delete a single entry regardless of source. */
|
|
168
|
+
export async function deleteStorageEntry(entry: StorageEntry): Promise<void> {
|
|
169
|
+
try {
|
|
170
|
+
if (entry.source === 'opfs') {
|
|
171
|
+
const root = await navigator.storage.getDirectory();
|
|
172
|
+
try { await root.removeEntry(entry.key, { recursive: true }); } catch { /* best-effort */ }
|
|
173
|
+
} else if (entry.source === 'cache-storage') {
|
|
174
|
+
try { await caches.delete(entry.key); } catch { /* best-effort */ }
|
|
175
|
+
} else if (entry.source === 'indexeddb') {
|
|
176
|
+
await deleteIdb(entry.key);
|
|
177
|
+
}
|
|
178
|
+
} catch { /* best-effort */ }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Delete every entry of a given source. */
|
|
182
|
+
export async function clearAllStorage(source: StorageSource): Promise<void> {
|
|
183
|
+
try {
|
|
184
|
+
if (source === 'opfs') {
|
|
185
|
+
const entries = await listOpfsEntries();
|
|
186
|
+
for (const e of entries) await deleteStorageEntry(e);
|
|
187
|
+
} else if (source === 'cache-storage') {
|
|
188
|
+
const names = await caches.keys();
|
|
189
|
+
for (const n of names) { try { await caches.delete(n); } catch { /* skip */ } }
|
|
190
|
+
} else if (source === 'indexeddb') {
|
|
191
|
+
const entries = await listIndexedDbEntries();
|
|
192
|
+
for (const e of entries) await deleteIdb(e.key);
|
|
193
|
+
}
|
|
194
|
+
} catch { /* best-effort */ }
|
|
195
|
+
}
|
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
// ---------------------------------------------------------------------------
|
|
3
|
-
// notebook-compact — reactive minimalist layout (marimo-like)
|
|
4
|
-
// Left gutter with type label + vertical line, named outputs, fresh/stale status.
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
createState, injectStyles, mountRunControls, mountHistoryPanel,
|
|
9
|
-
setupDnD, deleteCellWithConfirm, restoreCellFromSnapshot, addCell,
|
|
10
|
-
logHistory, autosize, openShareModal, registerHistoryObserver,
|
|
11
|
-
buildServersButton,
|
|
12
|
-
type NotebookState, type NotebookCell,
|
|
13
|
-
} from './shared.js';
|
|
14
|
-
|
|
15
|
-
export async function render(container: HTMLElement, data: Record<string, unknown>): Promise<() => void> {
|
|
16
|
-
injectStyles();
|
|
17
|
-
injectLayoutStyles();
|
|
18
|
-
|
|
19
|
-
const state: NotebookState = createState({
|
|
20
|
-
id: data.id as string,
|
|
21
|
-
title: data.title as string,
|
|
22
|
-
mode: (data.mode as any) ?? 'edit',
|
|
23
|
-
cells: data.cells as any,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
container.classList.add('nb-root');
|
|
27
|
-
container.classList.toggle('nb-view-mode', state.mode === 'view');
|
|
28
|
-
|
|
29
|
-
container.innerHTML = `
|
|
30
|
-
<div class="nbc-shell">
|
|
31
|
-
<div class="nbc-toolbar">
|
|
32
|
-
<div class="nbc-status">
|
|
33
|
-
<span class="nbc-status-dot"></span>
|
|
34
|
-
<span class="nbc-status-text">reactive · 0 cells</span>
|
|
35
|
-
</div>
|
|
36
|
-
<div class="nbc-actions">
|
|
37
|
-
<div class="nb-mode-switch">
|
|
38
|
-
<button class="nb-mode-edit nb-on">edit</button>
|
|
39
|
-
<button class="nb-mode-view">view</button>
|
|
40
|
-
</div>
|
|
41
|
-
<button class="nb-btn nb-add-cell" data-add="md">+ md</button>
|
|
42
|
-
<button class="nb-btn nb-add-cell" data-add="sql">+ sql</button>
|
|
43
|
-
<button class="nb-btn nb-add-cell" data-add="js">+ js</button>
|
|
44
|
-
<button class="nb-btn nbc-history-btn">⟲ history</button>
|
|
45
|
-
<span class="nbc-servers-slot"></span>
|
|
46
|
-
<button class="nb-btn nbc-share-btn">share</button>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
<div class="nb-history-panel nbc-history-panel"></div>
|
|
50
|
-
<div class="nbc-cells"></div>
|
|
51
|
-
</div>`;
|
|
52
|
-
|
|
53
|
-
const shell = container.querySelector('.nbc-shell') as HTMLElement;
|
|
54
|
-
const cellsEl = shell.querySelector('.nbc-cells') as HTMLElement;
|
|
55
|
-
const historyPanel = shell.querySelector('.nbc-history-panel') as HTMLElement;
|
|
56
|
-
|
|
57
|
-
function renderCells() {
|
|
58
|
-
cellsEl.innerHTML = '';
|
|
59
|
-
state.cells.forEach((cell) => cellsEl.appendChild(renderCell(cell, state, rerender)));
|
|
60
|
-
updateStatus();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function updateStatus() {
|
|
64
|
-
const n = state.cells.length;
|
|
65
|
-
const stale = state.cells.filter((c) => c.status === 'stale').length;
|
|
66
|
-
(shell.querySelector('.nbc-status-text') as HTMLElement).textContent =
|
|
67
|
-
stale > 0 ? `reactive · ${n} cells · ${stale} stale` : `reactive · ${n} cells · synced`;
|
|
68
|
-
(shell.querySelector('.nbc-status-dot') as HTMLElement).classList.toggle('nbc-stale', stale > 0);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function rerender() {
|
|
72
|
-
mountHistoryPanel(historyPanel, state, (snap) => { restoreCellFromSnapshot(state, snap); rerender(); });
|
|
73
|
-
renderCells();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Toolbar bindings
|
|
77
|
-
shell.querySelectorAll<HTMLElement>('[data-add]').forEach((btn) => {
|
|
78
|
-
btn.addEventListener('click', () => {
|
|
79
|
-
const type = btn.dataset.add as any;
|
|
80
|
-
addCell(state, type, { varname: type === 'sql' ? 'rows_' + (state.cells.length + 1) : undefined });
|
|
81
|
-
rerender();
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
(shell.querySelector('.nbc-history-btn') as HTMLElement).addEventListener('click', () => {
|
|
85
|
-
historyPanel.classList.toggle('nb-open');
|
|
86
|
-
});
|
|
87
|
-
(shell.querySelector('.nbc-share-btn') as HTMLElement).addEventListener('click', () => {
|
|
88
|
-
openShareModal(state, (fmt) => console.log('[notebook-compact] share as', fmt, state));
|
|
89
|
-
});
|
|
90
|
-
const editBtn = shell.querySelector('.nb-mode-edit') as HTMLElement;
|
|
91
|
-
const viewBtn = shell.querySelector('.nb-mode-view') as HTMLElement;
|
|
92
|
-
editBtn.addEventListener('click', () => {
|
|
93
|
-
state.mode = 'edit';
|
|
94
|
-
container.classList.remove('nb-view-mode');
|
|
95
|
-
editBtn.classList.add('nb-on'); viewBtn.classList.remove('nb-on');
|
|
96
|
-
});
|
|
97
|
-
viewBtn.addEventListener('click', () => {
|
|
98
|
-
state.mode = 'view';
|
|
99
|
-
container.classList.add('nb-view-mode');
|
|
100
|
-
viewBtn.classList.add('nb-on'); editBtn.classList.remove('nb-on');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
buildServersButton(state, shell.querySelector('.nbc-servers-slot') as HTMLElement, data, rerender);
|
|
104
|
-
|
|
105
|
-
setupDnD(cellsEl, state, rerender);
|
|
106
|
-
const unsubHistory = registerHistoryObserver(() => mountHistoryPanel(historyPanel, state, (snap) => { restoreCellFromSnapshot(state, snap); rerender(); }));
|
|
107
|
-
|
|
108
|
-
rerender();
|
|
109
|
-
|
|
110
|
-
return () => { unsubHistory(); };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
// Cell rendering (compact layout)
|
|
115
|
-
// ---------------------------------------------------------------------------
|
|
116
|
-
|
|
117
|
-
function renderCell(cell: NotebookCell, state: NotebookState, rerender: () => void): HTMLElement {
|
|
118
|
-
const wrap = document.createElement('div');
|
|
119
|
-
wrap.className = 'nb-cell-wrapper nbc-cell';
|
|
120
|
-
wrap.dataset.id = cell.id;
|
|
121
|
-
|
|
122
|
-
const row = document.createElement('div');
|
|
123
|
-
row.className = 'nbc-row';
|
|
124
|
-
|
|
125
|
-
const handle = document.createElement('span');
|
|
126
|
-
handle.className = 'nb-drag-handle';
|
|
127
|
-
handle.draggable = true;
|
|
128
|
-
handle.textContent = '⋮⋮';
|
|
129
|
-
row.appendChild(handle);
|
|
130
|
-
|
|
131
|
-
const gutter = document.createElement('div');
|
|
132
|
-
gutter.className = `nbc-gutter nbc-gutter-${cell.type}`;
|
|
133
|
-
gutter.innerHTML = `<span class="nbc-type-label">${cell.type}</span><span class="nbc-line"></span>`;
|
|
134
|
-
row.appendChild(gutter);
|
|
135
|
-
|
|
136
|
-
const body = document.createElement('div');
|
|
137
|
-
body.className = 'nbc-body';
|
|
138
|
-
body.style.minWidth = '0';
|
|
139
|
-
|
|
140
|
-
if (cell.type === 'md') {
|
|
141
|
-
const mdBody = document.createElement('div');
|
|
142
|
-
mdBody.className = 'nbc-md-body';
|
|
143
|
-
const ta = document.createElement('textarea');
|
|
144
|
-
ta.className = 'nb-md-edit';
|
|
145
|
-
ta.value = cell.content;
|
|
146
|
-
ta.rows = 2;
|
|
147
|
-
ta.placeholder = 'write markdown…';
|
|
148
|
-
ta.addEventListener('input', () => { cell.content = ta.value; autosize(ta); });
|
|
149
|
-
mdBody.appendChild(ta);
|
|
150
|
-
body.appendChild(mdBody);
|
|
151
|
-
setTimeout(() => autosize(ta), 0);
|
|
152
|
-
|
|
153
|
-
const del = document.createElement('button');
|
|
154
|
-
del.className = 'nb-icon-btn nb-danger nbc-md-del';
|
|
155
|
-
del.textContent = '✕';
|
|
156
|
-
del.title = 'delete cell';
|
|
157
|
-
del.addEventListener('click', () => deleteCellWithConfirm(state, cell, (c) => 'markdown cell', rerender));
|
|
158
|
-
wrap.appendChild(del);
|
|
159
|
-
} else {
|
|
160
|
-
const codeCell = document.createElement('div');
|
|
161
|
-
codeCell.className = 'nb-code-cell nbc-code-cell';
|
|
162
|
-
|
|
163
|
-
// Cell title row with run controls FIRST (left), then meta
|
|
164
|
-
const titleRow = document.createElement('div');
|
|
165
|
-
titleRow.className = 'nbc-title-row';
|
|
166
|
-
titleRow.innerHTML = `
|
|
167
|
-
<span class="nbc-run-controls"></span>
|
|
168
|
-
${cell.varname ? `<span class="nbc-arrow-var">→ ${cell.varname}</span>` : ''}
|
|
169
|
-
<span class="nbc-meta-info">${cell.type === 'sql' ? '4 rows' : 'depends on rows'}</span>
|
|
170
|
-
<button class="nb-icon-btn nb-toggle-src">${cell.hideSource ? '▸ src' : '◂ src'}</button>
|
|
171
|
-
<button class="nb-icon-btn nb-toggle-res">${cell.hideResult ? '▸ res' : '◂ res'}</button>
|
|
172
|
-
<button class="nb-icon-btn nb-danger nbc-code-del">✕</button>
|
|
173
|
-
`;
|
|
174
|
-
codeCell.appendChild(titleRow);
|
|
175
|
-
mountRunControls(titleRow.querySelector('.nbc-run-controls') as HTMLElement, cell, wrap, rerender);
|
|
176
|
-
|
|
177
|
-
const codeBody = document.createElement('div');
|
|
178
|
-
codeBody.className = 'nbc-code-body' + (cell.hideSource ? ' nbc-hidden' : '');
|
|
179
|
-
const ta = document.createElement('textarea');
|
|
180
|
-
ta.className = 'nb-code-edit';
|
|
181
|
-
ta.value = cell.content;
|
|
182
|
-
ta.rows = 1;
|
|
183
|
-
ta.spellcheck = false;
|
|
184
|
-
ta.addEventListener('input', () => { cell.content = ta.value; autosize(ta); cell.status = 'stale'; });
|
|
185
|
-
codeBody.appendChild(ta);
|
|
186
|
-
codeCell.appendChild(codeBody);
|
|
187
|
-
setTimeout(() => autosize(ta), 0);
|
|
188
|
-
|
|
189
|
-
const result = document.createElement('div');
|
|
190
|
-
result.className = 'nbc-result-body' + (cell.hideResult ? ' nbc-hidden' : '');
|
|
191
|
-
if (cell.type === 'sql') {
|
|
192
|
-
result.innerHTML = `
|
|
193
|
-
<div class="nbc-result-row">
|
|
194
|
-
<span>row_1</span><span>42</span>
|
|
195
|
-
<span>row_2</span><span>17</span>
|
|
196
|
-
<span>row_3</span><span>8</span>
|
|
197
|
-
<span>row_4</span><span>3</span>
|
|
198
|
-
</div>`;
|
|
199
|
-
} else {
|
|
200
|
-
result.className = 'nbc-chart-result' + (cell.hideResult ? ' nbc-hidden' : '');
|
|
201
|
-
result.innerHTML = `
|
|
202
|
-
<div class="nbc-bar" style="height:100%"></div>
|
|
203
|
-
<div class="nbc-bar" style="height:68%"></div>
|
|
204
|
-
<div class="nbc-bar" style="height:52%"></div>
|
|
205
|
-
<div class="nbc-bar" style="height:22%"></div>`;
|
|
206
|
-
}
|
|
207
|
-
codeCell.appendChild(result);
|
|
208
|
-
body.appendChild(codeCell);
|
|
209
|
-
|
|
210
|
-
(titleRow.querySelector('.nb-toggle-src') as HTMLElement).addEventListener('click', () => { cell.hideSource = !cell.hideSource; rerender(); });
|
|
211
|
-
(titleRow.querySelector('.nb-toggle-res') as HTMLElement).addEventListener('click', () => { cell.hideResult = !cell.hideResult; rerender(); });
|
|
212
|
-
(titleRow.querySelector('.nbc-code-del') as HTMLElement).addEventListener('click', () =>
|
|
213
|
-
deleteCellWithConfirm(state, cell, (c) => `${c.type} cell${c.varname ? ' → ' + c.varname : ''}`, rerender)
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
row.appendChild(body);
|
|
218
|
-
wrap.appendChild(row);
|
|
219
|
-
return wrap;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// ---------------------------------------------------------------------------
|
|
223
|
-
// Layout-specific styles
|
|
224
|
-
// ---------------------------------------------------------------------------
|
|
225
|
-
|
|
226
|
-
function injectLayoutStyles(): void {
|
|
227
|
-
if (document.getElementById('nbc-styles')) return;
|
|
228
|
-
const style = document.createElement('style');
|
|
229
|
-
style.id = 'nbc-styles';
|
|
230
|
-
style.textContent = `
|
|
231
|
-
.nbc-shell {
|
|
232
|
-
background: var(--color-surface); border: 1px solid var(--color-border);
|
|
233
|
-
border-radius: 12px; padding: 18px;
|
|
234
|
-
}
|
|
235
|
-
.nbc-toolbar {
|
|
236
|
-
display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;
|
|
237
|
-
}
|
|
238
|
-
.nbc-status {
|
|
239
|
-
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
240
|
-
font-size: 11px; color: var(--color-text2);
|
|
241
|
-
display: inline-flex; align-items: center; gap: 8px;
|
|
242
|
-
}
|
|
243
|
-
.nbc-status-dot {
|
|
244
|
-
width: 6px; height: 6px; border-radius: 50%;
|
|
245
|
-
background: var(--color-teal);
|
|
246
|
-
}
|
|
247
|
-
.nbc-status-dot.nbc-stale { background: var(--color-amber); }
|
|
248
|
-
.nbc-actions { display: flex; gap: 6px; align-items: center; }
|
|
249
|
-
.nbc-history-panel { margin-bottom: 12px; }
|
|
250
|
-
|
|
251
|
-
.nbc-cell { margin-bottom: 14px; position: relative; }
|
|
252
|
-
.nbc-cell:last-child { margin-bottom: 0; }
|
|
253
|
-
.nbc-row {
|
|
254
|
-
display: grid; grid-template-columns: 20px 34px 1fr; gap: 6px;
|
|
255
|
-
}
|
|
256
|
-
.nbc-gutter {
|
|
257
|
-
display: flex; flex-direction: column; align-items: center; gap: 6px;
|
|
258
|
-
}
|
|
259
|
-
.nbc-type-label {
|
|
260
|
-
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
261
|
-
font-size: 10px; letter-spacing: 0.12em; color: var(--color-text2);
|
|
262
|
-
}
|
|
263
|
-
.nbc-gutter-sql .nbc-type-label { color: var(--color-accent); }
|
|
264
|
-
.nbc-gutter-js .nbc-type-label { color: var(--color-teal); }
|
|
265
|
-
.nbc-line { width: 1px; flex: 1; background: var(--color-border); }
|
|
266
|
-
.nbc-md-body { padding: 4px 2px; border: 1px dashed transparent; border-radius: 4px; }
|
|
267
|
-
.nbc-md-body:focus-within { border-color: var(--color-border); background: var(--color-bg); }
|
|
268
|
-
.nbc-md-del {
|
|
269
|
-
position: absolute; top: 4px; right: 4px;
|
|
270
|
-
opacity: 0; transition: opacity 0.15s;
|
|
271
|
-
}
|
|
272
|
-
.nbc-cell:hover .nbc-md-del { opacity: 0.5; }
|
|
273
|
-
.nbc-md-del:hover { opacity: 1 !important; }
|
|
274
|
-
|
|
275
|
-
.nbc-code-cell {
|
|
276
|
-
background: var(--color-bg); border: 1px solid var(--color-border);
|
|
277
|
-
border-radius: 8px; overflow: hidden;
|
|
278
|
-
transition: border-color 0.15s;
|
|
279
|
-
}
|
|
280
|
-
.nbc-code-cell:focus-within { border-color: var(--color-border2); }
|
|
281
|
-
.nbc-title-row {
|
|
282
|
-
display: flex; align-items: center; gap: 8px;
|
|
283
|
-
padding: 8px 12px; border-bottom: 1px solid var(--color-border);
|
|
284
|
-
background: var(--color-surface2);
|
|
285
|
-
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
286
|
-
font-size: 10.5px; color: var(--color-text2);
|
|
287
|
-
}
|
|
288
|
-
.nbc-title-row .nbc-arrow-var { color: var(--color-accent); }
|
|
289
|
-
.nbc-title-row .nbc-meta-info { margin-right: auto; }
|
|
290
|
-
.nbc-code-body { padding: 10px 14px; }
|
|
291
|
-
.nbc-hidden { display: none !important; }
|
|
292
|
-
.nbc-result-body {
|
|
293
|
-
padding: 10px 14px;
|
|
294
|
-
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
295
|
-
font-size: 12px;
|
|
296
|
-
border-top: 1px solid var(--color-border);
|
|
297
|
-
}
|
|
298
|
-
.nbc-result-row {
|
|
299
|
-
display: grid; grid-template-columns: 1fr auto; gap: 3px 24px;
|
|
300
|
-
color: var(--color-text2);
|
|
301
|
-
}
|
|
302
|
-
.nbc-result-row span:nth-child(even) {
|
|
303
|
-
color: var(--color-text1); font-variant-numeric: tabular-nums;
|
|
304
|
-
}
|
|
305
|
-
.nbc-chart-result {
|
|
306
|
-
padding: 14px; display: flex; align-items: flex-end; gap: 6px;
|
|
307
|
-
height: 92px; border-top: 1px solid var(--color-border);
|
|
308
|
-
}
|
|
309
|
-
.nbc-bar { flex: 1; background: var(--color-accent); border-radius: 2px 2px 0 0; opacity: 0.55; }
|
|
310
|
-
`;
|
|
311
|
-
document.head.appendChild(style);
|
|
312
|
-
}
|