@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 runtime = require('@almadar/runtime');
47
48
  var registry = require('@almadar/std/registry');
@@ -34770,7 +34771,10 @@ function resolveColor3(color, el) {
34770
34771
  const resolved = getComputedStyle(el).getPropertyValue(m[1]).trim();
34771
34772
  return resolved || (m[2]?.trim() ?? "#888888");
34772
34773
  }
34773
- var GROUP_COLORS2, labelMeasureCtx, GraphCanvas;
34774
+ function truncateLabel(label) {
34775
+ return label.length > MAX_LABEL_CHARS ? label.slice(0, MAX_LABEL_CHARS - 1) + "\u2026" : label;
34776
+ }
34777
+ var GROUP_COLORS2, labelMeasureCtx, MAX_LABEL_CHARS, GraphCanvas;
34774
34778
  var init_GraphCanvas = __esm({
34775
34779
  "components/core/molecules/GraphCanvas.tsx"() {
34776
34780
  "use client";
@@ -34791,6 +34795,7 @@ var init_GraphCanvas = __esm({
34791
34795
  "var(--color-accent)"
34792
34796
  ];
34793
34797
  labelMeasureCtx = null;
34798
+ MAX_LABEL_CHARS = 22;
34794
34799
  GraphCanvas = ({
34795
34800
  title,
34796
34801
  nodes: propNodes = [],
@@ -34895,9 +34900,13 @@ var init_GraphCanvas = __esm({
34895
34900
  React81.useEffect(() => {
34896
34901
  const canvas = canvasRef.current;
34897
34902
  if (!canvas || propNodes.length === 0) return;
34898
- const w = logicalW;
34899
- const h = height;
34903
+ const viewW = logicalW;
34904
+ const viewH = height;
34905
+ const layoutScale = Math.max(1, Math.min(3, Math.sqrt(propNodes.length / 12)));
34906
+ const w = viewW * layoutScale;
34907
+ const h = viewH * layoutScale;
34900
34908
  const labelFont = resolveColor3("var(--font-family)", canvas) || "system-ui";
34909
+ const clamp01 = (v) => Math.min(1, Math.max(0, v));
34901
34910
  const prevPos = /* @__PURE__ */ new Map();
34902
34911
  for (const pn of nodesRef.current) if (pn.x != null && pn.y != null) prevPos.set(pn.id, { x: pn.x, y: pn.y });
34903
34912
  const simNodes = propNodes.map((n, idx) => {
@@ -34922,7 +34931,7 @@ var init_GraphCanvas = __esm({
34922
34931
  y = h * 0.2 + rand() * h * 0.6;
34923
34932
  }
34924
34933
  }
34925
- return { ...n, x, y, vx: 0, vy: 0, fx: 0, fy: 0 };
34934
+ return { ...n, x, y, vx: 0, vy: 0 };
34926
34935
  });
34927
34936
  nodesRef.current = simNodes;
34928
34937
  const prevIds = /* @__PURE__ */ new Set([...prevPos.keys()]);
@@ -34931,105 +34940,52 @@ var init_GraphCanvas = __esm({
34931
34940
  const overlap = prevIds.size === 0 ? 0 : kept / Math.max(prevIds.size, newIdList.length);
34932
34941
  const fullRelayout = !laidOutRef.current || overlap < 0.5;
34933
34942
  if (layout === "force") {
34934
- const maxIterations = 300;
34935
- const COOL = 0.12;
34936
- const COLLIDE_PASSES = 6;
34937
- const LABEL_GAP = 12;
34938
- const LABEL_H = 16;
34939
- const tick = (iter) => {
34940
- const nodes = nodesRef.current;
34941
- const centerX = w / 2;
34942
- const centerY = h / 2;
34943
- const temp = Math.max(COOL, 1 - iter / maxIterations);
34944
- for (const node of nodes) {
34945
- node.fx = 0;
34946
- node.fy = 0;
34947
- }
34948
- for (let i = 0; i < nodes.length; i++) {
34949
- for (let j = i + 1; j < nodes.length; j++) {
34950
- const dx = nodes[j].x - nodes[i].x;
34951
- const dy = nodes[j].y - nodes[i].y;
34952
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
34953
- const force = repulsion / (dist * dist) * temp;
34954
- const fx = dx / dist * force;
34955
- const fy = dy / dist * force;
34956
- nodes[i].fx -= fx;
34957
- nodes[i].fy -= fy;
34958
- nodes[j].fx += fx;
34959
- nodes[j].fy += fy;
34960
- }
34961
- }
34962
- const nodeById = new Map(nodes.map((n) => [n.id, n]));
34963
- const spring = (a, b, rest, k) => {
34964
- const dx = b.x - a.x;
34965
- const dy = b.y - a.y;
34966
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
34967
- const force = (dist - rest) * k * temp;
34968
- const fx = dx / dist * force;
34969
- const fy = dy / dist * force;
34970
- a.fx += fx;
34971
- a.fy += fy;
34972
- b.fx -= fx;
34973
- b.fy -= fy;
34974
- };
34943
+ if (fullRelayout) {
34944
+ const LABEL_GAP = 12;
34945
+ const LABEL_H = 16;
34946
+ for (const n of simNodes) n.labelW = showLabels && n.label ? measureLabelWidth(truncateLabel(n.label), labelFont) : 0;
34947
+ const SIMILARITY_NEIGHBORS = 5;
34948
+ const SIMILARITY_MIN_WEIGHT = 0.25;
34975
34949
  const drawnPairs = /* @__PURE__ */ new Set();
34976
- for (const edge of propEdges) {
34977
- const source = nodeById.get(edge.source);
34978
- const target = nodeById.get(edge.target);
34979
- if (!source || !target) continue;
34980
- drawnPairs.add(edgeKeyOf(edge.source, edge.target));
34981
- const w2 = Math.min(1, Math.max(0, edge.weight ?? 1));
34982
- spring(source, target, linkDistance * (0.35 + (1 - w2) * 0.3), 0.14);
34983
- }
34950
+ for (const edge of propEdges) drawnPairs.add(edgeKeyOf(edge.source, edge.target));
34951
+ const similarityNeighbors = /* @__PURE__ */ new Map();
34952
+ const bySource = /* @__PURE__ */ new Map();
34984
34953
  for (const pair of propSimilarity) {
34954
+ if (pair.weight < SIMILARITY_MIN_WEIGHT) continue;
34985
34955
  if (drawnPairs.has(edgeKeyOf(pair.source, pair.target))) continue;
34986
- const source = nodeById.get(pair.source);
34987
- const target = nodeById.get(pair.target);
34988
- if (!source || !target) continue;
34989
- const w2 = Math.min(1, Math.max(0, pair.weight));
34990
- spring(source, target, linkDistance * (1.35 - 0.55 * w2), 0.015);
34956
+ const arr = bySource.get(pair.source) ?? [];
34957
+ arr.push(pair);
34958
+ bySource.set(pair.source, arr);
34991
34959
  }
34992
- const centroids = /* @__PURE__ */ new Map();
34993
- for (const node of nodes) {
34994
- const g = node.group ?? "__none__";
34995
- let c = centroids.get(g);
34996
- if (!c) {
34997
- c = { x: 0, y: 0, n: 0 };
34998
- centroids.set(g, c);
34999
- }
35000
- c.x += node.x;
35001
- c.y += node.y;
35002
- c.n += 1;
34960
+ for (const [id, arr] of bySource) {
34961
+ arr.sort((a, b) => b.weight - a.weight);
34962
+ similarityNeighbors.set(id, new Set(arr.slice(0, SIMILARITY_NEIGHBORS).map((p) => p.target)));
35003
34963
  }
35004
- const multiCluster = centroids.size > 1;
35005
- for (const node of nodes) {
35006
- if (multiCluster) {
35007
- const c = centroids.get(node.group ?? "__none__");
35008
- if (c && c.n > 1) {
35009
- node.fx += (c.x / c.n - node.x) * 0.04 * temp;
35010
- node.fy += (c.y / c.n - node.y) * 0.04 * temp;
35011
- } else {
35012
- node.fx += (centerX - node.x) * 0.01 * temp;
35013
- node.fy += (centerY - node.y) * 0.01 * temp;
35014
- }
35015
- } else {
35016
- node.fx += (centerX - node.x) * 8e-3 * temp;
35017
- node.fy += (centerY - node.y) * 8e-3 * temp;
35018
- }
35019
- }
35020
- const damping = 0.9;
35021
- for (const node of nodes) {
35022
- node.vx = (node.vx + node.fx) * damping;
35023
- node.vy = (node.vy + node.fy) * damping;
35024
- node.x += node.vx;
35025
- node.y += node.vy;
35026
- node.x = Math.max(30, Math.min(w - 30, node.x));
35027
- node.y = Math.max(30, Math.min(h - 30, node.y));
34964
+ const activeSimilarity = propSimilarity.filter((pair) => {
34965
+ if (drawnPairs.has(edgeKeyOf(pair.source, pair.target))) return false;
34966
+ const a = similarityNeighbors.get(pair.source);
34967
+ const b = similarityNeighbors.get(pair.target);
34968
+ return (a?.has(pair.target) ?? false) || (b?.has(pair.source) ?? false);
34969
+ });
34970
+ const groupTarget = /* @__PURE__ */ new Map();
34971
+ const multiCluster = groups.length > 1;
34972
+ if (multiCluster) {
34973
+ const ring = Math.min(w, h) * 0.34;
34974
+ groups.forEach((g, i) => {
34975
+ const a = i / groups.length * 2 * Math.PI;
34976
+ groupTarget.set(g, { x: w / 2 + ring * Math.cos(a), y: h / 2 + ring * Math.sin(a) });
34977
+ });
35028
34978
  }
34979
+ const present = new Set(simNodes.map((n) => n.id));
34980
+ 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) }));
34981
+ 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) }));
34982
+ const collideRadius = (n) => Math.max(n.size || 8, (n.labelW ?? 0) / 2) + nodeSpacing / 2;
34983
+ 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();
34984
+ for (let i = 0; i < 300; i++) simulation.tick();
35029
34985
  const pad = nodeSpacing / 2;
35030
34986
  const rectsOf = (n) => {
35031
34987
  const r = n.size || 8;
35032
- const lw = showLabels ? n.labelW ?? (n.labelW = n.label ? measureLabelWidth(n.label, labelFont) : 0) : 0;
34988
+ const lw = showLabels ? n.labelW ?? 0 : 0;
35033
34989
  return {
35034
34990
  circle: [n.x - r - pad, n.y - r - pad, n.x + r + pad, n.y + r + pad],
35035
34991
  label: [n.x - lw / 2 - pad, n.y + r + LABEL_GAP - 8, n.x + lw / 2 + pad, n.y + r + LABEL_GAP + LABEL_H]
@@ -35041,11 +34997,12 @@ var init_GraphCanvas = __esm({
35041
34997
  if (ox <= 0 || oy <= 0) return null;
35042
34998
  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 };
35043
34999
  };
35044
- for (let pass = 0; pass < COLLIDE_PASSES; pass++) {
35045
- for (let i = 0; i < nodes.length; i++) {
35046
- for (let j = i + 1; j < nodes.length; j++) {
35047
- const a = nodes[i];
35048
- const b = nodes[j];
35000
+ for (let pass = 0; pass < 24; pass++) {
35001
+ let moved = false;
35002
+ for (let i = 0; i < simNodes.length; i++) {
35003
+ for (let j = i + 1; j < simNodes.length; j++) {
35004
+ const a = simNodes[i];
35005
+ const b = simNodes[j];
35049
35006
  const ra = rectsOf(a);
35050
35007
  const rb = rectsOf(b);
35051
35008
  let best = null;
@@ -35054,25 +35011,35 @@ var init_GraphCanvas = __esm({
35054
35011
  if (s && (!best || s.depth < best.depth)) best = s;
35055
35012
  }
35056
35013
  if (best) {
35014
+ moved = true;
35057
35015
  const push = best.depth / 2;
35058
35016
  if (best.axis === "x") {
35059
35017
  a.x = Math.max(30, Math.min(w - 30, a.x - push * best.sign));
35060
35018
  b.x = Math.max(30, Math.min(w - 30, b.x + push * best.sign));
35061
- a.vx = 0;
35062
- b.vx = 0;
35063
35019
  } else {
35064
35020
  a.y = Math.max(30, Math.min(h - 30, a.y - push * best.sign));
35065
35021
  b.y = Math.max(30, Math.min(h - 30, b.y + push * best.sign));
35066
- a.vy = 0;
35067
- b.vy = 0;
35068
35022
  }
35069
35023
  }
35070
35024
  }
35071
35025
  }
35026
+ if (!moved) break;
35072
35027
  }
35073
- };
35074
- if (fullRelayout) {
35075
- for (let i = 0; i < maxIterations; i++) tick(i);
35028
+ const fitPad = 40;
35029
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
35030
+ for (const node of nodesRef.current) {
35031
+ const r = node.size || 8;
35032
+ const lw = showLabels ? node.labelW ?? 0 : 0;
35033
+ minX = Math.min(minX, node.x - r, node.x - lw / 2);
35034
+ minY = Math.min(minY, node.y - r);
35035
+ maxX = Math.max(maxX, node.x + r, node.x + lw / 2);
35036
+ maxY = Math.max(maxY, node.y + r + LABEL_GAP + LABEL_H);
35037
+ }
35038
+ const bboxW = Math.max(1, maxX - minX + fitPad * 2);
35039
+ const bboxH = Math.max(1, maxY - minY + fitPad * 2);
35040
+ const nextZoom = Math.min(1, viewW / bboxW, viewH / bboxH);
35041
+ setZoom(nextZoom);
35042
+ setOffset({ x: fitPad - minX * nextZoom, y: fitPad - minY * nextZoom });
35076
35043
  laidOutRef.current = true;
35077
35044
  } else {
35078
35045
  const centroids = /* @__PURE__ */ new Map();
@@ -35183,6 +35150,7 @@ var init_GraphCanvas = __esm({
35183
35150
  }
35184
35151
  ctx.stroke();
35185
35152
  if (showLabels && node.label) {
35153
+ const displayLabel = truncateLabel(node.label);
35186
35154
  ctx.font = `${isSelected || isHovered ? "600" : "500"} 12px ${fontFamily}`;
35187
35155
  ctx.textAlign = "center";
35188
35156
  ctx.textBaseline = "middle";
@@ -35190,9 +35158,9 @@ var init_GraphCanvas = __esm({
35190
35158
  ctx.lineWidth = 3;
35191
35159
  ctx.lineJoin = "round";
35192
35160
  ctx.strokeStyle = bgColor;
35193
- ctx.strokeText(node.label, node.x, ly);
35161
+ ctx.strokeText(displayLabel, node.x, ly);
35194
35162
  ctx.fillStyle = isSelected || isHovered ? fgColor : mutedColor;
35195
- ctx.fillText(node.label, node.x, ly);
35163
+ ctx.fillText(displayLabel, node.x, ly);
35196
35164
  }
35197
35165
  if (node.badge && node.badge > 1) {
35198
35166
  const bx = node.x + radius * 0.7;
@@ -35340,6 +35308,20 @@ var init_GraphCanvas = __esm({
35340
35308
  },
35341
35309
  [toCoords, nodeAt, onNodeDoubleClick]
35342
35310
  );
35311
+ const hoveredObj = hoveredNode ? nodesRef.current.find((n) => n.id === hoveredNode) : void 0;
35312
+ const canvasEl = canvasRef.current;
35313
+ const showLabelTooltip = Boolean(
35314
+ hoveredObj?.label && hoveredObj.label.length > MAX_LABEL_CHARS && hoveredObj.x != null && canvasEl
35315
+ );
35316
+ const labelTooltipStyle = (() => {
35317
+ if (!showLabelTooltip || !canvasEl || !hoveredObj || hoveredObj.x == null || hoveredObj.y == null) return void 0;
35318
+ const rect = canvasEl.getBoundingClientRect();
35319
+ return {
35320
+ left: rect.left + offset.x + hoveredObj.x * zoom,
35321
+ top: rect.top + offset.y + hoveredObj.y * zoom - 10,
35322
+ transform: "translate(-50%, -100%)"
35323
+ };
35324
+ })();
35343
35325
  if (isLoading) {
35344
35326
  return /* @__PURE__ */ jsxRuntime.jsx(LoadingState, { message: t("common.loading"), className });
35345
35327
  }
@@ -35393,23 +35375,43 @@ var init_GraphCanvas = __esm({
35393
35375
  ]
35394
35376
  }
35395
35377
  ),
35396
- /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "w-full bg-background", children: /* @__PURE__ */ jsxRuntime.jsx(
35397
- "canvas",
35398
- {
35399
- ref: canvasRef,
35400
- width: Math.round(logicalW * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
35401
- height: Math.round(height * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
35402
- className: "w-full cursor-grab active:cursor-grabbing touch-none",
35403
- style: { height },
35404
- onPointerDown: gestureHandlers.onPointerDown,
35405
- onPointerMove: gestureHandlers.onPointerMove,
35406
- onPointerUp: gestureHandlers.onPointerUp,
35407
- onPointerCancel: gestureHandlers.onPointerCancel,
35408
- onPointerLeave: handlePointerLeave,
35409
- onWheel: gestureHandlers.onWheel,
35410
- onDoubleClick: handleDoubleClick
35411
- }
35412
- ) }),
35378
+ /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "w-full bg-background", children: [
35379
+ /* @__PURE__ */ jsxRuntime.jsx(
35380
+ "canvas",
35381
+ {
35382
+ ref: canvasRef,
35383
+ width: Math.round(logicalW * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
35384
+ height: Math.round(height * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
35385
+ className: "w-full cursor-grab active:cursor-grabbing touch-none",
35386
+ style: { height },
35387
+ onPointerDown: gestureHandlers.onPointerDown,
35388
+ onPointerMove: gestureHandlers.onPointerMove,
35389
+ onPointerUp: gestureHandlers.onPointerUp,
35390
+ onPointerCancel: gestureHandlers.onPointerCancel,
35391
+ onPointerLeave: handlePointerLeave,
35392
+ onWheel: gestureHandlers.onWheel,
35393
+ onDoubleClick: handleDoubleClick
35394
+ }
35395
+ ),
35396
+ typeof window !== "undefined" && showLabelTooltip && labelTooltipStyle && reactDom.createPortal(
35397
+ /* @__PURE__ */ jsxRuntime.jsx(
35398
+ "div",
35399
+ {
35400
+ className: cn(
35401
+ "fixed z-50 px-3 py-2 max-w-xs",
35402
+ "bg-primary text-primary-foreground",
35403
+ "shadow-elevation-popover rounded-sm",
35404
+ "text-sm pointer-events-none",
35405
+ "break-words whitespace-normal"
35406
+ ),
35407
+ style: labelTooltipStyle,
35408
+ role: "tooltip",
35409
+ children: hoveredObj?.label
35410
+ }
35411
+ ),
35412
+ document.body
35413
+ )
35414
+ ] }),
35413
35415
  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: [
35414
35416
  /* @__PURE__ */ jsxRuntime.jsx(
35415
35417
  Box,