@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, useSensors, useSensor, PointerSensor, KeyboardSensor, useDr
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 as getPatternDefinition$1, getComponentForPattern as getComponentForPattern$1 } from '@almadar/core/patterns';
47
48
  import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
48
49
 
@@ -36619,7 +36620,10 @@ function resolveColor3(color, el) {
36619
36620
  const resolved = getComputedStyle(el).getPropertyValue(m[1]).trim();
36620
36621
  return resolved || (m[2]?.trim() ?? "#888888");
36621
36622
  }
36622
- var GROUP_COLORS2, labelMeasureCtx, GraphCanvas;
36623
+ function truncateLabel(label) {
36624
+ return label.length > MAX_LABEL_CHARS ? label.slice(0, MAX_LABEL_CHARS - 1) + "\u2026" : label;
36625
+ }
36626
+ var GROUP_COLORS2, labelMeasureCtx, MAX_LABEL_CHARS, GraphCanvas;
36623
36627
  var init_GraphCanvas = __esm({
36624
36628
  "components/core/molecules/GraphCanvas.tsx"() {
36625
36629
  "use client";
@@ -36640,6 +36644,7 @@ var init_GraphCanvas = __esm({
36640
36644
  "var(--color-accent)"
36641
36645
  ];
36642
36646
  labelMeasureCtx = null;
36647
+ MAX_LABEL_CHARS = 22;
36643
36648
  GraphCanvas = ({
36644
36649
  title,
36645
36650
  nodes: propNodes = [],
@@ -36744,9 +36749,13 @@ var init_GraphCanvas = __esm({
36744
36749
  useEffect(() => {
36745
36750
  const canvas = canvasRef.current;
36746
36751
  if (!canvas || propNodes.length === 0) return;
36747
- const w = logicalW;
36748
- const h = height;
36752
+ const viewW = logicalW;
36753
+ const viewH = height;
36754
+ const layoutScale = Math.max(1, Math.min(3, Math.sqrt(propNodes.length / 12)));
36755
+ const w = viewW * layoutScale;
36756
+ const h = viewH * layoutScale;
36749
36757
  const labelFont = resolveColor3("var(--font-family)", canvas) || "system-ui";
36758
+ const clamp01 = (v) => Math.min(1, Math.max(0, v));
36750
36759
  const prevPos = /* @__PURE__ */ new Map();
36751
36760
  for (const pn of nodesRef.current) if (pn.x != null && pn.y != null) prevPos.set(pn.id, { x: pn.x, y: pn.y });
36752
36761
  const simNodes = propNodes.map((n, idx) => {
@@ -36771,7 +36780,7 @@ var init_GraphCanvas = __esm({
36771
36780
  y = h * 0.2 + rand() * h * 0.6;
36772
36781
  }
36773
36782
  }
36774
- return { ...n, x, y, vx: 0, vy: 0, fx: 0, fy: 0 };
36783
+ return { ...n, x, y, vx: 0, vy: 0 };
36775
36784
  });
36776
36785
  nodesRef.current = simNodes;
36777
36786
  const prevIds = /* @__PURE__ */ new Set([...prevPos.keys()]);
@@ -36780,105 +36789,52 @@ var init_GraphCanvas = __esm({
36780
36789
  const overlap = prevIds.size === 0 ? 0 : kept / Math.max(prevIds.size, newIdList.length);
36781
36790
  const fullRelayout = !laidOutRef.current || overlap < 0.5;
36782
36791
  if (layout === "force") {
36783
- const maxIterations = 300;
36784
- const COOL = 0.12;
36785
- const COLLIDE_PASSES = 6;
36786
- const LABEL_GAP = 12;
36787
- const LABEL_H = 16;
36788
- const tick = (iter) => {
36789
- const nodes = nodesRef.current;
36790
- const centerX = w / 2;
36791
- const centerY = h / 2;
36792
- const temp = Math.max(COOL, 1 - iter / maxIterations);
36793
- for (const node of nodes) {
36794
- node.fx = 0;
36795
- node.fy = 0;
36796
- }
36797
- for (let i = 0; i < nodes.length; i++) {
36798
- for (let j = i + 1; j < nodes.length; j++) {
36799
- const dx = nodes[j].x - nodes[i].x;
36800
- const dy = nodes[j].y - nodes[i].y;
36801
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
36802
- const force = repulsion / (dist * dist) * temp;
36803
- const fx = dx / dist * force;
36804
- const fy = dy / dist * force;
36805
- nodes[i].fx -= fx;
36806
- nodes[i].fy -= fy;
36807
- nodes[j].fx += fx;
36808
- nodes[j].fy += fy;
36809
- }
36810
- }
36811
- const nodeById = new Map(nodes.map((n) => [n.id, n]));
36812
- const spring = (a, b, rest, k) => {
36813
- const dx = b.x - a.x;
36814
- const dy = b.y - a.y;
36815
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
36816
- const force = (dist - rest) * k * temp;
36817
- const fx = dx / dist * force;
36818
- const fy = dy / dist * force;
36819
- a.fx += fx;
36820
- a.fy += fy;
36821
- b.fx -= fx;
36822
- b.fy -= fy;
36823
- };
36792
+ if (fullRelayout) {
36793
+ const LABEL_GAP = 12;
36794
+ const LABEL_H = 16;
36795
+ for (const n of simNodes) n.labelW = showLabels && n.label ? measureLabelWidth(truncateLabel(n.label), labelFont) : 0;
36796
+ const SIMILARITY_NEIGHBORS = 5;
36797
+ const SIMILARITY_MIN_WEIGHT = 0.25;
36824
36798
  const drawnPairs = /* @__PURE__ */ new Set();
36825
- for (const edge of propEdges) {
36826
- const source = nodeById.get(edge.source);
36827
- const target = nodeById.get(edge.target);
36828
- if (!source || !target) continue;
36829
- drawnPairs.add(edgeKeyOf(edge.source, edge.target));
36830
- const w2 = Math.min(1, Math.max(0, edge.weight ?? 1));
36831
- spring(source, target, linkDistance * (0.35 + (1 - w2) * 0.3), 0.14);
36832
- }
36799
+ for (const edge of propEdges) drawnPairs.add(edgeKeyOf(edge.source, edge.target));
36800
+ const similarityNeighbors = /* @__PURE__ */ new Map();
36801
+ const bySource = /* @__PURE__ */ new Map();
36833
36802
  for (const pair of propSimilarity) {
36803
+ if (pair.weight < SIMILARITY_MIN_WEIGHT) continue;
36834
36804
  if (drawnPairs.has(edgeKeyOf(pair.source, pair.target))) continue;
36835
- const source = nodeById.get(pair.source);
36836
- const target = nodeById.get(pair.target);
36837
- if (!source || !target) continue;
36838
- const w2 = Math.min(1, Math.max(0, pair.weight));
36839
- spring(source, target, linkDistance * (1.35 - 0.55 * w2), 0.015);
36805
+ const arr = bySource.get(pair.source) ?? [];
36806
+ arr.push(pair);
36807
+ bySource.set(pair.source, arr);
36840
36808
  }
36841
- const centroids = /* @__PURE__ */ new Map();
36842
- for (const node of nodes) {
36843
- const g = node.group ?? "__none__";
36844
- let c = centroids.get(g);
36845
- if (!c) {
36846
- c = { x: 0, y: 0, n: 0 };
36847
- centroids.set(g, c);
36848
- }
36849
- c.x += node.x;
36850
- c.y += node.y;
36851
- c.n += 1;
36809
+ for (const [id, arr] of bySource) {
36810
+ arr.sort((a, b) => b.weight - a.weight);
36811
+ similarityNeighbors.set(id, new Set(arr.slice(0, SIMILARITY_NEIGHBORS).map((p) => p.target)));
36852
36812
  }
36853
- const multiCluster = centroids.size > 1;
36854
- for (const node of nodes) {
36855
- if (multiCluster) {
36856
- const c = centroids.get(node.group ?? "__none__");
36857
- if (c && c.n > 1) {
36858
- node.fx += (c.x / c.n - node.x) * 0.04 * temp;
36859
- node.fy += (c.y / c.n - node.y) * 0.04 * temp;
36860
- } else {
36861
- node.fx += (centerX - node.x) * 0.01 * temp;
36862
- node.fy += (centerY - node.y) * 0.01 * temp;
36863
- }
36864
- } else {
36865
- node.fx += (centerX - node.x) * 8e-3 * temp;
36866
- node.fy += (centerY - node.y) * 8e-3 * temp;
36867
- }
36868
- }
36869
- const damping = 0.9;
36870
- for (const node of nodes) {
36871
- node.vx = (node.vx + node.fx) * damping;
36872
- node.vy = (node.vy + node.fy) * damping;
36873
- node.x += node.vx;
36874
- node.y += node.vy;
36875
- node.x = Math.max(30, Math.min(w - 30, node.x));
36876
- node.y = Math.max(30, Math.min(h - 30, node.y));
36813
+ const activeSimilarity = propSimilarity.filter((pair) => {
36814
+ if (drawnPairs.has(edgeKeyOf(pair.source, pair.target))) return false;
36815
+ const a = similarityNeighbors.get(pair.source);
36816
+ const b = similarityNeighbors.get(pair.target);
36817
+ return (a?.has(pair.target) ?? false) || (b?.has(pair.source) ?? false);
36818
+ });
36819
+ const groupTarget = /* @__PURE__ */ new Map();
36820
+ const multiCluster = groups.length > 1;
36821
+ if (multiCluster) {
36822
+ const ring = Math.min(w, h) * 0.34;
36823
+ groups.forEach((g, i) => {
36824
+ const a = i / groups.length * 2 * Math.PI;
36825
+ groupTarget.set(g, { x: w / 2 + ring * Math.cos(a), y: h / 2 + ring * Math.sin(a) });
36826
+ });
36877
36827
  }
36828
+ const present = new Set(simNodes.map((n) => n.id));
36829
+ 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) }));
36830
+ 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) }));
36831
+ const collideRadius = (n) => Math.max(n.size || 8, (n.labelW ?? 0) / 2) + nodeSpacing / 2;
36832
+ 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();
36833
+ for (let i = 0; i < 300; i++) simulation.tick();
36878
36834
  const pad = nodeSpacing / 2;
36879
36835
  const rectsOf = (n) => {
36880
36836
  const r = n.size || 8;
36881
- const lw = showLabels ? n.labelW ?? (n.labelW = n.label ? measureLabelWidth(n.label, labelFont) : 0) : 0;
36837
+ const lw = showLabels ? n.labelW ?? 0 : 0;
36882
36838
  return {
36883
36839
  circle: [n.x - r - pad, n.y - r - pad, n.x + r + pad, n.y + r + pad],
36884
36840
  label: [n.x - lw / 2 - pad, n.y + r + LABEL_GAP - 8, n.x + lw / 2 + pad, n.y + r + LABEL_GAP + LABEL_H]
@@ -36890,11 +36846,12 @@ var init_GraphCanvas = __esm({
36890
36846
  if (ox <= 0 || oy <= 0) return null;
36891
36847
  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 };
36892
36848
  };
36893
- for (let pass = 0; pass < COLLIDE_PASSES; pass++) {
36894
- for (let i = 0; i < nodes.length; i++) {
36895
- for (let j = i + 1; j < nodes.length; j++) {
36896
- const a = nodes[i];
36897
- const b = nodes[j];
36849
+ for (let pass = 0; pass < 24; pass++) {
36850
+ let moved = false;
36851
+ for (let i = 0; i < simNodes.length; i++) {
36852
+ for (let j = i + 1; j < simNodes.length; j++) {
36853
+ const a = simNodes[i];
36854
+ const b = simNodes[j];
36898
36855
  const ra = rectsOf(a);
36899
36856
  const rb = rectsOf(b);
36900
36857
  let best = null;
@@ -36903,25 +36860,35 @@ var init_GraphCanvas = __esm({
36903
36860
  if (s && (!best || s.depth < best.depth)) best = s;
36904
36861
  }
36905
36862
  if (best) {
36863
+ moved = true;
36906
36864
  const push = best.depth / 2;
36907
36865
  if (best.axis === "x") {
36908
36866
  a.x = Math.max(30, Math.min(w - 30, a.x - push * best.sign));
36909
36867
  b.x = Math.max(30, Math.min(w - 30, b.x + push * best.sign));
36910
- a.vx = 0;
36911
- b.vx = 0;
36912
36868
  } else {
36913
36869
  a.y = Math.max(30, Math.min(h - 30, a.y - push * best.sign));
36914
36870
  b.y = Math.max(30, Math.min(h - 30, b.y + push * best.sign));
36915
- a.vy = 0;
36916
- b.vy = 0;
36917
36871
  }
36918
36872
  }
36919
36873
  }
36920
36874
  }
36875
+ if (!moved) break;
36921
36876
  }
36922
- };
36923
- if (fullRelayout) {
36924
- for (let i = 0; i < maxIterations; i++) tick(i);
36877
+ const fitPad = 40;
36878
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
36879
+ for (const node of nodesRef.current) {
36880
+ const r = node.size || 8;
36881
+ const lw = showLabels ? node.labelW ?? 0 : 0;
36882
+ minX = Math.min(minX, node.x - r, node.x - lw / 2);
36883
+ minY = Math.min(minY, node.y - r);
36884
+ maxX = Math.max(maxX, node.x + r, node.x + lw / 2);
36885
+ maxY = Math.max(maxY, node.y + r + LABEL_GAP + LABEL_H);
36886
+ }
36887
+ const bboxW = Math.max(1, maxX - minX + fitPad * 2);
36888
+ const bboxH = Math.max(1, maxY - minY + fitPad * 2);
36889
+ const nextZoom = Math.min(1, viewW / bboxW, viewH / bboxH);
36890
+ setZoom(nextZoom);
36891
+ setOffset({ x: fitPad - minX * nextZoom, y: fitPad - minY * nextZoom });
36925
36892
  laidOutRef.current = true;
36926
36893
  } else {
36927
36894
  const centroids = /* @__PURE__ */ new Map();
@@ -37032,6 +36999,7 @@ var init_GraphCanvas = __esm({
37032
36999
  }
37033
37000
  ctx.stroke();
37034
37001
  if (showLabels && node.label) {
37002
+ const displayLabel = truncateLabel(node.label);
37035
37003
  ctx.font = `${isSelected || isHovered ? "600" : "500"} 12px ${fontFamily}`;
37036
37004
  ctx.textAlign = "center";
37037
37005
  ctx.textBaseline = "middle";
@@ -37039,9 +37007,9 @@ var init_GraphCanvas = __esm({
37039
37007
  ctx.lineWidth = 3;
37040
37008
  ctx.lineJoin = "round";
37041
37009
  ctx.strokeStyle = bgColor;
37042
- ctx.strokeText(node.label, node.x, ly);
37010
+ ctx.strokeText(displayLabel, node.x, ly);
37043
37011
  ctx.fillStyle = isSelected || isHovered ? fgColor : mutedColor;
37044
- ctx.fillText(node.label, node.x, ly);
37012
+ ctx.fillText(displayLabel, node.x, ly);
37045
37013
  }
37046
37014
  if (node.badge && node.badge > 1) {
37047
37015
  const bx = node.x + radius * 0.7;
@@ -37189,6 +37157,20 @@ var init_GraphCanvas = __esm({
37189
37157
  },
37190
37158
  [toCoords, nodeAt, onNodeDoubleClick]
37191
37159
  );
37160
+ const hoveredObj = hoveredNode ? nodesRef.current.find((n) => n.id === hoveredNode) : void 0;
37161
+ const canvasEl = canvasRef.current;
37162
+ const showLabelTooltip = Boolean(
37163
+ hoveredObj?.label && hoveredObj.label.length > MAX_LABEL_CHARS && hoveredObj.x != null && canvasEl
37164
+ );
37165
+ const labelTooltipStyle = (() => {
37166
+ if (!showLabelTooltip || !canvasEl || !hoveredObj || hoveredObj.x == null || hoveredObj.y == null) return void 0;
37167
+ const rect = canvasEl.getBoundingClientRect();
37168
+ return {
37169
+ left: rect.left + offset.x + hoveredObj.x * zoom,
37170
+ top: rect.top + offset.y + hoveredObj.y * zoom - 10,
37171
+ transform: "translate(-50%, -100%)"
37172
+ };
37173
+ })();
37192
37174
  if (isLoading) {
37193
37175
  return /* @__PURE__ */ jsx(LoadingState, { message: t("common.loading"), className });
37194
37176
  }
@@ -37242,23 +37224,43 @@ var init_GraphCanvas = __esm({
37242
37224
  ]
37243
37225
  }
37244
37226
  ),
37245
- /* @__PURE__ */ jsx(Box, { className: "w-full bg-background", children: /* @__PURE__ */ jsx(
37246
- "canvas",
37247
- {
37248
- ref: canvasRef,
37249
- width: Math.round(logicalW * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
37250
- height: Math.round(height * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
37251
- className: "w-full cursor-grab active:cursor-grabbing touch-none",
37252
- style: { height },
37253
- onPointerDown: gestureHandlers.onPointerDown,
37254
- onPointerMove: gestureHandlers.onPointerMove,
37255
- onPointerUp: gestureHandlers.onPointerUp,
37256
- onPointerCancel: gestureHandlers.onPointerCancel,
37257
- onPointerLeave: handlePointerLeave,
37258
- onWheel: gestureHandlers.onWheel,
37259
- onDoubleClick: handleDoubleClick
37260
- }
37261
- ) }),
37227
+ /* @__PURE__ */ jsxs(Box, { className: "w-full bg-background", children: [
37228
+ /* @__PURE__ */ jsx(
37229
+ "canvas",
37230
+ {
37231
+ ref: canvasRef,
37232
+ width: Math.round(logicalW * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
37233
+ height: Math.round(height * (typeof window !== "undefined" && window.devicePixelRatio || 1)),
37234
+ className: "w-full cursor-grab active:cursor-grabbing touch-none",
37235
+ style: { height },
37236
+ onPointerDown: gestureHandlers.onPointerDown,
37237
+ onPointerMove: gestureHandlers.onPointerMove,
37238
+ onPointerUp: gestureHandlers.onPointerUp,
37239
+ onPointerCancel: gestureHandlers.onPointerCancel,
37240
+ onPointerLeave: handlePointerLeave,
37241
+ onWheel: gestureHandlers.onWheel,
37242
+ onDoubleClick: handleDoubleClick
37243
+ }
37244
+ ),
37245
+ typeof window !== "undefined" && showLabelTooltip && labelTooltipStyle && createPortal(
37246
+ /* @__PURE__ */ jsx(
37247
+ "div",
37248
+ {
37249
+ className: cn(
37250
+ "fixed z-50 px-3 py-2 max-w-xs",
37251
+ "bg-primary text-primary-foreground",
37252
+ "shadow-elevation-popover rounded-sm",
37253
+ "text-sm pointer-events-none",
37254
+ "break-words whitespace-normal"
37255
+ ),
37256
+ style: labelTooltipStyle,
37257
+ role: "tooltip",
37258
+ children: hoveredObj?.label
37259
+ }
37260
+ ),
37261
+ document.body
37262
+ )
37263
+ ] }),
37262
37264
  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: [
37263
37265
  /* @__PURE__ */ jsx(
37264
37266
  Box,