@webmcp-auto-ui/ui 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.
Files changed (82) hide show
  1. package/package.json +15 -3
  2. package/src/agent/DataServersPanel.svelte +164 -0
  3. package/src/agent/LLMSelector.svelte +11 -3
  4. package/src/agent/ModelCacheManager.svelte +359 -0
  5. package/src/index.ts +42 -30
  6. package/src/widgets/WidgetRenderer.svelte +114 -104
  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/compact.ts +823 -0
  12. package/src/widgets/notebook/document.ts +1065 -0
  13. package/src/widgets/notebook/editorial.ts +936 -0
  14. package/src/widgets/notebook/executors/.gitkeep +1 -0
  15. package/src/widgets/notebook/executors/index.ts +4 -0
  16. package/src/widgets/notebook/executors/js-worker.ts +269 -0
  17. package/src/widgets/notebook/executors/sql.ts +206 -0
  18. package/src/widgets/notebook/import-modals.ts +553 -0
  19. package/src/widgets/notebook/left-pane.ts +249 -0
  20. package/src/widgets/notebook/prose.ts +280 -0
  21. package/src/widgets/notebook/recipe-browser.ts +350 -0
  22. package/src/widgets/notebook/recipes/compact.md +124 -0
  23. package/src/widgets/notebook/recipes/document.md +139 -0
  24. package/src/widgets/notebook/recipes/editorial.md +120 -0
  25. package/src/widgets/notebook/recipes/workspace.md +119 -0
  26. package/src/widgets/notebook/resource-extractor.ts +162 -0
  27. package/src/widgets/notebook/share-handlers.ts +222 -0
  28. package/src/widgets/notebook/shared.ts +1592 -0
  29. package/src/widgets/notebook/workspace.ts +852 -0
  30. package/src/widgets/rich/cards.ts +181 -0
  31. package/src/widgets/rich/carousel.ts +319 -0
  32. package/src/widgets/rich/chart-rich.ts +386 -0
  33. package/src/widgets/rich/d3.ts +503 -0
  34. package/src/widgets/rich/data-table.ts +342 -0
  35. package/src/widgets/rich/gallery.ts +350 -0
  36. package/src/widgets/rich/grid-data.ts +173 -0
  37. package/src/widgets/rich/hemicycle.ts +313 -0
  38. package/src/widgets/rich/js-sandbox.ts +106 -0
  39. package/src/widgets/rich/json-viewer.ts +202 -0
  40. package/src/widgets/rich/log.ts +143 -0
  41. package/src/widgets/rich/map.ts +218 -0
  42. package/src/widgets/rich/profile.ts +256 -0
  43. package/src/widgets/rich/sankey.ts +262 -0
  44. package/src/widgets/rich/stat-card.ts +125 -0
  45. package/src/widgets/rich/timeline.ts +179 -0
  46. package/src/widgets/rich/trombinoscope.ts +246 -0
  47. package/src/widgets/simple/actions.ts +89 -0
  48. package/src/widgets/simple/alert.ts +100 -0
  49. package/src/widgets/simple/chart.ts +189 -0
  50. package/src/widgets/simple/code.ts +79 -0
  51. package/src/widgets/simple/kv.ts +68 -0
  52. package/src/widgets/simple/list.ts +89 -0
  53. package/src/widgets/simple/stat.ts +58 -0
  54. package/src/widgets/simple/tags.ts +125 -0
  55. package/src/widgets/simple/text.ts +198 -0
  56. package/src/widgets/SafeImage.svelte +0 -76
  57. package/src/widgets/rich/Cards.svelte +0 -39
  58. package/src/widgets/rich/Carousel.svelte +0 -88
  59. package/src/widgets/rich/Chart.svelte +0 -142
  60. package/src/widgets/rich/D3Widget.svelte +0 -378
  61. package/src/widgets/rich/DataTable.svelte +0 -62
  62. package/src/widgets/rich/Gallery.svelte +0 -94
  63. package/src/widgets/rich/GridData.svelte +0 -44
  64. package/src/widgets/rich/Hemicycle.svelte +0 -78
  65. package/src/widgets/rich/JsSandbox.svelte +0 -51
  66. package/src/widgets/rich/JsonViewer.svelte +0 -42
  67. package/src/widgets/rich/LogViewer.svelte +0 -24
  68. package/src/widgets/rich/MapView.svelte +0 -140
  69. package/src/widgets/rich/ProfileCard.svelte +0 -59
  70. package/src/widgets/rich/Sankey.svelte +0 -56
  71. package/src/widgets/rich/StatCard.svelte +0 -35
  72. package/src/widgets/rich/Timeline.svelte +0 -43
  73. package/src/widgets/rich/Trombinoscope.svelte +0 -48
  74. package/src/widgets/simple/ActionsBlock.svelte +0 -15
  75. package/src/widgets/simple/AlertBlock.svelte +0 -11
  76. package/src/widgets/simple/ChartBlock.svelte +0 -21
  77. package/src/widgets/simple/CodeBlock.svelte +0 -11
  78. package/src/widgets/simple/KVBlock.svelte +0 -16
  79. package/src/widgets/simple/ListBlock.svelte +0 -17
  80. package/src/widgets/simple/StatBlock.svelte +0 -14
  81. package/src/widgets/simple/TagsBlock.svelte +0 -15
  82. package/src/widgets/simple/TextBlock.svelte +0 -122
