@sanity/workflow-diagram 0.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,2112 @@
1
+ import { jsxs, jsx, Fragment as Fragment$1 } from "react/jsx-runtime";
2
+
3
+ import "@xyflow/react/dist/style.css";
4
+
5
+ import { Stack, Text, Tooltip, Box, Card } from "@sanity/ui";
6
+
7
+ import { MarkerType, getSmoothStepPath, Handle, Position, BaseEdge, EdgeLabelRenderer, ReactFlowProvider, ReactFlow, Background, BackgroundVariant, Controls } from "@xyflow/react";
8
+
9
+ import { createContext, useContext, Fragment, useState, useMemo, useEffect, useRef, useCallback } from "react";
10
+
11
+ import { sentenceCase, describeSiteHeading, describeCondition, checklistLines, isTerminalStage } from "@sanity/workflow-engine";
12
+
13
+ import ELK from "elkjs/lib/elk.bundled.js";
14
+
15
+ import { CheckmarkCircleIcon, CircleIcon, LockIcon } from "@sanity/icons";
16
+
17
+ import { createPortal } from "react-dom";
18
+
19
+ const DevModeContext = createContext(!1);
20
+
21
+ function useDevMode() {
22
+ return useContext(DevModeContext);
23
+ }
24
+
25
+ const HoveredStageContext = createContext(void 0);
26
+
27
+ function useHoveredStage() {
28
+ return useContext(HoveredStageContext);
29
+ }
30
+
31
+ const ExplainContext = createContext(void 0);
32
+
33
+ function useExplain() {
34
+ return useContext(ExplainContext);
35
+ }
36
+
37
+ function indexExplanations(sites, definition) {
38
+ const transitions = /* @__PURE__ */ new Map, byStage = /* @__PURE__ */ new Map;
39
+ for (const site of sites) {
40
+ if (site.stage === void 0) continue;
41
+ if (site.address.kind === "transition") {
42
+ transitions.set(`${site.stage}#${site.address.transition}`, {
43
+ description: site.description,
44
+ live: !1
45
+ });
46
+ continue;
47
+ }
48
+ if (site.address.kind === "action") continue;
49
+ const heading = sentenceCase(describeSiteHeading(site.address, {
50
+ definition: definition
51
+ }).text), list = byStage.get(site.stage) ?? [];
52
+ list.push({
53
+ heading: heading,
54
+ description: site.description,
55
+ live: !1
56
+ }), byStage.set(site.stage, list);
57
+ }
58
+ return {
59
+ transition: (stage, name) => transitions.get(`${stage}#${name}`),
60
+ stageGates: stage => byStage.get(stage) ?? []
61
+ };
62
+ }
63
+
64
+ function indexLiveExplanations(evaluation) {
65
+ const ctx = {
66
+ definition: evaluation.definition
67
+ }, stageName = evaluation.currentStage.stage.name, transitions = /* @__PURE__ */ new Map;
68
+ for (const t of evaluation.currentStage.transitions) transitions.set(`${stageName}#${t.transition.name}`, {
69
+ description: describeCondition(t.insight, ctx),
70
+ live: !0
71
+ });
72
+ const gates = [], gate = (address, insight) => {
73
+ insight !== void 0 && gates.push({
74
+ heading: sentenceCase(describeSiteHeading(address, ctx).text),
75
+ description: describeCondition(insight, ctx),
76
+ live: !0
77
+ });
78
+ };
79
+ for (const a of evaluation.currentStage.activities) {
80
+ const activity = a.activity.name;
81
+ gate({
82
+ kind: "activity-filter",
83
+ activity: activity
84
+ }, a.filterInsight);
85
+ for (const [requirement, insight] of Object.entries(a.requirementInsights ?? {})) gate({
86
+ kind: "requirement",
87
+ activity: activity,
88
+ requirement: requirement
89
+ }, insight);
90
+ gate({
91
+ kind: "complete-when",
92
+ activity: activity
93
+ }, a.completeWhenInsight), gate({
94
+ kind: "fail-when",
95
+ activity: activity
96
+ }, a.failWhenInsight);
97
+ }
98
+ for (const field of evaluation.editableFields) {
99
+ if (field.scope === "workflow") continue;
100
+ const address = field.scope === "activity" && field.activity !== void 0 ? {
101
+ kind: "editable-field",
102
+ scope: field.scope,
103
+ name: field.name,
104
+ activity: field.activity
105
+ } : {
106
+ kind: "editable-field",
107
+ scope: field.scope,
108
+ name: field.name
109
+ };
110
+ gate(address, field.insight);
111
+ }
112
+ return {
113
+ transition: (stage, name) => transitions.get(`${stage}#${name}`),
114
+ stageGates: stage => stage === stageName ? gates : []
115
+ };
116
+ }
117
+
118
+ function mergeExplainData(live, fallback) {
119
+ return !live || !fallback ? live ?? fallback : {
120
+ transition: (stage, name) => live.transition(stage, name) ?? fallback.transition(stage, name),
121
+ stageGates: stage => {
122
+ const gates = live.stageGates(stage);
123
+ return gates.length > 0 ? gates : fallback.stageGates(stage);
124
+ }
125
+ };
126
+ }
127
+
128
+ function ExplainChecklist({description: description, marks: marks = !1}) {
129
+ const root = description.checklist, checklist = root.kind === "group" && root.items.length === 1 ? root.items[0] : root, rows = checklistLines({
130
+ ...description,
131
+ checklist: checklist
132
+ }, {
133
+ marks: marks
134
+ });
135
+ /* @__PURE__ */
136
+ return jsx(Stack, {
137
+ space: 2,
138
+ children: rows.map((row, i) => /* @__PURE__ */ jsx(Text, {
139
+ size: 1,
140
+ muted: !0,
141
+ style: {
142
+ lineHeight: 1.4,
143
+ whiteSpace: "pre-wrap"
144
+ },
145
+ children: row
146
+ }, i))
147
+ });
148
+ }
149
+
150
+ const STAGE_GATE_CAP = 4;
151
+
152
+ function cappedGates(gates) {
153
+ const shown = gates.slice(0, STAGE_GATE_CAP), hidden = gates.length - shown.length;
154
+ return {
155
+ shown: shown,
156
+ overflowNote: hidden > 0 ? `+ ${hidden} more gate${hidden === 1 ? "" : "s"}` : void 0
157
+ };
158
+ }
159
+
160
+ function ExplainedGateView({gate: gate}) {
161
+ /* @__PURE__ */
162
+ return jsxs(Stack, {
163
+ space: 2,
164
+ children: [
165
+ /* @__PURE__ */ jsx(Text, {
166
+ size: 1,
167
+ weight: "semibold",
168
+ muted: !0,
169
+ children: gate.heading
170
+ }),
171
+ /* @__PURE__ */ jsx(ExplainChecklist, {
172
+ description: gate.description,
173
+ marks: gate.live
174
+ }) ]
175
+ });
176
+ }
177
+
178
+ function edgeMarker(traversed) {
179
+ return {
180
+ type: MarkerType.ArrowClosed,
181
+ width: 18,
182
+ height: 18,
183
+ color: traversed ? "var(--ws-positive)" : "var(--ws-border)"
184
+ };
185
+ }
186
+
187
+ function pointsToPath(points, radius = 8) {
188
+ const first = points[0];
189
+ if (!first) return "";
190
+ if (points.length === 1) return `M${first.x},${first.y}`;
191
+ const second = points[1];
192
+ if (points.length === 2) return `M${first.x},${first.y} L${second.x},${second.y}`;
193
+ let path = `M${first.x},${first.y}`;
194
+ for (let i = 1; i < points.length - 1; i++) {
195
+ const prev = points[i - 1], curr = points[i], next = points[i + 1], inDx = curr.x - prev.x, inDy = curr.y - prev.y, outDx = next.x - curr.x, outDy = next.y - curr.y, inLen = Math.hypot(inDx, inDy), outLen = Math.hypot(outDx, outDy), r = Math.min(radius, inLen / 2, outLen / 2);
196
+ if (r <= 0 || !Number.isFinite(r)) {
197
+ path += ` L${curr.x},${curr.y}`;
198
+ continue;
199
+ }
200
+ const inEndX = curr.x - inDx / inLen * r, inEndY = curr.y - inDy / inLen * r, outStartX = curr.x + outDx / outLen * r, outStartY = curr.y + outDy / outLen * r;
201
+ path += ` L${inEndX},${inEndY} Q${curr.x},${curr.y} ${outStartX},${outStartY}`;
202
+ }
203
+ const last = points[points.length - 1];
204
+ return path += ` L${last.x},${last.y}`, path;
205
+ }
206
+
207
+ const ARROW_LEN = 11, ARROW_HALF_WIDTH = 6;
208
+
209
+ function arrowHead(points) {
210
+ const tip = points[points.length - 1], prev = points[points.length - 2], dx = tip.x - prev.x, dy = tip.y - prev.y, len = Math.hypot(dx, dy) || 1, ux = dx / len, uy = dy / len, baseX = tip.x - ux * ARROW_LEN, baseY = tip.y - uy * ARROW_LEN, px = -uy, py = ux, c1x = baseX + px * ARROW_HALF_WIDTH, c1y = baseY + py * ARROW_HALF_WIDTH, c2x = baseX - px * ARROW_HALF_WIDTH, c2y = baseY - py * ARROW_HALF_WIDTH;
211
+ return {
212
+ line: [ ...points.slice(0, -1), {
213
+ x: baseX,
214
+ y: baseY
215
+ } ],
216
+ triangle: `${tip.x},${tip.y} ${c1x},${c1y} ${c2x},${c2y}`
217
+ };
218
+ }
219
+
220
+ function transitionPath(route, fallback) {
221
+ if (route && route.points.length >= 2) {
222
+ const head = arrowHead(route.points), mid = longestSegmentMidpoint(route.points);
223
+ return {
224
+ path: pointsToPath(head.line),
225
+ markerX: mid.x,
226
+ markerY: mid.y,
227
+ triangle: head.triangle
228
+ };
229
+ }
230
+ const [path, markerX, markerY] = getSmoothStepPath({
231
+ ...fallback,
232
+ borderRadius: 10
233
+ });
234
+ return {
235
+ path: path,
236
+ markerX: markerX,
237
+ markerY: markerY,
238
+ triangle: null
239
+ };
240
+ }
241
+
242
+ function longestSegmentMidpoint(points) {
243
+ let bestLen = -1, mid = points[0] ?? {
244
+ x: 0,
245
+ y: 0
246
+ };
247
+ for (let i = 0; i < points.length - 1; i++) {
248
+ const a = points[i], b = points[i + 1], len = Math.hypot(b.x - a.x, b.y - a.y);
249
+ len > bestLen && (bestLen = len, mid = {
250
+ x: (a.x + b.x) / 2,
251
+ y: (a.y + b.y) / 2
252
+ });
253
+ }
254
+ return mid;
255
+ }
256
+
257
+ const GHOST_SIZE = 16, ghostId = stage => `ghost:${stage}`;
258
+
259
+ function makeGhost(id, title) {
260
+ return {
261
+ id: id,
262
+ type: "ghost",
263
+ position: {
264
+ x: 0,
265
+ y: 0
266
+ },
267
+ width: GHOST_SIZE,
268
+ height: GHOST_SIZE,
269
+ data: {
270
+ title: title
271
+ }
272
+ };
273
+ }
274
+
275
+ function visibleStages(incident, anchor) {
276
+ const visible = /* @__PURE__ */ new Set([ anchor.currentStage ]);
277
+ for (const e of incident) anchor.show !== "previous" && e.source === anchor.currentStage && visible.add(e.target),
278
+ anchor.show !== "next" && e.target === anchor.currentStage && visible.add(e.source);
279
+ return visible;
280
+ }
281
+
282
+ const GROUP_HEIGHT = 36, GROUP_CHAR_PX = 8.5, GROUP_PAD_X = 14;
283
+
284
+ function sideAggregate(edges, opts) {
285
+ const far = e => opts.farEnd === "source" ? e.source : e.target, datas = edges.map(e => e.data), stageNames = [ ...new Set(edges.map(far)) ];
286
+ return {
287
+ stageNames: stageNames,
288
+ stageTitles: stageNames.map(s => opts.titleOf.get(s) ?? s),
289
+ transitionTitles: datas.map(d => d.transition.title ?? d.transition.name),
290
+ traversed: datas.some(d => d.wasTraversed),
291
+ gatedCount: datas.filter(d => d.gated).length
292
+ };
293
+ }
294
+
295
+ function groupNode(id, agg) {
296
+ const count = agg.stageTitles.length, label = `${count} stage${count === 1 ? "" : "s"}`, width = Math.round(label.length * GROUP_CHAR_PX) + GROUP_PAD_X * 2;
297
+ return {
298
+ id: id,
299
+ type: "stageGroup",
300
+ position: {
301
+ x: 0,
302
+ y: 0
303
+ },
304
+ width: width,
305
+ height: GROUP_HEIGHT,
306
+ data: {
307
+ label: label,
308
+ titles: agg.stageTitles,
309
+ boxWidth: width,
310
+ boxHeight: GROUP_HEIGHT
311
+ }
312
+ };
313
+ }
314
+
315
+ function groupEdge(agg, route) {
316
+ return {
317
+ id: `${route.from}->${route.to}#grouped`,
318
+ source: route.from,
319
+ sourceHandle: "t-out",
320
+ target: route.to,
321
+ targetHandle: "t-in",
322
+ type: "transition",
323
+ markerEnd: edgeMarker(agg.traversed),
324
+ data: {
325
+ transition: {
326
+ name: "grouped",
327
+ from: route.from,
328
+ to: route.to,
329
+ filter: "true"
330
+ },
331
+ wasTraversed: agg.traversed,
332
+ isPrimary: !0,
333
+ isReturn: !1,
334
+ gated: route.gated,
335
+ grouped: {
336
+ transitions: agg.transitionTitles,
337
+ gatedCount: agg.gatedCount
338
+ }
339
+ }
340
+ };
341
+ }
342
+
343
+ const dropLayerPin = n => ({
344
+ ...n,
345
+ data: {
346
+ ...n.data,
347
+ isInitial: !1
348
+ }
349
+ });
350
+
351
+ function sideView(side, ctx) {
352
+ if (side.edges.length === 0) return {
353
+ nodes: [],
354
+ edges: []
355
+ };
356
+ const agg = sideAggregate(side.edges, {
357
+ titleOf: ctx.titleOf,
358
+ farEnd: side.farEnd
359
+ });
360
+ if (agg.stageNames.length === 1) return {
361
+ nodes: ctx.nodes.filter(n => n.id === agg.stageNames[0]).map(dropLayerPin),
362
+ edges: side.edges.map(e => ({
363
+ ...e,
364
+ data: {
365
+ ...e.data,
366
+ isPrimary: !0,
367
+ isReturn: !1
368
+ }
369
+ }))
370
+ };
371
+ const gated = side.lockWhenAllGated && agg.gatedCount === side.edges.length, route = side.farEnd === "source" ? {
372
+ from: side.groupId,
373
+ to: ctx.currentStage,
374
+ gated: gated
375
+ } : {
376
+ from: ctx.currentStage,
377
+ to: side.groupId,
378
+ gated: gated
379
+ };
380
+ return {
381
+ nodes: [ groupNode(side.groupId, agg) ],
382
+ edges: [ groupEdge(agg, route) ]
383
+ };
384
+ }
385
+
386
+ function groupedView(model) {
387
+ const {nodes: nodes, incident: incident, currentStage: currentStage, titleOf: titleOf} = model, ctx = {
388
+ nodes: nodes,
389
+ currentStage: currentStage,
390
+ titleOf: titleOf
391
+ }, inSide = sideView({
392
+ edges: incident.filter(e => e.target === currentStage),
393
+ farEnd: "source",
394
+ groupId: "group:in",
395
+ lockWhenAllGated: !1
396
+ }, ctx), outSide = sideView({
397
+ edges: incident.filter(e => e.source === currentStage),
398
+ farEnd: "target",
399
+ groupId: "group:out",
400
+ lockWhenAllGated: !0
401
+ }, ctx), current = nodes.filter(n => n.id === currentStage).map(dropLayerPin), nodeById = new Map([ ...current, ...inSide.nodes, ...outSide.nodes ].map(n => [ n.id, n ])), edgeById = new Map([ ...inSide.edges, ...outSide.edges ].map(e => [ e.id, e ]));
402
+ return {
403
+ nodes: [ ...nodeById.values() ],
404
+ edges: [ ...edgeById.values() ]
405
+ };
406
+ }
407
+
408
+ function applyLocalView({nodes: nodes, edges: edges, currentStage: currentStage, view: view}) {
409
+ const show = view.show ?? "next", incident = edges.filter(e => e.source === currentStage || e.target === currentStage), titleOf = new Map(nodes.map(n => [ n.id, n.data.stage.title ?? n.data.stage.name ]));
410
+ if (view.groupEdges) return groupedView({
411
+ nodes: nodes,
412
+ incident: incident,
413
+ currentStage: currentStage,
414
+ titleOf: titleOf
415
+ });
416
+ const visible = visibleStages(incident, {
417
+ currentStage: currentStage,
418
+ show: show
419
+ }), ghosts = /* @__PURE__ */ new Map, ghostFor = stage => {
420
+ const id = ghostId(stage);
421
+ return ghosts.has(id) || ghosts.set(id, makeGhost(id, titleOf.get(stage) ?? stage)),
422
+ id;
423
+ }, trimmed = incident.map(e => {
424
+ const flat = {
425
+ ...e.data,
426
+ isPrimary: !0,
427
+ isReturn: !1
428
+ };
429
+ return visible.has(e.source) && visible.has(e.target) ? {
430
+ ...e,
431
+ data: flat
432
+ } : e.source === currentStage ? {
433
+ ...e,
434
+ target: ghostFor(e.target),
435
+ data: {
436
+ ...flat,
437
+ hiddenTo: titleOf.get(e.target) ?? e.target
438
+ }
439
+ } : {
440
+ ...e,
441
+ source: ghostFor(e.source),
442
+ data: {
443
+ ...flat,
444
+ hiddenFrom: titleOf.get(e.source) ?? e.source
445
+ }
446
+ };
447
+ }), renderedSources = new Set(trimmed.map(e => e.source)), continuations = [ ...visible ].filter(name => name !== currentStage && !renderedSources.has(name)).flatMap(name => {
448
+ const onward = edges.filter(e => e.source === name);
449
+ if (onward.length === 0) return [];
450
+ const agg = sideAggregate(onward, {
451
+ titleOf: titleOf,
452
+ farEnd: "target"
453
+ }), id = `ghost:onward:${name}`;
454
+ return ghosts.set(id, makeGhost(id, agg.stageTitles.join(", "))), [ groupEdge(agg, {
455
+ from: name,
456
+ to: id,
457
+ gated: !1
458
+ }) ];
459
+ });
460
+ return {
461
+ nodes: [ ...nodes.filter(n => visible.has(n.id)).map(dropLayerPin), ...ghosts.values() ],
462
+ edges: [ ...trimmed, ...continuations ]
463
+ };
464
+ }
465
+
466
+ function NodeShell({width: width, height: height, tooltip: tooltip, children: children}) {
467
+ /* @__PURE__ */
468
+ return jsxs("div", {
469
+ style: {
470
+ position: "relative",
471
+ width: width,
472
+ height: height,
473
+ overflow: "visible",
474
+ pointerEvents: "all"
475
+ },
476
+ children: [
477
+ /* @__PURE__ */ jsx(Handle, {
478
+ type: "target",
479
+ position: Position.Left,
480
+ id: "t-in",
481
+ style: {
482
+ opacity: 0
483
+ }
484
+ }),
485
+ /* @__PURE__ */ jsx(Handle, {
486
+ type: "source",
487
+ position: Position.Right,
488
+ id: "t-out",
489
+ style: {
490
+ opacity: 0
491
+ }
492
+ }),
493
+ /* @__PURE__ */ jsx(Tooltip, {
494
+ content: tooltip,
495
+ placement: "top",
496
+ portal: !0,
497
+ children: children
498
+ }) ]
499
+ });
500
+ }
501
+
502
+ function GhostNode({data: data}) {
503
+ /* @__PURE__ */
504
+ return jsx(NodeShell, {
505
+ width: GHOST_SIZE,
506
+ height: GHOST_SIZE,
507
+ tooltip: /* @__PURE__ */ jsx(Box, {
508
+ padding: 3,
509
+ children: /* @__PURE__ */ jsx(Text, {
510
+ size: 1,
511
+ weight: "semibold",
512
+ children: data.title
513
+ })
514
+ }),
515
+ children: /* @__PURE__ */ jsx("div", {
516
+ style: {
517
+ boxSizing: "border-box",
518
+ width: GHOST_SIZE,
519
+ height: GHOST_SIZE,
520
+ borderRadius: 999,
521
+ border: "2px dashed color-mix(in srgb, var(--ws-ink) 35%, transparent)",
522
+ cursor: "help"
523
+ }
524
+ })
525
+ });
526
+ }
527
+
528
+ const DEFAULT_WIDTH = 180, DEFAULT_HEIGHT = 72, elk = new ELK, STAGE_CHAR_PX = 8.5, STAGE_PAD_X = 16, STAGE_ICON_W = 25, STAGE_PAD_Y = 13, STAGE_LINE_H = 18, STAGE_MAX_TEXT_WIDTH = 150, STAGE_MIN_TEXT_WIDTH = 44;
529
+
530
+ function wrapStageLabel(title) {
531
+ const words = title.trim().split(/\s+/), lines = [];
532
+ let line = "";
533
+ for (const word of words) {
534
+ const candidate = line ? `${line} ${word}` : word;
535
+ line && candidate.length * STAGE_CHAR_PX > STAGE_MAX_TEXT_WIDTH ? (lines.push(line),
536
+ line = word) : line = candidate;
537
+ }
538
+ return [ ...lines, line ];
539
+ }
540
+
541
+ function stageNodeSize(title) {
542
+ const lines = wrapStageLabel(title), longest = Math.max(...lines.map(l => l.length)) * STAGE_CHAR_PX, textWidth = Math.min(STAGE_MAX_TEXT_WIDTH, Math.max(STAGE_MIN_TEXT_WIDTH, longest));
543
+ return {
544
+ width: Math.round(textWidth) + STAGE_PAD_X * 2 + STAGE_ICON_W,
545
+ height: lines.length * STAGE_LINE_H + STAGE_PAD_Y * 2
546
+ };
547
+ }
548
+
549
+ function defaultLayoutOptions() {
550
+ return {
551
+ "elk.algorithm": "layered",
552
+ "elk.direction": "RIGHT",
553
+ "elk.edgeRouting": "ORTHOGONAL",
554
+ "elk.layered.spacing.nodeNodeBetweenLayers": "56",
555
+ "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
556
+ "elk.layered.nodePlacement.favorStraightEdges": "true",
557
+ "elk.layered.spacing.edgeNodeBetweenLayers": "20",
558
+ "elk.layered.spacing.edgeEdgeBetweenLayers": "16",
559
+ "elk.spacing.nodeNode": "44",
560
+ "elk.spacing.edgeNode": "24",
561
+ "elk.spacing.edgeEdge": "16",
562
+ "elk.layered.cycleBreaking.strategy": "GREEDY"
563
+ };
564
+ }
565
+
566
+ function layerConstraintFor(n) {
567
+ const d = nodeFlags(n);
568
+ return d.isInitial ? {
569
+ layoutOptions: {
570
+ "elk.layered.layering.layerConstraint": "FIRST"
571
+ }
572
+ } : d.isTerminal ? {
573
+ layoutOptions: {
574
+ "elk.layered.layering.layerConstraint": "LAST"
575
+ }
576
+ } : {};
577
+ }
578
+
579
+ const nodeFlags = n => n.data ?? {}, edgeFlags = e => e.data ?? {};
580
+
581
+ function graphBounds(nodes, edges) {
582
+ const xs = [], ys = [];
583
+ for (const n of nodes) xs.push(n.position.x, n.position.x + (n.width ?? 0)), ys.push(n.position.y, n.position.y + (n.height ?? 0));
584
+ for (const e of edges) for (const p of edgeFlags(e).route?.points ?? []) xs.push(p.x),
585
+ ys.push(p.y);
586
+ if (xs.length === 0) return;
587
+ const x = Math.min(...xs), y = Math.min(...ys);
588
+ return {
589
+ x: x,
590
+ y: y,
591
+ width: Math.max(...xs) - x,
592
+ height: Math.max(...ys) - y
593
+ };
594
+ }
595
+
596
+ async function layoutGraph(nodes, edges) {
597
+ if (nodes.length === 0) return {
598
+ nodes: [ ...nodes ],
599
+ edges: [ ...edges ]
600
+ };
601
+ const railNodes = new Set(edges.flatMap(e => edgeFlags(e).isPrimary ? [ e.source, e.target ] : [])), boxOf = new Map(nodes.map(n => [ n.id, {
602
+ w: n.width ?? DEFAULT_WIDTH,
603
+ h: n.height ?? DEFAULT_HEIGHT
604
+ } ])), portMeta = /* @__PURE__ */ new Map, portFor = args => {
605
+ const id = `${args.node}.${args.key}`, list = portMeta.get(args.node) ?? [];
606
+ return list.push({
607
+ id: id,
608
+ side: args.side,
609
+ centered: args.centered,
610
+ peer: args.peer,
611
+ out: args.out
612
+ }), portMeta.set(args.node, list), id;
613
+ }, sourceSide = (flags, ends) => flags.isPrimary ? "EAST" : railNodes.has(ends.node) ? flags.isReturn && railNodes.has(ends.peer) ? "NORTH" : "SOUTH" : flags.isReturn ? "WEST" : "EAST", targetSide = (flags, ends) => !flags.isReturn || !railNodes.has(ends.node) ? "WEST" : railNodes.has(ends.peer) ? "NORTH" : "SOUTH", elkEdges = edges.filter(e => e.source !== e.target).map(e => {
614
+ const data = edgeFlags(e), isPrimary = !!data.isPrimary, isReturn = !!data.isReturn, flags = {
615
+ isPrimary: isPrimary,
616
+ isReturn: isReturn
617
+ };
618
+ return {
619
+ id: e.id,
620
+ sources: [ portFor({
621
+ node: e.source,
622
+ side: sourceSide(flags, {
623
+ node: e.source,
624
+ peer: e.target
625
+ }),
626
+ key: `out:${e.id}`,
627
+ centered: isPrimary,
628
+ peer: e.target,
629
+ out: !0
630
+ }) ],
631
+ targets: [ portFor({
632
+ node: e.target,
633
+ side: targetSide(flags, {
634
+ node: e.target,
635
+ peer: e.source
636
+ }),
637
+ key: `in:${e.id}`,
638
+ centered: isPrimary,
639
+ peer: e.source,
640
+ out: !1
641
+ }) ],
642
+ ...isPrimary ? {
643
+ layoutOptions: {
644
+ "elk.layered.priority.straightness": "10",
645
+ "elk.layered.priority.direction": "10"
646
+ }
647
+ } : {}
648
+ };
649
+ }), graphWith = (portsOf, constraint) => ({
650
+ id: "root",
651
+ layoutOptions: defaultLayoutOptions(),
652
+ children: nodes.map(n => ({
653
+ id: n.id,
654
+ width: n.width ?? DEFAULT_WIDTH,
655
+ height: n.height ?? DEFAULT_HEIGHT,
656
+ layoutOptions: {
657
+ ...layerConstraintFor(n).layoutOptions,
658
+ "elk.portConstraints": constraint
659
+ },
660
+ ports: portsOf(n.id)
661
+ })),
662
+ edges: elkEdges
663
+ }), pass1 = await elk.layout(graphWith(id => (portMeta.get(id) ?? []).map(p => ({
664
+ id: p.id,
665
+ layoutOptions: {
666
+ "elk.port.side": p.side
667
+ }
668
+ })), "FIXED_SIDE"));
669
+ faceOffRailPorts({
670
+ portMeta: portMeta,
671
+ railNodes: railNodes,
672
+ centers: pass1Centers(pass1, boxOf)
673
+ });
674
+ const positions = pinnedPortPositions({
675
+ pass1: pass1,
676
+ portMeta: portMeta,
677
+ boxOf: boxOf
678
+ }), result = await elk.layout(graphWith(id => (portMeta.get(id) ?? []).map(p => ({
679
+ id: p.id,
680
+ ...positions.get(p.id) ?? {},
681
+ layoutOptions: {
682
+ "elk.port.side": p.side
683
+ }
684
+ })), "FIXED_POS"));
685
+ return materializeLayout({
686
+ nodes: nodes,
687
+ edges: edges
688
+ }, result);
689
+ }
690
+
691
+ const PORT_STEP = 14;
692
+
693
+ function faceOffRailPorts(args) {
694
+ for (const [node, list] of args.portMeta) {
695
+ if (args.railNodes.has(node)) continue;
696
+ const here = args.centers.get(node);
697
+ if (!here) continue;
698
+ const level = list.filter(p => p.side === "EAST" || p.side === "WEST"), wants = level.map(p => {
699
+ const peer = args.centers.get(p.peer);
700
+ return !peer || Math.abs(peer.cx - here.cx) < 1 ? p.side : peer.cx > here.cx ? "EAST" : "WEST";
701
+ });
702
+ if (wants.length > 0 && wants.every(w => w === wants[0])) for (const p of level) p.side = wants[0];
703
+ }
704
+ }
705
+
706
+ function pass1Centers(pass1, boxOf) {
707
+ return new Map((pass1.children ?? []).map(c => {
708
+ const box = boxOf.get(c.id) ?? {
709
+ w: DEFAULT_WIDTH,
710
+ h: DEFAULT_HEIGHT
711
+ };
712
+ return [ c.id, {
713
+ cx: (c.x ?? 0) + box.w / 2,
714
+ cy: (c.y ?? 0) + box.h / 2
715
+ } ];
716
+ }));
717
+ }
718
+
719
+ function orderLevelSide(group, geo) {
720
+ const here = geo.centers.get(geo.node) ?? {
721
+ cy: 0
722
+ }, onRail = group.some(p => p.centered), fromAbove = p => {
723
+ const peer = geo.centers.get(p.peer) ?? {
724
+ cy: 0
725
+ };
726
+ return onRail && !p.out && Math.abs(peer.cy - here.cy) < 20 ? !1 : geo.along(p.id) < geo.sideMiddle;
727
+ }, byPass1 = (a, b) => geo.along(a.id) - geo.along(b.id), above = group.filter(p => !p.centered && fromAbove(p)).sort(byPass1), below = group.filter(p => !p.centered && !fromAbove(p)).sort(byPass1);
728
+ return [ ...group.filter(p => p.centered).map(port => ({
729
+ port: port,
730
+ slot: 0
731
+ })), ...above.map((port, i) => ({
732
+ port: port,
733
+ slot: i - above.length
734
+ })), ...below.map((port, i) => ({
735
+ port: port,
736
+ slot: i + 1
737
+ })) ];
738
+ }
739
+
740
+ function orderSouthSide(group, geo) {
741
+ const depth = p => geo.centers.get(p.peer)?.cy ?? 0, base = [ ...group ].sort((a, b) => geo.along(a.id) - geo.along(b.id)), outs = base.filter(p => p.out).sort((a, b) => depth(b) - depth(a) || geo.along(a.id) - geo.along(b.id));
742
+ let outIdx = 0;
743
+ const ordered = base.map(p => p.out ? outs[outIdx++] : p), mid = (ordered.length - 1) / 2;
744
+ return ordered.map((port, i) => ({
745
+ port: port,
746
+ slot: i - mid
747
+ }));
748
+ }
749
+
750
+ function portPointAt(side, at) {
751
+ return side === "EAST" ? {
752
+ x: at.box.w,
753
+ y: at.box.h / 2 + at.offset
754
+ } : side === "WEST" ? {
755
+ x: 0,
756
+ y: at.box.h / 2 + at.offset
757
+ } : {
758
+ x: at.box.w / 2 + at.offset,
759
+ y: side === "NORTH" ? 0 : at.box.h
760
+ };
761
+ }
762
+
763
+ function pinChildPorts(child, ctx) {
764
+ const laidOut = new Map((child.ports ?? []).map(p => [ p.id, p ]));
765
+ for (const side of [ "EAST", "WEST", "SOUTH", "NORTH" ]) {
766
+ const group = ctx.meta.filter(p => p.side === side);
767
+ if (group.length === 0) continue;
768
+ const stack = side === "SOUTH" || side === "NORTH", along = id => {
769
+ const p = laidOut.get(id);
770
+ return stack ? p?.x ?? 0 : p?.y ?? 0;
771
+ }, slotted = stack ? orderSouthSide(group, {
772
+ node: child.id,
773
+ centers: ctx.centers,
774
+ along: along
775
+ }) : orderLevelSide(group, {
776
+ node: child.id,
777
+ centers: ctx.centers,
778
+ along: along,
779
+ sideMiddle: ctx.box.h / 2
780
+ });
781
+ for (const {port: port, slot: slot} of slotted) ctx.out.set(port.id, portPointAt(side, {
782
+ box: ctx.box,
783
+ offset: slot * PORT_STEP
784
+ }));
785
+ }
786
+ }
787
+
788
+ function pinnedPortPositions(args) {
789
+ const centers = pass1Centers(args.pass1, args.boxOf), out = /* @__PURE__ */ new Map;
790
+ for (const child of args.pass1.children ?? []) pinChildPorts(child, {
791
+ meta: args.portMeta.get(child.id) ?? [],
792
+ box: args.boxOf.get(child.id) ?? {
793
+ w: DEFAULT_WIDTH,
794
+ h: DEFAULT_HEIGHT
795
+ },
796
+ centers: centers,
797
+ out: out
798
+ });
799
+ return out;
800
+ }
801
+
802
+ function nearestSide(p, rect) {
803
+ const dLeft = Math.abs(p.x - rect.x), dRight = Math.abs(p.x - (rect.x + rect.w)), dTop = Math.abs(p.y - rect.y), dBottom = Math.abs(p.y - (rect.y + rect.h)), min = Math.min(dLeft, dRight, dTop, dBottom);
804
+ return min === dLeft ? "left" : min === dRight ? "right" : min === dTop ? "top" : "bottom";
805
+ }
806
+
807
+ function boundaryPoint({rect: rect, side: side}, offset) {
808
+ const r = Math.min(rect.w, rect.h) / 2, cx = rect.x + rect.w / 2, cy = rect.y + rect.h / 2;
809
+ if (side === "left" || side === "right") {
810
+ const flat2 = rect.h / 2 - r, over2 = Math.max(0, Math.abs(offset) - flat2), inset2 = r - Math.sqrt(Math.max(0, r * r - over2 * over2));
811
+ return {
812
+ x: side === "left" ? rect.x + inset2 : rect.x + rect.w - inset2,
813
+ y: cy + offset
814
+ };
815
+ }
816
+ const flat = rect.w / 2 - r, over = Math.max(0, Math.abs(offset) - flat), inset = r - Math.sqrt(Math.max(0, r * r - over * over));
817
+ return {
818
+ x: cx + offset,
819
+ y: side === "top" ? rect.y + inset : rect.y + rect.h - inset
820
+ };
821
+ }
822
+
823
+ function nbrCross(ep) {
824
+ const nbr = ep.pts[ep.nbrIdx];
825
+ return ep.side === "left" || ep.side === "right" ? nbr.y : nbr.x;
826
+ }
827
+
828
+ const FAN_MAX_SPACING = 14, SIDE_PAD = 8;
829
+
830
+ function placeEndpoints(endpoints) {
831
+ const groups = /* @__PURE__ */ new Map;
832
+ for (const ep of endpoints) {
833
+ const key = `${ep.rect.x},${ep.rect.y}:${ep.side}`, list = groups.get(key);
834
+ list ? list.push(ep) : groups.set(key, [ ep ]);
835
+ }
836
+ for (const group of groups.values()) {
837
+ const n = group.length, {side: side, rect: rect} = group[0], span = side === "left" || side === "right" ? rect.h : rect.w, usable = Math.max(0, span - 2 * SIDE_PAD);
838
+ group.sort((a, b) => nbrCross(a) - nbrCross(b));
839
+ const slots = fanSlots(group), maxAbsSlot = Math.max(...slots.map(Math.abs), 1), spacing = n > 1 ? Math.min(FAN_MAX_SPACING, usable / 2 / maxAbsSlot) : 0;
840
+ group.forEach((ep, i) => {
841
+ const offset = slots[i] * spacing, bp = boundaryPoint(ep, offset);
842
+ ep.pts[ep.endIdx] = bp;
843
+ const nbr = ep.pts[ep.nbrIdx];
844
+ nbr && (ep.pts[ep.nbrIdx] = ep.side === "left" || ep.side === "right" ? {
845
+ x: nbr.x,
846
+ y: bp.y
847
+ } : {
848
+ x: bp.x,
849
+ y: nbr.y
850
+ });
851
+ });
852
+ }
853
+ endpoints.forEach(reprojectEndpoint);
854
+ }
855
+
856
+ function fanSlots(group) {
857
+ const railIdx = group.findIndex(ep => ep.keepCenter);
858
+ if (railIdx < 0) return group.map((_, i) => i - (group.length - 1) / 2);
859
+ const others = group.length - 1, negatives = Math.floor(others / 2), available = [];
860
+ for (let s = -negatives; s <= others - negatives; s++) s !== 0 && available.push(s);
861
+ let next = 0;
862
+ return group.map((_, i) => i === railIdx ? 0 : available[next++]);
863
+ }
864
+
865
+ function reprojectEndpoint(ep) {
866
+ const p = ep.pts[ep.endIdx], horizontal = ep.side === "left" || ep.side === "right", mid = horizontal ? ep.rect.y + ep.rect.h / 2 : ep.rect.x + ep.rect.w / 2, cross = (horizontal ? p.y : p.x) - mid;
867
+ ep.pts[ep.endIdx] = boundaryPoint(ep, cross);
868
+ }
869
+
870
+ function endpointFor({points: points, rect: rect, keepCenter: keepCenter}, [endIdx, nbrIdx]) {
871
+ return {
872
+ pts: points,
873
+ endIdx: endIdx,
874
+ nbrIdx: nbrIdx,
875
+ rect: rect,
876
+ side: nearestSide(points[endIdx], rect),
877
+ keepCenter: keepCenter
878
+ };
879
+ }
880
+
881
+ function nodeRects(nodes, posById) {
882
+ const rectById = /* @__PURE__ */ new Map;
883
+ for (const n of nodes) {
884
+ const pos = posById.get(n.id);
885
+ pos && rectById.set(n.id, {
886
+ x: pos.x,
887
+ y: pos.y,
888
+ w: n.width ?? 0,
889
+ h: n.height ?? 0
890
+ });
891
+ }
892
+ return rectById;
893
+ }
894
+
895
+ function sectionPolyline(sec) {
896
+ return [ {
897
+ x: sec.startPoint.x,
898
+ y: sec.startPoint.y
899
+ }, ...(sec.bendPoints ?? []).map(p => ({
900
+ x: p.x,
901
+ y: p.y
902
+ })), {
903
+ x: sec.endPoint.x,
904
+ y: sec.endPoint.y
905
+ } ];
906
+ }
907
+
908
+ function routeEndpoints(points, rects) {
909
+ const out = [];
910
+ return points.length < 2 || (rects.src && rects.src.w > 0 && out.push(endpointFor({
911
+ points: points,
912
+ rect: rects.src,
913
+ keepCenter: rects.primary
914
+ }, [ 0, 1 ])), rects.tgt && rects.tgt.w > 0 && out.push(endpointFor({
915
+ points: points,
916
+ rect: rects.tgt,
917
+ keepCenter: rects.primary
918
+ }, [ points.length - 1, points.length - 2 ]))), out;
919
+ }
920
+
921
+ const JOG = 12, EPS = .01, near = (a, b) => Math.abs(a - b) < EPS;
922
+
923
+ function jogOrientation(pts, i) {
924
+ const [a, b, c, d] = [ pts[i - 1], pts[i], pts[i + 1], pts[i + 2] ];
925
+ if (!(Math.hypot(c.x - b.x, c.y - b.y) >= JOG)) {
926
+ if (near(a.y, b.y) && near(c.y, d.y) && near(b.x, c.x)) return !0;
927
+ if (near(a.x, b.x) && near(c.x, d.x) && near(b.y, c.y)) return !1;
928
+ }
929
+ }
930
+
931
+ function alignRun({pts: pts, horizontal: horizontal}, {at: at, lane: lane}) {
932
+ for (const k of [ at, at + 1 ]) {
933
+ const p = pts[k];
934
+ pts[k] = horizontal ? {
935
+ x: p.x,
936
+ y: lane.y
937
+ } : {
938
+ x: lane.x,
939
+ y: p.y
940
+ };
941
+ }
942
+ }
943
+
944
+ function snapJog(site) {
945
+ const {pts: pts, i: i, horizontal: horizontal} = site, [a, b, c, d] = [ pts[i - 1], pts[i], pts[i + 1], pts[i + 2] ], abInterior = i - 1 > 0, cdInterior = i + 2 < pts.length - 1, runLen = (p, q) => Math.abs(horizontal ? q.x - p.x : q.y - p.y), snapAb = abInterior && (!cdInterior || runLen(a, b) <= runLen(c, d));
946
+ return !snapAb && !cdInterior ? !1 : (snapAb ? alignRun(site, {
947
+ at: i - 1,
948
+ lane: c
949
+ }) : alignRun(site, {
950
+ at: i + 1,
951
+ lane: b
952
+ }), !0);
953
+ }
954
+
955
+ function dropRedundantPoints(pts) {
956
+ return pts.filter((p, i) => {
957
+ if (i === 0 || i === pts.length - 1) return !0;
958
+ const prev = pts[i - 1], next = pts[i + 1];
959
+ return near(p.x, prev.x) && near(p.y, prev.y) ? !1 : !(near(prev.x, p.x) && near(p.x, next.x) || near(prev.y, p.y) && near(p.y, next.y));
960
+ });
961
+ }
962
+
963
+ function dejagRoute(points) {
964
+ const pts = [ ...points ];
965
+ for (let pass = 0; pass < 3; pass++) {
966
+ let changed = !1;
967
+ for (let i = 1; i + 2 < pts.length; i++) {
968
+ const horizontal = jogOrientation(pts, i);
969
+ horizontal !== void 0 && snapJog({
970
+ pts: pts,
971
+ i: i,
972
+ horizontal: horizontal
973
+ }) && (changed = !0);
974
+ }
975
+ if (!changed) break;
976
+ }
977
+ return dropRedundantPoints(pts);
978
+ }
979
+
980
+ const bottomOf = r => r.y + r.h;
981
+
982
+ function lShape(pts, rects) {
983
+ if (pts.length !== 3) return;
984
+ const bottomAt = (i, r) => Math.abs(pts[i].y - bottomOf(r)) < 2, levelAt = (i, r) => Math.abs(pts[i].x - r.x) < 2 || Math.abs(pts[i].x - (r.x + r.w)) < 2;
985
+ if (bottomAt(0, rects.a) && levelAt(2, rects.b)) return {
986
+ bottomIdx: 0,
987
+ levelIdx: 2
988
+ };
989
+ if (levelAt(0, rects.a) && bottomAt(2, rects.b)) return {
990
+ bottomIdx: 2,
991
+ levelIdx: 0
992
+ };
993
+ }
994
+
995
+ function applyL(pts, l) {
996
+ pts[l.shape.bottomIdx] = {
997
+ x: l.alpha,
998
+ y: pts[l.shape.bottomIdx].y
999
+ }, pts[l.shape.levelIdx] = {
1000
+ x: pts[l.shape.levelIdx].x,
1001
+ y: l.beta
1002
+ }, pts[1] = {
1003
+ x: l.alpha,
1004
+ y: l.beta
1005
+ };
1006
+ }
1007
+
1008
+ function nestPair({e1: e1, e2: e2}, rectById) {
1009
+ const r1a = rectById.get(e1.ends.source), r1b = rectById.get(e1.ends.target);
1010
+ if (!r1a || !r1b) return;
1011
+ const s1 = lShape(e1.pts, {
1012
+ a: r1a,
1013
+ b: r1b
1014
+ }), s2 = lShape(e2.pts, {
1015
+ a: r1b,
1016
+ b: r1a
1017
+ });
1018
+ if (!s1 || !s2) return;
1019
+ const bottomRect = s1.bottomIdx === 0 ? r1a : r1b, levelRect = s1.bottomIdx === 0 ? r1b : r1a;
1020
+ if ((s2.bottomIdx === 0 ? r1b : r1a) !== bottomRect) return;
1021
+ const alphas = [ e1.pts[s1.bottomIdx].x, e2.pts[s2.bottomIdx].x ], betas = [ e1.pts[s1.levelIdx].y, e2.pts[s2.levelIdx].y ], levelCx = levelRect.x + levelRect.w / 2, bottomCy = bottomRect.y + bottomRect.h / 2, e1Inner = Math.abs(alphas[0] - levelCx) <= Math.abs(alphas[1] - levelCx), innerAlpha = e1Inner ? alphas[0] : alphas[1], outerAlpha = e1Inner ? alphas[1] : alphas[0], e1InnerBeta = Math.abs(betas[0] - bottomCy) <= Math.abs(betas[1] - bottomCy), innerBeta = e1InnerBeta ? betas[0] : betas[1], outerBeta = e1InnerBeta ? betas[1] : betas[0];
1022
+ applyL(e1.pts, {
1023
+ shape: s1,
1024
+ alpha: innerAlpha,
1025
+ beta: innerBeta
1026
+ }), applyL(e2.pts, {
1027
+ shape: s2,
1028
+ alpha: outerAlpha,
1029
+ beta: outerBeta
1030
+ });
1031
+ }
1032
+
1033
+ function nestOppositePairs(args) {
1034
+ const seen = /* @__PURE__ */ new Map;
1035
+ for (const [id, ends] of args.edgeEnds) {
1036
+ if (!args.routeById.has(id)) continue;
1037
+ const key = [ ends.source, ends.target ].sort().join("⇄"), otherId = seen.get(key);
1038
+ if (otherId === void 0) {
1039
+ seen.set(key, id);
1040
+ continue;
1041
+ }
1042
+ const other = args.edgeEnds.get(otherId);
1043
+ other.source !== ends.target || other.target !== ends.source || nestPair({
1044
+ e1: {
1045
+ pts: args.routeById.get(id).points,
1046
+ ends: ends
1047
+ },
1048
+ e2: {
1049
+ pts: args.routeById.get(otherId).points
1050
+ }
1051
+ }, args.rectById);
1052
+ }
1053
+ }
1054
+
1055
+ const LANE_STEM = 24, LANE_PASS = 14;
1056
+
1057
+ function corridorCandidate(pts, env) {
1058
+ const shape = corridorShape(pts);
1059
+ if (!shape) return;
1060
+ const {laneY: laneY, x1: x1, x2: x2, dir: d} = shape, overlapping = env.rects.filter(r => r.x < x2 + 8 && r.x + r.w > x1 - 8), innerEdge = r => d === 1 ? bottomOf(r) : r.y, outerEdge = r => d === 1 ? r.y : bottomOf(r), passedOver = overlapping.filter(r => (laneY - innerEdge(r)) * d >= -1), beyond = overlapping.filter(r => (outerEdge(r) - laneY) * d >= -1), toward = d === 1 ? Math.max : Math.min, away = d === 1 ? Math.min : Math.max, base = toward(pts[0].y + d * LANE_STEM, pts[3].y + d * LANE_STEM, ...passedOver.map(r => innerEdge(r) + d * LANE_PASS)), cap = away(...beyond.map(r => outerEdge(r) - d * 10));
1061
+ if (!((base - cap) * d > 0)) return {
1062
+ pts: pts,
1063
+ x1: x1,
1064
+ x2: x2,
1065
+ base: base,
1066
+ cap: cap,
1067
+ dir: d
1068
+ };
1069
+ }
1070
+
1071
+ function corridorShape(pts) {
1072
+ if (pts.length !== 4) return;
1073
+ const [s, a, b, e] = [ pts[0], pts[1], pts[2], pts[3] ];
1074
+ if (Math.abs(a.y - b.y) > .5 || Math.abs(a.x - s.x) > .5 || Math.abs(b.x - e.x) > .5) return;
1075
+ const span = {
1076
+ laneY: a.y,
1077
+ x1: Math.min(a.x, b.x),
1078
+ x2: Math.max(a.x, b.x)
1079
+ };
1080
+ if (a.y > Math.max(s.y, e.y) + 4) return {
1081
+ ...span,
1082
+ dir: 1
1083
+ };
1084
+ if (a.y < Math.min(s.y, e.y) - 4) return {
1085
+ ...span,
1086
+ dir: -1
1087
+ };
1088
+ }
1089
+
1090
+ function fixedHorizontals(routes, movable) {
1091
+ return routes.filter(r => !movable.has(r.points)).flatMap(r => r.points.slice(1).flatMap((p, i) => {
1092
+ const prev = r.points[i];
1093
+ return near(p.y, prev.y) ? [ {
1094
+ x1: Math.min(p.x, prev.x),
1095
+ x2: Math.max(p.x, prev.x),
1096
+ y: p.y
1097
+ } ] : [];
1098
+ }));
1099
+ }
1100
+
1101
+ function mergeCorridorLanes(args) {
1102
+ const routes = [ ...args.routeById.values() ], cands = routes.flatMap(route => {
1103
+ const cand = corridorCandidate(route.points, {
1104
+ rects: args.rects
1105
+ });
1106
+ return cand ? [ cand ] : [];
1107
+ }), fixed = fixedHorizontals(routes, new Set(cands.map(c => c.pts)));
1108
+ cands.sort((a, b) => a.x2 - a.x1 - (b.x2 - b.x1) || a.x1 - b.x1);
1109
+ const placed = [];
1110
+ for (const cand of cands) {
1111
+ const d = cand.dir, overlapsSpan = o => o.x1 < cand.x2 - 6 && o.x2 > cand.x1 + 6, cap = (d === 1 ? Math.min : Math.max)(cand.cap, ...fixed.filter(f => (f.y - cand.base) * d >= 0 && overlapsSpan(f)).map(f => f.y - d * 10));
1112
+ let lane = cand.pts[1].y;
1113
+ for (let step = 0; step <= 2 * placed.length; step++) {
1114
+ const y = cand.base + d * step * 16;
1115
+ if ((y - cap) * d > .1) break;
1116
+ if (!placed.some(p => overlapsSpan(p) && Math.abs(p.y - y) < 14)) {
1117
+ lane = y;
1118
+ break;
1119
+ }
1120
+ }
1121
+ cand.pts[1] = {
1122
+ x: cand.pts[1].x,
1123
+ y: lane
1124
+ }, cand.pts[2] = {
1125
+ x: cand.pts[2].x,
1126
+ y: lane
1127
+ }, placed.push({
1128
+ x1: cand.x1,
1129
+ x2: cand.x2,
1130
+ y: lane
1131
+ });
1132
+ }
1133
+ }
1134
+
1135
+ function stackFanGroups(args) {
1136
+ const groups = /* @__PURE__ */ new Map;
1137
+ for (const [id, route] of args.routeById) {
1138
+ const pts = route.points, source = args.edgeEnds.get(id)?.source ?? "", rect = args.rectById.get(source);
1139
+ if (pts.length < 3 || !rect) continue;
1140
+ const [p0, p1] = [ pts[0], pts[1] ];
1141
+ if (Math.abs(p1.x - p0.x) > .5) continue;
1142
+ const dir = stackDirOf({
1143
+ p0: p0,
1144
+ p1: p1
1145
+ }, rect);
1146
+ if (!dir) continue;
1147
+ const key = `${source}:${dir}`, list = groups.get(key) ?? [];
1148
+ list.push({
1149
+ pts: pts,
1150
+ dir: dir
1151
+ }), groups.set(key, list);
1152
+ }
1153
+ return groups;
1154
+ }
1155
+
1156
+ function stackDirOf(leg, rect) {
1157
+ if (Math.abs(leg.p0.y - bottomOf(rect)) < 2 && leg.p1.y > leg.p0.y + 4) return 1;
1158
+ if (Math.abs(leg.p0.y - rect.y) < 2 && leg.p1.y < leg.p0.y - 4) return -1;
1159
+ }
1160
+
1161
+ function untangleStackFans(args) {
1162
+ for (const members of stackFanGroups(args).values()) {
1163
+ if (members.length < 2) continue;
1164
+ const xs = members.map(m => m.pts[0].x).sort((a, b) => a - b), byDepth = [ ...members ].sort((a, b) => (b.pts[1].y - a.pts[1].y) * a.dir), deque = [];
1165
+ for (const {pts: pts} of byDepth) pts[2].x >= pts[1].x ? deque.push(pts) : deque.unshift(pts);
1166
+ deque.forEach((pts, i) => {
1167
+ pts[0] = {
1168
+ x: xs[i],
1169
+ y: pts[0].y
1170
+ }, pts[1] = {
1171
+ x: xs[i],
1172
+ y: pts[1].y
1173
+ };
1174
+ });
1175
+ }
1176
+ }
1177
+
1178
+ function materializeLayout({nodes: nodes, edges: edges}, result) {
1179
+ const posById = new Map((result.children ?? []).map(c => [ c.id, {
1180
+ x: c.x ?? 0,
1181
+ y: c.y ?? 0
1182
+ } ])), rectById = nodeRects(nodes, posById), edgeEnds = new Map(edges.map(e => [ e.id, {
1183
+ source: e.source,
1184
+ target: e.target,
1185
+ primary: !!edgeFlags(e).isPrimary
1186
+ } ])), endpoints = [], routeById = /* @__PURE__ */ new Map;
1187
+ for (const e of result.edges ?? []) {
1188
+ const sec = e.sections?.[0];
1189
+ if (!sec) continue;
1190
+ const points = dropRedundantPoints(sectionPolyline(sec)), ends = edgeEnds.get(e.id);
1191
+ ends && endpoints.push(...routeEndpoints(points, {
1192
+ src: rectById.get(ends.source),
1193
+ tgt: rectById.get(ends.target),
1194
+ primary: ends.primary
1195
+ })), routeById.set(e.id, {
1196
+ points: points
1197
+ });
1198
+ }
1199
+ mergeCorridorLanes({
1200
+ routeById: routeById,
1201
+ rects: [ ...rectById.values() ]
1202
+ }), untangleStackFans({
1203
+ routeById: routeById,
1204
+ edgeEnds: edgeEnds,
1205
+ rectById: rectById
1206
+ }), nestOppositePairs({
1207
+ routeById: routeById,
1208
+ edgeEnds: edgeEnds,
1209
+ rectById: rectById
1210
+ }), placeEndpoints(endpoints);
1211
+ const positionedNodes = nodes.map(n => ({
1212
+ ...n,
1213
+ position: posById.get(n.id) ?? {
1214
+ x: 0,
1215
+ y: 0
1216
+ }
1217
+ })), enrichedEdges = edges.map(e => {
1218
+ const route = routeById.get(e.id);
1219
+ return route ? {
1220
+ ...e,
1221
+ data: {
1222
+ ...e.data,
1223
+ route: {
1224
+ points: dejagRoute([ ...route.points ])
1225
+ }
1226
+ }
1227
+ } : e;
1228
+ });
1229
+ return {
1230
+ nodes: positionedNodes,
1231
+ edges: enrichedEdges
1232
+ };
1233
+ }
1234
+
1235
+ function reachabilityOf(stages) {
1236
+ const byName = new Map(stages.map(s => [ s.name, s ])), cache = /* @__PURE__ */ new Map;
1237
+ return start => {
1238
+ const cached = cache.get(start);
1239
+ if (cached) return cached;
1240
+ const seen = /* @__PURE__ */ new Set;
1241
+ cache.set(start, seen);
1242
+ const stack = [ start ];
1243
+ for (;stack.length > 0; ) {
1244
+ const name = stack.pop();
1245
+ for (const t of byName.get(name)?.transitions ?? []) seen.has(t.to) || (seen.add(t.to),
1246
+ stack.push(t.to));
1247
+ }
1248
+ return seen;
1249
+ };
1250
+ }
1251
+
1252
+ function scoreBeats(a, b) {
1253
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return a[i] > b[i];
1254
+ return !1;
1255
+ }
1256
+
1257
+ const SPINE_BUDGET = 2e4;
1258
+
1259
+ function railTransitions(stages, initialStage) {
1260
+ const byName = new Map(stages.map(s => [ s.name, s ])), inDegree = /* @__PURE__ */ new Map;
1261
+ for (const s of stages) for (const t of s.transitions ?? []) inDegree.set(t.to, (inDegree.get(t.to) ?? 0) + 1);
1262
+ let best = [], bestScore = [ -1, 0 ], budget = SPINE_BUDGET;
1263
+ const path = [], visited = /* @__PURE__ */ new Set([ initialStage ]), dfs = name => {
1264
+ const end = path[path.length - 1]?.to ?? initialStage, score = [ path.length, -(inDegree.get(end) ?? 0) ];
1265
+ if (scoreBeats(score, bestScore) && (best = [ ...path ], bestScore = score), !(budget-- <= 0)) for (const t of byName.get(name)?.transitions ?? []) visited.has(t.to) || (visited.add(t.to),
1266
+ path.push({
1267
+ ...t,
1268
+ from: name
1269
+ }), dfs(t.to), path.pop(), visited.delete(t.to));
1270
+ };
1271
+ return dfs(initialStage), best;
1272
+ }
1273
+
1274
+ function visitedFromHistory(history) {
1275
+ const visitedStages = /* @__PURE__ */ new Set, traversed = /* @__PURE__ */ new Set;
1276
+ for (const h of history) if (h._type === "stageEntered") visitedStages.add(h.stage),
1277
+ h.fromStage && (visitedStages.add(h.fromStage), traversed.add(`${h.fromStage}->${h.stage}`)); else if (h._type === "transitionFired") {
1278
+ visitedStages.add(h.fromStage), visitedStages.add(h.toStage);
1279
+ const pair = `${h.fromStage}->${h.toStage}`;
1280
+ traversed.add(h.transition === void 0 ? pair : `${pair}#${h.transition}`);
1281
+ }
1282
+ return {
1283
+ visitedStages: visitedStages,
1284
+ traversed: traversed
1285
+ };
1286
+ }
1287
+
1288
+ function buildGraphModel({stages: stages, transitions: transitions, initialStage: initialStage, currentStage: currentStage, selectedStage: selectedStage, history: history, guardCount: guardCount, gatedTransitions: gatedTransitions, local: local}) {
1289
+ const {visitedStages: visitedStages, traversed: traversedKeys} = visitedFromHistory(history ?? []), nodes = stages.map(s => {
1290
+ const isTerminal = isTerminalStage(s), box = stageNodeSize(s.title ?? s.name);
1291
+ return {
1292
+ id: s.name,
1293
+ type: "stage",
1294
+ position: {
1295
+ x: 0,
1296
+ y: 0
1297
+ },
1298
+ width: box.width,
1299
+ height: box.height,
1300
+ data: {
1301
+ stage: s,
1302
+ isCurrent: s.name === currentStage,
1303
+ isVisited: visitedStages.has(s.name) && s.name !== currentStage,
1304
+ isSelected: s.name === selectedStage && s.name !== currentStage,
1305
+ isInitial: s.name === initialStage,
1306
+ isTerminal: isTerminal,
1307
+ guardCount: s.name === currentStage ? guardCount ?? 0 : 0,
1308
+ boxWidth: box.width,
1309
+ boxHeight: box.height
1310
+ }
1311
+ };
1312
+ }), railPairs = new Set(railTransitions(stages, initialStage).map(t => `${t.from}->${t.to}#${t.name}`)), reach = reachabilityOf(stages), edges = transitions.map(t => {
1313
+ const traversed = traversedKeys.has(`${t.from}->${t.to}#${t.name}`) || traversedKeys.has(`${t.from}->${t.to}`);
1314
+ return {
1315
+ id: `${t.from}->${t.to}#${t.name}`,
1316
+ source: t.from,
1317
+ sourceHandle: "t-out",
1318
+ target: t.to,
1319
+ targetHandle: "t-in",
1320
+ type: "transition",
1321
+ markerEnd: edgeMarker(traversed),
1322
+ data: {
1323
+ transition: t,
1324
+ wasTraversed: traversed,
1325
+ isPrimary: railPairs.has(`${t.from}->${t.to}#${t.name}`),
1326
+ isReturn: !railPairs.has(`${t.from}->${t.to}#${t.name}`) && reach(t.to).has(t.from),
1327
+ gated: t.from === currentStage && (gatedTransitions ?? []).includes(t.name)
1328
+ }
1329
+ };
1330
+ });
1331
+ if (!local) return {
1332
+ nodes: nodes,
1333
+ edges: edges
1334
+ };
1335
+ if (currentStage === void 0) throw new Error("WorkflowDiagram: the local view needs a currentStage to anchor on");
1336
+ return applyLocalView({
1337
+ nodes: nodes,
1338
+ edges: edges,
1339
+ currentStage: currentStage,
1340
+ view: local
1341
+ });
1342
+ }
1343
+
1344
+ function GroupNode({data: data}) {
1345
+ /* @__PURE__ */
1346
+ return jsx(NodeShell, {
1347
+ width: data.boxWidth,
1348
+ height: data.boxHeight,
1349
+ tooltip: /* @__PURE__ */ jsx(Box, {
1350
+ padding: 3,
1351
+ style: {
1352
+ maxWidth: 280
1353
+ },
1354
+ children: /* @__PURE__ */ jsxs(Stack, {
1355
+ space: 4,
1356
+ children: [
1357
+ /* @__PURE__ */ jsx(Stack, {
1358
+ space: 2,
1359
+ children: data.titles.map(title => /* @__PURE__ */ jsx(Text, {
1360
+ size: 1,
1361
+ weight: "semibold",
1362
+ children: `• ${title}`
1363
+ }, title))
1364
+ }),
1365
+ /* @__PURE__ */ jsx(Text, {
1366
+ size: 1,
1367
+ muted: !0,
1368
+ children: "Click to expand"
1369
+ }) ]
1370
+ })
1371
+ }),
1372
+ children: /* @__PURE__ */ jsx("div", {
1373
+ style: {
1374
+ boxSizing: "border-box",
1375
+ width: data.boxWidth,
1376
+ height: data.boxHeight,
1377
+ borderRadius: 999,
1378
+ border: "1.5px dashed color-mix(in srgb, var(--ws-ink) 35%, transparent)",
1379
+ display: "flex",
1380
+ alignItems: "center",
1381
+ justifyContent: "center",
1382
+ cursor: "pointer",
1383
+ fontFamily: "var(--ws-font)",
1384
+ fontWeight: 600,
1385
+ fontSize: 12,
1386
+ color: "var(--ws-ink-2)"
1387
+ },
1388
+ children: data.label
1389
+ })
1390
+ });
1391
+ }
1392
+
1393
+ function MetaTable({rows: rows, description: description, title: title}) {
1394
+ /* @__PURE__ */
1395
+ return jsxs(Stack, {
1396
+ space: 4,
1397
+ style: {
1398
+ minWidth: 240,
1399
+ maxWidth: 340
1400
+ },
1401
+ children: [ title ? /* @__PURE__ */ jsx(Text, {
1402
+ size: 1,
1403
+ weight: "semibold",
1404
+ children: title
1405
+ }) : null,
1406
+ /* @__PURE__ */ jsx("div", {
1407
+ style: {
1408
+ display: "grid",
1409
+ gridTemplateColumns: "auto 1fr",
1410
+ columnGap: 16,
1411
+ rowGap: 8
1412
+ },
1413
+ children: rows.map(row => /* @__PURE__ */ jsxs(Fragment, {
1414
+ children: [
1415
+ /* @__PURE__ */ jsx(Text, {
1416
+ size: 1,
1417
+ muted: !0,
1418
+ children: row.label
1419
+ }),
1420
+ /* @__PURE__ */ jsx(Text, {
1421
+ size: 1,
1422
+ style: row.mono ? {
1423
+ fontFamily: "monospace",
1424
+ overflowWrap: "anywhere"
1425
+ } : void 0,
1426
+ children: row.value
1427
+ }) ]
1428
+ }, row.label))
1429
+ }), description ? /* @__PURE__ */ jsx(Text, {
1430
+ size: 1,
1431
+ muted: !0,
1432
+ style: {
1433
+ lineHeight: 1.4
1434
+ },
1435
+ children: description
1436
+ }) : null ]
1437
+ });
1438
+ }
1439
+
1440
+ function StageExplanation({stage: stage}) {
1441
+ const gates = useExplain()?.stageGates(stage) ?? [];
1442
+ if (gates.length === 0) return null;
1443
+ const {shown: shown, overflowNote: overflowNote} = cappedGates(gates);
1444
+ /* @__PURE__ */
1445
+ return jsxs(Stack, {
1446
+ space: 3,
1447
+ children: [ shown.map((gate, i) => /* @__PURE__ */ jsx(ExplainedGateView, {
1448
+ gate: gate
1449
+ }, i)), overflowNote === void 0 ? null : /* @__PURE__ */ jsx(Text, {
1450
+ size: 1,
1451
+ muted: !0,
1452
+ children: overflowNote
1453
+ }) ]
1454
+ });
1455
+ }
1456
+
1457
+ function stageTooltip(data, devMode) {
1458
+ const {stage: stage} = data;
1459
+ return devMode ? /* @__PURE__ */ jsx(Box, {
1460
+ padding: 3,
1461
+ style: {
1462
+ minWidth: 280
1463
+ },
1464
+ children: /* @__PURE__ */ jsxs(Stack, {
1465
+ space: 3,
1466
+ children: [
1467
+ /* @__PURE__ */ jsx(MetaTable, {
1468
+ title: stage.title ?? stage.name,
1469
+ description: stage.description,
1470
+ rows: [ {
1471
+ label: "Name",
1472
+ value: stage.name,
1473
+ mono: !0
1474
+ }, ...data.isTerminal ? [ {
1475
+ label: "Terminal",
1476
+ value: "Yes (no transitions out)"
1477
+ } ] : [] ]
1478
+ }),
1479
+ /* @__PURE__ */ jsx(StageExplanation, {
1480
+ stage: stage.name
1481
+ }) ]
1482
+ })
1483
+ }) : /* @__PURE__ */ jsx(Box, {
1484
+ padding: 3,
1485
+ style: {
1486
+ maxWidth: 320
1487
+ },
1488
+ children: /* @__PURE__ */ jsxs(Stack, {
1489
+ space: 3,
1490
+ children: [
1491
+ /* @__PURE__ */ jsx(Text, {
1492
+ size: 1,
1493
+ weight: "semibold",
1494
+ children: stage.title ?? stage.name
1495
+ }), stage.description ? /* @__PURE__ */ jsx(Text, {
1496
+ size: 1,
1497
+ muted: !0,
1498
+ style: {
1499
+ lineHeight: 1.4
1500
+ },
1501
+ children: stage.description
1502
+ }) : null,
1503
+ /* @__PURE__ */ jsx(StageExplanation, {
1504
+ stage: stage.name
1505
+ }) ]
1506
+ })
1507
+ });
1508
+ }
1509
+
1510
+ function emphasisStyle(data) {
1511
+ return data.isCurrent ? {
1512
+ boxShadow: "inset 0 0 0 2.5px var(--ws-accent)"
1513
+ } : data.isSelected ? {
1514
+ outline: "2px dashed var(--ws-accent)",
1515
+ outlineOffset: -4
1516
+ } : {};
1517
+ }
1518
+
1519
+ function nodeOpacity(data) {
1520
+ return data.isCurrent || data.isSelected ? 1 : data.isVisited ? .95 : .75;
1521
+ }
1522
+
1523
+ function GuardBadge({count: count}) {
1524
+ /* @__PURE__ */
1525
+ return jsxs("span", {
1526
+ title: `${count} mutation guard${count === 1 ? "" : "s"} active`,
1527
+ style: {
1528
+ display: "inline-flex",
1529
+ alignItems: "center",
1530
+ gap: 2,
1531
+ color: "var(--ws-red)",
1532
+ fontSize: 13,
1533
+ fontWeight: 600
1534
+ },
1535
+ children: [
1536
+ /* @__PURE__ */ jsx(LockIcon, {}), " ", count ]
1537
+ });
1538
+ }
1539
+
1540
+ function StatusIcon({data: data}) {
1541
+ return data.isVisited ? /* @__PURE__ */ jsx("span", {
1542
+ style: {
1543
+ color: "var(--ws-positive)",
1544
+ fontSize: 19,
1545
+ display: "flex"
1546
+ },
1547
+ children: /* @__PURE__ */ jsx(CheckmarkCircleIcon, {})
1548
+ }) : /* @__PURE__ */ jsx("span", {
1549
+ style: {
1550
+ color: data.isCurrent ? "var(--ws-accent)" : "var(--ws-ink-2)",
1551
+ opacity: data.isCurrent ? 1 : .55,
1552
+ fontSize: 19,
1553
+ display: "flex"
1554
+ },
1555
+ children: /* @__PURE__ */ jsx(CircleIcon, {})
1556
+ });
1557
+ }
1558
+
1559
+ function StageNode({data: data}) {
1560
+ const devMode = useDevMode(), {boxWidth: boxWidth, boxHeight: boxHeight} = data;
1561
+ /* @__PURE__ */
1562
+ return jsx(NodeShell, {
1563
+ width: boxWidth,
1564
+ height: boxHeight,
1565
+ tooltip: stageTooltip(data, devMode),
1566
+ children: /* @__PURE__ */ jsxs("div", {
1567
+ style: {
1568
+ boxSizing: "border-box",
1569
+ cursor: "pointer",
1570
+ width: boxWidth,
1571
+ height: boxHeight,
1572
+ background: "var(--ws-panel)",
1573
+ border: "1.5px solid color-mix(in srgb, var(--ws-ink) 35%, transparent)",
1574
+ borderRadius: boxHeight / 2,
1575
+ boxShadow: "0 1px 2px color-mix(in srgb, var(--ws-ink) 10%, transparent)",
1576
+ display: "flex",
1577
+ alignItems: "center",
1578
+ justifyContent: "center",
1579
+ gap: 6,
1580
+ padding: `0 ${STAGE_PAD_X}px`,
1581
+ opacity: nodeOpacity(data),
1582
+ ...emphasisStyle(data)
1583
+ },
1584
+ children: [
1585
+ /* @__PURE__ */ jsx(StatusIcon, {
1586
+ data: data
1587
+ }),
1588
+ /* @__PURE__ */ jsx("span", {
1589
+ style: {
1590
+ fontFamily: "var(--ws-font)",
1591
+ fontWeight: 600,
1592
+ fontSize: 14,
1593
+ lineHeight: "18px",
1594
+ color: "var(--ws-ink)",
1595
+ letterSpacing: "-0.005em",
1596
+ textAlign: "center",
1597
+ whiteSpace: "pre-line",
1598
+ overflowWrap: "break-word"
1599
+ },
1600
+ children: wrapStageLabel(data.stage.title ?? data.stage.name).join(`\n`)
1601
+ }), data.isCurrent && data.guardCount > 0 ? /* @__PURE__ */ jsx(GuardBadge, {
1602
+ count: data.guardCount
1603
+ }) : null ]
1604
+ })
1605
+ });
1606
+ }
1607
+
1608
+ const WS_CARD_TOKENS = {
1609
+ "--ws-accent": "var(--card-focus-ring-color)",
1610
+ "--ws-accent-bg": "color-mix(in srgb, var(--card-focus-ring-color) 18%, transparent)",
1611
+ "--ws-panel": "var(--card-bg-color)",
1612
+ "--ws-ink": "var(--card-fg-color)",
1613
+ "--ws-ink-2": "var(--card-muted-fg-color)",
1614
+ "--ws-border": "var(--card-border-color)",
1615
+ "--ws-red": "var(--card-critical-fg-color)",
1616
+ "--ws-positive": "var(--card-badge-positive-fg-color)",
1617
+ "--ws-font": '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif'
1618
+ };
1619
+
1620
+ function flattenTransitions(stages) {
1621
+ return stages.flatMap(s => (s.transitions ?? []).map(t => ({
1622
+ ...t,
1623
+ from: s.name
1624
+ })));
1625
+ }
1626
+
1627
+ function devRows(t) {
1628
+ return [ {
1629
+ label: "Name",
1630
+ value: t.name,
1631
+ mono: !0
1632
+ }, {
1633
+ label: "Filter",
1634
+ value: t.filter,
1635
+ mono: !0
1636
+ }, ...t.effects?.length ? [ {
1637
+ label: "Effects",
1638
+ value: t.effects.map(e => e.name).join(", "),
1639
+ mono: !0
1640
+ } ] : [] ];
1641
+ }
1642
+
1643
+ const LOCK_SIZE = 18;
1644
+
1645
+ function strokeFor(data, hovered) {
1646
+ return data.wasTraversed ? {
1647
+ stroke: "var(--ws-positive)",
1648
+ strokeWidth: hovered ? 2.5 : 2,
1649
+ strokeDasharray: void 0
1650
+ } : hovered ? {
1651
+ stroke: "var(--ws-ink-2)",
1652
+ strokeWidth: 1.75,
1653
+ strokeDasharray: "4 4"
1654
+ } : {
1655
+ stroke: "color-mix(in srgb, var(--ws-ink) 40%, transparent)",
1656
+ strokeWidth: 1.25,
1657
+ strokeDasharray: "4 4"
1658
+ };
1659
+ }
1660
+
1661
+ function groupedGateNote(data) {
1662
+ const grouped = data.grouped;
1663
+ return !grouped || grouped.gatedCount === 0 || data.gated ? [] : [ `${grouped.gatedCount} of ${grouped.transitions.length} can’t fire yet.` ];
1664
+ }
1665
+
1666
+ function tooltipNotes(data) {
1667
+ const notes = [];
1668
+ return data.hiddenFrom && notes.push(`From “${data.hiddenFrom}”`), data.hiddenTo && notes.push(`Continues to “${data.hiddenTo}”`),
1669
+ data.gated && notes.push("Can’t proceed yet — the criteria for this transition aren’t met."),
1670
+ notes.push(...groupedGateNote(data)), notes;
1671
+ }
1672
+
1673
+ function TooltipNotes({data: data}) {
1674
+ return tooltipNotes(data).map(note => /* @__PURE__ */ jsx(Text, {
1675
+ size: 1,
1676
+ muted: !0,
1677
+ style: {
1678
+ lineHeight: 1.4
1679
+ },
1680
+ children: note
1681
+ }, note));
1682
+ }
1683
+
1684
+ function groupedTooltip(grouped, data) {
1685
+ /* @__PURE__ */
1686
+ return jsx(Box, {
1687
+ padding: 3,
1688
+ style: {
1689
+ maxWidth: 320
1690
+ },
1691
+ children: /* @__PURE__ */ jsxs(Stack, {
1692
+ space: 3,
1693
+ children: [ grouped.transitions.map(title => /* @__PURE__ */ jsx(Text, {
1694
+ size: 1,
1695
+ weight: "semibold",
1696
+ children: title
1697
+ }, title)),
1698
+ /* @__PURE__ */ jsx(TooltipNotes, {
1699
+ data: data
1700
+ }) ]
1701
+ })
1702
+ });
1703
+ }
1704
+
1705
+ function TransitionExplanation({data: data}) {
1706
+ const explained = useExplain()?.transition(data.transition.from, data.transition.name);
1707
+ return explained ? /* @__PURE__ */ jsx(ExplainedGateView, {
1708
+ gate: {
1709
+ heading: "Transitions when:",
1710
+ ...explained
1711
+ }
1712
+ }) : null;
1713
+ }
1714
+
1715
+ function endUserTooltip(data) {
1716
+ const t = data.transition;
1717
+ /* @__PURE__ */
1718
+ return jsx(Box, {
1719
+ padding: 3,
1720
+ style: {
1721
+ maxWidth: 320
1722
+ },
1723
+ children: /* @__PURE__ */ jsxs(Stack, {
1724
+ space: 3,
1725
+ children: [
1726
+ /* @__PURE__ */ jsx(Text, {
1727
+ size: 1,
1728
+ weight: "semibold",
1729
+ children: t.title ?? t.name
1730
+ }), t.description ? /* @__PURE__ */ jsx(Text, {
1731
+ size: 1,
1732
+ muted: !0,
1733
+ style: {
1734
+ lineHeight: 1.4
1735
+ },
1736
+ children: t.description
1737
+ }) : null,
1738
+ /* @__PURE__ */ jsx(TransitionExplanation, {
1739
+ data: data
1740
+ }),
1741
+ /* @__PURE__ */ jsx(TooltipNotes, {
1742
+ data: data
1743
+ }) ]
1744
+ })
1745
+ });
1746
+ }
1747
+
1748
+ function hiddenEndpointRows(data) {
1749
+ const rows = [];
1750
+ return data.hiddenFrom && rows.push({
1751
+ label: "From",
1752
+ value: data.transition.from,
1753
+ mono: !0
1754
+ }), data.hiddenTo && rows.push({
1755
+ label: "To",
1756
+ value: data.transition.to,
1757
+ mono: !0
1758
+ }), rows;
1759
+ }
1760
+
1761
+ function transitionTooltip(data, devMode) {
1762
+ if (data.grouped) return groupedTooltip(data.grouped, data);
1763
+ if (!devMode) return endUserTooltip(data);
1764
+ const t = data.transition;
1765
+ /* @__PURE__ */
1766
+ return jsx(Box, {
1767
+ padding: 3,
1768
+ style: {
1769
+ minWidth: 320
1770
+ },
1771
+ children: /* @__PURE__ */ jsxs(Stack, {
1772
+ space: 3,
1773
+ children: [
1774
+ /* @__PURE__ */ jsx(MetaTable, {
1775
+ title: t.title ?? t.name,
1776
+ description: t.description,
1777
+ rows: [ ...hiddenEndpointRows(data), ...devRows(t) ]
1778
+ }),
1779
+ /* @__PURE__ */ jsx(TransitionExplanation, {
1780
+ data: data
1781
+ }),
1782
+ /* @__PURE__ */ jsx(TooltipNotes, {
1783
+ data: data
1784
+ }) ]
1785
+ })
1786
+ });
1787
+ }
1788
+
1789
+ function GatedMarker({gated: gated, at: at, stroke: stroke}) {
1790
+ return gated ? /* @__PURE__ */ jsx("div", {
1791
+ style: {
1792
+ position: "absolute",
1793
+ transform: `translate(-50%, -50%) translate(${at.x}px, ${at.y}px)`,
1794
+ pointerEvents: "none",
1795
+ zIndex: 10
1796
+ },
1797
+ children: /* @__PURE__ */ jsx("div", {
1798
+ style: {
1799
+ width: LOCK_SIZE,
1800
+ height: LOCK_SIZE,
1801
+ borderRadius: 999,
1802
+ background: "var(--ws-panel)",
1803
+ border: `2px solid ${stroke}`,
1804
+ boxShadow: "0 1px 2px color-mix(in srgb, var(--ws-ink) 12%, transparent)",
1805
+ display: "flex",
1806
+ alignItems: "center",
1807
+ justifyContent: "center",
1808
+ color: "var(--ws-ink-2)",
1809
+ fontSize: 12
1810
+ },
1811
+ children: /* @__PURE__ */ jsx(LockIcon, {})
1812
+ })
1813
+ }) : null;
1814
+ }
1815
+
1816
+ function LineTooltip({at: at, data: data}) {
1817
+ const devMode = useDevMode();
1818
+ return at ? createPortal(
1819
+ /* @__PURE__ */ jsx("div", {
1820
+ style: {
1821
+ position: "fixed",
1822
+ left: at.x,
1823
+ top: at.y - 12,
1824
+ transform: "translate(-50%, -100%)",
1825
+ pointerEvents: "none",
1826
+ zIndex: 1e4
1827
+ },
1828
+ children: /* @__PURE__ */ jsx(Card, {
1829
+ radius: 2,
1830
+ shadow: 3,
1831
+ style: {
1832
+ overflow: "hidden"
1833
+ },
1834
+ children: transitionTooltip(data, devMode)
1835
+ })
1836
+ }), document.body) : null;
1837
+ }
1838
+
1839
+ function fallbackMarker(triangle, markerEnd) {
1840
+ return triangle || !markerEnd ? {} : {
1841
+ markerEnd: markerEnd
1842
+ };
1843
+ }
1844
+
1845
+ function TransitionEdge({id: id, sourceX: sourceX, sourceY: sourceY, targetX: targetX, targetY: targetY, sourcePosition: sourcePosition, targetPosition: targetPosition, markerEnd: markerEnd, data: data}) {
1846
+ const {path: path, markerX: markerX, markerY: markerY, triangle: triangle} = transitionPath(data.route, {
1847
+ sourceX: sourceX,
1848
+ sourceY: sourceY,
1849
+ sourcePosition: sourcePosition,
1850
+ targetX: targetX,
1851
+ targetY: targetY,
1852
+ targetPosition: targetPosition
1853
+ }), [hoverAt, setHoverAt] = useState(void 0), stageHovered = useHoveredStage() === data.transition.from, {stroke: stroke, strokeWidth: strokeWidth, strokeDasharray: strokeDasharray} = strokeFor(data, stageHovered || hoverAt !== void 0);
1854
+ /* @__PURE__ */
1855
+ return jsxs(Fragment$1, {
1856
+ children: [
1857
+ /* @__PURE__ */ jsx(BaseEdge, {
1858
+ id: id,
1859
+ path: path,
1860
+ ...fallbackMarker(triangle, markerEnd),
1861
+ style: {
1862
+ stroke: stroke,
1863
+ strokeWidth: strokeWidth,
1864
+ fill: "none",
1865
+ strokeDasharray: strokeDasharray
1866
+ }
1867
+ }),
1868
+ /* @__PURE__ */ jsx("path", {
1869
+ d: path,
1870
+ fill: "none",
1871
+ stroke: "transparent",
1872
+ strokeWidth: 16,
1873
+ style: {
1874
+ pointerEvents: "stroke",
1875
+ cursor: "help"
1876
+ },
1877
+ onMouseEnter: e => setHoverAt({
1878
+ x: e.clientX,
1879
+ y: e.clientY
1880
+ }),
1881
+ onMouseLeave: () => setHoverAt(void 0)
1882
+ }), triangle ? /* @__PURE__ */ jsx("polygon", {
1883
+ points: triangle,
1884
+ fill: stroke,
1885
+ stroke: stroke,
1886
+ strokeLinejoin: "round"
1887
+ }) : null,
1888
+ /* @__PURE__ */ jsx(LineTooltip, {
1889
+ at: hoverAt,
1890
+ data: data
1891
+ }),
1892
+ /* @__PURE__ */ jsx(EdgeLabelRenderer, {
1893
+ children: /* @__PURE__ */ jsx(GatedMarker, {
1894
+ gated: data.gated,
1895
+ at: {
1896
+ x: markerX,
1897
+ y: markerY
1898
+ },
1899
+ stroke: stroke
1900
+ })
1901
+ }) ]
1902
+ });
1903
+ }
1904
+
1905
+ const nodeTypes = {
1906
+ stage: StageNode,
1907
+ ghost: GhostNode,
1908
+ stageGroup: GroupNode
1909
+ }, edgeTypes = {
1910
+ transition: TransitionEdge
1911
+ };
1912
+
1913
+ function viewportProps(isStatic) {
1914
+ return {
1915
+ panOnDrag: !isStatic,
1916
+ zoomOnPinch: !isStatic,
1917
+ zoomOnDoubleClick: !isStatic,
1918
+ preventScrolling: !isStatic,
1919
+ minZoom: isStatic ? .05 : .3
1920
+ };
1921
+ }
1922
+
1923
+ const CANVAS_MARGIN = 24, CONTROLS_GUTTER = 44, CONTROLS_MIN_HEIGHT = 130;
1924
+
1925
+ function fitRect(bounds, isStatic) {
1926
+ const left = CANVAS_MARGIN + (isStatic ? 0 : CONTROLS_GUTTER);
1927
+ return {
1928
+ x: bounds.x - left,
1929
+ y: bounds.y - CANVAS_MARGIN,
1930
+ width: bounds.width + left + CANVAS_MARGIN,
1931
+ height: bounds.height + CANVAS_MARGIN * 2
1932
+ };
1933
+ }
1934
+
1935
+ function hugStyle(bounds, opts) {
1936
+ const {height: height, isStatic: isStatic} = opts;
1937
+ if (!bounds) return {
1938
+ height: height,
1939
+ width: "100%"
1940
+ };
1941
+ const gutter = isStatic ? 0 : CONTROLS_GUTTER, hugWidth = Math.ceil(bounds.width) + CANVAS_MARGIN * 2 + gutter, contentHeight = Math.ceil(bounds.height) + CANVAS_MARGIN * 2, hugHeight = isStatic ? contentHeight : Math.max(contentHeight, CONTROLS_MIN_HEIGHT);
1942
+ return {
1943
+ height: typeof height == "number" ? Math.min(height, hugHeight) : height,
1944
+ width: hugWidth,
1945
+ maxWidth: "100%",
1946
+ marginInline: "auto"
1947
+ };
1948
+ }
1949
+
1950
+ const WS_VARS = WS_CARD_TOKENS;
1951
+
1952
+ function WorkflowDiagram({definition: definition, currentStage: currentStage, selectedStage: selectedStage, onSelectStage: onSelectStage, history: history, guardCount: guardCount, gatedTransitions: gatedTransitions, local: local, static: isStatic = !1, devMode: devMode, explain: explain, evaluation: evaluation, height: height = 360}) {
1953
+ const {stages: stages, initialStage: initialStage} = definition, transitions = useMemo(() => flattenTransitions(stages), [ stages ]), hasLocal = local !== void 0, localShow = local?.show, localGroupEdges = local?.groupEdges ?? !1, [groupsExpanded, setGroupsExpanded] = useState(!1), groupEdges = localGroupEdges && !groupsExpanded, base = useMemo(() => buildGraphModel({
1954
+ stages: stages,
1955
+ transitions: transitions,
1956
+ initialStage: initialStage,
1957
+ currentStage: currentStage,
1958
+ selectedStage: selectedStage,
1959
+ history: history,
1960
+ guardCount: guardCount,
1961
+ gatedTransitions: gatedTransitions,
1962
+ local: hasLocal ? {
1963
+ show: localShow,
1964
+ groupEdges: groupEdges
1965
+ } : void 0
1966
+ }), [ stages, transitions, initialStage, currentStage, selectedStage, history, guardCount, gatedTransitions, hasLocal, localShow, groupEdges ]), [laidOut, setLaidOut] = useState({
1967
+ nodes: [],
1968
+ edges: []
1969
+ }), [layoutFailed, setLayoutFailed] = useState(!1), [explained, setExplained] = useState(void 0);
1970
+ useEffect(() => {
1971
+ if (setExplained(void 0), !explain) return;
1972
+ let cancelled = !1;
1973
+ return (async () => {
1974
+ try {
1975
+ const {describeDefinition: describeDefinition} = await import("@sanity/workflow-engine"), sites = await describeDefinition(definition);
1976
+ cancelled || setExplained(indexExplanations(sites, definition));
1977
+ } catch (err) {
1978
+ cancelled || console.error("WorkflowDiagram: describeDefinition failed", err);
1979
+ }
1980
+ })(), () => {
1981
+ cancelled = !0;
1982
+ };
1983
+ }, [ definition, explain ]);
1984
+ const liveExplained = useMemo(() => explain && evaluation ? indexLiveExplanations(evaluation) : void 0, [ explain, evaluation ]), explainData = useMemo(() => mergeExplainData(liveExplained, explained), [ liveExplained, explained ]), flowRef = useRef(null), fittedRef = useRef(!1);
1985
+ useEffect(() => {
1986
+ let cancelled = !1;
1987
+ return (async () => {
1988
+ try {
1989
+ const res = await layoutGraph(base.nodes, base.edges);
1990
+ cancelled || (setLaidOut(res), setLayoutFailed(!1));
1991
+ } catch (err) {
1992
+ console.error("WorkflowDiagram: layout failed", err), cancelled || setLayoutFailed(!0);
1993
+ }
1994
+ })(), () => {
1995
+ cancelled = !0;
1996
+ };
1997
+ }, [ base ]);
1998
+ const bounds = useMemo(() => graphBounds(laidOut.nodes, laidOut.edges), [ laidOut ]), boundsRef = useRef(bounds);
1999
+ boundsRef.current = bounds;
2000
+ const staticRef = useRef(isStatic);
2001
+ staticRef.current = isStatic;
2002
+ const fitAll = useCallback(() => {
2003
+ const b = boundsRef.current, flow = flowRef.current, el = cardRef.current;
2004
+ if (!b || !flow || !el) return;
2005
+ const rect = fitRect(b, staticRef.current);
2006
+ flow.fitBounds(rect, {
2007
+ duration: 0
2008
+ }), flow.getViewport().zoom > 1 && flow.setViewport({
2009
+ x: (el.clientWidth - rect.width) / 2 - rect.x,
2010
+ y: (el.clientHeight - rect.height) / 2 - rect.y,
2011
+ zoom: 1
2012
+ });
2013
+ }, []), prevStaticRef = useRef(isStatic);
2014
+ useEffect(() => {
2015
+ const staticChanged = prevStaticRef.current !== isStatic;
2016
+ if (prevStaticRef.current = isStatic, laidOut.nodes.length === 0 || fittedRef.current && !isStatic && !staticChanged) return;
2017
+ fittedRef.current = !0;
2018
+ const handle = requestAnimationFrame(fitAll);
2019
+ return () => cancelAnimationFrame(handle);
2020
+ }, [ laidOut, isStatic, fitAll ]);
2021
+ const cardRef = useRef(null), userMovedRef = useRef(!1);
2022
+ useEffect(() => {
2023
+ if (typeof ResizeObserver > "u") return;
2024
+ const el = cardRef.current;
2025
+ if (!el) return;
2026
+ const observer = new ResizeObserver(() => {
2027
+ (staticRef.current || !userMovedRef.current) && fitAll();
2028
+ });
2029
+ return observer.observe(el), () => observer.disconnect();
2030
+ }, [ fitAll ]);
2031
+ const onNodeClick = useCallback((_e, node) => {
2032
+ if (node.type === "stageGroup") {
2033
+ setGroupsExpanded(!0);
2034
+ return;
2035
+ }
2036
+ onSelectStage?.(node.id);
2037
+ }, [ onSelectStage ]), onPaneClick = useCallback(() => {
2038
+ setGroupsExpanded(!1), onSelectStage?.(void 0);
2039
+ }, [ onSelectStage ]), [hoveredStage, setHoveredStage] = useState(void 0), onNodeMouseEnter = useCallback((_e, node) => setHoveredStage(node.id), []), onNodeMouseLeave = useCallback(() => setHoveredStage(void 0), []), onMoveStart = useCallback(event => {
2040
+ event && (userMovedRef.current = !0);
2041
+ }, []);
2042
+ return layoutFailed ? /* @__PURE__ */ jsx(Card, {
2043
+ padding: 4,
2044
+ tone: "critical",
2045
+ border: !0,
2046
+ radius: 2,
2047
+ children: /* @__PURE__ */ jsx(Text, {
2048
+ size: 1,
2049
+ children: "Couldn’t lay out this workflow diagram — see the browser console for the underlying error."
2050
+ })
2051
+ }) : /* @__PURE__ */ jsx(Card, {
2052
+ ref: cardRef,
2053
+ style: {
2054
+ ...hugStyle(bounds, {
2055
+ height: height,
2056
+ isStatic: isStatic
2057
+ }),
2058
+ ...WS_VARS,
2059
+ background: "color-mix(in srgb, var(--ws-ink) 3%, var(--ws-panel))",
2060
+ border: "1px solid var(--ws-border)",
2061
+ borderRadius: 6,
2062
+ overflow: "hidden"
2063
+ },
2064
+ children: /* @__PURE__ */ jsx(DevModeContext.Provider, {
2065
+ value: devMode ?? !1,
2066
+ children: /* @__PURE__ */ jsx(ExplainContext.Provider, {
2067
+ value: explainData,
2068
+ children: /* @__PURE__ */ jsx(HoveredStageContext.Provider, {
2069
+ value: hoveredStage,
2070
+ children: /* @__PURE__ */ jsx(ReactFlowProvider, {
2071
+ children: /* @__PURE__ */ jsxs(ReactFlow, {
2072
+ nodes: laidOut.nodes,
2073
+ edges: laidOut.edges,
2074
+ nodeTypes: nodeTypes,
2075
+ edgeTypes: edgeTypes,
2076
+ onInit: instance => {
2077
+ flowRef.current = instance;
2078
+ },
2079
+ ...onSelectStage || localGroupEdges ? {
2080
+ onNodeClick: onNodeClick,
2081
+ onPaneClick: onPaneClick
2082
+ } : {},
2083
+ onNodeMouseEnter: onNodeMouseEnter,
2084
+ onNodeMouseLeave: onNodeMouseLeave,
2085
+ onMoveStart: onMoveStart,
2086
+ maxZoom: 1.5,
2087
+ nodesDraggable: !1,
2088
+ nodesConnectable: !1,
2089
+ elementsSelectable: !1,
2090
+ zoomOnScroll: !1,
2091
+ ...viewportProps(isStatic),
2092
+ proOptions: {
2093
+ hideAttribution: !0
2094
+ },
2095
+ children: [
2096
+ /* @__PURE__ */ jsx(Background, {
2097
+ variant: BackgroundVariant.Dots,
2098
+ gap: 20,
2099
+ size: 1.5,
2100
+ color: "color-mix(in srgb, var(--ws-ink) 18%, transparent)"
2101
+ }), isStatic ? null : /* @__PURE__ */ jsx(Controls, {
2102
+ showInteractive: !1
2103
+ }) ]
2104
+ })
2105
+ })
2106
+ })
2107
+ })
2108
+ })
2109
+ });
2110
+ }
2111
+
2112
+ export { WS_CARD_TOKENS, WorkflowDiagram, flattenTransitions };