@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.
@@ -44,6 +44,7 @@ import { DndContext, pointerWithin, rectIntersection, closestCorners, useSensors
44
44
  import { useSortable, arrayMove, sortableKeyboardCoordinates, SortableContext, rectSortingStrategy, verticalListSortingStrategy } from '@dnd-kit/sortable';
45
45
  import { CSS } from '@dnd-kit/utilities';
46
46
  import { useNodeId, ReactFlowProvider, Handle, Position } from '@xyflow/react';
47
+ import { forceSimulation, forceLink, forceManyBody, forceCollide, forceX, forceY } from 'd3-force';
47
48
  import { isDrawHostPattern, getPatternDefinition, getComponentForPattern as getComponentForPattern$1 } from '@almadar/core/patterns';
48
49
  import { StateMachineManager, collectDeclaredConfigDefaults, resolveCallSitePayloadCaptures, createServerEffectHandlers, EffectExecutor, createContextFromBindings, createTickScheduler, isValidCronExpression, parseDurationString, InMemoryPersistence, normalizeCallSiteConfigToValues } from '@almadar/runtime';
49
50
  import { isKnownStdOperator } from '@almadar/std/registry';
@@ -34726,7 +34727,10 @@ function resolveColor3(color, el) {
34726
34727
  const resolved = getComputedStyle(el).getPropertyValue(m[1]).trim();
34727
34728
  return resolved || (m[2]?.trim() ?? "#888888");
34728
34729
  }
34729
- var GROUP_COLORS2, labelMeasureCtx, GraphCanvas;
34730
+ function truncateLabel(label) {
34731
+ return label.length > MAX_LABEL_CHARS ? label.slice(0, MAX_LABEL_CHARS - 1) + "\u2026" : label;
34732
+ }
34733
+ var GROUP_COLORS2, labelMeasureCtx, MAX_LABEL_CHARS, GraphCanvas;
34730
34734
  var init_GraphCanvas = __esm({
34731
34735
  "components/core/molecules/GraphCanvas.tsx"() {
34732
34736
  "use client";
@@ -34747,6 +34751,7 @@ var init_GraphCanvas = __esm({
34747
34751
  "var(--color-accent)"
34748
34752
  ];
34749
34753
  labelMeasureCtx = null;
34754
+ MAX_LABEL_CHARS = 22;
34750
34755
  GraphCanvas = ({
34751
34756
  title,
34752
34757
  nodes: propNodes = [],
@@ -34851,9 +34856,13 @@ var init_GraphCanvas = __esm({
34851
34856
  useEffect(() => {
34852
34857
  const canvas = canvasRef.current;
34853
34858
  if (!canvas || propNodes.length === 0) return;
34854
- const w = logicalW;
34855
- const h = height;
34859
+ const viewW = logicalW;
34860
+ const viewH = height;
34861
+ const layoutScale = Math.max(1, Math.min(3, Math.sqrt(propNodes.length / 12)));
34862
+ const w = viewW * layoutScale;
34863
+ const h = viewH * layoutScale;
34856
34864
  const labelFont = resolveColor3("var(--font-family)", canvas) || "system-ui";
34865
+ const clamp01 = (v) => Math.min(1, Math.max(0, v));
34857
34866
  const prevPos = /* @__PURE__ */ new Map();
34858
34867
  for (const pn of nodesRef.current) if (pn.x != null && pn.y != null) prevPos.set(pn.id, { x: pn.x, y: pn.y });
34859
34868
  const simNodes = propNodes.map((n, idx) => {
@@ -34878,7 +34887,7 @@ var init_GraphCanvas = __esm({
34878
34887
  y = h * 0.2 + rand() * h * 0.6;
34879
34888
  }
34880
34889
  }
34881
- return { ...n, x, y, vx: 0, vy: 0, fx: 0, fy: 0 };
34890
+ return { ...n, x, y, vx: 0, vy: 0 };
34882
34891
  });
34883
34892
  nodesRef.current = simNodes;
34884
34893
  const prevIds = /* @__PURE__ */ new Set([...prevPos.keys()]);
@@ -34887,105 +34896,52 @@ var init_GraphCanvas = __esm({
34887
34896
  const overlap = prevIds.size === 0 ? 0 : kept / Math.max(prevIds.size, newIdList.length);
34888
34897
  const fullRelayout = !laidOutRef.current || overlap < 0.5;
34889
34898
  if (layout === "force") {
34890
- const maxIterations = 300;
34891
- const COOL = 0.12;
34892
- const COLLIDE_PASSES = 6;
34893
- const LABEL_GAP = 12;
34894
- const LABEL_H = 16;
34895
- const tick = (iter) => {
34896
- const nodes = nodesRef.current;
34897
- const centerX = w / 2;
34898
- const centerY = h / 2;
34899
- const temp = Math.max(COOL, 1 - iter / maxIterations);
34900
- for (const node of nodes) {
34901
- node.fx = 0;
34902
- node.fy = 0;
34903
- }
34904
- for (let i = 0; i < nodes.length; i++) {
34905
- for (let j = i + 1; j < nodes.length; j++) {
34906
- const dx = nodes[j].x - nodes[i].x;
34907
- const dy = nodes[j].y - nodes[i].y;
34908
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
34909
- const force = repulsion / (dist * dist) * temp;
34910
- const fx = dx / dist * force;
34911
- const fy = dy / dist * force;
34912
- nodes[i].fx -= fx;
34913
- nodes[i].fy -= fy;
34914
- nodes[j].fx += fx;
34915
- nodes[j].fy += fy;
34916
- }
34917
- }
34918
- const nodeById = new Map(nodes.map((n) => [n.id, n]));
34919
- const spring = (a, b, rest, k) => {
34920
- const dx = b.x - a.x;
34921
- const dy = b.y - a.y;
34922
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
34923
- const force = (dist - rest) * k * temp;
34924
- const fx = dx / dist * force;
34925
- const fy = dy / dist * force;
34926
- a.fx += fx;
34927
- a.fy += fy;
34928
- b.fx -= fx;
34929
- b.fy -= fy;
34930
- };
34899
+ if (fullRelayout) {
34900
+ const LABEL_GAP = 12;
34901
+ const LABEL_H = 16;
34902
+ for (const n of simNodes) n.labelW = showLabels && n.label ? measureLabelWidth(truncateLabel(n.label), labelFont) : 0;
34903
+ const SIMILARITY_NEIGHBORS = 5;
34904
+ const SIMILARITY_MIN_WEIGHT = 0.25;
34931
34905
  const drawnPairs = /* @__PURE__ */ new Set();
34932
- for (const edge of propEdges) {
34933
- const source = nodeById.get(edge.source);
34934
- const target = nodeById.get(edge.target);
34935
- if (!source || !target) continue;
34936
- drawnPairs.add(edgeKeyOf(edge.source, edge.target));
34937
- const w2 = Math.min(1, Math.max(0, edge.weight ?? 1));
34938
- spring(source, target, linkDistance * (0.35 + (1 - w2) * 0.3), 0.14);
34939
- }
34906
+ for (const edge of propEdges) drawnPairs.add(edgeKeyOf(edge.source, edge.target));
34907
+ const similarityNeighbors = /* @__PURE__ */ new Map();
34908
+ const bySource = /* @__PURE__ */ new Map();
34940
34909
  for (const pair of propSimilarity) {
34910
+ if (pair.weight < SIMILARITY_MIN_WEIGHT) continue;
34941
34911
  if (drawnPairs.has(edgeKeyOf(pair.source, pair.target))) continue;
34942
- const source = nodeById.get(pair.source);
34943
- const target = nodeById.get(pair.target);
34944
- if (!source || !target) continue;
34945
- const w2 = Math.min(1, Math.max(0, pair.weight));
34946
- spring(source, target, linkDistance * (1.35 - 0.55 * w2), 0.015);
34912
+ const arr = bySource.get(pair.source) ?? [];
34913
+ arr.push(pair);
34914
+ bySource.set(pair.source, arr);
34947
34915
  }
34948
- const centroids = /* @__PURE__ */ new Map();
34949
- for (const node of nodes) {
34950
- const g = node.group ?? "__none__";
34951
- let c = centroids.get(g);
34952
- if (!c) {
34953
- c = { x: 0, y: 0, n: 0 };
34954
- centroids.set(g, c);
34955
- }
34956
- c.x += node.x;
34957
- c.y += node.y;
34958
- c.n += 1;
34916
+ for (const [id, arr] of bySource) {
34917
+ arr.sort((a, b) => b.weight - a.weight);
34918
+ similarityNeighbors.set(id, new Set(arr.slice(0, SIMILARITY_NEIGHBORS).map((p) => p.target)));
34959
34919
  }
34960
- const multiCluster = centroids.size > 1;
34961
- for (const node of nodes) {
34962
- if (multiCluster) {
34963
- const c = centroids.get(node.group ?? "__none__");
34964
- if (c && c.n > 1) {
34965
- node.fx += (c.x / c.n - node.x) * 0.04 * temp;
34966
- node.fy += (c.y / c.n - node.y) * 0.04 * temp;
34967
- } else {
34968
- node.fx += (centerX - node.x) * 0.01 * temp;
34969
- node.fy += (centerY - node.y) * 0.01 * temp;
34970
- }
34971
- } else {
34972
- node.fx += (centerX - node.x) * 8e-3 * temp;
34973
- node.fy += (centerY - node.y) * 8e-3 * temp;
34974
- }
34975
- }
34976
- const damping = 0.9;
34977
- for (const node of nodes) {
34978
- node.vx = (node.vx + node.fx) * damping;
34979
- node.vy = (node.vy + node.fy) * damping;
34980
- node.x += node.vx;
34981
- node.y += node.vy;
34982
- node.x = Math.max(30, Math.min(w - 30, node.x));
34983
- node.y = Math.max(30, Math.min(h - 30, node.y));
34920
+ const activeSimilarity = propSimilarity.filter((pair) => {
34921
+ if (drawnPairs.has(edgeKeyOf(pair.source, pair.target))) return false;
34922
+ const a = similarityNeighbors.get(pair.source);
34923
+ const b = similarityNeighbors.get(pair.target);
34924
+ return (a?.has(pair.target) ?? false) || (b?.has(pair.source) ?? false);
34925
+ });
34926
+ const groupTarget = /* @__PURE__ */ new Map();
34927
+ const multiCluster = groups.length > 1;
34928
+ if (multiCluster) {
34929
+ const ring = Math.min(w, h) * 0.34;
34930
+ groups.forEach((g, i) => {
34931
+ const a = i / groups.length * 2 * Math.PI;
34932
+ groupTarget.set(g, { x: w / 2 + ring * Math.cos(a), y: h / 2 + ring * Math.sin(a) });
34933
+ });
34984
34934
  }
34935
+ const present = new Set(simNodes.map((n) => n.id));
34936
+ 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) }));
34937
+ 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) }));
34938
+ const collideRadius = (n) => Math.max(n.size || 8, (n.labelW ?? 0) / 2) + nodeSpacing / 2;
34939
+ 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();
34940
+ for (let i = 0; i < 300; i++) simulation.tick();
34985
34941
  const pad = nodeSpacing / 2;
34986
34942
  const rectsOf = (n) => {
34987
34943
  const r = n.size || 8;
34988
- const lw = showLabels ? n.labelW ?? (n.labelW = n.label ? measureLabelWidth(n.label, labelFont) : 0) : 0;
34944
+ const lw = showLabels ? n.labelW ?? 0 : 0;
34989
34945
  return {
34990
34946
  circle: [n.x - r - pad, n.y - r - pad, n.x + r + pad, n.y + r + pad],
34991
34947
  label: [n.x - lw / 2 - pad, n.y + r + LABEL_GAP - 8, n.x + lw / 2 + pad, n.y + r + LABEL_GAP + LABEL_H]
@@ -34997,11 +34953,12 @@ var init_GraphCanvas = __esm({
34997
34953
  if (ox <= 0 || oy <= 0) return null;
34998
34954
  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 };
34999
34955
  };
35000
- for (let pass = 0; pass < COLLIDE_PASSES; pass++) {
35001
- for (let i = 0; i < nodes.length; i++) {
35002
- for (let j = i + 1; j < nodes.length; j++) {
35003
- const a = nodes[i];
35004
- const b = nodes[j];
34956
+ for (let pass = 0; pass < 24; pass++) {
34957
+ let moved = false;
34958
+ for (let i = 0; i < simNodes.length; i++) {
34959
+ for (let j = i + 1; j < simNodes.length; j++) {
34960
+ const a = simNodes[i];
34961
+ const b = simNodes[j];
35005
34962
  const ra = rectsOf(a);
35006
34963
  const rb = rectsOf(b);
35007
34964
  let best = null;
@@ -35010,25 +34967,35 @@ var init_GraphCanvas = __esm({
35010
34967
  if (s && (!best || s.depth < best.depth)) best = s;
35011
34968
  }
35012
34969
  if (best) {
34970
+ moved = true;
35013
34971
  const push = best.depth / 2;
35014
34972
  if (best.axis === "x") {
35015
34973
  a.x = Math.max(30, Math.min(w - 30, a.x - push * best.sign));
35016
34974
  b.x = Math.max(30, Math.min(w - 30, b.x + push * best.sign));
35017
- a.vx = 0;
35018
- b.vx = 0;
35019
34975
  } else {
35020
34976
  a.y = Math.max(30, Math.min(h - 30, a.y - push * best.sign));
35021
34977
  b.y = Math.max(30, Math.min(h - 30, b.y + push * best.sign));
35022
- a.vy = 0;
35023
- b.vy = 0;
35024
34978
  }
35025
34979
  }
35026
34980
  }
35027
34981
  }
34982
+ if (!moved) break;
35028
34983
  }
35029
- };
35030
- if (fullRelayout) {
35031
- for (let i = 0; i < maxIterations; i++) tick(i);
34984
+ const fitPad = 40;
34985
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
34986
+ for (const node of nodesRef.current) {
34987
+ const r = node.size || 8;
34988
+ const lw = showLabels ? node.labelW ?? 0 : 0;
34989
+ minX = Math.min(minX, node.x - r, node.x - lw / 2);
34990
+ minY = Math.min(minY, node.y - r);
34991
+ maxX = Math.max(maxX, node.x + r, node.x + lw / 2);
34992
+ maxY = Math.max(maxY, node.y + r + LABEL_GAP + LABEL_H);
34993
+ }
34994
+ const bboxW = Math.max(1, maxX - minX + fitPad * 2);
34995
+ const bboxH = Math.max(1, maxY - minY + fitPad * 2);
34996
+ const nextZoom = Math.min(1, viewW / bboxW, viewH / bboxH);
34997
+ setZoom(nextZoom);
34998
+ setOffset({ x: fitPad - minX * nextZoom, y: fitPad - minY * nextZoom });
35032
34999
  laidOutRef.current = true;
35033
35000
  } else {
35034
35001
  const centroids = /* @__PURE__ */ new Map();
@@ -35139,6 +35106,7 @@ var init_GraphCanvas = __esm({
35139
35106
  }
35140
35107
  ctx.stroke();
35141
35108
  if (showLabels && node.label) {
35109
+ const displayLabel = truncateLabel(node.label);
35142
35110
  ctx.font = `${isSelected || isHovered ? "600" : "500"} 12px ${fontFamily}`;
35143
35111
  ctx.textAlign = "center";
35144
35112
  ctx.textBaseline = "middle";
@@ -35146,9 +35114,9 @@ var init_GraphCanvas = __esm({
35146
35114
  ctx.lineWidth = 3;
35147
35115
  ctx.lineJoin = "round";
35148
35116
  ctx.strokeStyle = bgColor;
35149
- ctx.strokeText(node.label, node.x, ly);
35117
+ ctx.strokeText(displayLabel, node.x, ly);
35150
35118
  ctx.fillStyle = isSelected || isHovered ? fgColor : mutedColor;
35151
- ctx.fillText(node.label, node.x, ly);
35119
+ ctx.fillText(displayLabel, node.x, ly);
35152
35120
  }
35153
35121
  if (node.badge && node.badge > 1) {
35154
35122
  const bx = node.x + radius * 0.7;
@@ -35296,6 +35264,20 @@ var init_GraphCanvas = __esm({
35296
35264
  },
35297
35265
  [toCoords, nodeAt, onNodeDoubleClick]
35298
35266
  );
35267
+ const hoveredObj = hoveredNode ? nodesRef.current.find((n) => n.id === hoveredNode) : void 0;
35268
+ const canvasEl = canvasRef.current;
35269
+ const showLabelTooltip = Boolean(
35270
+ hoveredObj?.label && hoveredObj.label.length > MAX_LABEL_CHARS && hoveredObj.x != null && canvasEl
35271
+ );
35272
+ const labelTooltipStyle = (() => {
35273
+ if (!showLabelTooltip || !canvasEl || !hoveredObj || hoveredObj.x == null || hoveredObj.y == null) return void 0;
35274
+ const rect = canvasEl.getBoundingClientRect();
35275
+ return {
35276
+ left: rect.left + offset.x + hoveredObj.x * zoom,
35277
+ top: rect.top + offset.y + hoveredObj.y * zoom - 10,
35278
+ transform: "translate(-50%, -100%)"
35279
+ };
35280
+ })();
35299
35281
  if (isLoading) {
35300
35282
  return /* @__PURE__ */ jsx(LoadingState, { message: t("common.loading"), className });
35301
35283
  }
@@ -35349,23 +35331,43 @@ var init_GraphCanvas = __esm({
35349
35331
  ]
35350
35332
  }
35351
35333
  ),
35352
- /* @__PURE__ */ jsx(Box, { className: "w-full bg-background", children: /* @__PURE__ */ jsx(
35353
- "canvas",
35354
- {
35355
- ref: canvasRef,
35356
- width: Math.round(logicalW * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
35357
- height: Math.round(height * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
35358
- className: "w-full cursor-grab active:cursor-grabbing touch-none",
35359
- style: { height },
35360
- onPointerDown: gestureHandlers.onPointerDown,
35361
- onPointerMove: gestureHandlers.onPointerMove,
35362
- onPointerUp: gestureHandlers.onPointerUp,
35363
- onPointerCancel: gestureHandlers.onPointerCancel,
35364
- onPointerLeave: handlePointerLeave,
35365
- onWheel: gestureHandlers.onWheel,
35366
- onDoubleClick: handleDoubleClick
35367
- }
35368
- ) }),
35334
+ /* @__PURE__ */ jsxs(Box, { className: "w-full bg-background", children: [
35335
+ /* @__PURE__ */ jsx(
35336
+ "canvas",
35337
+ {
35338
+ ref: canvasRef,
35339
+ width: Math.round(logicalW * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
35340
+ height: Math.round(height * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
35341
+ className: "w-full cursor-grab active:cursor-grabbing touch-none",
35342
+ style: { height },
35343
+ onPointerDown: gestureHandlers.onPointerDown,
35344
+ onPointerMove: gestureHandlers.onPointerMove,
35345
+ onPointerUp: gestureHandlers.onPointerUp,
35346
+ onPointerCancel: gestureHandlers.onPointerCancel,
35347
+ onPointerLeave: handlePointerLeave,
35348
+ onWheel: gestureHandlers.onWheel,
35349
+ onDoubleClick: handleDoubleClick
35350
+ }
35351
+ ),
35352
+ typeof window !== "undefined" && showLabelTooltip && labelTooltipStyle && createPortal(
35353
+ /* @__PURE__ */ jsx(
35354
+ "div",
35355
+ {
35356
+ className: cn(
35357
+ "fixed z-50 px-3 py-2 max-w-xs",
35358
+ "bg-primary text-primary-foreground",
35359
+ "shadow-elevation-popover rounded-sm",
35360
+ "text-sm pointer-events-none",
35361
+ "break-words whitespace-normal"
35362
+ ),
35363
+ style: labelTooltipStyle,
35364
+ role: "tooltip",
35365
+ children: hoveredObj?.label
35366
+ }
35367
+ ),
35368
+ document.body
35369
+ )
35370
+ ] }),
35369
35371
  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: [
35370
35372
  /* @__PURE__ */ jsx(
35371
35373
  Box,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/ui",
3
- "version": "5.119.0",
3
+ "version": "5.121.0",
4
4
  "description": "React UI components, hooks, and providers for Almadar",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -130,6 +130,7 @@
130
130
  "@tabler/icons-react": "^3.44.0",
131
131
  "@xyflow/react": "12.10.1",
132
132
  "clsx": "^2.1.0",
133
+ "d3-force": "^3.0.0",
133
134
  "elkjs": "0.11.1",
134
135
  "hastscript": "^9.0.0",
135
136
  "leaflet": "1.9.4",
@@ -179,6 +180,7 @@
179
180
  "@testing-library/jest-dom": "^6.4.0",
180
181
  "@testing-library/react": "^14.2.0",
181
182
  "@testing-library/user-event": "^14.5.0",
183
+ "@types/d3-force": "^3.0.10",
182
184
  "@types/leaflet": "1.9.21",
183
185
  "@types/node": "^22.0.0",
184
186
  "@types/react": "^19.0.0",