package/src/index.ts CHANGED
@@ -16,38 +16,48 @@ export { default as MarkdownView } from './primitives/MarkdownView.svelte';
16
16
  export { default as CodeView } from './primitives/CodeView.svelte';
17
17
  export { renderMarkdown, highlightCode, createMarkdownRenderer } from './primitives/markdown-renderer.js';
18
18
 
19
- // Simple widgets (PJ blocks)
20
- export { default as StatBlock } from './widgets/simple/StatBlock.svelte';
21
- export { default as KVBlock } from './widgets/simple/KVBlock.svelte';
22
- export { default as ListBlock } from './widgets/simple/ListBlock.svelte';
23
- export { default as ChartBlock } from './widgets/simple/ChartBlock.svelte';
24
- export { default as AlertBlock } from './widgets/simple/AlertBlock.svelte';
25
- export { default as CodeBlock } from './widgets/simple/CodeBlock.svelte';
26
- export { default as TextBlock } from './widgets/simple/TextBlock.svelte';
27
- export { default as ActionsBlock } from './widgets/simple/ActionsBlock.svelte';
28
- export { default as TagsBlock } from './widgets/simple/TagsBlock.svelte';
19
+ // Simple widgets (vanilla renderers) — contract: render(container, data): () => void
20
+ export { render as renderStat } from './widgets/simple/stat.js';
21
+ export { render as renderKv } from './widgets/simple/kv.js';
22
+ export { render as renderList } from './widgets/simple/list.js';
23
+ export { render as renderChart } from './widgets/simple/chart.js';
24
+ export { render as renderAlert } from './widgets/simple/alert.js';
25
+ export { render as renderCode } from './widgets/simple/code.js';
26
+ export { render as renderText } from './widgets/simple/text.js';
27
+ export { render as renderActions } from './widgets/simple/actions.js';
28
+ export { render as renderTags } from './widgets/simple/tags.js';
29
29
 
30
- // Rich widgets (Archive)
31
- export { default as StatCard } from './widgets/rich/StatCard.svelte';
32
- export { default as DataTable } from './widgets/rich/DataTable.svelte';
33
- export { default as Timeline } from './widgets/rich/Timeline.svelte';
34
- export { default as ProfileCard } from './widgets/rich/ProfileCard.svelte';
35
- export { default as Trombinoscope } from './widgets/rich/Trombinoscope.svelte';
36
- export { default as JsonViewer } from './widgets/rich/JsonViewer.svelte';
37
- export { default as Hemicycle } from './widgets/rich/Hemicycle.svelte';
38
- export { default as Chart } from './widgets/rich/Chart.svelte';
39
- export { default as Cards } from './widgets/rich/Cards.svelte';
40
- export { default as GridData } from './widgets/rich/GridData.svelte';
41
- export { default as Sankey } from './widgets/rich/Sankey.svelte';
42
- export { default as MapView } from './widgets/rich/MapView.svelte';
43
- export { default as D3Widget } from './widgets/rich/D3Widget.svelte';
44
- export { default as JsSandbox } from './widgets/rich/JsSandbox.svelte';
45
- export { default as LogViewer } from './widgets/rich/LogViewer.svelte';
46
- export { default as Gallery } from './widgets/rich/Gallery.svelte';
47
- export { default as Carousel } from './widgets/rich/Carousel.svelte';
30
+ // Rich widgets (vanilla renderers)
31
+ export { render as renderStatCard } from './widgets/rich/stat-card.js';
32
+ export { render as renderDataTable } from './widgets/rich/data-table.js';
33
+ export { render as renderTimeline } from './widgets/rich/timeline.js';
34
+ export { render as renderProfile } from './widgets/rich/profile.js';
35
+ export { render as renderTrombinoscope } from './widgets/rich/trombinoscope.js';
36
+ export { render as renderJsonViewer } from './widgets/rich/json-viewer.js';
37
+ export { render as renderHemicycle } from './widgets/rich/hemicycle.js';
38
+ export { render as renderChartRich } from './widgets/rich/chart-rich.js';
39
+ export { render as renderCards } from './widgets/rich/cards.js';
40
+ export { render as renderGridData } from './widgets/rich/grid-data.js';
41
+ export { render as renderSankey } from './widgets/rich/sankey.js';
42
+ export { render as renderMap } from './widgets/rich/map.js';
43
+ export { render as renderD3 } from './widgets/rich/d3.js';
44
+ export { render as renderJsSandbox } from './widgets/rich/js-sandbox.js';
45
+ export { render as renderLog } from './widgets/rich/log.js';
46
+ export { render as renderGallery } from './widgets/rich/gallery.js';
47
+ export { render as renderCarousel } from './widgets/rich/carousel.js';
48
48
 
