footprint-explainable-ui 0.25.0 → 0.25.2

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.
package/dist/flowchart.js CHANGED
@@ -490,7 +490,7 @@ var StageNode = memo(function StageNode2({
490
490
  background: bg,
491
491
  border: `${isHero ? "2.5px" : isMuted ? "1px" : "2px"} ${isLazyUnresolved ? "dashed" : "solid"} ${borderColor}`,
492
492
  borderRadius: theme.radius,
493
- padding: description ? `${Math.round(8 * sizeScale)}px ${Math.round(16 * sizeScale)}px` : `${Math.round(10 * sizeScale)}px ${Math.round(20 * sizeScale)}px`,
493
+ padding: description ? `${Math.round(6 * sizeScale)}px ${Math.round(12 * sizeScale)}px` : `${Math.round(7 * sizeScale)}px ${Math.round(14 * sizeScale)}px`,
494
494
  display: "flex",
495
495
  flexDirection: "column",
496
496
  alignItems: "center",
@@ -1464,6 +1464,14 @@ function staggeredBendY(sourceBottom, targetTop, others, minGapFromTarget = 8) {
1464
1464
  if (lowestSkippedBottom === -Infinity) return null;
1465
1465
  return Math.min((lowestSkippedBottom + targetTop) / 2, targetTop - minGapFromTarget);
1466
1466
  }
1467
+ function forkFanBendY(sourceBottom, childTops, minGapFromTarget = 8) {
1468
+ if (childTops.length < 2) return null;
1469
+ const nearestTop = Math.min(...childTops);
1470
+ return Math.min((sourceBottom + nearestTop) / 2, nearestTop - minGapFromTarget);
1471
+ }
1472
+ function resolveStepBendY(forkBend, staggeredBend) {
1473
+ return staggeredBend ?? forkBend;
1474
+ }
1467
1475
 
1468
1476
  // src/components/SmartStepEdge/SmartStepEdge.tsx
1469
1477
  import { jsx as jsx7 } from "react/jsx-runtime";
@@ -1486,6 +1494,16 @@ function SmartStepEdge({
1486
1494
  if (!src || !tgt) return null;
1487
1495
  const sourceBottom = src.internals.positionAbsolute.y + (src.measured.height ?? 0);
1488
1496
  const targetTop = tgt.internals.positionAbsolute.y;
1497
+ const childTops = [];
1498
+ for (const e of s.edges) {
1499
+ if (e.source !== source) continue;
1500
+ if (e.data?.kind === "loop") continue;
1501
+ const c = s.nodeLookup.get(e.target);
1502
+ if (c && c.type !== GROUP_CONTAINER_NODE_TYPE) {
1503
+ childTops.push(c.internals.positionAbsolute.y);
1504
+ }
1505
+ }
1506
+ const fan = forkFanBendY(sourceBottom, childTops);
1489
1507
  const others = [];
1490
1508
  for (const n of s.nodeLookup.values()) {
1491
1509
  if (n.id === source || n.id === target) continue;
@@ -1493,7 +1511,8 @@ function SmartStepEdge({
1493
1511
  const top = n.internals.positionAbsolute.y;
1494
1512
  others.push({ top, bottom: top + (n.measured.height ?? 0) });
1495
1513
  }
1496
- return staggeredBendY(sourceBottom, targetTop, others);
1514
+ const staggered = staggeredBendY(sourceBottom, targetTop, others);
1515
+ return resolveStepBendY(fan, staggered);
1497
1516
  });
1498
1517
  const [path] = getSmoothStepPath({
1499
1518
  sourceX,
@@ -1856,7 +1875,7 @@ function TraceFlow(props) {
1856
1875
  }
1857
1876
 
1858
1877
  // src/components/FlowchartView/TracedFlow.tsx
1859
- import { useCallback as useCallback3, useMemo as useMemo5, useRef as useRef5, useState as useState4 } from "react";
1878
+ import { useCallback as useCallback3, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef5, useState as useState4 } from "react";
1860
1879
  import {
1861
1880
  ReactFlow as ReactFlow2,
1862
1881
  Background as Background2,
@@ -1874,6 +1893,150 @@ function devWarn(messageFn, ...extras) {
1874
1893
  console.warn(messageFn(), ...extras);
1875
1894
  }
1876
1895
 
1896
+ // src/components/FlowchartView/_internal/snapLinearSuccessors.ts
1897
+ function snapLinearSuccessors(graph, options = {}) {
1898
+ if (graph.nodes.length === 0) return graph;
1899
+ const fallbackW = options.nodeWidth ?? DEFAULT_NODE_W2;
1900
+ const fallbackH = options.nodeHeight ?? DEFAULT_NODE_H2;
1901
+ const byId = /* @__PURE__ */ new Map();
1902
+ const width = /* @__PURE__ */ new Map();
1903
+ for (const n of graph.nodes) {
1904
+ byId.set(n.id, n);
1905
+ width.set(n.id, sizeOf(n, fallbackW, fallbackH, options.nodeSize).width);
1906
+ }
1907
+ const preds = /* @__PURE__ */ new Map();
1908
+ const outDegree = /* @__PURE__ */ new Map();
1909
+ const seenEdge = /* @__PURE__ */ new Set();
1910
+ for (const e of graph.edges) {
1911
+ if (e.data?.kind === "loop") continue;
1912
+ if (!byId.has(e.source) || !byId.has(e.target)) continue;
1913
+ const key = `${e.source}\0${e.target}`;
1914
+ if (seenEdge.has(key)) continue;
1915
+ seenEdge.add(key);
1916
+ const list = preds.get(e.target);
1917
+ if (list) list.push(e.source);
1918
+ else preds.set(e.target, [e.source]);
1919
+ outDegree.set(e.source, (outDegree.get(e.source) ?? 0) + 1);
1920
+ }
1921
+ const workingX = /* @__PURE__ */ new Map();
1922
+ for (const n of graph.nodes) workingX.set(n.id, n.position.x);
1923
+ const centerX = (id) => workingX.get(id) + width.get(id) / 2;
1924
+ const order = [...graph.nodes].sort(
1925
+ (a, b) => a.position.y - b.position.y || a.position.x - b.position.x || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
1926
+ );
1927
+ for (const n of order) {
1928
+ const p = preds.get(n.id);
1929
+ if (!p || p.length !== 1) continue;
1930
+ const pid = p[0];
1931
+ if ((outDegree.get(pid) ?? 0) !== 1) continue;
1932
+ const P = byId.get(pid);
1933
+ if ((n.parentId ?? void 0) !== (P.parentId ?? void 0)) continue;
1934
+ workingX.set(n.id, centerX(pid) - width.get(n.id) / 2);
1935
+ }
1936
+ const nodes = graph.nodes.map((n) => {
1937
+ const nx = workingX.get(n.id);
1938
+ return nx === n.position.x ? n : { ...n, position: { x: nx, y: n.position.y } };
1939
+ });
1940
+ return { nodes, edges: graph.edges };
1941
+ }
1942
+ function createSnappedDagreLayout(base, options = {}) {
1943
+ return (graph) => snapLinearSuccessors(base(graph), options);
1944
+ }
1945
+
1946
+ // src/components/FlowchartView/_internal/centerForkParents.ts
1947
+ function centerForkParents(graph, options = {}) {
1948
+ if (graph.nodes.length === 0) return graph;
1949
+ const fallbackW = options.nodeWidth ?? DEFAULT_NODE_W2;
1950
+ const fallbackH = options.nodeHeight ?? DEFAULT_NODE_H2;
1951
+ const byId = /* @__PURE__ */ new Map();
1952
+ const width = /* @__PURE__ */ new Map();
1953
+ for (const n of graph.nodes) {
1954
+ byId.set(n.id, n);
1955
+ width.set(n.id, sizeOf(n, fallbackW, fallbackH, options.nodeSize).width);
1956
+ }
1957
+ const childrenOf = /* @__PURE__ */ new Map();
1958
+ const predsOf = /* @__PURE__ */ new Map();
1959
+ const outDegree = /* @__PURE__ */ new Map();
1960
+ const inDegree = /* @__PURE__ */ new Map();
1961
+ const seen = /* @__PURE__ */ new Set();
1962
+ for (const e of graph.edges) {
1963
+ if (e.data?.kind === "loop") continue;
1964
+ if (!byId.has(e.source) || !byId.has(e.target)) continue;
1965
+ const key = `${e.source} ${e.target}`;
1966
+ if (seen.has(key)) continue;
1967
+ seen.add(key);
1968
+ const cl = childrenOf.get(e.source);
1969
+ if (cl) cl.push(e.target);
1970
+ else childrenOf.set(e.source, [e.target]);
1971
+ const pl = predsOf.get(e.target);
1972
+ if (pl) pl.push(e.source);
1973
+ else predsOf.set(e.target, [e.source]);
1974
+ outDegree.set(e.source, (outDegree.get(e.source) ?? 0) + 1);
1975
+ inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);
1976
+ }
1977
+ const workingX = /* @__PURE__ */ new Map();
1978
+ for (const n of graph.nodes) workingX.set(n.id, n.position.x);
1979
+ const centerX = (id) => workingX.get(id) + width.get(id) / 2;
1980
+ const nodeSep = options.nodeSep ?? 60;
1981
+ const clampX = (id, desiredX) => {
1982
+ const w = width.get(id);
1983
+ const x0 = workingX.get(id);
1984
+ const self = byId.get(id);
1985
+ let minX = -Infinity;
1986
+ let maxX = Infinity;
1987
+ for (const m of graph.nodes) {
1988
+ if (m.id === id || m.parentId !== self.parentId) continue;
1989
+ if (Math.abs(m.position.y - self.position.y) > 1) continue;
1990
+ const mLeft = workingX.get(m.id);
1991
+ const mRight = mLeft + width.get(m.id);
1992
+ if (mRight <= x0) minX = Math.max(minX, mRight + nodeSep);
1993
+ else if (mLeft >= x0 + w) maxX = Math.min(maxX, mLeft - nodeSep - w);
1994
+ }
1995
+ return minX <= maxX ? Math.max(minX, Math.min(maxX, desiredX)) : x0;
1996
+ };
1997
+ const order = [...graph.nodes].sort(
1998
+ (a, b) => b.position.y - a.position.y || a.position.x - b.position.x || a.id.localeCompare(b.id)
1999
+ );
2000
+ for (const n of order) {
2001
+ const outD = outDegree.get(n.id) ?? 0;
2002
+ const inD = inDegree.get(n.id) ?? 0;
2003
+ const isFork = outD >= 2 && inD <= 1;
2004
+ const isMerge = inD >= 2 && outD <= 1;
2005
+ if (!isFork && !isMerge) continue;
2006
+ const kin = ((isFork ? childrenOf.get(n.id) : predsOf.get(n.id)) ?? []).filter(
2007
+ (k) => byId.get(k)?.parentId === n.parentId
2008
+ // same compound only
2009
+ );
2010
+ if (kin.length < 2) continue;
2011
+ const centers = kin.map(centerX);
2012
+ const wN = width.get(n.id);
2013
+ const span = (Math.min(...centers) + Math.max(...centers)) / 2;
2014
+ workingX.set(n.id, clampX(n.id, span - wN / 2));
2015
+ const stepOf = isFork ? predsOf : childrenOf;
2016
+ let curId = n.id;
2017
+ const walked = /* @__PURE__ */ new Set([curId]);
2018
+ for (; ; ) {
2019
+ const nexts = stepOf.get(curId);
2020
+ if (!nexts || nexts.length !== 1) break;
2021
+ const m = nexts[0];
2022
+ if (walked.has(m)) break;
2023
+ if ((outDegree.get(m) ?? 0) > 1) break;
2024
+ if ((inDegree.get(m) ?? 0) > 1) break;
2025
+ if (byId.get(m)?.parentId !== byId.get(curId)?.parentId) break;
2026
+ workingX.set(m, clampX(m, centerX(curId) - width.get(m) / 2));
2027
+ walked.add(m);
2028
+ curId = m;
2029
+ }
2030
+ }
2031
+ const nodes = graph.nodes.map(
2032
+ (n) => workingX.get(n.id) === n.position.x ? n : { ...n, position: { x: workingX.get(n.id), y: n.position.y } }
2033
+ );
2034
+ return { nodes, edges: graph.edges };
2035
+ }
2036
+ function withForkCentering(base, options = {}) {
2037
+ return (graph) => centerForkParents(base(graph), options);
2038
+ }
2039
+
1877
2040
  // src/components/FlowchartView/_internal/notifyChange.ts
1878
2041
  function createNotifier(label = "notifier") {
1879
2042
  const listeners = /* @__PURE__ */ new Set();
@@ -2231,6 +2394,49 @@ function GroupContainerNode({ data }) {
2231
2394
  );
2232
2395
  }
2233
2396
 
2397
+ // src/components/FlowchartView/_internal/MeasuredNodeSizes.tsx
2398
+ import { useEffect as useEffect6 } from "react";
2399
+ import { useNodesInitialized, useStore as useStore3 } from "@xyflow/react";
2400
+
2401
+ // src/components/FlowchartView/_internal/measuredFootprints.ts
2402
+ function extractMeasuredFootprints(entries) {
2403
+ const sizes = /* @__PURE__ */ new Map();
2404
+ for (const [id, node] of entries) {
2405
+ const width = node.measured?.width;
2406
+ const height = node.measured?.height;
2407
+ if (typeof width === "number" && typeof height === "number" && width > 0 && height > 0) {
2408
+ sizes.set(id, { width: Math.round(width), height: Math.round(height) });
2409
+ }
2410
+ }
2411
+ return sizes;
2412
+ }
2413
+ function sameFootprints(a, b) {
2414
+ if (a === b) return true;
2415
+ if (a.size !== b.size) return false;
2416
+ for (const [id, s] of a) {
2417
+ const t = b.get(id);
2418
+ if (!t || t.width !== s.width || t.height !== s.height) return false;
2419
+ }
2420
+ return true;
2421
+ }
2422
+
2423
+ // src/components/FlowchartView/_internal/MeasuredNodeSizes.tsx
2424
+ function MeasuredNodeSizes({
2425
+ onSizes,
2426
+ includeHiddenNodes = false
2427
+ }) {
2428
+ const initialized = useNodesInitialized({ includeHiddenNodes });
2429
+ const sizes = useStore3(
2430
+ (s) => extractMeasuredFootprints(s.nodeLookup),
2431
+ sameFootprints
2432
+ );
2433
+ useEffect6(() => {
2434
+ if (!initialized || sizes.size === 0) return;
2435
+ onSizes(sizes);
2436
+ }, [initialized, sizes, onSizes]);
2437
+ return null;
2438
+ }
2439
+
2234
2440
  // src/components/FlowchartView/TracedFlow.tsx
2235
2441
  import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
2236
2442
  var DEFAULT_COLORS = {
@@ -2370,6 +2576,13 @@ function TracedFlow({
2370
2576
  style
2371
2577
  }) {
2372
2578
  const layout = layoutProp ?? dagreTraceLayout;
2579
+ useEffect7(() => {
2580
+ if (layoutProp === dagreTraceLayout) {
2581
+ devWarn(
2582
+ () => "[footprint-explainable-ui] <TracedFlow layout={dagreTraceLayout}> bypasses the built-in measure-then-layout pipeline (content-exact sizing, fork/merge centering, straight spines). OMIT the `layout` prop to use it \u2014 passing the raw dagreTraceLayout silently forfeits every layout improvement eui ships."
2583
+ );
2584
+ }
2585
+ }, [layoutProp]);
2373
2586
  const colors = useMemo5(
2374
2587
  () => ({ ...DEFAULT_COLORS, ...colorOverrides ?? {} }),
2375
2588
  [colorOverrides]
@@ -2403,8 +2616,19 @@ function TracedFlow({
2403
2616
  () => buildSubflowBreadcrumb(graph, drill.currentSubflowId),
2404
2617
  [graph, drill.currentSubflowId]
2405
2618
  );
2619
+ const [measuredSizes, setMeasuredSizes] = useState4(null);
2406
2620
  const positioned = useMemo5(() => {
2407
- const realBase = layout === "passthrough" ? (g) => g : layout;
2621
+ const nodeSize = measuredSizes ? (n) => measuredSizes.get(n.id) : void 0;
2622
+ const sizeOpts = nodeSize ? { nodeSize } : {};
2623
+ const dagreBase = withForkCentering(
2624
+ createSnappedDagreLayout(
2625
+ createDagreTraceLayout({ ...sizeOpts, rankSep: 52, nodeSep: 36 }),
2626
+ sizeOpts
2627
+ ),
2628
+ { ...sizeOpts, nodeSep: 36 }
2629
+ // same nodeSep → clamp preserves dagre's reserved gap
2630
+ );
2631
+ const realBase = layout === "passthrough" ? (g) => g : layoutProp === void 0 ? dagreBase : layout;
2408
2632
  if (groupedSet.size > 0) {
2409
2633
  const grouped = applyGroupLayout(filteredGraph, {
2410
2634
  groupedSubflowIds: [...groupedSet],
@@ -2415,8 +2639,8 @@ function TracedFlow({
2415
2639
  if (mainChartBox) {
2416
2640
  return wrapInMainChartBox(filteredGraph, { baseLayout: realBase, ...mainChartBox });
2417
2641
  }
2418
- return layout === "passthrough" ? filteredGraph : layout(filteredGraph);
2419
- }, [filteredGraph, layout, groupedSet, mainChartBox]);
2642
+ return realBase(filteredGraph);
2643
+ }, [filteredGraph, layout, layoutProp, groupedSet, mainChartBox, measuredSizes]);
2420
2644
  const slice = useMemo5(() => {
2421
2645
  const empty = {
2422
2646
  doneStageIds: /* @__PURE__ */ new Set(),
@@ -2460,7 +2684,11 @@ function TracedFlow({
2460
2684
  );
2461
2685
  const wrapperRef = useRef5(null);
2462
2686
  const [rfInstance, setRfInstance] = useState4(null);
2463
- useChartAutoRefit(wrapperRef, rfInstance, { refitKey: drill.currentSubflowId });
2687
+ useChartAutoRefit(wrapperRef, rfInstance, {
2688
+ // Re-fit on drill AND after the measured-size re-layout settles.
2689
+ refitKey: `${drill.currentSubflowId ?? ""}:${measuredSizes ? "measured" : "estimated"}`,
2690
+ padding: 0.18
2691
+ });
2464
2692
  return /* @__PURE__ */ jsxs8(
2465
2693
  "div",
2466
2694
  {
@@ -2492,8 +2720,11 @@ function TracedFlow({
2492
2720
  onNodeClick: handleNodeClick,
2493
2721
  onInit: setRfInstance,
2494
2722
  fitView: true,
2723
+ fitViewOptions: { padding: 0.18 },
2724
+ minZoom: 0.1,
2495
2725
  proOptions: { hideAttribution: true },
2496
2726
  children: [
2727
+ /* @__PURE__ */ jsx11(MeasuredNodeSizes, { onSizes: setMeasuredSizes }),
2497
2728
  /* @__PURE__ */ jsx11(Background2, { variant: BackgroundVariant2.Dots, gap: 20, size: 1 }),
2498
2729
  children
2499
2730
  ]
@@ -3508,56 +3739,6 @@ function SlotPillNode({ data }) {
3508
3739
  );
3509
3740
  }
3510
3741
 
3511
- // src/components/FlowchartView/_internal/snapLinearSuccessors.ts
3512
- function snapLinearSuccessors(graph, options = {}) {
3513
- if (graph.nodes.length === 0) return graph;
3514
- const fallbackW = options.nodeWidth ?? DEFAULT_NODE_W2;
3515
- const fallbackH = options.nodeHeight ?? DEFAULT_NODE_H2;
3516
- const byId = /* @__PURE__ */ new Map();
3517
- const width = /* @__PURE__ */ new Map();
3518
- for (const n of graph.nodes) {
3519
- byId.set(n.id, n);
3520
- width.set(n.id, sizeOf(n, fallbackW, fallbackH, options.nodeSize).width);
3521
- }
3522
- const preds = /* @__PURE__ */ new Map();
3523
- const outDegree = /* @__PURE__ */ new Map();
3524
- const seenEdge = /* @__PURE__ */ new Set();
3525
- for (const e of graph.edges) {
3526
- if (e.data?.kind === "loop") continue;
3527
- if (!byId.has(e.source) || !byId.has(e.target)) continue;
3528
- const key = `${e.source}\0${e.target}`;
3529
- if (seenEdge.has(key)) continue;
3530
- seenEdge.add(key);
3531
- const list = preds.get(e.target);
3532
- if (list) list.push(e.source);
3533
- else preds.set(e.target, [e.source]);
3534
- outDegree.set(e.source, (outDegree.get(e.source) ?? 0) + 1);
3535
- }
3536
- const workingX = /* @__PURE__ */ new Map();
3537
- for (const n of graph.nodes) workingX.set(n.id, n.position.x);
3538
- const centerX = (id) => workingX.get(id) + width.get(id) / 2;
3539
- const order = [...graph.nodes].sort(
3540
- (a, b) => a.position.y - b.position.y || a.position.x - b.position.x || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
3541
- );
3542
- for (const n of order) {
3543
- const p = preds.get(n.id);
3544
- if (!p || p.length !== 1) continue;
3545
- const pid = p[0];
3546
- if ((outDegree.get(pid) ?? 0) !== 1) continue;
3547
- const P = byId.get(pid);
3548
- if ((n.parentId ?? void 0) !== (P.parentId ?? void 0)) continue;
3549
- workingX.set(n.id, centerX(pid) - width.get(n.id) / 2);
3550
- }
3551
- const nodes = graph.nodes.map((n) => {
3552
- const nx = workingX.get(n.id);
3553
- return nx === n.position.x ? n : { ...n, position: { x: nx, y: n.position.y } };
3554
- });
3555
- return { nodes, edges: graph.edges };
3556
- }
3557
- function createSnappedDagreLayout(base, options = {}) {
3558
- return (graph) => snapLinearSuccessors(base(graph), options);
3559
- }
3560
-
3561
3742
  // src/components/FlowchartView/_internal/traceGroupLayout.ts
3562
3743
  function buildAdjacency(graph, fallbackW, fallbackH, nodeSize) {
3563
3744
  const preds = /* @__PURE__ */ new Map();