@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.
@@ -43,6 +43,7 @@ import { DndContext, pointerWithin, rectIntersection, closestCorners, useSensors
43
43
  import { useSortable, arrayMove, sortableKeyboardCoordinates, SortableContext, rectSortingStrategy, verticalListSortingStrategy } from '@dnd-kit/sortable';
44
44
  import { CSS } from '@dnd-kit/utilities';
45
45
  import { useNodeId, ReactFlowProvider, Handle, Position } from '@xyflow/react';
46
+ import { forceSimulation, forceLink, forceManyBody, forceCollide, forceX, forceY } from 'd3-force';
46
47
  import { isDrawHostPattern, getPatternDefinition, getComponentForPattern as getComponentForPattern$1 } from '@almadar/core/patterns';
47
48
 
48
49
  var __defProp = Object.defineProperty;
@@ -35360,7 +35361,10 @@ function resolveColor3(color, el) {
35360
35361
  const resolved = getComputedStyle(el).getPropertyValue(m[1]).trim();
35361
35362
  return resolved || (m[2]?.trim() ?? "#888888");
35362
35363
  }
35363
- var GROUP_COLORS2, labelMeasureCtx, GraphCanvas;
35364
+ function truncateLabel(label) {
35365
+ return label.length > MAX_LABEL_CHARS ? label.slice(0, MAX_LABEL_CHARS - 1) + "\u2026" : label;
35366
+ }
35367
+ var GROUP_COLORS2, labelMeasureCtx, MAX_LABEL_CHARS, GraphCanvas;
35364
35368
  var init_GraphCanvas = __esm({
35365
35369
  "components/core/molecules/GraphCanvas.tsx"() {
35366
35370
  "use client";
@@ -35381,6 +35385,7 @@ var init_GraphCanvas = __esm({
35381
35385
  "var(--color-accent)"
35382
35386
  ];
35383
35387
  labelMeasureCtx = null;
35388
+ MAX_LABEL_CHARS = 22;
35384
35389
  GraphCanvas = ({
35385
35390
  title,
35386
35391
  nodes: propNodes = [],
@@ -35485,9 +35490,13 @@ var init_GraphCanvas = __esm({
35485
35490
  useEffect(() => {
35486
35491
  const canvas = canvasRef.current;
35487
35492
  if (!canvas || propNodes.length === 0) return;
35488
- const w = logicalW;
35489
- const h = height;
35493
+ const viewW = logicalW;
35494
+ const viewH = height;
35495
+ const layoutScale = Math.max(1, Math.min(3, Math.sqrt(propNodes.length / 12)));
35496
+ const w = viewW * layoutScale;
35497
+ const h = viewH * layoutScale;
35490
35498
  const labelFont = resolveColor3("var(--font-family)", canvas) || "system-ui";
35499
+ const clamp01 = (v) => Math.min(1, Math.max(0, v));
35491
35500
  const prevPos = /* @__PURE__ */ new Map();
35492
35501
  for (const pn of nodesRef.current) if (pn.x != null && pn.y != null) prevPos.set(pn.id, { x: pn.x, y: pn.y });
35493
35502
  const simNodes = propNodes.map((n, idx) => {
@@ -35512,7 +35521,7 @@ var init_GraphCanvas = __esm({
35512
35521
  y = h * 0.2 + rand() * h * 0.6;
35513
35522
  }
35514
35523
  }
35515
- return { ...n, x, y, vx: 0, vy: 0, fx: 0, fy: 0 };
35524
+ return { ...n, x, y, vx: 0, vy: 0 };
35516
35525
  });
35517
35526
  nodesRef.current = simNodes;
35518
35527
  const prevIds = /* @__PURE__ */ new Set([...prevPos.keys()]);
@@ -35521,105 +35530,52 @@ var init_GraphCanvas = __esm({
35521
35530
  const overlap = prevIds.size === 0 ? 0 : kept / Math.max(prevIds.size, newIdList.length);
35522
35531
  const fullRelayout = !laidOutRef.current || overlap < 0.5;
35523
35532
  if (layout === "force") {
35524
- const maxIterations = 300;
35525
- const COOL = 0.12;
35526
- const COLLIDE_PASSES = 6;
35527
- const LABEL_GAP = 12;
35528
- const LABEL_H = 16;
35529
- const tick = (iter) => {
35530
- const nodes = nodesRef.current;
35531
- const centerX = w / 2;
35532
- const centerY = h / 2;
35533
- const temp = Math.max(COOL, 1 - iter / maxIterations);
35534
- for (const node of nodes) {
35535
- node.fx = 0;
35536
- node.fy = 0;
35537
- }
35538
- for (let i = 0; i < nodes.length; i++) {
35539
- for (let j = i + 1; j < nodes.length; j++) {
35540
- const dx = nodes[j].x - nodes[i].x;
35541
- const dy = nodes[j].y - nodes[i].y;
35542
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
35543
- const force = repulsion / (dist * dist) * temp;
35544
- const fx = dx / dist * force;
35545
- const fy = dy / dist * force;
35546
- nodes[i].fx -= fx;
35547
- nodes[i].fy -= fy;
35548
- nodes[j].fx += fx;
35549
- nodes[j].fy += fy;
35550
- }
35551
- }
35552
- const nodeById = new Map(nodes.map((n) => [n.id, n]));
35553
- const spring = (a, b, rest, k) => {
35554
- const dx = b.x - a.x;
35555
- const dy = b.y - a.y;
35556
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
35557
- const force = (dist - rest) * k * temp;
35558
- const fx = dx / dist * force;
35559
- const fy = dy / dist * force;
35560
- a.fx += fx;
35561
- a.fy += fy;
35562
- b.fx -= fx;
35563
- b.fy -= fy;
35564
- };
35533
+ if (fullRelayout) {
35534
+ const LABEL_GAP = 12;
35535
+ const LABEL_H = 16;
35536
+ for (const n of simNodes) n.labelW = showLabels && n.label ? measureLabelWidth(truncateLabel(n.label), labelFont) : 0;
35537
+ const SIMILARITY_NEIGHBORS = 5;
35538
+ const SIMILARITY_MIN_WEIGHT = 0.25;
35565
35539
  const drawnPairs = /* @__PURE__ */ new Set();
35566
- for (const edge of propEdges) {
35567
- const source = nodeById.get(edge.source);
35568
- const target = nodeById.get(edge.target);
35569
- if (!source || !target) continue;
35570
- drawnPairs.add(edgeKeyOf(edge.source, edge.target));
35571
- const w2 = Math.min(1, Math.max(0, edge.weight ?? 1));
35572
- spring(source, target, linkDistance * (0.35 + (1 - w2) * 0.3), 0.14);
35573
- }
35540
+ for (const edge of propEdges) drawnPairs.add(edgeKeyOf(edge.source, edge.target));
35541
+ const similarityNeighbors = /* @__PURE__ */ new Map();
35542
+ const bySource = /* @__PURE__ */ new Map();
35574
35543
  for (const pair of propSimilarity) {
35544
+ if (pair.weight < SIMILARITY_MIN_WEIGHT) continue;
35575
35545
  if (drawnPairs.has(edgeKeyOf(pair.source, pair.target))) continue;
35576
- const source = nodeById.get(pair.source);
35577
- const target = nodeById.get(pair.target);
35578
- if (!source || !target) continue;
35579
- const w2 = Math.min(1, Math.max(0, pair.weight));
35580
- spring(source, target, linkDistance * (1.35 - 0.55 * w2), 0.015);
35546
+ const arr = bySource.get(pair.source) ?? [];
35547
+ arr.push(pair);
35548
+ bySource.set(pair.source, arr);
35581
35549
  }
35582
- const centroids = /* @__PURE__ */ new Map();
35583
- for (const node of nodes) {
35584
- const g = node.group ?? "__none__";
35585
- let c = centroids.get(g);
35586
- if (!c) {
35587
- c = { x: 0, y: 0, n: 0 };
35588
- centroids.set(g, c);
35589
- }
35590
- c.x += node.x;
35591
- c.y += node.y;
35592
- c.n += 1;
35550
+ for (const [id, arr] of bySource) {
35551
+ arr.sort((a, b) => b.weight - a.weight);
35552
+ similarityNeighbors.set(id, new Set(arr.slice(0, SIMILARITY_NEIGHBORS).map((p) => p.target)));
35593
35553
  }
35594
- const multiCluster = centroids.size > 1;
35595
- for (const node of nodes) {
35596
- if (multiCluster) {
35597
- const c = centroids.get(node.group ?? "__none__");
35598
- if (c && c.n > 1) {
35599
- node.fx += (c.x / c.n - node.x) * 0.04 * temp;
35600
- node.fy += (c.y / c.n - node.y) * 0.04 * temp;
35601
- } else {
35602
- node.fx += (centerX - node.x) * 0.01 * temp;
35603
- node.fy += (centerY - node.y) * 0.01 * temp;
35604
- }
35605
- } else {
35606
- node.fx += (centerX - node.x) * 8e-3 * temp;
35607
- node.fy += (centerY - node.y) * 8e-3 * temp;
35608
- }
35609
- }
35610
- const damping = 0.9;
35611
- for (const node of nodes) {
35612
- node.vx = (node.vx + node.fx) * damping;
35613
- node.vy = (node.vy + node.fy) * damping;
35614
- node.x += node.vx;
35615
- node.y += node.vy;
35616
- node.x = Math.max(30, Math.min(w - 30, node.x));
35617
- node.y = Math.max(30, Math.min(h - 30, node.y));
35554
+ const activeSimilarity = propSimilarity.filter((pair) => {
35555
+ if (drawnPairs.has(edgeKeyOf(pair.source, pair.target))) return false;
35556
+ const a = similarityNeighbors.get(pair.source);
35557
+ const b = similarityNeighbors.get(pair.target);
35558
+ return (a?.has(pair.target) ?? false) || (b?.has(pair.source) ?? false);
35559
+ });
35560
+ const groupTarget = /* @__PURE__ */ new Map();
35561
+ const multiCluster = groups.length > 1;
35562
+ if (multiCluster) {
35563
+ const ring = Math.min(w, h) * 0.34;
35564
+ groups.forEach((g, i) => {
35565
+ const a = i / groups.length * 2 * Math.PI;
35566
+ groupTarget.set(g, { x: w / 2 + ring * Math.cos(a), y: h / 2 + ring * Math.sin(a) });
35567
+ });
35618
35568
  }
35569
+ const present = new Set(simNodes.map((n) => n.id));
35570
+ 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) }));
35571
+ 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) }));
35572
+ const collideRadius = (n) => Math.max(n.size || 8, (n.labelW ?? 0) / 2) + nodeSpacing / 2;
35573
+ const simulation = forceSimulation(simNodes).force("edge", forceLink(edgeLinks).id((d) => d.id).distance((l) => linkDistance * (0.35 + (1 - l.weight) * 0.3)).strength(0.9)).force("similarity", forceLink(simLinks).id((d) => d.id).distance((l) => linkDistance * (1.35 - 0.55 * l.weight)).strength(0.03)).force("charge", forceManyBody().strength(-repulsion * 0.5)).force("collide", forceCollide().radius(collideRadius).strength(0.9)).force("x", forceX((n) => groupTarget.get(n.group ?? "")?.x ?? w / 2).strength(multiCluster ? 0.05 : 0.02)).force("y", forceY((n) => groupTarget.get(n.group ?? "")?.y ?? h / 2).strength(multiCluster ? 0.05 : 0.02)).stop();
35574
+ for (let i = 0; i < 300; i++) simulation.tick();
35619
35575
  const pad = nodeSpacing / 2;
35620
35576
  const rectsOf = (n) => {
35621
35577
  const r = n.size || 8;
35622
- const lw = showLabels ? n.labelW ?? (n.labelW = n.label ? measureLabelWidth(n.label, labelFont) : 0) : 0;
35578
+ const lw = showLabels ? n.labelW ?? 0 : 0;
35623
35579
  return {
35624
35580
  circle: [n.x - r - pad, n.y - r - pad, n.x + r + pad, n.y + r + pad],
35625
35581
  label: [n.x - lw / 2 - pad, n.y + r + LABEL_GAP - 8, n.x + lw / 2 + pad, n.y + r + LABEL_GAP + LABEL_H]
@@ -35631,11 +35587,12 @@ var init_GraphCanvas = __esm({
35631
35587
  if (ox <= 0 || oy <= 0) return null;
35632
35588
  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 };
35633
35589
  };
35634
- for (let pass = 0; pass < COLLIDE_PASSES; pass++) {
35635
- for (let i = 0; i < nodes.length; i++) {
35636
- for (let j = i + 1; j < nodes.length; j++) {
35637
- const a = nodes[i];
35638
- const b = nodes[j];
35590
+ for (let pass = 0; pass < 24; pass++) {
35591
+ let moved = false;
35592
+ for (let i = 0; i < simNodes.length; i++) {
35593
+ for (let j = i + 1; j < simNodes.length; j++) {
35594
+ const a = simNodes[i];
35595
+ const b = simNodes[j];
35639
35596
  const ra = rectsOf(a);
35640
35597
  const rb = rectsOf(b);
35641
35598
  let best = null;
@@ -35644,25 +35601,35 @@ var init_GraphCanvas = __esm({
35644
35601
  if (s && (!best || s.depth < best.depth)) best = s;
35645
35602
  }
35646
35603
  if (best) {
35604
+ moved = true;
35647
35605
  const push = best.depth / 2;
35648
35606
  if (best.axis === "x") {
35649
35607
  a.x = Math.max(30, Math.min(w - 30, a.x - push * best.sign));
35650
35608
  b.x = Math.max(30, Math.min(w - 30, b.x + push * best.sign));
35651
- a.vx = 0;
35652
- b.vx = 0;
35653
35609
  } else {
35654
35610
  a.y = Math.max(30, Math.min(h - 30, a.y - push * best.sign));
35655
35611
  b.y = Math.max(30, Math.min(h - 30, b.y + push * best.sign));
35656
- a.vy = 0;
35657
- b.vy = 0;
35658
35612
  }
35659
35613
  }
35660
35614
  }
35661
35615
  }
35616
+ if (!moved) break;
35662
35617
  }
35663
- };
35664
- if (fullRelayout) {
35665
- for (let i = 0; i < maxIterations; i++) tick(i);
35618
+ const fitPad = 40;
35619
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
35620
+ for (const node of nodesRef.current) {
35621
+ const r = node.size || 8;
35622
+ const lw = showLabels ? node.labelW ?? 0 : 0;
35623
+ minX = Math.min(minX, node.x - r, node.x - lw / 2);
35624
+ minY = Math.min(minY, node.y - r);
35625
+ maxX = Math.max(maxX, node.x + r, node.x + lw / 2);
35626
+ maxY = Math.max(maxY, node.y + r + LABEL_GAP + LABEL_H);
35627
+ }
35628
+ const bboxW = Math.max(1, maxX - minX + fitPad * 2);
35629
+ const bboxH = Math.max(1, maxY - minY + fitPad * 2);
35630
+ const nextZoom = Math.min(1, viewW / bboxW, viewH / bboxH);
35631
+ setZoom(nextZoom);
35632
+ setOffset({ x: fitPad - minX * nextZoom, y: fitPad - minY * nextZoom });
35666
35633
  laidOutRef.current = true;
35667
35634
  } else {
35668
35635
  const centroids = /* @__PURE__ */ new Map();
@@ -35773,6 +35740,7 @@ var init_GraphCanvas = __esm({
35773
35740
  }
35774
35741
  ctx.stroke();
35775
35742
  if (showLabels && node.label) {
35743
+ const displayLabel = truncateLabel(node.label);
35776
35744
  ctx.font = `${isSelected || isHovered ? "600" : "500"} 12px ${fontFamily}`;
35777
35745
  ctx.textAlign = "center";
35778
35746
  ctx.textBaseline = "middle";
@@ -35780,9 +35748,9 @@ var init_GraphCanvas = __esm({
35780
35748
  ctx.lineWidth = 3;
35781
35749
  ctx.lineJoin = "round";
35782
35750
  ctx.strokeStyle = bgColor;
35783
- ctx.strokeText(node.label, node.x, ly);
35751
+ ctx.strokeText(displayLabel, node.x, ly);
35784
35752
  ctx.fillStyle = isSelected || isHovered ? fgColor : mutedColor;
35785
- ctx.fillText(node.label, node.x, ly);
35753
+ ctx.fillText(displayLabel, node.x, ly);
35786
35754
  }
35787
35755
  if (node.badge && node.badge > 1) {
35788
35756
  const bx = node.x + radius * 0.7;
@@ -35930,6 +35898,20 @@ var init_GraphCanvas = __esm({
35930
35898
  },
35931
35899
  [toCoords, nodeAt, onNodeDoubleClick]
35932
35900
  );
35901
+ const hoveredObj = hoveredNode ? nodesRef.current.find((n) => n.id === hoveredNode) : void 0;
35902
+ const canvasEl = canvasRef.current;
35903
+ const showLabelTooltip = Boolean(
35904
+ hoveredObj?.label && hoveredObj.label.length > MAX_LABEL_CHARS && hoveredObj.x != null && canvasEl
35905
+ );
35906
+ const labelTooltipStyle = (() => {
35907
+ if (!showLabelTooltip || !canvasEl || !hoveredObj || hoveredObj.x == null || hoveredObj.y == null) return void 0;
35908
+ const rect = canvasEl.getBoundingClientRect();
35909
+ return {
35910
+ left: rect.left + offset.x + hoveredObj.x * zoom,
35911
+ top: rect.top + offset.y + hoveredObj.y * zoom - 10,
35912
+ transform: "translate(-50%, -100%)"
35913
+ };
35914
+ })();
35933
35915
  if (isLoading) {
35934
35916
  return /* @__PURE__ */ jsx(LoadingState, { message: t("common.loading"), className });
35935
35917
  }
@@ -35983,23 +35965,43 @@ var init_GraphCanvas = __esm({
35983
35965
  ]
35984
35966
  }
35985
35967
  ),
35986
- /* @__PURE__ */ jsx(Box, { className: "w-full bg-background", children: /* @__PURE__ */ jsx(
35987
- "canvas",
35988
- {
35989
- ref: canvasRef,
35990
- width: Math.round(logicalW * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
35991
- height: Math.round(height * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
35992
- className: "w-full cursor-grab active:cursor-grabbing touch-none",
35993
- style: { height },
35994
- onPointerDown: gestureHandlers.onPointerDown,
35995
- onPointerMove: gestureHandlers.onPointerMove,
35996
- onPointerUp: gestureHandlers.onPointerUp,
35997
- onPointerCancel: gestureHandlers.onPointerCancel,
35998
- onPointerLeave: handlePointerLeave,
35999
- onWheel: gestureHandlers.onWheel,
36000
- onDoubleClick: handleDoubleClick
36001
- }
36002
- ) }),
35968
+ /* @__PURE__ */ jsxs(Box, { className: "w-full bg-background", children: [
35969
+ /* @__PURE__ */ jsx(
35970
+ "canvas",
35971
+ {
35972
+ ref: canvasRef,
35973
+ width: Math.round(logicalW * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
35974
+ height: Math.round(height * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
35975
+ className: "w-full cursor-grab active:cursor-grabbing touch-none",
35976
+ style: { height },
35977
+ onPointerDown: gestureHandlers.onPointerDown,
35978
+ onPointerMove: gestureHandlers.onPointerMove,
35979
+ onPointerUp: gestureHandlers.onPointerUp,
35980
+ onPointerCancel: gestureHandlers.onPointerCancel,
35981
+ onPointerLeave: handlePointerLeave,
35982
+ onWheel: gestureHandlers.onWheel,
35983
+ onDoubleClick: handleDoubleClick
35984
+ }
35985
+ ),
35986
+ typeof window !== "undefined" && showLabelTooltip && labelTooltipStyle && createPortal(
35987
+ /* @__PURE__ */ jsx(
35988
+ "div",
35989
+ {
35990
+ className: cn(
35991
+ "fixed z-50 px-3 py-2 max-w-xs",
35992
+ "bg-primary text-primary-foreground",
35993
+ "shadow-elevation-popover rounded-sm",
35994
+ "text-sm pointer-events-none",
35995
+ "break-words whitespace-normal"
35996
+ ),
35997
+ style: labelTooltipStyle,
35998
+ role: "tooltip",
35999
+ children: hoveredObj?.label
36000
+ }
36001
+ ),
36002
+ document.body
36003
+ )
36004
+ ] }),
36003
36005
  groups.length > 1 && /* @__PURE__ */ jsx(HStack, { gap: "md", className: "px-4 py-2 border-t border-border", wrap: true, children: groups.map((group, idx) => /* @__PURE__ */ jsxs(HStack, { gap: "xs", align: "center", children: [
36004
36006
  /* @__PURE__ */ jsx(
36005
36007
  Box,