49
- // Safe image (URL validation + error fallback)
50
- export { default as SafeImage } from './widgets/SafeImage.svelte';
49
+ // Notebook widget renderers (vanilla)
50
+ export { render as renderCompact } from './widgets/notebook/compact.js';
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';
54
+ export { render as renderRecipeBrowserWidget } from './widgets/notebook/recipe-browser.js';
55
+ // Notebook types (optional public API)
56
+ export type { NotebookState, NotebookCell } from './widgets/notebook/shared.js';
57
+
58
+ // Safe image helper (URL validation + error fallback)
59
+ export { createSafeImage } from './widgets/helpers/safe-image.js';
60
+ export type { SafeImageOptions } from './widgets/helpers/safe-image.js';
51
61
 
52
62
  // Widget export utility
53
63
  export { exportWidget, getExportFormats, exportWidgetAs } from './widgets/export-widget.js';
@@ -90,6 +100,7 @@ export { default as ModelLoader } from './agent/ModelLoader.svelte';
90
100
  /** @deprecated Use ModelLoader instead. Alias maintained for backward compatibility. */
91
101
  export { default as GemmaLoader } from './agent/ModelLoader.svelte';
92
102
  export { default as McpStatus } from './agent/McpStatus.svelte';
103
+ export { default as ModelCacheManager } from './agent/ModelCacheManager.svelte';
93
104
  export { default as AgentProgress } from './agent/AgentProgress.svelte';
94
105
  export { default as McpConnector } from './agent/McpConnector.svelte';
95
106
  export { default as ChatPanel } from './agent/ChatPanel.svelte';
@@ -98,6 +109,7 @@ export { default as AgentConsole } from './agent/AgentConsole.svelte';
98
109
  export { default as SettingsPanel } from './agent/SettingsPanel.svelte';
99
110
  export { default as RemoteMCPserversDemo } from './agent/RemoteMCPserversDemo.svelte';
100
111
  export { default as WebMCPserversList } from './agent/WebMCPserversList.svelte';
112
+ export { default as DataServersPanel } from './agent/DataServersPanel.svelte';
101
113
  export { default as EphemeralBubble } from './agent/EphemeralBubble.svelte';
102
114
  export { default as TokenBubble } from './agent/TokenBubble.svelte';
103
115
  export { default as DiagnosticModal } from './agent/DiagnosticModal.svelte';
@@ -2,95 +2,94 @@
2
2
  import { type Component, onMount, onDestroy } from 'svelte';
3
3
  import type { WebMcpServer } from '@webmcp-auto-ui/core';
4
4
  import { bus } from '../messaging/bus.svelte.js';
5
- // Simple widgets
6
- import StatBlock from './simple/StatBlock.svelte';
7
- import KVBlock from './simple/KVBlock.svelte';
8
- import ListBlock from './simple/ListBlock.svelte';
9
- import ChartBlock from './simple/ChartBlock.svelte';
10
- import AlertBlock from './simple/AlertBlock.svelte';
11
- import CodeBlock from './simple/CodeBlock.svelte';
12
- import TextBlock from './simple/TextBlock.svelte';
13
- import ActionsBlock from './simple/ActionsBlock.svelte';
14
- import TagsBlock from './simple/TagsBlock.svelte';
15
- // Rich widgets
16
- import StatCard from './rich/StatCard.svelte';
17
- import DataTable from './rich/DataTable.svelte';
18
- import Timeline from './rich/Timeline.svelte';
19
- import ProfileCard from './rich/ProfileCard.svelte';
20
- import Trombinoscope from './rich/Trombinoscope.svelte';
21
- import JsonViewer from './rich/JsonViewer.svelte';
22
- import Hemicycle from './rich/Hemicycle.svelte';
23
- import Chart from './rich/Chart.svelte';
24
- import Cards from './rich/Cards.svelte';
25
- import GridData from './rich/GridData.svelte';
26
- import Sankey from './rich/Sankey.svelte';
27
- import MapView from './rich/MapView.svelte';
28
- import D3Widget from './rich/D3Widget.svelte';
29
- import JsSandbox from './rich/JsSandbox.svelte';
30
- import LogViewer from './rich/LogViewer.svelte';
31
- import Gallery from './rich/Gallery.svelte';
32
- import Carousel from './rich/Carousel.svelte';
33
-
34
- type Emit = (action: string, payload: unknown) => void;
35
5
 
