@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
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { onMount, onDestroy } from 'svelte';
|
|
3
|
-
|
|
4
|
-
export interface D3Spec {
|
|
5
|
-
title?: string;
|
|
6
|
-
preset?: 'hex-heatmap' | 'radial' | 'treemap' | 'force';
|
|
7
|
-
data: unknown;
|
|
8
|
-
config?: Record<string, unknown>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface Props { spec: D3Spec; }
|
|
12
|
-
let { spec }: Props = $props();
|
|
13
|
-
|
|
14
|
-
let container: HTMLDivElement;
|
|
15
|
-
let width = $state(0);
|
|
16
|
-
let height = $state(0);
|
|
17
|
-
let loading = $state(true);
|
|
18
|
-
let error = $state<string | null>(null);
|
|
19
|
-
let ro: ResizeObserver | undefined;
|
|
20
|
-
|
|
21
|
-
// ── Colour helpers ─────────────────────────────────────────────────────────
|
|
22
|
-
function cssVar(name: string, fallback: string): string {
|
|
23
|
-
if (typeof document === 'undefined') return fallback;
|
|
24
|
-
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ── Presets ────────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
function renderHexHeatmap(d3: typeof import('d3'), el: HTMLDivElement, d: D3Spec) {
|
|
30
|
-
const values = (d.data as { values?: number[][] })?.values;
|
|
31
|
-
if (!values || values.length === 0) {
|
|
32
|
-
el.innerHTML = '<p class="text-xs text-text2 p-2">No data (values missing)</p>';
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
const cfg = d.config ?? {};
|
|
36
|
-
const cellSize: number = (cfg.cellSize as number) ?? 14;
|
|
37
|
-
const colorScale: string[] = (cfg.colorScale as string[]) ?? [
|
|
38
|
-
cssVar('--color-surface2', '#f0f0f6'),
|
|
39
|
-
cssVar('--color-accent', '#6c5ce7'),
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
const rows = values.length;
|
|
43
|
-
const cols = Math.max(...values.map((r) => r.length));
|
|
44
|
-
const gap = 2;
|
|
45
|
-
const hex = cellSize;
|
|
46
|
-
const hx = hex * Math.sqrt(3);
|
|
47
|
-
const hy = hex * 1.5;
|
|
48
|
-
const svgW = cols * hx + hex * 0.5 + gap * cols;
|
|
49
|
-
const svgH = rows * hy + hex * 0.5 + gap * rows;
|
|
50
|
-
|
|
51
|
-
const allVals = values.flat();
|
|
52
|
-
const minVal = d3.min(allVals) ?? 0;
|
|
53
|
-
const maxVal = d3.max(allVals) ?? 1;
|
|
54
|
-
|
|
55
|
-
const colorInterp = d3.scaleLinear<string>()
|
|
56
|
-
.domain([minVal, maxVal])
|
|
57
|
-
.range(colorScale as [string, string]);
|
|
58
|
-
|
|
59
|
-
const svg = d3
|
|
60
|
-
.select(el)
|
|
61
|
-
.append('svg')
|
|
62
|
-
.attr('viewBox', `0 0 ${svgW} ${svgH}`)
|
|
63
|
-
.attr('width', '100%')
|
|
64
|
-
.attr('height', svgH);
|
|
65
|
-
|
|
66
|
-
// Hexagon path generator
|
|
67
|
-
function hexPath(cx: number, cy: number, r: number): string {
|
|
68
|
-
const pts = d3.range(6).map((i) => {
|
|
69
|
-
const angle = (Math.PI / 180) * (60 * i - 30);
|
|
70
|
-
return [cx + r * Math.cos(angle), cy + r * Math.sin(angle)];
|
|
71
|
-
});
|
|
72
|
-
return `M${pts.map((p) => p.join(',')).join('L')}Z`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
values.forEach((row, ri) => {
|
|
76
|
-
row.forEach((val, ci) => {
|
|
77
|
-
const cx = ci * (hx + gap) + (ri % 2 === 1 ? hx / 2 : 0) + hx / 2;
|
|
78
|
-
const cy = ri * (hy + gap) + hex;
|
|
79
|
-
svg
|
|
80
|
-
.append('path')
|
|
81
|
-
.attr('d', hexPath(cx, cy, hex * 0.9))
|
|
82
|
-
.attr('fill', colorInterp(val))
|
|
83
|
-
.attr('stroke', 'none')
|
|
84
|
-
.append('title')
|
|
85
|
-
.text(String(val));
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function renderRadial(d3: typeof import('d3'), el: HTMLDivElement, d: D3Spec) {
|
|
91
|
-
const segments = (d.data as { segments?: { label: string; value: number; color?: string }[] })?.segments;
|
|
92
|
-
if (!segments || segments.length === 0) {
|
|
93
|
-
el.innerHTML = '<p class="text-xs text-text2 p-2">No data (segments missing)</p>';
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
const cfg = d.config ?? {};
|
|
97
|
-
const innerRatio: number = (cfg.innerRadius as number) ?? 0.4;
|
|
98
|
-
const size = Math.min(width || 300, 300);
|
|
99
|
-
const outerR = size / 2 - 10;
|
|
100
|
-
const innerR = outerR * innerRatio;
|
|
101
|
-
|
|
102
|
-
const defaultColors = [
|
|
103
|
-
cssVar('--color-accent', '#6c5ce7'),
|
|
104
|
-
cssVar('--color-accent2', '#e17055'),
|
|
105
|
-
'#00b894', '#fdcb6e', '#0984e3', '#e84393',
|
|
106
|
-
];
|
|
107
|
-
|
|
108
|
-
const svg = d3
|
|
109
|
-
.select(el)
|
|
110
|
-
.append('svg')
|
|
111
|
-
.attr('viewBox', `0 0 ${size} ${size}`)
|
|
112
|
-
.attr('width', '100%')
|
|
113
|
-
.attr('height', size);
|
|
114
|
-
|
|
115
|
-
const g = svg.append('g').attr('transform', `translate(${size / 2},${size / 2})`);
|
|
116
|
-
|
|
117
|
-
const pie = d3.pie<{ label: string; value: number; color?: string }>()
|
|
118
|
-
.value((seg) => seg.value)
|
|
119
|
-
.sort(null);
|
|
120
|
-
|
|
121
|
-
const arc = d3.arc<d3.PieArcDatum<{ label: string; value: number; color?: string }>>()
|
|
122
|
-
.innerRadius(innerR)
|
|
123
|
-
.outerRadius(outerR);
|
|
124
|
-
|
|
125
|
-
const arcs = pie(segments);
|
|
126
|
-
|
|
127
|
-
g.selectAll('path')
|
|
128
|
-
.data(arcs)
|
|
129
|
-
.enter()
|
|
130
|
-
.append('path')
|
|
131
|
-
.attr('d', arc)
|
|
132
|
-
.attr('fill', (_, i) => arcs[i].data.color ?? defaultColors[i % defaultColors.length])
|
|
133
|
-
.attr('stroke', 'var(--color-surface, #fff)')
|
|
134
|
-
.attr('stroke-width', 2)
|
|
135
|
-
.append('title')
|
|
136
|
-
.text((arcDatum) => `${arcDatum.data.label}: ${arcDatum.data.value}`);
|
|
137
|
-
|
|
138
|
-
// Labels
|
|
139
|
-
const labelArc = d3.arc<d3.PieArcDatum<{ label: string; value: number; color?: string }>>()
|
|
140
|
-
.innerRadius(outerR * 0.7)
|
|
141
|
-
.outerRadius(outerR * 0.7);
|
|
142
|
-
|
|
143
|
-
g.selectAll('text')
|
|
144
|
-
.data(arcs)
|
|
145
|
-
.enter()
|
|
146
|
-
.append('text')
|
|
147
|
-
.attr('transform', (arcDatum) => `translate(${labelArc.centroid(arcDatum)})`)
|
|
148
|
-
.attr('text-anchor', 'middle')
|
|
149
|
-
.attr('font-size', '10px')
|
|
150
|
-
.attr('fill', 'var(--color-text1, #111)')
|
|
151
|
-
.text((arcDatum) => arcDatum.data.label.length > 8 ? arcDatum.data.label.slice(0, 7) + '…' : arcDatum.data.label);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function renderTreemap(d3: typeof import('d3'), el: HTMLDivElement, d: D3Spec) {
|
|
155
|
-
type TreeNode = { name: string; value?: number; children?: TreeNode[] };
|
|
156
|
-
const rawData = d.data as { children?: TreeNode[] };
|
|
157
|
-
if (!rawData?.children || rawData.children.length === 0) {
|
|
158
|
-
el.innerHTML = '<p class="text-xs text-text2 p-2">No data (children missing)</p>';
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
const cfg = d.config ?? {};
|
|
162
|
-
const padding: number = (cfg.padding as number) ?? 2;
|
|
163
|
-
const w = width || 400;
|
|
164
|
-
const h = Math.max(200, Math.round(w * 0.6));
|
|
165
|
-
|
|
166
|
-
const accent = cssVar('--color-accent', '#6c5ce7');
|
|
167
|
-
const colorScale = d3.scaleSequential(d3.interpolateBlues).domain([0, 10]);
|
|
168
|
-
|
|
169
|
-
const root = d3
|
|
170
|
-
.hierarchy<TreeNode>({ name: 'root', children: rawData.children })
|
|
171
|
-
.sum((node) => node.value ?? 1)
|
|
172
|
-
.sort((a, b) => (b.value ?? 0) - (a.value ?? 0));
|
|
173
|
-
|
|
174
|
-
d3.treemap<TreeNode>()
|
|
175
|
-
.size([w, h])
|
|
176
|
-
.padding(padding)(root);
|
|
177
|
-
|
|
178
|
-
const svg = d3
|
|
179
|
-
.select(el)
|
|
180
|
-
.append('svg')
|
|
181
|
-
.attr('viewBox', `0 0 ${w} ${h}`)
|
|
182
|
-
.attr('width', '100%')
|
|
183
|
-
.attr('height', h);
|
|
184
|
-
|
|
185
|
-
const leaves = root.leaves() as (d3.HierarchyRectangularNode<TreeNode>)[];
|
|
186
|
-
|
|
187
|
-
const cell = svg
|
|
188
|
-
.selectAll('g')
|
|
189
|
-
.data(leaves)
|
|
190
|
-
.enter()
|
|
191
|
-
.append('g')
|
|
192
|
-
.attr('transform', (node) => `translate(${node.x0},${node.y0})`);
|
|
193
|
-
|
|
194
|
-
cell
|
|
195
|
-
.append('rect')
|
|
196
|
-
.attr('width', (node) => Math.max(0, node.x1 - node.x0))
|
|
197
|
-
.attr('height', (node) => Math.max(0, node.y1 - node.y0))
|
|
198
|
-
.attr('fill', (_, i) => colorScale(i % 11))
|
|
199
|
-
.attr('stroke', accent)
|
|
200
|
-
.attr('stroke-width', 1)
|
|
201
|
-
.append('title')
|
|
202
|
-
.text((node) => `${node.data.name}: ${node.value}`);
|
|
203
|
-
|
|
204
|
-
cell
|
|
205
|
-
.append('text')
|
|
206
|
-
.attr('x', 4)
|
|
207
|
-
.attr('y', 14)
|
|
208
|
-
.attr('font-size', '11px')
|
|
209
|
-
.attr('fill', '#fff')
|
|
210
|
-
.text((node) => {
|
|
211
|
-
const w2 = node.x1 - node.x0;
|
|
212
|
-
const label = node.data.name;
|
|
213
|
-
return w2 > 30 ? (label.length > 12 ? label.slice(0, 11) + '…' : label) : '';
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function renderForce(d3: typeof import('d3'), el: HTMLDivElement, d: D3Spec) {
|
|
218
|
-
type FNode = { id: string; label?: string; group?: number };
|
|
219
|
-
type FLink = { source: string; target: string; value?: number };
|
|
220
|
-
const rawData = d.data as { nodes?: FNode[]; links?: FLink[] };
|
|
221
|
-
if (!rawData?.nodes || rawData.nodes.length === 0) {
|
|
222
|
-
el.innerHTML = '<p class="text-xs text-text2 p-2">No data (nodes missing)</p>';
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
const nodes: FNode[] = rawData.nodes.map((n) => ({ ...n }));
|
|
226
|
-
const links: FLink[] = (rawData.links ?? []).map((l) => ({ ...l }));
|
|
227
|
-
const w = width || 400;
|
|
228
|
-
const h = Math.max(250, Math.round(w * 0.65));
|
|
229
|
-
|
|
230
|
-
const accent = cssVar('--color-accent', '#6c5ce7');
|
|
231
|
-
const accent2 = cssVar('--color-accent2', '#e17055');
|
|
232
|
-
const groups = Array.from(new Set(nodes.map((n) => n.group ?? 0)));
|
|
233
|
-
const colorMap = d3.scaleOrdinal<number, string>()
|
|
234
|
-
.domain(groups)
|
|
235
|
-
.range([accent, accent2, '#00b894', '#fdcb6e', '#0984e3', '#e84393']);
|
|
236
|
-
|
|
237
|
-
const svg = d3
|
|
238
|
-
.select(el)
|
|
239
|
-
.append('svg')
|
|
240
|
-
.attr('viewBox', `0 0 ${w} ${h}`)
|
|
241
|
-
.attr('width', '100%')
|
|
242
|
-
.attr('height', h);
|
|
243
|
-
|
|
244
|
-
const sim = d3
|
|
245
|
-
.forceSimulation(nodes as d3.SimulationNodeDatum[])
|
|
246
|
-
.force('link', d3.forceLink(links).id((n: d3.SimulationNodeDatum) => (n as FNode).id).distance(60))
|
|
247
|
-
.force('charge', d3.forceManyBody().strength(-120))
|
|
248
|
-
.force('center', d3.forceCenter(w / 2, h / 2))
|
|
249
|
-
.force('collision', d3.forceCollide(18));
|
|
250
|
-
|
|
251
|
-
const link = svg
|
|
252
|
-
.append('g')
|
|
253
|
-
.attr('stroke', 'var(--color-border, #ccc)')
|
|
254
|
-
.attr('stroke-opacity', 0.6)
|
|
255
|
-
.selectAll('line')
|
|
256
|
-
.data(links)
|
|
257
|
-
.enter()
|
|
258
|
-
.append('line')
|
|
259
|
-
.attr('stroke-width', (l) => Math.sqrt(l.value ?? 1));
|
|
260
|
-
|
|
261
|
-
const node = svg
|
|
262
|
-
.append('g')
|
|
263
|
-
.selectAll('g')
|
|
264
|
-
.data(nodes)
|
|
265
|
-
.enter()
|
|
266
|
-
.append('g')
|
|
267
|
-
.call(
|
|
268
|
-
d3.drag<SVGGElement, FNode>()
|
|
269
|
-
.on('start', (event, n) => {
|
|
270
|
-
if (!event.active) sim.alphaTarget(0.3).restart();
|
|
271
|
-
(n as d3.SimulationNodeDatum).fx = (n as d3.SimulationNodeDatum).x;
|
|
272
|
-
(n as d3.SimulationNodeDatum).fy = (n as d3.SimulationNodeDatum).y;
|
|
273
|
-
})
|
|
274
|
-
.on('drag', (event, n) => {
|
|
275
|
-
(n as d3.SimulationNodeDatum).fx = event.x;
|
|
276
|
-
(n as d3.SimulationNodeDatum).fy = event.y;
|
|
277
|
-
})
|
|
278
|
-
.on('end', (event, n) => {
|
|
279
|
-
if (!event.active) sim.alphaTarget(0);
|
|
280
|
-
(n as d3.SimulationNodeDatum).fx = null;
|
|
281
|
-
(n as d3.SimulationNodeDatum).fy = null;
|
|
282
|
-
})
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
node
|
|
286
|
-
.append('circle')
|
|
287
|
-
.attr('r', 10)
|
|
288
|
-
.attr('fill', (n) => colorMap(n.group ?? 0))
|
|
289
|
-
.attr('stroke', '#fff')
|
|
290
|
-
.attr('stroke-width', 1.5);
|
|
291
|
-
|
|
292
|
-
node
|
|
293
|
-
.append('text')
|
|
294
|
-
.attr('x', 14)
|
|
295
|
-
.attr('y', 4)
|
|
296
|
-
.attr('font-size', '10px')
|
|
297
|
-
.attr('fill', 'var(--color-text1, #111)')
|
|
298
|
-
.text((n) => n.label ?? n.id);
|
|
299
|
-
|
|
300
|
-
node.append('title').text((n) => n.label ?? n.id);
|
|
301
|
-
|
|
302
|
-
sim.on('tick', () => {
|
|
303
|
-
link
|
|
304
|
-
.attr('x1', (l) => (l.source as d3.SimulationNodeDatum).x ?? 0)
|
|
305
|
-
.attr('y1', (l) => (l.source as d3.SimulationNodeDatum).y ?? 0)
|
|
306
|
-
.attr('x2', (l) => (l.target as d3.SimulationNodeDatum).x ?? 0)
|
|
307
|
-
.attr('y2', (l) => (l.target as d3.SimulationNodeDatum).y ?? 0);
|
|
308
|
-
node.attr('transform', (n) => `translate(${(n as d3.SimulationNodeDatum).x ?? 0},${(n as d3.SimulationNodeDatum).y ?? 0})`);
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// ── Main render dispatcher ─────────────────────────────────────────────────
|
|
313
|
-
function render(d3: typeof import('d3')) {
|
|
314
|
-
if (!container) return;
|
|
315
|
-
container.innerHTML = '';
|
|
316
|
-
if (!spec?.preset) {
|
|
317
|
-
container.innerHTML = '<p class="text-xs text-text2 p-2">No preset specified.</p>';
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
switch (spec.preset) {
|
|
321
|
-
case 'hex-heatmap': renderHexHeatmap(d3, container, spec); break;
|
|
322
|
-
case 'radial': renderRadial(d3, container, spec); break;
|
|
323
|
-
case 'treemap': renderTreemap(d3, container, spec); break;
|
|
324
|
-
case 'force': renderForce(d3, container, spec); break;
|
|
325
|
-
default:
|
|
326
|
-
container.innerHTML = `<p class="text-xs text-text2 p-2">Preset inconnu : ${spec.preset}</p>`;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
331
|
-
onMount(async () => {
|
|
332
|
-
ro = new ResizeObserver((entries) => {
|
|
333
|
-
const r = entries[0]?.contentRect;
|
|
334
|
-
if (r) { width = r.width; height = r.height; }
|
|
335
|
-
});
|
|
336
|
-
ro.observe(container);
|
|
337
|
-
|
|
338
|
-
try {
|
|
339
|
-
const d3 = await import('d3');
|
|
340
|
-
loading = false;
|
|
341
|
-
render(d3);
|
|
342
|
-
} catch (e) {
|
|
343
|
-
error = e instanceof Error ? e.message : 'D3 load failed';
|
|
344
|
-
loading = false;
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
onDestroy(() => {
|
|
349
|
-
ro?.disconnect();
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
// Re-render when spec or size changes
|
|
353
|
-
$effect(() => {
|
|
354
|
-
void spec;
|
|
355
|
-
void width;
|
|
356
|
-
if (!loading && !error && width > 0) {
|
|
357
|
-
import('d3').then((d3) => render(d3));
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
</script>
|
|
361
|
-
|
|
362
|
-
<div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
|
|
363
|
-
{#if spec.title}
|
|
364
|
-
<h3 class="text-sm font-semibold text-text1 mb-3">{spec.title}</h3>
|
|
365
|
-
{/if}
|
|
366
|
-
{#if loading}
|
|
367
|
-
<div class="flex items-center justify-center h-48 text-text2 text-sm">Chargement D3...</div>
|
|
368
|
-
{:else if error}
|
|
369
|
-
<div class="text-accent2 text-sm">{error}</div>
|
|
370
|
-
{:else}
|
|
371
|
-
<div bind:this={container} class="w-full min-h-[200px]"></div>
|
|
372
|
-
{/if}
|
|
373
|
-
</div>
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
export interface DataTableColumn { key: string; label: string; align?: 'left'|'center'|'right'; type?: 'text'|'number'|'boolean'|'link'; }
|
|
3
|
-
export interface DataTableSpec { title?: string; columns?: DataTableColumn[]; rows?: Record<string,unknown>[]; compact?: boolean; striped?: boolean; emptyMessage?: string; }
|
|
4
|
-
interface Props { spec: Partial<DataTableSpec>; data?: unknown; onrowclick?: (row: Record<string,unknown>) => void; }
|
|
5
|
-
let { spec, data, onrowclick }: Props = $props();
|
|
6
|
-
const MAX = 200;
|
|
7
|
-
const fmt = new Intl.NumberFormat('fr-FR');
|
|
8
|
-
let sortCol = $state<string|null>(null), sortAsc = $state(true);
|
|
9
|
-
const rows = $derived<Record<string,unknown>[]>(Array.isArray(spec.rows)&&spec.rows.length ? spec.rows as Record<string,unknown>[] : Array.isArray(data) ? data as Record<string,unknown>[] : []);
|
|
10
|
-
const columns = $derived<DataTableColumn[]>(Array.isArray(spec.columns)&&(spec.columns as DataTableColumn[]).length ? spec.columns as DataTableColumn[] : rows.length>0 ? Object.keys(rows[0] as object).map(k=>({key:k,label:k})) : []);
|
|
11
|
-
const sorted = $derived.by<Record<string,unknown>[]>(()=>{
|
|
12
|
-
if (!sortCol) return rows;
|
|
13
|
-
return [...rows].sort((a,b)=>{ const av=a[sortCol!],bv=b[sortCol!],an=Number(av),bn=Number(bv); const c=!isNaN(an)&&!isNaN(bn)?an-bn:String(av??'').localeCompare(String(bv??''),'fr'); return sortAsc?c:-c; });
|
|
14
|
-
});
|
|
15
|
-
const displayed = $derived(sorted.slice(0,MAX));
|
|
16
|
-
const overflow = $derived(rows.length>MAX?rows.length-MAX:0);
|
|
17
|
-
function dv(v: unknown): string { if(v==null)return''; if(typeof v==='object')return JSON.stringify(v); return String(v); }
|
|
18
|
-
const compact = $derived(spec.compact===true);
|
|
19
|
-
</script>
|
|
20
|
-
<div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
|
|
21
|
-
{#if spec.title}<h3 class="text-sm font-semibold text-text1 mb-3">{spec.title}</h3>{/if}
|
|
22
|
-
{#if columns.length===0&&rows.length===0}
|
|
23
|
-
<p class="text-text2 text-sm">{spec.emptyMessage??'No data'}</p>
|
|
24
|
-
{:else}
|
|
25
|
-
<div class="overflow-auto max-h-[480px] rounded border border-border">
|
|
26
|
-
<table class="w-full border-collapse">
|
|
27
|
-
<thead>
|
|
28
|
-
<tr>
|
|
29
|
-
{#each columns as col}
|
|
30
|
-
<th class="sticky top-0 bg-surface2 border-b-2 border-border2 px-3 py-2 text-left text-xs font-mono font-medium text-text2 uppercase tracking-wider whitespace-nowrap cursor-pointer select-none hover:text-text1 transition-colors {col.align==='center'?'text-center':col.align==='right'?'text-right':''}"
|
|
31
|
-
onclick={()=>{ if(sortCol===col.key){sortAsc=!sortAsc;}else{sortCol=col.key;sortAsc=true;} }}>
|
|
32
|
-
{col.label}{#if sortCol===col.key}<span class="ml-1 text-accent">{sortAsc?'↑':'↓'}</span>{/if}
|
|
33
|
-
</th>
|
|
34
|
-
{/each}
|
|
35
|
-
</tr>
|
|
36
|
-
</thead>
|
|
37
|
-
<tbody>
|
|
38
|
-
{#each displayed as row, i}
|
|
39
|
-
<tr class="hover:bg-surface2 transition-colors {spec.striped!==false&&i%2===1?'bg-white/[0.02]':''} {onrowclick?'cursor-pointer':''}"
|
|
40
|
-
title={onrowclick?"Double-cliquez pour interagir":undefined}
|
|
41
|
-
ondblclick={()=>onrowclick?.(row)}>
|
|
42
|
-
{#each columns as col}
|
|
43
|
-
{@const val=row[col.key]}
|
|
44
|
-
<td class="border-b border-border text-text1 overflow-hidden text-ellipsis whitespace-nowrap max-w-[280px] {compact?'px-2 py-1 text-xs':'px-3 py-2 text-sm'} {col.align==='center'?'text-center':col.align==='right'?'text-right':''}">
|
|
45
|
-
{#if val==null}<span class="text-text2">—</span>
|
|
46
|
-
{:else if col.type==='boolean'}<span>{val?'✓':'✗'}</span>
|
|
47
|
-
{:else if col.type==='link'&&typeof val==='string'}<a href={val} class="text-accent hover:underline">{val}</a>
|
|
48
|
-
{:else if typeof val==='object'}<code class="text-xs bg-surface2 px-1 py-0.5 rounded">{JSON.stringify(val)}</code>
|
|
49
|
-
{:else if typeof val==='number'}<span title={String(val)}>{fmt.format(val)}</span>
|
|
50
|
-
{:else}{@const s=String(val)}<span title={s}>{s.length>80?s.slice(0,77)+'…':s}</span>
|
|
51
|
-
{/if}
|
|
52
|
-
</td>
|
|
53
|
-
{/each}
|
|
54
|
-
</tr>
|
|
55
|
-
{/each}
|
|
56
|
-
{#if overflow>0}<tr><td colspan={columns.length} class="text-center text-text2 text-xs py-2 px-3">… {overflow} more rows</td></tr>{/if}
|
|
57
|
-
</tbody>
|
|
58
|
-
</table>
|
|
59
|
-
</div>
|
|
60
|
-
<div class="mt-2 text-text2 text-xs">{rows.length} ligne{rows.length!==1?'s':''}</div>
|
|
61
|
-
{/if}
|
|
62
|
-
</div>
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import SafeImage from '../SafeImage.svelte';
|
|
3
|
-
export interface GalleryImage { src: string; alt?: string; caption?: string; href?: string; }
|
|
4
|
-
export interface GallerySpec { title?: string; images?: GalleryImage[]; columns?: number; gap?: string; emptyMessage?: string; }
|
|
5
|
-
interface Props { spec: Partial<GallerySpec>; data?: unknown; onimageclick?: (img: GalleryImage, index: number) => void; }
|
|
6
|
-
let { spec, data, onimageclick }: Props = $props();
|
|
7
|
-
|
|
8
|
-
const images = $derived.by<GalleryImage[]>(() => {
|
|
9
|
-
if (Array.isArray(spec.images) && spec.images.length) return spec.images;
|
|
10
|
-
if (Array.isArray(data)) return (data as GalleryImage[]);
|
|
11
|
-
return [];
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
let lightboxIdx = $state<number | null>(null);
|
|
15
|
-
const lightboxImg = $derived(lightboxIdx !== null ? images[lightboxIdx] : null);
|
|
16
|
-
|
|
17
|
-
function open(i: number) {
|
|
18
|
-
lightboxIdx = i;
|
|
19
|
-
}
|
|
20
|
-
function interact(i: number) {
|
|
21
|
-
onimageclick?.(images[i], i);
|
|
22
|
-
}
|
|
23
|
-
function close() { lightboxIdx = null; }
|
|
24
|
-
function prev() { if (lightboxIdx !== null && lightboxIdx > 0) lightboxIdx--; }
|
|
25
|
-
function next() { if (lightboxIdx !== null && lightboxIdx < images.length - 1) lightboxIdx++; }
|
|
26
|
-
function onKey(e: KeyboardEvent) {
|
|
27
|
-
if (lightboxIdx === null) return;
|
|
28
|
-
if (e.key === 'Escape') close();
|
|
29
|
-
else if (e.key === 'ArrowLeft') prev();
|
|
30
|
-
else if (e.key === 'ArrowRight') next();
|
|
31
|
-
}
|
|
32
|
-
</script>
|
|
33
|
-
|
|
34
|
-
<svelte:window onkeydown={onKey} />
|
|
35
|
-
|
|
36
|
-
<div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
|
|
37
|
-
{#if spec.title}<h3 class="text-sm font-semibold text-text1 mb-3">{spec.title}</h3>{/if}
|
|
38
|
-
{#if images.length === 0}
|
|
39
|
-
<p class="text-text2 text-sm">{spec.emptyMessage ?? 'Aucune image'}</p>
|
|
40
|
-
{:else if images.length === 1}
|
|
41
|
-
{@const img = images[0]}
|
|
42
|
-
<button class="relative overflow-hidden rounded-lg border border-border hover:border-border2 transition-all cursor-pointer bg-transparent p-0 group w-full"
|
|
43
|
-
onclick={() => open(0)}>
|
|
44
|
-
<SafeImage src={img.src} alt={img.alt ?? ''} class="w-full max-h-[400px] object-contain rounded-lg" loading="eager" />
|
|
45
|
-
{#if img.caption || img.alt}
|
|
46
|
-
<div class="mt-2 text-center text-xs text-text2">{img.caption ?? img.alt}</div>
|
|
47
|
-
{/if}
|
|
48
|
-
</button>
|
|
49
|
-
{:else}
|
|
50
|
-
<div class="grid gap-2 responsive-gallery" style="--gallery-cols: repeat({spec.columns ?? 3}, minmax(0, 1fr));">
|
|
51
|
-
{#each images as img, i}
|
|
52
|
-
<button class="relative overflow-hidden rounded-lg border border-border hover:border-border2 transition-all cursor-pointer bg-transparent p-0 group"
|
|
53
|
-
title={onimageclick?"Double-cliquez pour interagir":undefined}
|
|
54
|
-
onclick={() => open(i)}
|
|
55
|
-
ondblclick={() => interact(i)}>
|
|
56
|
-
<SafeImage src={img.src} alt={img.alt ?? ''} class="w-full h-32 sm:h-40 object-cover transition-transform group-hover:scale-105" loading="lazy" />
|
|
57
|
-
{#if img.caption}
|
|
58
|
-
<div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent px-2 py-1.5">
|
|
59
|
-
<span class="text-white text-xs">{img.caption}</span>
|
|
60
|
-
</div>
|
|
61
|
-
{/if}
|
|
62
|
-
</button>
|
|
63
|
-
{/each}
|
|
64
|
-
</div>
|
|
65
|
-
<div class="mt-2 text-xs text-text2">{images.length} image{images.length !== 1 ? 's' : ''}</div>
|
|
66
|
-
{/if}
|
|
67
|
-
|
|
68
|
-
{#if lightboxImg}
|
|
69
|
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
70
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
71
|
-
<div class="fixed inset-0 z-50 bg-black/80 flex items-center justify-center" onclick={close}>
|
|
72
|
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
73
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
74
|
-
<div class="relative max-w-[90vw] max-h-[90vh]" onclick={(e) => e.stopPropagation()}>
|
|
75
|
-
<SafeImage src={lightboxImg.src} alt={lightboxImg.alt ?? ''} class="max-w-full max-h-[85vh] object-contain rounded" />
|
|
76
|
-
{#if lightboxImg.caption}
|
|
77
|
-
<div class="text-center text-white text-sm mt-2">{lightboxImg.caption}</div>
|
|
78
|
-
{/if}
|
|
79
|
-
<button class="absolute top-2 right-2 w-8 h-8 rounded-full bg-black/50 text-white flex items-center justify-center text-lg hover:bg-black/70" onclick={close}>×</button>
|
|
80
|
-
{#if lightboxIdx !== null && lightboxIdx > 0}
|
|
81
|
-
<button class="absolute left-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-black/50 text-white flex items-center justify-center hover:bg-black/70" onclick={prev}>‹</button>
|
|
82
|
-
{/if}
|
|
83
|
-
{#if lightboxIdx !== null && lightboxIdx < images.length - 1}
|
|
84
|
-
<button class="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-black/50 text-white flex items-center justify-center hover:bg-black/70" onclick={next}>›</button>
|
|
85
|
-
{/if}
|
|
86
|
-
</div>
|
|
87
|
-
</div>
|
|
88
|
-
{/if}
|
|
89
|
-
</div>
|
|
90
|
-
|
|
91
|
-
<style>
|
|
92
|
-
.responsive-gallery { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
93
|
-
@media (min-width: 768px) { .responsive-gallery { grid-template-columns: var(--gallery-cols); } }
|
|
94
|
-
</style>
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
export interface GridDataColumn { key: string; label: string; width?: string; }
|
|
3
|
-
export interface GridDataHighlight { row: number; col: number; color?: string; }
|
|
4
|
-
export interface GridDataSpec { title?: string; columns?: GridDataColumn[]; rows?: unknown[][]; highlights?: GridDataHighlight[]; cellHeight?: number; }
|
|
5
|
-
interface Props { spec: Partial<GridDataSpec>; oncellclick?: (row: number, col: number, value: unknown) => void; }
|
|
6
|
-
let { spec, oncellclick }: Props = $props();
|
|
7
|
-
const columns=$derived(Array.isArray(spec.columns)?spec.columns:[]);
|
|
8
|
-
const rows=$derived(Array.isArray(spec.rows)?spec.rows:[]);
|
|
9
|
-
const cellH=$derived(spec.cellHeight??32);
|
|
10
|
-
const hlMap=$derived(()=>{const m=new Map<string,string>();if(Array.isArray(spec.highlights))for(const h of spec.highlights)m.set(`${h.row},${h.col}`,h.color??'color-mix(in srgb, var(--color-accent) 20%, transparent)');return m;});
|
|
11
|
-
function bg(r:number,c:number):string{return hlMap().get(`${r},${c}`)??'';}
|
|
12
|
-
function dv(v:unknown):string{if(v==null)return'';if(typeof v==='object')return JSON.stringify(v);return String(v);}
|
|
13
|
-
</script>
|
|
14
|
-
<div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
|
|
15
|
-
{#if spec.title}<h3 class="text-sm font-semibold text-text1 mb-3">{spec.title}</h3>{/if}
|
|
16
|
-
{#if !columns.length&&!rows.length}<p class="text-text2 text-sm">No data</p>
|
|
17
|
-
{:else}
|
|
18
|
-
<div class="overflow-auto rounded border border-border">
|
|
19
|
-
<table class="w-full border-collapse text-xs font-mono">
|
|
20
|
-
{#if columns.length}
|
|
21
|
-
<thead>
|
|
22
|
-
<tr>
|
|
23
|
-
{#each columns as col}<th class="sticky top-0 bg-surface2 px-3 py-2 text-left text-text2 border-b border-r border-border whitespace-nowrap font-medium" style={col.width?`width:${col.width}`:''}>
|
|
24
|
-
{col.label}
|
|
25
|
-
</th>{/each}
|
|
26
|
-
</tr>
|
|
27
|
-
</thead>
|
|
28
|
-
{/if}
|
|
29
|
-
<tbody>
|
|
30
|
-
{#each rows as row, ri}
|
|
31
|
-
<tr class="hover:bg-surface2">
|
|
32
|
-
{#each (Array.isArray(row)?row:[]) as cell, ci}
|
|
33
|
-
<td class="px-3 text-text2 border-b border-r border-border {oncellclick?'cursor-pointer':''} hover:bg-surface2"
|
|
34
|
-
style="height:{cellH}px;{bg(ri,ci)?`background:${bg(ri,ci)};`:''}"
|
|
35
|
-
title={oncellclick?"Double-cliquez pour interagir":undefined}
|
|
36
|
-
ondblclick={()=>oncellclick?.(ri,ci,cell)}>{dv(cell)}</td>
|
|
37
|
-
{/each}
|
|
38
|
-
</tr>
|
|
39
|
-
{/each}
|
|
40
|
-
</tbody>
|
|
41
|
-
</table>
|
|
42
|
-
</div>
|
|
43
|
-
{/if}
|
|
44
|
-
</div>
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
export interface HemicycleGroup { id: string; label: string; seats: number; color: string; }
|
|
3
|
-
export interface HemicycleSpec { title?: string; groups?: HemicycleGroup[]; totalSeats?: number; rows?: number; }
|
|
4
|
-
interface Props { spec: Partial<HemicycleSpec>; ongroupclick?: (g: HemicycleGroup) => void; }
|
|
5
|
-
let { spec, ongroupclick }: Props = $props();
|
|
6
|
-
const groups = $derived<HemicycleGroup[]>(Array.isArray(spec.groups) ? spec.groups : []);
|
|
7
|
-
const total = $derived(spec.totalSeats ?? groups.reduce((s,g)=>s+g.seats,0));
|
|
8
|
-
const W=420, H=230, cx=W/2, cy=H-10, rMin=60, step=28;
|
|
9
|
-
const rows = $derived(spec.rows ?? Math.min(Math.max(3, Math.ceil(Math.sqrt(total/6))), 7));
|
|
10
|
-
interface Seat { x: number; y: number; color: string; gid: string }
|
|
11
|
-
const seats = $derived.by<Seat[]>(()=>{
|
|
12
|
-
if(!groups.length||!total) return [];
|
|
13
|
-
const radii = Array.from({length:rows},(_,i)=>rMin+i*step);
|
|
14
|
-
const circs = radii.map(r=>Math.PI*r);
|
|
15
|
-
const totalC = circs.reduce((a,b)=>a+b,0);
|
|
16
|
-
const spr = radii.map(r=>Math.round(Math.PI*r/totalC*total));
|
|
17
|
-
spr[spr.length-1]+=total-spr.reduce((a,b)=>a+b,0);
|
|
18
|
-
const sorted=[...groups].sort((a,b)=>a.seats-b.seats);
|
|
19
|
-
const colors: {color:string;gid:string}[]=[];
|
|
20
|
-
for(const g of sorted) for(let i=0;i<g.seats;i++) colors.push({color:g.color,gid:g.id});
|
|
21
|
-
while(colors.length<total) colors.push({color:'#333355',gid:''});
|
|
22
|
-
const result: Seat[]=[];
|
|
23
|
-
let idx=0;
|
|
24
|
-
for(let row=0;row<rows;row++){
|
|
25
|
-
const r=radii[row], n=spr[row];
|
|
26
|
-
for(let j=0;j<n;j++){
|
|
27
|
-
if(idx>=colors.length) break;
|
|
28
|
-
const angle=Math.PI-(j/(n-1||1))*Math.PI;
|
|
29
|
-
result.push({x:cx+r*Math.cos(angle),y:cy-r*Math.sin(angle),...colors[idx++]});
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return result;
|
|
33
|
-
});
|
|
34
|
-
const rMax = $derived(rMin+rows*step);
|
|
35
|
-
const legend = $derived([...groups].sort((a,b)=>b.seats-a.seats));
|
|
36
|
-
let tooltip = $state<{label:string;seats:number}|null>(null);
|
|
37
|
-
</script>
|
|
38
|
-
<div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
|
|
39
|
-
{#if spec.title}<h3 class="text-sm font-semibold text-text1 mb-3">{spec.title}</h3>{/if}
|
|
40
|
-
{#if !groups.length||!total}<p class="text-text2 text-sm">No data</p>
|
|
41
|
-
{:else}
|
|
42
|
-
<div class="relative">
|
|
43
|
-
<svg viewBox="0 0 {W} {H}" class="block w-full max-h-[220px]" xmlns="http://www.w3.org/2000/svg">
|
|
44
|
-
<path d="M {cx-rMax-15} {cy} A {rMax+15} {rMax+15} 0 0 1 {cx+rMax+15} {cy}" fill="none" stroke="var(--color-surface2)" stroke-width="2"/>
|
|
45
|
-
{#each seats as s}
|
|
46
|
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
47
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
48
|
-
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
|
49
|
-
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
50
|
-
<circle cx={s.x.toFixed(1)} cy={s.y.toFixed(1)} r="4" fill={s.color} opacity="0.9"
|
|
51
|
-
class={s.gid?'cursor-pointer':''}
|
|
52
|
-
onmouseenter={()=>{const g=groups.find(g=>g.id===s.gid);if(g)tooltip={label:g.label,seats:g.seats};}}
|
|
53
|
-
onmouseleave={()=>tooltip=null}
|
|
54
|
-
ondblclick={()=>{const g=groups.find(g=>g.id===s.gid);if(g)ongroupclick?.(g);}}>
|
|
55
|
-
{#if s.gid}<title>Double-click to interact</title>{/if}
|
|
56
|
-
</circle>
|
|
57
|
-
{/each}
|
|
58
|
-
<text x={cx} y={cy+18} text-anchor="middle" font-size="11" fill="var(--color-text2)" font-family="system-ui">{total} seats</text>
|
|
59
|
-
</svg>
|
|
60
|
-
{#if tooltip}
|
|
61
|
-
<div class="absolute top-0 right-0 bg-surface2 border border-border2 rounded px-2 py-1 text-xs text-text1 pointer-events-none">
|
|
62
|
-
<span class="font-semibold">{tooltip.label}</span> — {tooltip.seats}
|
|
63
|
-
</div>
|
|
64
|
-
{/if}
|
|
65
|
-
</div>
|
|
66
|
-
<div class="flex flex-wrap gap-x-4 gap-y-1 mt-3">
|
|
67
|
-
{#each legend as g}
|
|
68
|
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
69
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
70
|
-
<div class="flex items-center gap-1.5 text-xs cursor-pointer hover:opacity-80" role="button" tabindex="0" title="Double-click to interact" ondblclick={()=>ongroupclick?.(g)} onkeydown={(e)=>{if(e.key==="Enter")ongroupclick?.(g)}}>
|
|
71
|
-
<div class="w-3 h-3 rounded-full flex-shrink-0" style="background:{g.color};"></div>
|
|
72
|
-
<span class="text-text2">{g.label}</span>
|
|
73
|
-
<span class="text-text2">{g.seats}</span>
|
|
74
|
-
</div>
|
|
75
|
-
{/each}
|
|
76
|
-
</div>
|
|
77
|
-
{/if}
|
|
78
|
-
</div>
|