@cyoda/workflow-react 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.js ADDED
@@ -0,0 +1,2918 @@
1
+ // src/components/WorkflowEditor.tsx
2
+ import { useCallback as useCallback2, useEffect as useEffect3, useMemo as useMemo5, useState as useState5 } from "react";
3
+
4
+ // src/i18n/context.ts
5
+ import { createContext, useContext } from "react";
6
+
7
+ // src/i18n/en.ts
8
+ var defaultMessages = {
9
+ toolbar: {
10
+ undo: "Undo",
11
+ redo: "Redo",
12
+ validate: "Validate",
13
+ errors: "errors",
14
+ warnings: "warnings",
15
+ infos: "infos",
16
+ save: "Save",
17
+ addWorkflow: "Add workflow"
18
+ },
19
+ inspector: {
20
+ empty: "Select a node or edge to edit its properties.",
21
+ properties: "Properties",
22
+ json: "JSON",
23
+ name: "Name",
24
+ description: "Description",
25
+ version: "Version",
26
+ active: "Active",
27
+ initialState: "Initial state",
28
+ manual: "Manual",
29
+ disabled: "Disabled",
30
+ processors: "Processors",
31
+ criterion: "Criterion",
32
+ executionMode: "Execution mode",
33
+ addProcessor: "Add processor",
34
+ removeProcessor: "Remove",
35
+ moveUp: "Move up",
36
+ moveDown: "Move down",
37
+ issues: "Issues",
38
+ sourceAnchor: "Source anchor",
39
+ targetAnchor: "Target anchor",
40
+ anchorDefault: "Default",
41
+ anchorTop: "Top",
42
+ anchorRight: "Right",
43
+ anchorBottom: "Bottom",
44
+ anchorLeft: "Left"
45
+ },
46
+ confirmDelete: {
47
+ title: "Delete state?",
48
+ message: "Deleting this state will also remove transitions that reference it.",
49
+ transitionsAffected: "Transitions affected",
50
+ confirm: "Delete",
51
+ cancel: "Cancel"
52
+ },
53
+ dragConnect: {
54
+ title: "New transition",
55
+ transitionName: "Transition name",
56
+ create: "Create",
57
+ cancel: "Cancel",
58
+ invalidName: "Name must start with a letter and contain only letters, digits, underscores, or hyphens.",
59
+ duplicateName: "A transition with this name already exists on the source state."
60
+ },
61
+ tabs: {
62
+ closeTab: "Close",
63
+ untitled: "(unnamed)"
64
+ },
65
+ saveConfirm: {
66
+ title: "Save workflows?",
67
+ modeLabel: "Import mode",
68
+ ackReplace: "I understand this will REPLACE all workflows on the server for this entity.",
69
+ ackActivate: "I understand this will ACTIVATE these workflows and deactivate the current set.",
70
+ ackWarnings: "I acknowledge {count} warning(s) will be saved.",
71
+ confirm: "Save",
72
+ cancel: "Cancel"
73
+ },
74
+ conflict: {
75
+ message: "Server state has changed since this editor was opened. Choose Reload to discard local changes or Force overwrite to keep them.",
76
+ reload: "Reload",
77
+ forceOverwrite: "Force overwrite"
78
+ }
79
+ };
80
+
81
+ // src/i18n/context.ts
82
+ var I18nContext = createContext(defaultMessages);
83
+ function useMessages() {
84
+ return useContext(I18nContext);
85
+ }
86
+ function mergeMessages(overrides) {
87
+ if (!overrides) return defaultMessages;
88
+ const next = { ...defaultMessages };
89
+ for (const key of Object.keys(overrides)) {
90
+ const base = defaultMessages[key] ?? {};
91
+ const patch = overrides[key] ?? {};
92
+ next[key] = { ...base, ...patch };
93
+ }
94
+ return next;
95
+ }
96
+
97
+ // src/state/store.ts
98
+ import { useCallback, useMemo, useRef, useState } from "react";
99
+ import {
100
+ applyPatch,
101
+ invertPatch
102
+ } from "@cyoda/workflow-core";
103
+ var MAX_UNDO = 100;
104
+ function summarize(patch) {
105
+ switch (patch.op) {
106
+ case "addWorkflow":
107
+ return `Add workflow "${patch.workflow.name}"`;
108
+ case "removeWorkflow":
109
+ return `Remove workflow "${patch.workflow}"`;
110
+ case "updateWorkflowMeta":
111
+ return `Update workflow "${patch.workflow}"`;
112
+ case "renameWorkflow":
113
+ return `Rename workflow "${patch.from}" \u2192 "${patch.to}"`;
114
+ case "setInitialState":
115
+ return `Set initial state to "${patch.stateCode}"`;
116
+ case "setWorkflowCriterion":
117
+ return patch.criterion ? `Set workflow criterion` : `Clear workflow criterion`;
118
+ case "addState":
119
+ return `Add state "${patch.stateCode}"`;
120
+ case "renameState":
121
+ return `Rename state "${patch.from}" \u2192 "${patch.to}"`;
122
+ case "removeState":
123
+ return `Remove state "${patch.stateCode}"`;
124
+ case "addTransition":
125
+ return `Add transition "${patch.transition.name}"`;
126
+ case "updateTransition":
127
+ return `Update transition`;
128
+ case "removeTransition":
129
+ return `Remove transition`;
130
+ case "reorderTransition":
131
+ return `Reorder transition`;
132
+ case "addProcessor":
133
+ return `Add processor "${patch.processor.name}"`;
134
+ case "updateProcessor":
135
+ return `Update processor`;
136
+ case "removeProcessor":
137
+ return `Remove processor`;
138
+ case "reorderProcessor":
139
+ return `Reorder processor`;
140
+ case "setCriterion":
141
+ return patch.criterion ? `Set criterion` : `Clear criterion`;
142
+ case "setImportMode":
143
+ return `Set import mode to "${patch.mode}"`;
144
+ case "setEntity":
145
+ return patch.entity ? `Set entity` : `Clear entity`;
146
+ case "replaceSession":
147
+ return `Replace session`;
148
+ case "setEdgeAnchors":
149
+ return patch.anchors ? `Update edge anchors` : `Clear edge anchors`;
150
+ }
151
+ }
152
+ function pickDefaultActiveWorkflow(doc) {
153
+ return doc.session.workflows[0]?.name ?? null;
154
+ }
155
+ function useEditorStore(initialDocument, initialMode = "editor") {
156
+ const [state, setState] = useState(() => ({
157
+ document: initialDocument,
158
+ selection: null,
159
+ activeWorkflow: pickDefaultActiveWorkflow(initialDocument),
160
+ mode: initialMode,
161
+ undoStack: [],
162
+ redoStack: []
163
+ }));
164
+ const stateRef = useRef(state);
165
+ stateRef.current = state;
166
+ const dispatch = useCallback((patch, summary) => {
167
+ const current = stateRef.current;
168
+ if (current.mode === "viewer") return;
169
+ const nextDoc = applyPatch(current.document, patch);
170
+ const inverse = invertPatch(current.document, patch);
171
+ const entry = {
172
+ forward: patch,
173
+ inverse,
174
+ summary: summary ?? summarize(patch)
175
+ };
176
+ const undoStack = [...current.undoStack, entry].slice(-MAX_UNDO);
177
+ setState({
178
+ ...current,
179
+ document: nextDoc,
180
+ undoStack,
181
+ redoStack: [],
182
+ activeWorkflow: reconcileActiveWorkflow(current.activeWorkflow, nextDoc),
183
+ selection: reconcileSelection(current.selection, nextDoc)
184
+ });
185
+ }, []);
186
+ const silentReplace = useCallback((document2, options) => {
187
+ const current = stateRef.current;
188
+ if (options?.preserveEditorState) {
189
+ setState({
190
+ ...current,
191
+ document: document2,
192
+ activeWorkflow: reconcileActiveWorkflow(current.activeWorkflow, document2),
193
+ selection: reconcileSelection(current.selection, document2)
194
+ });
195
+ return;
196
+ }
197
+ setState({
198
+ ...current,
199
+ document: document2,
200
+ undoStack: [],
201
+ redoStack: [],
202
+ activeWorkflow: pickDefaultActiveWorkflow(document2),
203
+ selection: null
204
+ });
205
+ }, []);
206
+ const undo = useCallback(() => {
207
+ const current = stateRef.current;
208
+ const top = current.undoStack[current.undoStack.length - 1];
209
+ if (!top) return;
210
+ const reverted = applyPatch(current.document, top.inverse);
211
+ setState({
212
+ ...current,
213
+ document: reverted,
214
+ undoStack: current.undoStack.slice(0, -1),
215
+ redoStack: [...current.redoStack, top],
216
+ activeWorkflow: reconcileActiveWorkflow(current.activeWorkflow, reverted),
217
+ selection: reconcileSelection(current.selection, reverted)
218
+ });
219
+ }, []);
220
+ const redo = useCallback(() => {
221
+ const current = stateRef.current;
222
+ const top = current.redoStack[current.redoStack.length - 1];
223
+ if (!top) return;
224
+ const next = applyPatch(current.document, top.forward);
225
+ setState({
226
+ ...current,
227
+ document: next,
228
+ undoStack: [...current.undoStack, top],
229
+ redoStack: current.redoStack.slice(0, -1),
230
+ activeWorkflow: reconcileActiveWorkflow(current.activeWorkflow, next),
231
+ selection: reconcileSelection(current.selection, next)
232
+ });
233
+ }, []);
234
+ const setSelection = useCallback((sel) => {
235
+ setState((s) => ({ ...s, selection: sel }));
236
+ }, []);
237
+ const setActiveWorkflow = useCallback((name) => {
238
+ setState((s) => ({ ...s, activeWorkflow: name, selection: null }));
239
+ }, []);
240
+ const setMode = useCallback((mode) => {
241
+ setState((s) => ({ ...s, mode }));
242
+ }, []);
243
+ const actions = useMemo(
244
+ () => ({ dispatch, silentReplace, undo, redo, setSelection, setActiveWorkflow, setMode }),
245
+ [dispatch, silentReplace, undo, redo, setSelection, setActiveWorkflow, setMode]
246
+ );
247
+ return [state, actions];
248
+ }
249
+ function reconcileActiveWorkflow(current, doc) {
250
+ if (!current) return doc.session.workflows[0]?.name ?? null;
251
+ const hit = doc.session.workflows.find((w) => w.name === current);
252
+ if (hit) return current;
253
+ return doc.session.workflows[0]?.name ?? null;
254
+ }
255
+ function reconcileSelection(selection, doc) {
256
+ if (!selection) return null;
257
+ const { ids } = doc.meta;
258
+ switch (selection.kind) {
259
+ case "workflow": {
260
+ const hit = doc.session.workflows.find((w) => w.name === selection.workflow);
261
+ return hit ? selection : null;
262
+ }
263
+ case "state": {
264
+ const wf = doc.session.workflows.find((w) => w.name === selection.workflow);
265
+ if (!wf || !wf.states[selection.stateCode]) return null;
266
+ return selection;
267
+ }
268
+ case "transition":
269
+ return ids.transitions[selection.transitionUuid] ? selection : null;
270
+ case "processor":
271
+ return ids.processors[selection.processorUuid] ? selection : null;
272
+ case "criterion":
273
+ return ids.criteria[selection.hostId] || ids.workflows[selection.hostId] || ids.transitions[selection.hostId] ? selection : null;
274
+ }
275
+ }
276
+
277
+ // src/state/derive.ts
278
+ import {
279
+ validateSession
280
+ } from "@cyoda/workflow-core";
281
+ import { projectToGraph } from "@cyoda/workflow-graph";
282
+ function deriveFromDocument(doc) {
283
+ const issues = validateSession(doc.session);
284
+ const graph = projectToGraph(doc, { issues });
285
+ let errorCount = 0;
286
+ let warningCount = 0;
287
+ let infoCount = 0;
288
+ for (const issue of issues) {
289
+ if (issue.severity === "error") errorCount++;
290
+ else if (issue.severity === "warning") warningCount++;
291
+ else infoCount++;
292
+ }
293
+ return { graph, issues, errorCount, warningCount, infoCount };
294
+ }
295
+
296
+ // src/components/Canvas.tsx
297
+ import { useEffect, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
298
+ import {
299
+ Background,
300
+ ConnectionMode,
301
+ Controls,
302
+ MiniMap,
303
+ ReactFlow,
304
+ ReactFlowProvider,
305
+ useReactFlow
306
+ } from "reactflow";
307
+ import "reactflow/dist/style.css";
308
+ import { layoutGraph, estimateNodeSize } from "@cyoda/workflow-layout";
309
+
310
+ // src/components/ArrowMarkers.tsx
311
+ import { geometry, workflowPalette } from "@cyoda/workflow-viewer/theme";
312
+ import { jsx } from "react/jsx-runtime";
313
+ function ArrowMarkers() {
314
+ const size = geometry.edge.arrowheadSize;
315
+ const colors = Array.from(new Set(Object.values(workflowPalette.edge)));
316
+ return /* @__PURE__ */ jsx(
317
+ "svg",
318
+ {
319
+ width: 0,
320
+ height: 0,
321
+ style: { position: "absolute", pointerEvents: "none" },
322
+ "aria-hidden": true,
323
+ children: /* @__PURE__ */ jsx("defs", { children: colors.map((color) => /* @__PURE__ */ jsx(
324
+ "marker",
325
+ {
326
+ id: arrowMarkerId(color),
327
+ viewBox: `0 0 ${size * 2} ${size * 2}`,
328
+ refX: size * 1.85,
329
+ refY: size,
330
+ markerWidth: size,
331
+ markerHeight: size,
332
+ orient: "auto-start-reverse",
333
+ children: /* @__PURE__ */ jsx(
334
+ "path",
335
+ {
336
+ d: `M 0 ${size / 2} L ${size * 2} ${size} L 0 ${size * 1.5} z`,
337
+ fill: color
338
+ }
339
+ )
340
+ },
341
+ color
342
+ )) })
343
+ }
344
+ );
345
+ }
346
+ function arrowMarkerId(color) {
347
+ return `wf-arrow-${color.replace("#", "").toLowerCase()}`;
348
+ }
349
+
350
+ // src/components/RfStateNode.tsx
351
+ import { memo } from "react";
352
+ import { Handle, Position } from "reactflow";
353
+ import {
354
+ geometry as geometry2,
355
+ paletteFor,
356
+ roleCategoryLabel,
357
+ typography,
358
+ workflowPalette as workflowPalette2
359
+ } from "@cyoda/workflow-viewer/theme";
360
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
361
+ function RfStateNodeImpl({ data, selected }) {
362
+ const { node, hasError, hasWarning, size } = data;
363
+ const palette = paletteFor(node);
364
+ const { radius, strokeWidth } = geometry2.node;
365
+ const width = size?.width ?? geometry2.node.width;
366
+ const height = size?.height ?? geometry2.node.height;
367
+ const category = roleCategoryLabel(node);
368
+ const isTerminal = node.role === "terminal" || node.role === "initial-terminal";
369
+ const borderColor = hasError ? "#DC2626" : hasWarning ? "#D97706" : selected ? workflowPalette2.neutrals.slate900 : palette.border;
370
+ const borderWidth = selected ? strokeWidth + 1 : strokeWidth;
371
+ return /* @__PURE__ */ jsxs(
372
+ "div",
373
+ {
374
+ style: {
375
+ width,
376
+ height,
377
+ background: palette.fill,
378
+ border: `${borderWidth}px solid ${borderColor}`,
379
+ borderRadius: radius,
380
+ boxShadow: selected ? "0 2px 4px rgba(15,23,42,0.14)" : "0 1px 2px rgba(15,23,42,0.08)",
381
+ position: "relative",
382
+ boxSizing: "border-box",
383
+ fontFamily: typography.fontFamily,
384
+ userSelect: "none"
385
+ },
386
+ "data-testid": `rf-state-${node.stateCode}`,
387
+ children: [
388
+ ANCHOR_SIDES.map(({ side, position }) => /* @__PURE__ */ jsx2(
389
+ AnchorHandle,
390
+ {
391
+ side,
392
+ position,
393
+ color: palette.border
394
+ },
395
+ side
396
+ )),
397
+ /* @__PURE__ */ jsxs(
398
+ "div",
399
+ {
400
+ style: {
401
+ display: "flex",
402
+ flexDirection: "column",
403
+ justifyContent: "center",
404
+ alignItems: "center",
405
+ height: "100%",
406
+ gap: 2,
407
+ padding: "0 8px"
408
+ },
409
+ children: [
410
+ /* @__PURE__ */ jsx2(
411
+ "div",
412
+ {
413
+ style: {
414
+ color: palette.meta,
415
+ fontSize: typography.stateCategory.size,
416
+ fontWeight: typography.stateCategory.weight,
417
+ letterSpacing: typography.stateCategory.tracking
418
+ },
419
+ children: category
420
+ }
421
+ ),
422
+ /* @__PURE__ */ jsx2(
423
+ "div",
424
+ {
425
+ style: {
426
+ color: palette.title,
427
+ fontFamily: typography.monoFamily,
428
+ fontSize: typography.stateTitle.size,
429
+ fontWeight: typography.stateTitle.weight,
430
+ letterSpacing: typography.stateTitle.tracking,
431
+ textAlign: "center",
432
+ overflow: "hidden",
433
+ textOverflow: "ellipsis",
434
+ whiteSpace: "nowrap",
435
+ maxWidth: "100%"
436
+ },
437
+ children: node.stateCode
438
+ }
439
+ )
440
+ ]
441
+ }
442
+ ),
443
+ isTerminal && /* @__PURE__ */ jsx2(
444
+ "div",
445
+ {
446
+ style: {
447
+ position: "absolute",
448
+ inset: 3,
449
+ borderRadius: 8,
450
+ border: `1px solid ${"innerRing" in palette ? palette.innerRing : workflowPalette2.neutrals.white75}`,
451
+ pointerEvents: "none"
452
+ }
453
+ }
454
+ )
455
+ ]
456
+ }
457
+ );
458
+ }
459
+ var ANCHOR_SIDES = [
460
+ { side: "top", position: Position.Top },
461
+ { side: "right", position: Position.Right },
462
+ { side: "bottom", position: Position.Bottom },
463
+ { side: "left", position: Position.Left }
464
+ ];
465
+ function AnchorHandle({
466
+ side,
467
+ position,
468
+ color
469
+ }) {
470
+ const isVertical = position === Position.Top || position === Position.Bottom;
471
+ const dotStyle = {
472
+ position: "absolute",
473
+ width: 8,
474
+ height: 8,
475
+ background: color,
476
+ borderRadius: "50%",
477
+ pointerEvents: "none",
478
+ ...side === "top" ? { top: -4, left: "calc(50% - 4px)" } : side === "bottom" ? { bottom: -4, left: "calc(50% - 4px)" } : side === "left" ? { left: -4, top: "calc(50% - 4px)" } : { right: -4, top: "calc(50% - 4px)" }
479
+ };
480
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
481
+ /* @__PURE__ */ jsx2(
482
+ Handle,
483
+ {
484
+ id: side,
485
+ type: "source",
486
+ position,
487
+ style: {
488
+ background: "transparent",
489
+ border: "none",
490
+ borderRadius: 0,
491
+ width: isVertical ? "80%" : 16,
492
+ height: isVertical ? 16 : "80%"
493
+ }
494
+ }
495
+ ),
496
+ /* @__PURE__ */ jsx2("div", { style: dotStyle })
497
+ ] });
498
+ }
499
+ var RfStateNode = memo(RfStateNodeImpl);
500
+
501
+ // src/components/RfTransitionEdge.tsx
502
+ import { memo as memo2 } from "react";
503
+ import {
504
+ BaseEdge,
505
+ EdgeLabelRenderer
506
+ } from "reactflow";
507
+ import {
508
+ badgesFor,
509
+ geometry as geometry3,
510
+ laneColor,
511
+ laneDashArray,
512
+ typography as typography2,
513
+ workflowPalette as workflowPalette3
514
+ } from "@cyoda/workflow-viewer/theme";
515
+
516
+ // src/routing/orthogonal.ts
517
+ import { Position as Position2 } from "reactflow";
518
+ var DEFAULT_TOLERANCE = 6;
519
+ var DEFAULT_STUB = 16;
520
+ function orthogonalEdgePath(input) {
521
+ const {
522
+ sourceX,
523
+ sourceY,
524
+ targetX,
525
+ targetY,
526
+ sourcePosition,
527
+ targetPosition,
528
+ routePoints,
529
+ obstacles = [],
530
+ alignmentTolerance = DEFAULT_TOLERANCE,
531
+ stubLength = DEFAULT_STUB
532
+ } = input;
533
+ if (routePoints && routePoints.length >= 2) {
534
+ const mid2 = midpoint(routePoints);
535
+ return {
536
+ path: polylineToPath(routePoints),
537
+ labelX: mid2.x,
538
+ labelY: mid2.y,
539
+ points: routePoints
540
+ };
541
+ }
542
+ const sx = sourceX;
543
+ const sy = sourceY;
544
+ const tx = targetX;
545
+ const ty = targetY;
546
+ const sourceNormal = normalOf(sourcePosition);
547
+ const targetNormal = normalOf(targetPosition);
548
+ const straight = tryStraight(
549
+ { x: sx, y: sy },
550
+ sourceNormal,
551
+ { x: tx, y: ty },
552
+ targetNormal,
553
+ alignmentTolerance
554
+ );
555
+ if (straight) {
556
+ return {
557
+ path: polylineToPath(straight),
558
+ labelX: (sx + tx) / 2,
559
+ labelY: (sy + ty) / 2,
560
+ points: straight
561
+ };
562
+ }
563
+ const sStub = { x: sx + sourceNormal.x * stubLength, y: sy + sourceNormal.y * stubLength };
564
+ const tStub = { x: tx + targetNormal.x * stubLength, y: ty + targetNormal.y * stubLength };
565
+ const sourceAxis = sourceNormal.x !== 0 ? "horizontal" : "vertical";
566
+ let path;
567
+ if (sourceAxis === "vertical") {
568
+ let midY = (sStub.y + tStub.y) / 2;
569
+ midY = nudgeHorizontalLine(
570
+ sStub.x,
571
+ tStub.x,
572
+ midY,
573
+ obstacles
574
+ );
575
+ path = [
576
+ { x: sx, y: sy },
577
+ { x: sStub.x, y: midY },
578
+ { x: tStub.x, y: midY },
579
+ { x: tx, y: ty }
580
+ ];
581
+ } else {
582
+ let midX = (sStub.x + tStub.x) / 2;
583
+ midX = nudgeVerticalLine(
584
+ sStub.y,
585
+ tStub.y,
586
+ midX,
587
+ obstacles
588
+ );
589
+ path = [
590
+ { x: sx, y: sy },
591
+ { x: midX, y: sStub.y },
592
+ { x: midX, y: tStub.y },
593
+ { x: tx, y: ty }
594
+ ];
595
+ }
596
+ path = simplify(path);
597
+ const mid = midpoint(path);
598
+ return {
599
+ path: polylineToPath(path),
600
+ labelX: mid.x,
601
+ labelY: mid.y,
602
+ points: path
603
+ };
604
+ }
605
+ function polylineToPath(points) {
606
+ if (points.length === 0) return "";
607
+ const [first, ...rest] = points;
608
+ let d = `M ${first.x} ${first.y}`;
609
+ for (const p of rest) d += ` L ${p.x} ${p.y}`;
610
+ return d;
611
+ }
612
+ function normalOf(pos) {
613
+ switch (pos) {
614
+ case Position2.Top:
615
+ return { x: 0, y: -1 };
616
+ case Position2.Right:
617
+ return { x: 1, y: 0 };
618
+ case Position2.Bottom:
619
+ return { x: 0, y: 1 };
620
+ case Position2.Left:
621
+ return { x: -1, y: 0 };
622
+ }
623
+ }
624
+ function tryStraight(s, sn, t, tn, tolerance) {
625
+ if (sn.x + tn.x !== 0 || sn.y + tn.y !== 0) return null;
626
+ if (sn.x !== 0) {
627
+ if (Math.abs(s.y - t.y) > tolerance) return null;
628
+ if (sn.x > 0 && t.x < s.x) return null;
629
+ if (sn.x < 0 && t.x > s.x) return null;
630
+ return [
631
+ { x: s.x, y: s.y },
632
+ { x: t.x, y: s.y }
633
+ ];
634
+ }
635
+ if (Math.abs(s.x - t.x) > tolerance) return null;
636
+ if (sn.y > 0 && t.y < s.y) return null;
637
+ if (sn.y < 0 && t.y > s.y) return null;
638
+ return [
639
+ { x: s.x, y: s.y },
640
+ { x: s.x, y: t.y }
641
+ ];
642
+ }
643
+ function nudgeHorizontalLine(x1, x2, y, obstacles) {
644
+ const pad = 8;
645
+ const loX = Math.min(x1, x2);
646
+ const hiX = Math.max(x1, x2);
647
+ for (const o of obstacles) {
648
+ const ox1 = o.x - pad;
649
+ const oy1 = o.y - pad;
650
+ const ox2 = o.x + o.width + pad;
651
+ const oy2 = o.y + o.height + pad;
652
+ if (hiX < ox1 || loX > ox2) continue;
653
+ if (y < oy1 || y > oy2) continue;
654
+ const above = oy1 - 1;
655
+ const below = oy2 + 1;
656
+ y = Math.abs(y - above) < Math.abs(y - below) ? above : below;
657
+ }
658
+ return y;
659
+ }
660
+ function nudgeVerticalLine(y1, y2, x, obstacles) {
661
+ const pad = 8;
662
+ const loY = Math.min(y1, y2);
663
+ const hiY = Math.max(y1, y2);
664
+ for (const o of obstacles) {
665
+ const ox1 = o.x - pad;
666
+ const oy1 = o.y - pad;
667
+ const ox2 = o.x + o.width + pad;
668
+ const oy2 = o.y + o.height + pad;
669
+ if (hiY < oy1 || loY > oy2) continue;
670
+ if (x < ox1 || x > ox2) continue;
671
+ const left = ox1 - 1;
672
+ const right = ox2 + 1;
673
+ x = Math.abs(x - left) < Math.abs(x - right) ? left : right;
674
+ }
675
+ return x;
676
+ }
677
+ function simplify(points) {
678
+ const out = [];
679
+ for (const p of points) {
680
+ const last = out[out.length - 1];
681
+ if (last && last.x === p.x && last.y === p.y) continue;
682
+ out.push(p);
683
+ }
684
+ let i = 1;
685
+ while (i < out.length - 1) {
686
+ const a = out[i - 1];
687
+ const b = out[i];
688
+ const c = out[i + 1];
689
+ const colinearX = a.x === b.x && b.x === c.x;
690
+ const colinearY = a.y === b.y && b.y === c.y;
691
+ if (colinearX || colinearY) {
692
+ out.splice(i, 1);
693
+ } else {
694
+ i++;
695
+ }
696
+ }
697
+ return out;
698
+ }
699
+ function midpoint(points) {
700
+ if (points.length === 0) return { x: 0, y: 0 };
701
+ if (points.length === 1) return points[0];
702
+ let bestLen = -1;
703
+ let best = points[0];
704
+ for (let i = 0; i < points.length - 1; i++) {
705
+ const a = points[i];
706
+ const b = points[i + 1];
707
+ const len = Math.hypot(b.x - a.x, b.y - a.y);
708
+ if (len > bestLen) {
709
+ bestLen = len;
710
+ best = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
711
+ }
712
+ }
713
+ return best;
714
+ }
715
+
716
+ // src/components/RfTransitionEdge.tsx
717
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
718
+ function RfTransitionEdgeImpl(props) {
719
+ const {
720
+ id,
721
+ sourceX,
722
+ sourceY,
723
+ targetX,
724
+ targetY,
725
+ sourcePosition,
726
+ targetPosition,
727
+ data,
728
+ selected
729
+ } = props;
730
+ if (!data) return null;
731
+ const { edge, targetIsTerminal, routePoints, obstacles } = data;
732
+ const { path, labelX: fallbackLabelX, labelY: fallbackLabelY } = orthogonalEdgePath({
733
+ sourceX,
734
+ sourceY,
735
+ targetX,
736
+ targetY,
737
+ sourcePosition,
738
+ targetPosition,
739
+ routePoints,
740
+ obstacles
741
+ });
742
+ const labelX = data.labelX ?? fallbackLabelX;
743
+ const labelY = data.labelY ?? fallbackLabelY;
744
+ const color = laneColor(edge, { targetIsTerminal });
745
+ const dash = laneDashArray(edge);
746
+ const strokeWidth = selected ? geometry3.edge.strokeWidth + 1 : edge.isLoopback ? geometry3.edge.loopStrokeWidth : geometry3.edge.strokeWidth;
747
+ const isManualSolid = edge.manual && !edge.disabled && !edge.isLoopback;
748
+ const badges = badgesFor(edge.summary, {
749
+ manual: edge.manual,
750
+ disabled: edge.disabled
751
+ });
752
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
753
+ /* @__PURE__ */ jsx3(
754
+ BaseEdge,
755
+ {
756
+ id,
757
+ path,
758
+ style: {
759
+ stroke: color,
760
+ strokeWidth,
761
+ strokeDasharray: dash
762
+ },
763
+ markerEnd: `url(#${arrowMarkerId(color)})`
764
+ }
765
+ ),
766
+ isManualSolid && /* @__PURE__ */ jsx3(
767
+ "path",
768
+ {
769
+ d: path,
770
+ fill: "none",
771
+ stroke: workflowPalette3.neutrals.white,
772
+ strokeWidth: 0.6,
773
+ pointerEvents: "none"
774
+ }
775
+ ),
776
+ /* @__PURE__ */ jsx3(EdgeLabelRenderer, { children: /* @__PURE__ */ jsxs2(
777
+ "div",
778
+ {
779
+ style: {
780
+ position: "absolute",
781
+ transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
782
+ fontFamily: typography2.fontFamily,
783
+ pointerEvents: "all",
784
+ background: workflowPalette3.edgeLabel.fill,
785
+ border: `1px solid ${workflowPalette3.edgeLabel.border}`,
786
+ borderRadius: geometry3.labelPill.radius,
787
+ padding: `${geometry3.labelPill.paddingY}px ${geometry3.labelPill.paddingX}px`,
788
+ boxShadow: "0 1px 2px rgba(15,23,42,0.08)",
789
+ display: "flex",
790
+ flexDirection: "column",
791
+ alignItems: "center",
792
+ gap: 3,
793
+ minWidth: 40,
794
+ width: data.labelWidth
795
+ },
796
+ className: "nodrag nopan",
797
+ "data-testid": `rf-edge-label-${edge.id}`,
798
+ children: [
799
+ /* @__PURE__ */ jsx3(
800
+ "div",
801
+ {
802
+ style: {
803
+ color: workflowPalette3.edgeLabel.text,
804
+ fontSize: typography2.edgeLabel.size,
805
+ fontWeight: typography2.edgeLabel.weight,
806
+ letterSpacing: typography2.edgeLabel.tracking,
807
+ textTransform: "uppercase"
808
+ },
809
+ children: edge.summary.display
810
+ }
811
+ ),
812
+ badges.length > 0 && /* @__PURE__ */ jsx3("div", { style: { display: "flex", gap: 3, flexWrap: "wrap", justifyContent: "center" }, children: badges.map((b, i) => {
813
+ const slot = b.key === "manual" ? workflowPalette3.badge.manual : b.key === "processor" || b.key === "execution" ? workflowPalette3.badge.processor : b.key === "criterion" ? workflowPalette3.badge.criterion : workflowPalette3.badge.disabled;
814
+ return /* @__PURE__ */ jsx3(
815
+ "span",
816
+ {
817
+ style: {
818
+ background: slot.fill,
819
+ border: `1px solid ${slot.border}`,
820
+ color: workflowPalette3.badge.text,
821
+ fontSize: typography2.badge.size,
822
+ fontWeight: typography2.badge.weight,
823
+ letterSpacing: typography2.badge.tracking,
824
+ padding: "1px 4px",
825
+ borderRadius: 8
826
+ },
827
+ children: b.label
828
+ },
829
+ `${b.key}-${i}`
830
+ );
831
+ }) })
832
+ ]
833
+ }
834
+ ) })
835
+ ] });
836
+ }
837
+ var RfTransitionEdge = memo2(RfTransitionEdgeImpl);
838
+
839
+ // src/components/Canvas.tsx
840
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
841
+ var nodeTypes = { stateNode: RfStateNode };
842
+ var edgeTypes = { transition: RfTransitionEdge };
843
+ function toRfNodes(graph, layout, activeWorkflow, issuesByNode, selection) {
844
+ return graph.nodes.filter((n) => n.kind === "state").filter((n) => !activeWorkflow || n.workflow === activeWorkflow).map((n) => {
845
+ const pos = layout.positions.get(n.id);
846
+ const nodeIssues = issuesByNode.get(n.id) ?? [];
847
+ const hasError = nodeIssues.some((i) => i.severity === "error");
848
+ const hasWarning = nodeIssues.some((i) => i.severity === "warning");
849
+ const selected = selection?.kind === "state" && selection.nodeId === n.id;
850
+ const size = pos ? { width: pos.width, height: pos.height } : estimateNodeSize(n.stateCode);
851
+ return {
852
+ id: n.id,
853
+ type: "stateNode",
854
+ data: { node: n, hasError, hasWarning, size },
855
+ position: pos ? { x: pos.x, y: pos.y } : { x: 0, y: 0 },
856
+ selected,
857
+ // width/height on the node object tells ReactFlow the dimensions
858
+ // before ResizeObserver fires. fitView's nodesInitialized guard
859
+ // (nodes.every(n => n.width && n.height)) requires these to be set
860
+ // or it returns false and leaves the viewport at {x:0, y:0, zoom:1}.
861
+ width: size.width,
862
+ height: size.height,
863
+ style: { width: size.width, height: size.height }
864
+ };
865
+ });
866
+ }
867
+ function toRfEdges(graph, layout, activeWorkflow, selection, orientation) {
868
+ const stateById = new Map(
869
+ graph.nodes.filter((n) => n.kind === "state").map((n) => [n.id, n])
870
+ );
871
+ const allObstacles = Array.from(layout.positions.values()).map((p) => ({
872
+ id: p.id,
873
+ x: p.x,
874
+ y: p.y,
875
+ width: p.width,
876
+ height: p.height
877
+ }));
878
+ return graph.edges.filter((e) => e.kind === "transition").filter((e) => !activeWorkflow || e.workflow === activeWorkflow).map((e) => {
879
+ const target = stateById.get(e.targetId);
880
+ const targetIsTerminal = target?.role === "terminal" || target?.role === "initial-terminal";
881
+ const selected = selection?.kind === "transition" && selection.transitionUuid === e.id;
882
+ const route = layout.edges.get(e.id);
883
+ const routePoints = route?.points;
884
+ const obstacles = allObstacles.filter(
885
+ (o) => o.id !== e.sourceId && o.id !== e.targetId
886
+ );
887
+ const isHorizontalBackEdge = !e.isSelf && orientation === "horizontal" && (layout.positions.get(e.sourceId)?.x ?? 0) > (layout.positions.get(e.targetId)?.x ?? 0);
888
+ return {
889
+ id: e.id,
890
+ source: e.sourceId,
891
+ target: e.targetId,
892
+ sourceHandle: anchorHandleId(e.sourceAnchor, "source", orientation, isHorizontalBackEdge),
893
+ targetHandle: anchorHandleId(e.targetAnchor, "target", orientation, isHorizontalBackEdge),
894
+ type: "transition",
895
+ data: {
896
+ edge: e,
897
+ targetIsTerminal: !!targetIsTerminal,
898
+ routePoints,
899
+ labelX: route?.labelX,
900
+ labelY: route?.labelY,
901
+ labelWidth: route?.labelWidth,
902
+ labelHeight: route?.labelHeight,
903
+ obstacles
904
+ },
905
+ selected
906
+ };
907
+ });
908
+ }
909
+ function anchorHandleId(anchor, role, orientation, isBackEdge = false) {
910
+ if (anchor) return anchor;
911
+ if (orientation === "horizontal") {
912
+ if (isBackEdge) return "bottom";
913
+ return role === "source" ? "right" : "left";
914
+ }
915
+ return role === "source" ? "bottom" : "top";
916
+ }
917
+ function groupIssuesByNode(graph, issues) {
918
+ const byNode = /* @__PURE__ */ new Map();
919
+ for (const ann of graph.annotations) {
920
+ const list = byNode.get(ann.targetId) ?? [];
921
+ const issue = issues.find((i) => i.code === ann.code);
922
+ if (issue) list.push(issue);
923
+ byNode.set(ann.targetId, list);
924
+ }
925
+ return byNode;
926
+ }
927
+ function CanvasInner({
928
+ graph,
929
+ issues,
930
+ activeWorkflow,
931
+ selection,
932
+ layoutOptions,
933
+ savedViewport,
934
+ onSelectionChange,
935
+ onViewportChange,
936
+ onConnect,
937
+ readOnly
938
+ }) {
939
+ const [layout, setLayout] = useState2(null);
940
+ const rf = useReactFlow();
941
+ const preset = layoutOptions?.preset ?? "configuratorReadable";
942
+ const orientation = layoutOptions?.orientation ?? "vertical";
943
+ const elkOverrides = layoutOptions?.elk;
944
+ const nodeSize = layoutOptions?.nodeSize;
945
+ const pinned = layoutOptions?.pinned;
946
+ const effectiveOpts = useMemo2(
947
+ () => ({ preset, orientation, elk: elkOverrides, nodeSize, pinned }),
948
+ // elkOverrides / nodeSize / pinned are objects; they are rarely supplied
949
+ // in practice, so a reference change there is an intentional re-layout.
950
+ // eslint-disable-next-line react-hooks/exhaustive-deps
951
+ [preset, orientation, elkOverrides, nodeSize, pinned]
952
+ );
953
+ const orientationAtLayoutRef = useRef2(orientation);
954
+ useEffect(() => {
955
+ let cancelled = false;
956
+ orientationAtLayoutRef.current = orientation;
957
+ layoutGraph(graph, effectiveOpts).then((result) => {
958
+ if (!cancelled) setLayout(result);
959
+ });
960
+ return () => {
961
+ cancelled = true;
962
+ };
963
+ }, [graph, effectiveOpts, orientation]);
964
+ const lastHandledViewportKeyRef = useRef2(null);
965
+ useEffect(() => {
966
+ if (!layout) return;
967
+ const viewportKey = `${activeWorkflow ?? "__all__"}:${orientationAtLayoutRef.current}`;
968
+ if (lastHandledViewportKeyRef.current === viewportKey) return;
969
+ const rafId = requestAnimationFrame(() => {
970
+ if (savedViewport) {
971
+ void rf.setViewport(savedViewport, { duration: 0 });
972
+ lastHandledViewportKeyRef.current = viewportKey;
973
+ } else {
974
+ const stateCount = graph.nodes.filter(
975
+ (node) => node.kind === "state" && (!activeWorkflow || node.workflow === activeWorkflow)
976
+ ).length;
977
+ const fitOptions = stateCount <= 6 ? { padding: 0.12, maxZoom: 1 } : { padding: 0.12 };
978
+ const fitted = rf.fitView(fitOptions);
979
+ if (fitted) lastHandledViewportKeyRef.current = viewportKey;
980
+ }
981
+ });
982
+ return () => cancelAnimationFrame(rafId);
983
+ }, [activeWorkflow, graph.nodes, layout, rf, savedViewport]);
984
+ const issuesByNode = useMemo2(() => groupIssuesByNode(graph, issues), [graph, issues]);
985
+ const nodes = useMemo2(
986
+ () => layout ? toRfNodes(graph, layout, activeWorkflow, issuesByNode, selection) : [],
987
+ [graph, layout, activeWorkflow, issuesByNode, selection]
988
+ );
989
+ const edges = useMemo2(
990
+ () => layout ? toRfEdges(graph, layout, activeWorkflow, selection, orientation) : [],
991
+ [graph, layout, activeWorkflow, selection, orientation]
992
+ );
993
+ const onNodeClick = (_, node) => {
994
+ const data = node.data;
995
+ onSelectionChange({
996
+ kind: "state",
997
+ workflow: data.node.workflow,
998
+ stateCode: data.node.stateCode,
999
+ nodeId: data.node.id
1000
+ });
1001
+ };
1002
+ const onEdgeClick = (_, edge) => {
1003
+ onSelectionChange({ kind: "transition", transitionUuid: edge.id });
1004
+ };
1005
+ return /* @__PURE__ */ jsxs3("div", { style: { width: "100%", height: "100%" }, "data-testid": "workflow-canvas", children: [
1006
+ /* @__PURE__ */ jsx4(ArrowMarkers, {}),
1007
+ /* @__PURE__ */ jsxs3(
1008
+ ReactFlow,
1009
+ {
1010
+ nodes,
1011
+ edges,
1012
+ nodeTypes,
1013
+ edgeTypes,
1014
+ onNodeClick,
1015
+ onEdgeClick,
1016
+ onPaneClick: () => onSelectionChange(null),
1017
+ onConnect: readOnly ? void 0 : onConnect,
1018
+ connectionMode: ConnectionMode.Loose,
1019
+ nodesDraggable: !readOnly,
1020
+ nodesConnectable: !readOnly,
1021
+ elementsSelectable: true,
1022
+ snapToGrid: true,
1023
+ snapGrid: [16, 16],
1024
+ minZoom: 0.1,
1025
+ maxZoom: 4,
1026
+ onMoveEnd: (_, viewport) => {
1027
+ if (layout) onViewportChange?.(viewport);
1028
+ },
1029
+ children: [
1030
+ /* @__PURE__ */ jsx4(Background, {}),
1031
+ /* @__PURE__ */ jsx4(Controls, { showInteractive: false }),
1032
+ /* @__PURE__ */ jsx4(MiniMap, { zoomable: true, pannable: true })
1033
+ ]
1034
+ }
1035
+ )
1036
+ ] });
1037
+ }
1038
+ function Canvas(props) {
1039
+ return /* @__PURE__ */ jsx4(ReactFlowProvider, { children: /* @__PURE__ */ jsx4(CanvasInner, { ...props }) });
1040
+ }
1041
+
1042
+ // src/components/resolveConnection.ts
1043
+ function resolveConnection(doc, connection) {
1044
+ const { source, target } = connection;
1045
+ if (!source || !target) return null;
1046
+ const sourcePtr = doc.meta.ids.states[source];
1047
+ const targetPtr = doc.meta.ids.states[target];
1048
+ if (!sourcePtr || !targetPtr) return null;
1049
+ if (sourcePtr.workflow !== targetPtr.workflow) return null;
1050
+ return {
1051
+ workflow: sourcePtr.workflow,
1052
+ fromState: sourcePtr.state,
1053
+ toState: targetPtr.state
1054
+ };
1055
+ }
1056
+
1057
+ // src/inspector/Inspector.tsx
1058
+ import { useMemo as useMemo3, useState as useState3 } from "react";
1059
+ import { serializeEditorDocument } from "@cyoda/workflow-core";
1060
+
1061
+ // src/inspector/resolve.ts
1062
+ function transitionUuidsInOrder(doc, workflow, state) {
1063
+ const uuids = [];
1064
+ for (const [uuid, ptr] of Object.entries(doc.meta.ids.transitions)) {
1065
+ if (ptr.workflow === workflow && ptr.state === state) uuids.push(uuid);
1066
+ }
1067
+ return uuids;
1068
+ }
1069
+ function processorUuidsInOrder(doc, transitionUuid) {
1070
+ const uuids = [];
1071
+ for (const [uuid, ptr] of Object.entries(doc.meta.ids.processors)) {
1072
+ if (ptr.transitionUuid === transitionUuid) uuids.push(uuid);
1073
+ }
1074
+ return uuids;
1075
+ }
1076
+ function resolveSelection(doc, selection) {
1077
+ if (!selection) return null;
1078
+ switch (selection.kind) {
1079
+ case "workflow": {
1080
+ const workflow = doc.session.workflows.find((w) => w.name === selection.workflow);
1081
+ return workflow ? { kind: "workflow", workflow } : null;
1082
+ }
1083
+ case "state": {
1084
+ const workflow = doc.session.workflows.find((w) => w.name === selection.workflow);
1085
+ if (!workflow) return null;
1086
+ const state = workflow.states[selection.stateCode];
1087
+ if (!state) return null;
1088
+ return { kind: "state", workflow, stateCode: selection.stateCode, state };
1089
+ }
1090
+ case "transition": {
1091
+ const ptr = doc.meta.ids.transitions[selection.transitionUuid];
1092
+ if (!ptr) return null;
1093
+ const workflow = doc.session.workflows.find((w) => w.name === ptr.workflow);
1094
+ if (!workflow) return null;
1095
+ const state = workflow.states[ptr.state];
1096
+ if (!state) return null;
1097
+ const orderedUuids = transitionUuidsInOrder(doc, ptr.workflow, ptr.state);
1098
+ const index = orderedUuids.indexOf(selection.transitionUuid);
1099
+ if (index < 0) return null;
1100
+ const transition = state.transitions[index];
1101
+ if (!transition) return null;
1102
+ return {
1103
+ kind: "transition",
1104
+ workflow,
1105
+ stateCode: ptr.state,
1106
+ transition,
1107
+ transitionUuid: selection.transitionUuid,
1108
+ transitionIndex: index
1109
+ };
1110
+ }
1111
+ case "processor": {
1112
+ const ptr = doc.meta.ids.processors[selection.processorUuid];
1113
+ if (!ptr) return null;
1114
+ const workflow = doc.session.workflows.find((w) => w.name === ptr.workflow);
1115
+ if (!workflow) return null;
1116
+ const state = workflow.states[ptr.state];
1117
+ if (!state) return null;
1118
+ const txUuids = transitionUuidsInOrder(doc, ptr.workflow, ptr.state);
1119
+ const txIndex = txUuids.indexOf(ptr.transitionUuid);
1120
+ if (txIndex < 0) return null;
1121
+ const transition = state.transitions[txIndex];
1122
+ if (!transition) return null;
1123
+ const procUuids = processorUuidsInOrder(doc, ptr.transitionUuid);
1124
+ const procIndex = procUuids.indexOf(selection.processorUuid);
1125
+ if (procIndex < 0) return null;
1126
+ const processor = transition.processors?.[procIndex];
1127
+ if (!processor) return null;
1128
+ return {
1129
+ kind: "processor",
1130
+ workflow,
1131
+ stateCode: ptr.state,
1132
+ transition,
1133
+ transitionUuid: ptr.transitionUuid,
1134
+ processor,
1135
+ processorUuid: selection.processorUuid,
1136
+ processorIndex: procIndex
1137
+ };
1138
+ }
1139
+ case "criterion":
1140
+ return null;
1141
+ }
1142
+ }
1143
+
1144
+ // src/inspector/fields.tsx
1145
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1146
+ function TextField({
1147
+ label,
1148
+ value,
1149
+ onCommit,
1150
+ disabled,
1151
+ placeholder,
1152
+ testId
1153
+ }) {
1154
+ return /* @__PURE__ */ jsxs4("label", { style: rowStyle, children: [
1155
+ /* @__PURE__ */ jsx5("span", { style: labelStyle, children: label }),
1156
+ /* @__PURE__ */ jsx5(
1157
+ "input",
1158
+ {
1159
+ type: "text",
1160
+ defaultValue: value,
1161
+ disabled,
1162
+ placeholder,
1163
+ "data-testid": testId,
1164
+ onBlur: (e) => {
1165
+ if (e.target.value !== value) onCommit(e.target.value);
1166
+ },
1167
+ onKeyDown: (e) => {
1168
+ if (e.key === "Enter") e.target.blur();
1169
+ },
1170
+ style: inputStyle
1171
+ }
1172
+ )
1173
+ ] });
1174
+ }
1175
+ function CheckboxField({
1176
+ label,
1177
+ checked,
1178
+ onChange,
1179
+ disabled,
1180
+ testId
1181
+ }) {
1182
+ return /* @__PURE__ */ jsxs4("label", { style: { ...rowStyle, flexDirection: "row", alignItems: "center", gap: 8 }, children: [
1183
+ /* @__PURE__ */ jsx5(
1184
+ "input",
1185
+ {
1186
+ type: "checkbox",
1187
+ checked,
1188
+ disabled,
1189
+ onChange: (e) => onChange(e.target.checked),
1190
+ "data-testid": testId
1191
+ }
1192
+ ),
1193
+ /* @__PURE__ */ jsx5("span", { style: { ...labelStyle, marginBottom: 0 }, children: label })
1194
+ ] });
1195
+ }
1196
+ function SelectField({
1197
+ label,
1198
+ value,
1199
+ options,
1200
+ onChange,
1201
+ disabled,
1202
+ testId
1203
+ }) {
1204
+ return /* @__PURE__ */ jsxs4("label", { style: rowStyle, children: [
1205
+ /* @__PURE__ */ jsx5("span", { style: labelStyle, children: label }),
1206
+ /* @__PURE__ */ jsx5(
1207
+ "select",
1208
+ {
1209
+ value,
1210
+ disabled,
1211
+ onChange: (e) => onChange(e.target.value),
1212
+ "data-testid": testId,
1213
+ style: inputStyle,
1214
+ children: options.map((o) => /* @__PURE__ */ jsx5("option", { value: o.value, children: o.label }, o.value))
1215
+ }
1216
+ )
1217
+ ] });
1218
+ }
1219
+ function FieldGroup({ title, children }) {
1220
+ return /* @__PURE__ */ jsxs4("section", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
1221
+ /* @__PURE__ */ jsx5("header", { style: { fontSize: 11, fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: "#475569" }, children: title }),
1222
+ children
1223
+ ] });
1224
+ }
1225
+ var rowStyle = {
1226
+ display: "flex",
1227
+ flexDirection: "column",
1228
+ gap: 4
1229
+ };
1230
+ var labelStyle = {
1231
+ fontSize: 12,
1232
+ color: "#475569",
1233
+ marginBottom: 2
1234
+ };
1235
+ var inputStyle = {
1236
+ padding: "6px 8px",
1237
+ fontSize: 13,
1238
+ border: "1px solid #CBD5E1",
1239
+ borderRadius: 4,
1240
+ background: "white"
1241
+ };
1242
+
1243
+ // src/inspector/WorkflowForm.tsx
1244
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1245
+ function WorkflowForm({
1246
+ workflow,
1247
+ disabled,
1248
+ onDispatch
1249
+ }) {
1250
+ const messages = useMessages();
1251
+ return /* @__PURE__ */ jsxs5(FieldGroup, { title: messages.inspector.properties, children: [
1252
+ /* @__PURE__ */ jsx6(
1253
+ TextField,
1254
+ {
1255
+ label: messages.inspector.name,
1256
+ value: workflow.name,
1257
+ disabled,
1258
+ onCommit: (next) => next !== workflow.name && onDispatch({ op: "renameWorkflow", from: workflow.name, to: next }),
1259
+ testId: "inspector-workflow-name"
1260
+ }
1261
+ ),
1262
+ /* @__PURE__ */ jsx6(
1263
+ TextField,
1264
+ {
1265
+ label: messages.inspector.version,
1266
+ value: workflow.version,
1267
+ disabled,
1268
+ onCommit: (next) => onDispatch({
1269
+ op: "updateWorkflowMeta",
1270
+ workflow: workflow.name,
1271
+ updates: { version: next }
1272
+ }),
1273
+ testId: "inspector-workflow-version"
1274
+ }
1275
+ ),
1276
+ /* @__PURE__ */ jsx6(
1277
+ TextField,
1278
+ {
1279
+ label: messages.inspector.description,
1280
+ value: workflow.desc ?? "",
1281
+ disabled,
1282
+ onCommit: (next) => onDispatch({
1283
+ op: "updateWorkflowMeta",
1284
+ workflow: workflow.name,
1285
+ updates: { desc: next === "" ? void 0 : next }
1286
+ }),
1287
+ testId: "inspector-workflow-desc"
1288
+ }
1289
+ ),
1290
+ /* @__PURE__ */ jsx6(
1291
+ CheckboxField,
1292
+ {
1293
+ label: messages.inspector.active,
1294
+ checked: workflow.active,
1295
+ disabled,
1296
+ onChange: (next) => onDispatch({
1297
+ op: "updateWorkflowMeta",
1298
+ workflow: workflow.name,
1299
+ updates: { active: next }
1300
+ }),
1301
+ testId: "inspector-workflow-active"
1302
+ }
1303
+ ),
1304
+ /* @__PURE__ */ jsx6(
1305
+ TextField,
1306
+ {
1307
+ label: messages.inspector.initialState,
1308
+ value: workflow.initialState,
1309
+ disabled,
1310
+ onCommit: (next) => onDispatch({
1311
+ op: "setInitialState",
1312
+ workflow: workflow.name,
1313
+ stateCode: next
1314
+ }),
1315
+ testId: "inspector-workflow-initial"
1316
+ }
1317
+ )
1318
+ ] });
1319
+ }
1320
+
1321
+ // src/inspector/StateForm.tsx
1322
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1323
+ function StateForm({
1324
+ workflow,
1325
+ stateCode,
1326
+ state,
1327
+ disabled,
1328
+ onDispatch,
1329
+ onRequestDelete
1330
+ }) {
1331
+ const messages = useMessages();
1332
+ const transitionCount = state.transitions.length;
1333
+ return /* @__PURE__ */ jsxs6(FieldGroup, { title: messages.inspector.properties, children: [
1334
+ /* @__PURE__ */ jsx7(
1335
+ TextField,
1336
+ {
1337
+ label: messages.inspector.name,
1338
+ value: stateCode,
1339
+ disabled,
1340
+ onCommit: (next) => next !== stateCode && onDispatch({
1341
+ op: "renameState",
1342
+ workflow: workflow.name,
1343
+ from: stateCode,
1344
+ to: next
1345
+ }),
1346
+ testId: "inspector-state-name"
1347
+ }
1348
+ ),
1349
+ /* @__PURE__ */ jsxs6("div", { style: { fontSize: 12, color: "#475569" }, children: [
1350
+ transitionCount,
1351
+ " outgoing transition",
1352
+ transitionCount === 1 ? "" : "s",
1353
+ "."
1354
+ ] }),
1355
+ /* @__PURE__ */ jsx7(
1356
+ "button",
1357
+ {
1358
+ type: "button",
1359
+ onClick: onRequestDelete,
1360
+ disabled,
1361
+ "data-testid": "inspector-state-delete",
1362
+ style: dangerBtn,
1363
+ children: "Delete state\u2026"
1364
+ }
1365
+ )
1366
+ ] });
1367
+ }
1368
+ var dangerBtn = {
1369
+ alignSelf: "flex-start",
1370
+ padding: "6px 10px",
1371
+ background: "#FEF2F2",
1372
+ border: "1px solid #FCA5A5",
1373
+ color: "#B91C1C",
1374
+ borderRadius: 4,
1375
+ fontSize: 13,
1376
+ cursor: "pointer"
1377
+ };
1378
+
1379
+ // src/inspector/TransitionForm.tsx
1380
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1381
+ function TransitionForm({
1382
+ workflow,
1383
+ stateCode,
1384
+ transition,
1385
+ transitionUuid,
1386
+ transitionIndex,
1387
+ anchors,
1388
+ disabled,
1389
+ onDispatch,
1390
+ onSelectProcessor
1391
+ }) {
1392
+ const messages = useMessages();
1393
+ const update = (updates) => onDispatch({ op: "updateTransition", transitionUuid, updates });
1394
+ const removeTransition = () => onDispatch({ op: "removeTransition", transitionUuid });
1395
+ const setAnchor = (role, next) => {
1396
+ const current = anchors ?? {};
1397
+ const updated = { ...current };
1398
+ if (next === "") delete updated[role];
1399
+ else updated[role] = next;
1400
+ const isEmpty = updated.source === void 0 && updated.target === void 0;
1401
+ onDispatch({
1402
+ op: "setEdgeAnchors",
1403
+ transitionUuid,
1404
+ anchors: isEmpty ? null : updated
1405
+ });
1406
+ };
1407
+ const reorder = (direction) => {
1408
+ const toIndex = transitionIndex + direction;
1409
+ if (toIndex < 0) return;
1410
+ onDispatch({
1411
+ op: "reorderTransition",
1412
+ workflow: workflow.name,
1413
+ fromState: stateCode,
1414
+ transitionUuid,
1415
+ toIndex
1416
+ });
1417
+ };
1418
+ return /* @__PURE__ */ jsxs7(FieldGroup, { title: messages.inspector.properties, children: [
1419
+ /* @__PURE__ */ jsx8(
1420
+ TextField,
1421
+ {
1422
+ label: messages.inspector.name,
1423
+ value: transition.name,
1424
+ disabled,
1425
+ onCommit: (next) => update({ name: next }),
1426
+ testId: "inspector-transition-name"
1427
+ }
1428
+ ),
1429
+ /* @__PURE__ */ jsx8(
1430
+ TextField,
1431
+ {
1432
+ label: "Target state",
1433
+ value: transition.next,
1434
+ disabled,
1435
+ onCommit: (next) => update({ next }),
1436
+ testId: "inspector-transition-next"
1437
+ }
1438
+ ),
1439
+ /* @__PURE__ */ jsx8(
1440
+ CheckboxField,
1441
+ {
1442
+ label: messages.inspector.manual,
1443
+ checked: transition.manual,
1444
+ disabled,
1445
+ onChange: (next) => update({ manual: next }),
1446
+ testId: "inspector-transition-manual"
1447
+ }
1448
+ ),
1449
+ /* @__PURE__ */ jsx8(
1450
+ CheckboxField,
1451
+ {
1452
+ label: messages.inspector.disabled,
1453
+ checked: transition.disabled,
1454
+ disabled,
1455
+ onChange: (next) => update({ disabled: next }),
1456
+ testId: "inspector-transition-disabled"
1457
+ }
1458
+ ),
1459
+ /* @__PURE__ */ jsx8(
1460
+ AnchorSelect,
1461
+ {
1462
+ label: messages.inspector.sourceAnchor,
1463
+ value: anchors?.source,
1464
+ disabled,
1465
+ messages,
1466
+ onChange: (next) => setAnchor("source", next),
1467
+ testId: "inspector-transition-source-anchor"
1468
+ }
1469
+ ),
1470
+ /* @__PURE__ */ jsx8(
1471
+ AnchorSelect,
1472
+ {
1473
+ label: messages.inspector.targetAnchor,
1474
+ value: anchors?.target,
1475
+ disabled,
1476
+ messages,
1477
+ onChange: (next) => setAnchor("target", next),
1478
+ testId: "inspector-transition-target-anchor"
1479
+ }
1480
+ ),
1481
+ /* @__PURE__ */ jsxs7("div", { style: { display: "flex", gap: 6 }, children: [
1482
+ /* @__PURE__ */ jsx8("button", { type: "button", disabled, onClick: () => reorder(-1), style: ghostBtn, children: messages.inspector.moveUp }),
1483
+ /* @__PURE__ */ jsx8("button", { type: "button", disabled, onClick: () => reorder(1), style: ghostBtn, children: messages.inspector.moveDown }),
1484
+ /* @__PURE__ */ jsx8(
1485
+ "button",
1486
+ {
1487
+ type: "button",
1488
+ disabled,
1489
+ onClick: removeTransition,
1490
+ style: dangerBtn2,
1491
+ "data-testid": "inspector-transition-delete",
1492
+ children: "Delete"
1493
+ }
1494
+ )
1495
+ ] }),
1496
+ /* @__PURE__ */ jsxs7("section", { style: { marginTop: 12, display: "flex", flexDirection: "column", gap: 6 }, children: [
1497
+ /* @__PURE__ */ jsx8("header", { style: { fontSize: 11, fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: "#475569" }, children: messages.inspector.processors }),
1498
+ (transition.processors ?? []).map((p, i) => /* @__PURE__ */ jsx8(
1499
+ "button",
1500
+ {
1501
+ type: "button",
1502
+ onClick: () => {
1503
+ onSelectProcessor(`__ordinal:${transitionUuid}:${i}`);
1504
+ },
1505
+ style: processorBtn,
1506
+ "data-testid": `inspector-processor-${i}`,
1507
+ children: p.name
1508
+ },
1509
+ `${p.name}-${i}`
1510
+ )),
1511
+ /* @__PURE__ */ jsx8(
1512
+ "button",
1513
+ {
1514
+ type: "button",
1515
+ disabled,
1516
+ onClick: () => onDispatch({
1517
+ op: "addProcessor",
1518
+ transitionUuid,
1519
+ processor: {
1520
+ type: "externalized",
1521
+ name: "newProcessor",
1522
+ executionMode: "ASYNC_NEW_TX",
1523
+ config: {
1524
+ attachEntity: false,
1525
+ responseTimeoutMs: 5e3
1526
+ }
1527
+ }
1528
+ }),
1529
+ style: ghostBtn,
1530
+ "data-testid": "inspector-add-processor",
1531
+ children: messages.inspector.addProcessor
1532
+ }
1533
+ )
1534
+ ] })
1535
+ ] });
1536
+ }
1537
+ var ghostBtn = {
1538
+ padding: "4px 8px",
1539
+ background: "white",
1540
+ border: "1px solid #CBD5E1",
1541
+ borderRadius: 4,
1542
+ fontSize: 12,
1543
+ cursor: "pointer"
1544
+ };
1545
+ var dangerBtn2 = {
1546
+ ...ghostBtn,
1547
+ background: "#FEF2F2",
1548
+ borderColor: "#FCA5A5",
1549
+ color: "#B91C1C"
1550
+ };
1551
+ var processorBtn = {
1552
+ ...ghostBtn,
1553
+ textAlign: "left",
1554
+ background: "#F8FAFC"
1555
+ };
1556
+ function AnchorSelect({
1557
+ label,
1558
+ value,
1559
+ disabled,
1560
+ messages,
1561
+ onChange,
1562
+ testId
1563
+ }) {
1564
+ return /* @__PURE__ */ jsxs7(
1565
+ "label",
1566
+ {
1567
+ style: {
1568
+ display: "flex",
1569
+ flexDirection: "column",
1570
+ gap: 4,
1571
+ fontSize: 12,
1572
+ color: "#334155"
1573
+ },
1574
+ children: [
1575
+ /* @__PURE__ */ jsx8("span", { style: { fontWeight: 500 }, children: label }),
1576
+ /* @__PURE__ */ jsxs7(
1577
+ "select",
1578
+ {
1579
+ value: value ?? "",
1580
+ disabled,
1581
+ onChange: (event) => onChange(event.target.value === "" ? "" : event.target.value),
1582
+ "data-testid": testId,
1583
+ style: {
1584
+ padding: "4px 6px",
1585
+ border: "1px solid #CBD5E1",
1586
+ borderRadius: 4,
1587
+ background: "white",
1588
+ fontSize: 12
1589
+ },
1590
+ children: [
1591
+ /* @__PURE__ */ jsx8("option", { value: "", children: messages.inspector.anchorDefault }),
1592
+ /* @__PURE__ */ jsx8("option", { value: "top", children: messages.inspector.anchorTop }),
1593
+ /* @__PURE__ */ jsx8("option", { value: "right", children: messages.inspector.anchorRight }),
1594
+ /* @__PURE__ */ jsx8("option", { value: "bottom", children: messages.inspector.anchorBottom }),
1595
+ /* @__PURE__ */ jsx8("option", { value: "left", children: messages.inspector.anchorLeft })
1596
+ ]
1597
+ }
1598
+ )
1599
+ ]
1600
+ }
1601
+ );
1602
+ }
1603
+
1604
+ // src/inspector/ProcessorForm.tsx
1605
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1606
+ var EXECUTION_MODES = [
1607
+ { value: "ASYNC_NEW_TX", label: "ASYNC_NEW_TX" },
1608
+ { value: "ASYNC_SAME_TX", label: "ASYNC_SAME_TX" },
1609
+ { value: "SYNC", label: "SYNC" }
1610
+ ];
1611
+ function ProcessorForm({
1612
+ processor,
1613
+ processorUuid,
1614
+ processorIndex,
1615
+ transitionUuid,
1616
+ disabled,
1617
+ onDispatch
1618
+ }) {
1619
+ const messages = useMessages();
1620
+ const update = (updates) => onDispatch({ op: "updateProcessor", processorUuid, updates });
1621
+ const isExternalized = processor.type === "externalized";
1622
+ return /* @__PURE__ */ jsxs8(FieldGroup, { title: messages.inspector.properties, children: [
1623
+ /* @__PURE__ */ jsx9(
1624
+ TextField,
1625
+ {
1626
+ label: messages.inspector.name,
1627
+ value: processor.name,
1628
+ disabled,
1629
+ onCommit: (next) => update({ name: next }),
1630
+ testId: "inspector-processor-name"
1631
+ }
1632
+ ),
1633
+ isExternalized && /* @__PURE__ */ jsxs8(Fragment3, { children: [
1634
+ /* @__PURE__ */ jsx9(
1635
+ SelectField,
1636
+ {
1637
+ label: messages.inspector.executionMode,
1638
+ value: processor.executionMode ?? "ASYNC_NEW_TX",
1639
+ options: EXECUTION_MODES,
1640
+ disabled,
1641
+ onChange: (next) => update({ executionMode: next }),
1642
+ testId: "inspector-processor-execmode"
1643
+ }
1644
+ ),
1645
+ /* @__PURE__ */ jsx9(
1646
+ CheckboxField,
1647
+ {
1648
+ label: "Attach entity",
1649
+ checked: processor.config?.attachEntity ?? false,
1650
+ disabled,
1651
+ onChange: (next) => update({
1652
+ config: { ...processor.config ?? {}, attachEntity: next }
1653
+ }),
1654
+ testId: "inspector-processor-attachentity"
1655
+ }
1656
+ ),
1657
+ /* @__PURE__ */ jsx9(
1658
+ TextField,
1659
+ {
1660
+ label: "Response timeout (ms)",
1661
+ value: String(processor.config?.responseTimeoutMs ?? 5e3),
1662
+ disabled,
1663
+ onCommit: (next) => {
1664
+ const parsed = Number.parseInt(next, 10);
1665
+ if (!Number.isFinite(parsed)) return;
1666
+ update({
1667
+ config: { ...processor.config ?? {}, responseTimeoutMs: parsed }
1668
+ });
1669
+ },
1670
+ testId: "inspector-processor-timeout"
1671
+ }
1672
+ )
1673
+ ] }),
1674
+ /* @__PURE__ */ jsxs8("div", { style: { display: "flex", gap: 6 }, children: [
1675
+ /* @__PURE__ */ jsx9(
1676
+ "button",
1677
+ {
1678
+ type: "button",
1679
+ disabled: disabled || processorIndex === 0,
1680
+ onClick: () => onDispatch({
1681
+ op: "reorderProcessor",
1682
+ transitionUuid,
1683
+ processorUuid,
1684
+ toIndex: processorIndex - 1
1685
+ }),
1686
+ style: ghostBtn2,
1687
+ children: messages.inspector.moveUp
1688
+ }
1689
+ ),
1690
+ /* @__PURE__ */ jsx9(
1691
+ "button",
1692
+ {
1693
+ type: "button",
1694
+ disabled,
1695
+ onClick: () => onDispatch({
1696
+ op: "reorderProcessor",
1697
+ transitionUuid,
1698
+ processorUuid,
1699
+ toIndex: processorIndex + 1
1700
+ }),
1701
+ style: ghostBtn2,
1702
+ children: messages.inspector.moveDown
1703
+ }
1704
+ ),
1705
+ /* @__PURE__ */ jsx9(
1706
+ "button",
1707
+ {
1708
+ type: "button",
1709
+ disabled,
1710
+ onClick: () => onDispatch({ op: "removeProcessor", processorUuid }),
1711
+ style: dangerBtn3,
1712
+ "data-testid": "inspector-processor-delete",
1713
+ children: messages.inspector.removeProcessor
1714
+ }
1715
+ )
1716
+ ] })
1717
+ ] });
1718
+ }
1719
+ var ghostBtn2 = {
1720
+ padding: "4px 8px",
1721
+ background: "white",
1722
+ border: "1px solid #CBD5E1",
1723
+ borderRadius: 4,
1724
+ fontSize: 12,
1725
+ cursor: "pointer"
1726
+ };
1727
+ var dangerBtn3 = {
1728
+ ...ghostBtn2,
1729
+ background: "#FEF2F2",
1730
+ borderColor: "#FCA5A5",
1731
+ color: "#B91C1C"
1732
+ };
1733
+
1734
+ // src/inspector/Inspector.tsx
1735
+ import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1736
+ function issueKeyForSelection(selection) {
1737
+ if (!selection) return null;
1738
+ switch (selection.kind) {
1739
+ case "workflow":
1740
+ return selection.workflow;
1741
+ case "state":
1742
+ return selection.nodeId;
1743
+ case "transition":
1744
+ return selection.transitionUuid;
1745
+ case "processor":
1746
+ return selection.processorUuid;
1747
+ case "criterion":
1748
+ return selection.hostId;
1749
+ }
1750
+ }
1751
+ function Inspector({
1752
+ document: doc,
1753
+ selection,
1754
+ issues,
1755
+ readOnly,
1756
+ onDispatch,
1757
+ onSelectionChange,
1758
+ onRequestDeleteState
1759
+ }) {
1760
+ const messages = useMessages();
1761
+ const [tab, setTab] = useState3("properties");
1762
+ const resolved = useMemo3(() => resolveSelection(doc, selection), [doc, selection]);
1763
+ const selectionIssueKey = issueKeyForSelection(selection);
1764
+ const selectionIssues = useMemo3(() => {
1765
+ if (!selectionIssueKey) return [];
1766
+ return issues.filter((i) => i.targetId === selectionIssueKey);
1767
+ }, [issues, selectionIssueKey]);
1768
+ const breadcrumb = renderBreadcrumb(resolved);
1769
+ return /* @__PURE__ */ jsxs9(
1770
+ "aside",
1771
+ {
1772
+ style: {
1773
+ height: "100%",
1774
+ display: "flex",
1775
+ flexDirection: "column",
1776
+ background: "#F8FAFC",
1777
+ borderLeft: "1px solid #E2E8F0",
1778
+ minWidth: 280
1779
+ },
1780
+ "data-testid": "inspector",
1781
+ children: [
1782
+ /* @__PURE__ */ jsx10(
1783
+ "header",
1784
+ {
1785
+ style: {
1786
+ padding: "10px 12px",
1787
+ borderBottom: "1px solid #E2E8F0",
1788
+ fontSize: 12,
1789
+ color: "#475569"
1790
+ },
1791
+ children: breadcrumb
1792
+ }
1793
+ ),
1794
+ /* @__PURE__ */ jsxs9("div", { style: { display: "flex", borderBottom: "1px solid #E2E8F0" }, children: [
1795
+ /* @__PURE__ */ jsx10(TabButton, { active: tab === "properties", onClick: () => setTab("properties"), children: messages.inspector.properties }),
1796
+ /* @__PURE__ */ jsx10(TabButton, { active: tab === "json", onClick: () => setTab("json"), children: messages.inspector.json })
1797
+ ] }),
1798
+ /* @__PURE__ */ jsxs9("div", { style: { padding: 12, overflowY: "auto", flex: 1, display: "flex", flexDirection: "column", gap: 16 }, children: [
1799
+ tab === "properties" && /* @__PURE__ */ jsxs9(Fragment4, { children: [
1800
+ !resolved && /* @__PURE__ */ jsx10(EmptyState, { message: messages.inspector.empty }),
1801
+ resolved?.kind === "workflow" && /* @__PURE__ */ jsx10(WorkflowForm, { workflow: resolved.workflow, disabled: readOnly, onDispatch }),
1802
+ resolved?.kind === "state" && /* @__PURE__ */ jsx10(
1803
+ StateForm,
1804
+ {
1805
+ workflow: resolved.workflow,
1806
+ stateCode: resolved.stateCode,
1807
+ state: resolved.state,
1808
+ disabled: readOnly,
1809
+ onDispatch,
1810
+ onRequestDelete: () => onRequestDeleteState(resolved.workflow.name, resolved.stateCode)
1811
+ }
1812
+ ),
1813
+ resolved?.kind === "transition" && /* @__PURE__ */ jsx10(
1814
+ TransitionForm,
1815
+ {
1816
+ workflow: resolved.workflow,
1817
+ stateCode: resolved.stateCode,
1818
+ transition: resolved.transition,
1819
+ transitionUuid: resolved.transitionUuid,
1820
+ transitionIndex: resolved.transitionIndex,
1821
+ anchors: doc.meta.workflowUi[resolved.workflow.name]?.edgeAnchors?.[resolved.transitionUuid],
1822
+ disabled: readOnly,
1823
+ onDispatch,
1824
+ onSelectProcessor: (ordinalKey) => {
1825
+ const [, transitionUuid, indexStr] = ordinalKey.split(":");
1826
+ if (!transitionUuid || !indexStr) return;
1827
+ const procUuids = processorUuidsInOrder(doc, transitionUuid);
1828
+ const uuid = procUuids[Number.parseInt(indexStr, 10)];
1829
+ if (uuid) onSelectionChange({ kind: "processor", processorUuid: uuid });
1830
+ }
1831
+ }
1832
+ ),
1833
+ resolved?.kind === "processor" && /* @__PURE__ */ jsx10(
1834
+ ProcessorForm,
1835
+ {
1836
+ processor: resolved.processor,
1837
+ processorUuid: resolved.processorUuid,
1838
+ processorIndex: resolved.processorIndex,
1839
+ transitionUuid: resolved.transitionUuid,
1840
+ disabled: readOnly,
1841
+ onDispatch
1842
+ }
1843
+ )
1844
+ ] }),
1845
+ tab === "json" && /* @__PURE__ */ jsx10(JsonPreview, { document: doc, resolved }),
1846
+ selectionIssues.length > 0 && /* @__PURE__ */ jsx10(IssuesList, { issues: selectionIssues, title: messages.inspector.issues })
1847
+ ] })
1848
+ ]
1849
+ }
1850
+ );
1851
+ }
1852
+ function renderBreadcrumb(resolved) {
1853
+ if (!resolved) return "";
1854
+ if (resolved.kind === "workflow") return resolved.workflow.name;
1855
+ if (resolved.kind === "state")
1856
+ return `${resolved.workflow.name} \u203A ${resolved.stateCode}`;
1857
+ if (resolved.kind === "transition")
1858
+ return `${resolved.workflow.name} \u203A ${resolved.stateCode} \u203A ${resolved.transition.name}`;
1859
+ if (resolved.kind === "processor")
1860
+ return `${resolved.workflow.name} \u203A ${resolved.stateCode} \u203A ${resolved.transition.name} \u203A ${resolved.processor.name}`;
1861
+ return "";
1862
+ }
1863
+ function TabButton({
1864
+ active,
1865
+ onClick,
1866
+ children
1867
+ }) {
1868
+ return /* @__PURE__ */ jsx10(
1869
+ "button",
1870
+ {
1871
+ type: "button",
1872
+ onClick,
1873
+ style: {
1874
+ flex: 1,
1875
+ padding: "8px 12px",
1876
+ background: active ? "white" : "transparent",
1877
+ border: "none",
1878
+ borderBottom: active ? "2px solid #0F172A" : "2px solid transparent",
1879
+ fontSize: 13,
1880
+ fontWeight: active ? 600 : 400,
1881
+ cursor: "pointer"
1882
+ },
1883
+ children
1884
+ }
1885
+ );
1886
+ }
1887
+ function EmptyState({ message }) {
1888
+ return /* @__PURE__ */ jsx10("p", { style: { color: "#64748B", fontSize: 13 }, children: message });
1889
+ }
1890
+ function IssuesList({
1891
+ issues,
1892
+ title
1893
+ }) {
1894
+ return /* @__PURE__ */ jsxs9("section", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
1895
+ /* @__PURE__ */ jsx10("header", { style: { fontSize: 11, fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: "#475569" }, children: title }),
1896
+ issues.map((issue, i) => /* @__PURE__ */ jsxs9(
1897
+ "div",
1898
+ {
1899
+ style: {
1900
+ padding: 8,
1901
+ border: `1px solid ${severityBorder(issue.severity)}`,
1902
+ background: severityBackground(issue.severity),
1903
+ borderRadius: 4,
1904
+ fontSize: 12
1905
+ },
1906
+ children: [
1907
+ /* @__PURE__ */ jsx10("strong", { children: issue.code }),
1908
+ /* @__PURE__ */ jsx10("div", { children: issue.message })
1909
+ ]
1910
+ },
1911
+ `${issue.code}-${i}`
1912
+ ))
1913
+ ] });
1914
+ }
1915
+ function severityBorder(severity) {
1916
+ if (severity === "error") return "#FCA5A5";
1917
+ if (severity === "warning") return "#FCD34D";
1918
+ return "#93C5FD";
1919
+ }
1920
+ function severityBackground(severity) {
1921
+ if (severity === "error") return "#FEF2F2";
1922
+ if (severity === "warning") return "#FFFBEB";
1923
+ return "#EFF6FF";
1924
+ }
1925
+ function JsonPreview({
1926
+ document: doc,
1927
+ resolved
1928
+ }) {
1929
+ const json = useMemo3(() => {
1930
+ if (!resolved) return serializeEditorDocument(doc);
1931
+ if (resolved.kind === "workflow") return JSON.stringify(resolved.workflow, null, 2);
1932
+ if (resolved.kind === "state") return JSON.stringify(resolved.state, null, 2);
1933
+ if (resolved.kind === "transition") return JSON.stringify(resolved.transition, null, 2);
1934
+ if (resolved.kind === "processor") return JSON.stringify(resolved.processor, null, 2);
1935
+ return "";
1936
+ }, [doc, resolved]);
1937
+ return /* @__PURE__ */ jsx10(
1938
+ "pre",
1939
+ {
1940
+ style: {
1941
+ fontFamily: "ui-monospace, 'SF Mono', Menlo, monospace",
1942
+ fontSize: 12,
1943
+ margin: 0,
1944
+ padding: 8,
1945
+ background: "white",
1946
+ border: "1px solid #E2E8F0",
1947
+ borderRadius: 4,
1948
+ maxHeight: 480,
1949
+ overflow: "auto"
1950
+ },
1951
+ "data-testid": "inspector-json",
1952
+ children: json
1953
+ }
1954
+ );
1955
+ }
1956
+
1957
+ // src/toolbar/Toolbar.tsx
1958
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1959
+ function Toolbar({
1960
+ derived,
1961
+ canUndo,
1962
+ canRedo,
1963
+ readOnly,
1964
+ onUndo,
1965
+ onRedo,
1966
+ onSave
1967
+ }) {
1968
+ const messages = useMessages();
1969
+ return /* @__PURE__ */ jsxs10(
1970
+ "header",
1971
+ {
1972
+ style: {
1973
+ padding: "8px 12px",
1974
+ borderBottom: "1px solid #E2E8F0",
1975
+ display: "flex",
1976
+ alignItems: "center",
1977
+ gap: 12,
1978
+ background: "white"
1979
+ },
1980
+ "data-testid": "toolbar",
1981
+ children: [
1982
+ /* @__PURE__ */ jsx11(
1983
+ "button",
1984
+ {
1985
+ type: "button",
1986
+ onClick: onUndo,
1987
+ disabled: !canUndo || readOnly,
1988
+ style: btnStyle,
1989
+ "data-testid": "toolbar-undo",
1990
+ children: messages.toolbar.undo
1991
+ }
1992
+ ),
1993
+ /* @__PURE__ */ jsx11(
1994
+ "button",
1995
+ {
1996
+ type: "button",
1997
+ onClick: onRedo,
1998
+ disabled: !canRedo || readOnly,
1999
+ style: btnStyle,
2000
+ "data-testid": "toolbar-redo",
2001
+ children: messages.toolbar.redo
2002
+ }
2003
+ ),
2004
+ /* @__PURE__ */ jsx11("div", { style: { flex: 1 } }),
2005
+ /* @__PURE__ */ jsx11(
2006
+ ValidationPill,
2007
+ {
2008
+ count: derived.errorCount,
2009
+ label: messages.toolbar.errors,
2010
+ background: "#FEF2F2",
2011
+ borderColor: "#FCA5A5",
2012
+ color: "#B91C1C",
2013
+ testId: "toolbar-errors"
2014
+ }
2015
+ ),
2016
+ /* @__PURE__ */ jsx11(
2017
+ ValidationPill,
2018
+ {
2019
+ count: derived.warningCount,
2020
+ label: messages.toolbar.warnings,
2021
+ background: "#FFFBEB",
2022
+ borderColor: "#FCD34D",
2023
+ color: "#B45309",
2024
+ testId: "toolbar-warnings"
2025
+ }
2026
+ ),
2027
+ /* @__PURE__ */ jsx11(
2028
+ ValidationPill,
2029
+ {
2030
+ count: derived.infoCount,
2031
+ label: messages.toolbar.infos,
2032
+ background: "#EFF6FF",
2033
+ borderColor: "#93C5FD",
2034
+ color: "#1D4ED8",
2035
+ testId: "toolbar-infos"
2036
+ }
2037
+ ),
2038
+ onSave && /* @__PURE__ */ jsx11(
2039
+ "button",
2040
+ {
2041
+ type: "button",
2042
+ onClick: onSave,
2043
+ disabled: readOnly || derived.errorCount > 0,
2044
+ style: { ...btnStyle, background: "#0F172A", color: "white", borderColor: "#0F172A" },
2045
+ "data-testid": "toolbar-save",
2046
+ children: messages.toolbar.save
2047
+ }
2048
+ )
2049
+ ]
2050
+ }
2051
+ );
2052
+ }
2053
+ function ValidationPill({
2054
+ count,
2055
+ label,
2056
+ background,
2057
+ borderColor,
2058
+ color,
2059
+ testId
2060
+ }) {
2061
+ return /* @__PURE__ */ jsxs10(
2062
+ "span",
2063
+ {
2064
+ role: "status",
2065
+ "aria-live": "polite",
2066
+ "aria-label": `${count} ${label}`,
2067
+ style: {
2068
+ padding: "3px 8px",
2069
+ background,
2070
+ border: `1px solid ${borderColor}`,
2071
+ color,
2072
+ borderRadius: 999,
2073
+ fontSize: 12,
2074
+ fontWeight: 600
2075
+ },
2076
+ "data-testid": testId,
2077
+ children: [
2078
+ count,
2079
+ " ",
2080
+ label
2081
+ ]
2082
+ }
2083
+ );
2084
+ }
2085
+ var btnStyle = {
2086
+ padding: "4px 10px",
2087
+ background: "white",
2088
+ border: "1px solid #CBD5E1",
2089
+ borderRadius: 4,
2090
+ fontSize: 13,
2091
+ cursor: "pointer"
2092
+ };
2093
+
2094
+ // src/toolbar/WorkflowTabs.tsx
2095
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
2096
+ function WorkflowTabs({
2097
+ workflows,
2098
+ activeWorkflow,
2099
+ onSelect,
2100
+ onAdd,
2101
+ onClose,
2102
+ readOnly
2103
+ }) {
2104
+ const messages = useMessages();
2105
+ return /* @__PURE__ */ jsxs11(
2106
+ "nav",
2107
+ {
2108
+ style: {
2109
+ display: "flex",
2110
+ alignItems: "center",
2111
+ gap: 4,
2112
+ padding: "6px 12px",
2113
+ borderBottom: "1px solid #E2E8F0",
2114
+ background: "#F8FAFC",
2115
+ overflowX: "auto"
2116
+ },
2117
+ "data-testid": "workflow-tabs",
2118
+ children: [
2119
+ workflows.map((w) => {
2120
+ const active = w.name === activeWorkflow;
2121
+ return /* @__PURE__ */ jsxs11(
2122
+ "div",
2123
+ {
2124
+ style: {
2125
+ display: "flex",
2126
+ alignItems: "center",
2127
+ borderRadius: 4,
2128
+ border: `1px solid ${active ? "#0F172A" : "#CBD5E1"}`,
2129
+ background: active ? "white" : "transparent"
2130
+ },
2131
+ children: [
2132
+ /* @__PURE__ */ jsx12(
2133
+ "button",
2134
+ {
2135
+ type: "button",
2136
+ onClick: () => onSelect(w.name),
2137
+ style: {
2138
+ padding: "4px 10px",
2139
+ background: "transparent",
2140
+ border: "none",
2141
+ fontSize: 13,
2142
+ fontWeight: active ? 600 : 400,
2143
+ cursor: "pointer"
2144
+ },
2145
+ "data-testid": `tab-${w.name}`,
2146
+ children: w.name || messages.tabs.untitled
2147
+ }
2148
+ ),
2149
+ onClose && !readOnly && workflows.length > 1 && /* @__PURE__ */ jsx12(
2150
+ "button",
2151
+ {
2152
+ type: "button",
2153
+ onClick: () => onClose(w.name),
2154
+ style: {
2155
+ padding: "0 8px",
2156
+ background: "transparent",
2157
+ border: "none",
2158
+ color: "#64748B",
2159
+ cursor: "pointer",
2160
+ fontSize: 14
2161
+ },
2162
+ "aria-label": messages.tabs.closeTab,
2163
+ "data-testid": `tab-close-${w.name}`,
2164
+ children: "\xD7"
2165
+ }
2166
+ )
2167
+ ]
2168
+ },
2169
+ w.name
2170
+ );
2171
+ }),
2172
+ onAdd && !readOnly && /* @__PURE__ */ jsxs11(
2173
+ "button",
2174
+ {
2175
+ type: "button",
2176
+ onClick: onAdd,
2177
+ style: {
2178
+ padding: "4px 8px",
2179
+ background: "white",
2180
+ border: "1px solid #CBD5E1",
2181
+ borderRadius: 4,
2182
+ fontSize: 12,
2183
+ cursor: "pointer"
2184
+ },
2185
+ "data-testid": "tab-add",
2186
+ children: [
2187
+ "+ ",
2188
+ messages.toolbar.addWorkflow
2189
+ ]
2190
+ }
2191
+ )
2192
+ ]
2193
+ }
2194
+ );
2195
+ }
2196
+
2197
+ // src/modals/DeleteStateModal.tsx
2198
+ import { useEffect as useEffect2, useMemo as useMemo4, useRef as useRef3 } from "react";
2199
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
2200
+ function countAffected(doc, workflow, stateCode) {
2201
+ let outgoing = 0;
2202
+ let incoming = 0;
2203
+ const wf = doc.session.workflows.find((w) => w.name === workflow);
2204
+ if (!wf) return { outgoing, incoming };
2205
+ for (const [code, state] of Object.entries(wf.states)) {
2206
+ for (const t of state.transitions) {
2207
+ if (code === stateCode) outgoing++;
2208
+ if (t.next === stateCode && code !== stateCode) incoming++;
2209
+ }
2210
+ }
2211
+ return { outgoing, incoming };
2212
+ }
2213
+ function DeleteStateModal({
2214
+ document: doc,
2215
+ workflow,
2216
+ stateCode,
2217
+ onConfirm,
2218
+ onCancel
2219
+ }) {
2220
+ const messages = useMessages();
2221
+ const counts = useMemo4(
2222
+ () => countAffected(doc, workflow, stateCode),
2223
+ [doc, workflow, stateCode]
2224
+ );
2225
+ return /* @__PURE__ */ jsxs12(ModalFrame, { onCancel, children: [
2226
+ /* @__PURE__ */ jsx13("h2", { style: { margin: 0, fontSize: 16 }, children: messages.confirmDelete.title }),
2227
+ /* @__PURE__ */ jsx13("p", { style: { margin: "12px 0", fontSize: 13, color: "#475569" }, children: messages.confirmDelete.message }),
2228
+ /* @__PURE__ */ jsxs12("div", { style: { padding: 8, background: "#F8FAFC", border: "1px solid #E2E8F0", borderRadius: 4, fontSize: 13 }, children: [
2229
+ /* @__PURE__ */ jsx13("strong", { children: stateCode }),
2230
+ /* @__PURE__ */ jsxs12("div", { style: { color: "#475569" }, children: [
2231
+ messages.confirmDelete.transitionsAffected,
2232
+ ": ",
2233
+ counts.outgoing + counts.incoming
2234
+ ] })
2235
+ ] }),
2236
+ /* @__PURE__ */ jsxs12("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 16 }, children: [
2237
+ /* @__PURE__ */ jsx13("button", { type: "button", onClick: onCancel, style: ghostBtn3, "data-testid": "modal-delete-cancel", children: messages.confirmDelete.cancel }),
2238
+ /* @__PURE__ */ jsx13("button", { type: "button", onClick: onConfirm, style: dangerBtn4, "data-testid": "modal-delete-confirm", children: messages.confirmDelete.confirm })
2239
+ ] })
2240
+ ] });
2241
+ }
2242
+ function ModalFrame({
2243
+ children,
2244
+ onCancel,
2245
+ labelledBy
2246
+ }) {
2247
+ const frameRef = useRef3(null);
2248
+ const previousFocusRef = useRef3(null);
2249
+ useEffect2(() => {
2250
+ previousFocusRef.current = document.activeElement ?? null;
2251
+ const node = frameRef.current;
2252
+ if (node) {
2253
+ const focusable = node.querySelector(
2254
+ 'input, select, textarea, button, [tabindex]:not([tabindex="-1"])'
2255
+ );
2256
+ (focusable ?? node).focus();
2257
+ }
2258
+ return () => {
2259
+ previousFocusRef.current?.focus?.();
2260
+ };
2261
+ }, []);
2262
+ return /* @__PURE__ */ jsx13(
2263
+ "div",
2264
+ {
2265
+ onClick: onCancel,
2266
+ onKeyDown: (e) => {
2267
+ if (e.key === "Escape") {
2268
+ e.stopPropagation();
2269
+ onCancel();
2270
+ }
2271
+ },
2272
+ style: {
2273
+ position: "fixed",
2274
+ inset: 0,
2275
+ background: "rgba(15,23,42,0.4)",
2276
+ display: "flex",
2277
+ alignItems: "center",
2278
+ justifyContent: "center",
2279
+ zIndex: 1e3
2280
+ },
2281
+ "data-testid": "modal-backdrop",
2282
+ children: /* @__PURE__ */ jsx13(
2283
+ "div",
2284
+ {
2285
+ ref: frameRef,
2286
+ role: "dialog",
2287
+ "aria-modal": "true",
2288
+ "aria-labelledby": labelledBy,
2289
+ tabIndex: -1,
2290
+ onClick: (e) => e.stopPropagation(),
2291
+ style: {
2292
+ background: "white",
2293
+ borderRadius: 6,
2294
+ padding: 20,
2295
+ minWidth: 340,
2296
+ boxShadow: "0 10px 30px rgba(15,23,42,0.25)",
2297
+ outline: "none"
2298
+ },
2299
+ "data-testid": "modal-frame",
2300
+ children
2301
+ }
2302
+ )
2303
+ }
2304
+ );
2305
+ }
2306
+ var ghostBtn3 = {
2307
+ padding: "6px 12px",
2308
+ background: "white",
2309
+ border: "1px solid #CBD5E1",
2310
+ borderRadius: 4,
2311
+ fontSize: 13,
2312
+ cursor: "pointer"
2313
+ };
2314
+ var dangerBtn4 = {
2315
+ ...ghostBtn3,
2316
+ background: "#DC2626",
2317
+ color: "white",
2318
+ borderColor: "#DC2626"
2319
+ };
2320
+
2321
+ // src/modals/DragConnectModal.tsx
2322
+ import { useState as useState4 } from "react";
2323
+ import { NAME_REGEX } from "@cyoda/workflow-core";
2324
+ import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
2325
+ function DragConnectModal({
2326
+ source,
2327
+ fromState,
2328
+ toState,
2329
+ onCreate,
2330
+ onCancel
2331
+ }) {
2332
+ const messages = useMessages();
2333
+ const [name, setName] = useState4("");
2334
+ const existing = new Set(source.transitions.map((t) => t.name));
2335
+ const invalidFormat = !!name && !NAME_REGEX.test(name);
2336
+ const duplicate = existing.has(name);
2337
+ const blocked = name.length === 0 || invalidFormat || duplicate;
2338
+ return /* @__PURE__ */ jsxs13(ModalFrame, { onCancel, children: [
2339
+ /* @__PURE__ */ jsx14("h2", { style: { margin: 0, fontSize: 16 }, children: messages.dragConnect.title }),
2340
+ /* @__PURE__ */ jsxs13("p", { style: { margin: "6px 0 14px", fontSize: 12, color: "#475569" }, children: [
2341
+ fromState,
2342
+ " \u2192 ",
2343
+ toState
2344
+ ] }),
2345
+ /* @__PURE__ */ jsxs13("label", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: [
2346
+ /* @__PURE__ */ jsx14("span", { style: { fontSize: 12, color: "#475569" }, children: messages.dragConnect.transitionName }),
2347
+ /* @__PURE__ */ jsx14(
2348
+ "input",
2349
+ {
2350
+ type: "text",
2351
+ value: name,
2352
+ onChange: (e) => setName(e.target.value),
2353
+ style: {
2354
+ padding: "6px 8px",
2355
+ fontSize: 13,
2356
+ border: "1px solid #CBD5E1",
2357
+ borderRadius: 4
2358
+ },
2359
+ "data-testid": "dragconnect-name",
2360
+ autoFocus: true
2361
+ }
2362
+ )
2363
+ ] }),
2364
+ invalidFormat && /* @__PURE__ */ jsx14("div", { style: errorMsg, "data-testid": "dragconnect-error-format", children: messages.dragConnect.invalidName }),
2365
+ duplicate && /* @__PURE__ */ jsx14("div", { style: errorMsg, "data-testid": "dragconnect-error-duplicate", children: messages.dragConnect.duplicateName }),
2366
+ /* @__PURE__ */ jsxs13("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 16 }, children: [
2367
+ /* @__PURE__ */ jsx14("button", { type: "button", onClick: onCancel, style: ghostBtn4, "data-testid": "dragconnect-cancel", children: messages.dragConnect.cancel }),
2368
+ /* @__PURE__ */ jsx14(
2369
+ "button",
2370
+ {
2371
+ type: "button",
2372
+ onClick: () => !blocked && onCreate(name),
2373
+ disabled: blocked,
2374
+ style: primaryBtn,
2375
+ "data-testid": "dragconnect-create",
2376
+ children: messages.dragConnect.create
2377
+ }
2378
+ )
2379
+ ] })
2380
+ ] });
2381
+ }
2382
+ var errorMsg = {
2383
+ marginTop: 6,
2384
+ fontSize: 12,
2385
+ color: "#B91C1C"
2386
+ };
2387
+ var ghostBtn4 = {
2388
+ padding: "6px 12px",
2389
+ background: "white",
2390
+ border: "1px solid #CBD5E1",
2391
+ borderRadius: 4,
2392
+ fontSize: 13,
2393
+ cursor: "pointer"
2394
+ };
2395
+ var primaryBtn = {
2396
+ ...ghostBtn4,
2397
+ background: "#0F172A",
2398
+ color: "white",
2399
+ borderColor: "#0F172A"
2400
+ };
2401
+
2402
+ // src/components/WorkflowEditor.tsx
2403
+ import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
2404
+ function defaultNewWorkflow(existing) {
2405
+ let n = existing.length + 1;
2406
+ while (existing.includes(`workflow${n}`)) n++;
2407
+ return {
2408
+ version: "1.0",
2409
+ name: `workflow${n}`,
2410
+ initialState: "start",
2411
+ active: true,
2412
+ states: { start: { transitions: [] } }
2413
+ };
2414
+ }
2415
+ function WorkflowEditor({
2416
+ document: initialDocument,
2417
+ mode = "editor",
2418
+ messages,
2419
+ layoutOptions,
2420
+ onChange,
2421
+ onSave
2422
+ }) {
2423
+ const mergedMessages = useMemo5(() => mergeMessages(messages), [messages]);
2424
+ const [state, actions] = useEditorStore(initialDocument, mode);
2425
+ const [pendingDelete, setPendingDelete] = useState5(null);
2426
+ const [pendingConnect, setPendingConnect] = useState5(null);
2427
+ useEffect3(() => {
2428
+ onChange?.(state.document);
2429
+ }, [state.document, onChange]);
2430
+ const readOnly = state.mode === "viewer";
2431
+ const derived = useMemo5(
2432
+ () => deriveFromDocument(state.document),
2433
+ [state.document.session, state.document.meta.ids]
2434
+ );
2435
+ const dispatch = (patch) => actions.dispatch(patch);
2436
+ const requestDeleteState = (workflow, stateCode) => {
2437
+ setPendingDelete({ workflow, stateCode });
2438
+ };
2439
+ const confirmDelete = () => {
2440
+ if (!pendingDelete) return;
2441
+ dispatch({
2442
+ op: "removeState",
2443
+ workflow: pendingDelete.workflow,
2444
+ stateCode: pendingDelete.stateCode
2445
+ });
2446
+ setPendingDelete(null);
2447
+ };
2448
+ const handleConnect = (connection) => {
2449
+ const resolved = resolveConnection(state.document, connection);
2450
+ if (resolved) setPendingConnect(resolved);
2451
+ };
2452
+ const confirmConnect = (name) => {
2453
+ if (!pendingConnect) return;
2454
+ dispatch({
2455
+ op: "addTransition",
2456
+ workflow: pendingConnect.workflow,
2457
+ fromState: pendingConnect.fromState,
2458
+ transition: {
2459
+ name,
2460
+ next: pendingConnect.toState,
2461
+ manual: false,
2462
+ disabled: false
2463
+ }
2464
+ });
2465
+ setPendingConnect(null);
2466
+ };
2467
+ const workflows = state.document.session.workflows;
2468
+ const showTabs = workflows.length > 1 || state.mode !== "viewer";
2469
+ const anyModalOpen = pendingDelete !== null || pendingConnect !== null;
2470
+ const handleKeyDown = useCallback2(
2471
+ (e) => {
2472
+ if (anyModalOpen) return;
2473
+ const mod = e.ctrlKey || e.metaKey;
2474
+ if (mod && (e.key === "z" || e.key === "Z") && !e.shiftKey) {
2475
+ if (!readOnly && state.undoStack.length > 0) {
2476
+ e.preventDefault();
2477
+ actions.undo();
2478
+ }
2479
+ return;
2480
+ }
2481
+ if (mod && (e.key === "z" && e.shiftKey || e.key === "y" || e.key === "Y")) {
2482
+ if (!readOnly && state.redoStack.length > 0) {
2483
+ e.preventDefault();
2484
+ actions.redo();
2485
+ }
2486
+ return;
2487
+ }
2488
+ if (mod && (e.key === "s" || e.key === "S")) {
2489
+ if (onSave && !readOnly && derived.errorCount === 0) {
2490
+ e.preventDefault();
2491
+ onSave(state.document);
2492
+ }
2493
+ return;
2494
+ }
2495
+ },
2496
+ [
2497
+ anyModalOpen,
2498
+ readOnly,
2499
+ state.undoStack.length,
2500
+ state.redoStack.length,
2501
+ state.document,
2502
+ actions,
2503
+ onSave,
2504
+ derived.errorCount
2505
+ ]
2506
+ );
2507
+ const pendingConnectState = useMemo5(() => {
2508
+ if (!pendingConnect) return null;
2509
+ const wf = state.document.session.workflows.find(
2510
+ (w) => w.name === pendingConnect.workflow
2511
+ );
2512
+ if (!wf) return null;
2513
+ return wf.states[pendingConnect.fromState] ?? null;
2514
+ }, [pendingConnect, state.document]);
2515
+ const orientation = layoutOptions?.orientation ?? "vertical";
2516
+ const savedViewport = state.activeWorkflow ? state.document.meta.workflowUi[state.activeWorkflow]?.viewports?.[orientation] : void 0;
2517
+ const handleViewportChange = useCallback2(
2518
+ (viewport) => {
2519
+ const workflow = state.activeWorkflow;
2520
+ if (!workflow) return;
2521
+ const current = state.document.meta.workflowUi[workflow] ?? {};
2522
+ const existing = current.viewports?.[orientation];
2523
+ const nextViewport = normalizeViewport(viewport);
2524
+ if (existing && sameViewport(existing, nextViewport)) return;
2525
+ actions.silentReplace(
2526
+ {
2527
+ session: state.document.session,
2528
+ meta: {
2529
+ ...state.document.meta,
2530
+ workflowUi: {
2531
+ ...state.document.meta.workflowUi,
2532
+ [workflow]: {
2533
+ ...current,
2534
+ viewports: {
2535
+ ...current.viewports ?? {},
2536
+ [orientation]: nextViewport
2537
+ }
2538
+ }
2539
+ }
2540
+ }
2541
+ },
2542
+ { preserveEditorState: true }
2543
+ );
2544
+ },
2545
+ [actions, orientation, state.activeWorkflow, state.document]
2546
+ );
2547
+ return /* @__PURE__ */ jsx15(I18nContext.Provider, { value: mergedMessages, children: /* @__PURE__ */ jsxs14(
2548
+ "div",
2549
+ {
2550
+ style: {
2551
+ height: "100%",
2552
+ display: "flex",
2553
+ flexDirection: "column",
2554
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Inter", system-ui, sans-serif',
2555
+ outline: "none"
2556
+ },
2557
+ "data-testid": "workflow-editor",
2558
+ onKeyDown: handleKeyDown,
2559
+ tabIndex: -1,
2560
+ children: [
2561
+ /* @__PURE__ */ jsx15(
2562
+ Toolbar,
2563
+ {
2564
+ derived,
2565
+ canUndo: state.undoStack.length > 0,
2566
+ canRedo: state.redoStack.length > 0,
2567
+ readOnly,
2568
+ onUndo: actions.undo,
2569
+ onRedo: actions.redo,
2570
+ onSave: onSave ? () => onSave(state.document) : void 0
2571
+ }
2572
+ ),
2573
+ showTabs && /* @__PURE__ */ jsx15(
2574
+ WorkflowTabs,
2575
+ {
2576
+ workflows,
2577
+ activeWorkflow: state.activeWorkflow,
2578
+ readOnly,
2579
+ onSelect: actions.setActiveWorkflow,
2580
+ onAdd: () => dispatch({
2581
+ op: "addWorkflow",
2582
+ workflow: defaultNewWorkflow(workflows.map((w) => w.name))
2583
+ }),
2584
+ onClose: (name) => dispatch({ op: "removeWorkflow", workflow: name })
2585
+ }
2586
+ ),
2587
+ /* @__PURE__ */ jsxs14("div", { style: { flex: 1, display: "flex", minHeight: 0 }, children: [
2588
+ /* @__PURE__ */ jsx15("div", { style: { flex: 1, minWidth: 0 }, children: /* @__PURE__ */ jsx15(
2589
+ Canvas,
2590
+ {
2591
+ graph: derived.graph,
2592
+ issues: derived.issues,
2593
+ activeWorkflow: state.activeWorkflow,
2594
+ selection: state.selection,
2595
+ layoutOptions,
2596
+ savedViewport,
2597
+ onSelectionChange: (sel) => actions.setSelection(sel),
2598
+ onViewportChange: handleViewportChange,
2599
+ onConnect: handleConnect,
2600
+ readOnly
2601
+ }
2602
+ ) }),
2603
+ /* @__PURE__ */ jsx15(
2604
+ Inspector,
2605
+ {
2606
+ document: state.document,
2607
+ selection: state.selection,
2608
+ issues: derived.issues,
2609
+ readOnly,
2610
+ onDispatch: dispatch,
2611
+ onSelectionChange: actions.setSelection,
2612
+ onRequestDeleteState: requestDeleteState
2613
+ }
2614
+ )
2615
+ ] }),
2616
+ pendingDelete && /* @__PURE__ */ jsx15(
2617
+ DeleteStateModal,
2618
+ {
2619
+ document: state.document,
2620
+ workflow: pendingDelete.workflow,
2621
+ stateCode: pendingDelete.stateCode,
2622
+ onConfirm: confirmDelete,
2623
+ onCancel: () => setPendingDelete(null)
2624
+ }
2625
+ ),
2626
+ pendingConnect && pendingConnectState && /* @__PURE__ */ jsx15(
2627
+ DragConnectModal,
2628
+ {
2629
+ source: pendingConnectState,
2630
+ fromState: pendingConnect.fromState,
2631
+ toState: pendingConnect.toState,
2632
+ onCreate: confirmConnect,
2633
+ onCancel: () => setPendingConnect(null)
2634
+ }
2635
+ )
2636
+ ]
2637
+ }
2638
+ ) });
2639
+ }
2640
+ function normalizeViewport(viewport) {
2641
+ return {
2642
+ x: Math.round(viewport.x * 100) / 100,
2643
+ y: Math.round(viewport.y * 100) / 100,
2644
+ zoom: Math.round(viewport.zoom * 1e3) / 1e3
2645
+ };
2646
+ }
2647
+ function sameViewport(a, b) {
2648
+ return a.x === b.x && a.y === b.y && a.zoom === b.zoom;
2649
+ }
2650
+
2651
+ // src/save/useSaveFlow.ts
2652
+ import { useCallback as useCallback3, useRef as useRef4, useState as useState6 } from "react";
2653
+ import {
2654
+ WorkflowApiConflictError
2655
+ } from "@cyoda/workflow-core";
2656
+ function useSaveFlow(args) {
2657
+ const { api, document: doc, concurrencyToken, onSaved, onReload } = args;
2658
+ const [status, setStatus] = useState6({ kind: "idle" });
2659
+ const tokenRef = useRef4(concurrencyToken);
2660
+ tokenRef.current = concurrencyToken;
2661
+ const entityRef = useRef4(doc.session.entity);
2662
+ entityRef.current = doc.session.entity;
2663
+ const payloadRef = useRef4({
2664
+ importMode: doc.session.importMode,
2665
+ workflows: doc.session.workflows
2666
+ });
2667
+ payloadRef.current = {
2668
+ importMode: doc.session.importMode,
2669
+ workflows: doc.session.workflows
2670
+ };
2671
+ const performImport = useCallback3(
2672
+ async (token) => {
2673
+ const entity = entityRef.current;
2674
+ if (!entity) {
2675
+ setStatus({
2676
+ kind: "error",
2677
+ message: "Cannot save: session has no entity identity."
2678
+ });
2679
+ return;
2680
+ }
2681
+ setStatus({ kind: "saving" });
2682
+ try {
2683
+ const result = await api.importWorkflows(entity, payloadRef.current, {
2684
+ concurrencyToken: token
2685
+ });
2686
+ onSaved(result.concurrencyToken);
2687
+ setStatus({ kind: "success", at: Date.now() });
2688
+ } catch (err) {
2689
+ if (err instanceof WorkflowApiConflictError) {
2690
+ setStatus({
2691
+ kind: "conflict",
2692
+ serverConcurrencyToken: err.serverConcurrencyToken
2693
+ });
2694
+ return;
2695
+ }
2696
+ setStatus({
2697
+ kind: "error",
2698
+ message: err instanceof Error ? err.message : "Unknown save error."
2699
+ });
2700
+ }
2701
+ },
2702
+ [api, onSaved]
2703
+ );
2704
+ const requestSave = useCallback3(() => {
2705
+ const mode = payloadRef.current.importMode;
2706
+ const requiresExplicitConfirm = mode === "REPLACE" || mode === "ACTIVATE";
2707
+ setStatus({ kind: "confirming", mode, requiresExplicitConfirm });
2708
+ }, []);
2709
+ const confirmSave = useCallback3(
2710
+ () => performImport(tokenRef.current),
2711
+ [performImport]
2712
+ );
2713
+ const cancel = useCallback3(() => setStatus({ kind: "idle" }), []);
2714
+ const forceOverwrite = useCallback3(() => performImport(null), [performImport]);
2715
+ const reload = useCallback3(() => {
2716
+ onReload?.();
2717
+ setStatus({ kind: "idle" });
2718
+ }, [onReload]);
2719
+ const clear = useCallback3(() => setStatus({ kind: "idle" }), []);
2720
+ return { status, requestSave, confirmSave, cancel, forceOverwrite, reload, clear };
2721
+ }
2722
+
2723
+ // src/save/SaveConfirmModal.tsx
2724
+ import { useState as useState7 } from "react";
2725
+ import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
2726
+ function SaveConfirmModal({
2727
+ mode,
2728
+ requiresExplicitConfirm,
2729
+ warningCount,
2730
+ diffSummary: diffSummary2,
2731
+ onConfirm,
2732
+ onCancel
2733
+ }) {
2734
+ const messages = useMessages();
2735
+ const [ackMode, setAckMode] = useState7(!requiresExplicitConfirm);
2736
+ const [ackWarnings, setAckWarnings] = useState7(warningCount === 0);
2737
+ const blocked = !ackMode || !ackWarnings;
2738
+ return /* @__PURE__ */ jsxs15(ModalFrame, { onCancel, children: [
2739
+ /* @__PURE__ */ jsx16("h2", { style: { margin: 0, fontSize: 16 }, children: messages.saveConfirm.title }),
2740
+ /* @__PURE__ */ jsxs15("p", { style: { margin: "12px 0", fontSize: 13, color: "#475569" }, children: [
2741
+ messages.saveConfirm.modeLabel,
2742
+ ": ",
2743
+ /* @__PURE__ */ jsx16("strong", { children: mode })
2744
+ ] }),
2745
+ diffSummary2 && /* @__PURE__ */ jsx16(
2746
+ "pre",
2747
+ {
2748
+ style: {
2749
+ fontFamily: "ui-monospace, SFMono-Regular, Consolas, monospace",
2750
+ background: "#F8FAFC",
2751
+ border: "1px solid #E2E8F0",
2752
+ padding: 8,
2753
+ borderRadius: 4,
2754
+ fontSize: 12,
2755
+ margin: "8px 0",
2756
+ maxHeight: 160,
2757
+ overflow: "auto",
2758
+ whiteSpace: "pre-wrap"
2759
+ },
2760
+ "data-testid": "save-diff-summary",
2761
+ children: diffSummary2
2762
+ }
2763
+ ),
2764
+ requiresExplicitConfirm && /* @__PURE__ */ jsxs15("label", { style: checkRow, "data-testid": "save-ack-mode", children: [
2765
+ /* @__PURE__ */ jsx16(
2766
+ "input",
2767
+ {
2768
+ type: "checkbox",
2769
+ checked: ackMode,
2770
+ onChange: (e) => setAckMode(e.target.checked)
2771
+ }
2772
+ ),
2773
+ /* @__PURE__ */ jsx16("span", { children: mode === "REPLACE" ? messages.saveConfirm.ackReplace : messages.saveConfirm.ackActivate })
2774
+ ] }),
2775
+ warningCount > 0 && /* @__PURE__ */ jsxs15("label", { style: checkRow, "data-testid": "save-ack-warnings", children: [
2776
+ /* @__PURE__ */ jsx16(
2777
+ "input",
2778
+ {
2779
+ type: "checkbox",
2780
+ checked: ackWarnings,
2781
+ onChange: (e) => setAckWarnings(e.target.checked)
2782
+ }
2783
+ ),
2784
+ /* @__PURE__ */ jsx16("span", { children: messages.saveConfirm.ackWarnings.replace("{count}", String(warningCount)) })
2785
+ ] }),
2786
+ /* @__PURE__ */ jsxs15("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 16 }, children: [
2787
+ /* @__PURE__ */ jsx16("button", { type: "button", onClick: onCancel, style: ghostBtn5, "data-testid": "save-cancel", children: messages.saveConfirm.cancel }),
2788
+ /* @__PURE__ */ jsx16(
2789
+ "button",
2790
+ {
2791
+ type: "button",
2792
+ disabled: blocked,
2793
+ onClick: onConfirm,
2794
+ style: primaryBtn2,
2795
+ "data-testid": "save-confirm",
2796
+ children: messages.saveConfirm.confirm
2797
+ }
2798
+ )
2799
+ ] })
2800
+ ] });
2801
+ }
2802
+ var checkRow = {
2803
+ display: "flex",
2804
+ alignItems: "center",
2805
+ gap: 8,
2806
+ fontSize: 13,
2807
+ color: "#1E293B",
2808
+ margin: "8px 0"
2809
+ };
2810
+ var ghostBtn5 = {
2811
+ padding: "6px 12px",
2812
+ background: "white",
2813
+ border: "1px solid #CBD5E1",
2814
+ borderRadius: 4,
2815
+ fontSize: 13,
2816
+ cursor: "pointer"
2817
+ };
2818
+ var primaryBtn2 = {
2819
+ ...ghostBtn5,
2820
+ background: "#0F172A",
2821
+ color: "white",
2822
+ borderColor: "#0F172A"
2823
+ };
2824
+
2825
+ // src/save/ConflictBanner.tsx
2826
+ import { jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
2827
+ function ConflictBanner({ onReload, onForceOverwrite }) {
2828
+ const messages = useMessages();
2829
+ return /* @__PURE__ */ jsxs16(
2830
+ "div",
2831
+ {
2832
+ style: {
2833
+ padding: "10px 14px",
2834
+ background: "#FEF3C7",
2835
+ borderBottom: "1px solid #F59E0B",
2836
+ color: "#78350F",
2837
+ fontSize: 13,
2838
+ display: "flex",
2839
+ alignItems: "center",
2840
+ gap: 12
2841
+ },
2842
+ role: "alert",
2843
+ "data-testid": "conflict-banner",
2844
+ children: [
2845
+ /* @__PURE__ */ jsx17("span", { style: { flex: 1 }, children: messages.conflict.message }),
2846
+ /* @__PURE__ */ jsx17(
2847
+ "button",
2848
+ {
2849
+ type: "button",
2850
+ onClick: onReload,
2851
+ style: btn,
2852
+ "data-testid": "conflict-reload",
2853
+ children: messages.conflict.reload
2854
+ }
2855
+ ),
2856
+ /* @__PURE__ */ jsx17(
2857
+ "button",
2858
+ {
2859
+ type: "button",
2860
+ onClick: onForceOverwrite,
2861
+ style: { ...btn, background: "#DC2626", color: "white", borderColor: "#DC2626" },
2862
+ "data-testid": "conflict-force",
2863
+ children: messages.conflict.forceOverwrite
2864
+ }
2865
+ )
2866
+ ]
2867
+ }
2868
+ );
2869
+ }
2870
+ var btn = {
2871
+ padding: "4px 10px",
2872
+ background: "white",
2873
+ border: "1px solid #CBD5E1",
2874
+ borderRadius: 4,
2875
+ fontSize: 12,
2876
+ cursor: "pointer"
2877
+ };
2878
+
2879
+ // src/save/diff.ts
2880
+ function diffSummary(server, local) {
2881
+ if (!server) return null;
2882
+ const sw = new Map(server.session.workflows.map((w) => [w.name, w]));
2883
+ const lw = new Map(local.session.workflows.map((w) => [w.name, w]));
2884
+ const added = [];
2885
+ const removed = [];
2886
+ const changed = [];
2887
+ for (const [name, localWf] of lw) {
2888
+ const serverWf = sw.get(name);
2889
+ if (!serverWf) {
2890
+ added.push(name);
2891
+ continue;
2892
+ }
2893
+ if (JSON.stringify(serverWf) !== JSON.stringify(localWf)) {
2894
+ changed.push(name);
2895
+ }
2896
+ }
2897
+ for (const [name] of sw) {
2898
+ if (!lw.has(name)) removed.push(name);
2899
+ }
2900
+ const lines = [];
2901
+ if (added.length) lines.push(`+ added: ${added.join(", ")}`);
2902
+ if (removed.length) lines.push(`- removed: ${removed.join(", ")}`);
2903
+ if (changed.length) lines.push(`~ changed: ${changed.join(", ")}`);
2904
+ if (lines.length === 0) lines.push("(no workflow-level differences)");
2905
+ return lines.join("\n");
2906
+ }
2907
+ export {
2908
+ ConflictBanner,
2909
+ I18nContext,
2910
+ SaveConfirmModal,
2911
+ WorkflowEditor,
2912
+ defaultMessages,
2913
+ diffSummary,
2914
+ mergeMessages,
2915
+ useMessages,
2916
+ useSaveFlow
2917
+ };
2918
+ //# sourceMappingURL=index.js.map