36
- /** Native widget entry: component + prop builder */
37
- type NativeEntry = {
38
- component: Component<any>;
39
- props: (data: Record<string, unknown>, emit: Emit) => Record<string, unknown>;
40
- };
6
+ // Monotonic counter for busId generation avoids collisions on sub-ms remounts.
7
+ let busIdCounter = 0;
8
+ function makeBusId(widgetType: string): string {
9
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
10
+ return `block_${widgetType}_${crypto.randomUUID()}`;
11
+ }
12
+ return `block_${widgetType}_${Date.now()}-${++busIdCounter}`;
13
+ }
41
14
 
42
- /** Simple helper: passes { data } */
43
- const d = (c: Component<any>): NativeEntry => ({
44
- component: c,
45
- props: (data) => ({ data }),
46
- });
15
+ // Safe clone for data passed to vanilla renderers — strips Svelte 5 $state
16
+ // proxies without crashing on BigInt / circular refs / undefined values.
17
+ function safeClone<T>(value: T): T {
18
+ if (value === null || typeof value !== 'object') return value;
19
+ try {
20
+ return structuredClone(value);
21
+ } catch (err) {
22
+ console.warn('[WidgetRenderer] structuredClone failed, falling back to shallow copy', err);
23
+ return { ...(value as Record<string, unknown>) } as T;
24
+ }
25
+ }
47
26
 
48
- /** Spec helper: passes { spec } */
49
- const s = (c: Component<any>): NativeEntry => ({
50
- component: c,
51
- props: (data) => ({ spec: data }),
52
- });
27
+ // ── Native vanilla renderers ─────────────────────────────────────────────
28
+ // Simple widgets
29
+ import { render as renderStat } from './simple/stat.js';
30
+ import { render as renderKv } from './simple/kv.js';
31
+ import { render as renderList } from './simple/list.js';
32
+ import { render as renderChart } from './simple/chart.js';
33
+ import { render as renderAlert } from './simple/alert.js';
34
+ import { render as renderCode } from './simple/code.js';
35
+ import { render as renderText } from './simple/text.js';
36
+ import { render as renderActions } from './simple/actions.js';
37
+ import { render as renderTags } from './simple/tags.js';
38
+ // Rich widgets
39
+ import { render as renderStatCard } from './rich/stat-card.js';
40
+ import { render as renderProfile } from './rich/profile.js';
41
+ import { render as renderJsonViewer } from './rich/json-viewer.js';
42
+ import { render as renderChartRich } from './rich/chart-rich.js';
43
+ import { render as renderSankey } from './rich/sankey.js';
44
+ import { render as renderHemicycle } from './rich/hemicycle.js';
45
+ import { render as renderDataTable } from './rich/data-table.js';
46
+ import { render as renderTimeline } from './rich/timeline.js';
47
+ import { render as renderTrombinoscope } from './rich/trombinoscope.js';
48
+ import { render as renderCards } from './rich/cards.js';
49
+ import { render as renderGridData } from './rich/grid-data.js';
50
+ import { render as renderMap } from './rich/map.js';
51
+ import { render as renderD3 } from './rich/d3.js';
52
+ import { render as renderJsSandbox } from './rich/js-sandbox.js';
53
+ import { render as renderLog } from './rich/log.js';
54
+ import { render as renderGallery } from './rich/gallery.js';
55
+ import { render as renderCarousel } from './rich/carousel.js';
53
56
 
