@sentropic/design-system-svelte 0.10.3 → 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.
Files changed (87) hide show
  1. package/dist/AreaChart.svelte +38 -26
  2. package/dist/AreaChart.svelte.d.ts.map +1 -1
  3. package/dist/BackToTop.svelte +157 -0
  4. package/dist/BackToTop.svelte.d.ts +14 -0
  5. package/dist/BackToTop.svelte.d.ts.map +1 -0
  6. package/dist/BarChart.svelte +38 -39
  7. package/dist/BarChart.svelte.d.ts.map +1 -1
  8. package/dist/Card.svelte +2 -0
  9. package/dist/ChartDataList.svelte +33 -0
  10. package/dist/ChartDataList.svelte.d.ts +9 -0
  11. package/dist/ChartDataList.svelte.d.ts.map +1 -0
  12. package/dist/ChatComposer.svelte +20 -3
  13. package/dist/ChatComposer.svelte.d.ts +6 -30
  14. package/dist/ChatComposer.svelte.d.ts.map +1 -1
  15. package/dist/Checkbox.svelte +4 -0
  16. package/dist/Combobox.svelte +1 -1
  17. package/dist/ContentSwitcher.svelte +1 -0
  18. package/dist/DataTable.svelte +4 -1
  19. package/dist/DatePicker.svelte +1 -1
  20. package/dist/DisplaySettings.svelte +210 -0
  21. package/dist/DisplaySettings.svelte.d.ts +24 -0
  22. package/dist/DisplaySettings.svelte.d.ts.map +1 -0
  23. package/dist/DonutChart.svelte +44 -29
  24. package/dist/DonutChart.svelte.d.ts.map +1 -1
  25. package/dist/Dropdown.svelte +1 -1
  26. package/dist/FileUploader.svelte +2 -2
  27. package/dist/ForceGraph.svelte +428 -26
  28. package/dist/ForceGraph.svelte.d.ts +27 -0
  29. package/dist/ForceGraph.svelte.d.ts.map +1 -1
  30. package/dist/GraphLegend.svelte +142 -0
  31. package/dist/GraphLegend.svelte.d.ts +12 -0
  32. package/dist/GraphLegend.svelte.d.ts.map +1 -0
  33. package/dist/Header.svelte +2 -1
  34. package/dist/IconButton.svelte +1 -1
  35. package/dist/InlineLoading.svelte +10 -1
  36. package/dist/InlineLoading.svelte.d.ts.map +1 -1
  37. package/dist/Input.svelte +3 -2
  38. package/dist/LanguageSelector.svelte +2 -1
  39. package/dist/LineChart.svelte +38 -26
  40. package/dist/LineChart.svelte.d.ts.map +1 -1
  41. package/dist/Link.svelte +7 -1
  42. package/dist/MediaContent.svelte +124 -0
  43. package/dist/MediaContent.svelte.d.ts +22 -0
  44. package/dist/MediaContent.svelte.d.ts.map +1 -0
  45. package/dist/Menu.svelte +56 -3
  46. package/dist/Menu.svelte.d.ts.map +1 -1
  47. package/dist/MessageStatusBadge.svelte +1 -1
  48. package/dist/MultiSelect.svelte +2 -2
  49. package/dist/Notification.svelte +150 -0
  50. package/dist/Notification.svelte.d.ts +17 -0
  51. package/dist/Notification.svelte.d.ts.map +1 -0
  52. package/dist/NumberInput.svelte +1 -0
  53. package/dist/OverflowMenu.svelte +84 -13
  54. package/dist/OverflowMenu.svelte.d.ts.map +1 -1
  55. package/dist/Pagination.svelte +7 -0
  56. package/dist/PaginationNav.svelte +2 -2
  57. package/dist/ProgressIndicator.svelte +13 -1
  58. package/dist/ProgressIndicator.svelte.d.ts +1 -0
  59. package/dist/ProgressIndicator.svelte.d.ts.map +1 -1
  60. package/dist/Radio.svelte +7 -3
  61. package/dist/ScatterPlot.svelte +64 -45
  62. package/dist/ScatterPlot.svelte.d.ts.map +1 -1
  63. package/dist/Search.svelte +6 -3
  64. package/dist/Select.svelte +8 -2
  65. package/dist/SideNav.svelte +6 -0
  66. package/dist/StackedBarChart.svelte +51 -30
  67. package/dist/StackedBarChart.svelte.d.ts.map +1 -1
  68. package/dist/StreamingMessage.svelte +2 -2
  69. package/dist/Switch.svelte +4 -0
  70. package/dist/Table.svelte +4 -1
  71. package/dist/TableOfContents.svelte +109 -0
  72. package/dist/TableOfContents.svelte.d.ts +16 -0
  73. package/dist/TableOfContents.svelte.d.ts.map +1 -0
  74. package/dist/Tag.svelte +1 -1
  75. package/dist/Textarea.svelte +3 -2
  76. package/dist/Tile.svelte +4 -0
  77. package/dist/TileGroup.svelte +4 -0
  78. package/dist/Toggle.svelte +4 -0
  79. package/dist/Toggletip.svelte +1 -1
  80. package/dist/Transcription.svelte +135 -0
  81. package/dist/Transcription.svelte.d.ts +19 -0
  82. package/dist/Transcription.svelte.d.ts.map +1 -0
  83. package/dist/TreeView.svelte +2 -2
  84. package/dist/index.d.ts +12 -1
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +8 -0
  87. package/package.json +1 -1
