@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
@@ -0,0 +1,202 @@
1
+ /**
2
+ * JsonViewer — vanilla renderer
3
+ *
4
+ * Ported from JsonViewer.svelte (recursive snippet -> recursive JS function).
5
+ * Renders a JSON value with collapsible object/array nodes, typed colors,
6
+ * and cycle detection. XSS-safe (textContent only).
7
+ */
8
+
9
+ export interface JsonViewerSpec {
10
+ title?: string;
11
+ data?: unknown;
12
+ maxDepth?: number;
13
+ expanded?: boolean;
14
+ theme?: 'dark' | 'light';
15
+ }
16
+
17
+ export interface JsonViewerProps {
18
+ spec?: Partial<JsonViewerSpec>;
19
+ data?: unknown;
20
+ }
21
+
22
+ const COLOR_NULL = 'var(--color-text2)';
23
+ const COLOR_BOOL = '#a855f7';
24
+ const COLOR_NUMBER = 'var(--color-amber)';
25
+ const COLOR_STRING = 'var(--color-teal)';
26
+
27
+ function dispatch(container: HTMLElement, action: string, payload: unknown) {
28
+ container.dispatchEvent(
29
+ new CustomEvent('widget:interact', {
30
+ detail: { action, payload },
31
+ bubbles: true,
32
+ }),
33
+ );
34
+ }
35
+
36
+ function makeSpan(text: string, color?: string, className?: string): HTMLSpanElement {
37
+ const s = document.createElement('span');
38
+ if (color) s.style.color = color;
39
+ if (className) s.className = className;
40
+ s.textContent = text;
41
+ return s;
42
+ }
43
+
44
+ export function render(container: HTMLElement, props: JsonViewerProps | unknown = {}): () => void {
45
+ // Tolerate being called with just the raw data, or with {spec, data}.
46
+ let spec: Partial<JsonViewerSpec> = {};
47
+ let rawData: unknown;
48
+ if (props && typeof props === 'object' && ('spec' in (props as any) || 'data' in (props as any))) {
49
+ const p = props as JsonViewerProps;
50
+ spec = p.spec ?? {};
51
+ rawData = p.data;
52
+ } else {
53
+ rawData = props;
54
+ }
55
+
56
+ const value = spec.data !== undefined ? spec.data : rawData;
57
+ const maxDepth = spec.maxDepth ?? 5;
58
+ const expanded = spec.expanded !== false;
59
+
60
+ // Root wrapper
61
+ container.innerHTML = '';
62
+ const root = document.createElement('div');
63
+ root.className = 'bg-bg border border-border rounded-lg p-3 md:p-4 font-mono text-xs leading-5 text-text1';
64
+ container.appendChild(root);
65
+
66
+ if (spec.title) {
67
+ const title = document.createElement('div');
68
+ title.className = 'font-sans text-sm font-semibold text-text1 mb-3';
69
+ title.textContent = spec.title;
70
+ root.appendChild(title);
71
+ }
72
+
73
+ // Track toggles for interact events + cleanup.
74
+ const toggleHandlers: Array<{ el: HTMLDetailsElement; handler: EventListener }> = [];
75
+
76
+ // Cycle detection: WeakSet of currently-visited object/array references in the
77
+ // current DFS path. We can't use a Set because we must allow the same object
78
+ // to appear in sibling branches (non-cycle). Use an ancestor stack instead.
79
+ function renderNode(val: unknown, depth: number, ancestors: Set<object>): HTMLElement {
80
+ // null
81
+ if (val === null) {
82
+ return makeSpan('null', COLOR_NULL);
83
+ }
84
+
85
+ const t = typeof val;
86
+
87
+ if (t === 'boolean') {
88
+ return makeSpan(String(val), COLOR_BOOL);
89
+ }
90
+ if (t === 'number') {
91
+ return makeSpan(String(val), COLOR_NUMBER);
92
+ }
93
+ if (t === 'string') {
94
+ return makeSpan(`"${val as string}"`, COLOR_STRING);
95
+ }
96
+
97
+ // Arrays
98
+ if (Array.isArray(val)) {
99
+ if (ancestors.has(val)) {
100
+ return makeSpan('[Circular]', undefined, 'text-text2');
101
+ }
102
+ if (depth >= maxDepth) {
103
+ return makeSpan(`[Array(${val.length})]`, undefined, 'text-text2');
104
+ }
105
+ const details = document.createElement('details');
106
+ if (expanded && depth < 2) details.open = true;
107
+
108
+ const summary = document.createElement('summary');
109
+ summary.className = 'cursor-pointer text-text2 hover:text-text1 select-none';
110
+ summary.textContent = `Array(${val.length})`;
111
+ details.appendChild(summary);
112
+
113
+ const body = document.createElement('div');
114
+ body.className = 'ml-4 border-l border-border pl-3 mt-0.5';
115
+
116
+ ancestors.add(val);
117
+ val.forEach((item, i) => {
118
+ const row = document.createElement('div');
119
+ row.className = 'py-0.5';
120
+ const idx = document.createElement('span');
121
+ idx.className = 'text-text2 text-xs mr-1';
122
+ idx.textContent = `${i}:`;
123
+ row.appendChild(idx);
124
+ row.appendChild(renderNode(item, depth + 1, ancestors));
125
+ body.appendChild(row);
126
+ });
127
+ ancestors.delete(val);
128
+
129
+ details.appendChild(body);
130
+
131
+ const handler: EventListener = () => {
132
+ dispatch(container, 'toggle', { kind: 'array', open: details.open, length: val.length });
133
+ };
134
+ details.addEventListener('toggle', handler);
135
+ toggleHandlers.push({ el: details, handler });
136
+
137
+ return details;
138
+ }
139
+
140
+ // Objects
141
+ if (t === 'object') {
142
+ const obj = val as Record<string, unknown>;
143
+ if (ancestors.has(obj)) {
144
+ return makeSpan('[Circular]', undefined, 'text-text2');
145
+ }
146
+ const keys = Object.keys(obj);
147
+ if (depth >= maxDepth) {
148
+ return makeSpan(`{Object(${keys.length})}`, undefined, 'text-text2');
149
+ }
150
+ const details = document.createElement('details');
151
+ if (expanded && depth < 2) details.open = true;
152
+
153
+ const summary = document.createElement('summary');
154
+ summary.className = 'cursor-pointer text-text2 hover:text-text1 select-none';
155
+ summary.textContent = `{${keys.length}}`;
156
+ details.appendChild(summary);
157
+
158
+ const body = document.createElement('div');
159
+ body.className = 'ml-4 border-l border-border pl-3 mt-0.5';
160
+
161
+ ancestors.add(obj);
162
+ for (const k of keys) {
163
+ const row = document.createElement('div');
164
+ row.className = 'py-0.5';
165
+ const key = document.createElement('span');
166
+ key.className = 'text-accent mr-1';
167
+ key.textContent = `"${k}":`;
168
+ row.appendChild(key);
169
+ row.appendChild(renderNode(obj[k], depth + 1, ancestors));
170
+ body.appendChild(row);
171
+ }
172
+ ancestors.delete(obj);
173
+
174
+ details.appendChild(body);
175
+
176
+ const handler: EventListener = () => {
177
+ dispatch(container, 'toggle', { kind: 'object', open: details.open, keys: keys.length });
178
+ };
179
+ details.addEventListener('toggle', handler);
180
+ toggleHandlers.push({ el: details, handler });
181
+
182
+ return details;
183
+ }
184
+
185
+ // Fallback (undefined, function, symbol, bigint)
186
+ return makeSpan(String(val), undefined, 'text-text2');
187
+ }
188
+
189
+ if (value === undefined) {
190
+ root.appendChild(makeSpan('undefined', undefined, 'text-text2'));
191
+ } else {
192
+ root.appendChild(renderNode(value, 0, new Set<object>()));
193
+ }
194
+
195
+ return () => {
196
+ for (const { el, handler } of toggleHandlers) {
197
+ el.removeEventListener('toggle', handler);
198
+ }
199
+ toggleHandlers.length = 0;
200
+ container.innerHTML = '';
201
+ };
202
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * LogViewer vanilla renderer.
3
+ *
4
+ * Contract:
5
+ * render(container, data): () => void (cleanup)
6
+ *
7
+ * `data` is expected to match { spec, data } where spec is Partial<LogViewerSpec>.
8
+ * For robustness, if the caller passes a bare spec or a bare array of entries,
9
+ * we try to coerce.
10
+ */
11
+
12
+ export interface LogEntry {
13
+ timestamp?: string;
14
+ level?: 'debug' | 'info' | 'warn' | 'error';
15
+ message: string;
16
+ source?: string;
17
+ }
18
+
19
+ export interface LogViewerSpec {
20
+ title?: string;
21
+ entries?: LogEntry[];
22
+ maxHeight?: string;
23
+ }
24
+
25
+ const LEVEL_CLASS: Record<string, string> = {
26
+ debug: 'text-text2',
27
+ info: 'text-teal',
28
+ warn: 'text-amber',
29
+ error: 'text-accent2',
30
+ };
31
+
32
+ function extractSpecAndData(input: any): { spec: Partial<LogViewerSpec>; data: unknown } {
33
+ if (input && typeof input === 'object' && !Array.isArray(input)) {
34
+ if ('spec' in input || 'data' in input) {
35
+ return {
36
+ spec: (input.spec ?? {}) as Partial<LogViewerSpec>,
37
+ data: input.data,
38
+ };
39
+ }
40
+ // Bare spec object.
41
+ return { spec: input as Partial<LogViewerSpec>, data: undefined };
42
+ }
43
+ if (Array.isArray(input)) {
44
+ return { spec: {}, data: input };
45
+ }
46
+ return { spec: {}, data: undefined };
47
+ }
48
+
49
+ function resolveEntries(spec: Partial<LogViewerSpec>, data: unknown): LogEntry[] {
50
+ if (Array.isArray(spec.entries) && spec.entries.length) return spec.entries;
51
+ if (Array.isArray(data)) return data as LogEntry[];
52
+ return [];
53
+ }
54
+
55
+ export function render(container: HTMLElement, data: any): () => void {
56
+ const { spec, data: payload } = extractSpecAndData(data);
57
+ const entries = resolveEntries(spec, payload);
58
+ const maxHeight = spec.maxHeight ?? '320px';
59
+
60
+ const rowListeners: Array<{ el: HTMLElement; handler: (ev: MouseEvent) => void }> = [];
61
+
62
+ const root = document.createElement('div');
63
+ root.className = 'bg-bg border border-border rounded-lg font-mono';
64
+ root.setAttribute('role', 'log');
65
+ root.setAttribute('aria-live', 'polite');
66
+
67
+ if (spec.title) {
68
+ const header = document.createElement('div');
69
+ header.className = 'px-4 py-2 border-b border-border text-xs text-text2';
70
+ header.textContent = spec.title;
71
+ root.appendChild(header);
72
+ }
73
+
74
+ const body = document.createElement('div');
75
+ body.className = 'overflow-y-auto text-xs leading-5 p-3 flex flex-col gap-0.5';
76
+ body.style.maxHeight = maxHeight;
77
+
78
+ if (!entries.length) {
79
+ const empty = document.createElement('span');
80
+ empty.className = 'text-text2';
81
+ empty.textContent = 'No log entries';
82
+ body.appendChild(empty);
83
+ } else {
84
+ for (let i = 0; i < entries.length; i++) {
85
+ const e = entries[i] ?? ({ message: '' } as LogEntry);
86
+ const row = document.createElement('div');
87
+ row.className = 'flex gap-2 items-start hover:bg-surface2 px-1 rounded cursor-pointer';
88
+ row.setAttribute('role', 'listitem');
89
+ row.dataset.index = String(i);
90
+ if (e.level) row.dataset.level = e.level;
91
+
92
+ if (e.timestamp) {
93
+ const ts = document.createElement('span');
94
+ ts.className = 'text-text2 flex-shrink-0';
95
+ ts.textContent = e.timestamp;
96
+ row.appendChild(ts);
97
+ }
98
+
99
+ const level = e.level ?? 'info';
100
+ const lvl = document.createElement('span');
101
+ lvl.className = `flex-shrink-0 uppercase text-[10px] font-semibold w-10 ${LEVEL_CLASS[level] ?? LEVEL_CLASS.info}`;
102
+ lvl.textContent = level;
103
+ row.appendChild(lvl);
104
+
105
+ if (e.source) {
106
+ const src = document.createElement('span');
107
+ src.className = 'text-text2 flex-shrink-0';
108
+ src.textContent = `[${e.source}]`;
109
+ row.appendChild(src);
110
+ }
111
+
112
+ const msg = document.createElement('span');
113
+ msg.className = 'text-text1 break-all';
114
+ msg.textContent = e.message ?? '';
115
+ row.appendChild(msg);
116
+
117
+ const handler = (_ev: MouseEvent) => {
118
+ row.dispatchEvent(
119
+ new CustomEvent('widget:interact', {
120
+ bubbles: true,
121
+ composed: true,
122
+ detail: { action: 'log-entry-click', payload: { index: i, entry: e } },
123
+ }),
124
+ );
125
+ };
126
+ row.addEventListener('click', handler);
127
+ rowListeners.push({ el: row, handler });
128
+
129
+ body.appendChild(row);
130
+ }
131
+ }
132
+
133
+ root.appendChild(body);
134
+ container.appendChild(root);
135
+
136
+ return () => {
137
+ for (const { el, handler } of rowListeners) {
138
+ el.removeEventListener('click', handler);
139
+ }
140
+ rowListeners.length = 0;
141
+ container.innerHTML = '';
142
+ };
143
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Vanilla renderer for MapView widget (Leaflet, dark basemap).
3
+ *
4
+ * Contract:
5
+ * render(container, spec) => Promise<cleanup>
6
+ *
7
+ * Leaflet is lazy-loaded via dynamic import so this widget costs nothing
8
+ * until actually mounted. The cleanup function calls `map.remove()` to
9
+ * release Leaflet's DOM listeners, then clears the container.
10
+ *
11
+ * Events:
12
+ * click on marker -> CustomEvent('widget:interact', {
13
+ * detail: { action: 'markerclick', payload: marker },
14
+ * bubbles: true
15
+ * })
16
+ */
17
+
18
+ export interface LatLng {
19
+ lat: number;
20
+ lng: number;
21
+ }
22
+
23
+ export interface MapMarker {
24
+ lat: number;
25
+ lng: number;
26
+ label?: string;
27
+ color?: string;
28
+ }
29
+
30
+ export interface MapSpec {
31
+ title?: string;
32
+ center?: LatLng;
33
+ zoom?: number;
34
+ height?: string;
35
+ markers?: MapMarker[];
36
+ }
37
+
38
+ function markerColor(color?: string): string {
39
+ return color ?? 'var(--color-accent)';
40
+ }
41
+
42
+ function escapeHtml(s: string): string {
43
+ return s
44
+ .replace(/&/g, '&amp;')
45
+ .replace(/</g, '&lt;')
46
+ .replace(/>/g, '&gt;')
47
+ .replace(/"/g, '&quot;')
48
+ .replace(/'/g, '&#39;');
49
+ }
50
+
51
+ export async function render(
52
+ container: HTMLElement,
53
+ data: Partial<MapSpec> | undefined
54
+ ): Promise<() => void> {
55
+ const spec: Partial<MapSpec> = data ?? {};
56
+
57
+ // Build static chrome (wrapper + optional title + map host + optional marker count).
58
+ container.innerHTML = '';
59
+
60
+ const wrapper = document.createElement('div');
61
+ wrapper.className = 'bg-surface border border-border rounded-lg p-3 md:p-4 font-sans';
62
+
63
+ if (spec.title) {
64
+ const h3 = document.createElement('h3');
65
+ h3.className = 'text-sm font-semibold text-text1 mb-3';
66
+ h3.textContent = spec.title;
67
+ wrapper.appendChild(h3);
68
+ }
69
+
70
+ const mapHost = document.createElement('div');
71
+ mapHost.className = 'rounded overflow-hidden border border-border';
72
+ mapHost.style.height = spec.height ?? '400px';
73
+ mapHost.setAttribute('role', 'region');
74
+ mapHost.setAttribute('aria-label', spec.title ?? 'Carte');
75
+
76
+ // Loader placeholder while Leaflet is being fetched.
77
+ const loader = document.createElement('div');
78
+ loader.className =
79
+ 'w-full h-full bg-bg flex flex-col items-center justify-center text-text2 text-sm gap-2';
80
+ loader.innerHTML = `
81
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 opacity-30" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
82
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 13l4.553 2.276A1 1 0 0021 21.382V10.618a1 1 0 00-1.447-.894L15 12m0 8V12M9 7l6-2.5"/>
83
+ </svg>
84
+ <span class="font-mono text-xs">Chargement de la carte…</span>
85
+ `;
86
+ mapHost.appendChild(loader);
87
+ wrapper.appendChild(mapHost);
88
+
89
+ const markers = spec.markers ?? [];
90
+ if (markers.length) {
91
+ const count = document.createElement('div');
92
+ count.className = 'mt-2 text-xs text-text2 font-mono';
93
+ count.textContent = `${markers.length} marqueur${markers.length > 1 ? 's' : ''}`;
94
+ wrapper.appendChild(count);
95
+ }
96
+
97
+ container.appendChild(wrapper);
98
+
99
+ // Inject tooltip styles once (Svelte version used :global CSS).
100
+ if (!document.getElementById('mapview-tooltip-style')) {
101
+ const style = document.createElement('style');
102
+ style.id = 'mapview-tooltip-style';
103
+ style.textContent = `
104
+ .mapview-tooltip {
105
+ background: var(--color-surface2);
106
+ border: 1px solid var(--color-border);
107
+ color: var(--color-text1);
108
+ font-size: 11px;
109
+ font-family: ui-monospace, monospace;
110
+ border-radius: 4px;
111
+ padding: 2px 6px;
112
+ box-shadow: 0 2px 8px rgba(0,0,0,0.4);
113
+ }
114
+ .mapview-tooltip::before {
115
+ border-top-color: var(--color-border);
116
+ }
117
+ .leaflet-container {
118
+ background: var(--color-bg);
119
+ }
120
+ `;
121
+ document.head.appendChild(style);
122
+ }
123
+
124
+ let cancelled = false;
125
+ let mapInstance: any = null;
126
+
127
+ // Lazy-load Leaflet + its CSS. Keep this exact shape: dispatcher awaits us.
128
+ const leafletMod = await import('leaflet');
129
+ // @ts-ignore — CSS import handled by Vite.
130
+ await import('leaflet/dist/leaflet.css');
131
+
132
+ if (cancelled) {
133
+ // Cleanup fired before Leaflet finished loading.
134
+ return () => {};
135
+ }
136
+
137
+ const L: any = (leafletMod as any).default ?? leafletMod;
138
+
139
+ // Clear loader now that Leaflet is ready.
140
+ mapHost.innerHTML = '';
141
+
142
+ const center: [number, number] = spec.center
143
+ ? [spec.center.lat, spec.center.lng]
144
+ : [46.6, 2.3];
145
+
146
+ const map = L.map(mapHost, {
147
+ center,
148
+ zoom: spec.zoom ?? 6,
149
+ zoomControl: true,
150
+ attributionControl: true,
151
+ });
152
+ mapInstance = map;
153
+
154
+ L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
155
+ attribution:
156
+ '&copy; <a href="https://carto.com/">CARTO</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
157
+ subdomains: 'abcd',
158
+ maxZoom: 19,
159
+ }).addTo(map);
160
+
161
+ for (const marker of markers) {
162
+ const cm = L.circleMarker([marker.lat, marker.lng], {
163
+ radius: 7,
164
+ color: markerColor(marker.color),
165
+ fillColor: markerColor(marker.color),
166
+ fillOpacity: 0.85,
167
+ weight: 1.5,
168
+ }).addTo(map);
169
+
170
+ if (marker.label) {
171
+ cm.bindTooltip(escapeHtml(marker.label), {
172
+ direction: 'top',
173
+ offset: L.point(0, -8),
174
+ className: 'mapview-tooltip',
175
+ });
176
+ }
177
+
178
+ cm.on('click', () => {
179
+ container.dispatchEvent(
180
+ new CustomEvent('widget:interact', {
181
+ detail: { action: 'markerclick', payload: marker },
182
+ bubbles: true,
183
+ })
184
+ );
185
+ });
186
+ }
187
+
188
+ // Optional PNG export hook. Best-effort rasterization of the current map tiles.
189
+ // Leaflet tiles are cross-origin, so a full-fidelity export would require
190
+ // leaflet-image + proxied tiles. We expose a no-op-ish stub that returns the
191
+ // map container as a data-url via html-to-canvas if available, else null.
192
+ (container as any).__exportPng = async (): Promise<string | null> => {
193
+ try {
194
+ // @ts-ignore — optional dependency, may be absent.
195
+ const mod = await import('html-to-image').catch(() => null);
196
+ if (!mod || !mapHost) return null;
197
+ return await mod.toPng(mapHost, { pixelRatio: 2 });
198
+ } catch {
199
+ return null;
200
+ }
201
+ };
202
+
203
+ return () => {
204
+ cancelled = true;
205
+ try {
206
+ if (mapInstance) mapInstance.remove();
207
+ } catch {
208
+ /* swallow — Leaflet may already be torn down */
209
+ }
210
+ mapInstance = null;
211
+ try {
212
+ delete (container as any).__exportPng;
213
+ } catch {
214
+ /* ignore */
215
+ }
216
+ container.innerHTML = '';
217
+ };
218
+ }