@pipelex/mthds-ui 0.5.2 → 0.6.1

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.
@@ -2,19 +2,22 @@
2
2
  import {
3
3
  DEFAULT_GRAPH_CONFIG,
4
4
  EDGE_TYPE,
5
+ FOLD_MODE,
5
6
  GRAPH_DIRECTION,
6
7
  MAX_VISIBLE_CONTROLLER_CHILDREN,
7
8
  applyControllers,
9
+ applyFolds,
8
10
  buildGraph,
11
+ findCousinControllers,
9
12
  getLayoutedElements,
10
13
  getPipeBlueprint,
11
14
  resolveConceptRef,
12
15
  stuffDigestFromId
13
- } from "../../chunk-OMJ6DRXM.js";
16
+ } from "../../chunk-NISDJYQJ.js";
14
17
  import {
15
18
  __spreadProps,
16
19
  __spreadValues
17
- } from "../../chunk-DDAAVRWG.js";
20
+ } from "../../chunk-2NMEKWO5.js";
18
21
 
19
22
  // src/graph/react/index.ts
20
23
  import "./graph-core.css";
@@ -1648,6 +1651,46 @@ var BOXES_ICON = /* @__PURE__ */ jsxs16(
1648
1651
  ]
1649
1652
  }
1650
1653
  );