@@ -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">
@@ -75,6 +139,16 @@
75
139
  * Fires with the node's stable id.
76
140
  */
77
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[];
78
152
  class?: string;
79
153
  };
80
154
 
@@ -91,6 +165,8 @@
91
165
  focusId = null,
92
166
  onSelect,
93
167
  onOpenEntity,
168
+ onEdgeHover,
169
+ legend,
94
170
  class: className
95
171
  }: ForceGraphProps = $props();
96
172
 
@@ -262,30 +338,46 @@
262
338
  const positionedNodes = $derived.by(() =>
263
339
  nodes.map((n, i) => {
264
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);
265
343
  return {
266
344
  node: n,
267
345
  i,
268
346
  x: p.x,
269
347
  y: p.y,
270
- r: nodeRadius * Math.sqrt(Math.max(n.weight ?? 1, 0.25)),
348
+ r,
271
349
  tone: toneMap.get(n.id) ?? "category1",
272
- title: n.label ?? n.id
350
+ title: n.label ?? n.id,
351
+ shapePath
273
352
  };
274
353
  })
275
354
  );
276
355
 
277
356
  const positionedEdges = $derived.by(() => {
357
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
278
358
  return edges
279
359
  .map((e, i) => {
280
360
  const a = layout.get(e.source);
281
361
  const b = layout.get(e.target);
282
362
  if (!a || !b) return null;
283
- return { edge: e, i, x1: a.x, y1: a.y, x2: b.x, y2: b.y };
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
+ };
284
375
  })
285
376
  .filter((e): e is NonNullable<typeof e> => e !== null);
286
377
  });
287
378
 
288
- let hoveredIndex: number | null = $state(null);
379
+ let hoveredNodeIndex: number | null = $state(null);
380
+ let hoveredEdgeIndex: number | null = $state(null);
289
381
 
290
382
  // Fast lookup sets — recomputed only when selectedIds/focusId props change,
291
383
  // never when nodes/edges change.
@@ -302,31 +394,128 @@
302
394
  }
303
395
  }
304
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);
469
+
305
470
  const classes = () =>
306
471
  ["st-forceGraph", prefersReducedMotion ? "st-forceGraph--static" : null, className]
307
472
  .filter(Boolean)
308
473
  .join(" ");
309
474
  </script>
310
475
 
311
- <div class={classes()} role="img" aria-label={label}>
476
+ <div
477
+ class={classes()}
478
+ role="img"
479
+ aria-label={label}
480
+ >
312
481
  <svg
313
- viewBox="0 0 {width} {height}"
482
+ bind:this={svgEl}
483
+ viewBox={viewBox}
314
484
  preserveAspectRatio="xMidYMid meet"
315
485
  width="100%"
316
486
  height="100%"
317
487
  focusable="false"
318
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}
319
495
  >
320
496
  <!-- edges first so nodes paint on top -->
321
497
  <g class="st-forceGraph__edges">
322
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
+ />
323
510
  <line
324
511
  class="st-forceGraph__edge"
