@almadar/ui 5.119.0 → 5.121.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.
@@ -40,6 +40,7 @@ var core = require('@almadar/core');
40
40
  var core$1 = require('@dnd-kit/core');
41
41
  var sortable = require('@dnd-kit/sortable');
42
42
  var utilities = require('@dnd-kit/utilities');
43
+ var d3Force = require('d3-force');
43
44
  var patterns = require('@almadar/core/patterns');
44
45
  var OrbitalServerRuntime = require('@almadar/runtime/OrbitalServerRuntime');
45
46
  var registry = require('@almadar/std/registry');
@@ -37275,7 +37276,10 @@ function resolveColor3(color, el) {
37275
37276
  const resolved = getComputedStyle(el).getPropertyValue(m[1]).trim();
37276
37277
  return resolved || (m[2]?.trim() ?? "#888888");
37277
37278
  }
37278
- var GROUP_COLORS2, labelMeasureCtx, GraphCanvas;
37279
+ function truncateLabel(label) {
37280
+ return label.length > MAX_LABEL_CHARS ? label.slice(0, MAX_LABEL_CHARS - 1) + "\u2026" : label;
37281
+ }
37282
+ var GROUP_COLORS2, labelMeasureCtx, MAX_LABEL_CHARS, GraphCanvas;
37279
37283
  var init_GraphCanvas = __esm({
37280
37284
  "components/core/molecules/GraphCanvas.tsx"() {
37281
37285
  "use client";
@@ -37296,6 +37300,7 @@ var init_GraphCanvas = __esm({
37296
37300
  "var(--color-accent)"
37297
37301
  ];
37298
37302
  labelMeasureCtx = null;
37303
+ MAX_LABEL_CHARS = 22;
37299
37304
  GraphCanvas = ({
37300
37305
  title,
37301
37306
  nodes: propNodes = [],
@@ -37400,9 +37405,13 @@ var init_GraphCanvas = __esm({
37400
37405
  React90.useEffect(() => {
37401
37406
  const canvas = canvasRef.current;
37402
37407
  if (!canvas || propNodes.length === 0) return;
37403
- const w = logicalW;
37404
- const h = height;
37408
+ const viewW = logicalW;
37409
+ const viewH = height;
37410
+ const layoutScale = Math.max(1, Math.min(3, Math.sqrt(propNodes.length / 12)));
37411
+ const w = viewW * layoutScale;
37412
+ const h = viewH * layoutScale;
37405
37413
  const labelFont = resolveColor3("var(--font-family)", canvas) || "system-ui";
37414
+ const clamp01 = (v) => Math.min(1, Math.max(0, v));
37406
37415
  const prevPos = /* @__PURE__ */ new Map();
37407
37416
  for (const pn of nodesRef.current) if (pn.x != null && pn.y != null) prevPos.set(pn.id, { x: pn.x, y: pn.y });
37408
37417
  const simNodes = propNodes.map((n, idx) => {
@@ -37427,7 +37436,7 @@ var init_GraphCanvas = __esm({
37427
37436
  y = h * 0.2 + rand() * h * 0.6;
37428
37437
  }
37429
37438
  }
37430
- return { ...n, x, y, vx: 0, vy: 0, fx: 0, fy: 0 };
37439
+ return { ...n, x, y, vx: 0, vy: 0 };
37431
37440
  });
37432
37441
  nodesRef.current = simNodes;
37433
37442
  const prevIds = /* @__PURE__ */ new Set([...prevPos.keys()]);
@@ -37436,105 +37445,52 @@ var init_GraphCanvas = __esm({
37436
37445
  const overlap = prevIds.size === 0 ? 0 : kept / Math.max(prevIds.size, newIdList.length);
37437
37446
  const fullRelayout = !laidOutRef.current || overlap < 0.5;
37438
37447
  if (layout === "force") {
37439
- const maxIterations = 300;
37440
- const COOL = 0.12;
37441
- const COLLIDE_PASSES = 6;
37442
- const LABEL_GAP = 12;
37443
- const LABEL_H = 16;
37444
- const tick = (iter) => {
37445
- const nodes = nodesRef.current;
37446
- const centerX = w / 2;
37447
- const centerY = h / 2;
37448
- const temp = Math.max(COOL, 1 - iter / maxIterations);
37449
- for (const node of nodes) {
37450
- node.fx = 0;
37451
- node.fy = 0;
37452
- }
37453
- for (let i = 0; i < nodes.length; i++) {
37454
- for (let j = i + 1; j < nodes.length; j++) {
37455
- const dx = nodes[j].x - nodes[i].x;
37456
- const dy = nodes[j].y - nodes[i].y;
37457
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
37458
- const force = repulsion / (dist * dist) * temp;
37459
- const fx = dx / dist * force;
37460
- const fy = dy / dist * force;
37461
- nodes[i].fx -= fx;
37462
- nodes[i].fy -= fy;
37463
- nodes[j].fx += fx;
37464
- nodes[j].fy += fy;
37465
- }
37466
- }
37467
- const nodeById = new Map(nodes.map((n) => [n.id, n]));
37468
- const spring = (a, b, rest, k) => {
37469
- const dx = b.x - a.x;
37470
- const dy = b.y - a.y;
37471
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
37472
- const force = (dist - rest) * k * temp;
37473
- const fx = dx / dist * force;
37474
- const fy = dy / dist * force;
37475
- a.fx += fx;
37476
- a.fy += fy;
37477
- b.fx -= fx;
37478
- b.fy -= fy;
37479
- };
37448
+ if (fullRelayout) {
37449
+ const LABEL_GAP = 12;
37450
+ const LABEL_H = 16;
37451
+ for (const n of simNodes) n.labelW = showLabels && n.label ? measureLabelWidth(truncateLabel(n.label), labelFont) : 0;
37452
+ const SIMILARITY_NEIGHBORS = 5;
37453
+ const SIMILARITY_MIN_WEIGHT = 0.25;
37480
37454
  const drawnPairs = /* @__PURE__ */ new Set();
37481
- for (const edge of propEdges) {
37482
- const source = nodeById.get(edge.source);
37483
- const target = nodeById.get(edge.target);
37484
- if (!source || !target) continue;
37485
- drawnPairs.add(edgeKeyOf(edge.source, edge.target));
37486
- const w2 = Math.min(1, Math.max(0, edge.weight ?? 1));
37487
- spring(source, target, linkDistance * (0.35 + (1 - w2) * 0.3), 0.14);
37488
- }
37455
+ for (const edge of propEdges) drawnPairs.add(edgeKeyOf(edge.source, edge.target));
37456
+ const similarityNeighbors = /* @__PURE__ */ new Map();
37457
+ const bySource = /* @__PURE__ */ new Map();
37489
37458
  for (const pair of propSimilarity) {
37459
+ if (pair.weight < SIMILARITY_MIN_WEIGHT) continue;
37490
37460
  if (drawnPairs.has(edgeKeyOf(pair.source, pair.target))) continue;
37491
- const source = nodeById.get(pair.source);
37492
- const target = nodeById.get(pair.target);
37493
- if (!source || !target) continue;
37494
- const w2 = Math.min(1, Math.max(0, pair.weight));
37495
- spring(source, target, linkDistance * (1.35 - 0.55 * w2), 0.015);
37461
+ const arr = bySource.get(pair.source) ?? [];
37462
+ arr.push(pair);
37463
+ bySource.set(pair.source, arr);
37496
37464
  }
37497
- const centroids = /* @__PURE__ */ new Map();
37498
- for (const node of nodes) {
37499
- const g = node.group ?? "__none__";
37500
- let c = centroids.get(g);
37501
- if (!c) {
37502
- c = { x: 0, y: 0, n: 0 };
37503
- centroids.set(g, c);
37504
- }
37505
- c.x += node.x;
37506
- c.y += node.y;
37507
- c.n += 1;
37465
+ for (const [id, arr] of bySource) {
37466
+ arr.sort((a, b) => b.weight - a.weight);
37467
+ similarityNeighbors.set(id, new Set(arr.slice(0, SIMILARITY_NEIGHBORS).map((p) => p.target)));
37508
37468
  }
37509
- const multiCluster = centroids.size > 1;
37510
- for (const node of nodes) {
37511
- if (multiCluster) {
37512
- const c = centroids.get(node.group ?? "__none__");
37513
- if (c && c.n > 1) {
37514
- node.fx += (c.x / c.n - node.x) * 0.04 * temp;
37515
- node.fy += (c.y / c.n - node.y) * 0.04 * temp;
37516
- } else {
37517
- node.fx += (centerX - node.x) * 0.01 * temp;
37518
- node.fy += (centerY - node.y) * 0.01 * temp;
37519
- }
37520
- } else {
37521
- node.fx += (centerX - node.x) * 8e-3 * temp;
37522
- node.fy += (centerY - node.y) * 8e-3 * temp;
37523
- }
37524
- }
37525
- const damping = 0.9;
37526
- for (const node of nodes) {
37527
- node.vx = (node.vx + node.fx) * damping;
37528
- node.vy = (node.vy + node.fy) * damping;
37529
- node.x += node.vx;
37530
- node.y += node.vy;
37531
- node.x = Math.max(30, Math.min(w - 30, node.x));
37532
- node.y = Math.max(30, Math.min(h - 30, node.y));
37469
+ const activeSimilarity = propSimilarity.filter((pair) => {
37470
+ if (drawnPairs.has(edgeKeyOf(pair.source, pair.target))) return false;
37471
+ const a = similarityNeighbors.get(pair.source);
37472
+ const b = similarityNeighbors.get(pair.target);
37473
+ return (a?.has(pair.target) ?? false) || (b?.has(pair.source) ?? false);
37474
+ });
37475
+ const groupTarget = /* @__PURE__ */ new Map();
37476
+ const multiCluster = groups.length > 1;
37477
+ if (multiCluster) {
37478
+ const ring = Math.min(w, h) * 0.34;
37479
+ groups.forEach((g, i) => {
37480
+ const a = i / groups.length * 2 * Math.PI;
37481
+ groupTarget.set(g, { x: w / 2 + ring * Math.cos(a), y: h / 2 + ring * Math.sin(a) });
37482
+ });
37533
37483
  }
37484
+ const present = new Set(simNodes.map((n) => n.id));
37485
+ const edgeLinks = propEdges.filter((e) => present.has(e.source) && present.has(e.target)).map((e) => ({ source: e.source, target: e.target, weight: clamp01(e.weight ?? 1) }));
37486
+ const simLinks = activeSimilarity.filter((p) => present.has(p.source) && present.has(p.target)).map((p) => ({ source: p.source, target: p.target, weight: clamp01(p.weight) }));
37487
+ const collideRadius = (n) => Math.max(n.size || 8, (n.labelW ?? 0) / 2) + nodeSpacing / 2;
37488
+ const simulation = d3Force.forceSimulation(simNodes).force("edge", d3Force.forceLink(edgeLinks).id((d) => d.id).distance((l) => linkDistance * (0.35 + (1 - l.weight) * 0.3)).strength(0.9)).force("similarity", d3Force.forceLink(simLinks).id((d) => d.id).distance((l) => linkDistance * (1.35 - 0.55 * l.weight)).strength(0.03)).force("charge", d3Force.forceManyBody().strength(-repulsion * 0.5)).force("collide", d3Force.forceCollide().radius(collideRadius).strength(0.9)).force("x", d3Force.forceX((n) => groupTarget.get(n.group ?? "")?.x ?? w / 2).strength(multiCluster ? 0.05 : 0.02)).force("y", d3Force.forceY((n) => groupTarget.get(n.group ?? "")?.y ?? h / 2).strength(multiCluster ? 0.05 : 0.02)).stop();
37489
+ for (let i = 0; i < 300; i++) simulation.tick();
37534
37490
  const pad = nodeSpacing / 2;
37535
37491
  const rectsOf = (n) => {
37536
37492
  const r2 = n.size || 8;
37537
- const lw = showLabels ? n.labelW ?? (n.labelW = n.label ? measureLabelWidth(n.label, labelFont) : 0) : 0;
37493
+ const lw = showLabels ? n.labelW ?? 0 : 0;
37538
37494
  return {
37539
37495
  circle: [n.x - r2 - pad, n.y - r2 - pad, n.x + r2 + pad, n.y + r2 + pad],
37540
37496
  label: [n.x - lw / 2 - pad, n.y + r2 + LABEL_GAP - 8, n.x + lw / 2 + pad, n.y + r2 + LABEL_GAP + LABEL_H]
@@ -37546,11 +37502,12 @@ var init_GraphCanvas = __esm({
37546
37502
  if (ox <= 0 || oy <= 0) return null;
37547
37503
  return ox <= oy ? { axis: "x", depth: ox, sign: r1[0] + r1[2] < r2[0] + r2[2] ? 1 : -1 } : { axis: "y", depth: oy, sign: r1[1] + r1[3] < r2[1] + r2[3] ? 1 : -1 };
37548
37504
  };
37549
- for (let pass = 0; pass < COLLIDE_PASSES; pass++) {
37550
- for (let i = 0; i < nodes.length; i++) {
37551
- for (let j = i + 1; j < nodes.length; j++) {
37552
- const a = nodes[i];
37553
- const b = nodes[j];
37505
+ for (let pass = 0; pass < 24; pass++) {
37506
+ let moved = false;
37507
+ for (let i = 0; i < simNodes.length; i++) {
37508
+ for (let j = i + 1; j < simNodes.length; j++) {
37509
+ const a = simNodes[i];
37510
+ const b = simNodes[j];
37554
37511
  const ra = rectsOf(a);
37555
37512
  const rb = rectsOf(b);
37556
37513
  let best = null;
@@ -37559,25 +37516,35 @@ var init_GraphCanvas = __esm({
37559
37516
  if (s && (!best || s.depth < best.depth)) best = s;
37560
37517
  }
37561
37518
  if (best) {
37519
+ moved = true;
37562
37520
  const push = best.depth / 2;
37563
37521
  if (best.axis === "x") {
37564
37522
  a.x = Math.max(30, Math.min(w - 30, a.x - push * best.sign));
37565
37523
  b.x = Math.max(30, Math.min(w - 30, b.x + push * best.sign));
37566
- a.vx = 0;
37567
- b.vx = 0;
37568
37524
  } else {
37569
37525
  a.y = Math.max(30, Math.min(h - 30, a.y - push * best.sign));
37570
37526
  b.y = Math.max(30, Math.min(h - 30, b.y + push * best.sign));
37571
- a.vy = 0;
37572
- b.vy = 0;
37573
37527
  }
37574
37528
  }
37575
37529
  }
37576
37530
  }
37531
+ if (!moved) break;
37577
37532
  }
37578
- };
37579
- if (fullRelayout) {
37580
- for (let i = 0; i < maxIterations; i++) tick(i);
37533
+ const fitPad = 40;
37534
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
37535
+ for (const node of nodesRef.current) {
37536
+ const r2 = node.size || 8;
37537
+ const lw = showLabels ? node.labelW ?? 0 : 0;
37538
+ minX = Math.min(minX, node.x - r2, node.x - lw / 2);
37539
+ minY = Math.min(minY, node.y - r2);
37540
+ maxX = Math.max(maxX, node.x + r2, node.x + lw / 2);
37541
+ maxY = Math.max(maxY, node.y + r2 + LABEL_GAP + LABEL_H);
37542
+ }
37543
+ const bboxW = Math.max(1, maxX - minX + fitPad * 2);
37544
+ const bboxH = Math.max(1, maxY - minY + fitPad * 2);
37545
+ const nextZoom = Math.min(1, viewW / bboxW, viewH / bboxH);
37546
+ setZoom(nextZoom);
37547
+ setOffset({ x: fitPad - minX * nextZoom, y: fitPad - minY * nextZoom });
37581
37548
  laidOutRef.current = true;
37582
37549
  } else {
37583
37550
  const centroids = /* @__PURE__ */ new Map();
@@ -37688,6 +37655,7 @@ var init_GraphCanvas = __esm({
37688
37655
  }
37689
37656
  ctx.stroke();
37690
37657
  if (showLabels && node.label) {
37658
+ const displayLabel = truncateLabel(node.label);
37691
37659
  ctx.font = `${isSelected || isHovered ? "600" : "500"} 12px ${fontFamily}`;
37692
37660
  ctx.textAlign = "center";
37693
37661
  ctx.textBaseline = "middle";
@@ -37695,9 +37663,9 @@ var init_GraphCanvas = __esm({
37695
37663
  ctx.lineWidth = 3;
37696
37664
  ctx.lineJoin = "round";
37697
37665
  ctx.strokeStyle = bgColor;
37698
- ctx.strokeText(node.label, node.x, ly);
37666
+ ctx.strokeText(displayLabel, node.x, ly);
37699
37667
  ctx.fillStyle = isSelected || isHovered ? fgColor : mutedColor;
37700
- ctx.fillText(node.label, node.x, ly);
37668
+ ctx.fillText(displayLabel, node.x, ly);
37701
37669
  }
37702
37670
  if (node.badge && node.badge > 1) {
37703
37671
  const bx = node.x + radius * 0.7;
@@ -37845,6 +37813,20 @@ var init_GraphCanvas = __esm({
37845
37813
  },
37846
37814
  [toCoords, nodeAt, onNodeDoubleClick]
37847
37815
  );
37816
+ const hoveredObj = hoveredNode ? nodesRef.current.find((n) => n.id === hoveredNode) : void 0;
37817
+ const canvasEl = canvasRef.current;
37818
+ const showLabelTooltip = Boolean(
37819
+ hoveredObj?.label && hoveredObj.label.length > MAX_LABEL_CHARS && hoveredObj.x != null && canvasEl
37820
+ );
37821
+ const labelTooltipStyle = (() => {
37822
+ if (!showLabelTooltip || !canvasEl || !hoveredObj || hoveredObj.x == null || hoveredObj.y == null) return void 0;
37823
+ const rect = canvasEl.getBoundingClientRect();
37824
+ return {
37825
+ left: rect.left + offset.x + hoveredObj.x * zoom,
37826
+ top: rect.top + offset.y + hoveredObj.y * zoom - 10,
37827
+ transform: "translate(-50%, -100%)"
37828
+ };
37829
+ })();
37848
37830
  if (isLoading) {
37849
37831
  return /* @__PURE__ */ jsxRuntime.jsx(LoadingState, { message: t("common.loading"), className });
37850
37832
  }
@@ -37898,23 +37880,43 @@ var init_GraphCanvas = __esm({
37898
37880
  ]
37899
37881
  }
37900
37882
  ),
37901
- /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "w-full bg-background", children: /* @__PURE__ */ jsxRuntime.jsx(
37902
- "canvas",
37903
- {
37904
- ref: canvasRef,
37905
- width: Math.round(logicalW * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
37906
- height: Math.round(height * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
37907
- className: "w-full cursor-grab active:cursor-grabbing touch-none",
37908
- style: { height },
37909
- onPointerDown: gestureHandlers.onPointerDown,
37910
- onPointerMove: gestureHandlers.onPointerMove,
37911
- onPointerUp: gestureHandlers.onPointerUp,
37912
- onPointerCancel: gestureHandlers.onPointerCancel,
37913
- onPointerLeave: handlePointerLeave,
37914
- onWheel: gestureHandlers.onWheel,
37915
- onDoubleClick: handleDoubleClick
37916
- }
37917
- ) }),
37883
+ /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "w-full bg-background", children: [
37884
+ /* @__PURE__ */ jsxRuntime.jsx(
37885
+ "canvas",
37886
+ {
37887
+ ref: canvasRef,
37888
+ width: Math.round(logicalW * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
37889
+ height: Math.round(height * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
37890
+ className: "w-full cursor-grab active:cursor-grabbing touch-none",
37891
+ style: { height },
37892
+ onPointerDown: gestureHandlers.onPointerDown,
37893
+ onPointerMove: gestureHandlers.onPointerMove,
37894
+ onPointerUp: gestureHandlers.onPointerUp,
37895
+ onPointerCancel: gestureHandlers.onPointerCancel,
37896
+ onPointerLeave: handlePointerLeave,
37897
+ onWheel: gestureHandlers.onWheel,
37898
+ onDoubleClick: handleDoubleClick
37899
+ }
37900
+ ),
37901
+ typeof window !== "undefined" && showLabelTooltip && labelTooltipStyle && reactDom.createPortal(
37902
+ /* @__PURE__ */ jsxRuntime.jsx(
37903
+ "div",
37904
+ {
37905
+ className: cn(
37906
+ "fixed z-50 px-3 py-2 max-w-xs",
37907
+ "bg-primary text-primary-foreground",
37908
+ "shadow-elevation-popover rounded-sm",
37909
+ "text-sm pointer-events-none",
37910
+ "break-words whitespace-normal"
37911
+ ),
37912
+ style: labelTooltipStyle,
37913
+ role: "tooltip",
37914
+ children: hoveredObj?.label
37915
+ }
37916
+ ),
37917
+ document.body
37918
+ )
37919
+ ] }),
37918
37920
  groups.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(HStack, { gap: "md", className: "px-4 py-2 border-t border-border", wrap: true, children: groups.map((group, idx) => /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", align: "center", children: [
37919
37921
  /* @__PURE__ */ jsxRuntime.jsx(
37920
37922
  Box,