@webmcp-auto-ui/ui 2.5.28 → 2.5.30
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 +4 -3
- package/src/index.ts +6 -6
- package/src/theme/scale.ts +128 -0
- package/src/widgets/WidgetRenderer.svelte +33 -6
- package/src/widgets/notebook/import-modals.ts +9 -2
- package/src/widgets/notebook/left-pane.ts +9 -2
- package/src/widgets/notebook/{editorial.ts → notebook.ts} +14 -20
- package/src/widgets/notebook/prose.ts +342 -0
- package/src/widgets/notebook/recipes/{editorial.md → notebook.md} +9 -5
- package/src/widgets/notebook/shared.ts +42 -1
- package/src/widgets/rich/js-sandbox.ts +21 -5
- package/src/widgets/rich/sankey.ts +3 -8
- package/src/agent/DataServersPanel.svelte +0 -164
- package/src/widgets/notebook/compact.ts +0 -823
- package/src/widgets/notebook/document.ts +0 -1065
- package/src/widgets/notebook/recipes/compact.md +0 -124
- package/src/widgets/notebook/recipes/document.md +0 -139
- package/src/widgets/notebook/recipes/workspace.md +0 -119
- package/src/widgets/notebook/workspace.ts +0 -852
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webmcp-auto-ui/ui",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.30",
|
|
4
4
|
"description": "Svelte 5 UI components — primitives, widgets, window manager",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -57,15 +57,16 @@
|
|
|
57
57
|
"auto-ui"
|
|
58
58
|
],
|
|
59
59
|
"dependencies": {
|
|
60
|
+
"@types/d3": "^7.4.3",
|
|
60
61
|
"@webmcp-auto-ui/core": "*",
|
|
61
62
|
"@webmcp-auto-ui/sdk": "*",
|
|
62
|
-
"@types/d3": "^7.4.3",
|
|
63
63
|
"bits-ui": "^2.17.2",
|
|
64
64
|
"clsx": "^2.1.1",
|
|
65
65
|
"highlight.js": "^11.10.0",
|
|
66
66
|
"html-to-image": "^1.11.13",
|
|
67
67
|
"marked": "^14.1.3",
|
|
68
68
|
"tailwind-merge": "^3.5.0",
|
|
69
|
-
"tailwind-variants": "^3.2.2"
|
|
69
|
+
"tailwind-variants": "^3.2.2",
|
|
70
|
+
"turndown": "^7.2.4"
|
|
70
71
|
}
|
|
71
72
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { default as ThemeProvider, getTheme } from './theme/ThemeProvider.svelte
|
|
|
5
5
|
export type { ThemeJSON } from './theme/ThemeProvider.svelte';
|
|
6
6
|
export { DARK_TOKENS, LIGHT_TOKENS, THEME_MAP } from './theme/tokens.js';
|
|
7
7
|
export type { ThemeMode, ThemeOverrides, ThemeTokens } from './theme/tokens.js';
|
|
8
|
+
export { getUIScale, setUIScale, toggleUIScale, initUIScale, isUIScaled } from './theme/scale.js';
|
|
9
|
+
export type { UIScale, UIScaleKey } from './theme/scale.js';
|
|
8
10
|
|
|
9
11
|
// Primitives
|
|
10
12
|
export { default as Card } from './primitives/Card.svelte';
|
|
@@ -46,14 +48,13 @@ export { render as renderLog } from './widgets/rich/log.js';
|
|
|
46
48
|
export { render as renderGallery } from './widgets/rich/gallery.js';
|
|
47
49
|
export { render as renderCarousel } from './widgets/rich/carousel.js';
|
|
48
50
|
|
|
49
|
-
// Notebook widget
|
|
50
|
-
export { render as
|
|
51
|
-
export { render as renderWorkspace } from './widgets/notebook/workspace.js';
|
|
52
|
-
export { render as renderDocument } from './widgets/notebook/document.js';
|
|
53
|
-
export { render as renderEditorial } from './widgets/notebook/editorial.js';
|
|
51
|
+
// Notebook widget renderer (vanilla)
|
|
52
|
+
export { render as renderNotebook } from './widgets/notebook/notebook.js';
|
|
54
53
|
export { render as renderRecipeBrowserWidget } from './widgets/notebook/recipe-browser.js';
|
|
55
54
|
// Notebook types (optional public API)
|
|
56
55
|
export type { NotebookState, NotebookCell } from './widgets/notebook/shared.js';
|
|
56
|
+
// Notebook cell extractors (for hosts that build notebooks from recipes/tools)
|
|
57
|
+
export { extractCellsFromRecipe, extractCellsFromTool, extractCellFromMarkdown, extractCellFromFence } from './widgets/notebook/resource-extractor.js';
|
|
57
58
|
|
|
58
59
|
// Safe image helper (URL validation + error fallback)
|
|
59
60
|
export { createSafeImage } from './widgets/helpers/safe-image.js';
|
|
@@ -109,7 +110,6 @@ export { default as AgentConsole } from './agent/AgentConsole.svelte';
|
|
|
109
110
|
export { default as SettingsPanel } from './agent/SettingsPanel.svelte';
|
|
110
111
|
export { default as RemoteMCPserversDemo } from './agent/RemoteMCPserversDemo.svelte';
|
|
111
112
|
export { default as WebMCPserversList } from './agent/WebMCPserversList.svelte';
|
|
112
|
-
export { default as DataServersPanel } from './agent/DataServersPanel.svelte';
|
|
113
113
|
export { default as EphemeralBubble } from './agent/EphemeralBubble.svelte';
|
|
114
114
|
export { default as TokenBubble } from './agent/TokenBubble.svelte';
|
|
115
115
|
export { default as DiagnosticModal } from './agent/DiagnosticModal.svelte';
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI scale — binary 1× / 1.5× toggle, labeled as "2×" in the UI for affordance.
|
|
3
|
+
*
|
|
4
|
+
* Implementation: CSS `zoom` on `<html>`, driven by a custom property set
|
|
5
|
+
* imperatively. This rescales everything uniformly (fonts, icons, borders,
|
|
6
|
+
* modals, widgets) while keeping layout reflow (contrary to `transform: scale`).
|
|
7
|
+
*
|
|
8
|
+
* Persistence: `localStorage['webmcp-ui-scale']` — "x1" | "x2".
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type UIScale = 1 | 1.5;
|
|
12
|
+
export type UIScaleKey = 'x1' | 'x2';
|
|
13
|
+
|
|
14
|
+
const STORAGE_KEY = 'webmcp-ui-scale';
|
|
15
|
+
const STYLE_ID = 'webmcp-ui-scale-style';
|
|
16
|
+
|
|
17
|
+
function injectStyle(): void {
|
|
18
|
+
if (typeof document === 'undefined') return;
|
|
19
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
20
|
+
const style = document.createElement('style');
|
|
21
|
+
style.id = STYLE_ID;
|
|
22
|
+
// When scaled:
|
|
23
|
+
// - zoom the root so everything grows uniformly
|
|
24
|
+
// - compensate layouts sized in 100vh/100vw (viewport units stay tied to the
|
|
25
|
+
// physical viewport regardless of zoom, so a `h-screen` container rendered
|
|
26
|
+
// at 1.5× would overflow by 50%). Force these containers to the logical
|
|
27
|
+
// equivalent so they fit after scaling.
|
|
28
|
+
// - keep html/body overflow:hidden (matches the unscaled baseline) so
|
|
29
|
+
// floating bars with flex-shrink-0 stay pinned to the viewport edge
|
|
30
|
+
// instead of drifting mid-screen when the body scrolls.
|
|
31
|
+
style.textContent = `
|
|
32
|
+
html { zoom: var(--ui-scale, 1); }
|
|
33
|
+
html[data-ui-scale="x2"] {
|
|
34
|
+
width: calc(100vw / 1.5);
|
|
35
|
+
height: calc(100vh / 1.5);
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
}
|
|
38
|
+
html[data-ui-scale="x2"] body {
|
|
39
|
+
width: 100%;
|
|
40
|
+
height: 100%;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
}
|
|
43
|
+
html[data-ui-scale="x2"] .h-screen,
|
|
44
|
+
html[data-ui-scale="x2"] [class*="h-screen"] {
|
|
45
|
+
height: calc(100vh / 1.5);
|
|
46
|
+
max-height: calc(100vh / 1.5);
|
|
47
|
+
}
|
|
48
|
+
html[data-ui-scale="x2"] .w-screen,
|
|
49
|
+
html[data-ui-scale="x2"] [class*="w-screen"] {
|
|
50
|
+
width: calc(100vw / 1.5);
|
|
51
|
+
max-width: calc(100vw / 1.5);
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
document.head.appendChild(style);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function applyScale(scale: UIScale): void {
|
|
58
|
+
if (typeof document === 'undefined') return;
|
|
59
|
+
injectStyle();
|
|
60
|
+
const root = document.documentElement;
|
|
61
|
+
root.style.setProperty('--ui-scale', String(scale));
|
|
62
|
+
root.dataset.uiScale = scale === 1 ? 'x1' : 'x2';
|
|
63
|
+
try {
|
|
64
|
+
window.dispatchEvent(new CustomEvent('webmcp:ui-scale-change', { detail: { scale } }));
|
|
65
|
+
} catch { /* ignore */ }
|
|
66
|
+
// Canvas-based widgets (cytoscape, d3, vega-embed, leaflet, custom SVG) size
|
|
67
|
+
// their rendering surface at mount via getBoundingClientRect and don't react
|
|
68
|
+
// to CSS-only zoom changes. Fire a resize event after the browser has applied
|
|
69
|
+
// the zoom (next RAF) so those libs redraw against the new container size.
|
|
70
|
+
// A second tick after ~200ms catches widgets that listen via debounced
|
|
71
|
+
// ResizeObserver + widgets that rebuild on a longer cycle.
|
|
72
|
+
try {
|
|
73
|
+
requestAnimationFrame(() => {
|
|
74
|
+
window.dispatchEvent(new Event('resize'));
|
|
75
|
+
setTimeout(() => window.dispatchEvent(new Event('resize')), 200);
|
|
76
|
+
});
|
|
77
|
+
} catch { /* ignore */ }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readStored(): UIScale {
|
|
81
|
+
if (typeof localStorage === 'undefined') return 1;
|
|
82
|
+
try {
|
|
83
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
84
|
+
if (raw === 'x2') return 1.5;
|
|
85
|
+
return 1;
|
|
86
|
+
} catch { return 1; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function writeStored(scale: UIScale): void {
|
|
90
|
+
if (typeof localStorage === 'undefined') return;
|
|
91
|
+
try { localStorage.setItem(STORAGE_KEY, scale === 1 ? 'x1' : 'x2'); } catch { /* ignore */ }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Current scale value. Safe to call before init. */
|
|
95
|
+
export function getUIScale(): UIScale {
|
|
96
|
+
if (typeof document === 'undefined') return readStored();
|
|
97
|
+
const key = document.documentElement.dataset.uiScale;
|
|
98
|
+
if (key === 'x2') return 1.5;
|
|
99
|
+
if (key === 'x1') return 1;
|
|
100
|
+
return readStored();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** True when the UI is currently displayed at 1.5× (label "2×" in the UI). */
|
|
104
|
+
export function isUIScaled(): boolean {
|
|
105
|
+
return getUIScale() !== 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Set the scale explicitly. Persists + applies. */
|
|
109
|
+
export function setUIScale(scale: UIScale): void {
|
|
110
|
+
writeStored(scale);
|
|
111
|
+
applyScale(scale);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Toggle between 1× and 1.5×. Returns the new scale. */
|
|
115
|
+
export function toggleUIScale(): UIScale {
|
|
116
|
+
const next: UIScale = getUIScale() === 1 ? 1.5 : 1;
|
|
117
|
+
setUIScale(next);
|
|
118
|
+
return next;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Read persisted scale and apply it. Call once at app boot (e.g. in a root
|
|
123
|
+
* +layout onMount) to restore the user's preference before first paint.
|
|
124
|
+
* Safe to call multiple times.
|
|
125
|
+
*/
|
|
126
|
+
export function initUIScale(): void {
|
|
127
|
+
applyScale(readStored());
|
|
128
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { type Component, onMount, onDestroy } from 'svelte';
|
|
2
|
+
import { type Component, onMount, onDestroy, untrack } from 'svelte';
|
|
3
3
|
import type { WebMcpServer } from '@webmcp-auto-ui/core';
|
|
4
4
|
import { bus } from '../messaging/bus.svelte.js';
|
|
5
5
|
|
|
@@ -85,8 +85,8 @@
|
|
|
85
85
|
'cards': renderCards,
|
|
86
86
|
'grid-data': renderGridData,
|
|
87
87
|
'map': renderMap,
|
|
88
|
-
'd3': renderD3,
|
|
89
|
-
'js-sandbox': renderJsSandbox,
|
|
88
|
+
'd3': renderD3 as unknown as VanillaRenderer,
|
|
89
|
+
'js-sandbox': renderJsSandbox as unknown as VanillaRenderer,
|
|
90
90
|
'log': renderLog,
|
|
91
91
|
'gallery': renderGallery,
|
|
92
92
|
'carousel': renderCarousel,
|
|
@@ -151,13 +151,17 @@
|
|
|
151
151
|
// ── Vanilla renderer container + lifecycle ────────────
|
|
152
152
|
let vanillaContainer: HTMLElement | undefined = $state(undefined);
|
|
153
153
|
|
|
154
|
+
// Mount effect — re-runs only when the widget identity changes (type /
|
|
155
|
+
// renderer / container). Data changes are handled separately to avoid a
|
|
156
|
+
// flickering full remount on every agent update.
|
|
154
157
|
$effect(() => {
|
|
158
|
+
// Touch only the mount deps; `plainData` is intentionally read via untrack
|
|
159
|
+
// so data updates don't retrigger this effect.
|
|
155
160
|
if (!useVanilla || !vanillaRenderer || !vanillaContainer) return;
|
|
156
161
|
const container = vanillaContainer;
|
|
157
|
-
|
|
162
|
+
const renderer = vanillaRenderer;
|
|
158
163
|
container.innerHTML = '';
|
|
159
164
|
|
|
160
|
-
// Listen for the standard vanilla event contract: CustomEvent('widget:interact')
|
|
161
165
|
const onInteract = (ev: Event) => {
|
|
162
166
|
const ce = ev as CustomEvent<{ action?: string; payload?: unknown }>;
|
|
163
167
|
const action = ce.detail?.action ?? 'interact';
|
|
@@ -169,7 +173,7 @@
|
|
|
169
173
|
let cancelled = false;
|
|
170
174
|
|
|
171
175
|
try {
|
|
172
|
-
const result =
|
|
176
|
+
const result = renderer(container, untrack(() => plainData));
|
|
173
177
|
if (result && typeof (result as Promise<unknown>).then === 'function') {
|
|
174
178
|
(result as Promise<void | (() => void)>).then(
|
|
175
179
|
(c) => { if (!cancelled) cleanup = c ?? undefined; },
|
|
@@ -190,6 +194,29 @@
|
|
|
190
194
|
};
|
|
191
195
|
});
|
|
192
196
|
|
|
197
|
+
// Data-update effect — only triggers when plainData changes. Renderers that
|
|
198
|
+
// support in-place updates should listen for `widget:data-update` and call
|
|
199
|
+
// preventDefault() on the event; doing so signals they've handled the new
|
|
200
|
+
// data themselves so we don't need to remount. Renderers that don't listen
|
|
201
|
+
// fall back to a full remount here (innerHTML cleared + mount effect re-run).
|
|
202
|
+
let firstDataCycle = true;
|
|
203
|
+
$effect(() => {
|
|
204
|
+
const data = plainData;
|
|
205
|
+
if (!vanillaContainer || !useVanilla) return;
|
|
206
|
+
if (firstDataCycle) { firstDataCycle = false; return; }
|
|
207
|
+
const ev = new CustomEvent('widget:data-update', { detail: data, cancelable: true });
|
|
208
|
+
const handled = !vanillaContainer.dispatchEvent(ev);
|
|
209
|
+
if (handled || !vanillaRenderer) return;
|
|
210
|
+
// Not handled — fall back to remount by clearing + calling renderer again.
|
|
211
|
+
const container = vanillaContainer;
|
|
212
|
+
container.innerHTML = '';
|
|
213
|
+
try {
|
|
214
|
+
vanillaRenderer(container, data);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error('[WidgetRenderer] fallback remount failed:', err);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
193
220
|
// ── Auto-register WebMCP tools when modelContext is available ────────────
|
|
194
221
|
type ModelContext = {
|
|
195
222
|
registerTool: (t: unknown) => void;
|
|
@@ -316,7 +316,14 @@ function extractRecipeBody(res: any): string | null {
|
|
|
316
316
|
if (!text) return null;
|
|
317
317
|
try {
|
|
318
318
|
const parsed = JSON.parse(text);
|
|
319
|
-
|
|
319
|
+
// Recipe servers return either { body: "..." } (autoui-style),
|
|
320
|
+
// { content: "..." } (legacy), or { markdown: "..." }. Pick whichever
|
|
321
|
+
// string field carries the markdown body, in priority order.
|
|
322
|
+
if (parsed && typeof parsed === 'object') {
|
|
323
|
+
if (typeof parsed.body === 'string') return parsed.body;
|
|
324
|
+
if (typeof parsed.content === 'string') return parsed.content;
|
|
325
|
+
if (typeof parsed.markdown === 'string') return parsed.markdown;
|
|
326
|
+
}
|
|
320
327
|
} catch { /* not JSON */ }
|
|
321
328
|
return text;
|
|
322
329
|
}
|
|
@@ -480,7 +487,7 @@ function injectImportStyles() {
|
|
|
480
487
|
}
|
|
481
488
|
.nb-imp-btn:hover { background: var(--color-surface3, #eeeef0); }
|
|
482
489
|
.nb-imp-primary { background: var(--color-accent, #6a55ff); color: #fff; border: 0; }
|
|
483
|
-
.nb-imp-primary:hover { filter: brightness(1.
|
|
490
|
+
.nb-imp-primary:hover { filter: brightness(1.1); color: #fff; }
|
|
484
491
|
.nb-imp-search, .nb-imp-url {
|
|
485
492
|
width: 100%; padding: 8px 10px; border: 1px solid var(--color-border, #e4e4e7);
|
|
486
493
|
border-radius: 6px; font-size: 12px; margin-bottom: 10px;
|
|
@@ -141,8 +141,15 @@ export function mountLeftPane(
|
|
|
141
141
|
let body = text;
|
|
142
142
|
try {
|
|
143
143
|
const parsed = JSON.parse(text);
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
// Recipe servers return either { content: "..." } (legacy) or
|
|
145
|
+
// { name, description, body, ... } (autoui-style). Pick whichever
|
|
146
|
+
// string field carries the markdown body, in priority order.
|
|
147
|
+
if (parsed && typeof parsed === 'object') {
|
|
148
|
+
if (typeof parsed.body === 'string') body = parsed.body;
|
|
149
|
+
else if (typeof parsed.content === 'string') body = parsed.content;
|
|
150
|
+
else if (typeof parsed.markdown === 'string') body = parsed.markdown;
|
|
151
|
+
}
|
|
152
|
+
} catch { /* not JSON, use raw text */ }
|
|
146
153
|
imported.body = body;
|
|
147
154
|
recipeBodyCache.set(key, body);
|
|
148
155
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
// ---------------------------------------------------------------------------
|
|
3
|
-
// notebook
|
|
3
|
+
// notebook — publication-ready layout (observable-like)
|
|
4
4
|
// Serif prose + cells in a single ordered list, all drag-and-droppable together.
|
|
5
5
|
// Cells alternate freely: md (prose paragraph) / sql / js cells share the flow.
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
@@ -13,13 +13,13 @@ import {
|
|
|
13
13
|
renderCellLogs,
|
|
14
14
|
createPublishControls, autoConnectFrontmatterServers,
|
|
15
15
|
createRuntimeOverlay, effectiveResult, cellRuntimeStatus,
|
|
16
|
-
lastRefreshedAt, bootstrapLiveRefresh, fmtRelTime,
|
|
16
|
+
lastRefreshedAt, bootstrapLiveRefresh, fmtRelTime, preserveScrollAround,
|
|
17
17
|
type NotebookState, type NotebookCell, type CellResult, type CellExecContext,
|
|
18
18
|
type RuntimeOverlay,
|
|
19
19
|
} from './shared.js';
|
|
20
20
|
import { renderChart } from './chart-renderer.js';
|
|
21
21
|
import { dispatchShare } from './share-handlers.js';
|
|
22
|
-
import { renderProse } from './prose.js';
|
|
22
|
+
import { renderProse, mountEditableProse } from './prose.js';
|
|
23
23
|
import { openAddMdModal, openAddRecipeModal } from './import-modals.js';
|
|
24
24
|
import { extractCellsFromRecipe, extractCellFromMarkdown } from './resource-extractor.js';
|
|
25
25
|
import { mountLeftPane } from './left-pane.js';
|
|
@@ -106,8 +106,11 @@ export async function render(container: HTMLElement, data: Record<string, unknow
|
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
const hideLiveToggle = (data as any).hideLiveToggle === true;
|
|
110
|
+
|
|
109
111
|
function renderLiveToggle() {
|
|
110
112
|
const slot = shell.querySelector('.nbe-live-toggle-slot') as HTMLElement;
|
|
113
|
+
if (hideLiveToggle) { slot.innerHTML = ''; return; }
|
|
111
114
|
if (state.mode === 'edit') {
|
|
112
115
|
const checked = state.autoRun === true ? 'checked' : '';
|
|
113
116
|
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>`;
|
|
@@ -185,11 +188,13 @@ export async function render(container: HTMLElement, data: Record<string, unknow
|
|
|
185
188
|
}
|
|
186
189
|
|
|
187
190
|
function rerender() {
|
|
191
|
+
const restore = preserveScrollAround(cellsEl);
|
|
188
192
|
mountHistoryPanel(historyPanel, state, (snap) => { restoreCellFromSnapshot(state, snap); rerender(); });
|
|
189
193
|
renderLiveToggle();
|
|
190
194
|
renderLiveBadge();
|
|
191
195
|
renderEmptyState();
|
|
192
196
|
renderCells();
|
|
197
|
+
restore();
|
|
193
198
|
}
|
|
194
199
|
|
|
195
200
|
// Toolbar: direct add (prose/sql/js)
|
|
@@ -429,24 +434,13 @@ function renderCell(cell: NotebookCell, state: NotebookState, overlay: RuntimeOv
|
|
|
429
434
|
rendered.innerHTML = renderProse(cell.content || '');
|
|
430
435
|
wrap.appendChild(rendered);
|
|
431
436
|
} else {
|
|
432
|
-
//
|
|
433
|
-
const editor =
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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 || '');
|
|
437
|
+
// Inline WYSIWYG — single contenteditable zone, floating toolbar on select.
|
|
438
|
+
const editor = mountEditableProse({
|
|
439
|
+
getContent: () => cell.content || '',
|
|
440
|
+
setContent: (md) => { cell.content = md; },
|
|
441
|
+
onChange: () => { state.lastEditAt = Date.now(); },
|
|
443
442
|
});
|
|
444
|
-
|
|
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)));
|
|
443
|
+
wrap.appendChild(editor.el);
|
|
450
444
|
}
|
|
451
445
|
return wrap;
|
|
452
446
|
}
|