@sentropic/design-system-svelte 0.10.2 → 0.10.4
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/dist/AreaChart.svelte +38 -26
- package/dist/AreaChart.svelte.d.ts.map +1 -1
- package/dist/BackToTop.svelte +157 -0
- package/dist/BackToTop.svelte.d.ts +14 -0
- package/dist/BackToTop.svelte.d.ts.map +1 -0
- package/dist/BarChart.svelte +38 -39
- package/dist/BarChart.svelte.d.ts.map +1 -1
- package/dist/Card.svelte +2 -0
- package/dist/ChartDataList.svelte +33 -0
- package/dist/ChartDataList.svelte.d.ts +9 -0
- package/dist/ChartDataList.svelte.d.ts.map +1 -0
- package/dist/ChatComposer.svelte +20 -3
- package/dist/ChatComposer.svelte.d.ts +6 -30
- package/dist/ChatComposer.svelte.d.ts.map +1 -1
- package/dist/Checkbox.svelte +4 -0
- package/dist/Combobox.svelte +1 -1
- package/dist/ContentSwitcher.svelte +1 -0
- package/dist/DataTable.svelte +4 -1
- package/dist/DatePicker.svelte +1 -1
- package/dist/DisplaySettings.svelte +210 -0
- package/dist/DisplaySettings.svelte.d.ts +24 -0
- package/dist/DisplaySettings.svelte.d.ts.map +1 -0
- package/dist/DonutChart.svelte +44 -29
- package/dist/DonutChart.svelte.d.ts.map +1 -1
- package/dist/Dropdown.svelte +1 -1
- package/dist/FileUploader.svelte +2 -2
- package/dist/ForceGraph.svelte +485 -22
- package/dist/ForceGraph.svelte.d.ts +48 -0
- package/dist/ForceGraph.svelte.d.ts.map +1 -1
- package/dist/GraphLegend.svelte +142 -0
- package/dist/GraphLegend.svelte.d.ts +12 -0
- package/dist/GraphLegend.svelte.d.ts.map +1 -0
- package/dist/Header.svelte +2 -1
- package/dist/IconButton.svelte +1 -1
- package/dist/InlineLoading.svelte +10 -1
- package/dist/InlineLoading.svelte.d.ts.map +1 -1
- package/dist/Input.svelte +3 -2
- package/dist/LanguageSelector.svelte +2 -1
- package/dist/LineChart.svelte +38 -26
- package/dist/LineChart.svelte.d.ts.map +1 -1
- package/dist/Link.svelte +7 -1
- package/dist/MediaContent.svelte +124 -0
- package/dist/MediaContent.svelte.d.ts +22 -0
- package/dist/MediaContent.svelte.d.ts.map +1 -0
- package/dist/Menu.svelte +56 -3
- package/dist/Menu.svelte.d.ts.map +1 -1
- package/dist/MessageStatusBadge.svelte +1 -1
- package/dist/MultiSelect.svelte +2 -2
- package/dist/Notification.svelte +150 -0
- package/dist/Notification.svelte.d.ts +17 -0
- package/dist/Notification.svelte.d.ts.map +1 -0
- package/dist/NumberInput.svelte +1 -0
- package/dist/OverflowMenu.svelte +84 -13
- package/dist/OverflowMenu.svelte.d.ts.map +1 -1
- package/dist/Pagination.svelte +7 -0
- package/dist/PaginationNav.svelte +2 -2
- package/dist/ProgressIndicator.svelte +13 -1
- package/dist/ProgressIndicator.svelte.d.ts +1 -0
- package/dist/ProgressIndicator.svelte.d.ts.map +1 -1
- package/dist/Radio.svelte +7 -3
- package/dist/ScatterPlot.svelte +64 -45
- package/dist/ScatterPlot.svelte.d.ts.map +1 -1
- package/dist/Search.svelte +6 -3
- package/dist/Select.svelte +8 -2
- package/dist/SideNav.svelte +6 -0
- package/dist/StackedBarChart.svelte +51 -30
- package/dist/StackedBarChart.svelte.d.ts.map +1 -1
- package/dist/StreamingMessage.svelte +2 -2
- package/dist/Switch.svelte +4 -0
- package/dist/Table.svelte +4 -1
- package/dist/TableOfContents.svelte +109 -0
- package/dist/TableOfContents.svelte.d.ts +16 -0
- package/dist/TableOfContents.svelte.d.ts.map +1 -0
- package/dist/Tag.svelte +1 -1
- package/dist/Textarea.svelte +3 -2
- package/dist/Tile.svelte +4 -0
- package/dist/TileGroup.svelte +4 -0
- package/dist/Toggle.svelte +4 -0
- package/dist/Toggletip.svelte +1 -1
- package/dist/Transcription.svelte +135 -0
- package/dist/Transcription.svelte.d.ts +19 -0
- package/dist/Transcription.svelte.d.ts.map +1 -0
- package/dist/TreeView.svelte +2 -2
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/package.json +2 -2
package/dist/ForceGraph.svelte
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
| "category1" | "category2" | "category3" | "category4"
|
|
4
4
|
| "category5" | "category6" | "category7" | "category8";
|
|
5
5
|
|
|
6
|
+
export type ForceGraphNodeShape =
|
|
7
|
+
| "dot" | "circle"
|
|
8
|
+
| "diamond"
|
|
9
|
+
| "star"
|
|
10
|
+
| "hexagon"
|
|
11
|
+
| "box" | "square"
|
|
12
|
+
| "triangle";
|
|
13
|
+
|
|
6
14
|
export type ForceGraphNode = {
|
|
7
15
|
/** Stable identifier; referenced by edges. */
|
|
8
16
|
id: string;
|
|
@@ -20,6 +28,11 @@
|
|
|
20
28
|
/** Pin the node to a fixed position (ignored by the simulation). */
|
|
21
29
|
fx?: number;
|
|
22
30
|
fy?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Visual shape for the node. Defaults to 'dot' (circle).
|
|
33
|
+
* Supported: 'dot'|'circle', 'diamond', 'star', 'hexagon', 'box'|'square', 'triangle'.
|
|
34
|
+
*/
|
|
35
|
+
shape?: ForceGraphNodeShape;
|
|
23
36
|
};
|
|
24
37
|
|
|
25
38
|
export type ForceGraphEdge = {
|
|
@@ -35,6 +48,57 @@
|
|
|
35
48
|
*/
|
|
36
49
|
weak?: boolean;
|
|
37
50
|
};
|
|
51
|
+
|
|
52
|
+
export type ForceGraphLegendEntry = {
|
|
53
|
+
/** Label shown in the legend. */
|
|
54
|
+
label: string;
|
|
55
|
+
/** Shape for this entry (node legend). Absent = line-style legend entry. */
|
|
56
|
+
shape?: ForceGraphNodeShape;
|
|
57
|
+
/** Tone for this entry. Defaults to category1. */
|
|
58
|
+
tone?: ForceGraphTone;
|
|
59
|
+
/** When true, renders as a dashed line (edge legend). */
|
|
60
|
+
weak?: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// SVG path helpers for the various node shapes.
|
|
65
|
+
// All shapes are centered at (0,0) and sized to inscribe within radius r.
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
export function nodeShapePath(shape: ForceGraphNodeShape | undefined, r: number): string | null {
|
|
68
|
+
const s = shape ?? "dot";
|
|
69
|
+
if (s === "dot" || s === "circle") return null; // use <circle>
|
|
70
|
+
if (s === "diamond") {
|
|
71
|
+
return `M 0 ${-r} L ${r} 0 L 0 ${r} L ${-r} 0 Z`;
|
|
72
|
+
}
|
|
73
|
+
if (s === "star") {
|
|
74
|
+
const outer = r;
|
|
75
|
+
const inner = r * 0.42;
|
|
76
|
+
const pts: string[] = [];
|
|
77
|
+
for (let i = 0; i < 10; i++) {
|
|
78
|
+
const angle = (i * Math.PI) / 5 - Math.PI / 2;
|
|
79
|
+
const rad = i % 2 === 0 ? outer : inner;
|
|
80
|
+
pts.push(`${rad * Math.cos(angle)},${rad * Math.sin(angle)}`);
|
|
81
|
+
}
|
|
82
|
+
return `M ${pts.join(" L ")} Z`;
|
|
83
|
+
}
|
|
84
|
+
if (s === "hexagon") {
|
|
85
|
+
const pts: string[] = [];
|
|
86
|
+
for (let i = 0; i < 6; i++) {
|
|
87
|
+
const angle = (i * Math.PI) / 3 - Math.PI / 6;
|
|
88
|
+
pts.push(`${r * Math.cos(angle)},${r * Math.sin(angle)}`);
|
|
89
|
+
}
|
|
90
|
+
return `M ${pts.join(" L ")} Z`;
|
|
91
|
+
}
|
|
92
|
+
if (s === "box" || s === "square") {
|
|
93
|
+
const h = r * 0.85;
|
|
94
|
+
return `M ${-h} ${-h} L ${h} ${-h} L ${h} ${h} L ${-h} ${h} Z`;
|
|
95
|
+
}
|
|
96
|
+
if (s === "triangle") {
|
|
97
|
+
const h = r * 1.1;
|
|
98
|
+
return `M 0 ${-h} L ${h * 0.9} ${h * 0.6} L ${-h * 0.9} ${h * 0.6} Z`;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
38
102
|
</script>
|
|
39
103
|
|
|
40
104
|
<script lang="ts">
|
|
@@ -54,6 +118,37 @@
|
|
|
54
118
|
* animates the remainder unless reduced motion is requested.
|
|
55
119
|
*/
|
|
56
120
|
iterations?: number;
|
|
121
|
+
/**
|
|
122
|
+
* IDs of currently selected nodes. Highlighted visually without
|
|
123
|
+
* re-running the layout. Defaults to [].
|
|
124
|
+
*/
|
|
125
|
+
selectedIds?: string[];
|
|
126
|
+
/**
|
|
127
|
+
* ID of the node to focus/centre visually (ring highlight). Does not
|
|
128
|
+
* re-run the layout. Defaults to null.
|
|
129
|
+
*/
|
|
130
|
+
focusId?: string | null;
|
|
131
|
+
/**
|
|
132
|
+
* Called when the user clicks (or presses Space/Enter) a node.
|
|
133
|
+
* Fires with the node's stable id.
|
|
134
|
+
*/
|
|
135
|
+
onSelect?: (id: string) => void;
|
|
136
|
+
/**
|
|
137
|
+
* Called when the user activates a node (double-click or Enter key while
|
|
138
|
+
* keyboard-focused). Intended to open a detail panel.
|
|
139
|
+
* Fires with the node's stable id.
|
|
140
|
+
*/
|
|
141
|
+
onOpenEntity?: (id: string) => void;
|
|
142
|
+
/**
|
|
143
|
+
* Called when the user hovers an edge.
|
|
144
|
+
* Fires with the edge object (source/target/relation/weak).
|
|
145
|
+
*/
|
|
146
|
+
onEdgeHover?: (edge: ForceGraphEdge) => void;
|
|
147
|
+
/**
|
|
148
|
+
* Legend entries rendered as a corner overlay.
|
|
149
|
+
* Each entry has a label + optional shape (node) or weak (edge).
|
|
150
|
+
*/
|
|
151
|
+
legend?: ForceGraphLegendEntry[];
|
|
57
152
|
class?: string;
|
|
58
153
|
};
|
|
59
154
|
|
|
@@ -66,6 +161,12 @@
|
|
|
66
161
|
nodeRadius = 7,
|
|
67
162
|
showLabels = true,
|
|
68
163
|
iterations = 300,
|
|
164
|
+
selectedIds = [],
|
|
165
|
+
focusId = null,
|
|
166
|
+
onSelect,
|
|
167
|
+
onOpenEntity,
|
|
168
|
+
onEdgeHover,
|
|
169
|
+
legend,
|
|
69
170
|
class: className
|
|
70
171
|
}: ForceGraphProps = $props();
|
|
71
172
|
|
|
@@ -237,30 +338,134 @@
|
|
|
237
338
|
const positionedNodes = $derived.by(() =>
|
|
238
339
|
nodes.map((n, i) => {
|
|
239
340
|
const p = layout.get(n.id) ?? { x: width / 2, y: height / 2 };
|
|
341
|
+
const r = nodeRadius * Math.sqrt(Math.max(n.weight ?? 1, 0.25));
|
|
342
|
+
const shapePath = nodeShapePath(n.shape, r);
|
|
240
343
|
return {
|
|
241
344
|
node: n,
|
|
242
345
|
i,
|
|
243
346
|
x: p.x,
|
|
244
347
|
y: p.y,
|
|
245
|
-
r
|
|
348
|
+
r,
|
|
246
349
|
tone: toneMap.get(n.id) ?? "category1",
|
|
247
|
-
title: n.label ?? n.id
|
|
350
|
+
title: n.label ?? n.id,
|
|
351
|
+
shapePath
|
|
248
352
|
};
|
|
249
353
|
})
|
|
250
354
|
);
|
|
251
355
|
|
|
252
356
|
const positionedEdges = $derived.by(() => {
|
|
357
|
+
const nodeById = new Map(nodes.map((n) => [n.id, n]));
|
|
253
358
|
return edges
|
|
254
359
|
.map((e, i) => {
|
|
255
360
|
const a = layout.get(e.source);
|
|
256
361
|
const b = layout.get(e.target);
|
|
257
362
|
if (!a || !b) return null;
|
|
258
|
-
|
|
363
|
+
const srcNode = nodeById.get(e.source);
|
|
364
|
+
const tgtNode = nodeById.get(e.target);
|
|
365
|
+
return {
|
|
366
|
+
edge: e,
|
|
367
|
+
i,
|
|
368
|
+
x1: a.x,
|
|
369
|
+
y1: a.y,
|
|
370
|
+
x2: b.x,
|
|
371
|
+
y2: b.y,
|
|
372
|
+
srcLabel: srcNode?.label ?? e.source,
|
|
373
|
+
tgtLabel: tgtNode?.label ?? e.target
|
|
374
|
+
};
|
|
259
375
|
})
|
|
260
376
|
.filter((e): e is NonNullable<typeof e> => e !== null);
|
|
261
377
|
});
|
|
262
378
|
|
|
263
|
-
let
|
|
379
|
+
let hoveredNodeIndex: number | null = $state(null);
|
|
380
|
+
let hoveredEdgeIndex: number | null = $state(null);
|
|
381
|
+
|
|
382
|
+
// Fast lookup sets — recomputed only when selectedIds/focusId props change,
|
|
383
|
+
// never when nodes/edges change.
|
|
384
|
+
const selectedSet = $derived(new Set<string>(selectedIds));
|
|
385
|
+
|
|
386
|
+
// Keyboard handler for a node circle: Space/Enter → onSelect, Enter → onOpenEntity.
|
|
387
|
+
function handleNodeKeydown(id: string, e: KeyboardEvent) {
|
|
388
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
389
|
+
e.preventDefault();
|
|
390
|
+
onSelect?.(id);
|
|
391
|
+
}
|
|
392
|
+
if (e.key === "Enter") {
|
|
393
|
+
onOpenEntity?.(id);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// Zoom + pan state (Feature 2)
|
|
399
|
+
// Store zoom as a scale multiplier + pan offset so syncing with width/height
|
|
400
|
+
// props is trivial (no stale-capture warnings).
|
|
401
|
+
// vbW = width / zoomScale, vbH = height / zoomScale
|
|
402
|
+
// vbX / vbY = pan offset in SVG coordinate space
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
let zoomScale = $state(1);
|
|
405
|
+
let panX = $state(0);
|
|
406
|
+
let panY = $state(0);
|
|
407
|
+
|
|
408
|
+
let isPanning = $state(false);
|
|
409
|
+
let panStart = $state({ x: 0, y: 0, panX: 0, panY: 0 });
|
|
410
|
+
let svgEl: SVGSVGElement | null = $state(null);
|
|
411
|
+
|
|
412
|
+
// Derived viewBox dimensions always reflect current props + zoom.
|
|
413
|
+
const vbW = $derived(width / zoomScale);
|
|
414
|
+
const vbH = $derived(height / zoomScale);
|
|
415
|
+
const vbX = $derived(panX);
|
|
416
|
+
const vbY = $derived(panY);
|
|
417
|
+
|
|
418
|
+
function resetView() {
|
|
419
|
+
zoomScale = 1;
|
|
420
|
+
panX = 0;
|
|
421
|
+
panY = 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function handleWheel(ev: WheelEvent) {
|
|
425
|
+
if (prefersReducedMotion) return;
|
|
426
|
+
ev.preventDefault();
|
|
427
|
+
// Zoom factor: ~10% per step.
|
|
428
|
+
const factor = ev.deltaY > 0 ? 0.9 : 1.1;
|
|
429
|
+
// Clamp zoom: 0.2x – 8x.
|
|
430
|
+
const newScale = Math.min(Math.max(zoomScale * factor, 0.2), 8);
|
|
431
|
+
// Anchor zoom around the cursor position in SVG coords.
|
|
432
|
+
if (svgEl) {
|
|
433
|
+
const rect = svgEl.getBoundingClientRect();
|
|
434
|
+
const cursorSvgX = panX + ((ev.clientX - rect.left) / rect.width) * (width / zoomScale);
|
|
435
|
+
const cursorSvgY = panY + ((ev.clientY - rect.top) / rect.height) * (height / zoomScale);
|
|
436
|
+
const newVbW = width / newScale;
|
|
437
|
+
const newVbH = height / newScale;
|
|
438
|
+
const ratioX = (cursorSvgX - panX) / (width / zoomScale);
|
|
439
|
+
const ratioY = (cursorSvgY - panY) / (height / zoomScale);
|
|
440
|
+
panX = cursorSvgX - ratioX * newVbW;
|
|
441
|
+
panY = cursorSvgY - ratioY * newVbH;
|
|
442
|
+
}
|
|
443
|
+
zoomScale = newScale;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function handleBgMouseDown(ev: MouseEvent) {
|
|
447
|
+
// Only start pan when clicking the background (not a node/edge element).
|
|
448
|
+
if ((ev.target as Element).closest(".st-forceGraph__node")) return;
|
|
449
|
+
if (prefersReducedMotion) return;
|
|
450
|
+
isPanning = true;
|
|
451
|
+
panStart = { x: ev.clientX, y: ev.clientY, panX, panY };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function handleMouseMove(ev: MouseEvent) {
|
|
455
|
+
if (!isPanning || !svgEl) return;
|
|
456
|
+
const rect = svgEl.getBoundingClientRect();
|
|
457
|
+
const dx = ((ev.clientX - panStart.x) / rect.width) * vbW;
|
|
458
|
+
const dy = ((ev.clientY - panStart.y) / rect.height) * vbH;
|
|
459
|
+
panX = panStart.panX - dx;
|
|
460
|
+
panY = panStart.panY - dy;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function handleMouseUp() {
|
|
464
|
+
isPanning = false;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const viewBox = $derived(`${vbX} ${vbY} ${vbW} ${vbH}`);
|
|
468
|
+
const isZoomed = $derived(zoomScale !== 1 || panX !== 0 || panY !== 0);
|
|
264
469
|
|
|
265
470
|
const classes = () =>
|
|
266
471
|
["st-forceGraph", prefersReducedMotion ? "st-forceGraph--static" : null, className]
|
|
@@ -268,25 +473,49 @@
|
|
|
268
473
|
.join(" ");
|
|
269
474
|
</script>
|
|
270
475
|
|
|
271
|
-
<div
|
|
476
|
+
<div
|
|
477
|
+
class={classes()}
|
|
478
|
+
role="img"
|
|
479
|
+
aria-label={label}
|
|
480
|
+
>
|
|
272
481
|
<svg
|
|
273
|
-
|
|
482
|
+
bind:this={svgEl}
|
|
483
|
+
viewBox={viewBox}
|
|
274
484
|
preserveAspectRatio="xMidYMid meet"
|
|
275
485
|
width="100%"
|
|
276
486
|
height="100%"
|
|
277
487
|
focusable="false"
|
|
278
488
|
aria-hidden="true"
|
|
489
|
+
class:st-forceGraph__svg--panning={isPanning}
|
|
490
|
+
onwheel={handleWheel}
|
|
491
|
+
onmousedown={handleBgMouseDown}
|
|
492
|
+
onmousemove={handleMouseMove}
|
|
493
|
+
onmouseup={handleMouseUp}
|
|
494
|
+
onmouseleave={handleMouseUp}
|
|
279
495
|
>
|
|
280
496
|
<!-- edges first so nodes paint on top -->
|
|
281
497
|
<g class="st-forceGraph__edges">
|
|
282
498
|
{#each positionedEdges as e (e.i)}
|
|
499
|
+
<!-- Invisible wider hit area for edge hover -->
|
|
500
|
+
<line
|
|
501
|
+
class="st-forceGraph__edgeHit"
|
|
502
|
+
role="presentation"
|
|
503
|
+
x1={e.x1}
|
|
504
|
+
y1={e.y1}
|
|
505
|
+
x2={e.x2}
|
|
506
|
+
y2={e.y2}
|
|
507
|
+
onmouseenter={() => { hoveredEdgeIndex = e.i; onEdgeHover?.(e.edge); }}
|
|
508
|
+
onmouseleave={() => { hoveredEdgeIndex = null; }}
|
|
509
|
+
/>
|
|
283
510
|
<line
|
|
284
511
|
class="st-forceGraph__edge"
|
|
285
512
|
class:st-forceGraph__edge--weak={e.edge.weak}
|
|
513
|
+
class:st-forceGraph__edge--hovered={hoveredEdgeIndex === e.i}
|
|
286
514
|
x1={e.x1}
|
|
287
515
|
y1={e.y1}
|
|
288
516
|
x2={e.x2}
|
|
289
517
|
y2={e.y2}
|
|
518
|
+
pointer-events="none"
|
|
290
519
|
/>
|
|
291
520
|
{/each}
|
|
292
521
|
</g>
|
|
@@ -295,20 +524,44 @@
|
|
|
295
524
|
{#each positionedNodes as p (p.node.id)}
|
|
296
525
|
<g
|
|
297
526
|
class="st-forceGraph__node st-forceGraph__node--{p.tone}"
|
|
298
|
-
class:st-forceGraph__node--dim={
|
|
527
|
+
class:st-forceGraph__node--dim={hoveredNodeIndex !== null && hoveredNodeIndex !== p.i}
|
|
528
|
+
class:st-forceGraph__node--selected={selectedSet.has(p.node.id)}
|
|
529
|
+
class:st-forceGraph__node--focus={focusId === p.node.id}
|
|
299
530
|
transform="translate({p.x} {p.y})"
|
|
300
531
|
>
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
532
|
+
{#if p.shapePath}
|
|
533
|
+
<path
|
|
534
|
+
class="st-forceGraph__dot"
|
|
535
|
+
d={p.shapePath}
|
|
536
|
+
tabindex="0"
|
|
537
|
+
role="button"
|
|
538
|
+
aria-label="{p.title}{p.node.group !== undefined ? `: ${p.node.group}` : ''}"
|
|
539
|
+
aria-pressed={selectedSet.has(p.node.id)}
|
|
540
|
+
onmouseenter={() => (hoveredNodeIndex = p.i)}
|
|
541
|
+
onmouseleave={() => (hoveredNodeIndex = null)}
|
|
542
|
+
onfocus={() => (hoveredNodeIndex = p.i)}
|
|
543
|
+
onblur={() => (hoveredNodeIndex = null)}
|
|
544
|
+
onclick={() => onSelect?.(p.node.id)}
|
|
545
|
+
ondblclick={() => onOpenEntity?.(p.node.id)}
|
|
546
|
+
onkeydown={(e) => handleNodeKeydown(p.node.id, e)}
|
|
547
|
+
/>
|
|
548
|
+
{:else}
|
|
549
|
+
<circle
|
|
550
|
+
class="st-forceGraph__dot"
|
|
551
|
+
r={p.r}
|
|
552
|
+
tabindex="0"
|
|
553
|
+
role="button"
|
|
554
|
+
aria-label="{p.title}{p.node.group !== undefined ? `: ${p.node.group}` : ''}"
|
|
555
|
+
aria-pressed={selectedSet.has(p.node.id)}
|
|
556
|
+
onmouseenter={() => (hoveredNodeIndex = p.i)}
|
|
557
|
+
onmouseleave={() => (hoveredNodeIndex = null)}
|
|
558
|
+
onfocus={() => (hoveredNodeIndex = p.i)}
|
|
559
|
+
onblur={() => (hoveredNodeIndex = null)}
|
|
560
|
+
onclick={() => onSelect?.(p.node.id)}
|
|
561
|
+
ondblclick={() => onOpenEntity?.(p.node.id)}
|
|
562
|
+
onkeydown={(e) => handleNodeKeydown(p.node.id, e)}
|
|
563
|
+
/>
|
|
564
|
+
{/if}
|
|
312
565
|
{#if showLabels}
|
|
313
566
|
<text class="st-forceGraph__label" x={p.r + 3} y="0" dominant-baseline="middle">{p.title}</text>
|
|
314
567
|
{/if}
|
|
@@ -317,15 +570,16 @@
|
|
|
317
570
|
</g>
|
|
318
571
|
</svg>
|
|
319
572
|
|
|
320
|
-
|
|
321
|
-
|
|
573
|
+
<!-- Node tooltip -->
|
|
574
|
+
{#if hoveredNodeIndex !== null && positionedNodes[hoveredNodeIndex]}
|
|
575
|
+
{@const p = positionedNodes[hoveredNodeIndex]}
|
|
322
576
|
{@const relCount = positionedEdges.filter(
|
|
323
577
|
(e) => e.edge.source === p.node.id || e.edge.target === p.node.id
|
|
324
578
|
).length}
|
|
325
579
|
<div
|
|
326
580
|
class="st-forceGraph__tooltip"
|
|
327
581
|
role="presentation"
|
|
328
|
-
style="left: {(p.x /
|
|
582
|
+
style="left: {((p.x - vbX) / vbW) * 100}%; top: {((p.y - vbY) / vbH) * 100}%"
|
|
329
583
|
>
|
|
330
584
|
<span class="st-forceGraph__tooltipLabel">{p.title}</span>
|
|
331
585
|
{#if p.node.group !== undefined}
|
|
@@ -336,6 +590,91 @@
|
|
|
336
590
|
{/if}
|
|
337
591
|
</div>
|
|
338
592
|
{/if}
|
|
593
|
+
|
|
594
|
+
<!-- Edge tooltip -->
|
|
595
|
+
{#if hoveredEdgeIndex !== null}
|
|
596
|
+
{@const e = positionedEdges.find((pe) => pe.i === hoveredEdgeIndex)}
|
|
597
|
+
{#if e}
|
|
598
|
+
{@const midX = (e.x1 + e.x2) / 2}
|
|
599
|
+
{@const midY = (e.y1 + e.y2) / 2}
|
|
600
|
+
<div
|
|
601
|
+
class="st-forceGraph__tooltip st-forceGraph__tooltip--edge"
|
|
602
|
+
role="presentation"
|
|
603
|
+
style="left: {((midX - vbX) / vbW) * 100}%; top: {((midY - vbY) / vbH) * 100}%"
|
|
604
|
+
>
|
|
605
|
+
<span class="st-forceGraph__tooltipLabel">{e.srcLabel}</span>
|
|
606
|
+
{#if e.edge.relation}
|
|
607
|
+
<span class="st-forceGraph__tooltipRelation">{e.edge.relation}</span>
|
|
608
|
+
{/if}
|
|
609
|
+
<span class="st-forceGraph__tooltipLabel">{e.tgtLabel}</span>
|
|
610
|
+
</div>
|
|
611
|
+
{/if}
|
|
612
|
+
{/if}
|
|
613
|
+
|
|
614
|
+
<!-- Reset view button (only shown when zoomed/panned) -->
|
|
615
|
+
{#if isZoomed}
|
|
616
|
+
<button
|
|
617
|
+
class="st-forceGraph__resetBtn"
|
|
618
|
+
type="button"
|
|
619
|
+
aria-label="Reset view"
|
|
620
|
+
onclick={resetView}
|
|
621
|
+
>
|
|
622
|
+
↺
|
|
623
|
+
</button>
|
|
624
|
+
{/if}
|
|
625
|
+
|
|
626
|
+
<!-- Legend overlay -->
|
|
627
|
+
{#if legend && legend.length > 0}
|
|
628
|
+
<div class="st-forceGraph__legend" aria-label="Graph legend">
|
|
629
|
+
{#each legend as entry}
|
|
630
|
+
{@const swatchPath = entry.shape !== undefined ? nodeShapePath(entry.shape, 7) : null}
|
|
631
|
+
{@const swatchTone = entry.tone ?? "category1"}
|
|
632
|
+
<div class="st-forceGraph__legendEntry">
|
|
633
|
+
{#if entry.shape !== undefined}
|
|
634
|
+
<!-- Node shape legend entry -->
|
|
635
|
+
<svg
|
|
636
|
+
class="st-forceGraph__legendSwatch"
|
|
637
|
+
viewBox="-8 -8 16 16"
|
|
638
|
+
width="16"
|
|
639
|
+
height="16"
|
|
640
|
+
aria-hidden="true"
|
|
641
|
+
>
|
|
642
|
+
{#if swatchPath}
|
|
643
|
+
<path
|
|
644
|
+
d={swatchPath}
|
|
645
|
+
class="st-forceGraph__legendShape st-forceGraph__legendShape--{swatchTone}"
|
|
646
|
+
/>
|
|
647
|
+
{:else}
|
|
648
|
+
<circle
|
|
649
|
+
r="7"
|
|
650
|
+
class="st-forceGraph__legendShape st-forceGraph__legendShape--{swatchTone}"
|
|
651
|
+
/>
|
|
652
|
+
{/if}
|
|
653
|
+
</svg>
|
|
654
|
+
{:else}
|
|
655
|
+
<!-- Edge style legend entry -->
|
|
656
|
+
<svg
|
|
657
|
+
class="st-forceGraph__legendSwatch"
|
|
658
|
+
viewBox="0 0 16 8"
|
|
659
|
+
width="16"
|
|
660
|
+
height="8"
|
|
661
|
+
aria-hidden="true"
|
|
662
|
+
>
|
|
663
|
+
<line
|
|
664
|
+
x1="0"
|
|
665
|
+
y1="4"
|
|
666
|
+
x2="16"
|
|
667
|
+
y2="4"
|
|
668
|
+
class="st-forceGraph__legendEdge"
|
|
669
|
+
class:st-forceGraph__legendEdge--weak={entry.weak}
|
|
670
|
+
/>
|
|
671
|
+
</svg>
|
|
672
|
+
{/if}
|
|
673
|
+
<span class="st-forceGraph__legendLabel">{entry.label}</span>
|
|
674
|
+
</div>
|
|
675
|
+
{/each}
|
|
676
|
+
</div>
|
|
677
|
+
{/if}
|
|
339
678
|
</div>
|
|
340
679
|
|
|
341
680
|
<style>
|
|
@@ -349,10 +688,13 @@
|
|
|
349
688
|
|
|
350
689
|
.st-forceGraph svg { display: block; overflow: visible; }
|
|
351
690
|
|
|
691
|
+
.st-forceGraph__svg--panning { cursor: grabbing; }
|
|
692
|
+
|
|
352
693
|
.st-forceGraph__edge {
|
|
353
694
|
stroke: var(--st-semantic-border-strong);
|
|
354
695
|
stroke-width: 1;
|
|
355
696
|
opacity: 0.55;
|
|
697
|
+
transition: opacity 120ms ease, stroke-width 120ms ease;
|
|
356
698
|
}
|
|
357
699
|
|
|
358
700
|
.st-forceGraph__edge--weak {
|
|
@@ -361,6 +703,19 @@
|
|
|
361
703
|
opacity: 0.5;
|
|
362
704
|
}
|
|
363
705
|
|
|
706
|
+
.st-forceGraph__edge--hovered {
|
|
707
|
+
opacity: 0.9;
|
|
708
|
+
stroke-width: 2;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/* Invisible wide hit target for edge hover */
|
|
712
|
+
.st-forceGraph__edgeHit {
|
|
713
|
+
stroke: transparent;
|
|
714
|
+
stroke-width: 10;
|
|
715
|
+
fill: none;
|
|
716
|
+
cursor: crosshair;
|
|
717
|
+
}
|
|
718
|
+
|
|
364
719
|
.st-forceGraph__node { transition: opacity 120ms ease; }
|
|
365
720
|
.st-forceGraph__node--dim { opacity: 0.3; }
|
|
366
721
|
|
|
@@ -380,6 +735,21 @@
|
|
|
380
735
|
outline-offset: 1px;
|
|
381
736
|
}
|
|
382
737
|
|
|
738
|
+
/* Selection highlight: slightly thicker stroke ring, full opacity. */
|
|
739
|
+
.st-forceGraph__node--selected .st-forceGraph__dot {
|
|
740
|
+
fill-opacity: 1;
|
|
741
|
+
stroke: var(--st-semantic-border-interactive, #0f62fe);
|
|
742
|
+
stroke-width: 2.5;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/* Focus (keyboard/programmatic focus): stronger ring + slight scale. */
|
|
746
|
+
.st-forceGraph__node--focus .st-forceGraph__dot {
|
|
747
|
+
fill-opacity: 1;
|
|
748
|
+
stroke: var(--st-semantic-border-interactive, #0f62fe);
|
|
749
|
+
stroke-width: 3.5;
|
|
750
|
+
filter: drop-shadow(0 0 4px var(--st-semantic-border-interactive, #0f62fe));
|
|
751
|
+
}
|
|
752
|
+
|
|
383
753
|
.st-forceGraph__label {
|
|
384
754
|
fill: var(--st-semantic-text-secondary);
|
|
385
755
|
font-size: 0.6875rem;
|
|
@@ -414,9 +784,102 @@
|
|
|
414
784
|
|
|
415
785
|
.st-forceGraph__tooltipLabel { font-weight: 600; }
|
|
416
786
|
.st-forceGraph__tooltipMeta { opacity: 0.85; }
|
|
787
|
+
.st-forceGraph__tooltipRelation {
|
|
788
|
+
opacity: 0.75;
|
|
789
|
+
font-style: italic;
|
|
790
|
+
font-size: 0.6875rem;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/* Reset view button */
|
|
794
|
+
.st-forceGraph__resetBtn {
|
|
795
|
+
background: var(--st-semantic-surface-overlay, rgba(0,0,0,0.55));
|
|
796
|
+
border: none;
|
|
797
|
+
border-radius: var(--st-radius-sm, 0.25rem);
|
|
798
|
+
color: var(--st-semantic-text-inverse, #fff);
|
|
799
|
+
cursor: pointer;
|
|
800
|
+
font-size: 1rem;
|
|
801
|
+
line-height: 1;
|
|
802
|
+
padding: 0.25rem 0.5rem;
|
|
803
|
+
position: absolute;
|
|
804
|
+
bottom: 0.5rem;
|
|
805
|
+
right: 0.5rem;
|
|
806
|
+
opacity: 0.8;
|
|
807
|
+
transition: opacity 120ms ease;
|
|
808
|
+
z-index: 2;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.st-forceGraph__resetBtn:hover,
|
|
812
|
+
.st-forceGraph__resetBtn:focus-visible {
|
|
813
|
+
opacity: 1;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
.st-forceGraph__resetBtn:focus-visible {
|
|
817
|
+
outline: 2px solid var(--st-semantic-border-interactive);
|
|
818
|
+
outline-offset: 2px;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/* Legend overlay */
|
|
822
|
+
.st-forceGraph__legend {
|
|
823
|
+
background: var(--st-semantic-surface-overlay, rgba(0,0,0,0.45));
|
|
824
|
+
border-radius: var(--st-radius-sm, 0.25rem);
|
|
825
|
+
color: var(--st-semantic-text-inverse, #fff);
|
|
826
|
+
display: flex;
|
|
827
|
+
flex-direction: column;
|
|
828
|
+
font-size: 0.6875rem;
|
|
829
|
+
gap: 0.25rem;
|
|
830
|
+
padding: 0.375rem 0.5rem;
|
|
831
|
+
pointer-events: none;
|
|
832
|
+
position: absolute;
|
|
833
|
+
bottom: 0.5rem;
|
|
834
|
+
left: 0.5rem;
|
|
835
|
+
z-index: 2;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.st-forceGraph__legendEntry {
|
|
839
|
+
align-items: center;
|
|
840
|
+
display: flex;
|
|
841
|
+
gap: 0.375rem;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
.st-forceGraph__legendSwatch {
|
|
845
|
+
flex-shrink: 0;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
.st-forceGraph__legendLabel {
|
|
849
|
+
white-space: nowrap;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
.st-forceGraph__legendShape {
|
|
853
|
+
fill-opacity: 0.9;
|
|
854
|
+
stroke: var(--st-semantic-surface-default, #fff);
|
|
855
|
+
stroke-width: 1;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.st-forceGraph__legendShape--category1 { fill: var(--st-semantic-data-category1); }
|
|
859
|
+
.st-forceGraph__legendShape--category2 { fill: var(--st-semantic-data-category2); }
|
|
860
|
+
.st-forceGraph__legendShape--category3 { fill: var(--st-semantic-data-category3); }
|
|
861
|
+
.st-forceGraph__legendShape--category4 { fill: var(--st-semantic-data-category4); }
|
|
862
|
+
.st-forceGraph__legendShape--category5 { fill: var(--st-semantic-data-category5); }
|
|
863
|
+
.st-forceGraph__legendShape--category6 { fill: var(--st-semantic-data-category6); }
|
|
864
|
+
.st-forceGraph__legendShape--category7 { fill: var(--st-semantic-data-category7); }
|
|
865
|
+
.st-forceGraph__legendShape--category8 { fill: var(--st-semantic-data-category8); }
|
|
866
|
+
|
|
867
|
+
.st-forceGraph__legendEdge {
|
|
868
|
+
stroke: var(--st-semantic-border-strong, #888);
|
|
869
|
+
stroke-width: 1.5;
|
|
870
|
+
opacity: 0.8;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.st-forceGraph__legendEdge--weak {
|
|
874
|
+
stroke: var(--st-semantic-border-subtle, #aaa);
|
|
875
|
+
stroke-dasharray: 3 3;
|
|
876
|
+
opacity: 0.65;
|
|
877
|
+
}
|
|
417
878
|
|
|
418
879
|
@media (prefers-reduced-motion: reduce) {
|
|
419
880
|
.st-forceGraph__node,
|
|
420
|
-
.st-forceGraph__dot
|
|
881
|
+
.st-forceGraph__dot,
|
|
882
|
+
.st-forceGraph__edge,
|
|
883
|
+
.st-forceGraph__resetBtn { transition: none; }
|
|
421
884
|
}
|
|
422
885
|
</style>
|