@webmcp-auto-ui/ui 2.5.27 → 2.5.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/package.json +18 -5
  2. package/src/agent/LLMSelector.svelte +11 -3
  3. package/src/agent/ModelCacheManager.svelte +359 -0
  4. package/src/index.ts +42 -30
  5. package/src/theme/scale.ts +128 -0
  6. package/src/widgets/WidgetRenderer.svelte +144 -107
  7. package/src/widgets/export-widget.ts +28 -1
  8. package/src/widgets/helpers/safe-image.ts +78 -0
  9. package/src/widgets/notebook/.gitkeep +0 -0
  10. package/src/widgets/notebook/chart-renderer.ts +63 -0
  11. package/src/widgets/notebook/executors/.gitkeep +1 -0
  12. package/src/widgets/notebook/executors/index.ts +4 -0
  13. package/src/widgets/notebook/executors/js-worker.ts +269 -0
  14. package/src/widgets/notebook/executors/sql.ts +206 -0
  15. package/src/widgets/notebook/import-modals.ts +560 -0
  16. package/src/widgets/notebook/left-pane.ts +256 -0
  17. package/src/widgets/notebook/notebook.ts +930 -0
  18. package/src/widgets/notebook/prose.ts +615 -0
  19. package/src/widgets/notebook/recipe-browser.ts +350 -0
  20. package/src/widgets/notebook/recipes/notebook.md +124 -0
  21. package/src/widgets/notebook/resource-extractor.ts +162 -0
  22. package/src/widgets/notebook/share-handlers.ts +222 -0
  23. package/src/widgets/notebook/shared.ts +1633 -0
  24. package/src/widgets/rich/cards.ts +181 -0
  25. package/src/widgets/rich/carousel.ts +319 -0
  26. package/src/widgets/rich/chart-rich.ts +386 -0
  27. package/src/widgets/rich/d3.ts +503 -0
  28. package/src/widgets/rich/data-table.ts +342 -0
  29. package/src/widgets/rich/gallery.ts +350 -0
  30. package/src/widgets/rich/grid-data.ts +173 -0
  31. package/src/widgets/rich/hemicycle.ts +313 -0
  32. package/src/widgets/rich/js-sandbox.ts +122 -0
  33. package/src/widgets/rich/json-viewer.ts +202 -0
  34. package/src/widgets/rich/log.ts +143 -0
  35. package/src/widgets/rich/map.ts +218 -0
  36. package/src/widgets/rich/profile.ts +256 -0
  37. package/src/widgets/rich/sankey.ts +257 -0
  38. package/src/widgets/rich/stat-card.ts +125 -0
  39. package/src/widgets/rich/timeline.ts +179 -0
  40. package/src/widgets/rich/trombinoscope.ts +246 -0
  41. package/src/widgets/simple/actions.ts +89 -0
  42. package/src/widgets/simple/alert.ts +100 -0
  43. package/src/widgets/simple/chart.ts +189 -0
  44. package/src/widgets/simple/code.ts +79 -0
  45. package/src/widgets/simple/kv.ts +68 -0
  46. package/src/widgets/simple/list.ts +89 -0
  47. package/src/widgets/simple/stat.ts +58 -0
  48. package/src/widgets/simple/tags.ts +125 -0
  49. package/src/widgets/simple/text.ts +198 -0
  50. package/src/widgets/SafeImage.svelte +0 -76
  51. package/src/widgets/rich/Cards.svelte +0 -39
  52. package/src/widgets/rich/Carousel.svelte +0 -88
  53. package/src/widgets/rich/Chart.svelte +0 -142
  54. package/src/widgets/rich/D3Widget.svelte +0 -378
  55. package/src/widgets/rich/DataTable.svelte +0 -62
  56. package/src/widgets/rich/Gallery.svelte +0 -94
  57. package/src/widgets/rich/GridData.svelte +0 -44
  58. package/src/widgets/rich/Hemicycle.svelte +0 -78
  59. package/src/widgets/rich/JsSandbox.svelte +0 -51
  60. package/src/widgets/rich/JsonViewer.svelte +0 -42
  61. package/src/widgets/rich/LogViewer.svelte +0 -24
  62. package/src/widgets/rich/MapView.svelte +0 -140
  63. package/src/widgets/rich/ProfileCard.svelte +0 -59
  64. package/src/widgets/rich/Sankey.svelte +0 -56
  65. package/src/widgets/rich/StatCard.svelte +0 -35
  66. package/src/widgets/rich/Timeline.svelte +0 -43
  67. package/src/widgets/rich/Trombinoscope.svelte +0 -48
  68. package/src/widgets/simple/ActionsBlock.svelte +0 -15
  69. package/src/widgets/simple/AlertBlock.svelte +0 -11
  70. package/src/widgets/simple/ChartBlock.svelte +0 -21
  71. package/src/widgets/simple/CodeBlock.svelte +0 -11
  72. package/src/widgets/simple/KVBlock.svelte +0 -16
  73. package/src/widgets/simple/ListBlock.svelte +0 -17
  74. package/src/widgets/simple/StatBlock.svelte +0 -14
  75. package/src/widgets/simple/TagsBlock.svelte +0 -15
  76. package/src/widgets/simple/TextBlock.svelte +0 -122
