@pipelex/mthds-ui 0.13.0 → 0.14.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.
@@ -4,14 +4,18 @@ import {
4
4
  MAX_VISIBLE_CONTROLLER_CHILDREN,
5
5
  applyControllers,
6
6
  applyFolds,
7
+ applyValidationDecorations,
8
+ buildChildToControllerMap,
7
9
  buildGraph,
10
+ buildValidationDecorations,
8
11
  findCousinControllers,
9
12
  getLayoutedElements,
10
13
  getPaletteForTheme,
11
14
  getPipeBlueprint,
12
15
  resolveConceptRef,
16
+ resolveIssueTargetNodeId,
13
17
  validateGraphSpec
14
- } from "../../chunk-XCZLRCEJ.js";
18
+ } from "../../chunk-V4FKGLKA.js";
15
19
  import {
16
20
  EDGE_TYPE,
17
21
  FOLD_MODE,
@@ -27,7 +31,7 @@ import {
27
31
  stuffDigestFromId,
28
32
  toolbarOrientation,
29
33
  toolbarSide
30
- } from "../../chunk-IMQ7MVYL.js";
34
+ } from "../../chunk-GS2KODND.js";
31
35
  import {
32
36
  __spreadProps,
33
37
  __spreadValues
@@ -2187,26 +2191,31 @@ function GraphToolbar({
2187
2191
  position,
2188
2192
  validationState,
2189
2193
  validationIssues = [],
2190
- onValidationIssueClick
2194
+ onValidationIssueClick,
2195
+ validationOpen = false,
2196
+ onValidationOpenChange
2191
2197
  }) {
2192
2198
  const themeToggleEnabled = themeMode !== void 0 && onThemeModeChange !== void 0;
2193
2199
  const orientation = toolbarOrientation(position);
2194
- const [validationOpen, setValidationOpen] = React8.useState(false);
2200
+ const setValidationOpen = onValidationOpenChange != null ? onValidationOpenChange : (() => {
2201
+ });
2202
+ const setValidationOpenRef = React8.useRef(setValidationOpen);
2203
+ setValidationOpenRef.current = setValidationOpen;
2195
2204
  const validationRef = React8.useRef(null);
2196
2205
  const validationPanelId = React8.useId();
2197
2206
  React8.useEffect(() => {
2198
- if (validationState === void 0) setValidationOpen(false);
2207
+ if (validationState === void 0) setValidationOpenRef.current(false);
2199
2208
  }, [validationState]);
2200
2209
  React8.useEffect(() => {
2201
2210
  if (!validationOpen) return;
2202
2211
  const onPointerDown = (event) => {
2203
2212
  const wrapper = validationRef.current;
2204
2213
  if (wrapper && event.target instanceof Node && !wrapper.contains(event.target)) {
2205
- setValidationOpen(false);
2214
+ setValidationOpenRef.current(false);
2206
2215
  }
2207
2216
  };
2208
2217
  const onKeyDown = (event) => {
2209
- if (event.key === "Escape") setValidationOpen(false);
2218
+ if (event.key === "Escape") setValidationOpenRef.current(false);
2210
2219
  };
2211
2220
  document.addEventListener("mousedown", onPointerDown);
2212
2221
  document.addEventListener("keydown", onKeyDown);
@@ -2231,7 +2240,7 @@ function GraphToolbar({
2231
2240
  {
2232
2241
  type: "button",
2233
2242
  className: `graph-toolbar-btn graph-toolbar-validation-btn graph-toolbar-validation-btn--${validationState}`,
2234
- onClick: () => setValidationOpen((open) => !open),
2243
+ onClick: () => setValidationOpen(!validationOpen),
2235
2244
  title: validationLabel(validationState, validationIssues.length),
2236
2245
  "aria-label": validationLabel(validationState, validationIssues.length),
2237
2246
  "aria-expanded": validationOpen,
@@ -2354,8 +2363,40 @@ function GraphToolbar({
2354
2363
  ] }) });
2355
2364
  }
2356
2365
 
2366
+ // src/graph/react/nodes/NodeValidationBadge.tsx
2367
+ import { jsx as jsx19 } from "react/jsx-runtime";
2368
+ function NodeValidationBadge({
2369
+ validation,
2370
+ onClick
2371
+ }) {
2372
+ const label = `${validation.count} validation ${validation.count === 1 ? "issue" : "issues"}`;
2373
+ const tooltip = validation.lines.join("\n");
2374
+ const className = `node-validation-badge node-validation-badge--${validation.severity}`;
2375
+ const text = validation.count > 99 ? "99+" : String(validation.count);
2376
+ if (!onClick) {
2377
+ return /* @__PURE__ */ jsx19("span", { className, title: tooltip, "aria-label": label, children: text });
2378
+ }
2379
+ return /* @__PURE__ */ jsx19(
2380
+ "button",
2381
+ {
2382
+ type: "button",
2383
+ className,
2384
+ title: tooltip,
2385
+ "aria-label": `${label} \u2014 open validation panel`,
2386
+ onClick: (e) => {
2387
+ e.stopPropagation();
2388
+ onClick();
2389
+ },
2390
+ children: text
2391
+ }
2392
+ );
2393
+ }
2394
+ function validationRingClass(validation) {
2395
+ return validation ? ` node-validation-ring--${validation.severity}` : "";
2396
+ }
2397
+
2357
2398
  // src/graph/react/nodes/controller/ControllerGroupNode.tsx
2358
- import { jsx as jsx19, jsxs as jsxs19 } from "react/jsx-runtime";
2399
+ import { jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
2359
2400
  var CONTROLLER_CONFIG = {
2360
2401
  PipeSequence: { badge: "Sequence", icon: "\u2192" },
2361
2402
  PipeParallel: { badge: "Parallel", icon: "//" },
@@ -2377,12 +2418,12 @@ function ControllerGroupNode({ data }) {
2377
2418
  const modifier = getControllerModifier(data.pipeType);
2378
2419
  const isCollapsible = (data.pipeType === "PipeParallel" || data.pipeType === "PipeBatch") && ((_a = data.childCount) != null ? _a : 0) > MAX_VISIBLE_CONTROLLER_CHILDREN;
2379
2420
  const hiddenCount = isCollapsible ? ((_b = data.childCount) != null ? _b : 0) - MAX_VISIBLE_CONTROLLER_CHILDREN : 0;
2380
- return /* @__PURE__ */ jsxs19("div", { className: `controller-group-node ${modifier}`, children: [
2421
+ return /* @__PURE__ */ jsxs19("div", { className: `controller-group-node ${modifier}${validationRingClass(data.validation)}`, children: [
2381
2422
  /* @__PURE__ */ jsxs19("div", { className: "controller-group-header", children: [
2382
- /* @__PURE__ */ jsx19("span", { className: "controller-group-icon", children: config.icon }),
2383
- /* @__PURE__ */ jsx19("span", { className: "controller-group-badge", children: config.badge }),
2384
- data.label && /* @__PURE__ */ jsx19("span", { className: "controller-group-label", children: data.label }),
2385
- data.onToggleFold && /* @__PURE__ */ jsx19(
2423
+ /* @__PURE__ */ jsx20("span", { className: "controller-group-icon", children: config.icon }),
2424
+ /* @__PURE__ */ jsx20("span", { className: "controller-group-badge", children: config.badge }),
2425
+ data.label && /* @__PURE__ */ jsx20("span", { className: "controller-group-label", children: data.label }),
2426
+ data.onToggleFold && /* @__PURE__ */ jsx20(
2386
2427
  "button",
2387
2428
  {
2388
2429
  type: "button",
@@ -2396,9 +2437,10 @@ function ControllerGroupNode({ data }) {
2396
2437
  },
2397
2438
  children: "\u2921"
2398
2439
  }
2399
- )
2440
+ ),
2441
+ data.validation && /* @__PURE__ */ jsx20(NodeValidationBadge, { validation: data.validation, onClick: data.onValidationBadgeClick })
2400
2442
  ] }),
2401
- isCollapsible && /* @__PURE__ */ jsx19(
2443
+ isCollapsible && /* @__PURE__ */ jsx20(
2402
2444
  "button",
2403
2445
  {
2404
2446
  className: "controller-group-collapse",
@@ -2419,7 +2461,7 @@ import { Handle, Position } from "@xyflow/react";
2419
2461
 
2420
2462
  // src/graph/react/nodes/pipe/PipeCardBase.tsx
2421
2463
  import { useState as useState2 } from "react";
2422
- import { jsx as jsx20, jsxs as jsxs20 } from "react/jsx-runtime";
2464
+ import { jsx as jsx21, jsxs as jsxs20 } from "react/jsx-runtime";
2423
2465
  var PIPE_TYPE_BADGES2 = {
2424
2466
  PipeLLM: "LLM",
2425
2467
  PipeExtract: "Extract",
@@ -2474,94 +2516,102 @@ function PipeCardBase({ data, children }) {
2474
2516
  const signatureClass = isSignature ? " pipe-card--signature" : "";
2475
2517
  const staticClass = isStatic ? " pipe-card--static" : "";
2476
2518
  const badgeClass = isController ? "pipe-card-badge pipe-card-badge--controller" : isSignature ? "pipe-card-badge pipe-card-badge--signature" : "pipe-card-badge";
2477
- return /* @__PURE__ */ jsxs20("div", { className: `pipe-card ${dirClass}${controllerClass}${signatureClass}${staticClass}`, children: [
2478
- /* @__PURE__ */ jsxs20("div", { className: "pipe-card-header", children: [
2479
- /* @__PURE__ */ jsx20("span", { className: badgeClass, children: badge }),
2480
- /* @__PURE__ */ jsx20("span", { className: "pipe-card-code", title: data.pipeCode, children: data.pipeCode }),
2481
- !isStatic && /* @__PURE__ */ jsx20(
2482
- "span",
2483
- {
2484
- className: "pipe-card-status",
2485
- style: { color: statusConfig.color },
2486
- title: statusConfig.label,
2487
- children: /* @__PURE__ */ jsx20(
2519
+ const ringClass = validationRingClass(data.validation);
2520
+ return /* @__PURE__ */ jsxs20(
2521
+ "div",
2522
+ {
2523
+ className: `pipe-card ${dirClass}${controllerClass}${signatureClass}${staticClass}${ringClass}`,
2524
+ children: [
2525
+ /* @__PURE__ */ jsxs20("div", { className: "pipe-card-header", children: [
2526
+ /* @__PURE__ */ jsx21("span", { className: badgeClass, children: badge }),
2527
+ /* @__PURE__ */ jsx21("span", { className: "pipe-card-code", title: data.pipeCode, children: data.pipeCode }),
2528
+ !isStatic && /* @__PURE__ */ jsx21(
2488
2529
  "span",
2489
2530
  {
2490
- className: `pipe-card-status-dot ${isRunning ? "pipe-card-status-dot--pulse" : ""}`,
2491
- style: { background: statusConfig.color }
2531
+ className: "pipe-card-status",
2532
+ style: { color: statusConfig.color },
2533
+ title: statusConfig.label,
2534
+ children: /* @__PURE__ */ jsx21(
2535
+ "span",
2536
+ {
2537
+ className: `pipe-card-status-dot ${isRunning ? "pipe-card-status-dot--pulse" : ""}`,
2538
+ style: { background: statusConfig.color }
2539
+ }
2540
+ )
2492
2541
  }
2493
- )
2494
- }
2495
- ),
2496
- data.onExpand && /* @__PURE__ */ jsx20(
2497
- "button",
2498
- {
2499
- type: "button",
2500
- className: "pipe-card-expand",
2501
- title: "Expand controller (alt/option: only this one)",
2502
- "aria-label": "Expand controller",
2503
- onClick: (e) => {
2504
- var _a2;
2505
- e.stopPropagation();
2506
- (_a2 = data.onExpand) == null ? void 0 : _a2.call(data, { soloMode: e.altKey });
2507
- },
2508
- children: "\u2922"
2509
- }
2510
- )
2511
- ] }),
2512
- (outcome || batchMultiplicity) && /* @__PURE__ */ jsxs20("div", { className: "pipe-card-annotations", children: [
2513
- outcome && /* @__PURE__ */ jsxs20("span", { className: "pipe-card-annotation pipe-card-annotation--outcome", children: [
2514
- "outcome: ",
2515
- outcome
2516
- ] }),
2517
- batchMultiplicity && /* @__PURE__ */ jsx20("span", { className: "pipe-card-annotation pipe-card-annotation--batch", children: batchMultiplicity })
2518
- ] }),
2519
- data.description && /* @__PURE__ */ jsx20("span", { className: "pipe-card-description", title: data.description, children: data.description }),
2520
- data.inputs.length > 0 && /* @__PURE__ */ jsxs20("div", { className: "pipe-card-io", children: [
2521
- /* @__PURE__ */ jsx20("span", { className: "pipe-card-io-label", children: "INPUTS" }),
2522
- /* @__PURE__ */ jsxs20("div", { className: "pipe-card-io-pills", children: [
2523
- visibleInputs.map((input) => /* @__PURE__ */ jsxs20(
2524
- "span",
2525
- {
2526
- className: "pipe-card-io-pill",
2527
- title: `${input.name}: ${input.concept}`,
2528
- children: [
2529
- /* @__PURE__ */ jsx20("span", { className: "pipe-card-io-pill-name", children: input.name }),
2530
- /* @__PURE__ */ jsx20("span", { className: "pipe-card-io-pill-concept", children: input.concept })
2531
- ]
2532
- },
2533
- input.name
2534
- )),
2535
- hasMany && /* @__PURE__ */ jsx20(
2536
- "button",
2537
- {
2538
- className: "pipe-card-io-more",
2539
- onClick: (e) => {
2540
- e.stopPropagation();
2541
- setInputsExpanded((v) => !v);
2542
+ ),
2543
+ data.onExpand && /* @__PURE__ */ jsx21(
2544
+ "button",
2545
+ {
2546
+ type: "button",
2547
+ className: "pipe-card-expand",
2548
+ title: "Expand controller (alt/option: only this one)",
2549
+ "aria-label": "Expand controller",
2550
+ onClick: (e) => {
2551
+ var _a2;
2552
+ e.stopPropagation();
2553
+ (_a2 = data.onExpand) == null ? void 0 : _a2.call(data, { soloMode: e.altKey });
2554
+ },
2555
+ children: "\u2922"
2556
+ }
2557
+ ),
2558
+ data.validation && /* @__PURE__ */ jsx21(NodeValidationBadge, { validation: data.validation, onClick: data.onValidationBadgeClick })
2559
+ ] }),
2560
+ (outcome || batchMultiplicity) && /* @__PURE__ */ jsxs20("div", { className: "pipe-card-annotations", children: [
2561
+ outcome && /* @__PURE__ */ jsxs20("span", { className: "pipe-card-annotation pipe-card-annotation--outcome", children: [
2562
+ "outcome: ",
2563
+ outcome
2564
+ ] }),
2565
+ batchMultiplicity && /* @__PURE__ */ jsx21("span", { className: "pipe-card-annotation pipe-card-annotation--batch", children: batchMultiplicity })
2566
+ ] }),
2567
+ data.description && /* @__PURE__ */ jsx21("span", { className: "pipe-card-description", title: data.description, children: data.description }),
2568
+ data.inputs.length > 0 && /* @__PURE__ */ jsxs20("div", { className: "pipe-card-io", children: [
2569
+ /* @__PURE__ */ jsx21("span", { className: "pipe-card-io-label", children: "INPUTS" }),
2570
+ /* @__PURE__ */ jsxs20("div", { className: "pipe-card-io-pills", children: [
2571
+ visibleInputs.map((input) => /* @__PURE__ */ jsxs20(
2572
+ "span",
2573
+ {
2574
+ className: "pipe-card-io-pill",
2575
+ title: `${input.name}: ${input.concept}`,
2576
+ children: [
2577
+ /* @__PURE__ */ jsx21("span", { className: "pipe-card-io-pill-name", children: input.name }),
2578
+ /* @__PURE__ */ jsx21("span", { className: "pipe-card-io-pill-concept", children: input.concept })
2579
+ ]
2580
+ },
2581
+ input.name
2582
+ )),
2583
+ hasMany && /* @__PURE__ */ jsx21(
2584
+ "button",
2585
+ {
2586
+ className: "pipe-card-io-more",
2587
+ onClick: (e) => {
2588
+ e.stopPropagation();
2589
+ setInputsExpanded((v) => !v);
2590
+ },
2591
+ children: inputsExpanded ? "show less" : `+${hiddenCount} more`
2592
+ }
2593
+ )
2594
+ ] })
2595
+ ] }),
2596
+ data.outputs.length > 0 && /* @__PURE__ */ jsxs20("div", { className: "pipe-card-io", children: [
2597
+ /* @__PURE__ */ jsx21("span", { className: "pipe-card-io-label", children: "OUTPUT" }),
2598
+ /* @__PURE__ */ jsx21("div", { className: "pipe-card-io-pills", children: data.outputs.map((output) => /* @__PURE__ */ jsxs20(
2599
+ "span",
2600
+ {
2601
+ className: "pipe-card-io-pill",
2602
+ title: `${output.name}: ${output.concept}`,
2603
+ children: [
2604
+ /* @__PURE__ */ jsx21("span", { className: "pipe-card-io-pill-name", children: output.name }),
2605
+ /* @__PURE__ */ jsx21("span", { className: "pipe-card-io-pill-concept", children: output.concept })
2606
+ ]
2542
2607
  },
2543
- children: inputsExpanded ? "show less" : `+${hiddenCount} more`
2544
- }
2545
- )
2546
- ] })
2547
- ] }),
2548
- data.outputs.length > 0 && /* @__PURE__ */ jsxs20("div", { className: "pipe-card-io", children: [
2549
- /* @__PURE__ */ jsx20("span", { className: "pipe-card-io-label", children: "OUTPUT" }),
2550
- /* @__PURE__ */ jsx20("div", { className: "pipe-card-io-pills", children: data.outputs.map((output) => /* @__PURE__ */ jsxs20(
2551
- "span",
2552
- {
2553
- className: "pipe-card-io-pill",
2554
- title: `${output.name}: ${output.concept}`,
2555
- children: [
2556
- /* @__PURE__ */ jsx20("span", { className: "pipe-card-io-pill-name", children: output.name }),
2557
- /* @__PURE__ */ jsx20("span", { className: "pipe-card-io-pill-concept", children: output.concept })
2558
- ]
2559
- },
2560
- output.name
2561
- )) })
2562
- ] }),
2563
- children
2564
- ] });
2608
+ output.name
2609
+ )) })
2610
+ ] }),
2611
+ children
2612
+ ]
2613
+ }
2614
+ );
2565
2615
  }
2566
2616
 
2567
2617
  // src/graph/react/nodes/pipe/pipeCardRegistry.ts
@@ -2584,11 +2634,11 @@ function getPipeCardComponent(pipeType) {
2584
2634
  }
2585
2635
 
2586
2636
  // src/graph/react/nodes/pipe/PipeCardNode.tsx
2587
- import { Fragment as Fragment13, jsx as jsx21, jsxs as jsxs21 } from "react/jsx-runtime";
2637
+ import { Fragment as Fragment13, jsx as jsx22, jsxs as jsxs21 } from "react/jsx-runtime";
2588
2638
  function PipeCardNode({ data }) {
2589
2639
  var _a;
2590
2640
  const Card = (_a = getPipeCardComponent(data.pipeType)) != null ? _a : PipeCardBase;
2591
- return /* @__PURE__ */ jsx21(Card, { data });
2641
+ return /* @__PURE__ */ jsx22(Card, { data });
2592
2642
  }
2593
2643
  function PipeCardRFNode({
2594
2644
  data,
@@ -2598,14 +2648,14 @@ function PipeCardRFNode({
2598
2648
  const payload = data.pipeCardData;
2599
2649
  if (!payload) return null;
2600
2650
  return /* @__PURE__ */ jsxs21(Fragment13, { children: [
2601
- /* @__PURE__ */ jsx21(Handle, { type: "target", position: targetPosition }),
2602
- /* @__PURE__ */ jsx21(PipeCardNode, { data: payload }),
2603
- /* @__PURE__ */ jsx21(Handle, { type: "source", position: sourcePosition })
2651
+ /* @__PURE__ */ jsx22(Handle, { type: "target", position: targetPosition }),
2652
+ /* @__PURE__ */ jsx22(PipeCardNode, { data: payload }),
2653
+ /* @__PURE__ */ jsx22(Handle, { type: "source", position: sourcePosition })
2604
2654
  ] });
2605
2655
  }
2606
2656
 
2607
2657
  // src/graph/react/viewer/GraphViewer.tsx
2608
- import { Fragment as Fragment14, jsx as jsx22, jsxs as jsxs22 } from "react/jsx-runtime";
2658
+ import { Fragment as Fragment14, jsx as jsx23, jsxs as jsxs22 } from "react/jsx-runtime";
2609
2659
  var nodeTypes = __spreadProps(__spreadValues({}, controllerNodeTypes), {
2610
2660
  pipeCard: PipeCardRFNode
2611
2661
  });
@@ -2619,7 +2669,7 @@ function StuffNodeDetail({
2619
2669
  }) {
2620
2670
  const conceptInfo = stuffData.concept && graphspec ? resolveConceptRef(graphspec, stuffData.concept) : void 0;
2621
2671
  const isDryRun = graphSpecMode(graphspec) === GRAPH_SPEC_MODE.DRY;
2622
- return /* @__PURE__ */ jsx22(Fragment14, { children: conceptInfo ? /* @__PURE__ */ jsx22(
2672
+ return /* @__PURE__ */ jsx23(Fragment14, { children: conceptInfo ? /* @__PURE__ */ jsx23(
2623
2673
  ConceptDetailPanel,
2624
2674
  {
2625
2675
  concept: conceptInfo,
@@ -2630,9 +2680,9 @@ function StuffNodeDetail({
2630
2680
  canEmbedPdf,
2631
2681
  onOpenExternally
2632
2682
  }
2633
- ) : isDryRun ? /* @__PURE__ */ jsx22("div", { className: "detail-not-available", children: "Dry run data hidden" }) : (
2683
+ ) : isDryRun ? /* @__PURE__ */ jsx23("div", { className: "detail-not-available", children: "Dry run data hidden" }) : (
2634
2684
  /* Fallback: just show the StuffViewer if no concept info */
2635
- /* @__PURE__ */ jsx22(
2685
+ /* @__PURE__ */ jsx23(
2636
2686
  StuffViewer,
2637
2687
  {
2638
2688
  stuff: stuffData,
@@ -2658,6 +2708,9 @@ function resolveToolbarPosition(positionProp, configPosition) {
2658
2708
  var _a;
2659
2709
  return (_a = positionProp != null ? positionProp : configPosition) != null ? _a : DEFAULT_GRAPH_CONFIG.toolbarPosition;
2660
2710
  }
2711
+ function stripFlash(className) {
2712
+ return (className == null ? void 0 : className.replace(/\s*\bnode-validation-flash\b/, "")) || void 0;
2713
+ }
2661
2714
  function cloneCachedNodes(nodes) {
2662
2715
  return nodes.map((n) => __spreadProps(__spreadValues({}, n), {
2663
2716
  position: __spreadValues({}, n.position),
@@ -2839,6 +2892,84 @@ function GraphViewer(props) {
2839
2892
  const skipNextFoldEffectRef = React10.useRef(false);
2840
2893
  const statusMapRef = React10.useRef(statusMap);
2841
2894
  statusMapRef.current = statusMap;
2895
+ const validationIssuesRef = React10.useRef(validationIssues);
2896
+ validationIssuesRef.current = validationIssues;
2897
+ const [validationOpen, setValidationOpen] = React10.useState(false);
2898
+ const openValidationPanel = React10.useCallback(() => setValidationOpen(true), []);
2899
+ const validationWidgetAvailable = validationState !== void 0 && !hideToolbar;
2900
+ const validationWidgetAvailableRef = React10.useRef(validationWidgetAvailable);
2901
+ validationWidgetAvailableRef.current = validationWidgetAvailable;
2902
+ const decorateNodes = React10.useCallback(
2903
+ (nodes2) => {
2904
+ var _a2, _b2;
2905
+ const raw = rawGraphDataRef.current;
2906
+ const decorations = buildValidationDecorations(
2907
+ validationIssuesRef.current,
2908
+ (_a2 = raw == null ? void 0 : raw.graphspec) != null ? _a2 : null,
2909
+ (_b2 = raw == null ? void 0 : raw.childToCtrl) != null ? _b2 : {},
2910
+ foldedRef.current
2911
+ );
2912
+ return applyValidationDecorations(
2913
+ applyStatusOverrides(toAppNodes(hydrateLabels(nodes2)), statusMapRef.current),
2914
+ decorations,
2915
+ validationWidgetAvailableRef.current ? openValidationPanel : void 0
2916
+ );
2917
+ },
2918
+ [openValidationPanel]
2919
+ );
2920
+ const nodesRef = React10.useRef([]);
2921
+ nodesRef.current = nodes;
2922
+ const onValidationIssueClickRef = React10.useRef(onValidationIssueClick);
2923
+ onValidationIssueClickRef.current = onValidationIssueClick;
2924
+ const flashTimeoutRef = React10.useRef(null);
2925
+ React10.useEffect(
2926
+ () => () => {
2927
+ if (flashTimeoutRef.current) clearTimeout(flashTimeoutRef.current);
2928
+ },
2929
+ []
2930
+ );
2931
+ const handleValidationIssueClick = React10.useCallback(
2932
+ (index, issue) => {
2933
+ var _a2, _b2;
2934
+ (_a2 = onValidationIssueClickRef.current) == null ? void 0 : _a2.call(onValidationIssueClickRef, index, issue);
2935
+ const raw = rawGraphDataRef.current;
2936
+ if (!(raw == null ? void 0 : raw.graphspec) || !raw.analysis) return;
2937
+ const targetId = resolveIssueTargetNodeId(
2938
+ issue,
2939
+ raw.graphspec,
2940
+ raw.childToCtrl,
2941
+ foldedRef.current,
2942
+ new Set(nodesRef.current.map((n) => n.id))
2943
+ );
2944
+ if (!targetId) return;
2945
+ void ((_b2 = reactFlowRef.current) == null ? void 0 : _b2.fitView({
2946
+ nodes: [{ id: targetId }],
2947
+ duration: 500,
2948
+ padding: 0.4
2949
+ }));
2950
+ if (flashTimeoutRef.current) clearTimeout(flashTimeoutRef.current);
2951
+ setNodes(
2952
+ (nds) => nds.map((n) => {
2953
+ const base = stripFlash(n.className);
2954
+ if (n.id === targetId) {
2955
+ return __spreadProps(__spreadValues({}, n), {
2956
+ className: base ? `${base} node-validation-flash` : "node-validation-flash"
2957
+ });
2958
+ }
2959
+ return base === n.className ? n : __spreadProps(__spreadValues({}, n), { className: base });
2960
+ })
2961
+ );
2962
+ flashTimeoutRef.current = setTimeout(() => {
2963
+ setNodes(
2964
+ (nds) => nds.map((n) => {
2965
+ const base = stripFlash(n.className);
2966
+ return base === n.className ? n : __spreadProps(__spreadValues({}, n), { className: base });
2967
+ })
2968
+ );
2969
+ }, 1800);
2970
+ },
2971
+ [setNodes]
2972
+ );
2842
2973
  React10.useEffect(() => {
2843
2974
  if (!initialDataRef.current) return;
2844
2975
  let cancelled = false;
@@ -2858,7 +2989,8 @@ function GraphViewer(props) {
2858
2989
  layoutCacheRef.current = {
2859
2990
  nodes: relayouted.nodes,
2860
2991
  edges: relayouted.edges,
2861
- controllerPositions: relayouted.controllerPositions
2992
+ controllerPositions: relayouted.controllerPositions,
2993
+ graphspec: data._graphspec
2862
2994
  };
2863
2995
  const withControllers = applyControllers(
2864
2996
  cloneCachedNodes(relayouted.nodes),
@@ -2871,12 +3003,7 @@ function GraphViewer(props) {
2871
3003
  relayouted.controllerPositions,
2872
3004
  toggleFoldRef.current
2873
3005
  );
2874
- setNodes(
2875
- applyStatusOverrides(
2876
- toAppNodes(hydrateLabels(withControllers.nodes)),
2877
- statusMapRef.current
2878
- )
2879
- );
3006
+ setNodes(decorateNodes(withControllers.nodes));
2880
3007
  setEdges(toAppEdges(withControllers.edges));
2881
3008
  setTimeout(() => {
2882
3009
  if (!cancelled && reactFlowRef.current) {
@@ -2893,6 +3020,7 @@ function GraphViewer(props) {
2893
3020
  }, [direction, layoutConfig]);
2894
3021
  React10.useEffect(() => {
2895
3022
  if (!layoutCacheRef.current || !initialDataRef.current) return;
3023
+ if (layoutCacheRef.current.graphspec !== initialDataRef.current._graphspec) return;
2896
3024
  const cachedNodes = cloneCachedNodes(layoutCacheRef.current.nodes);
2897
3025
  const cachedEdges = layoutCacheRef.current.edges;
2898
3026
  const withControllers = applyControllers(
@@ -2906,11 +3034,9 @@ function GraphViewer(props) {
2906
3034
  layoutCacheRef.current.controllerPositions,
2907
3035
  toggleFold
2908
3036
  );
2909
- setNodes(
2910
- applyStatusOverrides(toAppNodes(hydrateLabels(withControllers.nodes)), statusMapRef.current)
2911
- );
3037
+ setNodes(decorateNodes(withControllers.nodes));
2912
3038
  setEdges(toAppEdges(withControllers.edges));
2913
- }, [showControllers, expandedControllers, toggleCollapse, toggleFold]);
3039
+ }, [showControllers, expandedControllers, toggleCollapse, toggleFold, decorateNodes]);
2914
3040
  React10.useEffect(() => {
2915
3041
  if (!graphspec) {
2916
3042
  initialDataRef.current = null;
@@ -2928,7 +3054,8 @@ function GraphViewer(props) {
2928
3054
  nodes: graphData.nodes,
2929
3055
  edges: graphData.edges,
2930
3056
  analysis,
2931
- graphspec
3057
+ graphspec,
3058
+ childToCtrl: analysis ? buildChildToControllerMap(graphspec, analysis) : {}
2932
3059
  };
2933
3060
  const seedSet = analysis ? seedFoldedControllers(foldModeRef.current, analysis.controllerNodeIds) : /* @__PURE__ */ new Set();
2934
3061
  setFoldedControllers(seedSet);
@@ -2975,7 +3102,8 @@ function GraphViewer(props) {
2975
3102
  layoutCacheRef.current = {
2976
3103
  nodes: layouted.nodes,
2977
3104
  edges: layouted.edges,
2978
- controllerPositions: layouted.controllerPositions
3105
+ controllerPositions: layouted.controllerPositions,
3106
+ graphspec
2979
3107
  };
2980
3108
  const withControllers = applyControllers(
2981
3109
  cloneCachedNodes(layouted.nodes),
@@ -2988,12 +3116,7 @@ function GraphViewer(props) {
2988
3116
  layouted.controllerPositions,
2989
3117
  toggleFoldRef.current
2990
3118
  );
2991
- setNodes(
2992
- applyStatusOverrides(
2993
- toAppNodes(hydrateLabels(withControllers.nodes)),
2994
- statusMapRef.current
2995
- )
2996
- );
3119
+ setNodes(decorateNodes(withControllers.nodes));
2997
3120
  setEdges(toAppEdges(withControllers.edges));
2998
3121
  setTimeout(() => {
2999
3122
  if (!cancelled && reactFlowRef.current) {
@@ -3062,7 +3185,8 @@ function GraphViewer(props) {
3062
3185
  layoutCacheRef.current = {
3063
3186
  nodes: layouted.nodes,
3064
3187
  edges: layouted.edges,
3065
- controllerPositions: layouted.controllerPositions
3188
+ controllerPositions: layouted.controllerPositions,
3189
+ graphspec: currentGraphspec
3066
3190
  };
3067
3191
  const withControllers = applyControllers(
3068
3192
  cloneCachedNodes(layouted.nodes),
@@ -3075,12 +3199,7 @@ function GraphViewer(props) {
3075
3199
  layouted.controllerPositions,
3076
3200
  toggleFoldRef.current
3077
3201
  );
3078
- setNodes(
3079
- applyStatusOverrides(
3080
- toAppNodes(hydrateLabels(withControllers.nodes)),
3081
- statusMapRef.current
3082
- )
3083
- );
3202
+ setNodes(decorateNodes(withControllers.nodes));
3084
3203
  setEdges(toAppEdges(withControllers.edges));
3085
3204
  setTimeout(() => {
3086
3205
  if (!cancelled && reactFlowRef.current) {
@@ -3097,6 +3216,7 @@ function GraphViewer(props) {
3097
3216
  }, [foldedControllers, toggleFold]);
3098
3217
  React10.useEffect(() => {
3099
3218
  if (!layoutCacheRef.current || !initialDataRef.current) return;
3219
+ if (layoutCacheRef.current.graphspec !== initialDataRef.current._graphspec) return;
3100
3220
  const cachedNodes = cloneCachedNodes(layoutCacheRef.current.nodes);
3101
3221
  const cachedEdges = layoutCacheRef.current.edges;
3102
3222
  const withControllers = applyControllers(
@@ -3110,9 +3230,26 @@ function GraphViewer(props) {
3110
3230
  layoutCacheRef.current.controllerPositions,
3111
3231
  toggleFoldRef.current
3112
3232
  );
3113
- setNodes(applyStatusOverrides(toAppNodes(hydrateLabels(withControllers.nodes)), statusMap));
3233
+ setNodes(decorateNodes(withControllers.nodes));
3114
3234
  setEdges(toAppEdges(withControllers.edges));
3115
- }, [statusMap]);
3235
+ }, [statusMap, decorateNodes]);
3236
+ React10.useEffect(() => {
3237
+ if (!layoutCacheRef.current || !initialDataRef.current) return;
3238
+ if (layoutCacheRef.current.graphspec !== initialDataRef.current._graphspec) return;
3239
+ const cachedNodes = cloneCachedNodes(layoutCacheRef.current.nodes);
3240
+ const withControllers = applyControllers(
3241
+ cachedNodes,
3242
+ layoutCacheRef.current.edges,
3243
+ initialDataRef.current._graphspec,
3244
+ initialDataRef.current._analysis,
3245
+ showControllersRef.current,
3246
+ expandedRef.current,
3247
+ toggleCollapseRef.current,
3248
+ layoutCacheRef.current.controllerPositions,
3249
+ toggleFoldRef.current
3250
+ );
3251
+ setNodes(decorateNodes(withControllers.nodes));
3252
+ }, [validationIssues, validationWidgetAvailable, decorateNodes]);
3116
3253
  const onNodeClick = React10.useCallback(
3117
3254
  (event, node) => {
3118
3255
  var _a2;
@@ -3226,7 +3363,7 @@ function GraphViewer(props) {
3226
3363
  proOptions: { hideAttribution: true },
3227
3364
  panActivationKeyCode: null,
3228
3365
  children: [
3229
- /* @__PURE__ */ jsx22(
3366
+ /* @__PURE__ */ jsx23(
3230
3367
  Background,
3231
3368
  {
3232
3369
  variant: BackgroundVariant.Dots,
@@ -3235,7 +3372,7 @@ function GraphViewer(props) {
3235
3372
  color: "var(--color-bg-dots)"
3236
3373
  }
3237
3374
  ),
3238
- !hideToolbar && /* @__PURE__ */ jsx22(
3375
+ !hideToolbar && /* @__PURE__ */ jsx23(
3239
3376
  GraphToolbar,
3240
3377
  {
3241
3378
  direction,
@@ -3264,7 +3401,9 @@ function GraphViewer(props) {
3264
3401
  position: effectiveToolbarPosition,
3265
3402
  validationState,
3266
3403
  validationIssues,
3267
- onValidationIssueClick
3404
+ onValidationIssueClick: onValidationIssueClick ? handleValidationIssueClick : void 0,
3405
+ validationOpen,
3406
+ onValidationOpenChange: setValidationOpen
3268
3407
  }
3269
3408
  )
3270
3409
  ]
@@ -3279,14 +3418,14 @@ function GraphViewer(props) {
3279
3418
  isDragging: isPanelDragging,
3280
3419
  onResizeHandleMouseDown: onResizeMouseDown,
3281
3420
  children: [
3282
- conceptOverride ? /* @__PURE__ */ jsx22(ConceptDetailPanel, { concept: conceptOverride }) : selectedSpecNode && graphspec ? /* @__PURE__ */ jsx22(
3421
+ conceptOverride ? /* @__PURE__ */ jsx23(ConceptDetailPanel, { concept: conceptOverride }) : selectedSpecNode && graphspec ? /* @__PURE__ */ jsx23(
3283
3422
  PipeDetailPanel,
3284
3423
  {
3285
3424
  node: selectedSpecNode,
3286
3425
  spec: graphspec,
3287
3426
  onConceptClick: handleConceptClick
3288
3427
  }
3289
- ) : (detailSelection == null ? void 0 : detailSelection.stuffData) ? /* @__PURE__ */ jsx22(
3428
+ ) : (detailSelection == null ? void 0 : detailSelection.stuffData) ? /* @__PURE__ */ jsx23(
3290
3429
  StuffNodeDetail,
3291
3430
  {
3292
3431
  nodeId: detailSelection.nodeId,