325
512
  class:st-forceGraph__edge--weak={e.edge.weak}
513
+ class:st-forceGraph__edge--hovered={hoveredEdgeIndex === e.i}
326
514
  x1={e.x1}
327
515
  y1={e.y1}
328
516
  x2={e.x2}
329
517
  y2={e.y2}
518
+ pointer-events="none"
330
519
  />
331
520
  {/each}
332
521
  </g>
@@ -335,26 +524,44 @@
335
524
  {#each positionedNodes as p (p.node.id)}
336
525
  <g
337
526
  class="st-forceGraph__node st-forceGraph__node--{p.tone}"
338
- class:st-forceGraph__node--dim={hoveredIndex !== null && hoveredIndex !== p.i}
527
+ class:st-forceGraph__node--dim={hoveredNodeIndex !== null && hoveredNodeIndex !== p.i}
339
528
  class:st-forceGraph__node--selected={selectedSet.has(p.node.id)}
340
529
  class:st-forceGraph__node--focus={focusId === p.node.id}
341
530
  transform="translate({p.x} {p.y})"
342
531
  >
343
- <circle
344
- class="st-forceGraph__dot"
345
- r={p.r}
346
- tabindex="0"
347
- role="button"
348
- aria-label="{p.title}{p.node.group !== undefined ? ` — ${p.node.group}` : ''}"
349
- aria-pressed={selectedSet.has(p.node.id)}
350
- onmouseenter={() => (hoveredIndex = p.i)}
351
- onmouseleave={() => (hoveredIndex = null)}
352
- onfocus={() => (hoveredIndex = p.i)}
353
- onblur={() => (hoveredIndex = null)}
354
- onclick={() => onSelect?.(p.node.id)}
355
- ondblclick={() => onOpenEntity?.(p.node.id)}
356
- onkeydown={(e) => handleNodeKeydown(p.node.id, e)}
357
- />
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}
358
565
  {#if showLabels}
359
566
  <text class="st-forceGraph__label" x={p.r + 3} y="0" dominant-baseline="middle">{p.title}</text>
360
567
  {/if}
@@ -363,15 +570,16 @@
363
570
  </g>
364
571
  </svg>
365
572
 
366
- {#if hoveredIndex !== null && positionedNodes[hoveredIndex]}
367
- {@const p = positionedNodes[hoveredIndex]}
573
+ <!-- Node tooltip -->
574
+ {#if hoveredNodeIndex !== null && positionedNodes[hoveredNodeIndex]}
575
+ {@const p = positionedNodes[hoveredNodeIndex]}
368
576
  {@const relCount = positionedEdges.filter(
369
577
  (e) => e.edge.source === p.node.id || e.edge.target === p.node.id
370
578
  ).length}
371
579
  <div
372
580
  class="st-forceGraph__tooltip"
373
581
  role="presentation"
374
- style="left: {(p.x / width) * 100}%; top: {(p.y / height) * 100}%"
582
+ style="left: {((p.x - vbX) / vbW) * 100}%; top: {((p.y - vbY) / vbH) * 100}%"
375
583
  >
376
584
  <span class="st-forceGraph__tooltipLabel">{p.title}</span>
377
585
  {#if p.node.group !== undefined}
@@ -382,6 +590,91 @@
382
590
  {/if}
383
591
  </div>
384
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}
385
678
  </div>
386
679
 
387
680
  <style>
@@ -395,10 +688,13 @@
395
688
 
396
689
  .st-forceGraph svg { display: block; overflow: visible; }
397
690
 
691
+ .st-forceGraph__svg--panning { cursor: grabbing; }
692
+
398
693
  .st-forceGraph__edge {
399
694
  stroke: var(--st-semantic-border-strong);
400
695
  stroke-width: 1;
401
696
  opacity: 0.55;
697
+ transition: opacity 120ms ease, stroke-width 120ms ease;
402
698
  }
403
699
 
404
700
  .st-forceGraph__edge--weak {
@@ -407,6 +703,19 @@
407
703
  opacity: 0.5;
408
704
  }
409
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
+
410
719
  .st-forceGraph__node { transition: opacity 120ms ease; }
411
720
  .st-forceGraph__node--dim { opacity: 0.3; }
412
721
 
@@ -475,9 +784,102 @@
475
784
 
476
785
  .st-forceGraph__tooltipLabel { font-weight: 600; }
477
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
+ }
478
878
 
479
879
  @media (prefers-reduced-motion: reduce) {
480
880
  .st-forceGraph__node,
481
- .st-forceGraph__dot { transition: none; }
881
+ .st-forceGraph__dot,
882
+ .st-forceGraph__edge,
883
+ .st-forceGraph__resetBtn { transition: none; }
482
884
  }
483
885
  </style>
@@ -1,4 +1,5 @@
1
1
  export type ForceGraphTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
2
+ export type ForceGraphNodeShape = "dot" | "circle" | "diamond" | "star" | "hexagon" | "box" | "square" | "triangle";
2
3
  export type ForceGraphNode = {
3
4
  /** Stable identifier; referenced by edges. */
4
5
  id: string;
@@ -16,6 +17,11 @@ export type ForceGraphNode = {
16
17
  /** Pin the node to a fixed position (ignored by the simulation). */
17
18
  fx?: number;
18
19
  fy?: number;
20
+ /**
21
+ * Visual shape for the node. Defaults to 'dot' (circle).
22
+ * Supported: 'dot'|'circle', 'diamond', 'star', 'hexagon', 'box'|'square', 'triangle'.
23
+ */
24
+ shape?: ForceGraphNodeShape;
19
25
  };
20
26
  export type ForceGraphEdge = {
21
27
  /** Source node id. */
@@ -30,6 +36,17 @@ export type ForceGraphEdge = {
30
36
  */
31
37
  weak?: boolean;
32
38
  };
39
+ export type ForceGraphLegendEntry = {
40
+ /** Label shown in the legend. */
41
+ label: string;
42
+ /** Shape for this entry (node legend). Absent = line-style legend entry. */
43
+ shape?: ForceGraphNodeShape;
44
+ /** Tone for this entry. Defaults to category1. */
45
+ tone?: ForceGraphTone;
46
+ /** When true, renders as a dashed line (edge legend). */
47
+ weak?: boolean;
48
+ };
49
+ export declare function nodeShapePath(shape: ForceGraphNodeShape | undefined, r: number): string | null;
33
50
  type ForceGraphProps = {
34
51
  nodes: ForceGraphNode[];
35
52
  edges: ForceGraphEdge[];
@@ -67,6 +84,16 @@ type ForceGraphProps = {
67
84
  * Fires with the node's stable id.
68
85
  */
69
86
  onOpenEntity?: (id: string) => void;
87
+ /**
88
+ * Called when the user hovers an edge.
89
+ * Fires with the edge object (source/target/relation/weak).
90
+ */
91
+ onEdgeHover?: (edge: ForceGraphEdge) => void;
92
+ /**
93
+ * Legend entries rendered as a corner overlay.
94
+ * Each entry has a label + optional shape (node) or weak (edge).
95
+ */
96
+ legend?: ForceGraphLegendEntry[];
70
97
  class?: string;
71
98
  };
72
99
  declare const ForceGraph: import("svelte").Component<ForceGraphProps, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"ForceGraph.svelte.d.ts","sourceRoot":"","sources":["../src/lib/ForceGraph.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,cAAc,GACtB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,cAAc,GAAG;IAC3B,8CAA8C;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,gEAAgE;IAChE,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAuRJ,QAAA,MAAM,UAAU,qDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"ForceGraph.svelte.d.ts","sourceRoot":"","sources":["../src/lib/ForceGraph.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,cAAc,GACtB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,mBAAmB,GAC3B,KAAK,GAAG,QAAQ,GAChB,SAAS,GACT,MAAM,GACN,SAAS,GACT,KAAK,GAAG,QAAQ,GAChB,UAAU,CAAC;AAEf,MAAM,MAAM,cAAc,GAAG;IAC3B,8CAA8C;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,gEAAgE;IAChE,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,kDAAkD;IAClD,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,yDAAyD;IACzD,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAMF,wBAAgB,aAAa,CAAC,KAAK,EAAE,mBAAmB,GAAG,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkC9F;AAED,KAAK,eAAe,GAAG;IACrB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IAC7C;;;OAGG;IACH,MAAM,CAAC,EAAE,qBAAqB,EAAE,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA4aJ,QAAA,MAAM,UAAU,qDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}