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