@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.
@@ -42,6 +42,7 @@ var core$1 = require('@dnd-kit/core');
42
42
  var sortable = require('@dnd-kit/sortable');
43
43
  var utilities = require('@dnd-kit/utilities');
44
44
  var react = require('@xyflow/react');
45
+ var d3Force = require('d3-force');
45
46
  var patterns = require('@almadar/core/patterns');
46
47
  var reactQuery = require('@tanstack/react-query');
47
48
 
@@ -36664,7 +36665,10 @@ function resolveColor3(color, el) {
36664
36665
  const resolved = getComputedStyle(el).getPropertyValue(m[1]).trim();
36665
36666
  return resolved || (m[2]?.trim() ?? "#888888");
36666
36667
  }
36667
- var GROUP_COLORS2, labelMeasureCtx; exports.GraphCanvas = void 0;
36668
+ function truncateLabel(label) {
36669
+ return label.length > MAX_LABEL_CHARS ? label.slice(0, MAX_LABEL_CHARS - 1) + "\u2026" : label;
36670
+ }
36671
+ var GROUP_COLORS2, labelMeasureCtx, MAX_LABEL_CHARS; exports.GraphCanvas = void 0;
36668
36672
  var init_GraphCanvas = __esm({
36669
36673
  "components/core/molecules/GraphCanvas.tsx"() {
36670
36674
  "use client";
@@ -36685,6 +36689,7 @@ var init_GraphCanvas = __esm({
36685
36689
  "var(--color-accent)"
36686
36690
  ];
36687
36691
  labelMeasureCtx = null;
36692
+ MAX_LABEL_CHARS = 22;
36688
36693
  exports.GraphCanvas = ({
36689
36694
  title,
36690
36695
  nodes: propNodes = [],
@@ -36789,9 +36794,13 @@ var init_GraphCanvas = __esm({
36789
36794
  React73.useEffect(() => {
36790
36795
  const canvas = canvasRef.current;
36791
36796
  if (!canvas || propNodes.length === 0) return;
36792
- const w = logicalW;
36793
- const h = height;
36797
+ const viewW = logicalW;
36798
+ const viewH = height;
36799
+ const layoutScale = Math.max(1, Math.min(3, Math.sqrt(propNodes.length / 12)));
36800
+ const w = viewW * layoutScale;
36801
+ const h = viewH * layoutScale;
36794
36802
  const labelFont = resolveColor3("var(--font-family)", canvas) || "system-ui";
36803
+ const clamp01 = (v) => Math.min(1, Math.max(0, v));
36795
36804
  const prevPos = /* @__PURE__ */ new Map();
36796
36805
  for (const pn of nodesRef.current) if (pn.x != null && pn.y != null) prevPos.set(pn.id, { x: pn.x, y: pn.y });
36797
36806
  const simNodes = propNodes.map((n, idx) => {
@@ -36816,7 +36825,7 @@ var init_GraphCanvas = __esm({
36816
36825
  y = h * 0.2 + rand() * h * 0.6;
36817
36826
  }
36818
36827
  }
36819
- return { ...n, x, y, vx: 0, vy: 0, fx: 0, fy: 0 };
36828
+ return { ...n, x, y, vx: 0, vy: 0 };
36820
36829
  });
36821
36830
  nodesRef.current = simNodes;
36822
36831
  const prevIds = /* @__PURE__ */ new Set([...prevPos.keys()]);
@@ -36825,105 +36834,52 @@ var init_GraphCanvas = __esm({
36825
36834
  const overlap = prevIds.size === 0 ? 0 : kept / Math.max(prevIds.size, newIdList.length);
36826
36835
  const fullRelayout = !laidOutRef.current || overlap < 0.5;
36827
36836
  if (layout === "force") {
36828
- const maxIterations = 300;
36829
- const COOL = 0.12;
36830
- const COLLIDE_PASSES = 6;
36831
- const LABEL_GAP = 12;
36832
- const LABEL_H = 16;
36833
- const tick = (iter) => {
36834
- const nodes = nodesRef.current;
36835
- const centerX = w / 2;
36836
- const centerY = h / 2;
36837
- const temp = Math.max(COOL, 1 - iter / maxIterations);
36838
- for (const node of nodes) {
36839
- node.fx = 0;
36840
- node.fy = 0;
36841
- }
36842
- for (let i = 0; i < nodes.length; i++) {
36843
- for (let j = i + 1; j < nodes.length; j++) {
36844
- const dx = nodes[j].x - nodes[i].x;
36845
- const dy = nodes[j].y - nodes[i].y;
36846
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
36847
- const force = repulsion / (dist * dist) * temp;
36848
- const fx = dx / dist * force;
36849
- const fy = dy / dist * force;
36850
- nodes[i].fx -= fx;
36851
- nodes[i].fy -= fy;
36852
- nodes[j].fx += fx;
36853
- nodes[j].fy += fy;
36854
- }
36855
- }
36856
- const nodeById = new Map(nodes.map((n) => [n.id, n]));
36857
- const spring = (a, b, rest, k) => {
36858
- const dx = b.x - a.x;
36859
- const dy = b.y - a.y;
36860
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
36861
- const force = (dist - rest) * k * temp;
36862
- const fx = dx / dist * force;
36863
- const fy = dy / dist * force;
36864
- a.fx += fx;
36865
- a.fy += fy;
36866
- b.fx -= fx;
36867
- b.fy -= fy;
36868
- };
36837
+ if (fullRelayout) {
36838
+ const LABEL_GAP = 12;
36839
+ const LABEL_H = 16;
36840
+ for (const n of simNodes) n.labelW = showLabels && n.label ? measureLabelWidth(truncateLabel(n.label), labelFont) : 0;
36841
+ const SIMILARITY_NEIGHBORS = 5;
36842
+ const SIMILARITY_MIN_WEIGHT = 0.25;
36869
36843
  const drawnPairs = /* @__PURE__ */ new Set();
36870
- for (const edge of propEdges) {
36871
- const source = nodeById.get(edge.source);
36872
- const target = nodeById.get(edge.target);
36873
- if (!source || !target) continue;
36874
- drawnPairs.add(edgeKeyOf(edge.source, edge.target));
36875
- const w2 = Math.min(1, Math.max(0, edge.weight ?? 1));
36876
- spring(source, target, linkDistance * (0.35 + (1 - w2) * 0.3), 0.14);
36877
- }
36844
+ for (const edge of propEdges) drawnPairs.add(edgeKeyOf(edge.source, edge.target));
36845
+ const similarityNeighbors = /* @__PURE__ */ new Map();
36846
+ const bySource = /* @__PURE__ */ new Map();
36878
36847
  for (const pair of propSimilarity) {
36848
+ if (pair.weight < SIMILARITY_MIN_WEIGHT) continue;
36879
36849
  if (drawnPairs.has(edgeKeyOf(pair.source, pair.target))) continue;
36880
- const source = nodeById.get(pair.source);
36881
- const target = nodeById.get(pair.target);
36882
- if (!source || !target) continue;
36883
- const w2 = Math.min(1, Math.max(0, pair.weight));
36884
- spring(source, target, linkDistance * (1.35 - 0.55 * w2), 0.015);
36850
+ const arr = bySource.get(pair.source) ?? [];
36851
+ arr.push(pair);
36852
+ bySource.set(pair.source, arr);
36885
36853
  }
36886
- const centroids = /* @__PURE__ */ new Map();
36887
- for (const node of nodes) {
36888
- const g = node.group ?? "__none__";
36889
- let c = centroids.get(g);
36890
- if (!c) {
36891
- c = { x: 0, y: 0, n: 0 };
36892
- centroids.set(g, c);
36893
- }
36894
- c.x += node.x;
36895
- c.y += node.y;
36896
- c.n += 1;
36854
+ for (const [id, arr] of bySource) {
36855
+ arr.sort((a, b) => b.weight - a.weight);
36856
+ similarityNeighbors.set(id, new Set(arr.slice(0, SIMILARITY_NEIGHBORS).map((p) => p.target)));
36897
36857
  }
36898
- const multiCluster = centroids.size > 1;
36899
- for (const node of nodes) {
36900
- if (multiCluster) {
36901
- const c = centroids.get(node.group ?? "__none__");
36902
- if (c && c.n > 1) {
36903
- node.fx += (c.x / c.n - node.x) * 0.04 * temp;
36904
- node.fy += (c.y / c.n - node.y) * 0.04 * temp;
36905
- } else {
36906
- node.fx += (centerX - node.x) * 0.01 * temp;
36907
- node.fy += (centerY - node.y) * 0.01 * temp;
36908
- }
36909
- } else {
36910
- node.fx += (centerX - node.x) * 8e-3 * temp;
36911
- node.fy += (centerY - node.y) * 8e-3 * temp;
36912
- }
36913
- }
36914
- const damping = 0.9;
36915
- for (const node of nodes) {
36916
- node.vx = (node.vx + node.fx) * damping;
36917
- node.vy = (node.vy + node.fy) * damping;
36918
- node.x += node.vx;
36919
- node.y += node.vy;
36920
- node.x = Math.max(30, Math.min(w - 30, node.x));
36921
- node.y = Math.max(30, Math.min(h - 30, node.y));
36858
+ const activeSimilarity = propSimilarity.filter((pair) => {
36859
+ if (drawnPairs.has(edgeKeyOf(pair.source, pair.target))) return false;
36860
+ const a = similarityNeighbors.get(pair.source);
36861
+ const b = similarityNeighbors.get(pair.target);
36862
+ return (a?.has(pair.target) ?? false) || (b?.has(pair.source) ?? false);
36863
+ });
36864
+ const groupTarget = /* @__PURE__ */ new Map();
36865
+ const multiCluster = groups.length > 1;
36866
+ if (multiCluster) {
36867
+ const ring = Math.min(w, h) * 0.34;
36868
+ groups.forEach((g, i) => {
36869
+ const a = i / groups.length * 2 * Math.PI;
36870
+ groupTarget.set(g, { x: w / 2 + ring * Math.cos(a), y: h / 2 + ring * Math.sin(a) });
36871
+ });
36922
36872
  }
36873
+ const present = new Set(simNodes.map((n) => n.id));
36874
+ 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) }));
36875
+ 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) }));
36876
+ const collideRadius = (n) => Math.max(n.size || 8, (n.labelW ?? 0) / 2) + nodeSpacing / 2;
36877
+ 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();
36878
+ for (let i = 0; i < 300; i++) simulation.tick();
36923
36879
  const pad = nodeSpacing / 2;
36924
36880
  const rectsOf = (n) => {
36925
36881
  const r = n.size || 8;
36926
- const lw = showLabels ? n.labelW ?? (n.labelW = n.label ? measureLabelWidth(n.label, labelFont) : 0) : 0;
36882
+ const lw = showLabels ? n.labelW ?? 0 : 0;
36927
36883
  return {
36928
36884
  circle: [n.x - r - pad, n.y - r - pad, n.x + r + pad, n.y + r + pad],
36929
36885
  label: [n.x - lw / 2 - pad, n.y + r + LABEL_GAP - 8, n.x + lw / 2 + pad, n.y + r + LABEL_GAP + LABEL_H]
@@ -36935,11 +36891,12 @@ var init_GraphCanvas = __esm({
36935
36891
  if (ox <= 0 || oy <= 0) return null;
36936
36892
  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 };
36937
36893
  };
36938
- for (let pass = 0; pass < COLLIDE_PASSES; pass++) {
36939
- for (let i = 0; i < nodes.length; i++) {
36940
- for (let j = i + 1; j < nodes.length; j++) {
36941
- const a = nodes[i];
36942
- const b = nodes[j];
36894
+ for (let pass = 0; pass < 24; pass++) {
36895
+ let moved = false;
36896
+ for (let i = 0; i < simNodes.length; i++) {
36897
+ for (let j = i + 1; j < simNodes.length; j++) {
36898
+ const a = simNodes[i];
36899
+ const b = simNodes[j];
36943
36900
  const ra = rectsOf(a);
36944
36901
  const rb = rectsOf(b);
36945
36902
  let best = null;
@@ -36948,25 +36905,35 @@ var init_GraphCanvas = __esm({
36948
36905
  if (s && (!best || s.depth < best.depth)) best = s;
36949
36906
  }
36950
36907
  if (best) {
36908
+ moved = true;
36951
36909
  const push = best.depth / 2;
36952
36910
  if (best.axis === "x") {
36953
36911
  a.x = Math.max(30, Math.min(w - 30, a.x - push * best.sign));
36954
36912
  b.x = Math.max(30, Math.min(w - 30, b.x + push * best.sign));
36955
- a.vx = 0;
36956
- b.vx = 0;
36957
36913
  } else {
36958
36914
  a.y = Math.max(30, Math.min(h - 30, a.y - push * best.sign));
36959
36915
  b.y = Math.max(30, Math.min(h - 30, b.y + push * best.sign));
36960
- a.vy = 0;
36961
- b.vy = 0;
36962
36916
  }
36963
36917
  }
36964
36918
  }
36965
36919
  }
36920
+ if (!moved) break;
36966
36921
  }
36967
- };
36968
- if (fullRelayout) {
36969
- for (let i = 0; i < maxIterations; i++) tick(i);
36922
+ const fitPad = 40;
36923
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
36924
+ for (const node of nodesRef.current) {
36925
+ const r = node.size || 8;
36926
+ const lw = showLabels ? node.labelW ?? 0 : 0;
36927
+ minX = Math.min(minX, node.x - r, node.x - lw / 2);
36928
+ minY = Math.min(minY, node.y - r);
36929
+ maxX = Math.max(maxX, node.x + r, node.x + lw / 2);
36930
+ maxY = Math.max(maxY, node.y + r + LABEL_GAP + LABEL_H);
36931
+ }
36932
+ const bboxW = Math.max(1, maxX - minX + fitPad * 2);
36933
+ const bboxH = Math.max(1, maxY - minY + fitPad * 2);
36934
+ const nextZoom = Math.min(1, viewW / bboxW, viewH / bboxH);
36935
+ setZoom(nextZoom);
36936
+ setOffset({ x: fitPad - minX * nextZoom, y: fitPad - minY * nextZoom });
36970
36937
  laidOutRef.current = true;
36971
36938
  } else {
36972
36939
  const centroids = /* @__PURE__ */ new Map();
@@ -37077,6 +37044,7 @@ var init_GraphCanvas = __esm({
37077
37044
  }
37078
37045
  ctx.stroke();
37079
37046
  if (showLabels && node.label) {
37047
+ const displayLabel = truncateLabel(node.label);
37080
37048
  ctx.font = `${isSelected || isHovered ? "600" : "500"} 12px ${fontFamily}`;
37081
37049
  ctx.textAlign = "center";
37082
37050
  ctx.textBaseline = "middle";
@@ -37084,9 +37052,9 @@ var init_GraphCanvas = __esm({
37084
37052
  ctx.lineWidth = 3;
37085
37053
  ctx.lineJoin = "round";
37086
37054
  ctx.strokeStyle = bgColor;
37087
- ctx.strokeText(node.label, node.x, ly);
37055
+ ctx.strokeText(displayLabel, node.x, ly);
37088
37056
  ctx.fillStyle = isSelected || isHovered ? fgColor : mutedColor;
37089
- ctx.fillText(node.label, node.x, ly);
37057
+ ctx.fillText(displayLabel, node.x, ly);
37090
37058
  }
37091
37059
  if (node.badge && node.badge > 1) {
37092
37060
  const bx = node.x + radius * 0.7;
@@ -37234,6 +37202,20 @@ var init_GraphCanvas = __esm({
37234
37202
  },
37235
37203
  [toCoords, nodeAt, onNodeDoubleClick]
37236
37204
  );
37205
+ const hoveredObj = hoveredNode ? nodesRef.current.find((n) => n.id === hoveredNode) : void 0;
37206
+ const canvasEl = canvasRef.current;
37207
+ const showLabelTooltip = Boolean(
37208
+ hoveredObj?.label && hoveredObj.label.length > MAX_LABEL_CHARS && hoveredObj.x != null && canvasEl
37209
+ );
37210
+ const labelTooltipStyle = (() => {
37211
+ if (!showLabelTooltip || !canvasEl || !hoveredObj || hoveredObj.x == null || hoveredObj.y == null) return void 0;
37212
+ const rect = canvasEl.getBoundingClientRect();
37213
+ return {
37214
+ left: rect.left + offset.x + hoveredObj.x * zoom,
37215
+ top: rect.top + offset.y + hoveredObj.y * zoom - 10,
37216
+ transform: "translate(-50%, -100%)"
37217
+ };
37218
+ })();
37237
37219
  if (isLoading) {
37238
37220
  return /* @__PURE__ */ jsxRuntime.jsx(exports.LoadingState, { message: t("common.loading"), className });
37239
37221
  }
@@ -37287,23 +37269,43 @@ var init_GraphCanvas = __esm({
37287
37269
  ]
37288
37270
  }
37289
37271
  ),
37290
- /* @__PURE__ */ jsxRuntime.jsx(exports.Box, { className: "w-full bg-background", children: /* @__PURE__ */ jsxRuntime.jsx(
37291
- "canvas",
37292
- {
37293
- ref: canvasRef,
37294
- width: Math.round(logicalW * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
37295
- height: Math.round(height * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
37296
- className: "w-full cursor-grab active:cursor-grabbing touch-none",
37297
- style: { height },
37298
- onPointerDown: gestureHandlers.onPointerDown,
37299
- onPointerMove: gestureHandlers.onPointerMove,
37300
- onPointerUp: gestureHandlers.onPointerUp,
37301
- onPointerCancel: gestureHandlers.onPointerCancel,
37302
- onPointerLeave: handlePointerLeave,
37303
- onWheel: gestureHandlers.onWheel,
37304
- onDoubleClick: handleDoubleClick
37305
- }
37306
- ) }),
37272
+ /* @__PURE__ */ jsxRuntime.jsxs(exports.Box, { className: "w-full bg-background", children: [
37273
+ /* @__PURE__ */ jsxRuntime.jsx(
37274
+ "canvas",
37275
+ {
37276
+ ref: canvasRef,
37277
+ width: Math.round(logicalW * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
37278
+ height: Math.round(height * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
37279
+ className: "w-full cursor-grab active:cursor-grabbing touch-none",
37280
+ style: { height },
37281
+ onPointerDown: gestureHandlers.onPointerDown,
37282
+ onPointerMove: gestureHandlers.onPointerMove,
37283
+ onPointerUp: gestureHandlers.onPointerUp,
37284
+ onPointerCancel: gestureHandlers.onPointerCancel,
37285
+ onPointerLeave: handlePointerLeave,
37286
+ onWheel: gestureHandlers.onWheel,
37287
+ onDoubleClick: handleDoubleClick
37288
+ }
37289
+ ),
37290
+ typeof window !== "undefined" && showLabelTooltip && labelTooltipStyle && reactDom.createPortal(
37291
+ /* @__PURE__ */ jsxRuntime.jsx(
37292
+ "div",
37293
+ {
37294
+ className: cn(
37295
+ "fixed z-50 px-3 py-2 max-w-xs",
37296
+ "bg-primary text-primary-foreground",
37297
+ "shadow-elevation-popover rounded-sm",
37298
+ "text-sm pointer-events-none",
37299
+ "break-words whitespace-normal"
37300
+ ),
37301
+ style: labelTooltipStyle,
37302
+ role: "tooltip",
37303
+ children: hoveredObj?.label
37304
+ }
37305
+ ),
37306
+ document.body
37307
+ )
37308
+ ] }),
37307
37309
  groups.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(exports.HStack, { gap: "md", className: "px-4 py-2 border-t border-border", wrap: true, children: groups.map((group, idx) => /* @__PURE__ */ jsxRuntime.jsxs(exports.HStack, { gap: "xs", align: "center", children: [
37308
37310
  /* @__PURE__ */ jsxRuntime.jsx(
37309
37311
  exports.Box,