@cyoda/workflow-viewer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,941 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ WorkflowViewer: () => WorkflowViewer,
24
+ simpleLayout: () => simpleLayout
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/components/WorkflowViewer.tsx
29
+ var import_react2 = require("react");
30
+
31
+ // src/theme/tokens.ts
32
+ var workflowPalette = {
33
+ neutrals: {
34
+ white: "#FFFFFF",
35
+ white95: "#FFFFFFF2",
36
+ white75: "#FFFFFFBF",
37
+ slate200: "#E2E8F0",
38
+ slate300: "#CBD5E1",
39
+ slate500: "#64748B",
40
+ slate600: "#475569",
41
+ slate900: "#0F172A"
42
+ },
43
+ node: {
44
+ default: {
45
+ fill: "#F0FDFA",
46
+ border: "#2DD4BF",
47
+ meta: "#0F766E",
48
+ title: "#0F172A"
49
+ },
50
+ initial: {
51
+ fill: "#D1FAE5",
52
+ border: "#059669",
53
+ meta: "#047857",
54
+ title: "#022C22"
55
+ },
56
+ terminal: {
57
+ fill: "#FFF1F2",
58
+ border: "#FDA4AF",
59
+ meta: "#BE123C",
60
+ title: "#4C0519",
61
+ innerRing: "#FFFFFFBF"
62
+ },
63
+ manualReview: {
64
+ fill: "#F5F3FF",
65
+ border: "#C4B5FD",
66
+ meta: "#6D28D9",
67
+ title: "#2E1065"
68
+ },
69
+ processing: {
70
+ fill: "#F0F9FF",
71
+ border: "#7DD3FC",
72
+ meta: "#0369A1",
73
+ title: "#082F49"
74
+ }
75
+ },
76
+ edge: {
77
+ automated: "#64748B",
78
+ manual: "#8B5CF6",
79
+ conditional: "#F59E0B",
80
+ processing: "#0EA5E9",
81
+ terminal: "#FB7185",
82
+ loop: "#14B8A6",
83
+ disabled: "#CBD5E1",
84
+ arrowhead: "#64748B"
85
+ },
86
+ edgeLabel: {
87
+ fill: "#FFFFFFF2",
88
+ border: "#E2E8F0",
89
+ text: "#475569"
90
+ },
91
+ badge: {
92
+ manual: { fill: "#F5F3FF", border: "#DDD6FE" },
93
+ processor: { fill: "#F0F9FF", border: "#BAE6FD" },
94
+ criterion: { fill: "#FFFBEB", border: "#FDE68A" },
95
+ disabled: { fill: "#F8FAFC", border: "#E2E8F0" },
96
+ text: "#475569"
97
+ }
98
+ };
99
+ var typography = {
100
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Inter", system-ui, sans-serif',
101
+ monoFamily: 'ui-monospace, "SF Mono", "Cascadia Code", Menlo, monospace',
102
+ stateCategory: { size: 10, weight: 700, tracking: "0.12em" },
103
+ stateTitle: { size: 14, weight: 700, tracking: "0.01em" },
104
+ edgeLabel: { size: 9, weight: 700, tracking: "0.04em" },
105
+ badge: { size: 8, weight: 600, tracking: "0.04em" }
106
+ };
107
+ var geometry = {
108
+ node: {
109
+ width: 144,
110
+ height: 72,
111
+ radius: 8,
112
+ strokeWidth: 1.5,
113
+ terminalInset: 3,
114
+ terminalInnerRadius: 6
115
+ },
116
+ edge: {
117
+ strokeWidth: 1.8,
118
+ loopStrokeWidth: 1.6,
119
+ arrowheadSize: 6
120
+ },
121
+ labelPill: {
122
+ paddingX: 6,
123
+ paddingY: 3,
124
+ radius: 6,
125
+ shadowOpacity: 0.08
126
+ }
127
+ };
128
+
129
+ // src/layout.ts
130
+ function simpleLayout(graph) {
131
+ const { width: nodeW, height: nodeH } = geometry.node;
132
+ const hGap = 48;
133
+ const vGap = 48;
134
+ const positions = /* @__PURE__ */ new Map();
135
+ const nodesByWorkflow = groupByWorkflow(graph.nodes);
136
+ let yCursor = 24;
137
+ let maxWidth = 0;
138
+ for (const wfNodes of nodesByWorkflow.values()) {
139
+ const layers = layerByBFS(wfNodes, graph);
140
+ let y = yCursor;
141
+ for (const layer of layers) {
142
+ const layerWidth = layer.length * nodeW + (layer.length - 1) * hGap;
143
+ maxWidth = Math.max(maxWidth, layerWidth + 48);
144
+ let x = Math.max(24, (maxWidth - layerWidth) / 2);
145
+ for (const node of layer) {
146
+ positions.set(node.id, { id: node.id, x, y, width: nodeW, height: nodeH });
147
+ x += nodeW + hGap;
148
+ }
149
+ y += nodeH + vGap;
150
+ }
151
+ yCursor = y + vGap;
152
+ }
153
+ return { positions, width: maxWidth + 24, height: yCursor };
154
+ }
155
+ function groupByWorkflow(nodes) {
156
+ const out = /* @__PURE__ */ new Map();
157
+ for (const n of nodes) {
158
+ const wf = "workflow" in n ? n.workflow : "";
159
+ const list = out.get(wf) ?? [];
160
+ list.push(n);
161
+ out.set(wf, list);
162
+ }
163
+ return out;
164
+ }
165
+ function layerByBFS(nodes, graph) {
166
+ const stateNodes = nodes.filter((n) => n.kind === "state");
167
+ const markers = nodes.filter((n) => n.kind === "startMarker");
168
+ if (stateNodes.length === 0) return markers.length > 0 ? [markers] : [];
169
+ const adj = /* @__PURE__ */ new Map();
170
+ const indeg = /* @__PURE__ */ new Map();
171
+ for (const n of stateNodes) {
172
+ adj.set(n.id, /* @__PURE__ */ new Set());
173
+ indeg.set(n.id, 0);
174
+ }
175
+ for (const e of graph.edges) {
176
+ if (e.kind !== "transition" || e.isLoopback) continue;
177
+ if (!adj.has(e.sourceId) || !adj.has(e.targetId)) continue;
178
+ const set = adj.get(e.sourceId);
179
+ if (!set.has(e.targetId)) {
180
+ set.add(e.targetId);
181
+ indeg.set(e.targetId, (indeg.get(e.targetId) ?? 0) + 1);
182
+ }
183
+ }
184
+ const layers = [];
185
+ if (markers.length > 0) layers.push(markers);
186
+ const byId = new Map(stateNodes.map((n) => [n.id, n]));
187
+ let frontier = stateNodes.filter((n) => (indeg.get(n.id) ?? 0) === 0);
188
+ const placed = /* @__PURE__ */ new Set();
189
+ while (frontier.length > 0) {
190
+ layers.push(frontier);
191
+ const next = [];
192
+ for (const n of frontier) {
193
+ placed.add(n.id);
194
+ for (const succ of adj.get(n.id) ?? []) {
195
+ const remaining2 = (indeg.get(succ) ?? 0) - 1;
196
+ indeg.set(succ, remaining2);
197
+ if (remaining2 === 0 && !placed.has(succ)) {
198
+ const node = byId.get(succ);
199
+ if (node) next.push(node);
200
+ }
201
+ }
202
+ }
203
+ frontier = next;
204
+ }
205
+ const remaining = stateNodes.filter((n) => !placed.has(n.id));
206
+ if (remaining.length > 0) layers.push(remaining);
207
+ return layers;
208
+ }
209
+
210
+ // src/hooks/usePanZoom.ts
211
+ var import_react = require("react");
212
+ var MIN_SCALE = 0.25;
213
+ var MAX_SCALE = 4;
214
+ var ZOOM_STEP = 1.1;
215
+ function usePanZoom(initial) {
216
+ const [transform, setTransform] = (0, import_react.useState)({
217
+ x: initial?.x ?? 0,
218
+ y: initial?.y ?? 0,
219
+ scale: initial?.scale ?? 1
220
+ });
221
+ const dragStart = (0, import_react.useRef)(null);
222
+ const onWheel = (0, import_react.useCallback)((e) => {
223
+ if (!e.ctrlKey && !e.metaKey) return;
224
+ e.preventDefault();
225
+ const delta = e.deltaY > 0 ? 1 / ZOOM_STEP : ZOOM_STEP;
226
+ setTransform((t) => {
227
+ const nextScale = clamp(t.scale * delta, MIN_SCALE, MAX_SCALE);
228
+ const ratio = nextScale / t.scale;
229
+ const rect = e.currentTarget.getBoundingClientRect();
230
+ const px = e.clientX - rect.left;
231
+ const py = e.clientY - rect.top;
232
+ return {
233
+ scale: nextScale,
234
+ x: px - (px - t.x) * ratio,
235
+ y: py - (py - t.y) * ratio
236
+ };
237
+ });
238
+ }, []);
239
+ const onMouseDown = (0, import_react.useCallback)((e) => {
240
+ if (e.button !== 0) return;
241
+ dragStart.current = { x: e.clientX, y: e.clientY, vx: transform.x, vy: transform.y };
242
+ }, [transform.x, transform.y]);
243
+ const onMouseMove = (0, import_react.useCallback)((e) => {
244
+ if (!dragStart.current) return;
245
+ const dx = e.clientX - dragStart.current.x;
246
+ const dy = e.clientY - dragStart.current.y;
247
+ setTransform((t) => ({ ...t, x: dragStart.current.vx + dx, y: dragStart.current.vy + dy }));
248
+ }, []);
249
+ const onMouseUp = (0, import_react.useCallback)(() => {
250
+ dragStart.current = null;
251
+ }, []);
252
+ const reset = (0, import_react.useCallback)(() => {
253
+ setTransform({ x: 0, y: 0, scale: 1 });
254
+ }, []);
255
+ return { transform, onWheel, onMouseDown, onMouseMove, onMouseUp, reset, setTransform };
256
+ }
257
+ function clamp(n, min, max) {
258
+ return Math.min(max, Math.max(min, n));
259
+ }
260
+
261
+ // src/theme/lane.ts
262
+ function laneColor(edge, opts) {
263
+ const e = workflowPalette.edge;
264
+ if (edge.disabled) return e.disabled;
265
+ if (edge.isLoopback) return e.loop;
266
+ if (opts.targetIsTerminal) return e.terminal;
267
+ if (edge.summary.processor && edge.summary.processor.kind !== "none") {
268
+ return e.processing;
269
+ }
270
+ if (edge.manual) return e.manual;
271
+ if (edge.summary.criterion) return e.conditional;
272
+ return e.automated;
273
+ }
274
+ function laneDashArray(edge) {
275
+ if (edge.disabled) return "3 2";
276
+ if (edge.isLoopback) return "6 4";
277
+ return void 0;
278
+ }
279
+
280
+ // src/components/EdgePath.tsx
281
+ var import_jsx_runtime = require("react/jsx-runtime");
282
+ function polylineToPath(points) {
283
+ if (points.length === 0) return "";
284
+ const [first, ...rest] = points;
285
+ let d = `M ${first.x} ${first.y}`;
286
+ for (const p of rest) d += ` L ${p.x} ${p.y}`;
287
+ return d;
288
+ }
289
+ function computeEdgeGeometry(edge, source, target) {
290
+ const sx = source.x + source.width / 2;
291
+ const sy = source.y + source.height / 2;
292
+ const tx = target.x + target.width / 2;
293
+ const ty = target.y + target.height / 2;
294
+ if (edge.isSelf) {
295
+ const rightX = source.x + source.width;
296
+ const topY = source.y + source.height / 3;
297
+ const bottomY = source.y + source.height * 2 / 3;
298
+ const loopX = rightX + 28;
299
+ const d2 = `M ${rightX} ${topY} C ${loopX} ${topY}, ${loopX} ${bottomY}, ${rightX} ${bottomY}`;
300
+ return { d: d2, midX: loopX, midY: (topY + bottomY) / 2 };
301
+ }
302
+ const offsetStep = 18;
303
+ const half = Math.floor(edge.parallelGroupSize / 2);
304
+ const signed = edge.parallelIndex - half;
305
+ const offset = signed * offsetStep;
306
+ const mx = (sx + tx) / 2 + offset;
307
+ const my = (sy + ty) / 2;
308
+ const d = `M ${sx} ${sy} Q ${mx} ${my}, ${tx} ${ty}`;
309
+ return { d, midX: mx, midY: my };
310
+ }
311
+ function EdgePath({
312
+ edge,
313
+ source,
314
+ target,
315
+ route,
316
+ targetIsTerminal,
317
+ highlighted,
318
+ dimmed,
319
+ selected,
320
+ onSelect,
321
+ onHoverEnter,
322
+ onHoverLeave
323
+ }) {
324
+ const color = laneColor(edge, { targetIsTerminal });
325
+ const dash = laneDashArray(edge);
326
+ const d = route && route.points.length >= 2 ? polylineToPath(route.points) : computeEdgeGeometry(edge, source, target).d;
327
+ const strokeWidth = selected || highlighted ? geometry.edge.strokeWidth + 0.8 : edge.isLoopback ? geometry.edge.loopStrokeWidth : geometry.edge.strokeWidth;
328
+ const opacity = dimmed ? 0.25 : 1;
329
+ const isManualSolid = edge.manual && !edge.disabled && !edge.isLoopback;
330
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
331
+ "g",
332
+ {
333
+ opacity,
334
+ onClick: (e) => {
335
+ e.stopPropagation();
336
+ onSelect(edge.id);
337
+ },
338
+ onMouseEnter: () => onHoverEnter(edge.id),
339
+ onMouseLeave: onHoverLeave,
340
+ style: { cursor: "pointer" },
341
+ "data-testid": `edge-${edge.id}`,
342
+ children: [
343
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d, fill: "none", stroke: "transparent", strokeWidth: 12 }),
344
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
345
+ "path",
346
+ {
347
+ d,
348
+ fill: "none",
349
+ stroke: color,
350
+ strokeWidth,
351
+ strokeDasharray: dash,
352
+ markerEnd: `url(#wf-arrow-${colorKey(color)})`
353
+ }
354
+ ),
355
+ isManualSolid && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
356
+ "path",
357
+ {
358
+ d,
359
+ fill: "none",
360
+ stroke: workflowPalette.neutrals.white,
361
+ strokeWidth: 0.6,
362
+ pointerEvents: "none"
363
+ }
364
+ )
365
+ ]
366
+ }
367
+ );
368
+ }
369
+ function colorKey(color) {
370
+ return color.replace("#", "").toLowerCase();
371
+ }
372
+ var laneColorSet = Object.values(workflowPalette.edge);
373
+
374
+ // src/components/Defs.tsx
375
+ var import_jsx_runtime2 = require("react/jsx-runtime");
376
+ function Defs() {
377
+ const size = geometry.edge.arrowheadSize;
378
+ const unique = Array.from(new Set(laneColorSet));
379
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("defs", { children: [
380
+ unique.map((color) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
381
+ "marker",
382
+ {
383
+ id: `wf-arrow-${colorKey(color)}`,
384
+ viewBox: `0 0 ${size * 2} ${size * 2}`,
385
+ refX: size * 1.85,
386
+ refY: size,
387
+ markerWidth: size,
388
+ markerHeight: size,
389
+ orient: "auto-start-reverse",
390
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
391
+ "path",
392
+ {
393
+ d: `M 0 ${size / 2} L ${size * 2} ${size} L 0 ${size * 1.5} z`,
394
+ fill: color
395
+ }
396
+ )
397
+ },
398
+ color
399
+ )),
400
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("filter", { id: "wf-node-shadow", x: "-10%", y: "-10%", width: "120%", height: "140%", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
401
+ "feDropShadow",
402
+ {
403
+ dx: 0,
404
+ dy: 2,
405
+ stdDeviation: 2,
406
+ floodColor: workflowPalette.neutrals.slate900,
407
+ floodOpacity: 0.08
408
+ }
409
+ ) }),
410
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
411
+ "filter",
412
+ {
413
+ id: "wf-node-shadow-strong",
414
+ x: "-10%",
415
+ y: "-10%",
416
+ width: "120%",
417
+ height: "140%",
418
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
419
+ "feDropShadow",
420
+ {
421
+ dx: 0,
422
+ dy: 3,
423
+ stdDeviation: 3,
424
+ floodColor: workflowPalette.neutrals.slate900,
425
+ floodOpacity: 0.18
426
+ }
427
+ )
428
+ }
429
+ ),
430
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("filter", { id: "wf-label-shadow", x: "-10%", y: "-10%", width: "120%", height: "140%", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
431
+ "feDropShadow",
432
+ {
433
+ dx: 0,
434
+ dy: 1,
435
+ stdDeviation: 1.2,
436
+ floodColor: workflowPalette.neutrals.slate900,
437
+ floodOpacity: geometry.labelPill.shadowOpacity
438
+ }
439
+ ) })
440
+ ] });
441
+ }
442
+
443
+ // src/components/StartMarker.tsx
444
+ var import_jsx_runtime3 = require("react/jsx-runtime");
445
+ function StartMarker({ position }) {
446
+ const cx = position.x + position.width / 2;
447
+ const cy = position.y + position.height / 2;
448
+ const r = Math.min(position.width, position.height) / 3;
449
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("g", { "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
450
+ "circle",
451
+ {
452
+ cx,
453
+ cy,
454
+ r,
455
+ fill: workflowPalette.node.initial.border,
456
+ stroke: workflowPalette.node.initial.meta,
457
+ strokeWidth: 1.5
458
+ }
459
+ ) });
460
+ }
461
+
462
+ // src/theme/role-label.ts
463
+ function roleCategoryLabel(node) {
464
+ if (node.role === "initial" || node.role === "initial-terminal") return "INITIAL";
465
+ if (node.role === "terminal") return "TERMINAL";
466
+ if (node.category === "MANUAL_REVIEW") return "MANUAL REVIEW";
467
+ if (node.category === "PROCESSING_STATE") return "PROCESSING STATE";
468
+ return "STATE";
469
+ }
470
+
471
+ // src/theme/node-palette.ts
472
+ function paletteFor(node) {
473
+ const p = workflowPalette.node;
474
+ if (node.role === "terminal" || node.role === "initial-terminal") return p.terminal;
475
+ if (node.role === "initial") return p.initial;
476
+ if (node.category === "MANUAL_REVIEW") return p.manualReview;
477
+ if (node.category === "PROCESSING_STATE") return p.processing;
478
+ return p.default;
479
+ }
480
+
481
+ // src/theme/badges.ts
482
+ function badgesFor(summary, flags) {
483
+ const out = [];
484
+ if (flags.manual) out.push({ key: "manual", label: "Manual" });
485
+ if (summary.processor) {
486
+ if (summary.processor.kind === "single") {
487
+ out.push({ key: "processor", label: summary.processor.name });
488
+ } else if (summary.processor.kind === "multiple") {
489
+ out.push({ key: "processor", label: `${summary.processor.count} processors` });
490
+ }
491
+ }
492
+ if (summary.criterion) {
493
+ const c = summary.criterion;
494
+ if (c.kind === "group") {
495
+ out.push({ key: "criterion", label: `${c.operator} \xB7 ${c.count}` });
496
+ } else {
497
+ out.push({ key: "criterion", label: "Criterion" });
498
+ }
499
+ }
500
+ if (summary.execution?.kind === "sync") {
501
+ out.push({ key: "execution", label: "SYNC" });
502
+ } else if (summary.execution?.kind === "asyncSameTx") {
503
+ out.push({ key: "execution", label: "ASYNC_SAME_TX" });
504
+ }
505
+ if (flags.disabled) out.push({ key: "disabled", label: "Disabled" });
506
+ return out;
507
+ }
508
+
509
+ // src/components/StateNode.tsx
510
+ var import_jsx_runtime4 = require("react/jsx-runtime");
511
+ function StateNodeView({
512
+ node,
513
+ position,
514
+ selected,
515
+ highlighted,
516
+ dimmed,
517
+ onSelect,
518
+ onHoverEnter,
519
+ onHoverLeave
520
+ }) {
521
+ const palette = paletteFor(node);
522
+ const { radius, strokeWidth, terminalInset, terminalInnerRadius } = geometry.node;
523
+ const { width, height } = position;
524
+ const isTerminal = node.role === "terminal" || node.role === "initial-terminal";
525
+ const isInitialTerminal = node.role === "initial-terminal";
526
+ const category = roleCategoryLabel(node);
527
+ const opacity = dimmed ? 0.35 : 1;
528
+ const outerStroke = selected ? workflowPalette.neutrals.slate900 : palette.border;
529
+ const outerStrokeWidth = selected ? strokeWidth + 1 : strokeWidth;
530
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
531
+ "g",
532
+ {
533
+ transform: `translate(${position.x}, ${position.y})`,
534
+ opacity,
535
+ onClick: (e) => {
536
+ e.stopPropagation();
537
+ onSelect(node.id);
538
+ },
539
+ onMouseEnter: () => onHoverEnter(node.id),
540
+ onMouseLeave: onHoverLeave,
541
+ style: { cursor: "pointer" },
542
+ "data-testid": `state-node-${node.stateCode}`,
543
+ "aria-label": `${category} ${node.stateCode}`,
544
+ role: "button",
545
+ tabIndex: 0,
546
+ children: [
547
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
548
+ "rect",
549
+ {
550
+ x: 0,
551
+ y: 0,
552
+ width,
553
+ height,
554
+ rx: radius,
555
+ ry: radius,
556
+ fill: palette.fill,
557
+ stroke: outerStroke,
558
+ strokeWidth: outerStrokeWidth,
559
+ filter: highlighted || selected ? "url(#wf-node-shadow-strong)" : "url(#wf-node-shadow)"
560
+ }
561
+ ),
562
+ isTerminal && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
563
+ "rect",
564
+ {
565
+ x: terminalInset,
566
+ y: terminalInset,
567
+ width: width - terminalInset * 2,
568
+ height: height - terminalInset * 2,
569
+ rx: terminalInnerRadius,
570
+ ry: terminalInnerRadius,
571
+ fill: "none",
572
+ stroke: "innerRing" in palette ? palette.innerRing : workflowPalette.neutrals.white75,
573
+ strokeWidth: 1
574
+ }
575
+ ),
576
+ isInitialTerminal && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
577
+ "rect",
578
+ {
579
+ x: terminalInset,
580
+ y: terminalInset,
581
+ width: width - terminalInset * 2,
582
+ height: height - terminalInset * 2,
583
+ rx: terminalInnerRadius,
584
+ ry: terminalInnerRadius,
585
+ fill: "none",
586
+ stroke: workflowPalette.node.initial.border,
587
+ strokeWidth: 1,
588
+ strokeDasharray: "3 3"
589
+ }
590
+ ),
591
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
592
+ "text",
593
+ {
594
+ x: width / 2,
595
+ y: height / 2 - 8,
596
+ textAnchor: "middle",
597
+ fill: palette.meta,
598
+ fontFamily: typography.fontFamily,
599
+ fontSize: typography.stateCategory.size,
600
+ fontWeight: typography.stateCategory.weight,
601
+ letterSpacing: typography.stateCategory.tracking,
602
+ children: category
603
+ }
604
+ ),
605
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
606
+ "text",
607
+ {
608
+ x: width / 2,
609
+ y: height / 2 + 12,
610
+ textAnchor: "middle",
611
+ fill: palette.title,
612
+ fontFamily: typography.monoFamily,
613
+ fontSize: typography.stateTitle.size,
614
+ fontWeight: typography.stateTitle.weight,
615
+ letterSpacing: typography.stateTitle.tracking,
616
+ children: truncate(node.stateCode, 18)
617
+ }
618
+ )
619
+ ]
620
+ }
621
+ );
622
+ }
623
+ function truncate(s, max) {
624
+ if (s.length <= max) return s;
625
+ return `${s.slice(0, max - 1)}\u2026`;
626
+ }
627
+
628
+ // src/components/EdgeLabel.tsx
629
+ var import_jsx_runtime5 = require("react/jsx-runtime");
630
+ var BADGE_HEIGHT = 14;
631
+ var BADGE_GAP = 4;
632
+ var LABEL_PADDING_X = geometry.labelPill.paddingX;
633
+ var LABEL_PADDING_Y = geometry.labelPill.paddingY;
634
+ var BADGE_TEXT_PADDING_X = 6;
635
+ function estimateWidth(text, fontSize) {
636
+ return Math.ceil(text.length * fontSize * 0.58);
637
+ }
638
+ function EdgeLabel({ edge, x, y, width, height, dimmed }) {
639
+ const title = edge.summary.display;
640
+ const badges = badgesFor(edge.summary, {
641
+ manual: edge.manual,
642
+ disabled: edge.disabled
643
+ });
644
+ const titleW = estimateWidth(title, typography.edgeLabel.size);
645
+ const badgeWidths = badges.map(
646
+ (b) => estimateWidth(b.label, typography.badge.size) + BADGE_TEXT_PADDING_X * 2
647
+ );
648
+ const badgesTotalW = badgeWidths.reduce((a, b) => a + b, 0) + Math.max(0, badges.length - 1) * BADGE_GAP;
649
+ const pillW = width ?? Math.max(titleW, badgesTotalW) + LABEL_PADDING_X * 2;
650
+ const hasBadges = badges.length > 0;
651
+ const pillH = height ?? typography.edgeLabel.size + LABEL_PADDING_Y * 2 + (hasBadges ? BADGE_HEIGHT + BADGE_GAP : 0);
652
+ const pillX = x - pillW / 2;
653
+ const pillY = y - pillH / 2;
654
+ const titleY = pillY + LABEL_PADDING_Y + typography.edgeLabel.size - 2;
655
+ const badgeY = titleY + BADGE_GAP + 2;
656
+ const opacity = dimmed ? 0.4 : 1;
657
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("g", { opacity, pointerEvents: "none", children: [
658
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
659
+ "rect",
660
+ {
661
+ x: pillX,
662
+ y: pillY,
663
+ width: pillW,
664
+ height: pillH,
665
+ rx: geometry.labelPill.radius,
666
+ ry: geometry.labelPill.radius,
667
+ fill: workflowPalette.edgeLabel.fill,
668
+ stroke: workflowPalette.edgeLabel.border,
669
+ strokeWidth: 1,
670
+ filter: "url(#wf-label-shadow)"
671
+ }
672
+ ),
673
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
674
+ "text",
675
+ {
676
+ x,
677
+ y: titleY,
678
+ textAnchor: "middle",
679
+ fill: workflowPalette.edgeLabel.text,
680
+ fontFamily: typography.fontFamily,
681
+ fontSize: typography.edgeLabel.size,
682
+ fontWeight: typography.edgeLabel.weight,
683
+ letterSpacing: typography.edgeLabel.tracking,
684
+ children: title
685
+ }
686
+ ),
687
+ hasBadges && renderBadges(badges, badgeWidths, pillX, pillW, badgeY)
688
+ ] });
689
+ }
690
+ function renderBadges(badges, widths, pillX, pillW, y) {
691
+ const totalW = widths.reduce((a, b) => a + b, 0) + Math.max(0, badges.length - 1) * BADGE_GAP;
692
+ let cursor = pillX + (pillW - totalW) / 2;
693
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("g", { children: badges.map((b, i) => {
694
+ const w = widths[i];
695
+ const slot = pickBadgePalette(b.key);
696
+ const node = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("g", { children: [
697
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
698
+ "rect",
699
+ {
700
+ x: cursor,
701
+ y,
702
+ width: w,
703
+ height: BADGE_HEIGHT,
704
+ rx: BADGE_HEIGHT / 2,
705
+ ry: BADGE_HEIGHT / 2,
706
+ fill: slot.fill,
707
+ stroke: slot.border,
708
+ strokeWidth: 1
709
+ }
710
+ ),
711
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
712
+ "text",
713
+ {
714
+ x: cursor + w / 2,
715
+ y: y + BADGE_HEIGHT - 4,
716
+ textAnchor: "middle",
717
+ fill: workflowPalette.badge.text,
718
+ fontFamily: typography.fontFamily,
719
+ fontSize: typography.badge.size,
720
+ fontWeight: typography.badge.weight,
721
+ letterSpacing: typography.badge.tracking,
722
+ children: b.label
723
+ }
724
+ )
725
+ ] }, `${b.key}-${i}`);
726
+ cursor += w + BADGE_GAP;
727
+ return node;
728
+ }) });
729
+ }
730
+ function pickBadgePalette(key) {
731
+ switch (key) {
732
+ case "manual":
733
+ return workflowPalette.badge.manual;
734
+ case "processor":
735
+ return workflowPalette.badge.processor;
736
+ case "criterion":
737
+ return workflowPalette.badge.criterion;
738
+ case "execution":
739
+ return workflowPalette.badge.processor;
740
+ case "disabled":
741
+ return workflowPalette.badge.disabled;
742
+ }
743
+ }
744
+
745
+ // src/components/WorkflowViewer.tsx
746
+ var import_jsx_runtime6 = require("react/jsx-runtime");
747
+ function WorkflowViewer({
748
+ graph,
749
+ layout,
750
+ width = "100%",
751
+ height = "100%",
752
+ selectedId,
753
+ onSelectionChange,
754
+ className
755
+ }) {
756
+ const effectiveLayout = (0, import_react2.useMemo)(
757
+ () => layout ?? simpleLayout(graph),
758
+ [graph, layout]
759
+ );
760
+ const pan = usePanZoom();
761
+ const [internalSelection, setInternalSelection] = (0, import_react2.useState)(null);
762
+ const [hovered, setHovered] = (0, import_react2.useState)(null);
763
+ const selection = selectedId ?? internalSelection;
764
+ const stateNodes = (0, import_react2.useMemo)(
765
+ () => graph.nodes.filter((n) => n.kind === "state"),
766
+ [graph.nodes]
767
+ );
768
+ const stateById = (0, import_react2.useMemo)(() => {
769
+ const m = /* @__PURE__ */ new Map();
770
+ for (const n of stateNodes) m.set(n.id, n);
771
+ return m;
772
+ }, [stateNodes]);
773
+ const transitionEdges = (0, import_react2.useMemo)(
774
+ () => graph.edges.filter((e) => e.kind === "transition"),
775
+ [graph.edges]
776
+ );
777
+ const highlightSet = (0, import_react2.useMemo)(
778
+ () => computeHighlightSet(hovered ?? selection, graph.nodes, graph.edges),
779
+ [hovered, selection, graph.nodes, graph.edges]
780
+ );
781
+ const anythingFocused = highlightSet !== null;
782
+ const handleSelect = (id) => {
783
+ setInternalSelection(id);
784
+ onSelectionChange?.(id);
785
+ };
786
+ const handleBackgroundClick = () => {
787
+ setInternalSelection(null);
788
+ onSelectionChange?.(null);
789
+ };
790
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
791
+ "svg",
792
+ {
793
+ width,
794
+ height,
795
+ viewBox: `0 0 ${effectiveLayout.width} ${effectiveLayout.height}`,
796
+ preserveAspectRatio: "xMidYMid meet",
797
+ onClick: handleBackgroundClick,
798
+ onWheel: pan.onWheel,
799
+ onMouseDown: pan.onMouseDown,
800
+ onMouseMove: pan.onMouseMove,
801
+ onMouseUp: pan.onMouseUp,
802
+ onMouseLeave: pan.onMouseUp,
803
+ className,
804
+ style: {
805
+ background: workflowPalette.neutrals.white,
806
+ fontFamily: "inherit",
807
+ userSelect: "none"
808
+ },
809
+ "data-testid": "workflow-viewer",
810
+ children: [
811
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Defs, {}),
812
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
813
+ "g",
814
+ {
815
+ transform: `translate(${pan.transform.x}, ${pan.transform.y}) scale(${pan.transform.scale})`,
816
+ children: [
817
+ transitionEdges.map((edge) => {
818
+ const source = effectiveLayout.positions.get(edge.sourceId);
819
+ const target = effectiveLayout.positions.get(edge.targetId);
820
+ if (!source || !target) return null;
821
+ const targetNode = stateById.get(edge.targetId);
822
+ const route = effectiveLayout.edges?.get(edge.id);
823
+ const isEdgeSelected = selection === edge.id;
824
+ const isHighlighted = highlightSet?.has(edge.id) ?? false;
825
+ const isDimmed = anythingFocused && !isHighlighted;
826
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
827
+ EdgePath,
828
+ {
829
+ edge,
830
+ source,
831
+ target,
832
+ route,
833
+ targetIsTerminal: targetNode?.role === "terminal" || targetNode?.role === "initial-terminal",
834
+ highlighted: isHighlighted,
835
+ dimmed: isDimmed,
836
+ selected: isEdgeSelected,
837
+ onSelect: handleSelect,
838
+ onHoverEnter: setHovered,
839
+ onHoverLeave: () => setHovered(null)
840
+ },
841
+ edge.id
842
+ );
843
+ }),
844
+ transitionEdges.map((edge) => {
845
+ const source = effectiveLayout.positions.get(edge.sourceId);
846
+ const target = effectiveLayout.positions.get(edge.targetId);
847
+ if (!source || !target) return null;
848
+ const route = effectiveLayout.edges?.get(edge.id);
849
+ const labelPos = route ? { midX: route.labelX, midY: route.labelY } : computeEdgeGeometry(edge, source, target);
850
+ const isHighlighted = highlightSet?.has(edge.id) ?? false;
851
+ const isDimmed = anythingFocused && !isHighlighted;
852
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
853
+ EdgeLabel,
854
+ {
855
+ edge,
856
+ x: labelPos.midX,
857
+ y: labelPos.midY,
858
+ width: route?.labelWidth,
859
+ height: route?.labelHeight,
860
+ dimmed: isDimmed
861
+ },
862
+ `label-${edge.id}`
863
+ );
864
+ }),
865
+ graph.nodes.map((node) => renderNode(node, effectiveLayout, {
866
+ selection,
867
+ highlightSet,
868
+ anythingFocused,
869
+ onSelect: handleSelect,
870
+ onHoverEnter: setHovered,
871
+ onHoverLeave: () => setHovered(null)
872
+ }))
873
+ ]
874
+ }
875
+ )
876
+ ]
877
+ }
878
+ );
879
+ }
880
+ function renderNode(node, layout, ctx) {
881
+ const pos = layout.positions.get(node.id);
882
+ if (!pos) return null;
883
+ if (node.kind === "startMarker") {
884
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StartMarker, { position: smallPositionForMarker(pos) }, node.id);
885
+ }
886
+ const isHighlighted = ctx.highlightSet?.has(node.id) ?? false;
887
+ const isDimmed = ctx.anythingFocused && !isHighlighted;
888
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
889
+ StateNodeView,
890
+ {
891
+ node,
892
+ position: pos,
893
+ selected: ctx.selection === node.id,
894
+ highlighted: isHighlighted,
895
+ dimmed: isDimmed,
896
+ onSelect: ctx.onSelect,
897
+ onHoverEnter: ctx.onHoverEnter,
898
+ onHoverLeave: ctx.onHoverLeave
899
+ },
900
+ node.id
901
+ );
902
+ }
903
+ function smallPositionForMarker(pos) {
904
+ const size = 16;
905
+ return {
906
+ id: pos.id,
907
+ x: pos.x + pos.width / 2 - size / 2,
908
+ y: pos.y + pos.height / 2 - size / 2,
909
+ width: size,
910
+ height: size
911
+ };
912
+ }
913
+ function computeHighlightSet(focusedId, nodes, edges) {
914
+ if (!focusedId) return null;
915
+ const set = /* @__PURE__ */ new Set();
916
+ set.add(focusedId);
917
+ const node = nodes.find((n) => n.id === focusedId);
918
+ if (node) {
919
+ for (const e of edges) {
920
+ if (e.kind !== "transition") continue;
921
+ if (e.sourceId === focusedId || e.targetId === focusedId) {
922
+ set.add(e.id);
923
+ set.add(e.sourceId);
924
+ set.add(e.targetId);
925
+ }
926
+ }
927
+ return set;
928
+ }
929
+ const edge = edges.find((e) => e.id === focusedId);
930
+ if (edge && edge.kind === "transition") {
931
+ set.add(edge.sourceId);
932
+ set.add(edge.targetId);
933
+ }
934
+ return set;
935
+ }
936
+ // Annotate the CommonJS export names for ESM import in node:
937
+ 0 && (module.exports = {
938
+ WorkflowViewer,
939
+ simpleLayout
940
+ });
941
+ //# sourceMappingURL=index.cjs.map