@@ -0,0 +1,173 @@
1
+ /**
2
+ * GridData — vanilla renderer.
3
+ * Port of GridData.svelte. Preserves Tailwind classes, CSS vars, highlights, a11y.
4
+ *
5
+ * Contract:
6
+ * render(container, spec): cleanup
7
+ * Events:
8
+ * On cell double-click -> CustomEvent('widget:interact', {
9
+ * detail: { action: 'cellclick', payload: { row, col, value } },
10
+ * bubbles: true
11
+ * })
12
+ */
13
+
14
+ export interface GridDataColumn {
15
+ key: string;
16
+ label: string;
17
+ width?: string;
18
+ }
19
+
20
+ export interface GridDataHighlight {
21
+ row: number;
22
+ col: number;
23
+ color?: string;
24
+ }
25
+
26
+ export interface GridDataSpec {
27
+ title?: string;
28
+ columns?: GridDataColumn[];
29
+ rows?: unknown[][];
30
+ highlights?: GridDataHighlight[];
31
+ cellHeight?: number;
32
+ }
33
+
34
+ function displayValue(v: unknown): string {
35
+ if (v == null) return '';
36
+ if (typeof v === 'object') {
37
+ try {
38
+ return JSON.stringify(v);
39
+ } catch {
40
+ return String(v);
41
+ }
42
+ }
43
+ return String(v);
44
+ }
45
+
46
+ function buildHighlightMap(highlights: GridDataHighlight[] | undefined): Map<string, string> {
47
+ const m = new Map<string, string>();
48
+ if (Array.isArray(highlights)) {
49
+ for (const h of highlights) {
50
+ if (!h || typeof h.row !== 'number' || typeof h.col !== 'number') continue;
51
+ const color =
52
+ h.color ?? 'color-mix(in srgb, var(--color-accent) 20%, transparent)';
53
+ m.set(`${h.row},${h.col}`, color);
54
+ }
55
+ }
56
+ return m;
57
+ }
58
+
59
+ export function render(container: HTMLElement, data: any): () => void {
60
+ const spec: Partial<GridDataSpec> = (data ?? {}) as Partial<GridDataSpec>;
61
+ const columns: GridDataColumn[] = Array.isArray(spec.columns) ? spec.columns : [];
62
+ const rows: unknown[][] = Array.isArray(spec.rows) ? (spec.rows as unknown[][]) : [];
63
+ const cellH: number = typeof spec.cellHeight === 'number' ? spec.cellHeight : 32;
64
+ const hlMap = buildHighlightMap(spec.highlights);
65
+
66
+ // Clear container
67
+ container.innerHTML = '';
68
+
69
+ // Root
70
+ const root = document.createElement('div');
71
+ root.className = 'bg-surface border border-border rounded-lg p-3 md:p-4 font-sans';
72
+
73
+ // Title
74
+ if (spec.title) {
75
+ const h3 = document.createElement('h3');
76
+ h3.className = 'text-sm font-semibold text-text1 mb-3';
77
+ h3.textContent = String(spec.title);
78
+ root.appendChild(h3);
79
+ }
80
+
81
+ // Empty state
82
+ if (!columns.length && !rows.length) {
83
+ const empty = document.createElement('p');
84
+ empty.className = 'text-text2 text-sm';
85
+ empty.textContent = 'No data';
86
+ root.appendChild(empty);
87
+ container.appendChild(root);
88
+ return () => {
89
+ container.innerHTML = '';
90
+ };
91
+ }
92
+
93
+ // Scroll wrapper
94
+ const scroll = document.createElement('div');
95
+ scroll.className = 'overflow-auto rounded border border-border';
96
+
97
+ // Table
98
+ const table = document.createElement('table');
99
+ table.className = 'w-full border-collapse text-xs font-mono';
100
+ table.setAttribute('role', 'grid');
101
+
102
+ // Head
103
+ if (columns.length) {
104
+ const thead = document.createElement('thead');
105
+ const trh = document.createElement('tr');
106
+ for (const col of columns) {
107
+ const th = document.createElement('th');
108
+ th.className =
109
+ 'sticky top-0 bg-surface2 px-3 py-2 text-left text-text2 border-b border-r border-border whitespace-nowrap font-medium';
110
+ if (col && col.width) th.style.width = String(col.width);
111
+ th.setAttribute('scope', 'col');
112
+ th.textContent = col && col.label != null ? String(col.label) : '';
113
+ trh.appendChild(th);
114
+ }
115
+ thead.appendChild(trh);
116
+ table.appendChild(thead);
117
+ }
118
+
119
+ // Body — store listeners for cleanup
120
+ const listeners: Array<{ el: HTMLElement; handler: EventListener }> = [];
121
+ const tbody = document.createElement('tbody');
122
+
123
+ rows.forEach((row, ri) => {
124
+ const tr = document.createElement('tr');
125
+ tr.className = 'hover:bg-surface2';
126
+ const cells = Array.isArray(row) ? row : [];
127
+ cells.forEach((cell, ci) => {
128
+ const td = document.createElement('td');
129
+ td.className =
130
+ 'px-3 text-text2 border-b border-r border-border cursor-pointer hover:bg-surface2';
131
+ const bgColor = hlMap.get(`${ri},${ci}`);
132
+ td.style.height = `${cellH}px`;
133
+ if (bgColor) td.style.background = bgColor;
134
+ td.setAttribute('role', 'gridcell');
135
+ td.title = 'Double-cliquez pour interagir';
136
+ td.textContent = displayValue(cell);
137
+
138
+ const handler = (() => {
139
+ return () => {
140
+ container.dispatchEvent(
141
+ new CustomEvent('widget:interact', {
142
+ detail: {
143
+ action: 'cellclick',
144
+ payload: { row: ri, col: ci, value: cell }
145
+ },
146
+ bubbles: true
147
+ })
148
+ );
149
+ };
150
+ })();
151
+ td.addEventListener('dblclick', handler);
152
+ listeners.push({ el: td, handler: handler as EventListener });
153
+
154
+ tr.appendChild(td);
155
+ });
156
+ tbody.appendChild(tr);
157
+ });
158
+
159
+ table.appendChild(tbody);
160
+ scroll.appendChild(table);
161
+ root.appendChild(scroll);
162
+ container.appendChild(root);
163
+
164
+ return () => {
165
+ for (const { el, handler } of listeners) {
166
+ el.removeEventListener('dblclick', handler);
167
+ }
168
+ listeners.length = 0;
169
+ container.innerHTML = '';
170
+ };
171
+ }
172
+
173
+ export default { render };
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Vanilla renderer for the Hemicycle widget.
3
+ *
4
+ * Geometric calculations are preserved 1:1 from Hemicycle.svelte:
5
+ * - viewBox 420x230, cx=W/2, cy=H-10
6
+ * - rings: radii = rMin + i*step (rMin=60, step=28)
7
+ * - seats-per-row proportional to ring circumference (Math.PI*r)
8
+ * - groups sorted ascending by seat count, colors filled left->right
9
+ * - per-row angle: Math.PI - (j/(n-1||1))*Math.PI
10
+ * - seat position: cx + r*cos(angle), cy - r*sin(angle)
11
+ *
12
+ * Contract:
13
+ * export function render(container, data): () => void
14
+ * data = spec (HemicycleSpec)
15
+ *
16
+ * Interactions:
17
+ * - mouseenter/mouseleave on seats -> tooltip
18
+ * - dblclick on seat or legend item -> 'widget:interact' CustomEvent
19
+ * with detail { action: 'groupclick', payload: group }
20
+ * - keydown Enter on legend item -> same event
21
+ */
22
+
23
+ export interface HemicycleGroup {
24
+ id: string;
25
+ label: string;
26
+ seats: number;
27
+ color: string;
28
+ }
29
+
30
+ export interface HemicycleSpec {
31
+ title?: string;
32
+ groups?: HemicycleGroup[];
33
+ totalSeats?: number;
34
+ rows?: number;
35
+ }
36
+
37
+ interface Seat {
38
+ x: number;
39
+ y: number;
40
+ color: string;
41
+ gid: string;
42
+ }
43
+
44
+ const SVG_NS = 'http://www.w3.org/2000/svg';
45
+
46
+ function dispatchGroupClick(container: HTMLElement, group: HemicycleGroup): void {
47
+ container.dispatchEvent(
48
+ new CustomEvent('widget:interact', {
49
+ detail: { action: 'groupclick', payload: group },
50
+ bubbles: true,
51
+ }),
52
+ );
53
+ }
54
+
55
+ function computeSeats(
56
+ groups: HemicycleGroup[],
57
+ total: number,
58
+ rows: number,
59
+ cx: number,
60
+ cy: number,
61
+ rMin: number,
62
+ step: number,
63
+ ): Seat[] {
64
+ if (!groups.length || !total) return [];
65
+ const radii = Array.from({ length: rows }, (_, i) => rMin + i * step);
66
+ const circs = radii.map((r) => Math.PI * r);
67
+ const totalC = circs.reduce((a, b) => a + b, 0);
68
+ const spr = radii.map((r) => Math.round((Math.PI * r) / totalC * total));
69
+ spr[spr.length - 1] += total - spr.reduce((a, b) => a + b, 0);
70
+ const sorted = [...groups].sort((a, b) => a.seats - b.seats);
71
+ const colors: { color: string; gid: string }[] = [];
72
+ for (const g of sorted) for (let i = 0; i < g.seats; i++) colors.push({ color: g.color, gid: g.id });
73
+ while (colors.length < total) colors.push({ color: '#333355', gid: '' });
74
+ const result: Seat[] = [];
75
+ let idx = 0;
76
+ for (let row = 0; row < rows; row++) {
77
+ const r = radii[row];
78
+ const n = spr[row];
79
+ for (let j = 0; j < n; j++) {
80
+ if (idx >= colors.length) break;
81
+ const angle = Math.PI - (j / (n - 1 || 1)) * Math.PI;
82
+ result.push({
83
+ x: cx + r * Math.cos(angle),
84
+ y: cy - r * Math.sin(angle),
85
+ ...colors[idx++],
86
+ });
87
+ }
88
+ }
89
+ return result;
90
+ }
91
+
92
+ export function render(container: HTMLElement, data: any): () => void {
93
+ const spec: Partial<HemicycleSpec> = (data ?? {}) as Partial<HemicycleSpec>;
94
+ const groups: HemicycleGroup[] = Array.isArray(spec.groups) ? spec.groups : [];
95
+ const total = spec.totalSeats ?? groups.reduce((s, g) => s + g.seats, 0);
96
+
97
+ const W = 420;
98
+ const H = 230;
99
+ const cx = W / 2;
100
+ const cy = H - 10;
101
+ const rMin = 60;
102
+ const step = 28;
103
+ const rows = spec.rows ?? Math.min(Math.max(3, Math.ceil(Math.sqrt(total / 6))), 7);
104
+ const rMax = rMin + rows * step;
105
+
106
+ // Cleanup any prior content
107
+ container.innerHTML = '';
108
+
109
+ const cleanups: Array<() => void> = [];
110
+ const addListener = <K extends keyof HTMLElementEventMap>(
111
+ el: Element,
112
+ type: string,
113
+ handler: EventListener,
114
+ ) => {
115
+ el.addEventListener(type, handler);
116
+ cleanups.push(() => el.removeEventListener(type, handler));
117
+ };
118
+
119
+ // Root card
120
+ const root = document.createElement('div');
121
+ root.className = 'bg-surface border border-border rounded-lg p-3 md:p-4 font-sans';
122
+
123
+ if (spec.title) {
124
+ const h3 = document.createElement('h3');
125
+ h3.className = 'text-sm font-semibold text-text1 mb-3';
126
+ h3.textContent = spec.title;
127
+ root.appendChild(h3);
128
+ }
129
+
130
+ if (!groups.length || !total) {
131
+ const p = document.createElement('p');
132
+ p.className = 'text-text2 text-sm';
133
+ p.textContent = 'No data';
134
+ root.appendChild(p);
135
+ container.appendChild(root);
136
+ return () => {
137
+ for (const c of cleanups) c();
138
+ container.innerHTML = '';
139
+ };
140
+ }
141
+
142
+ const seats = computeSeats(groups, total, rows, cx, cy, rMin, step);
143
+ const legend = [...groups].sort((a, b) => b.seats - a.seats);
144
+
145
+ // SVG container (relative wrapper for tooltip positioning)
146
+ const svgWrap = document.createElement('div');
147
+ svgWrap.className = 'relative';
148
+
149
+ const svg = document.createElementNS(SVG_NS, 'svg');
150
+ svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
151
+ svg.setAttribute('xmlns', SVG_NS);
152
+ svg.setAttribute('class', 'block w-full max-h-[220px]');
153
+
154
+ // Backdrop arc
155
+ const arc = document.createElementNS(SVG_NS, 'path');
156
+ arc.setAttribute(
157
+ 'd',
158
+ `M ${cx - rMax - 15} ${cy} A ${rMax + 15} ${rMax + 15} 0 0 1 ${cx + rMax + 15} ${cy}`,
159
+ );
160
+ arc.setAttribute('fill', 'none');
161
+ arc.setAttribute('stroke', 'var(--color-surface2)');
162
+ arc.setAttribute('stroke-width', '2');
163
+ svg.appendChild(arc);
164
+
165
+ // Tooltip DOM (created eagerly, toggled via hidden)
166
+ const tooltip = document.createElement('div');
167
+ tooltip.className =
168
+ 'absolute top-0 right-0 bg-surface2 border border-border2 rounded px-2 py-1 text-xs text-text1 pointer-events-none';
169
+ tooltip.style.display = 'none';
170
+ const tooltipLabel = document.createElement('span');
171
+ tooltipLabel.className = 'font-semibold';
172
+ const tooltipSep = document.createTextNode(' — ');
173
+ const tooltipSeats = document.createTextNode('');
174
+ tooltip.appendChild(tooltipLabel);
175
+ tooltip.appendChild(tooltipSep);
176
+ tooltip.appendChild(tooltipSeats);
177
+
178
+ const showTooltip = (gid: string) => {
179
+ const g = groups.find((x) => x.id === gid);
180
+ if (!g) return;
181
+ tooltipLabel.textContent = g.label;
182
+ tooltipSeats.textContent = String(g.seats);
183
+ tooltip.style.display = '';
184
+ };
185
+ const hideTooltip = () => {
186
+ tooltip.style.display = 'none';
187
+ };
188
+
189
+ // Seats
190
+ for (const s of seats) {
191
+ const c = document.createElementNS(SVG_NS, 'circle');
192
+ c.setAttribute('cx', s.x.toFixed(1));
193
+ c.setAttribute('cy', s.y.toFixed(1));
194
+ c.setAttribute('r', '4');
195
+ c.setAttribute('fill', s.color);
196
+ c.setAttribute('opacity', '0.9');
197
+ if (s.gid) {
198
+ c.setAttribute('class', 'cursor-pointer');
199
+ const title = document.createElementNS(SVG_NS, 'title');
200
+ title.textContent = 'Double-click to interact';
201
+ c.appendChild(title);
202
+
203
+ const gid = s.gid;
204
+ const onEnter = () => showTooltip(gid);
205
+ const onLeave = () => hideTooltip();
206
+ const onDbl = () => {
207
+ const g = groups.find((x) => x.id === gid);
208
+ if (g) dispatchGroupClick(container, g);
209
+ };
210
+ addListener(c, 'mouseenter', onEnter);
211
+ addListener(c, 'mouseleave', onLeave);
212
+ addListener(c, 'dblclick', onDbl);
213
+ }
214
+ svg.appendChild(c);
215
+ }
216
+
217
+ // Total seats label
218
+ const label = document.createElementNS(SVG_NS, 'text');
219
+ label.setAttribute('x', String(cx));
220
+ label.setAttribute('y', String(cy + 18));
221
+ label.setAttribute('text-anchor', 'middle');
222
+ label.setAttribute('font-size', '11');
223
+ label.setAttribute('fill', 'var(--color-text2)');
224
+ label.setAttribute('font-family', 'system-ui');
225
+ label.textContent = `${total} seats`;
226
+ svg.appendChild(label);
227
+
228
+ svgWrap.appendChild(svg);
229
+ svgWrap.appendChild(tooltip);
230
+ root.appendChild(svgWrap);
231
+
232
+ // Legend
233
+ const legendWrap = document.createElement('div');
234
+ legendWrap.className = 'flex flex-wrap gap-x-4 gap-y-1 mt-3';
235
+ for (const g of legend) {
236
+ const item = document.createElement('div');
237
+ item.className = 'flex items-center gap-1.5 text-xs cursor-pointer hover:opacity-80';
238
+ item.setAttribute('role', 'button');
239
+ item.setAttribute('tabindex', '0');
240
+ item.setAttribute('title', 'Double-click to interact');
241
+
242
+ const swatch = document.createElement('div');
243
+ swatch.className = 'w-3 h-3 rounded-full flex-shrink-0';
244
+ swatch.style.background = g.color;
245
+
246
+ const labelEl = document.createElement('span');
247
+ labelEl.className = 'text-text2';
248
+ labelEl.textContent = g.label;
249
+
250
+ const seatsEl = document.createElement('span');
251
+ seatsEl.className = 'text-text2';
252
+ seatsEl.textContent = String(g.seats);
253
+
254
+ item.appendChild(swatch);
255
+ item.appendChild(labelEl);
256
+ item.appendChild(seatsEl);
257
+
258
+ const onDbl = () => dispatchGroupClick(container, g);
259
+ const onKey = (e: Event) => {
260
+ const ke = e as KeyboardEvent;
261
+ if (ke.key === 'Enter') dispatchGroupClick(container, g);
262
+ };
263
+ addListener(item, 'dblclick', onDbl);
264
+ addListener(item, 'keydown', onKey);
265
+
266
+ legendWrap.appendChild(item);
267
+ }
268
+ root.appendChild(legendWrap);
269
+
270
+ container.appendChild(root);
271
+
272
+ // High-res SVG -> PNG export hook
273
+ (container as any).__exportPng = async (scale = 2): Promise<string | null> => {
274
+ try {
275
+ const svgEl = container.querySelector('svg');
276
+ if (!svgEl) return null;
277
+ const clone = svgEl.cloneNode(true) as SVGSVGElement;
278
+ clone.setAttribute('xmlns', SVG_NS);
279
+ if (!clone.getAttribute('width')) clone.setAttribute('width', String(W));
280
+ if (!clone.getAttribute('height')) clone.setAttribute('height', String(H));
281
+ const xml = new XMLSerializer().serializeToString(clone);
282
+ const svg64 = btoa(unescape(encodeURIComponent(xml)));
283
+ const src = `data:image/svg+xml;base64,${svg64}`;
284
+ const img = new Image();
285
+ img.crossOrigin = 'anonymous';
286
+ await new Promise<void>((resolve, reject) => {
287
+ img.onload = () => resolve();
288
+ img.onerror = (e) => reject(e);
289
+ img.src = src;
290
+ });
291
+ const canvas = document.createElement('canvas');
292
+ canvas.width = Math.round(W * scale);
293
+ canvas.height = Math.round(H * scale);
294
+ const ctx = canvas.getContext('2d');
295
+ if (!ctx) return null;
296
+ ctx.scale(scale, scale);
297
+ ctx.drawImage(img, 0, 0, W, H);
298
+ return canvas.toDataURL('image/png');
299
+ } catch {
300
+ return null;
301
+ }
302
+ };
303
+
304
+ return () => {
305
+ for (const c of cleanups) c();
306
+ try {
307
+ delete (container as any).__exportPng;
308
+ } catch {
309
+ /* ignore */
310
+ }
311
+ container.innerHTML = '';
312
+ };
313
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * JsSandbox vanilla renderer.
3
+ *
4
+ * Renders an iframe sandbox that executes user-provided JS + HTML + CSS in
5
+ * isolation. Mirrors the Svelte version (JsSandbox.svelte) 1:1.
6
+ */
7
+
8
+ export interface JsSandboxSpec {
9
+ title?: string;
10
+ code: string;
11
+ html?: string;
12
+ css?: string;
13
+ height?: string;
14
+ }
15
+
16
+ export interface JsSandboxData {
17
+ spec: JsSandboxSpec;
18
+ }
19
+
20
+ function buildSrcdoc(spec: JsSandboxSpec): string {
21
+ const css = spec.css ?? '';
22
+ const html = spec.html ?? '';
23
+ const code = spec.code ?? '';
24
+ // Neutral base: just reset box-sizing and remove body margin. NO default
25
+ // colours — sandboxed iframes can't inherit the host's CSS custom properties,
26
+ // so forcing a dark palette here would clash with user-provided `css` that
27
+ // assumes the browser default (white bg, black text).
28
+ return `<!DOCTYPE html>
29
+ <html>
30
+ <head>
31
+ <meta charset="utf-8">
32
+ <style>
33
+ *,*::before,*::after{box-sizing:border-box}
34
+ html,body{margin:0;padding:0}
35
+ body{padding:8px;font-family:system-ui,sans-serif;font-size:13px}
36
+ ${css}
37
+ </style>
38
+ </head>
39
+ <body>
40
+ <div id="root">${html}</div>
41
+ <script>
42
+ (function(){
43
+ try{
44
+ ${code}
45
+ }catch(e){
46
+ document.getElementById('root').innerHTML='<pre style="color:red;white-space:pre-wrap;padding:8px;margin:0">'+e+'</pre>';
47
+ }
48
+ })();
49
+ <\/script>
50
+ </body>
51
+ </html>`;
52
+ }
53
+
54
+ export function render(container: HTMLElement, data: JsSandboxData | JsSandboxSpec | undefined): () => void {
55
+ // Tolerate three shapes from callers:
56
+ // { spec: { code, html, css, ... } } — explicit spec wrapper
57
+ // { code, html, css, ... } — bare spec (widget_display params)
58
+ // undefined / {} — empty placeholder
59
+ const raw = data ?? {};
60
+ const spec: JsSandboxSpec = ('spec' in raw && raw.spec)
61
+ ? (raw as JsSandboxData).spec
62
+ : (raw as JsSandboxSpec);
63
+
64
+ // Outer wrapper mirrors the Svelte markup classes.
65
+ const wrapper = document.createElement('div');
66
+ wrapper.className = 'bg-surface border border-border rounded-lg overflow-hidden font-sans';
67
+
68
+ if (spec.title) {
69
+ const header = document.createElement('div');
70
+ header.className =
71
+ 'px-3 py-2 border-b border-border text-sm font-semibold text-text1 flex items-center gap-2';
72
+
73
+ const tag = document.createElement('span');
74
+ tag.className = 'text-xs opacity-50';
75
+ tag.textContent = 'JS';
76
+ header.appendChild(tag);
77
+
78
+ header.appendChild(document.createTextNode(' ' + spec.title));
79
+ wrapper.appendChild(header);
80
+ }
81
+
82
+ const iframe = document.createElement('iframe');
83
+ // Exact sandbox flags from the Svelte source: allow-scripts only.
84
+ iframe.setAttribute('sandbox', 'allow-scripts');
85
+ iframe.setAttribute('title', spec.title ?? 'JS Sandbox');
86
+ iframe.style.width = '100%';
87
+ iframe.style.height = spec.height ?? '300px';
88
+ iframe.style.border = 'none';
89
+ iframe.style.display = 'block';
90
+
91
+ // Only build/assign srcdoc when we have code (or html) — otherwise render an
92
+ // empty iframe placeholder. This matches the Svelte behaviour (srcdoc is
93
+ // always computed) while being a touch more defensive for empty specs.
94
+ if (spec && (spec.code || spec.html || spec.css)) {
95
+ iframe.srcdoc = buildSrcdoc(spec);
96
+ } else {
97
+ iframe.srcdoc = buildSrcdoc({
98
+ code: '',
99
+ html: '<div style="opacity:.7;font-family:system-ui;font-size:13px;padding:12px">⚠ js-sandbox: no code/html/css provided.</div>',
100
+ css: 'body{background:#f5f5f7;color:#333}',
101
+ });
102
+ }
103
+
104
+ wrapper.appendChild(iframe);
105
+ container.appendChild(wrapper);
106
+
107
+ return () => {
108
+ try {
109
+ // Blank the iframe first to stop any running scripts cleanly.
110
+ iframe.srcdoc = '';
111
+ } catch {
112
+ /* noop */
113
+ }
114
+ if (wrapper.parentNode === container) {
115
+ container.removeChild(wrapper);
116
+ } else {
117
+ container.innerHTML = '';
118
+ }
119
+ };
120
+ }
121
+
122
+ export default { render };