@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.
- package/package.json +15 -3
- package/src/agent/DataServersPanel.svelte +164 -0
- package/src/agent/LLMSelector.svelte +11 -3
- package/src/agent/ModelCacheManager.svelte +359 -0
- package/src/index.ts +42 -30
- package/src/widgets/WidgetRenderer.svelte +114 -104
- package/src/widgets/export-widget.ts +28 -1
- package/src/widgets/helpers/safe-image.ts +78 -0
- package/src/widgets/notebook/.gitkeep +0 -0
- package/src/widgets/notebook/chart-renderer.ts +63 -0
- package/src/widgets/notebook/compact.ts +823 -0
- package/src/widgets/notebook/document.ts +1065 -0
- package/src/widgets/notebook/editorial.ts +936 -0
- package/src/widgets/notebook/executors/.gitkeep +1 -0
- package/src/widgets/notebook/executors/index.ts +4 -0
- package/src/widgets/notebook/executors/js-worker.ts +269 -0
- package/src/widgets/notebook/executors/sql.ts +206 -0
- package/src/widgets/notebook/import-modals.ts +553 -0
- package/src/widgets/notebook/left-pane.ts +249 -0
- package/src/widgets/notebook/prose.ts +280 -0
- package/src/widgets/notebook/recipe-browser.ts +350 -0
- package/src/widgets/notebook/recipes/compact.md +124 -0
- package/src/widgets/notebook/recipes/document.md +139 -0
- package/src/widgets/notebook/recipes/editorial.md +120 -0
- package/src/widgets/notebook/recipes/workspace.md +119 -0
- package/src/widgets/notebook/resource-extractor.ts +162 -0
- package/src/widgets/notebook/share-handlers.ts +222 -0
- package/src/widgets/notebook/shared.ts +1592 -0
- package/src/widgets/notebook/workspace.ts +852 -0
- package/src/widgets/rich/cards.ts +181 -0
- package/src/widgets/rich/carousel.ts +319 -0
- package/src/widgets/rich/chart-rich.ts +386 -0
- package/src/widgets/rich/d3.ts +503 -0
- package/src/widgets/rich/data-table.ts +342 -0
- package/src/widgets/rich/gallery.ts +350 -0
- package/src/widgets/rich/grid-data.ts +173 -0
- package/src/widgets/rich/hemicycle.ts +313 -0
- package/src/widgets/rich/js-sandbox.ts +106 -0
- package/src/widgets/rich/json-viewer.ts +202 -0
- package/src/widgets/rich/log.ts +143 -0
- package/src/widgets/rich/map.ts +218 -0
- package/src/widgets/rich/profile.ts +256 -0
- package/src/widgets/rich/sankey.ts +262 -0
- package/src/widgets/rich/stat-card.ts +125 -0
- package/src/widgets/rich/timeline.ts +179 -0
- package/src/widgets/rich/trombinoscope.ts +246 -0
- package/src/widgets/simple/actions.ts +89 -0
- package/src/widgets/simple/alert.ts +100 -0
- package/src/widgets/simple/chart.ts +189 -0
- package/src/widgets/simple/code.ts +79 -0
- package/src/widgets/simple/kv.ts +68 -0
- package/src/widgets/simple/list.ts +89 -0
- package/src/widgets/simple/stat.ts +58 -0
- package/src/widgets/simple/tags.ts +125 -0
- package/src/widgets/simple/text.ts +198 -0
- package/src/widgets/SafeImage.svelte +0 -76
- package/src/widgets/rich/Cards.svelte +0 -39
- package/src/widgets/rich/Carousel.svelte +0 -88
- package/src/widgets/rich/Chart.svelte +0 -142
- package/src/widgets/rich/D3Widget.svelte +0 -378
- package/src/widgets/rich/DataTable.svelte +0 -62
- package/src/widgets/rich/Gallery.svelte +0 -94
- package/src/widgets/rich/GridData.svelte +0 -44
- package/src/widgets/rich/Hemicycle.svelte +0 -78
- package/src/widgets/rich/JsSandbox.svelte +0 -51
- package/src/widgets/rich/JsonViewer.svelte +0 -42
- package/src/widgets/rich/LogViewer.svelte +0 -24
- package/src/widgets/rich/MapView.svelte +0 -140
- package/src/widgets/rich/ProfileCard.svelte +0 -59
- package/src/widgets/rich/Sankey.svelte +0 -56
- package/src/widgets/rich/StatCard.svelte +0 -35
- package/src/widgets/rich/Timeline.svelte +0 -43
- package/src/widgets/rich/Trombinoscope.svelte +0 -48
- package/src/widgets/simple/ActionsBlock.svelte +0 -15
- package/src/widgets/simple/AlertBlock.svelte +0 -11
- package/src/widgets/simple/ChartBlock.svelte +0 -21
- package/src/widgets/simple/CodeBlock.svelte +0 -11
- package/src/widgets/simple/KVBlock.svelte +0 -16
- package/src/widgets/simple/ListBlock.svelte +0 -17
- package/src/widgets/simple/StatBlock.svelte +0 -14
- package/src/widgets/simple/TagsBlock.svelte +0 -15
- package/src/widgets/simple/TextBlock.svelte +0 -122
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 (
|
|
20
|
-
export {
|
|
21
|
-
export {
|
|
22
|
-
export {
|
|
23
|
-
export {
|
|
24
|
-
export {
|
|
25
|
-
export {
|
|
26
|
-
export {
|
|
27
|
-
export {
|
|
28
|
-
export {
|
|
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 (
|
|
31
|
-
export {
|
|
32
|
-
export {
|
|
33
|
-
export {
|
|
34
|
-
export {
|
|
35
|
-
export {
|
|
36
|
-
export {
|
|
37
|
-
export {
|
|
38
|
-
export {
|
|
39
|
-
export {
|
|
40
|
-
export {
|
|
41
|
-
export {
|
|
42
|
-
export {
|
|
43
|
-
export {
|
|
44
|
-
export {
|
|
45
|
-
export {
|
|
46
|
-
export {
|
|
47
|
-
export {
|
|
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
|
-
//
|
|
50
|
-
export {
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
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 →
|
|
64
|
-
const
|
|
65
|
-
// Simple
|
|
66
|
-
'stat':
|
|
67
|
-
'kv':
|
|
68
|
-
'list':
|
|
69
|
-
'chart':
|
|
70
|
-
'alert':
|
|
71
|
-
'code':
|
|
72
|
-
'text':
|
|
73
|
-
'actions':
|
|
74
|
-
'tags':
|
|
75
|
-
// Rich
|
|
76
|
-
'stat-card':
|
|
77
|
-
'profile':
|
|
78
|
-
'json-viewer':
|
|
79
|
-
'chart-rich':
|
|
80
|
-
'sankey':
|
|
81
|
-
'
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
|
|
86
|
-
'data
|
|
87
|
-
'
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
'
|
|
91
|
-
'
|
|
92
|
-
'
|
|
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 ??
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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 —
|
|
141
|
-
//
|
|
142
|
-
|
|
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 (!
|
|
155
|
+
if (!useVanilla || !vanillaRenderer || !vanillaContainer) return;
|
|
156
|
+
const container = vanillaContainer;
|
|
152
157
|
// Clear previous content
|
|
153
|
-
|
|
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 = (
|
|
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
|
|
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 =
|
|
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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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
|
+
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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
|
+
}
|