@sentropic/design-system-react 0.1.0 → 0.2.0
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/ForceGraph.d.ts +2 -2
- package/dist/ForceGraph.d.ts.map +1 -1
- package/dist/ForceGraph.js +1 -1
- package/dist/ForceGraph.js.map +1 -1
- package/dist/catalog.d.ts +82 -4
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +367 -6
- package/dist/catalog.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/styles.css +178 -3
- package/package.json +1 -1
package/dist/catalog.js
CHANGED
|
@@ -399,12 +399,373 @@ export function Footer({ brand, columns, links, copyright, className, ...rest })
|
|
|
399
399
|
const groups = columns ?? (links ? [{ links }] : []);
|
|
400
400
|
return (_jsxs("footer", { ...rest, className: classNames("st-footer", className), children: [_jsxs("div", { className: "st-footer__top", children: [brand ? _jsx("div", { className: "st-footer__brand", children: brand }) : null, _jsx("div", { className: "st-footer__columns", children: groups.map((group, index) => (_jsxs("nav", { children: [group.title ? _jsx("h2", { children: group.title }) : null, group.links.map((link) => (_jsx("a", { href: link.href, children: link.label }, link.href)))] }, index))) })] }), copyright ? _jsx("div", { className: "st-footer__copyright", children: copyright }) : null] }));
|
|
401
401
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
// SVG path helpers for the various node shapes.
|
|
404
|
+
// All shapes are centered at (0,0) and sized to inscribe within radius r.
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
export function nodeShapePath(shape, r) {
|
|
407
|
+
const s = shape ?? "dot";
|
|
408
|
+
if (s === "dot" || s === "circle")
|
|
409
|
+
return null; // use <circle>
|
|
410
|
+
if (s === "diamond") {
|
|
411
|
+
return `M 0 ${-r} L ${r} 0 L 0 ${r} L ${-r} 0 Z`;
|
|
412
|
+
}
|
|
413
|
+
if (s === "star") {
|
|
414
|
+
const outer = r;
|
|
415
|
+
const inner = r * 0.42;
|
|
416
|
+
const pts = [];
|
|
417
|
+
for (let i = 0; i < 10; i++) {
|
|
418
|
+
const angle = (i * Math.PI) / 5 - Math.PI / 2;
|
|
419
|
+
const rad = i % 2 === 0 ? outer : inner;
|
|
420
|
+
pts.push(`${rad * Math.cos(angle)},${rad * Math.sin(angle)}`);
|
|
421
|
+
}
|
|
422
|
+
return `M ${pts.join(" L ")} Z`;
|
|
423
|
+
}
|
|
424
|
+
if (s === "hexagon") {
|
|
425
|
+
const pts = [];
|
|
426
|
+
for (let i = 0; i < 6; i++) {
|
|
427
|
+
const angle = (i * Math.PI) / 3 - Math.PI / 6;
|
|
428
|
+
pts.push(`${r * Math.cos(angle)},${r * Math.sin(angle)}`);
|
|
429
|
+
}
|
|
430
|
+
return `M ${pts.join(" L ")} Z`;
|
|
431
|
+
}
|
|
432
|
+
if (s === "box" || s === "square") {
|
|
433
|
+
const h = r * 0.85;
|
|
434
|
+
return `M ${-h} ${-h} L ${h} ${-h} L ${h} ${h} L ${-h} ${h} Z`;
|
|
435
|
+
}
|
|
436
|
+
if (s === "triangle") {
|
|
437
|
+
const h = r * 1.1;
|
|
438
|
+
return `M 0 ${-h} L ${h * 0.9} ${h * 0.6} L ${-h * 0.9} ${h * 0.6} Z`;
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
const FORCE_GRAPH_TONES = [
|
|
443
|
+
"category1",
|
|
444
|
+
"category2",
|
|
445
|
+
"category3",
|
|
446
|
+
"category4",
|
|
447
|
+
"category5",
|
|
448
|
+
"category6",
|
|
449
|
+
"category7",
|
|
450
|
+
"category8",
|
|
451
|
+
];
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
// Tone assignment: explicit tone wins, else stable per-group, else per-index.
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
function buildForceGraphToneMap(ns) {
|
|
456
|
+
const groups = [];
|
|
457
|
+
const seen = new Set();
|
|
458
|
+
for (const n of ns) {
|
|
459
|
+
if (n.group === undefined)
|
|
460
|
+
continue;
|
|
461
|
+
if (seen.has(n.group))
|
|
462
|
+
continue;
|
|
463
|
+
seen.add(n.group);
|
|
464
|
+
groups.push(n.group);
|
|
465
|
+
}
|
|
466
|
+
const groupTone = new Map();
|
|
467
|
+
groups.forEach((g, i) => groupTone.set(g, FORCE_GRAPH_TONES[i % FORCE_GRAPH_TONES.length]));
|
|
468
|
+
const map = new Map();
|
|
469
|
+
ns.forEach((n, i) => {
|
|
470
|
+
if (n.tone)
|
|
471
|
+
map.set(n.id, n.tone);
|
|
472
|
+
else if (n.group !== undefined && groupTone.has(n.group))
|
|
473
|
+
map.set(n.id, groupTone.get(n.group));
|
|
474
|
+
else
|
|
475
|
+
map.set(n.id, FORCE_GRAPH_TONES[i % FORCE_GRAPH_TONES.length]);
|
|
476
|
+
});
|
|
477
|
+
return map;
|
|
478
|
+
}
|
|
479
|
+
function forceGraphMulberry32(seed) {
|
|
480
|
+
let a = seed >>> 0;
|
|
481
|
+
return () => {
|
|
482
|
+
a |= 0;
|
|
483
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
484
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
485
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
486
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function runForceGraphSimulation(ns, es, w, h, ticks, nodeRadius) {
|
|
490
|
+
const cx = w / 2;
|
|
491
|
+
const cy = h / 2;
|
|
492
|
+
const rand = forceGraphMulberry32(ns.length * 2654435761 + es.length);
|
|
493
|
+
const idIndex = new Map();
|
|
494
|
+
const sim = ns.map((n, i) => {
|
|
495
|
+
idIndex.set(n.id, i);
|
|
496
|
+
const fixed = typeof n.fx === "number" && typeof n.fy === "number";
|
|
497
|
+
// Seed on a loose ring so the first ticks fan the graph out predictably.
|
|
498
|
+
const angle = (i / Math.max(ns.length, 1)) * Math.PI * 2;
|
|
499
|
+
const r = Math.min(w, h) * 0.3 * (0.5 + rand() * 0.5);
|
|
500
|
+
return {
|
|
501
|
+
id: n.id,
|
|
502
|
+
x: fixed ? n.fx : cx + Math.cos(angle) * r,
|
|
503
|
+
y: fixed ? n.fy : cy + Math.sin(angle) * r,
|
|
504
|
+
vx: 0,
|
|
505
|
+
vy: 0,
|
|
506
|
+
fixed,
|
|
507
|
+
};
|
|
508
|
+
});
|
|
509
|
+
const links = es
|
|
510
|
+
.map((e) => ({ s: idIndex.get(e.source), t: idIndex.get(e.target) }))
|
|
511
|
+
.filter((l) => l.s !== undefined && l.t !== undefined);
|
|
512
|
+
const area = w * h;
|
|
513
|
+
const k = Math.sqrt(area / Math.max(ns.length, 1)); // ideal node distance
|
|
514
|
+
const repulsion = k * k * 0.9;
|
|
515
|
+
const restLength = k * 0.8;
|
|
516
|
+
const springK = 0.04;
|
|
517
|
+
const gravity = 0.012;
|
|
518
|
+
const damping = 0.85;
|
|
519
|
+
let temperature = Math.min(w, h) * 0.08;
|
|
520
|
+
const cooling = ticks > 0 ? Math.pow(0.02, 1 / ticks) : 0.95;
|
|
521
|
+
for (let step = 0; step < ticks; step++) {
|
|
522
|
+
// Repulsion between all node pairs.
|
|
523
|
+
for (let i = 0; i < sim.length; i++) {
|
|
524
|
+
for (let j = i + 1; j < sim.length; j++) {
|
|
525
|
+
let dx = sim[i].x - sim[j].x;
|
|
526
|
+
let dy = sim[i].y - sim[j].y;
|
|
527
|
+
let dist2 = dx * dx + dy * dy;
|
|
528
|
+
if (dist2 < 0.01) {
|
|
529
|
+
dx = (rand() - 0.5) * 0.1;
|
|
530
|
+
dy = (rand() - 0.5) * 0.1;
|
|
531
|
+
dist2 = dx * dx + dy * dy + 0.01;
|
|
532
|
+
}
|
|
533
|
+
const dist = Math.sqrt(dist2);
|
|
534
|
+
const force = repulsion / dist2;
|
|
535
|
+
const fx = (dx / dist) * force;
|
|
536
|
+
const fy = (dy / dist) * force;
|
|
537
|
+
sim[i].vx += fx;
|
|
538
|
+
sim[i].vy += fy;
|
|
539
|
+
sim[j].vx -= fx;
|
|
540
|
+
sim[j].vy -= fy;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Spring attraction along links.
|
|
544
|
+
for (const l of links) {
|
|
545
|
+
const a = sim[l.s];
|
|
546
|
+
const b = sim[l.t];
|
|
547
|
+
const dx = b.x - a.x;
|
|
548
|
+
const dy = b.y - a.y;
|
|
549
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 0.01;
|
|
550
|
+
const force = (dist - restLength) * springK;
|
|
551
|
+
const fx = (dx / dist) * force;
|
|
552
|
+
const fy = (dy / dist) * force;
|
|
553
|
+
a.vx += fx;
|
|
554
|
+
a.vy += fy;
|
|
555
|
+
b.vx -= fx;
|
|
556
|
+
b.vy -= fy;
|
|
557
|
+
}
|
|
558
|
+
// Gravity toward centre + integrate with capped, cooling step.
|
|
559
|
+
for (const sn of sim) {
|
|
560
|
+
if (sn.fixed) {
|
|
561
|
+
sn.vx = 0;
|
|
562
|
+
sn.vy = 0;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
sn.vx += (cx - sn.x) * gravity;
|
|
566
|
+
sn.vy += (cy - sn.y) * gravity;
|
|
567
|
+
sn.vx *= damping;
|
|
568
|
+
sn.vy *= damping;
|
|
569
|
+
const speed = Math.sqrt(sn.vx * sn.vx + sn.vy * sn.vy);
|
|
570
|
+
if (speed > temperature) {
|
|
571
|
+
sn.vx = (sn.vx / speed) * temperature;
|
|
572
|
+
sn.vy = (sn.vy / speed) * temperature;
|
|
573
|
+
}
|
|
574
|
+
sn.x += sn.vx;
|
|
575
|
+
sn.y += sn.vy;
|
|
576
|
+
// Keep inside a padded viewport.
|
|
577
|
+
sn.x = Math.max(nodeRadius * 2, Math.min(w - nodeRadius * 2, sn.x));
|
|
578
|
+
sn.y = Math.max(nodeRadius * 2, Math.min(h - nodeRadius * 2, sn.y));
|
|
579
|
+
}
|
|
580
|
+
temperature *= cooling;
|
|
581
|
+
}
|
|
582
|
+
const out = new Map();
|
|
583
|
+
for (const sn of sim)
|
|
584
|
+
out.set(sn.id, { x: sn.x, y: sn.y });
|
|
585
|
+
return out;
|
|
586
|
+
}
|
|
587
|
+
export function ForceGraph({ nodes, edges, label, width = 480, height = 360, nodeRadius = 7, showLabels = true, iterations = 300, selectedIds = [], focusId = null, onSelect, onOpenEntity, onEdgeHover, legend, className, ...rest }) {
|
|
588
|
+
// SSR-safe reduced-motion check (window may be undefined during SSR/tests).
|
|
589
|
+
const prefersReducedMotion = typeof window !== "undefined" &&
|
|
590
|
+
typeof window.matchMedia === "function" &&
|
|
591
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
592
|
+
const toneMap = React.useMemo(() => buildForceGraphToneMap(nodes), [nodes]);
|
|
593
|
+
// The whole layout is recomputed when inputs change. The same settled
|
|
594
|
+
// layout is used as the rendered target — a static, deterministic frame —
|
|
595
|
+
// which keeps the component framework-light and test-friendly while still
|
|
596
|
+
// honouring the motion preference (no rAF loop, no jitter).
|
|
597
|
+
const layout = React.useMemo(() => {
|
|
598
|
+
const ticks = Math.max(1, Math.round(iterations));
|
|
599
|
+
return runForceGraphSimulation(nodes, edges, width, height, ticks, nodeRadius);
|
|
600
|
+
}, [nodes, edges, width, height, iterations, nodeRadius]);
|
|
601
|
+
const positionedNodes = React.useMemo(() => nodes.map((n, i) => {
|
|
602
|
+
const p = layout.get(n.id) ?? { x: width / 2, y: height / 2 };
|
|
603
|
+
const r = nodeRadius * Math.sqrt(Math.max(n.weight ?? 1, 0.25));
|
|
604
|
+
const shapePath = nodeShapePath(n.shape, r);
|
|
605
|
+
return {
|
|
606
|
+
node: n,
|
|
607
|
+
i,
|
|
608
|
+
x: p.x,
|
|
609
|
+
y: p.y,
|
|
610
|
+
r,
|
|
611
|
+
tone: toneMap.get(n.id) ?? "category1",
|
|
612
|
+
title: n.label ?? n.id,
|
|
613
|
+
shapePath,
|
|
614
|
+
};
|
|
615
|
+
}), [nodes, layout, width, height, nodeRadius, toneMap]);
|
|
616
|
+
const positionedEdges = React.useMemo(() => {
|
|
617
|
+
const nodeById = new Map(nodes.map((n) => [n.id, n]));
|
|
618
|
+
return edges
|
|
619
|
+
.map((e, i) => {
|
|
620
|
+
const a = layout.get(e.source);
|
|
621
|
+
const b = layout.get(e.target);
|
|
622
|
+
if (!a || !b)
|
|
623
|
+
return null;
|
|
624
|
+
const srcNode = nodeById.get(e.source);
|
|
625
|
+
const tgtNode = nodeById.get(e.target);
|
|
626
|
+
return {
|
|
627
|
+
edge: e,
|
|
628
|
+
i,
|
|
629
|
+
x1: a.x,
|
|
630
|
+
y1: a.y,
|
|
631
|
+
x2: b.x,
|
|
632
|
+
y2: b.y,
|
|
633
|
+
srcLabel: srcNode?.label ?? e.source,
|
|
634
|
+
tgtLabel: tgtNode?.label ?? e.target,
|
|
635
|
+
};
|
|
636
|
+
})
|
|
637
|
+
.filter((e) => e !== null);
|
|
638
|
+
}, [nodes, edges, layout]);
|
|
639
|
+
const [hoveredNodeIndex, setHoveredNodeIndex] = React.useState(null);
|
|
640
|
+
const [hoveredEdgeIndex, setHoveredEdgeIndex] = React.useState(null);
|
|
641
|
+
const selectedSet = React.useMemo(() => new Set(selectedIds), [selectedIds]);
|
|
642
|
+
// Keyboard handler for a node element: Space/Enter → onSelect, Enter → onOpenEntity.
|
|
643
|
+
function handleNodeKeydown(id, e) {
|
|
644
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
645
|
+
e.preventDefault();
|
|
646
|
+
onSelect?.(id);
|
|
647
|
+
}
|
|
648
|
+
if (e.key === "Enter") {
|
|
649
|
+
onOpenEntity?.(id);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// ---------------------------------------------------------------------------
|
|
653
|
+
// Zoom + pan state. Store zoom as a scale multiplier + pan offset so syncing
|
|
654
|
+
// with width/height props is trivial.
|
|
655
|
+
// vbW = width / zoomScale, vbH = height / zoomScale
|
|
656
|
+
// vbX / vbY = pan offset in SVG coordinate space
|
|
657
|
+
// ---------------------------------------------------------------------------
|
|
658
|
+
const [zoomScale, setZoomScale] = React.useState(1);
|
|
659
|
+
const [panX, setPanX] = React.useState(0);
|
|
660
|
+
const [panY, setPanY] = React.useState(0);
|
|
661
|
+
const isPanningRef = React.useRef(false);
|
|
662
|
+
const panStartRef = React.useRef({ x: 0, y: 0, panX: 0, panY: 0 });
|
|
663
|
+
const svgRef = React.useRef(null);
|
|
664
|
+
const [isPanning, setIsPanning] = React.useState(false);
|
|
665
|
+
const vbW = width / zoomScale;
|
|
666
|
+
const vbH = height / zoomScale;
|
|
667
|
+
const vbX = panX;
|
|
668
|
+
const vbY = panY;
|
|
669
|
+
function resetView() {
|
|
670
|
+
setZoomScale(1);
|
|
671
|
+
setPanX(0);
|
|
672
|
+
setPanY(0);
|
|
673
|
+
}
|
|
674
|
+
function handleWheel(ev) {
|
|
675
|
+
if (prefersReducedMotion)
|
|
676
|
+
return;
|
|
677
|
+
ev.preventDefault();
|
|
678
|
+
// Zoom factor: ~10% per step.
|
|
679
|
+
const factor = ev.deltaY > 0 ? 0.9 : 1.1;
|
|
680
|
+
// Clamp zoom: 0.2x – 8x.
|
|
681
|
+
const newScale = Math.min(Math.max(zoomScale * factor, 0.2), 8);
|
|
682
|
+
// Anchor zoom around the cursor position in SVG coords.
|
|
683
|
+
if (svgRef.current) {
|
|
684
|
+
const rect = svgRef.current.getBoundingClientRect();
|
|
685
|
+
const cursorSvgX = panX + ((ev.clientX - rect.left) / rect.width) * (width / zoomScale);
|
|
686
|
+
const cursorSvgY = panY + ((ev.clientY - rect.top) / rect.height) * (height / zoomScale);
|
|
687
|
+
const newVbW = width / newScale;
|
|
688
|
+
const newVbH = height / newScale;
|
|
689
|
+
const ratioX = (cursorSvgX - panX) / (width / zoomScale);
|
|
690
|
+
const ratioY = (cursorSvgY - panY) / (height / zoomScale);
|
|
691
|
+
setPanX(cursorSvgX - ratioX * newVbW);
|
|
692
|
+
setPanY(cursorSvgY - ratioY * newVbH);
|
|
693
|
+
}
|
|
694
|
+
setZoomScale(newScale);
|
|
695
|
+
}
|
|
696
|
+
function handleBgMouseDown(ev) {
|
|
697
|
+
// Only start pan when clicking the background (not a node/edge element).
|
|
698
|
+
if (ev.target.closest(".st-forceGraph__node"))
|
|
699
|
+
return;
|
|
700
|
+
if (prefersReducedMotion)
|
|
701
|
+
return;
|
|
702
|
+
isPanningRef.current = true;
|
|
703
|
+
setIsPanning(true);
|
|
704
|
+
panStartRef.current = { x: ev.clientX, y: ev.clientY, panX, panY };
|
|
705
|
+
}
|
|
706
|
+
function handleMouseMove(ev) {
|
|
707
|
+
if (!isPanningRef.current || !svgRef.current)
|
|
708
|
+
return;
|
|
709
|
+
const rect = svgRef.current.getBoundingClientRect();
|
|
710
|
+
const dx = ((ev.clientX - panStartRef.current.x) / rect.width) * vbW;
|
|
711
|
+
const dy = ((ev.clientY - panStartRef.current.y) / rect.height) * vbH;
|
|
712
|
+
setPanX(panStartRef.current.panX - dx);
|
|
713
|
+
setPanY(panStartRef.current.panY - dy);
|
|
714
|
+
}
|
|
715
|
+
function handleMouseUp() {
|
|
716
|
+
isPanningRef.current = false;
|
|
717
|
+
setIsPanning(false);
|
|
718
|
+
}
|
|
719
|
+
const viewBox = `${vbX} ${vbY} ${vbW} ${vbH}`;
|
|
720
|
+
const isZoomed = zoomScale !== 1 || panX !== 0 || panY !== 0;
|
|
721
|
+
const hoveredNode = hoveredNodeIndex !== null ? positionedNodes[hoveredNodeIndex] : null;
|
|
722
|
+
const hoveredEdge = hoveredEdgeIndex !== null ? positionedEdges.find((pe) => pe.i === hoveredEdgeIndex) : null;
|
|
723
|
+
const hoveredNodeRelCount = hoveredNode
|
|
724
|
+
? positionedEdges.filter((e) => e.edge.source === hoveredNode.node.id || e.edge.target === hoveredNode.node.id).length
|
|
725
|
+
: 0;
|
|
726
|
+
return (_jsxs("div", { ...rest, className: classNames("st-forceGraph", prefersReducedMotion && "st-forceGraph--static", className), role: "img", "aria-label": label, children: [_jsxs("svg", { ref: svgRef, viewBox: viewBox, preserveAspectRatio: "xMidYMid meet", width: "100%", height: "100%", focusable: "false", "aria-hidden": "true", className: classNames(isPanning && "st-forceGraph__svg--panning"), onWheel: handleWheel, onMouseDown: handleBgMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, children: [_jsx("g", { className: "st-forceGraph__edges", children: positionedEdges.map((e) => (_jsxs(React.Fragment, { children: [_jsx("line", { className: "st-forceGraph__edgeHit", role: "presentation", x1: e.x1, y1: e.y1, x2: e.x2, y2: e.y2, onMouseEnter: () => {
|
|
727
|
+
setHoveredEdgeIndex(e.i);
|
|
728
|
+
onEdgeHover?.(e.edge);
|
|
729
|
+
}, onMouseLeave: () => setHoveredEdgeIndex(null) }), _jsx("line", { className: classNames("st-forceGraph__edge", e.edge.weak && "st-forceGraph__edge--weak", hoveredEdgeIndex === e.i && "st-forceGraph__edge--hovered"), x1: e.x1, y1: e.y1, x2: e.x2, y2: e.y2, pointerEvents: "none" })] }, e.i))) }), _jsx("g", { className: "st-forceGraph__nodes", children: positionedNodes.map((p) => {
|
|
730
|
+
const ariaLabel = `${p.title}${p.node.group !== undefined ? `: ${p.node.group}` : ""}`;
|
|
731
|
+
const pressed = selectedSet.has(p.node.id);
|
|
732
|
+
const shapeProps = {
|
|
733
|
+
className: "st-forceGraph__shape st-forceGraph__dot",
|
|
734
|
+
tabIndex: 0,
|
|
735
|
+
role: "button",
|
|
736
|
+
"aria-label": ariaLabel,
|
|
737
|
+
"aria-pressed": pressed,
|
|
738
|
+
onMouseEnter: () => setHoveredNodeIndex(p.i),
|
|
739
|
+
onMouseLeave: () => setHoveredNodeIndex(null),
|
|
740
|
+
onFocus: () => setHoveredNodeIndex(p.i),
|
|
741
|
+
onBlur: () => setHoveredNodeIndex(null),
|
|
742
|
+
onClick: () => onSelect?.(p.node.id),
|
|
743
|
+
onDoubleClick: () => onOpenEntity?.(p.node.id),
|
|
744
|
+
onKeyDown: (e) => handleNodeKeydown(p.node.id, e),
|
|
745
|
+
};
|
|
746
|
+
return (_jsxs("g", { className: classNames("st-forceGraph__node", `st-forceGraph__node--${p.tone}`, hoveredNodeIndex !== null && hoveredNodeIndex !== p.i && "st-forceGraph__node--dim", pressed && "st-forceGraph__node--selected", focusId === p.node.id && "st-forceGraph__node--focus"), transform: `translate(${p.x} ${p.y})`, children: [p.shapePath ? (_jsx("path", { ...shapeProps, d: p.shapePath })) : (_jsx("circle", { ...shapeProps, r: p.r })), showLabels ? (_jsx("text", { className: "st-forceGraph__label", x: p.r + 3, y: 0, dominantBaseline: "middle", children: p.title })) : null] }, p.node.id));
|
|
747
|
+
}) })] }), hoveredNode ? (_jsxs("div", { className: "st-forceGraph__tooltip", role: "presentation", style: {
|
|
748
|
+
left: `${((hoveredNode.x - vbX) / vbW) * 100}%`,
|
|
749
|
+
top: `${((hoveredNode.y - vbY) / vbH) * 100}%`,
|
|
750
|
+
}, children: [_jsx("span", { className: "st-forceGraph__tooltipLabel", children: hoveredNode.title }), hoveredNode.node.group !== undefined ? (_jsx("span", { className: "st-forceGraph__tooltipMeta", children: hoveredNode.node.group })) : null, hoveredNodeRelCount > 0 ? (_jsxs("span", { className: "st-forceGraph__tooltipMeta", children: [hoveredNodeRelCount, " relation", hoveredNodeRelCount === 1 ? "" : "s"] })) : null] })) : null, hoveredEdge ? (_jsxs("div", { className: "st-forceGraph__tooltip st-forceGraph__tooltip--edge", role: "presentation", style: {
|
|
751
|
+
left: `${(((hoveredEdge.x1 + hoveredEdge.x2) / 2 - vbX) / vbW) * 100}%`,
|
|
752
|
+
top: `${(((hoveredEdge.y1 + hoveredEdge.y2) / 2 - vbY) / vbH) * 100}%`,
|
|
753
|
+
}, children: [_jsx("span", { className: "st-forceGraph__tooltipLabel", children: hoveredEdge.srcLabel }), hoveredEdge.edge.relation ? (_jsx("span", { className: "st-forceGraph__tooltipRelation", children: hoveredEdge.edge.relation })) : null, _jsx("span", { className: "st-forceGraph__tooltipLabel", children: hoveredEdge.tgtLabel })] })) : null, isZoomed ? (_jsx("button", { className: "st-forceGraph__resetBtn", type: "button", "aria-label": "Reset view", onClick: resetView, children: "\u21BA" })) : null, legend && legend.length > 0 ? (_jsx("div", { className: "st-forceGraph__legend", "aria-label": "Graph legend", children: legend.map((entry, idx) => {
|
|
754
|
+
const swatchPath = entry.shape !== undefined ? nodeShapePath(entry.shape, 7) : null;
|
|
755
|
+
const swatchTone = entry.tone ?? "category1";
|
|
756
|
+
return (_jsxs("div", { className: "st-forceGraph__legendEntry", children: [entry.shape !== undefined ? (
|
|
757
|
+
// Node shape legend entry
|
|
758
|
+
_jsx("svg", { className: "st-forceGraph__legendSwatch", viewBox: "-8 -8 16 16", width: "16", height: "16", "aria-hidden": "true", children: swatchPath ? (_jsx("path", { d: swatchPath, className: `st-forceGraph__legendShape st-forceGraph__legendShape--${swatchTone}` })) : (_jsx("circle", { r: "7", className: `st-forceGraph__legendShape st-forceGraph__legendShape--${swatchTone}` })) })) : (
|
|
759
|
+
// Edge style legend entry
|
|
760
|
+
_jsx("svg", { className: "st-forceGraph__legendSwatch", viewBox: "0 0 16 8", width: "16", height: "8", "aria-hidden": "true", children: _jsx("line", { x1: "0", y1: "4", x2: "16", y2: "4", className: classNames("st-forceGraph__legendEdge", entry.weak && "st-forceGraph__legendEdge--weak") }) })), _jsx("span", { className: "st-forceGraph__legendLabel", children: entry.label })] }, idx));
|
|
761
|
+
}) })) : null] }));
|
|
762
|
+
}
|
|
763
|
+
export function GraphLegend({ entries, title, className, ...rest }) {
|
|
764
|
+
return (_jsxs("div", { ...rest, className: classNames("st-graphLegend", className), "aria-label": title ?? "Graph legend", children: [title ? _jsx("p", { className: "st-graphLegend__title", children: title }) : null, _jsx("ul", { className: "st-graphLegend__list", role: "list", children: entries.map((entry, idx) => {
|
|
765
|
+
const swatchPath = entry.shape !== undefined ? nodeShapePath(entry.shape, 7) : null;
|
|
766
|
+
const swatchTone = entry.tone ?? "category1";
|
|
767
|
+
return (_jsxs("li", { className: "st-graphLegend__entry", children: [entry.shape !== undefined ? (_jsx("svg", { className: "st-graphLegend__swatch", viewBox: "-8 -8 16 16", width: "16", height: "16", "aria-hidden": "true", children: swatchPath ? (_jsx("path", { d: swatchPath, className: `st-graphLegend__shape st-graphLegend__shape--${swatchTone}` })) : (_jsx("circle", { r: "7", className: `st-graphLegend__shape st-graphLegend__shape--${swatchTone}` })) })) : (_jsx("svg", { className: "st-graphLegend__swatch", viewBox: "0 0 16 8", width: "16", height: "8", "aria-hidden": "true", children: _jsx("line", { x1: "0", y1: "4", x2: "16", y2: "4", className: classNames("st-graphLegend__edge", entry.weak && "st-graphLegend__edge--weak") }) })), _jsx("span", { className: "st-graphLegend__label", children: entry.label })] }, idx));
|
|
768
|
+
}) })] }));
|
|
408
769
|
}
|
|
409
770
|
export function Form({ status = "idle", message, children, className, ...rest }) {
|
|
410
771
|
return (_jsxs("form", { ...rest, className: classNames("st-form", className), children: [_jsx("div", { className: "st-form__body", children: children }), message ? _jsx("p", { className: classNames("st-form__message", `st-form__message--${status === "submitted" ? "success" : status === "error" ? "error" : "help"}`), children: message }) : null] }));
|