1654
+ var FOLD_ALL_ICON = /* @__PURE__ */ jsxs16(
1655
+ "svg",
1656
+ {
1657
+ viewBox: "0 0 24 24",
1658
+ width: "14",
1659
+ height: "14",
1660
+ fill: "none",
1661
+ stroke: "currentColor",
1662
+ strokeWidth: "2",
1663
+ strokeLinecap: "round",
1664
+ strokeLinejoin: "round",
1665
+ children: [
1666
+ /* @__PURE__ */ jsx16("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }),
1667
+ /* @__PURE__ */ jsx16("polyline", { points: "2 2 6 6 2 6" }),
1668
+ /* @__PURE__ */ jsx16("polyline", { points: "22 2 18 6 22 6" }),
1669
+ /* @__PURE__ */ jsx16("polyline", { points: "2 22 6 18 2 18" }),
1670
+ /* @__PURE__ */ jsx16("polyline", { points: "22 22 18 18 22 18" })
1671
+ ]
1672
+ }
1673
+ );
1674
+ var EXPAND_ALL_ICON = /* @__PURE__ */ jsxs16(
1675
+ "svg",
1676
+ {
1677
+ viewBox: "0 0 24 24",
1678
+ width: "14",
1679
+ height: "14",
1680
+ fill: "none",
1681
+ stroke: "currentColor",
1682
+ strokeWidth: "2",
1683
+ strokeLinecap: "round",
1684
+ strokeLinejoin: "round",
1685
+ children: [
1686
+ /* @__PURE__ */ jsx16("rect", { x: "9", y: "9", width: "6", height: "6", rx: "1" }),
1687
+ /* @__PURE__ */ jsx16("polyline", { points: "3 3 7 7 3 7" }),
1688
+ /* @__PURE__ */ jsx16("polyline", { points: "21 3 17 7 21 7" }),
1689
+ /* @__PURE__ */ jsx16("polyline", { points: "3 21 7 17 3 17" }),
1690
+ /* @__PURE__ */ jsx16("polyline", { points: "21 21 17 17 21 17" })
1691
+ ]
1692
+ }
1693
+ );
1651
1694
  function GraphToolbar({
1652
1695
  direction,
1653
1696
  onDirectionChange,
@@ -1656,11 +1699,18 @@ function GraphToolbar({
1656
1699
  onZoomIn,
1657
1700
  onZoomOut,
1658
1701
  onFitView,
1702
+ onFoldAll,
1703
+ onExpandAll,
1704
+ foldAllDisabled = false,
1705
+ expandAllDisabled = false,
1659
1706
  rightOffset = 0
1660
1707
  }) {
1661
1708
  const isVertical = direction === GRAPH_DIRECTION.TB || direction === GRAPH_DIRECTION.BT;
1662
1709
  const directionLabel = isVertical ? "Switch to horizontal layout" : "Switch to vertical layout";
1663
1710
  const controllersLabel = showControllers ? "Hide pipe controllers" : "Show pipe controllers \u2014 groups pipes by their controlling pipe";
1711
+ const foldAllSection = onFoldAll || onExpandAll;
1712
+ const foldAllTitle = foldAllDisabled ? "Fold all controllers (nothing to fold)" : "Fold all controllers";
1713
+ const expandAllTitle = expandAllDisabled ? "Expand all controllers (nothing to expand)" : "Expand all controllers";
1664
1714
  return /* @__PURE__ */ jsxs16("div", { className: "graph-toolbar", style: { right: `${rightOffset + 8}px` }, children: [
1665
1715
  /* @__PURE__ */ jsx16(
1666
1716
  "button",
@@ -1684,6 +1734,31 @@ function GraphToolbar({
1684
1734
  children: BOXES_ICON
1685
1735
  }
1686
1736
  ),
1737
+ foldAllSection && /* @__PURE__ */ jsx16("div", { className: "graph-toolbar-separator" }),
1738
+ onFoldAll && /* @__PURE__ */ jsx16(
1739
+ "button",
1740
+ {
1741
+ type: "button",
1742
+ className: "graph-toolbar-btn",
1743
+ onClick: onFoldAll,
1744
+ disabled: foldAllDisabled,
1745
+ title: foldAllTitle,
1746
+ "aria-label": foldAllTitle,
1747
+ children: FOLD_ALL_ICON
1748
+ }
1749
+ ),
1750
+ onExpandAll && /* @__PURE__ */ jsx16(
1751
+ "button",
1752
+ {
1753
+ type: "button",
1754
+ className: "graph-toolbar-btn",
1755
+ onClick: onExpandAll,
1756
+ disabled: expandAllDisabled,
1757
+ title: expandAllTitle,
1758
+ "aria-label": expandAllTitle,
1759
+ children: EXPAND_ALL_ICON
1760
+ }
1761
+ ),
1687
1762
  (onZoomOut || onZoomIn || onFitView) && /* @__PURE__ */ jsx16("div", { className: "graph-toolbar-separator" }),
1688
1763
  onZoomOut && /* @__PURE__ */ jsx16(
1689
1764
  "button",
@@ -1748,7 +1823,22 @@ function ControllerGroupNode({ data }) {
1748
1823
  /* @__PURE__ */ jsxs17("div", { className: "controller-group-header", children: [
1749
1824
  /* @__PURE__ */ jsx17("span", { className: "controller-group-icon", children: config.icon }),
1750
1825
  /* @__PURE__ */ jsx17("span", { className: "controller-group-badge", children: config.badge }),
1751
- data.label && /* @__PURE__ */ jsx17("span", { className: "controller-group-label", children: data.label })
1826
+ data.label && /* @__PURE__ */ jsx17("span", { className: "controller-group-label", children: data.label }),
1827
+ data.onToggleFold && /* @__PURE__ */ jsx17(
1828
+ "button",
1829
+ {
1830
+ type: "button",
1831
+ className: "controller-group-fold",
1832
+ title: "Fold controller (alt/option: only this one)",
1833
+ "aria-label": "Fold controller",
1834
+ onClick: (e) => {
1835
+ var _a2;
1836
+ e.stopPropagation();
1837
+ (_a2 = data.onToggleFold) == null ? void 0 : _a2.call(data, { soloMode: e.altKey });
1838
+ },
1839
+ children: "\u2921"
1840
+ }
1841
+ )
1752
1842
  ] }),
1753
1843
  isCollapsible && /* @__PURE__ */ jsx17(
1754
1844
  "button",
@@ -1778,8 +1868,21 @@ var PIPE_TYPE_BADGES2 = {
1778
1868
  PipeCompose: "Compose",
1779
1869
  PipeImgGen: "ImgGen",
1780
1870
  PipeSearch: "Search",
1781
- PipeFunc: "Func"
1871
+ PipeFunc: "Func",
1872
+ PipeSequence: "Sequence",
1873
+ PipeParallel: "Parallel",
1874
+ PipeCondition: "Condition",
1875
+ PipeBatch: "Batch"
1876
+ };
1877
+ var CONTROLLER_TYPE_TABLE = {
1878
+ PipeSequence: true,
1879
+ PipeParallel: true,
1880
+ PipeCondition: true,
1881
+ PipeBatch: true
1782
1882
  };
1883
+ function isControllerType(pipeType) {
1884
+ return pipeType in CONTROLLER_TYPE_TABLE;
1885
+ }
1783
1886
  var STATUS_CONFIG = {
1784
1887
  succeeded: { color: "#50FA7B", label: "Succeeded" },
1785
1888
  failed: { color: "#FF5555", label: "Failed" },
@@ -1796,14 +1899,17 @@ function PipeCardBase({ data, children }) {
1796
1899
  const badge = getBadge(data.pipeType);
1797
1900
  const statusConfig = (_a = STATUS_CONFIG[data.status]) != null ? _a : STATUS_CONFIG.scheduled;
1798
1901
  const isRunning = data.status === "running";
1902
+ const isController = isControllerType(data.pipeType);
1799
1903
  const [inputsExpanded, setInputsExpanded] = useState2(false);
1800
1904
  const hasMany = data.inputs.length > MAX_VISIBLE_INPUTS;
1801
1905
  const visibleInputs = hasMany && !inputsExpanded ? data.inputs.slice(0, MAX_VISIBLE_INPUTS) : data.inputs;
1802
1906
  const hiddenCount = data.inputs.length - MAX_VISIBLE_INPUTS;
1803
1907
  const dirClass = data.direction === "TB" ? "pipe-card--tb" : "pipe-card--lr";
1804
- return /* @__PURE__ */ jsxs18("div", { className: `pipe-card ${dirClass}`, children: [
1908
+ const controllerClass = isController ? " pipe-card--controller" : "";
1909
+ const badgeClass = isController ? "pipe-card-badge pipe-card-badge--controller" : "pipe-card-badge";
1910
+ return /* @__PURE__ */ jsxs18("div", { className: `pipe-card ${dirClass}${controllerClass}`, children: [
1805
1911
  /* @__PURE__ */ jsxs18("div", { className: "pipe-card-header", children: [
1806
- /* @__PURE__ */ jsx18("span", { className: "pipe-card-badge", children: badge }),
1912
+ /* @__PURE__ */ jsx18("span", { className: badgeClass, children: badge }),
1807
1913
  /* @__PURE__ */ jsx18("span", { className: "pipe-card-code", title: data.pipeCode, children: data.pipeCode }),
1808
1914
  /* @__PURE__ */ jsx18(
1809
1915
  "span",
@@ -1819,6 +1925,21 @@ function PipeCardBase({ data, children }) {
1819
1925
  }
1820
1926
  )
1821
1927
  }
1928
+ ),
1929
+ data.onExpand && /* @__PURE__ */ jsx18(
1930
+ "button",
1931
+ {
1932
+ type: "button",
1933
+ className: "pipe-card-expand",
1934
+ title: "Expand controller (alt/option: only this one)",
1935
+ "aria-label": "Expand controller",
1936
+ onClick: (e) => {
1937
+ var _a2;
1938
+ e.stopPropagation();
1939
+ (_a2 = data.onExpand) == null ? void 0 : _a2.call(data, { soloMode: e.altKey });
1940
+ },
1941
+ children: "\u2922"
1942
+ }
1822
1943
  )
1823
1944
  ] }),
1824
1945
  data.description && /* @__PURE__ */ jsx18("span", { className: "pipe-card-description", title: data.description, children: data.description }),
@@ -1876,7 +1997,11 @@ var PIPE_CARD_REGISTRY = {
1876
1997
  PipeCompose: PipeCardBase,
1877
1998
  PipeImgGen: PipeCardBase,
1878
1999
  PipeSearch: PipeCardBase,
1879
- PipeFunc: PipeCardBase
2000
+ PipeFunc: PipeCardBase,
2001
+ PipeSequence: PipeCardBase,
2002
+ PipeParallel: PipeCardBase,
2003
+ PipeCondition: PipeCardBase,
2004
+ PipeBatch: PipeCardBase
1880
2005
  };
1881
2006
  function getPipeCardComponent(pipeType) {
1882
2007
  return PIPE_CARD_REGISTRY[pipeType];
@@ -1938,6 +2063,10 @@ function StuffNodeDetail({
1938
2063
  )
1939
2064
  ) });
1940
2065
  }
2066
+ function seedFoldedControllers(mode, controllerIds) {
2067
+ if (mode === FOLD_MODE.FOLDED) return new Set(controllerIds);
2068
+ return /* @__PURE__ */ new Set();
2069
+ }
1941
2070
  function cloneCachedNodes(nodes) {
1942
2071
  return nodes.map((n) => __spreadProps(__spreadValues({}, n), {
1943
2072
  position: __spreadValues({}, n.position),
@@ -1962,11 +2091,13 @@ function applyStatusOverrides(nodes, statusMap) {
1962
2091
  });
1963
2092
  }
1964
2093
  function GraphViewer(props) {
2094
+ var _a, _b, _c;
1965
2095
  const {
1966
2096
  graphspec,
1967
2097
  config = DEFAULT_GRAPH_CONFIG,
1968
2098
  initialDirection,
1969
2099
  initialShowControllers,
2100
+ initialFoldMode,
1970
2101
  hideToolbar = false,
1971
2102
  onNavigateToPipe,
1972
2103
  onStuffNodeClick,
@@ -1981,16 +2112,19 @@ function GraphViewer(props) {
1981
2112
  } = props;
1982
2113
  const [direction, setDirection] = React7.useState(
1983
2114
  () => {
1984
- var _a, _b;
1985
- return (_b = (_a = initialDirection != null ? initialDirection : config.direction) != null ? _a : DEFAULT_GRAPH_CONFIG.direction) != null ? _b : GRAPH_DIRECTION.TB;
2115
+ var _a2, _b2;
2116
+ return (_b2 = (_a2 = initialDirection != null ? initialDirection : config.direction) != null ? _a2 : DEFAULT_GRAPH_CONFIG.direction) != null ? _b2 : GRAPH_DIRECTION.TB;
1986
2117
  }
1987
2118
  );
1988
2119
  const [showControllers, setShowControllers] = React7.useState(
1989
2120
  () => {
1990
- var _a, _b;
1991
- return (_b = (_a = initialShowControllers != null ? initialShowControllers : config.showControllers) != null ? _a : DEFAULT_GRAPH_CONFIG.showControllers) != null ? _b : false;
2121
+ var _a2, _b2;
2122
+ return (_b2 = (_a2 = initialShowControllers != null ? initialShowControllers : config.showControllers) != null ? _a2 : DEFAULT_GRAPH_CONFIG.showControllers) != null ? _b2 : false;
1992
2123
  }
1993
2124
  );
2125
+ const effectiveFoldMode = (_b = (_a = initialFoldMode != null ? initialFoldMode : config.foldMode) != null ? _a : DEFAULT_GRAPH_CONFIG.foldMode) != null ? _b : FOLD_MODE.EXPANDED;
2126
+ const foldModeRef = React7.useRef(effectiveFoldMode);
2127
+ foldModeRef.current = effectiveFoldMode;
1994
2128
  const containerRef = React7.useRef(null);
1995
2129
  const [detailSelection, setDetailSelection] = React7.useState(null);
1996
2130
  const [conceptOverride, setConceptOverride] = React7.useState(null);
@@ -2004,10 +2138,10 @@ function GraphViewer(props) {
2004
2138
  setConceptOverride(null);
2005
2139
  }, [graphspec]);
2006
2140
  React7.useEffect(() => {
2007
- var _a;
2141
+ var _a2;
2008
2142
  const el = containerRef.current;
2009
2143
  if (!el) return;
2010
- const palette = (_a = config.paletteColors) != null ? _a : DEFAULT_GRAPH_CONFIG.paletteColors;
2144
+ const palette = (_a2 = config.paletteColors) != null ? _a2 : DEFAULT_GRAPH_CONFIG.paletteColors;
2011
2145
  if (!palette) return;
2012
2146
  for (const [cssVar, value] of Object.entries(palette)) {
2013
2147
  el.style.setProperty(cssVar, value);
@@ -2022,6 +2156,7 @@ function GraphViewer(props) {
2022
2156
  const [edges, setEdges, onEdgesChange] = useEdgesState([]);
2023
2157
  const reactFlowRef = React7.useRef(null);
2024
2158
  const initialDataRef = React7.useRef(null);
2159
+ const rawGraphDataRef = React7.useRef(null);
2025
2160
  const layoutCacheRef = React7.useRef(null);
2026
2161
  const [expandedControllers, setExpandedControllers] = React7.useState(/* @__PURE__ */ new Set());
2027
2162
  const toggleCollapse = React7.useCallback((controllerId) => {
@@ -2032,6 +2167,20 @@ function GraphViewer(props) {
2032
2167
  return next;
2033
2168
  });
2034
2169
  }, []);
2170
+ const [foldedControllers, setFoldedControllers] = React7.useState(/* @__PURE__ */ new Set());
2171
+ const toggleFold = React7.useCallback((controllerId, options) => {
2172
+ setFoldedControllers((prev) => {
2173
+ const next = new Set(prev);
2174
+ const shouldFold = !next.has(controllerId);
2175
+ const raw = rawGraphDataRef.current;
2176
+ const targets = !(options == null ? void 0 : options.soloMode) && (raw == null ? void 0 : raw.graphspec) && raw.analysis ? findCousinControllers(controllerId, raw.graphspec, raw.analysis.controllerNodeIds) : /* @__PURE__ */ new Set([controllerId]);
2177
+ for (const id of targets) {
2178
+ if (shouldFold) next.add(id);
2179
+ else next.delete(id);
2180
+ }
2181
+ return next;
2182
+ });
2183
+ }, []);
2035
2184
  const edgeType = config.edgeType || EDGE_TYPE.DEFAULT;
2036
2185
  const layoutConfig = React7.useMemo(
2037
2186
  () => ({ nodesep: config.nodesep, ranksep: config.ranksep }),
@@ -2051,6 +2200,13 @@ function GraphViewer(props) {
2051
2200
  expandedRef.current = expandedControllers;
2052
2201
  const toggleCollapseRef = React7.useRef(toggleCollapse);
2053
2202
  toggleCollapseRef.current = toggleCollapse;
2203
+ const foldedRef = React7.useRef(foldedControllers);
2204
+ foldedRef.current = foldedControllers;
2205
+ const toggleFoldRef = React7.useRef(toggleFold);
2206
+ toggleFoldRef.current = toggleFold;
2207
+ const isFirstFoldEffect = React7.useRef(true);
2208
+ const prevFoldSizeRef = React7.useRef(0);
2209
+ const skipNextFoldEffectRef = React7.useRef(false);
2054
2210
  const statusMapRef = React7.useRef(statusMap);
2055
2211
  statusMapRef.current = statusMap;
2056
2212
  React7.useEffect(() => {
@@ -2082,7 +2238,8 @@ function GraphViewer(props) {
2082
2238
  showControllersRef.current,
2083
2239
  expandedRef.current,
2084
2240
  toggleCollapseRef.current,
2085
- relayouted.controllerPositions
2241
+ relayouted.controllerPositions,
2242
+ toggleFoldRef.current
2086
2243
  );
2087
2244
  setNodes(
2088
2245
  applyStatusOverrides(
@@ -2116,16 +2273,18 @@ function GraphViewer(props) {
2116
2273
  showControllers,
2117
2274
  expandedControllers,
2118
2275
  toggleCollapse,
2119
- layoutCacheRef.current.controllerPositions
2276
+ layoutCacheRef.current.controllerPositions,
2277
+ toggleFold
2120
2278
  );
2121
2279
  setNodes(
2122
2280
  applyStatusOverrides(toAppNodes(hydrateLabels(withControllers.nodes)), statusMapRef.current)
2123
2281
  );
2124
2282
  setEdges(toAppEdges(withControllers.edges));
2125
- }, [showControllers, expandedControllers, toggleCollapse]);
2283
+ }, [showControllers, expandedControllers, toggleCollapse, toggleFold]);
2126
2284
  React7.useEffect(() => {
2127
2285
  if (!graphspec) {
2128
2286
  initialDataRef.current = null;
2287
+ rawGraphDataRef.current = null;
2129
2288
  layoutCacheRef.current = null;
2130
2289
  setNodes([]);
2131
2290
  setEdges([]);
@@ -2133,30 +2292,51 @@ function GraphViewer(props) {
2133
2292
  }
2134
2293
  let cancelled = false;
2135
2294
  setExpandedControllers(/* @__PURE__ */ new Set());
2295
+ expandedRef.current = /* @__PURE__ */ new Set();
2136
2296
  const { graphData, analysis } = buildGraph(graphspec, edgeType);
2137
- initialDataRef.current = {
2297
+ rawGraphDataRef.current = {
2138
2298
  nodes: graphData.nodes,
2139
2299
  edges: graphData.edges,
2140
- _analysis: analysis,
2300
+ analysis,
2301
+ graphspec
2302
+ };
2303
+ const seedSet = analysis ? seedFoldedControllers(foldModeRef.current, analysis.controllerNodeIds) : /* @__PURE__ */ new Set();
2304
+ setFoldedControllers(seedSet);
2305
+ foldedRef.current = seedSet;
2306
+ skipNextFoldEffectRef.current = seedSet.size > 0;
2307
+ prevFoldSizeRef.current = seedSet.size;
2308
+ const folded = seedSet.size > 0 && analysis ? applyFolds(
2309
+ { nodes: graphData.nodes, edges: graphData.edges },
2310
+ analysis,
2311
+ graphspec,
2312
+ seedSet,
2313
+ toggleFoldRef.current
2314
+ ) : { nodes: graphData.nodes, edges: graphData.edges, analysis };
2315
+ initialDataRef.current = {
2316
+ nodes: folded.nodes,
2317
+ edges: folded.edges,
2318
+ _analysis: folded.analysis,
2141
2319
  _graphspec: graphspec
2142
2320
  };
2143
2321
  (async () => {
2144
2322
  try {
2145
2323
  const currentDirection = directionRef.current;
2146
2324
  const currentLayoutConfig = layoutConfigRef.current;
2147
- const needsLayout = graphData.nodes.some(
2325
+ const needsLayout = folded.nodes.some(
2148
2326
  (n) => !n.position || n.position.x === 0 && n.position.y === 0
2149
2327
  );
2150
2328
  const layouted = needsLayout ? await getLayoutedElements(
2151
- graphData.nodes,
2152
- graphData.edges,
2329
+ folded.nodes,
2330
+ folded.edges,
2153
2331
  currentDirection,
2154
2332
  currentLayoutConfig,
2155
2333
  graphspec,
2156
- analysis
2157
- ) : __spreadProps(__spreadValues({}, graphData), {
2334
+ folded.analysis
2335
+ ) : {
2336
+ nodes: folded.nodes,
2337
+ edges: folded.edges,
2158
2338
  controllerPositions: {}
2159
- });
2339
+ };
2160
2340
  if (cancelled) return;
2161
2341
  layoutCacheRef.current = {
2162
2342
  nodes: layouted.nodes,
@@ -2167,11 +2347,12 @@ function GraphViewer(props) {
2167
2347
  cloneCachedNodes(layouted.nodes),
2168
2348
  layouted.edges,
2169
2349
  graphspec,
2170
- analysis,
2350
+ folded.analysis,
2171
2351
  showControllersRef.current,
2172
2352
  expandedRef.current,
2173
2353
  toggleCollapseRef.current,
2174
- layouted.controllerPositions
2354
+ layouted.controllerPositions,
2355
+ toggleFoldRef.current
2175
2356
  );
2176
2357
  setNodes(
2177
2358
  applyStatusOverrides(
@@ -2200,6 +2381,86 @@ function GraphViewer(props) {
2200
2381
  cancelled = true;
2201
2382
  };
2202
2383
  }, [graphspec, edgeType]);
2384
+ React7.useEffect(() => {
2385
+ if (isFirstFoldEffect.current) {
2386
+ isFirstFoldEffect.current = false;
2387
+ prevFoldSizeRef.current = foldedControllers.size;
2388
+ return;
2389
+ }
2390
+ if (skipNextFoldEffectRef.current) {
2391
+ skipNextFoldEffectRef.current = false;
2392
+ prevFoldSizeRef.current = foldedControllers.size;
2393
+ return;
2394
+ }
2395
+ const prevSize = prevFoldSizeRef.current;
2396
+ prevFoldSizeRef.current = foldedControllers.size;
2397
+ if (prevSize === 0 && foldedControllers.size === 0) return;
2398
+ if (!rawGraphDataRef.current || !rawGraphDataRef.current.analysis) return;
2399
+ const raw = rawGraphDataRef.current;
2400
+ const currentGraphspec = raw.graphspec;
2401
+ const currentAnalysis = raw.analysis;
2402
+ if (!currentGraphspec || !currentAnalysis) return;
2403
+ let cancelled = false;
2404
+ const folded = applyFolds(
2405
+ { nodes: raw.nodes, edges: raw.edges },
2406
+ currentAnalysis,
2407
+ currentGraphspec,
2408
+ foldedControllers,
2409
+ toggleFold
2410
+ );
2411
+ initialDataRef.current = {
2412
+ nodes: folded.nodes,
2413
+ edges: folded.edges,
2414
+ _analysis: folded.analysis,
2415
+ _graphspec: currentGraphspec
2416
+ };
2417
+ (async () => {
2418
+ try {
2419
+ const layouted = await getLayoutedElements(
2420
+ folded.nodes,
2421
+ folded.edges,
2422
+ directionRef.current,
2423
+ layoutConfigRef.current,
2424
+ currentGraphspec,
2425
+ folded.analysis
2426
+ );
2427
+ if (cancelled) return;
2428
+ layoutCacheRef.current = {
2429
+ nodes: layouted.nodes,
2430
+ edges: layouted.edges,
2431
+ controllerPositions: layouted.controllerPositions
2432
+ };
2433
+ const withControllers = applyControllers(
2434
+ cloneCachedNodes(layouted.nodes),
2435
+ layouted.edges,
2436
+ currentGraphspec,
2437
+ folded.analysis,
2438
+ showControllersRef.current,
2439
+ expandedRef.current,
2440
+ toggleCollapseRef.current,
2441
+ layouted.controllerPositions,
2442
+ toggleFoldRef.current
2443
+ );
2444
+ setNodes(
2445
+ applyStatusOverrides(
2446
+ toAppNodes(hydrateLabels(withControllers.nodes)),
2447
+ statusMapRef.current
2448
+ )
2449
+ );
2450
+ setEdges(toAppEdges(withControllers.edges));
2451
+ setTimeout(() => {
2452
+ if (!cancelled && reactFlowRef.current) {
2453
+ reactFlowRef.current.fitView({ padding: 0.1 });
2454
+ }
2455
+ }, 50);
2456
+ } catch (err) {
2457
+ console.error("[GraphViewer] ELK layout failed:", err);
2458
+ }
2459
+ })();
2460
+ return () => {
2461
+ cancelled = true;
2462
+ };
2463
+ }, [foldedControllers, toggleFold]);
2203
2464
  React7.useEffect(() => {
2204
2465
  if (!layoutCacheRef.current || !initialDataRef.current) return;
2205
2466
  const cachedNodes = cloneCachedNodes(layoutCacheRef.current.nodes);
@@ -2212,20 +2473,21 @@ function GraphViewer(props) {
2212
2473
  showControllersRef.current,
2213
2474
  expandedRef.current,
2214
2475
  toggleCollapseRef.current,
2215
- layoutCacheRef.current.controllerPositions
2476
+ layoutCacheRef.current.controllerPositions,
2477
+ toggleFoldRef.current
2216
2478
  );
2217
2479
  setNodes(applyStatusOverrides(toAppNodes(hydrateLabels(withControllers.nodes)), statusMap));
2218
2480
  setEdges(toAppEdges(withControllers.edges));
2219
2481
  }, [statusMap]);
2220
2482
  const onNodeClick = React7.useCallback(
2221
2483
  (event, node) => {
2222
- var _a;
2484
+ var _a2;
2223
2485
  const nodeData = node.data;
2224
2486
  onNodeSelect == null ? void 0 : onNodeSelect(node.id, nodeData, event);
2225
2487
  if (nodeData.isController || nodeData.isPipe) {
2226
2488
  const code = nodeData.pipeCode || nodeData.labelText;
2227
2489
  if (code && onNavigateToPipe) {
2228
- onNavigateToPipe(code, (_a = nodeData.pipeCardData) == null ? void 0 : _a.status);
2490
+ onNavigateToPipe(code, (_a2 = nodeData.pipeCardData) == null ? void 0 : _a2.status);
2229
2491
  }
2230
2492
  } else if (nodeData.isStuff && onStuffNodeClick && graphspec) {
2231
2493
  const digest = stuffDigestFromId(node.id);
@@ -2283,6 +2545,24 @@ function GraphViewer(props) {
2283
2545
  );
2284
2546
  const selectedSpecNode = (detailSelection == null ? void 0 : detailSelection.kind) === "pipe" && graphspec ? graphspec.nodes.find((n) => n.pipe_code === detailSelection.nodeData.pipeCode) : void 0;
2285
2547
  const detailOpen = detailSelection !== null || conceptOverride !== null;
2548
+ const rawAnalysis = (_c = rawGraphDataRef.current) == null ? void 0 : _c.analysis;
2549
+ const allControllerIds = rawAnalysis == null ? void 0 : rawAnalysis.controllerNodeIds;
2550
+ const foldAllProps = React7.useMemo(() => {
2551
+ if (!showControllers || !allControllerIds || allControllerIds.size === 0) {
2552
+ return {
2553
+ onFoldAll: void 0,
2554
+ onExpandAll: void 0,
2555
+ foldAllDisabled: false,
2556
+ expandAllDisabled: false
2557
+ };
2558
+ }
2559
+ return {
2560
+ onFoldAll: () => setFoldedControllers(new Set(allControllerIds)),
2561
+ onExpandAll: () => setFoldedControllers(/* @__PURE__ */ new Set()),
2562
+ foldAllDisabled: foldedControllers.size === allControllerIds.size,
2563
+ expandAllDisabled: foldedControllers.size === 0
2564
+ };
2565
+ }, [showControllers, allControllerIds, foldedControllers]);
2286
2566
  return /* @__PURE__ */ jsxs20("div", { ref: containerRef, className: "react-flow-container", children: [
2287
2567
  /* @__PURE__ */ jsx20(
2288
2568
  ReactFlow,
@@ -2351,17 +2631,21 @@ function GraphViewer(props) {
2351
2631
  showControllers,
2352
2632
  onShowControllersChange: setShowControllers,
2353
2633
  onZoomIn: () => {
2354
- var _a;
2355
- return (_a = reactFlowRef.current) == null ? void 0 : _a.zoomIn();
2634
+ var _a2;
2635
+ return (_a2 = reactFlowRef.current) == null ? void 0 : _a2.zoomIn();
2356
2636
  },
2357
2637
  onZoomOut: () => {
2358
- var _a;
2359
- return (_a = reactFlowRef.current) == null ? void 0 : _a.zoomOut();
2638
+ var _a2;
2639
+ return (_a2 = reactFlowRef.current) == null ? void 0 : _a2.zoomOut();
2360
2640
  },
2361
2641
  onFitView: () => {
2362
- var _a;
2363
- return (_a = reactFlowRef.current) == null ? void 0 : _a.fitView({ padding: 0.1 });
2642
+ var _a2;
2643
+ return (_a2 = reactFlowRef.current) == null ? void 0 : _a2.fitView({ padding: 0.1 });
2364
2644
  },
2645
+ onFoldAll: foldAllProps.onFoldAll,
2646
+ onExpandAll: foldAllProps.onExpandAll,
2647
+ foldAllDisabled: foldAllProps.foldAllDisabled,
2648
+ expandAllDisabled: foldAllProps.expandAllDisabled,
2365
2649
  rightOffset: detailOpen ? panelWidth : 0
2366
2650
  }
2367
2651
  )