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