@webmcp-auto-ui/ui 2.5.26 → 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/README.md +2 -2
- package/package.json +15 -3
- package/src/agent/AgentConsole.svelte +1 -21
- package/src/agent/DataServersPanel.svelte +164 -0
- package/src/agent/LLMSelector.svelte +26 -8
- package/src/agent/ModelCacheManager.svelte +359 -0
- package/src/agent/{GemmaLoader.svelte → ModelLoader.svelte} +1 -1
- package/src/agent/SettingsPanel.svelte +16 -2
- package/src/index.ts +45 -31
- package/src/widgets/WidgetRenderer.svelte +118 -115
- 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/wm/FloatingLayout.svelte +2 -0
- package/src/wm/LinkIndicators.svelte +8 -15
- 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 -373
- 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 -38
- 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
|
@@ -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,106 @@
|
|
|
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
|
+
return `<!DOCTYPE html>
|
|
25
|
+
<html>
|
|
26
|
+
<head>
|
|
27
|
+
<meta charset="utf-8">
|
|
28
|
+
<style>
|
|
29
|
+
*,*::before,*::after{box-sizing:border-box}
|
|
30
|
+
body{margin:0;padding:8px;font-family:system-ui,sans-serif;font-size:13px;background:var(--bg,#1a1a2e);color:var(--fg,#e2e2e8)}
|
|
31
|
+
${css}
|
|
32
|
+
</style>
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<div id="root">${html}</div>
|
|
36
|
+
<script>
|
|
37
|
+
(function(){
|
|
38
|
+
try{
|
|
39
|
+
${code}
|
|
40
|
+
}catch(e){
|
|
41
|
+
document.getElementById('root').innerHTML='<pre style="color:red;white-space:pre-wrap">'+e+'</pre>';
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
44
|
+
<\/script>
|
|
45
|
+
</body>
|
|
46
|
+
</html>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function render(container: HTMLElement, data: JsSandboxData): () => void {
|
|
50
|
+
const spec: JsSandboxSpec = (data && data.spec) || ({ code: '' } as JsSandboxSpec);
|
|
51
|
+
|
|
52
|
+
// Outer wrapper mirrors the Svelte markup classes.
|
|
53
|
+
const wrapper = document.createElement('div');
|
|
54
|
+
wrapper.className = 'bg-surface border border-border rounded-lg overflow-hidden font-sans';
|
|
55
|
+
|
|
56
|
+
if (spec.title) {
|
|
57
|
+
const header = document.createElement('div');
|
|
58
|
+
header.className =
|
|
59
|
+
'px-3 py-2 border-b border-border text-sm font-semibold text-text1 flex items-center gap-2';
|
|
60
|
+
|
|
61
|
+
const tag = document.createElement('span');
|
|
62
|
+
tag.className = 'text-xs opacity-50';
|
|
63
|
+
tag.textContent = 'JS';
|
|
64
|
+
header.appendChild(tag);
|
|
65
|
+
|
|
66
|
+
header.appendChild(document.createTextNode(' ' + spec.title));
|
|
67
|
+
wrapper.appendChild(header);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const iframe = document.createElement('iframe');
|
|
71
|
+
// Exact sandbox flags from the Svelte source: allow-scripts only.
|
|
72
|
+
iframe.setAttribute('sandbox', 'allow-scripts');
|
|
73
|
+
iframe.setAttribute('title', spec.title ?? 'JS Sandbox');
|
|
74
|
+
iframe.style.width = '100%';
|
|
75
|
+
iframe.style.height = spec.height ?? '300px';
|
|
76
|
+
iframe.style.border = 'none';
|
|
77
|
+
iframe.style.display = 'block';
|
|
78
|
+
|
|
79
|
+
// Only build/assign srcdoc when we have code (or html) — otherwise render an
|
|
80
|
+
// empty iframe placeholder. This matches the Svelte behaviour (srcdoc is
|
|
81
|
+
// always computed) while being a touch more defensive for empty specs.
|
|
82
|
+
if (spec && (spec.code || spec.html || spec.css)) {
|
|
83
|
+
iframe.srcdoc = buildSrcdoc(spec);
|
|
84
|
+
} else {
|
|
85
|
+
iframe.srcdoc = buildSrcdoc({ code: '', html: '', css: '' });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
wrapper.appendChild(iframe);
|
|
89
|
+
container.appendChild(wrapper);
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
try {
|
|
93
|
+
// Blank the iframe first to stop any running scripts cleanly.
|
|
94
|
+
iframe.srcdoc = '';
|
|
95
|
+
} catch {
|
|
96
|
+
/* noop */
|
|
97
|
+
}
|
|
98
|
+
if (wrapper.parentNode === container) {
|
|
99
|
+
container.removeChild(wrapper);
|
|
100
|
+
} else {
|
|
101
|
+
container.innerHTML = '';
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default { render };
|