@magic-marker/nurt 0.1.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.
@@ -0,0 +1,1234 @@
1
+ // src/react/nurt-flow-chart.tsx
2
+ import { useEffect, useState } from "react";
3
+
4
+ // src/react/nurt-flow-chart-edges.tsx
5
+ import { jsx } from "react/jsx-runtime";
6
+ var FlowEdge = ({ edge }) => {
7
+ const { bendPoints, x1, x2, y1, y2 } = edge;
8
+ if (bendPoints && bendPoints.length > 0) {
9
+ const points = [{ x: x1, y: y1 }, ...bendPoints, { x: x2, y: y2 }];
10
+ const d2 = buildSplinePath(points);
11
+ return /* @__PURE__ */ jsx(
12
+ "path",
13
+ {
14
+ d: d2,
15
+ fill: "none",
16
+ markerEnd: "url(#arrowhead)",
17
+ stroke: "#4b5563",
18
+ strokeWidth: 1.5
19
+ }
20
+ );
21
+ }
22
+ if (Math.abs(x1 - x2) < 2) {
23
+ return /* @__PURE__ */ jsx(
24
+ "line",
25
+ {
26
+ markerEnd: "url(#arrowhead)",
27
+ stroke: "#4b5563",
28
+ strokeWidth: 1.5,
29
+ x1,
30
+ x2,
31
+ y1,
32
+ y2
33
+ }
34
+ );
35
+ }
36
+ const midY = (y1 + y2) / 2;
37
+ const d = `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`;
38
+ return /* @__PURE__ */ jsx(
39
+ "path",
40
+ {
41
+ d,
42
+ fill: "none",
43
+ markerEnd: "url(#arrowhead)",
44
+ stroke: "#4b5563",
45
+ strokeWidth: 1.5
46
+ }
47
+ );
48
+ };
49
+ function buildSplinePath(points) {
50
+ if (points.length < 2) return "";
51
+ if (points.length === 2) {
52
+ return `M ${points[0].x} ${points[0].y} L ${points[1].x} ${points[1].y}`;
53
+ }
54
+ let d = `M ${points[0].x} ${points[0].y}`;
55
+ for (let i = 1; i < points.length; i++) {
56
+ const prev = points[i - 1];
57
+ const curr = points[i];
58
+ const midY = (prev.y + curr.y) / 2;
59
+ d += ` C ${prev.x} ${midY}, ${curr.x} ${midY}, ${curr.x} ${curr.y}`;
60
+ }
61
+ return d;
62
+ }
63
+
64
+ // src/react/nurt-flow-chart-layout.ts
65
+ import ELK from "elkjs/lib/elk.bundled.js";
66
+ var NODE_W = 180;
67
+ var NODE_H = 58;
68
+ var MEMBER_W = 160;
69
+ var MEMBER_H = 28;
70
+ var SUBGRAPH_STEP_W = 110;
71
+ var SUBGRAPH_STEP_H = 30;
72
+ var elk = new ELK();
73
+ async function layoutSnapshot(snapshot) {
74
+ const { groups, steps } = snapshot.flow;
75
+ if (steps.length === 0 && groups.length === 0) {
76
+ return { edges: [], externalRefEdges: [], height: 0, nodes: [], width: 0 };
77
+ }
78
+ const groupMap = /* @__PURE__ */ new Map();
79
+ for (const g of groups) groupMap.set(g.name, g);
80
+ const elkGraph = buildElkGraph(steps, groups, groupMap);
81
+ const laid = await elk.layout(elkGraph);
82
+ return extractLayout(laid, steps, groupMap);
83
+ }
84
+ function buildElkGraph(steps, groups, groupMap) {
85
+ const allIds = /* @__PURE__ */ new Set([
86
+ ...steps.map((s) => s.name),
87
+ ...groups.map((g) => g.name)
88
+ ]);
89
+ const children = [];
90
+ for (const step of steps) {
91
+ children.push({
92
+ height: NODE_H,
93
+ id: step.name,
94
+ width: NODE_W
95
+ });
96
+ }
97
+ for (const g of groups) {
98
+ const memberChildren = [];
99
+ for (const m of g.members) {
100
+ const memberId = `${g.name}/${m.name}`;
101
+ if (m.type === "subgraph" && m.subgraph && m.subgraph.flow.steps.length > 0) {
102
+ const subSteps = m.subgraph.flow.steps;
103
+ const subChildren = subSteps.map((s) => ({
104
+ height: SUBGRAPH_STEP_H,
105
+ id: `${memberId}/${s.name}`,
106
+ width: SUBGRAPH_STEP_W
107
+ }));
108
+ const subEdges = [];
109
+ let edgeIdx = 0;
110
+ for (const s of subSteps) {
111
+ for (const parent of s.parentNames) {
112
+ subEdges.push({
113
+ id: `pe-${memberId}-${edgeIdx++}`,
114
+ sources: [`${memberId}/${parent}`],
115
+ targets: [`${memberId}/${s.name}`]
116
+ });
117
+ }
118
+ }
119
+ memberChildren.push({
120
+ children: subChildren,
121
+ edges: subEdges,
122
+ id: memberId,
123
+ layoutOptions: {
124
+ "elk.algorithm": "layered",
125
+ "elk.direction": "DOWN",
126
+ "elk.edgeRouting": "SPLINES",
127
+ "elk.layered.spacing.nodeNodeBetweenLayers": "30",
128
+ "elk.padding": "[top=24,left=8,bottom=8,right=8]",
129
+ "elk.spacing.nodeNode": "12"
130
+ }
131
+ });
132
+ } else {
133
+ memberChildren.push({
134
+ height: MEMBER_H,
135
+ id: memberId,
136
+ width: MEMBER_W
137
+ });
138
+ }
139
+ }
140
+ const isEmpty = memberChildren.length === 0;
141
+ children.push({
142
+ children: memberChildren,
143
+ // Empty groups get a minimum size so they render visibly
144
+ ...isEmpty ? { height: NODE_H, width: NODE_W } : {},
145
+ id: g.name,
146
+ layoutOptions: {
147
+ "elk.padding": "[top=36,left=10,bottom=10,right=10]"
148
+ }
149
+ });
150
+ }
151
+ const edges = [];
152
+ let edgeId = 0;
153
+ for (const step of steps) {
154
+ for (const parent of step.parentNames) {
155
+ if (allIds.has(parent)) {
156
+ edges.push({
157
+ id: `e${edgeId++}`,
158
+ sources: [parent],
159
+ targets: [step.name]
160
+ });
161
+ }
162
+ }
163
+ }
164
+ for (const g of groups) {
165
+ for (const dep of g.dependsOn) {
166
+ if (allIds.has(dep)) {
167
+ edges.push({
168
+ id: `e${edgeId++}`,
169
+ sources: [dep],
170
+ targets: [g.name]
171
+ });
172
+ }
173
+ }
174
+ }
175
+ return {
176
+ children,
177
+ edges,
178
+ id: "root",
179
+ layoutOptions: {
180
+ "elk.algorithm": "layered",
181
+ "elk.direction": "DOWN",
182
+ "elk.edgeRouting": "SPLINES",
183
+ "elk.layered.spacing.edgeNodeBetweenLayers": "30",
184
+ "elk.layered.spacing.nodeNodeBetweenLayers": "50",
185
+ "elk.spacing.nodeNode": "20"
186
+ }
187
+ };
188
+ }
189
+ function extractLayout(laid, steps, groupMap) {
190
+ const nodes = [];
191
+ const edges = [];
192
+ const stepMap = new Map(steps.map((s) => [s.name, s]));
193
+ for (const child of laid.children ?? []) {
194
+ const group = groupMap.get(child.id);
195
+ const step = stepMap.get(child.id);
196
+ const cx = child.x ?? 0;
197
+ const cy = child.y ?? 0;
198
+ const cw = child.width ?? NODE_W;
199
+ const ch = child.height ?? NODE_H;
200
+ if (group) {
201
+ const members = (child.children ?? []).map(
202
+ (memberElk) => {
203
+ const memberName = memberElk.id.split("/").at(-1) ?? memberElk.id;
204
+ const memberData = group.members.find((m) => m.name === memberName);
205
+ const mx = (memberElk.x ?? 0) + cx;
206
+ const my = (memberElk.y ?? 0) + cy;
207
+ let subgraphSteps;
208
+ let memberSubgraphEdges;
209
+ if (memberData?.type === "subgraph" && memberData.subgraph && memberElk.children && memberElk.children.length > 0) {
210
+ const subSteps = memberData.subgraph.flow.steps;
211
+ subgraphSteps = memberElk.children.map((subElk) => {
212
+ const subName = subElk.id.split("/").at(-1) ?? subElk.id;
213
+ const subData = subSteps.find((s) => s.name === subName);
214
+ return {
215
+ durationMs: subData?.durationMs,
216
+ height: subElk.height ?? SUBGRAPH_STEP_H,
217
+ name: subName,
218
+ status: subData?.status ?? "pending",
219
+ width: subElk.width ?? SUBGRAPH_STEP_W,
220
+ x: (subElk.x ?? 0) + mx,
221
+ y: (subElk.y ?? 0) + my
222
+ };
223
+ });
224
+ memberSubgraphEdges = [];
225
+ for (const pEdge of memberElk.edges ?? []) {
226
+ const sections = pEdge.sections ?? [];
227
+ if (sections.length > 0) {
228
+ const sec = sections[0];
229
+ memberSubgraphEdges.push({
230
+ bendPoints: sec.bendPoints?.map((bp) => ({
231
+ x: bp.x + mx,
232
+ y: bp.y + my
233
+ })),
234
+ memberName,
235
+ x1: sec.startPoint.x + mx,
236
+ x2: sec.endPoint.x + mx,
237
+ y1: sec.startPoint.y + my,
238
+ y2: sec.endPoint.y + my
239
+ });
240
+ }
241
+ }
242
+ }
243
+ return {
244
+ durationMs: memberData?.durationMs,
245
+ error: memberData?.error,
246
+ height: memberElk.height ?? MEMBER_H,
247
+ name: memberName,
248
+ status: memberData?.status ?? "pending",
249
+ subgraphEdges: memberSubgraphEdges,
250
+ subgraphSteps,
251
+ type: memberData?.type ?? "single",
252
+ width: memberElk.width ?? MEMBER_W,
253
+ x: mx,
254
+ y: my
255
+ };
256
+ }
257
+ );
258
+ nodes.push({
259
+ groupName: group.name,
260
+ height: ch,
261
+ id: child.id,
262
+ label: group.name,
263
+ members,
264
+ status: deriveGroupStatus(group),
265
+ type: "group",
266
+ width: cw,
267
+ x: cx,
268
+ y: cy
269
+ });
270
+ } else if (step) {
271
+ nodes.push({
272
+ height: ch,
273
+ id: child.id,
274
+ label: step.name,
275
+ status: step.status,
276
+ step,
277
+ type: "step",
278
+ width: cw,
279
+ x: cx,
280
+ y: cy
281
+ });
282
+ }
283
+ }
284
+ for (const elkEdge of laid.edges ?? []) {
285
+ const source = elkEdge.sources?.[0] ?? "";
286
+ const target = elkEdge.targets?.[0] ?? "";
287
+ const sections = elkEdge.sections ?? [];
288
+ if (sections.length > 0) {
289
+ const section = sections[0];
290
+ const bendPoints = section.bendPoints?.map((bp) => ({
291
+ x: bp.x,
292
+ y: bp.y
293
+ }));
294
+ edges.push({
295
+ bendPoints,
296
+ source,
297
+ target,
298
+ x1: section.startPoint.x,
299
+ x2: section.endPoint.x,
300
+ y1: section.startPoint.y,
301
+ y2: section.endPoint.y
302
+ });
303
+ } else {
304
+ const srcNode = nodes.find((n) => n.id === source);
305
+ const tgtNode = nodes.find((n) => n.id === target);
306
+ if (srcNode && tgtNode) {
307
+ edges.push({
308
+ source,
309
+ target,
310
+ x1: srcNode.x + srcNode.width / 2,
311
+ x2: tgtNode.x + tgtNode.width / 2,
312
+ y1: srcNode.y + srcNode.height,
313
+ y2: tgtNode.y
314
+ });
315
+ }
316
+ }
317
+ }
318
+ const externalRefEdges = [];
319
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
320
+ for (const node of nodes) {
321
+ if (node.type !== "group" || !node.members) continue;
322
+ for (const member of node.members) {
323
+ if (member.type !== "subgraph") continue;
324
+ const group = groupMap.get(node.id);
325
+ const memberData = group?.members.find((m) => m.name === member.name);
326
+ if (!memberData?.externalDeps) continue;
327
+ for (const [, parentStepName] of Object.entries(
328
+ memberData.externalDeps
329
+ )) {
330
+ const srcNode = nodeMap.get(parentStepName);
331
+ if (srcNode) {
332
+ externalRefEdges.push({
333
+ sourceStepName: parentStepName,
334
+ targetMemberName: member.name,
335
+ x1: srcNode.x + srcNode.width / 2,
336
+ x2: member.x + member.width / 2,
337
+ y1: srcNode.y + srcNode.height,
338
+ y2: member.y
339
+ });
340
+ }
341
+ }
342
+ }
343
+ }
344
+ const totalWidth = laid.width ?? 900;
345
+ const totalHeight = laid.height ?? 400;
346
+ return {
347
+ edges,
348
+ externalRefEdges,
349
+ height: Math.max(400, totalHeight + 40),
350
+ nodes,
351
+ width: Math.max(900, totalWidth + 40)
352
+ };
353
+ }
354
+ function deriveGroupStatus(group) {
355
+ if (group.members.length === 0) return "pending";
356
+ if (group.members.some((m) => m.status === "running")) return "running";
357
+ if (group.members.every((m) => m.status === "success")) return "success";
358
+ if (group.members.every((m) => m.status === "success" || m.status === "error"))
359
+ return "error";
360
+ return "pending";
361
+ }
362
+
363
+ // src/react/nurt-flow-chart-utils.ts
364
+ var formatDuration = (ms) => {
365
+ if (ms < 1e3) return `${ms}ms`;
366
+ return `${(ms / 1e3).toFixed(1)}s`;
367
+ };
368
+ var statusIndicator = (status) => {
369
+ switch (status) {
370
+ case "success":
371
+ return "\u2713";
372
+ case "error":
373
+ return "\u2717";
374
+ case "running":
375
+ return "\u25CB";
376
+ case "skipped":
377
+ return "\u2014";
378
+ case "pending":
379
+ return "\u2022";
380
+ default:
381
+ return "";
382
+ }
383
+ };
384
+ var getNodeStyle = (status, hovered) => {
385
+ switch (status) {
386
+ case "running":
387
+ return {
388
+ fill: "#1e3a5f",
389
+ opacity: 1,
390
+ stroke: hovered ? "#60a5fa" : "#3b82f6",
391
+ strokeWidth: hovered ? 2 : 1.5
392
+ };
393
+ case "pending":
394
+ return {
395
+ fill: "#1f2937",
396
+ opacity: 0.5,
397
+ stroke: "#374151",
398
+ strokeWidth: 1
399
+ };
400
+ case "error":
401
+ return {
402
+ fill: "#3b1c1c",
403
+ opacity: 1,
404
+ stroke: hovered ? "#f87171" : "#ef4444",
405
+ strokeWidth: hovered ? 2 : 1
406
+ };
407
+ case "skipped":
408
+ return {
409
+ fill: "#1f2937",
410
+ opacity: 0.6,
411
+ stroke: "#4b5563",
412
+ strokeWidth: 1
413
+ };
414
+ default:
415
+ return {
416
+ fill: "#374151",
417
+ opacity: 1,
418
+ stroke: hovered ? "#9ca3af" : "#6b7280",
419
+ strokeWidth: hovered ? 2 : 1
420
+ };
421
+ }
422
+ };
423
+ var getMemberStyle = (status) => {
424
+ switch (status) {
425
+ case "success":
426
+ return { fill: "#065f46", stroke: "#10b981" };
427
+ case "error":
428
+ return { fill: "#7f1d1d", stroke: "#ef4444" };
429
+ case "running":
430
+ return { fill: "#1e3a5f", stroke: "#3b82f6" };
431
+ default:
432
+ return { fill: "#374151", stroke: "#6b7280" };
433
+ }
434
+ };
435
+ var statusTextColor = (status) => {
436
+ switch (status) {
437
+ case "success":
438
+ return "#34d399";
439
+ case "error":
440
+ return "#f87171";
441
+ case "running":
442
+ return "#60a5fa";
443
+ case "skipped":
444
+ return "#6b7280";
445
+ default:
446
+ return "#9ca3af";
447
+ }
448
+ };
449
+
450
+ // src/react/nurt-flow-chart-nodes.tsx
451
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
452
+ var StepNode = ({ hovered, node, onClick, onHover }) => {
453
+ const rectX = node.x;
454
+ const rectY = node.y;
455
+ const centerX = node.x + node.width / 2;
456
+ const centerY = node.y + node.height / 2;
457
+ const style = getNodeStyle(node.status, hovered);
458
+ const step = node.step;
459
+ const isClickable = node.status !== "pending" && node.status !== "running";
460
+ let durationText = "";
461
+ if (step?.status === "running") durationText = "running\u2026";
462
+ else if (step?.durationMs && step.durationMs > 0)
463
+ durationText = formatDuration(step.durationMs);
464
+ const hasSubline = durationText || step?.terminal;
465
+ return /* @__PURE__ */ jsxs(
466
+ "g",
467
+ {
468
+ className: node.status === "running" ? "node-running" : void 0,
469
+ onClick: () => isClickable && onClick?.(node.id),
470
+ onMouseEnter: () => onHover(node.id),
471
+ onMouseLeave: () => onHover(null),
472
+ style: { cursor: isClickable ? "pointer" : "default" },
473
+ children: [
474
+ /* @__PURE__ */ jsx2(
475
+ "rect",
476
+ {
477
+ fill: style.fill,
478
+ height: node.height,
479
+ opacity: style.opacity,
480
+ rx: 6,
481
+ stroke: style.stroke,
482
+ strokeDasharray: node.status === "skipped" ? "4 3" : void 0,
483
+ strokeWidth: style.strokeWidth,
484
+ width: node.width,
485
+ x: rectX,
486
+ y: rectY
487
+ }
488
+ ),
489
+ /* @__PURE__ */ jsxs(
490
+ "text",
491
+ {
492
+ dominantBaseline: "central",
493
+ fill: node.status === "pending" ? "#6b7280" : "#d1d5db",
494
+ fontSize: 11,
495
+ opacity: style.opacity,
496
+ textAnchor: "middle",
497
+ x: centerX,
498
+ y: centerY - (hasSubline ? 8 : 0),
499
+ children: [
500
+ statusIndicator(node.status),
501
+ " ",
502
+ node.label
503
+ ]
504
+ }
505
+ ),
506
+ durationText && /* @__PURE__ */ jsxs(
507
+ "text",
508
+ {
509
+ dominantBaseline: "central",
510
+ fill: statusTextColor(node.status),
511
+ fontSize: 10,
512
+ opacity: style.opacity,
513
+ textAnchor: "middle",
514
+ x: centerX,
515
+ y: centerY + 10,
516
+ children: [
517
+ durationText,
518
+ step?.terminal ? " \u25C6" : ""
519
+ ]
520
+ }
521
+ ),
522
+ !durationText && step?.terminal && /* @__PURE__ */ jsxs(
523
+ "text",
524
+ {
525
+ dominantBaseline: "central",
526
+ fill: "#9ca3af",
527
+ fontSize: 9,
528
+ opacity: style.opacity,
529
+ textAnchor: "middle",
530
+ x: centerX,
531
+ y: centerY + 10,
532
+ children: [
533
+ "terminal ",
534
+ "\u25C6"
535
+ ]
536
+ }
537
+ )
538
+ ]
539
+ }
540
+ );
541
+ };
542
+ var GroupNode = ({ hovered, node, onClick, onHover }) => {
543
+ const rectX = node.x;
544
+ const rectY = node.y;
545
+ const centerX = node.x + node.width / 2;
546
+ const centerY = node.y + node.height / 2;
547
+ const style = getNodeStyle(node.status, hovered);
548
+ const isEmpty = !node.members || node.members.length === 0;
549
+ return /* @__PURE__ */ jsxs(
550
+ "g",
551
+ {
552
+ onMouseEnter: () => onHover(node.id),
553
+ onMouseLeave: () => onHover(null),
554
+ style: { cursor: "pointer" },
555
+ children: [
556
+ /* @__PURE__ */ jsx2(
557
+ "rect",
558
+ {
559
+ fill: isEmpty ? "none" : style.fill,
560
+ height: node.height,
561
+ onClick: () => onClick?.(node.id),
562
+ opacity: isEmpty ? 0.4 : style.opacity,
563
+ rx: 8,
564
+ stroke: isEmpty ? "#4b5563" : style.stroke,
565
+ strokeDasharray: isEmpty ? "4 4" : "6 3",
566
+ strokeWidth: style.strokeWidth,
567
+ width: node.width,
568
+ x: rectX,
569
+ y: rectY
570
+ }
571
+ ),
572
+ isEmpty ? /* @__PURE__ */ jsxs(Fragment, { children: [
573
+ /* @__PURE__ */ jsx2(
574
+ "text",
575
+ {
576
+ dominantBaseline: "central",
577
+ fill: "#6b7280",
578
+ fontSize: 16,
579
+ opacity: 0.5,
580
+ textAnchor: "middle",
581
+ x: centerX,
582
+ y: centerY - 8,
583
+ children: "\u2026"
584
+ }
585
+ ),
586
+ /* @__PURE__ */ jsx2(
587
+ "text",
588
+ {
589
+ dominantBaseline: "central",
590
+ fill: "#6b7280",
591
+ fontSize: 9,
592
+ opacity: 0.5,
593
+ textAnchor: "middle",
594
+ x: centerX,
595
+ y: centerY + 10,
596
+ children: node.label
597
+ }
598
+ )
599
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
600
+ /* @__PURE__ */ jsx2(
601
+ "text",
602
+ {
603
+ dominantBaseline: "central",
604
+ fill: "#9ca3af",
605
+ fontSize: 10,
606
+ fontWeight: "bold",
607
+ textAnchor: "middle",
608
+ x: centerX,
609
+ y: rectY + 18,
610
+ children: node.label
611
+ }
612
+ ),
613
+ node.members?.map((member) => /* @__PURE__ */ jsx2(MemberNode, { member, onClick }, member.name))
614
+ ] })
615
+ ]
616
+ }
617
+ );
618
+ };
619
+ var MemberNode = ({ member, onClick }) => {
620
+ const mStyle = getMemberStyle(member.status);
621
+ const hasSubgraphSteps = member.type === "subgraph" && member.subgraphSteps && member.subgraphSteps.length > 0;
622
+ if (hasSubgraphSteps) {
623
+ return /* @__PURE__ */ jsxs(
624
+ "g",
625
+ {
626
+ className: member.status === "running" ? "node-running" : void 0,
627
+ onClick: (e) => {
628
+ e.stopPropagation();
629
+ onClick?.(member.name);
630
+ },
631
+ style: { cursor: "pointer" },
632
+ children: [
633
+ /* @__PURE__ */ jsx2(
634
+ "rect",
635
+ {
636
+ fill: mStyle.fill,
637
+ height: member.height,
638
+ opacity: 0.6,
639
+ rx: 4,
640
+ stroke: mStyle.stroke,
641
+ strokeDasharray: "3 2",
642
+ strokeWidth: 0.5,
643
+ width: member.width,
644
+ x: member.x,
645
+ y: member.y
646
+ }
647
+ ),
648
+ /* @__PURE__ */ jsxs(
649
+ "text",
650
+ {
651
+ dominantBaseline: "central",
652
+ fill: "#9ca3af",
653
+ fontSize: 8,
654
+ textAnchor: "middle",
655
+ x: member.x + member.width / 2,
656
+ y: member.y + 10,
657
+ children: [
658
+ member.name,
659
+ member.durationMs && member.durationMs > 0 ? ` (${formatDuration(member.durationMs)})` : member.status === "running" ? " (running\u2026)" : ""
660
+ ]
661
+ }
662
+ ),
663
+ member.subgraphEdges?.map((se, i) => /* @__PURE__ */ jsx2(SubgraphInternalEdge, { edge: se }, `${member.name}-e${i}`)),
664
+ member.subgraphSteps.map((ps) => /* @__PURE__ */ jsx2(SubgraphStepNode, { step: ps }, ps.name))
665
+ ]
666
+ }
667
+ );
668
+ }
669
+ const centerX = member.x + member.width / 2;
670
+ const centerY = member.y + member.height / 2;
671
+ let memberDurationText = "";
672
+ if (member.status === "running") memberDurationText = "running\u2026";
673
+ else if (member.durationMs && member.durationMs > 0)
674
+ memberDurationText = formatDuration(member.durationMs);
675
+ return /* @__PURE__ */ jsxs(
676
+ "g",
677
+ {
678
+ className: member.status === "running" ? "node-running" : void 0,
679
+ onClick: (e) => {
680
+ e.stopPropagation();
681
+ onClick?.(member.name);
682
+ },
683
+ style: { cursor: "pointer" },
684
+ children: [
685
+ /* @__PURE__ */ jsx2(
686
+ "rect",
687
+ {
688
+ fill: mStyle.fill,
689
+ height: member.height,
690
+ rx: 4,
691
+ stroke: mStyle.stroke,
692
+ strokeWidth: 0.5,
693
+ width: member.width,
694
+ x: member.x,
695
+ y: member.y
696
+ }
697
+ ),
698
+ /* @__PURE__ */ jsxs(
699
+ "text",
700
+ {
701
+ dominantBaseline: "central",
702
+ fill: "#d1d5db",
703
+ fontSize: 9,
704
+ textAnchor: "middle",
705
+ x: centerX,
706
+ y: memberDurationText ? centerY - 5 : centerY,
707
+ children: [
708
+ statusIndicator(member.status),
709
+ " ",
710
+ member.name
711
+ ]
712
+ }
713
+ ),
714
+ memberDurationText && /* @__PURE__ */ jsx2(
715
+ "text",
716
+ {
717
+ dominantBaseline: "central",
718
+ fill: statusTextColor(member.status),
719
+ fontSize: 8,
720
+ textAnchor: "middle",
721
+ x: centerX,
722
+ y: centerY + 7,
723
+ children: memberDurationText
724
+ }
725
+ )
726
+ ]
727
+ }
728
+ );
729
+ };
730
+ var SubgraphStepNode = ({ step }) => {
731
+ const style = getNodeStyle(step.status, false);
732
+ const centerX = step.x + step.width / 2;
733
+ const centerY = step.y + step.height / 2;
734
+ let stepDurationText = "";
735
+ if (step.status === "running") stepDurationText = "running\u2026";
736
+ else if (step.durationMs && step.durationMs > 0)
737
+ stepDurationText = formatDuration(step.durationMs);
738
+ return /* @__PURE__ */ jsxs("g", { className: step.status === "running" ? "node-running" : void 0, children: [
739
+ /* @__PURE__ */ jsx2(
740
+ "rect",
741
+ {
742
+ fill: style.fill,
743
+ height: step.height,
744
+ opacity: style.opacity,
745
+ rx: 4,
746
+ stroke: style.stroke,
747
+ strokeDasharray: step.status === "skipped" ? "3 2" : void 0,
748
+ strokeWidth: 0.75,
749
+ width: step.width,
750
+ x: step.x,
751
+ y: step.y
752
+ }
753
+ ),
754
+ /* @__PURE__ */ jsxs(
755
+ "text",
756
+ {
757
+ dominantBaseline: "central",
758
+ fill: step.status === "pending" ? "#6b7280" : "#d1d5db",
759
+ fontSize: 9,
760
+ opacity: style.opacity,
761
+ textAnchor: "middle",
762
+ x: centerX,
763
+ y: stepDurationText ? centerY - 5 : centerY,
764
+ children: [
765
+ statusIndicator(step.status),
766
+ " ",
767
+ step.name
768
+ ]
769
+ }
770
+ ),
771
+ stepDurationText && /* @__PURE__ */ jsx2(
772
+ "text",
773
+ {
774
+ dominantBaseline: "central",
775
+ fill: statusTextColor(step.status),
776
+ fontSize: 8,
777
+ opacity: style.opacity,
778
+ textAnchor: "middle",
779
+ x: centerX,
780
+ y: centerY + 7,
781
+ children: stepDurationText
782
+ }
783
+ )
784
+ ] });
785
+ };
786
+ var SubgraphInternalEdge = ({
787
+ edge
788
+ }) => {
789
+ const { bendPoints, x1, x2, y1, y2 } = edge;
790
+ if (bendPoints && bendPoints.length > 0) {
791
+ const points = [{ x: x1, y: y1 }, ...bendPoints, { x: x2, y: y2 }];
792
+ let d = `M ${points[0].x} ${points[0].y}`;
793
+ for (let i = 1; i < points.length; i++) {
794
+ const prev = points[i - 1];
795
+ const curr = points[i];
796
+ const midY = (prev.y + curr.y) / 2;
797
+ d += ` C ${prev.x} ${midY}, ${curr.x} ${midY}, ${curr.x} ${curr.y}`;
798
+ }
799
+ return /* @__PURE__ */ jsx2(
800
+ "path",
801
+ {
802
+ d,
803
+ fill: "none",
804
+ markerEnd: "url(#arrowhead-sm)",
805
+ stroke: "#6b7280",
806
+ strokeWidth: 1
807
+ }
808
+ );
809
+ }
810
+ return /* @__PURE__ */ jsx2(
811
+ "line",
812
+ {
813
+ markerEnd: "url(#arrowhead-sm)",
814
+ stroke: "#6b7280",
815
+ strokeWidth: 1,
816
+ x1,
817
+ x2,
818
+ y1,
819
+ y2
820
+ }
821
+ );
822
+ };
823
+
824
+ // src/react/nurt-flow-chart.tsx
825
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
826
+ var EMPTY_LAYOUT = {
827
+ edges: [],
828
+ externalRefEdges: [],
829
+ height: 400,
830
+ nodes: [],
831
+ width: 900
832
+ };
833
+ var NurtFlowChart = ({
834
+ className,
835
+ onNodeClick,
836
+ snapshot
837
+ }) => {
838
+ const [layout, setLayout] = useState(EMPTY_LAYOUT);
839
+ const [hoveredId, setHoveredId] = useState(null);
840
+ useEffect(() => {
841
+ let cancelled = false;
842
+ layoutSnapshot(snapshot).then((result) => {
843
+ if (!cancelled) setLayout(result);
844
+ });
845
+ return () => {
846
+ cancelled = true;
847
+ };
848
+ }, [snapshot]);
849
+ const { edges, externalRefEdges, height, nodes, width } = layout;
850
+ return /* @__PURE__ */ jsxs2(
851
+ "svg",
852
+ {
853
+ className,
854
+ height,
855
+ preserveAspectRatio: "xMidYMin meet",
856
+ viewBox: `0 0 ${width} ${height}`,
857
+ width: "100%",
858
+ children: [
859
+ /* @__PURE__ */ jsxs2("defs", { children: [
860
+ /* @__PURE__ */ jsx3(
861
+ "marker",
862
+ {
863
+ id: "arrowhead",
864
+ markerHeight: "6",
865
+ markerWidth: "8",
866
+ orient: "auto",
867
+ refX: "8",
868
+ refY: "3",
869
+ children: /* @__PURE__ */ jsx3("polygon", { fill: "#4b5563", points: "0 0, 8 3, 0 6" })
870
+ }
871
+ ),
872
+ /* @__PURE__ */ jsx3(
873
+ "marker",
874
+ {
875
+ id: "arrowhead-sm",
876
+ markerHeight: "4",
877
+ markerWidth: "6",
878
+ orient: "auto",
879
+ refX: "6",
880
+ refY: "2",
881
+ children: /* @__PURE__ */ jsx3("polygon", { fill: "#6b7280", points: "0 0, 6 2, 0 4" })
882
+ }
883
+ ),
884
+ /* @__PURE__ */ jsx3("style", { children: `
885
+ @keyframes pulse-glow {
886
+ 0%, 100% { opacity: 1; }
887
+ 50% { opacity: 0.6; }
888
+ }
889
+ .node-running { animation: pulse-glow 1.5s ease-in-out infinite; }
890
+ ` })
891
+ ] }),
892
+ edges.map((e) => /* @__PURE__ */ jsx3(FlowEdge, { edge: e }, `${e.source}-${e.target}`)),
893
+ externalRefEdges.map((ref, i) => {
894
+ const midY = (ref.y1 + ref.y2) / 2;
895
+ const d = `M ${ref.x1} ${ref.y1} C ${ref.x1} ${midY}, ${ref.x2} ${midY}, ${ref.x2} ${ref.y2}`;
896
+ return /* @__PURE__ */ jsx3(
897
+ "path",
898
+ {
899
+ d,
900
+ fill: "none",
901
+ markerEnd: "url(#arrowhead-sm)",
902
+ opacity: 0.6,
903
+ stroke: "#a78bfa",
904
+ strokeDasharray: "6 4",
905
+ strokeWidth: 1.5
906
+ },
907
+ `ref-${ref.sourceStepName}-${ref.targetMemberName}-${i}`
908
+ );
909
+ }),
910
+ nodes.map(
911
+ (node) => node.type === "group" ? /* @__PURE__ */ jsx3(
912
+ GroupNode,
913
+ {
914
+ hovered: hoveredId === node.id,
915
+ node,
916
+ onClick: onNodeClick,
917
+ onHover: setHoveredId
918
+ },
919
+ node.id
920
+ ) : /* @__PURE__ */ jsx3(
921
+ StepNode,
922
+ {
923
+ hovered: hoveredId === node.id,
924
+ node,
925
+ onClick: onNodeClick,
926
+ onHover: setHoveredId
927
+ },
928
+ node.id
929
+ )
930
+ )
931
+ ]
932
+ }
933
+ );
934
+ };
935
+
936
+ // src/react/nurt-flow-chart-with-panel.tsx
937
+ import { useCallback, useEffect as useEffect2, useState as useState2 } from "react";
938
+
939
+ // src/react/nurt-step-detail-panel.tsx
940
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
941
+ var STATUS_DOT = {
942
+ error: "bg-red-500",
943
+ pending: "bg-gray-400",
944
+ running: "bg-blue-500 animate-pulse",
945
+ skipped: "bg-gray-400",
946
+ success: "bg-green-500"
947
+ };
948
+ var StatusDot = ({ status }) => /* @__PURE__ */ jsx4(
949
+ "span",
950
+ {
951
+ className: `inline-block h-2 w-2 rounded-full ${STATUS_DOT[status] ?? "bg-gray-400"}`
952
+ }
953
+ );
954
+ var NurtStepDetailPanel = ({
955
+ onClose,
956
+ onNavigate,
957
+ selectedNode,
958
+ snapshot
959
+ }) => {
960
+ const step = snapshot.flow.steps.find((s) => s.name === selectedNode);
961
+ const groupData = snapshot.flow.groups.find((g) => g.name === selectedNode);
962
+ const member = findMember(snapshot, selectedNode);
963
+ const subgraphStep = findSubgraphStep(snapshot, selectedNode);
964
+ const status = step?.status ?? subgraphStep?.step.status ?? member?.status ?? deriveGroupStatus2(groupData) ?? "pending";
965
+ if (!step && !member && !groupData && !subgraphStep) {
966
+ return /* @__PURE__ */ jsx4("div", { className: "flex h-full items-center justify-center text-sm text-gray-500", children: "Node not found" });
967
+ }
968
+ return /* @__PURE__ */ jsxs3("div", { className: "flex h-full flex-col overflow-hidden", children: [
969
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between border-b border-gray-700 px-4 py-3", children: [
970
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
971
+ /* @__PURE__ */ jsx4(StatusDot, { status }),
972
+ /* @__PURE__ */ jsx4("span", { className: "text-sm font-medium text-gray-200", children: selectedNode }),
973
+ groupData && /* @__PURE__ */ jsx4("span", { className: "rounded bg-gray-700 px-1.5 py-0.5 text-xs text-gray-400", children: "group" }),
974
+ member?.type === "subgraph" && /* @__PURE__ */ jsx4("span", { className: "rounded bg-gray-700 px-1.5 py-0.5 text-xs text-gray-400", children: "subgraph" }),
975
+ subgraphStep && /* @__PURE__ */ jsx4("span", { className: "rounded bg-gray-700 px-1.5 py-0.5 text-xs text-gray-400", children: subgraphStep.memberName })
976
+ ] }),
977
+ /* @__PURE__ */ jsx4(
978
+ "button",
979
+ {
980
+ className: "rounded px-2 py-1 text-sm text-gray-400 hover:bg-gray-700 hover:text-gray-200",
981
+ onClick: onClose,
982
+ children: "\xD7"
983
+ }
984
+ )
985
+ ] }),
986
+ /* @__PURE__ */ jsxs3("div", { className: "flex-1 overflow-y-auto px-4 py-3", children: [
987
+ step && /* @__PURE__ */ jsx4(StepDetails, { onNavigate, step }),
988
+ !step && groupData && /* @__PURE__ */ jsx4(GroupDetails, { group: groupData, onNavigate }),
989
+ !step && !groupData && !subgraphStep && member && /* @__PURE__ */ jsx4(MemberDetails, { member, onNavigate }),
990
+ subgraphStep && /* @__PURE__ */ jsx4(
991
+ SubgraphStepDetails,
992
+ {
993
+ memberName: subgraphStep.memberName,
994
+ onNavigate,
995
+ step: subgraphStep.step
996
+ }
997
+ )
998
+ ] })
999
+ ] });
1000
+ };
1001
+ var StepDetails = ({ onNavigate, step }) => /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-4", children: [
1002
+ /* @__PURE__ */ jsxs3(Section, { title: "Status", children: [
1003
+ /* @__PURE__ */ jsx4(Row, { label: "Status", children: /* @__PURE__ */ jsxs3("span", { className: "font-mono", children: [
1004
+ statusIndicator(step.status),
1005
+ " ",
1006
+ step.status
1007
+ ] }) }),
1008
+ step.durationMs != null && step.durationMs > 0 && /* @__PURE__ */ jsx4(Row, { label: "Duration", children: /* @__PURE__ */ jsx4("span", { className: "font-mono", children: formatDuration(step.durationMs) }) }),
1009
+ step.startedAt != null && /* @__PURE__ */ jsx4(Row, { label: "Started", children: formatTime(step.startedAt) }),
1010
+ step.completedAt != null && /* @__PURE__ */ jsx4(Row, { label: "Completed", children: formatTime(step.completedAt) }),
1011
+ step.terminal && /* @__PURE__ */ jsx4(Row, { label: "Terminal", children: /* @__PURE__ */ jsx4("span", { className: "text-yellow-400", children: "\u25C6 yes" }) }),
1012
+ step.allowFailures && /* @__PURE__ */ jsx4(Row, { label: "Allow failures", children: /* @__PURE__ */ jsx4("span", { className: "text-blue-400", children: "yes" }) })
1013
+ ] }),
1014
+ step.parentNames.length > 0 && /* @__PURE__ */ jsx4(Section, { title: "Parents", children: /* @__PURE__ */ jsx4("div", { className: "flex flex-wrap gap-1", children: step.parentNames.map((p) => /* @__PURE__ */ jsx4(
1015
+ "button",
1016
+ {
1017
+ className: "rounded bg-gray-700 px-2 py-0.5 font-mono text-xs text-gray-300 hover:bg-gray-600",
1018
+ onClick: () => onNavigate?.(p),
1019
+ children: p
1020
+ },
1021
+ p
1022
+ )) }) }),
1023
+ step.error && /* @__PURE__ */ jsx4(Section, { title: "Error", children: /* @__PURE__ */ jsx4("pre", { className: "overflow-x-auto whitespace-pre-wrap rounded bg-red-950 p-2 font-mono text-xs text-red-300", children: step.error }) }),
1024
+ step.output != null && /* @__PURE__ */ jsx4(Section, { title: "Output", children: /* @__PURE__ */ jsx4(JsonBlock, { data: step.output }) }),
1025
+ step.output == null && step.status === "success" && /* @__PURE__ */ jsx4(Section, { title: "Output", children: /* @__PURE__ */ jsx4("span", { className: "text-xs text-gray-500", children: "No output data" }) })
1026
+ ] });
1027
+ var GroupDetails = ({ group, onNavigate }) => /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-4", children: [
1028
+ /* @__PURE__ */ jsxs3(Section, { title: "Group Info", children: [
1029
+ /* @__PURE__ */ jsx4(Row, { label: "Sealed", children: group.sealed ? "yes" : "no" }),
1030
+ /* @__PURE__ */ jsx4(Row, { label: "Members", children: group.members.length })
1031
+ ] }),
1032
+ group.dependsOn.length > 0 && /* @__PURE__ */ jsx4(Section, { title: "Depends On", children: /* @__PURE__ */ jsx4("div", { className: "flex flex-wrap gap-1", children: group.dependsOn.map((d) => /* @__PURE__ */ jsx4(
1033
+ "button",
1034
+ {
1035
+ className: "rounded bg-gray-700 px-2 py-0.5 font-mono text-xs text-gray-300 hover:bg-gray-600",
1036
+ onClick: () => onNavigate?.(d),
1037
+ children: d
1038
+ },
1039
+ d
1040
+ )) }) }),
1041
+ group.members.length > 0 && /* @__PURE__ */ jsx4(Section, { title: "Members", children: /* @__PURE__ */ jsx4("div", { className: "flex flex-col gap-1", children: group.members.map((m) => /* @__PURE__ */ jsxs3(
1042
+ "button",
1043
+ {
1044
+ className: "flex items-center justify-between rounded bg-gray-800 px-2 py-1.5 text-left hover:bg-gray-750",
1045
+ onClick: () => onNavigate?.(m.name),
1046
+ children: [
1047
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
1048
+ /* @__PURE__ */ jsx4(StatusDot, { status: m.status }),
1049
+ /* @__PURE__ */ jsx4("span", { className: "font-mono text-xs text-gray-300", children: m.name }),
1050
+ m.type === "subgraph" && /* @__PURE__ */ jsx4("span", { className: "rounded bg-gray-700 px-1 py-0.5 text-xs text-gray-500", children: "subgraph" })
1051
+ ] }),
1052
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
1053
+ m.error && /* @__PURE__ */ jsx4("span", { className: "text-xs text-red-400", children: "error" }),
1054
+ m.durationMs != null && m.durationMs > 0 && /* @__PURE__ */ jsx4("span", { className: "font-mono text-xs text-gray-500", children: formatDuration(m.durationMs) })
1055
+ ] })
1056
+ ]
1057
+ },
1058
+ m.name
1059
+ )) }) })
1060
+ ] });
1061
+ var MemberDetails = ({ member, onNavigate }) => /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-4", children: [
1062
+ /* @__PURE__ */ jsxs3(Section, { title: "Status", children: [
1063
+ /* @__PURE__ */ jsx4(Row, { label: "Status", children: /* @__PURE__ */ jsxs3("span", { className: "font-mono", children: [
1064
+ statusIndicator(member.status),
1065
+ " ",
1066
+ member.status
1067
+ ] }) }),
1068
+ /* @__PURE__ */ jsx4(Row, { label: "Type", children: /* @__PURE__ */ jsx4("span", { className: "font-mono", children: member.type }) }),
1069
+ member.durationMs != null && member.durationMs > 0 && /* @__PURE__ */ jsx4(Row, { label: "Duration", children: /* @__PURE__ */ jsx4("span", { className: "font-mono", children: formatDuration(member.durationMs) }) }),
1070
+ member.startedAt != null && /* @__PURE__ */ jsx4(Row, { label: "Started", children: formatTime(member.startedAt) }),
1071
+ member.completedAt != null && /* @__PURE__ */ jsx4(Row, { label: "Completed", children: formatTime(member.completedAt) })
1072
+ ] }),
1073
+ member.error && /* @__PURE__ */ jsx4(Section, { title: "Error", children: /* @__PURE__ */ jsx4("pre", { className: "overflow-x-auto whitespace-pre-wrap rounded bg-red-950 p-2 font-mono text-xs text-red-300", children: member.error }) }),
1074
+ member.output != null && /* @__PURE__ */ jsx4(Section, { title: "Output", children: /* @__PURE__ */ jsx4(JsonBlock, { data: member.output }) }),
1075
+ member.subgraph && member.subgraph.flow.steps.length > 0 && /* @__PURE__ */ jsx4(Section, { title: "Subgraph Steps", children: /* @__PURE__ */ jsx4("div", { className: "flex flex-col gap-1", children: member.subgraph.flow.steps.map((s) => /* @__PURE__ */ jsx4(
1076
+ "button",
1077
+ {
1078
+ className: "block w-full text-left",
1079
+ onClick: () => onNavigate?.(s.name),
1080
+ children: /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between rounded bg-gray-800 px-2 py-1.5 hover:bg-gray-750", children: [
1081
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
1082
+ /* @__PURE__ */ jsx4(StatusDot, { status: s.status }),
1083
+ /* @__PURE__ */ jsx4("span", { className: "font-mono text-xs text-gray-300", children: s.name })
1084
+ ] }),
1085
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
1086
+ s.error && /* @__PURE__ */ jsx4("span", { className: "text-xs text-red-400", children: "error" }),
1087
+ s.durationMs != null && s.durationMs > 0 && /* @__PURE__ */ jsx4("span", { className: "font-mono text-xs text-gray-500", children: formatDuration(s.durationMs) })
1088
+ ] })
1089
+ ] })
1090
+ },
1091
+ s.name
1092
+ )) }) })
1093
+ ] });
1094
+ var SubgraphStepDetails = ({ memberName, onNavigate, step }) => /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-4", children: [
1095
+ /* @__PURE__ */ jsxs3(Section, { title: "Status", children: [
1096
+ /* @__PURE__ */ jsx4(Row, { label: "Status", children: /* @__PURE__ */ jsxs3("span", { className: "font-mono", children: [
1097
+ statusIndicator(step.status),
1098
+ " ",
1099
+ step.status
1100
+ ] }) }),
1101
+ step.durationMs != null && step.durationMs > 0 && /* @__PURE__ */ jsx4(Row, { label: "Duration", children: /* @__PURE__ */ jsx4("span", { className: "font-mono", children: formatDuration(step.durationMs) }) }),
1102
+ step.startedAt != null && /* @__PURE__ */ jsx4(Row, { label: "Started", children: formatTime(step.startedAt) }),
1103
+ step.completedAt != null && /* @__PURE__ */ jsx4(Row, { label: "Completed", children: formatTime(step.completedAt) })
1104
+ ] }),
1105
+ /* @__PURE__ */ jsx4(Section, { title: "Part of", children: /* @__PURE__ */ jsx4(
1106
+ "button",
1107
+ {
1108
+ className: "rounded bg-gray-700 px-2 py-0.5 font-mono text-xs text-gray-300 hover:bg-gray-600",
1109
+ onClick: () => onNavigate?.(memberName),
1110
+ children: memberName
1111
+ }
1112
+ ) }),
1113
+ step.parentNames.length > 0 && /* @__PURE__ */ jsx4(Section, { title: "Parents", children: /* @__PURE__ */ jsx4("div", { className: "flex flex-wrap gap-1", children: step.parentNames.map((p) => /* @__PURE__ */ jsx4(
1114
+ "button",
1115
+ {
1116
+ className: "rounded bg-gray-700 px-2 py-0.5 font-mono text-xs text-gray-300 hover:bg-gray-600",
1117
+ onClick: () => onNavigate?.(p),
1118
+ children: p
1119
+ },
1120
+ p
1121
+ )) }) }),
1122
+ step.error && /* @__PURE__ */ jsx4(Section, { title: "Error", children: /* @__PURE__ */ jsx4("pre", { className: "overflow-x-auto whitespace-pre-wrap rounded bg-red-950 p-2 font-mono text-xs text-red-300", children: step.error }) }),
1123
+ step.output != null && /* @__PURE__ */ jsx4(Section, { title: "Output", children: /* @__PURE__ */ jsx4(JsonBlock, { data: step.output }) }),
1124
+ step.output == null && step.status === "success" && /* @__PURE__ */ jsx4(Section, { title: "Output", children: /* @__PURE__ */ jsx4("span", { className: "text-xs text-gray-500", children: "No output data" }) })
1125
+ ] });
1126
+ var Section = ({ children, title }) => /* @__PURE__ */ jsxs3("div", { children: [
1127
+ /* @__PURE__ */ jsx4("h3", { className: "mb-1.5 text-xs font-semibold uppercase tracking-wider text-gray-500", children: title }),
1128
+ children
1129
+ ] });
1130
+ var Row = ({ children, label }) => /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between py-0.5 text-xs", children: [
1131
+ /* @__PURE__ */ jsx4("span", { className: "text-gray-500", children: label }),
1132
+ /* @__PURE__ */ jsx4("span", { className: "text-gray-300", children })
1133
+ ] });
1134
+ var JsonBlock = ({ data }) => /* @__PURE__ */ jsx4("pre", { className: "max-h-64 overflow-auto whitespace-pre-wrap rounded bg-gray-800 p-2 font-mono text-xs text-gray-300", children: JSON.stringify(data, null, 2) });
1135
+ function findMember(snapshot, name) {
1136
+ for (const g of snapshot.flow.groups) {
1137
+ const m = g.members.find((m2) => m2.name === name);
1138
+ if (m) return m;
1139
+ }
1140
+ return void 0;
1141
+ }
1142
+ function findSubgraphStep(snapshot, name) {
1143
+ for (const g of snapshot.flow.groups) {
1144
+ for (const m of g.members) {
1145
+ if (m.subgraph) {
1146
+ const s = m.subgraph.flow.steps.find((s2) => s2.name === name);
1147
+ if (s) return { memberName: m.name, step: s };
1148
+ }
1149
+ }
1150
+ }
1151
+ return void 0;
1152
+ }
1153
+ function formatTime(ts) {
1154
+ return new Date(ts).toLocaleTimeString();
1155
+ }
1156
+ function deriveGroupStatus2(group) {
1157
+ if (!group) return void 0;
1158
+ if (group.members.length === 0) return "pending";
1159
+ if (group.members.some((m) => m.status === "running")) return "running";
1160
+ if (group.members.every((m) => m.status === "success")) return "success";
1161
+ if (group.members.every((m) => m.status === "success" || m.status === "error"))
1162
+ return "error";
1163
+ return "pending";
1164
+ }
1165
+
1166
+ // src/react/nurt-flow-chart-with-panel.tsx
1167
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1168
+ var NurtFlowChartWithPanel = ({
1169
+ className,
1170
+ snapshot
1171
+ }) => {
1172
+ const [selectedNode, setSelectedNode] = useState2(null);
1173
+ const handleNodeClick = useCallback((name) => {
1174
+ setSelectedNode((prev) => prev === name ? null : name);
1175
+ }, []);
1176
+ const handleClose = useCallback(() => {
1177
+ setSelectedNode(null);
1178
+ }, []);
1179
+ const handleNavigate = useCallback((name) => {
1180
+ setSelectedNode(name);
1181
+ }, []);
1182
+ useEffect2(() => {
1183
+ if (!selectedNode) return;
1184
+ const handleKey = (e) => {
1185
+ if (e.key === "Escape") setSelectedNode(null);
1186
+ };
1187
+ document.addEventListener("keydown", handleKey);
1188
+ return () => document.removeEventListener("keydown", handleKey);
1189
+ }, [selectedNode]);
1190
+ const isOpen = selectedNode !== null;
1191
+ return /* @__PURE__ */ jsxs4("div", { className: `flex overflow-hidden ${className ?? ""}`, children: [
1192
+ /* @__PURE__ */ jsx5(
1193
+ "div",
1194
+ {
1195
+ className: "min-w-0 transition-all duration-200 ease-out",
1196
+ style: { flex: isOpen ? "1 1 0%" : "1 1 100%" },
1197
+ children: /* @__PURE__ */ jsx5(NurtFlowChart, { onNodeClick: handleNodeClick, snapshot })
1198
+ }
1199
+ ),
1200
+ /* @__PURE__ */ jsx5(
1201
+ "div",
1202
+ {
1203
+ className: "shrink-0 overflow-hidden border-gray-700 bg-gray-900 transition-all duration-200 ease-out",
1204
+ style: {
1205
+ borderLeftWidth: isOpen ? 1 : 0,
1206
+ opacity: isOpen ? 1 : 0,
1207
+ width: isOpen ? 320 : 0
1208
+ },
1209
+ children: selectedNode && /* @__PURE__ */ jsx5("div", { className: "h-full w-80", children: /* @__PURE__ */ jsx5(
1210
+ NurtStepDetailPanel,
1211
+ {
1212
+ onClose: handleClose,
1213
+ onNavigate: handleNavigate,
1214
+ selectedNode,
1215
+ snapshot
1216
+ }
1217
+ ) })
1218
+ }
1219
+ )
1220
+ ] });
1221
+ };
1222
+ export {
1223
+ FlowEdge,
1224
+ NurtFlowChart,
1225
+ NurtFlowChartWithPanel,
1226
+ NurtStepDetailPanel,
1227
+ formatDuration,
1228
+ getMemberStyle,
1229
+ getNodeStyle,
1230
+ layoutSnapshot,
1231
+ statusIndicator,
1232
+ statusTextColor
1233
+ };
1234
+ //# sourceMappingURL=index.js.map