54
- /** Spec + events helper */
55
- const se = (
56
- c: Component<any>,
57
- events: (emit: Emit) => Record<string, unknown>,
58
- ): NativeEntry => ({
59
- component: c,
60
- props: (data, emit) => ({ spec: data, ...events(emit) }),
61
- });
57
+ /** A vanilla renderer: returns cleanup or Promise thereof. */
58
+ type VanillaRenderer = (
59
+ container: HTMLElement,
60
+ data: Record<string, unknown>,
61
+ ) => void | (() => void) | Promise<void | (() => void)>;
62
62
 
63
- /** Static map of all native widget types → component + props */
64
- const NATIVE_MAP: Record<string, NativeEntry> = {
65
- // Simple widgets (data prop)
66
- 'stat': d(StatBlock),
67
- 'kv': d(KVBlock),
68
- 'list': { component: ListBlock, props: (data, emit) => ({ data, onitemclick: (item: unknown, index: unknown) => emit('itemclick', { item, index }) }) },
69
- 'chart': d(ChartBlock),
70
- 'alert': d(AlertBlock),
71
- 'code': d(CodeBlock),
72
- 'text': d(TextBlock),
73
- 'actions': d(ActionsBlock),
74
- 'tags': d(TagsBlock),
75
- // Rich widgets (spec prop)
76
- 'stat-card': s(StatCard),
77
- 'profile': s(ProfileCard),
78
- 'json-viewer': s(JsonViewer),
79
- 'chart-rich': s(Chart),
80
- 'sankey': s(Sankey),
81
- 'map': s(MapView),
82
- 'd3': s(D3Widget),
83
- 'js-sandbox': s(JsSandbox),
84
- 'log': s(LogViewer),
85
- // Rich widgets (spec prop + event callbacks)
86
- 'data-table': se(DataTable, (emit) => ({ onrowclick: (row: unknown) => emit('rowclick', row) })),
87
- 'timeline': se(Timeline, (emit) => ({ oneventclick: (e: unknown) => emit('eventclick', e) })),
88
- 'trombinoscope': se(Trombinoscope, (emit) => ({ onpersonclick: (p: unknown) => emit('personclick', p) })),
89
- 'hemicycle': se(Hemicycle, (emit) => ({ ongroupclick: (g: unknown) => emit('groupclick', g) })),
90
- 'cards': se(Cards, (emit) => ({ oncardclick: (c: unknown) => emit('cardclick', c) })),
91
- 'grid-data': se(GridData, (emit) => ({ oncellclick: (r: unknown, c: unknown, v: unknown) => emit('cellclick', { row: r, col: c, value: v }) })),
92
- 'gallery': se(Gallery, (emit) => ({ onimageclick: (img: unknown, i: unknown) => emit('imageclick', { image: img, index: i }) })),
93
- 'carousel': se(Carousel, (emit) => ({ onslidechange: (slide: unknown, i: unknown) => emit('slidechange', { slide, index: i }) })),
63
+ /** Static map of all native widget types → vanilla renderer */
64
+ const NATIVE_VANILLA_MAP: Record<string, VanillaRenderer> = {
65
+ // Simple
66
+ 'stat': renderStat,
67
+ 'kv': renderKv,
68
+ 'list': renderList,
69
+ 'chart': renderChart,
70
+ 'alert': renderAlert,
71
+ 'code': renderCode,
72
+ 'text': renderText,
73
+ 'actions': renderActions,
74
+ 'tags': renderTags,
75
+ // Rich
76
+ 'stat-card': renderStatCard,
77
+ 'profile': renderProfile,
78
+ 'json-viewer': renderJsonViewer,
79
+ 'chart-rich': renderChartRich,
80
+ 'sankey': renderSankey,
81
+ 'hemicycle': renderHemicycle,
82
+ 'data-table': renderDataTable,
83
+ 'timeline': renderTimeline,
84
+ 'trombinoscope': renderTrombinoscope,
85
+ 'cards': renderCards,
86
+ 'grid-data': renderGridData,
87
+ 'map': renderMap,
88
+ 'd3': renderD3,
89
+ 'js-sandbox': renderJsSandbox,
90
+ 'log': renderLog,
91
+ 'gallery': renderGallery,
92
+ 'carousel': renderCarousel,
94
93
  };
95
94
 
96
95
  interface Props {
@@ -103,7 +102,7 @@
103
102
  let { id, type, data, servers, oninteract }: Props = $props();
104
103
 
105
104
  // Auto-register on the FONC message bus
106
- const busId = id ?? `block_${type}_${Date.now().toString(36)}`;
105
+ const busId = id ?? makeBusId(type);
107
106
  const unregisterBus = bus.register(busId, type, ['data-update', 'interact', '*'], (msg) => {
108
107
  if (msg.channel === 'data-update' && msg.payload && typeof msg.payload === 'object') {
109
108
  oninteract?.(type, 'bus-update', msg.payload);
@@ -116,7 +115,7 @@
116
115
  bus.broadcast(busId, 'interact', { type, action, payload });
117
116
  }
118
117
 
119
- // ── Renderer resolution: servers > native > fallback ────────────
118
+ // ── Renderer resolution: servers > native vanilla > fallback ────────────
120
119
 
121
120
  // Look up a custom widget entry from connected WebMCP servers
122
121
  const customWidgetEntry = $derived.by(() => {
@@ -129,36 +128,48 @@
129
128
  });
130
129
 
131
130
  const customRenderer = $derived(customWidgetEntry?.renderer ?? null);
131
+ const isCustomVanilla = $derived(customWidgetEntry?.vanilla === true);
132
132
 
133
- /** True when the custom renderer is a vanilla function (not a Svelte component) */
134
- const isVanillaRenderer = $derived(customWidgetEntry?.vanilla === true);
133
+ const nativeVanillaRenderer = $derived<VanillaRenderer | undefined>(
134
+ customRenderer ? undefined : NATIVE_VANILLA_MAP[type],
135
+ );
136
+
137
+ /** True when a vanilla renderer (custom or native) should be used */
138
+ const useVanilla = $derived(isCustomVanilla || !!nativeVanillaRenderer);
135
139
 
136
- const nativeEntry: NativeEntry | undefined = $derived(
137
- customRenderer ? undefined : NATIVE_MAP[type],
140
+ /** The effective vanilla renderer to invoke */
141
+ const vanillaRenderer = $derived<VanillaRenderer | null>(
142
+ isCustomVanilla
143
+ ? (customRenderer as VanillaRenderer)
144
+ : (nativeVanillaRenderer ?? null),
138
145
  );
139
146
 
140
- // Deep-clone data to strip Svelte 5 $state proxies — only needed for vanilla
141
- // renderers whose third-party libs (Cytoscape, Plotly, etc.) use
142
- // Object.defineProperty which conflicts with Svelte 5 proxies.
143
- // Native Svelte components handle proxied data fine and benefit from
144
- // fine-grained reactivity, so they receive raw `data` directly.
145
- const plainData: Record<string, unknown> = $derived(JSON.parse(JSON.stringify(data)));
147
+ // Deep-clone data to strip Svelte 5 $state proxies — vanilla renderers + third-party
148
+ // libs (D3, Leaflet, etc.) rely on Object.defineProperty which conflicts with proxies.
149
+ const plainData: Record<string, unknown> = $derived(safeClone(data) as Record<string, unknown>);
146
150
 
147
151
  // ── Vanilla renderer container + lifecycle ────────────
148
152
  let vanillaContainer: HTMLElement | undefined = $state(undefined);
149
153
 
150
154
  $effect(() => {
151
- if (!isVanillaRenderer || !vanillaContainer) return;
155
+ if (!useVanilla || !vanillaRenderer || !vanillaContainer) return;
156
+ const container = vanillaContainer;
152
157
  // Clear previous content
153
- vanillaContainer.innerHTML = '';
158
+ container.innerHTML = '';
159
+
160
+ // Listen for the standard vanilla event contract: CustomEvent('widget:interact')
161
+ const onInteract = (ev: Event) => {
162
+ const ce = ev as CustomEvent<{ action?: string; payload?: unknown }>;
163
+ const action = ce.detail?.action ?? 'interact';
164
+ emit(action, ce.detail?.payload);
165
+ };
166
+ container.addEventListener('widget:interact', onInteract);
154
167
 
155
168
  let cleanup: (() => void) | void;
156
169
  let cancelled = false;
157
170
 
158
171
  try {
159
- const result = (customRenderer as (container: HTMLElement, data: Record<string, unknown>) => void | (() => void) | Promise<void | (() => void)>)(
160
- vanillaContainer, plainData,
161
- );
172
+ const result = vanillaRenderer(container, plainData);
162
173
  if (result && typeof (result as Promise<unknown>).then === 'function') {
163
174
  (result as Promise<void | (() => void)>).then(
164
175
  (c) => { if (!cancelled) cleanup = c ?? undefined; },
@@ -172,6 +183,7 @@
172
183
 
173
184
  return () => {
174
185
  cancelled = true;
186
+ container.removeEventListener('widget:interact', onInteract);
175
187
  if (typeof cleanup === 'function') {
176
188
  try { cleanup(); } catch (err) { console.error('[WidgetRenderer] cleanup failed:', err); }
177
189
  }
@@ -238,12 +250,10 @@
238
250
  });
239
251
  </script>
240
252
 
241
- {#if isVanillaRenderer}
253
+ {#if useVanilla}
242
254
  <div bind:this={vanillaContainer} class="vanilla-container w-full h-full overflow-auto p-2"></div>
243
255
  {:else if customRenderer}
244
256
  <svelte:component this={customRenderer as Component<any>} {data} {id} />
245
- {:else if nativeEntry}
246
- <svelte:component this={nativeEntry.component} {...nativeEntry.props(data, emit)} />
247
257
  {:else}
248
258
  <div class="p-3 font-mono text-xs text-text2">[{type}]</div>
249
259
  {/if}
@@ -5,7 +5,15 @@
5
5
 
6
6
  import { toPng } from 'html-to-image';
7
7
 
8
- const TARGET_PNG_WIDTH = 2048;
8
+ const TARGET_PNG_WIDTH = 4096;
9
+
10
+ export type WidgetExportHook = (scale: number) => Promise<Blob>;
11
+
12
+ declare global {
13
+ interface HTMLElement {
14
+ __exportPng?: WidgetExportHook;
15
+ }
16
+ }
9
17
 
10
18
  // ── helpers ──────────────────────────────────────────────────────────────────
11
19
 
@@ -95,6 +103,25 @@ async function exportPng(type: string, containerEl: HTMLElement): Promise<void>
95
103
  const scrollH = Math.max(containerEl.scrollHeight, containerEl.clientHeight, 1);
96
104
  const pixelRatio = TARGET_PNG_WIDTH / scrollW;
97
105
 
106
+ // Canvas-based widgets (cytoscape, etc.) expose a native exporter via __exportPng.
107
+ // html-to-image would only recapture the already-rasterised on-screen pixels,
108
+ // so we route to the widget's own resolution-independent exporter when available.
109
+ if (typeof containerEl.__exportPng === 'function') {
110
+ const scale = Math.max(2, Math.ceil(TARGET_PNG_WIDTH / scrollW));
111
+ try {
112
+ const blob = await containerEl.__exportPng(scale);
113
+ const url = URL.createObjectURL(blob);
114
+ const a = document.createElement('a');
115
+ a.href = url;
116
+ a.download = name;
117
+ a.click();
118
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
119
+ return;
120
+ } catch (err) {
121
+ console.error('[exportWidget] native __exportPng failed, falling back to html-to-image', err);
122
+ }
123
+ }
124
+
98
125
  const bg = (typeof document !== 'undefined'
99
126
  ? getComputedStyle(document.documentElement).getPropertyValue('--color-surface').trim()
100
127
  : '') || '#ffffff';
@@ -0,0 +1,78 @@
1
+ /**
2
+ * SafeImage (vanilla) — URL validation, error fallback, placeholder rendering.
3
+ * Replaces SafeImage.svelte. Used by Profile/Cards/Carousel/Gallery/Trombinoscope renderers.
4
+ */
5
+
6
+ export interface SafeImageOptions {
7
+ src: string | undefined | null;
8
+ alt?: string;
9
+ className?: string;
10
+ style?: string;
11
+ loading?: 'lazy' | 'eager';
12
+ fallbackText?: string;
13
+ hideOnError?: boolean;
14
+ }
15
+
16
+ const VALID_PREFIXES = ['http://', 'https://', 'data:', '/'];
17
+
18
+ function isValidSrc(src: unknown): src is string {
19
+ return typeof src === 'string' && src.length > 0 && VALID_PREFIXES.some((p) => (src as string).startsWith(p));
20
+ }
21
+
22
+ function escapeHtml(s: string): string {
23
+ return s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]!));
24
+ }
25
+
26
+ function buildPlaceholder(label: string, opts: SafeImageOptions): HTMLElement {
27
+ const div = document.createElement('div');
28
+ div.className = `flex items-center justify-center bg-surface2 text-text2 text-xs ${opts.className ?? ''}`.trim();
29
+ if (opts.style) div.setAttribute('style', opts.style);
30
+ div.setAttribute('role', 'img');
31
+ div.setAttribute('aria-label', label);
32
+ div.innerHTML =
33
+ '<svg class="w-5 h-5 opacity-40 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">' +
34
+ '<rect x="3" y="3" width="18" height="18" rx="2"/>' +
35
+ '<circle cx="8.5" cy="8.5" r="1.5"/>' +
36
+ '<path d="M21 15l-5-5L5 21"/></svg>' +
37
+ `<span class="truncate max-w-[80%]">${escapeHtml(label)}</span>`;
38
+ return div;
39
+ }
40
+
41
+ function buildHidden(): HTMLElement {
42
+ const el = document.createElement('div');
43
+ el.style.display = 'none';
44
+ el.setAttribute('aria-hidden', 'true');
45
+ return el;
46
+ }
47
+
48
+ /**
49
+ * Returns an HTMLElement for the given image source.
50
+ * - Invalid/missing URL → placeholder (or hidden element if hideOnError).
51
+ * - Valid URL → <img> that self-replaces with placeholder on load error.
52
+ */
53
+ export function createSafeImage(opts: SafeImageOptions): HTMLElement {
54
+ const label = opts.fallbackText ?? opts.alt ?? 'Image';
55
+
56
+ if (!isValidSrc(opts.src)) {
57
+ return opts.hideOnError ? buildHidden() : buildPlaceholder(label, opts);
58
+ }
59
+
60
+ const img = document.createElement('img');
61
+ img.src = opts.src;
62
+ img.alt = opts.alt ?? '';
63
+ if (opts.className) img.className = opts.className;
64
+ if (opts.style) img.setAttribute('style', opts.style);
65
+ img.loading = opts.loading ?? 'lazy';
66
+ img.referrerPolicy = 'no-referrer';
67
+
68
+ img.addEventListener(
69
+ 'error',
70
+ () => {
71
+ const replacement = opts.hideOnError ? buildHidden() : buildPlaceholder(label, opts);
72
+ img.replaceWith(replacement);
73
+ },
74
+ { once: true },
75
+ );
76
+
77
+ return img;
78
+ }
File without changes
@@ -0,0 +1,63 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Chart renderer — Vega-Lite via lazy dynamic import of vega-embed.
3
+ //
4
+ // Design notes:
5
+ // - vega-embed is imported dynamically so it does not bloat the initial
6
+ // bundle. The first chart render pays a one-time cost; subsequent charts
7
+ // share the cached module.
8
+ // - On failure (missing dep, invalid spec, CSP denies eval, etc.) we fall
9
+ // back to a readable <pre>JSON</pre> preview of the spec so the user
10
+ // still sees *something* rather than an empty panel.
11
+ // - vega-embed is declared as an optional peer dependency in package.json;
12
+ // apps that want interactive charts install it, others degrade gracefully.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export interface ChartRenderer {
16
+ mount(container: HTMLElement, spec: unknown): void | Promise<void>;
17
+ destroy(): void;
18
+ }
19
+
20
+ let cachedEmbed: Promise<any> | null = null;
21
+
22
+ function loadEmbed(): Promise<any> {
23
+ if (!cachedEmbed) {
24
+ // @ts-ignore — vega-embed is an optional peer dep; resolved at runtime
25
+ cachedEmbed = import('vega-embed').then((m: any) => m?.default ?? m);
26
+ }
27
+ return cachedEmbed;
28
+ }
29
+
30
+ function escapeHtml(s: string): string {
31
+ return String(s ?? '').replace(/[&<>"']/g, (c) =>
32
+ ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]!));
33
+ }
34
+
35
+ function renderFallback(container: HTMLElement, spec: unknown, reason?: string): void {
36
+ let preview: string;
37
+ try { preview = JSON.stringify(spec, null, 2); } catch { preview = String(spec); }
38
+ const note = reason ? `<div class="nb-chart-fallback-note">${escapeHtml(reason)}</div>` : '';
39
+ container.innerHTML = `${note}<pre class="nb-chart-fallback">${escapeHtml(preview)}</pre>`;
40
+ }
41
+
42
+ /**
43
+ * Render a Vega / Vega-Lite spec into the given container. Resolves once the
44
+ * chart is mounted (or falls back to a JSON preview on error). Never throws.
45
+ */
46
+ export async function renderChart(container: HTMLElement, spec: unknown): Promise<void> {
47
+ if (!container) return;
48
+ if (!spec || typeof spec !== 'object') {
49
+ renderFallback(container, spec, 'Invalid chart spec');
50
+ return;
51
+ }
52
+ try {
53
+ const embed = await loadEmbed();
54
+ if (typeof embed !== 'function') {
55
+ renderFallback(container, spec, 'vega-embed unavailable');
56
+ return;
57
+ }
58
+ container.innerHTML = '';
59
+ await embed(container, spec as any, { actions: false, renderer: 'canvas' });
60
+ } catch (err: any) {
61
+ renderFallback(container, spec, `chart render failed: ${String(err?.message ?? err)}`);
62
+ }
63
+ }