@ai-digital-factory/state-flow 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1721 @@
1
+ // src/StateFlowPlaceholder.tsx
2
+ import { jsx } from "react/jsx-runtime";
3
+ function StateFlowPlaceholder() {
4
+ return /* @__PURE__ */ jsx("p", { children: "State Flow library stub" });
5
+ }
6
+
7
+ // src/Flow.tsx
8
+ import { useId, useMemo } from "react";
9
+
10
+ // src/flow-visuals.tsx
11
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
12
+ function SvgIcon({
13
+ children,
14
+ size = 28
15
+ }) {
16
+ return /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", width: size, height: size, fill: "none", children });
17
+ }
18
+ function FlowHeaderIcon({ flowId }) {
19
+ const key = (flowId ?? "").toLowerCase();
20
+ if (key.includes("visitor") || key.includes("access")) {
21
+ return /* @__PURE__ */ jsxs(SvgIcon, { children: [
22
+ /* @__PURE__ */ jsx2(
23
+ "circle",
24
+ {
25
+ cx: "12",
26
+ cy: "8",
27
+ r: "3.25",
28
+ stroke: "currentColor",
29
+ strokeWidth: "1.75"
30
+ }
31
+ ),
32
+ /* @__PURE__ */ jsx2(
33
+ "path",
34
+ {
35
+ d: "M5 20c0-3.5 3.1-6 7-6s7 2.5 7 6",
36
+ stroke: "currentColor",
37
+ strokeWidth: "1.75",
38
+ strokeLinecap: "round"
39
+ }
40
+ )
41
+ ] });
42
+ }
43
+ if (key.includes("operator") || key.includes("session") || key.includes("gate")) {
44
+ return /* @__PURE__ */ jsxs(SvgIcon, { children: [
45
+ /* @__PURE__ */ jsx2(
46
+ "rect",
47
+ {
48
+ x: "5",
49
+ y: "10",
50
+ width: "14",
51
+ height: "10",
52
+ rx: "2",
53
+ stroke: "currentColor",
54
+ strokeWidth: "1.75"
55
+ }
56
+ ),
57
+ /* @__PURE__ */ jsx2(
58
+ "path",
59
+ {
60
+ d: "M8 10V8a4 4 0 0 1 8 0v2",
61
+ stroke: "currentColor",
62
+ strokeWidth: "1.75",
63
+ strokeLinecap: "round"
64
+ }
65
+ )
66
+ ] });
67
+ }
68
+ if (key.includes("search") || key.includes("instrument")) {
69
+ return /* @__PURE__ */ jsxs(SvgIcon, { children: [
70
+ /* @__PURE__ */ jsx2(
71
+ "circle",
72
+ {
73
+ cx: "10.5",
74
+ cy: "10.5",
75
+ r: "5.5",
76
+ stroke: "currentColor",
77
+ strokeWidth: "1.75"
78
+ }
79
+ ),
80
+ /* @__PURE__ */ jsx2(
81
+ "path",
82
+ {
83
+ d: "M15 15l4.5 4.5",
84
+ stroke: "currentColor",
85
+ strokeWidth: "1.75",
86
+ strokeLinecap: "round"
87
+ }
88
+ )
89
+ ] });
90
+ }
91
+ return /* @__PURE__ */ jsxs(SvgIcon, { children: [
92
+ /* @__PURE__ */ jsx2(
93
+ "rect",
94
+ {
95
+ x: "3",
96
+ y: "4",
97
+ width: "8",
98
+ height: "6",
99
+ rx: "2",
100
+ stroke: "currentColor",
101
+ strokeWidth: "1.75"
102
+ }
103
+ ),
104
+ /* @__PURE__ */ jsx2(
105
+ "rect",
106
+ {
107
+ x: "13",
108
+ y: "14",
109
+ width: "8",
110
+ height: "6",
111
+ rx: "2",
112
+ stroke: "currentColor",
113
+ strokeWidth: "1.75"
114
+ }
115
+ ),
116
+ /* @__PURE__ */ jsx2(
117
+ "path",
118
+ {
119
+ d: "M11 7h2M11 7l2 7",
120
+ stroke: "currentColor",
121
+ strokeWidth: "1.75",
122
+ strokeLinecap: "round"
123
+ }
124
+ )
125
+ ] });
126
+ }
127
+ function flowNavIcon(flowId) {
128
+ const key = flowId.toLowerCase();
129
+ if (key.includes("visitor") || key.includes("access")) {
130
+ return "\u25CE";
131
+ }
132
+ if (key.includes("search") || key.includes("instrument")) {
133
+ return "\u2315";
134
+ }
135
+ if (key.includes("operator") || key.includes("session") || key.includes("gate")) {
136
+ return "\u2B21";
137
+ }
138
+ return "\u25C6";
139
+ }
140
+
141
+ // src/FlowPageHeader.tsx
142
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
143
+ function FlowPageHeader({
144
+ title,
145
+ description,
146
+ flowId
147
+ }) {
148
+ return /* @__PURE__ */ jsxs2("header", { className: "sf-flow-page-header", children: [
149
+ /* @__PURE__ */ jsx3("div", { className: "sf-flow-page-header-icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(FlowHeaderIcon, { flowId: flowId ?? title }) }),
150
+ /* @__PURE__ */ jsxs2("div", { className: "sf-flow-page-header-text", children: [
151
+ /* @__PURE__ */ jsx3("h2", { className: "sf-title", children: title }),
152
+ /* @__PURE__ */ jsx3("p", { className: "sf-intro", children: description })
153
+ ] })
154
+ ] });
155
+ }
156
+
157
+ // src/DiagramOverview.tsx
158
+ import {
159
+ useCallback,
160
+ useEffect,
161
+ useRef,
162
+ useState
163
+ } from "react";
164
+
165
+ // src/DiagramLegend.tsx
166
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
167
+ function DiagramLegend() {
168
+ return /* @__PURE__ */ jsxs3("footer", { className: "sf-diagram-legend", "aria-label": "Diagram legend", children: [
169
+ /* @__PURE__ */ jsx4("p", { className: "sf-diagram-legend-title", children: "Legend" }),
170
+ /* @__PURE__ */ jsxs3("ul", { className: "sf-diagram-legend-list", children: [
171
+ /* @__PURE__ */ jsxs3("li", { children: [
172
+ /* @__PURE__ */ jsx4(
173
+ "span",
174
+ {
175
+ className: "sf-diagram-legend-swatch is-active-state",
176
+ "aria-hidden": true
177
+ }
178
+ ),
179
+ /* @__PURE__ */ jsxs3("span", { children: [
180
+ /* @__PURE__ */ jsx4("strong", { children: "Active state" }),
181
+ " \u2014 Selected state."
182
+ ] })
183
+ ] }),
184
+ /* @__PURE__ */ jsxs3("li", { children: [
185
+ /* @__PURE__ */ jsx4("span", { className: "sf-diagram-legend-swatch is-state", "aria-hidden": true }),
186
+ /* @__PURE__ */ jsxs3("span", { children: [
187
+ /* @__PURE__ */ jsx4("strong", { children: "State" }),
188
+ " \u2014 A possible state."
189
+ ] })
190
+ ] }),
191
+ /* @__PURE__ */ jsxs3("li", { children: [
192
+ /* @__PURE__ */ jsx4("span", { className: "sf-diagram-legend-swatch is-connector", "aria-hidden": true }),
193
+ /* @__PURE__ */ jsxs3("span", { children: [
194
+ /* @__PURE__ */ jsx4("strong", { children: "Transition" }),
195
+ " \u2014 Movement between states."
196
+ ] })
197
+ ] }),
198
+ /* @__PURE__ */ jsxs3("li", { children: [
199
+ /* @__PURE__ */ jsx4("span", { className: "sf-diagram-legend-swatch is-label", "aria-hidden": true }),
200
+ /* @__PURE__ */ jsxs3("span", { children: [
201
+ /* @__PURE__ */ jsx4("strong", { children: "Transition label" }),
202
+ " \u2014 Event or condition."
203
+ ] })
204
+ ] })
205
+ ] })
206
+ ] });
207
+ }
208
+
209
+ // src/diagram-view-url.ts
210
+ var DIAGRAM_VIEW_ZOOM_MIN = 0.5;
211
+ var DIAGRAM_VIEW_ZOOM_MAX = 2;
212
+ var PARAM_ZOOM = "zoom";
213
+ var PARAM_PAN_X = "panX";
214
+ var PARAM_PAN_Y = "panY";
215
+ var PARAM_STATE = "state";
216
+ var PARAM_TRANSITION = "transition";
217
+ var PARAM_SCROLL_Y = "scrollY";
218
+ function parseFiniteNumber(raw) {
219
+ if (raw === null || raw === "") {
220
+ return null;
221
+ }
222
+ const value = Number(raw);
223
+ return Number.isFinite(value) ? value : null;
224
+ }
225
+ function clampDiagramZoom(scale) {
226
+ return Math.min(
227
+ DIAGRAM_VIEW_ZOOM_MAX,
228
+ Math.max(DIAGRAM_VIEW_ZOOM_MIN, scale)
229
+ );
230
+ }
231
+ function roundZoomForUrl(zoom) {
232
+ return Math.round(zoom * 1e3) / 1e3;
233
+ }
234
+ function roundPanForUrl(pan) {
235
+ return Math.round(pan);
236
+ }
237
+ function parseDiagramViewSearchParams(search, defaults) {
238
+ const params = new URLSearchParams(
239
+ search.startsWith("?") ? search.slice(1) : search
240
+ );
241
+ const zoomRaw = parseFiniteNumber(params.get(PARAM_ZOOM));
242
+ const panXRaw = parseFiniteNumber(params.get(PARAM_PAN_X));
243
+ const panYRaw = parseFiniteNumber(params.get(PARAM_PAN_Y));
244
+ const view = {
245
+ zoom: zoomRaw !== null ? clampDiagramZoom(zoomRaw) : defaults.zoom,
246
+ panX: panXRaw !== null ? panXRaw : defaults.panX,
247
+ panY: panYRaw !== null ? panYRaw : defaults.panY
248
+ };
249
+ const stateId = params.get(PARAM_STATE)?.trim() ?? "";
250
+ const transitionName = params.get(PARAM_TRANSITION)?.trim() ?? "";
251
+ let focus = null;
252
+ if (transitionName.length > 0) {
253
+ focus = { kind: "transition", transitionName };
254
+ } else if (stateId.length > 0) {
255
+ focus = { kind: "state", stateId };
256
+ }
257
+ return { view, focus };
258
+ }
259
+ function validateDiagramFocus(focus, states, edges) {
260
+ if (!focus) {
261
+ return null;
262
+ }
263
+ if (focus.kind === "state") {
264
+ return states.some((state) => state.id === focus.stateId) ? focus : null;
265
+ }
266
+ return edges.some((edge) => edge.name === focus.transitionName) ? focus : null;
267
+ }
268
+ function edgeIndexForTransitionName(edges, transitionName) {
269
+ const index = edges.findIndex((edge) => edge.name === transitionName);
270
+ return index >= 0 ? index : null;
271
+ }
272
+ function buildDiagramViewSearchParams(existingSearch, view, focus, defaults) {
273
+ const params = new URLSearchParams(
274
+ existingSearch.startsWith("?") ? existingSearch.slice(1) : existingSearch
275
+ );
276
+ params.delete(PARAM_ZOOM);
277
+ params.delete(PARAM_PAN_X);
278
+ params.delete(PARAM_PAN_Y);
279
+ params.delete(PARAM_STATE);
280
+ params.delete(PARAM_TRANSITION);
281
+ if (roundZoomForUrl(view.zoom) !== roundZoomForUrl(defaults.zoom)) {
282
+ params.set(PARAM_ZOOM, String(roundZoomForUrl(view.zoom)));
283
+ }
284
+ if (roundPanForUrl(view.panX) !== roundPanForUrl(defaults.panX)) {
285
+ params.set(PARAM_PAN_X, String(roundPanForUrl(view.panX)));
286
+ }
287
+ if (roundPanForUrl(view.panY) !== roundPanForUrl(defaults.panY)) {
288
+ params.set(PARAM_PAN_Y, String(roundPanForUrl(view.panY)));
289
+ }
290
+ if (focus?.kind === "state") {
291
+ params.set(PARAM_STATE, focus.stateId);
292
+ } else if (focus?.kind === "transition") {
293
+ params.set(PARAM_TRANSITION, focus.transitionName);
294
+ }
295
+ const serialized = params.toString();
296
+ return serialized.length > 0 ? `?${serialized}` : "";
297
+ }
298
+ function parseScrollYFromSearch(search) {
299
+ const params = new URLSearchParams(
300
+ search.startsWith("?") ? search.slice(1) : search
301
+ );
302
+ const raw = parseFiniteNumber(params.get(PARAM_SCROLL_Y));
303
+ if (raw === null) {
304
+ return null;
305
+ }
306
+ const rounded = Math.round(raw);
307
+ return rounded > 0 ? rounded : null;
308
+ }
309
+ function mergeScrollYIntoSearch(search, scrollY) {
310
+ const params = new URLSearchParams(
311
+ search.startsWith("?") ? search.slice(1) : search
312
+ );
313
+ const rounded = Math.round(scrollY);
314
+ if (rounded <= 0) {
315
+ params.delete(PARAM_SCROLL_Y);
316
+ } else {
317
+ params.set(PARAM_SCROLL_Y, String(rounded));
318
+ }
319
+ const serialized = params.toString();
320
+ return serialized.length > 0 ? `?${serialized}` : "";
321
+ }
322
+ function replaceBrowserUrlSearch(search) {
323
+ if (typeof window === "undefined") {
324
+ return;
325
+ }
326
+ const next = `${window.location.pathname}${search}${window.location.hash}`;
327
+ if (`${window.location.pathname}${window.location.search}${window.location.hash}` === next) {
328
+ return;
329
+ }
330
+ window.history.replaceState(null, "", next);
331
+ }
332
+ function readDiagramViewFromBrowser(defaults) {
333
+ if (typeof window === "undefined") {
334
+ return { view: defaults, focus: null };
335
+ }
336
+ return parseDiagramViewSearchParams(window.location.search, defaults);
337
+ }
338
+
339
+ // src/DiagramOverview.tsx
340
+ import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
341
+ function stateById(states, id) {
342
+ return states.find((state) => state.id === id);
343
+ }
344
+ var ZOOM_MIN = 0.5;
345
+ var ZOOM_MAX = 2;
346
+ var ZOOM_STEP = 0.15;
347
+ var ZOOM_DEFAULT = 1;
348
+ var PAN_CLICK_THRESHOLD_PX = 4;
349
+ function clampZoom(scale) {
350
+ return clampDiagramZoom(scale);
351
+ }
352
+ function viewStateDefaults() {
353
+ return { zoom: ZOOM_DEFAULT, panX: 0, panY: 0 };
354
+ }
355
+ function selectionToFocus(selection, edges) {
356
+ if (!selection) {
357
+ return null;
358
+ }
359
+ if (selection.kind === "state") {
360
+ return { kind: "state", stateId: selection.stateId };
361
+ }
362
+ const edge = edges[selection.edgeIndex];
363
+ return edge ? { kind: "transition", transitionName: edge.name } : null;
364
+ }
365
+ function focusToSelection(focus, edges) {
366
+ if (!focus) {
367
+ return null;
368
+ }
369
+ if (focus.kind === "state") {
370
+ return { kind: "state", stateId: focus.stateId };
371
+ }
372
+ const edgeIndex = edgeIndexForTransitionName(edges, focus.transitionName);
373
+ return edgeIndex === null ? null : { kind: "edge", edgeIndex };
374
+ }
375
+ function outgoingTransitions(edges, stateId) {
376
+ return edges.filter((edge) => edge.fromId === stateId);
377
+ }
378
+ function DestinationList({
379
+ edges,
380
+ edge
381
+ }) {
382
+ const destinations = edges.filter(
383
+ (candidate) => candidate.name === edge.name && candidate.fromId === edge.fromId
384
+ );
385
+ return /* @__PURE__ */ jsx5(
386
+ "ul",
387
+ {
388
+ className: "sf-overview-destination-list",
389
+ "aria-label": destinations.length > 1 ? "Branch destinations" : "Destination",
390
+ children: destinations.map((destination) => /* @__PURE__ */ jsxs4(
391
+ "li",
392
+ {
393
+ className: `sf-overview-legend-route${destination.toId === edge.toId && destinations.length > 1 ? " is-selected" : ""}`,
394
+ children: [
395
+ /* @__PURE__ */ jsxs4("code", { children: [
396
+ destination.fromName,
397
+ " \u2192 ",
398
+ destination.toName
399
+ ] }),
400
+ destination.destinationDescription ? /* @__PURE__ */ jsxs4("span", { className: "sf-overview-destination-detail", children: [
401
+ " ",
402
+ destination.destinationDescription
403
+ ] }) : null
404
+ ]
405
+ },
406
+ destination.toId
407
+ ))
408
+ }
409
+ );
410
+ }
411
+ function DiagramOverview({
412
+ svgHtml,
413
+ states,
414
+ edges,
415
+ viewPersistenceKey,
416
+ guardBlockedEdgeIndex = null,
417
+ detailLayout = "inline",
418
+ initialStateId
419
+ }) {
420
+ const figureRef = useRef(null);
421
+ const viewportRef = useRef(null);
422
+ const detailRef = useRef(null);
423
+ const viewDefaults = viewStateDefaults();
424
+ const restoredFromUrl = viewPersistenceKey ? readDiagramViewFromBrowser(viewDefaults) : { view: viewDefaults, focus: null };
425
+ const [zoom, setZoom] = useState(() => clampZoom(restoredFromUrl.view.zoom));
426
+ const [pan, setPan] = useState({
427
+ x: restoredFromUrl.view.panX,
428
+ y: restoredFromUrl.view.panY
429
+ });
430
+ const [isPanning, setIsPanning] = useState(false);
431
+ const [selection, setSelection] = useState(() => {
432
+ if (!viewPersistenceKey) {
433
+ return null;
434
+ }
435
+ const validFocus = validateDiagramFocus(
436
+ restoredFromUrl.focus,
437
+ states,
438
+ edges
439
+ );
440
+ return focusToSelection(validFocus, edges);
441
+ });
442
+ const panSessionRef = useRef(null);
443
+ const suppressSelectionClickRef = useRef(false);
444
+ const edgesRef = useRef(edges);
445
+ const statesRef = useRef(states);
446
+ useEffect(() => {
447
+ edgesRef.current = edges;
448
+ statesRef.current = states;
449
+ }, [edges, states]);
450
+ const syncViewToUrl = useCallback(() => {
451
+ if (!viewPersistenceKey) {
452
+ return;
453
+ }
454
+ const view = { zoom, panX: pan.x, panY: pan.y };
455
+ const focus = selectionToFocus(selection, edges);
456
+ const search = buildDiagramViewSearchParams(
457
+ window.location.search,
458
+ view,
459
+ focus,
460
+ viewDefaults
461
+ );
462
+ replaceBrowserUrlSearch(search);
463
+ }, [viewPersistenceKey, zoom, pan.x, pan.y, selection, edges]);
464
+ useEffect(() => {
465
+ syncViewToUrl();
466
+ }, [syncViewToUrl]);
467
+ useEffect(() => {
468
+ if (!viewPersistenceKey) {
469
+ return;
470
+ }
471
+ const onPopState = () => {
472
+ const { view, focus } = readDiagramViewFromBrowser(viewDefaults);
473
+ const validFocus = validateDiagramFocus(focus, states, edges);
474
+ setZoom(clampZoom(view.zoom));
475
+ setPan({ x: view.panX, y: view.panY });
476
+ setSelection(focusToSelection(validFocus, edges));
477
+ };
478
+ window.addEventListener("popstate", onPopState);
479
+ return () => window.removeEventListener("popstate", onPopState);
480
+ }, [viewPersistenceKey, states, edges]);
481
+ useEffect(() => {
482
+ if (!viewPersistenceKey) {
483
+ return;
484
+ }
485
+ const { view, focus } = readDiagramViewFromBrowser(viewDefaults);
486
+ if (!focus) {
487
+ return;
488
+ }
489
+ const validFocus = validateDiagramFocus(focus, states, edges);
490
+ if (validFocus) {
491
+ return;
492
+ }
493
+ const search = buildDiagramViewSearchParams(
494
+ window.location.search,
495
+ view,
496
+ null,
497
+ viewDefaults
498
+ );
499
+ replaceBrowserUrlSearch(search);
500
+ setSelection(null);
501
+ }, [viewPersistenceKey, states, edges, svgHtml]);
502
+ const preparedSvg = svgHtml;
503
+ const applySelectionToDom = useCallback((next) => {
504
+ const figure = figureRef.current;
505
+ if (!figure) {
506
+ return;
507
+ }
508
+ const svg = figure.querySelector("svg");
509
+ figure.querySelectorAll(".overview-edge-hit").forEach((node) => {
510
+ node.classList.remove("is-selected");
511
+ node.removeAttribute("aria-pressed");
512
+ });
513
+ figure.querySelectorAll(".flow-edge.is-selected").forEach((node) => {
514
+ node.classList.remove("is-selected");
515
+ });
516
+ figure.querySelectorAll(".overview-state-hit").forEach((node) => {
517
+ node.classList.remove("is-selected");
518
+ node.removeAttribute("aria-pressed");
519
+ });
520
+ svg?.classList.remove("has-edge-selection", "has-state-selection");
521
+ if (!next) {
522
+ return;
523
+ }
524
+ if (next.kind === "edge") {
525
+ figure.querySelectorAll(".overview-edge-hit").forEach((node) => {
526
+ const active = Number(node.getAttribute("data-edge-index")) === next.edgeIndex;
527
+ node.classList.toggle("is-selected", active);
528
+ node.setAttribute("aria-pressed", String(active));
529
+ });
530
+ figure.querySelectorAll(".flow-edge").forEach((node) => {
531
+ node.classList.toggle(
532
+ "is-selected",
533
+ Number(node.getAttribute("data-edge-index")) === next.edgeIndex
534
+ );
535
+ });
536
+ svg?.classList.add("has-edge-selection");
537
+ return;
538
+ }
539
+ figure.querySelectorAll(".overview-state-hit").forEach((node) => {
540
+ const active = node.getAttribute("data-state-id") === next.stateId;
541
+ node.classList.toggle("is-selected", active);
542
+ node.setAttribute("aria-pressed", String(active));
543
+ });
544
+ svg?.classList.add("has-state-selection");
545
+ }, []);
546
+ const applyGuardBlockedToDom = useCallback((edgeIndex) => {
547
+ const figure = figureRef.current;
548
+ if (!figure) {
549
+ return;
550
+ }
551
+ const svg = figure.querySelector("svg");
552
+ figure.querySelectorAll(".flow-edge.is-guard-blocked").forEach((node) => {
553
+ node.classList.remove("is-guard-blocked");
554
+ if (node instanceof SVGElement) {
555
+ node.style.removeProperty("stroke");
556
+ node.style.removeProperty("stroke-width");
557
+ node.style.removeProperty("filter");
558
+ node.style.removeProperty("opacity");
559
+ }
560
+ });
561
+ figure.querySelectorAll(".overview-edge-hit.is-guard-blocked").forEach((node) => {
562
+ node.classList.remove("is-guard-blocked");
563
+ });
564
+ svg?.classList.remove("has-guard-blocked");
565
+ if (edgeIndex === null || edgeIndex < 0) {
566
+ return;
567
+ }
568
+ svg?.classList.add("has-guard-blocked");
569
+ const path = figure.querySelector(
570
+ `.flow-edge[data-edge-index="${edgeIndex}"]`
571
+ );
572
+ const hit = figure.querySelector(
573
+ `.overview-edge-hit[data-edge-index="${edgeIndex}"]`
574
+ );
575
+ path?.classList.add("is-guard-blocked");
576
+ if (path instanceof SVGElement) {
577
+ path.style.stroke = "var(--sf-warning)";
578
+ path.style.strokeWidth = "3.75";
579
+ path.style.filter = "drop-shadow(0 0 10px color-mix(in srgb, var(--sf-warning) 75%, transparent))";
580
+ path.style.opacity = "1";
581
+ }
582
+ hit?.classList.add("is-guard-blocked");
583
+ }, []);
584
+ useEffect(() => {
585
+ const figure = figureRef.current;
586
+ if (!figure) {
587
+ return;
588
+ }
589
+ figure.innerHTML = preparedSvg;
590
+ }, [preparedSvg]);
591
+ useEffect(() => {
592
+ if (detailLayout !== "split" || !initialStateId) {
593
+ return;
594
+ }
595
+ setSelection((current) => {
596
+ if (current !== null) {
597
+ return current;
598
+ }
599
+ return stateById(states, initialStateId) ? { kind: "state", stateId: initialStateId } : null;
600
+ });
601
+ }, [detailLayout, initialStateId, states, preparedSvg]);
602
+ useEffect(() => {
603
+ if (guardBlockedEdgeIndex === null || guardBlockedEdgeIndex < 0) {
604
+ return;
605
+ }
606
+ setSelection({ kind: "edge", edgeIndex: guardBlockedEdgeIndex });
607
+ }, [guardBlockedEdgeIndex]);
608
+ useEffect(() => {
609
+ applySelectionToDom(selection);
610
+ applyGuardBlockedToDom(guardBlockedEdgeIndex ?? null);
611
+ }, [
612
+ selection,
613
+ guardBlockedEdgeIndex,
614
+ applySelectionToDom,
615
+ applyGuardBlockedToDom,
616
+ preparedSvg
617
+ ]);
618
+ useEffect(() => {
619
+ const figure = figureRef.current;
620
+ if (!figure) {
621
+ return;
622
+ }
623
+ const revealEdge = (edgeIndex) => {
624
+ const edge = edgesRef.current[edgeIndex];
625
+ if (!edge) {
626
+ return;
627
+ }
628
+ setSelection({ kind: "edge", edgeIndex });
629
+ if (detailLayout !== "split") {
630
+ detailRef.current?.scrollIntoView({
631
+ block: "nearest",
632
+ behavior: "smooth"
633
+ });
634
+ }
635
+ };
636
+ const revealState = (stateId) => {
637
+ const state = stateById(statesRef.current, stateId);
638
+ if (!state) {
639
+ return;
640
+ }
641
+ setSelection({ kind: "state", stateId });
642
+ if (detailLayout !== "split") {
643
+ detailRef.current?.scrollIntoView({
644
+ block: "nearest",
645
+ behavior: "smooth"
646
+ });
647
+ }
648
+ };
649
+ const onClick = (event) => {
650
+ if (suppressSelectionClickRef.current) {
651
+ suppressSelectionClickRef.current = false;
652
+ return;
653
+ }
654
+ const target = event.target;
655
+ if (!(target instanceof Element)) {
656
+ return;
657
+ }
658
+ const edgeHit = target.closest(".overview-edge-hit");
659
+ if (edgeHit && figure.contains(edgeHit)) {
660
+ event.preventDefault();
661
+ revealEdge(Number(edgeHit.getAttribute("data-edge-index")));
662
+ return;
663
+ }
664
+ const stateHit = target.closest(".overview-state-hit");
665
+ if (stateHit && figure.contains(stateHit)) {
666
+ event.preventDefault();
667
+ const stateId = stateHit.getAttribute("data-state-id");
668
+ if (stateId) {
669
+ revealState(stateId);
670
+ }
671
+ }
672
+ };
673
+ const onKeyDown = (event) => {
674
+ const target = event.target;
675
+ if (!(target instanceof Element)) {
676
+ return;
677
+ }
678
+ if (event.key !== "Enter" && event.key !== " ") {
679
+ return;
680
+ }
681
+ const edgeHit = target.closest(".overview-edge-hit");
682
+ if (edgeHit && figure.contains(edgeHit)) {
683
+ event.preventDefault();
684
+ revealEdge(Number(edgeHit.getAttribute("data-edge-index")));
685
+ return;
686
+ }
687
+ const stateHit = target.closest(".overview-state-hit");
688
+ if (stateHit && figure.contains(stateHit)) {
689
+ event.preventDefault();
690
+ const stateId = stateHit.getAttribute("data-state-id");
691
+ if (stateId) {
692
+ revealState(stateId);
693
+ }
694
+ }
695
+ };
696
+ figure.addEventListener("click", onClick);
697
+ figure.addEventListener("keydown", onKeyDown);
698
+ return () => {
699
+ figure.removeEventListener("click", onClick);
700
+ figure.removeEventListener("keydown", onKeyDown);
701
+ };
702
+ }, [preparedSvg, detailLayout]);
703
+ const guardBlockedActive = guardBlockedEdgeIndex !== null && guardBlockedEdgeIndex >= 0 && selection?.kind === "edge" && selection.edgeIndex === guardBlockedEdgeIndex;
704
+ const detailAside = (() => {
705
+ if (selection?.kind === "edge") {
706
+ const edge = edges[selection.edgeIndex];
707
+ if (!edge) {
708
+ return null;
709
+ }
710
+ return /* @__PURE__ */ jsxs4(
711
+ "div",
712
+ {
713
+ className: `sf-overview-detail-card${guardBlockedActive ? " is-guard-blocked" : ""}`,
714
+ children: [
715
+ /* @__PURE__ */ jsxs4("h3", { className: "sf-overview-detail-heading", children: [
716
+ /* @__PURE__ */ jsx5("span", { className: "sf-overview-detail-heading-icon", "aria-hidden": true, children: "\u2192" }),
717
+ "Transition"
718
+ ] }),
719
+ /* @__PURE__ */ jsx5("p", { className: "sf-overview-legend-title", children: /* @__PURE__ */ jsx5("strong", { children: edge.name }) }),
720
+ guardBlockedActive ? /* @__PURE__ */ jsx5("p", { className: "sf-overview-guard-blocked-note", children: "Guard blocked this transition." }) : null,
721
+ /* @__PURE__ */ jsx5("p", { className: "sf-overview-legend-detail", children: edge.description }),
722
+ /* @__PURE__ */ jsx5(DestinationList, { edges, edge })
723
+ ]
724
+ }
725
+ );
726
+ }
727
+ if (selection?.kind === "state") {
728
+ const state = stateById(states, selection.stateId);
729
+ if (!state) {
730
+ return null;
731
+ }
732
+ const outgoing = outgoingTransitions(edges, state.id);
733
+ return /* @__PURE__ */ jsxs4(Fragment, { children: [
734
+ /* @__PURE__ */ jsxs4("div", { className: "sf-overview-detail-card", children: [
735
+ /* @__PURE__ */ jsxs4("h3", { className: "sf-overview-detail-heading", children: [
736
+ /* @__PURE__ */ jsx5("span", { className: "sf-overview-detail-heading-icon", "aria-hidden": true, children: "\u25C6" }),
737
+ "State details"
738
+ ] }),
739
+ /* @__PURE__ */ jsxs4("div", { className: "sf-overview-detail-title-row", children: [
740
+ /* @__PURE__ */ jsx5("p", { className: "sf-overview-legend-title", children: /* @__PURE__ */ jsx5("strong", { children: state.name }) }),
741
+ /* @__PURE__ */ jsx5(
742
+ "span",
743
+ {
744
+ className: `sf-overview-detail-kind-tag${state.kind === "final" ? " is-final" : ""}`,
745
+ children: state.kind === "final" ? "Final state" : "State"
746
+ }
747
+ )
748
+ ] }),
749
+ /* @__PURE__ */ jsx5("p", { className: "sf-overview-legend-detail", children: state.description })
750
+ ] }),
751
+ outgoing.length > 0 ? /* @__PURE__ */ jsxs4("div", { className: "sf-overview-transitions-card", children: [
752
+ /* @__PURE__ */ jsxs4("h4", { className: "sf-overview-transitions-heading", children: [
753
+ "Transitions (",
754
+ outgoing.length,
755
+ ")"
756
+ ] }),
757
+ /* @__PURE__ */ jsx5("ul", { className: "sf-overview-transitions-list", children: outgoing.map((edge, index) => /* @__PURE__ */ jsx5(
758
+ "li",
759
+ {
760
+ className: "sf-overview-transitions-item",
761
+ children: /* @__PURE__ */ jsxs4(
762
+ "button",
763
+ {
764
+ type: "button",
765
+ className: "sf-overview-transitions-button",
766
+ "aria-pressed": void 0,
767
+ onClick: () => {
768
+ const edgeIndex = edges.indexOf(edge);
769
+ if (edgeIndex >= 0) {
770
+ setSelection({ kind: "edge", edgeIndex });
771
+ }
772
+ },
773
+ children: [
774
+ /* @__PURE__ */ jsx5(
775
+ "span",
776
+ {
777
+ className: "sf-overview-transitions-dot",
778
+ "aria-hidden": true
779
+ }
780
+ ),
781
+ /* @__PURE__ */ jsx5("span", { className: "sf-overview-transitions-name", children: edge.name }),
782
+ /* @__PURE__ */ jsxs4("span", { className: "sf-overview-transitions-route", children: [
783
+ edge.fromName,
784
+ " \u2192 ",
785
+ edge.toName,
786
+ edge.destinationCount > 1 ? ` (${edge.destinationDescription})` : ""
787
+ ] })
788
+ ]
789
+ }
790
+ )
791
+ },
792
+ `${edge.name}-${edge.toId}-${index}`
793
+ )) })
794
+ ] }) : null
795
+ ] });
796
+ }
797
+ if (detailLayout === "split") {
798
+ return /* @__PURE__ */ jsxs4("div", { className: "sf-overview-detail-empty", children: [
799
+ /* @__PURE__ */ jsx5("p", { className: "sf-overview-detail-empty-title", children: "Nothing selected" }),
800
+ /* @__PURE__ */ jsx5("p", { className: "sf-overview-detail-empty-hint", children: "Click a state or transition on the diagram to inspect it." })
801
+ ] });
802
+ }
803
+ return null;
804
+ })();
805
+ let detailContent = null;
806
+ if (detailLayout === "inline" && selection !== null) {
807
+ if (selection.kind === "edge") {
808
+ const edge = edges[selection.edgeIndex];
809
+ if (edge) {
810
+ detailContent = /* @__PURE__ */ jsxs4(Fragment, { children: [
811
+ /* @__PURE__ */ jsx5("p", { className: "sf-overview-legend-title", children: /* @__PURE__ */ jsx5("strong", { children: edge.name }) }),
812
+ guardBlockedActive ? /* @__PURE__ */ jsx5("p", { className: "sf-overview-guard-blocked-note", children: "Guard blocked this transition." }) : null,
813
+ /* @__PURE__ */ jsx5("p", { className: "sf-overview-legend-detail", children: edge.description }),
814
+ /* @__PURE__ */ jsx5(DestinationList, { edges, edge })
815
+ ] });
816
+ }
817
+ } else {
818
+ const state = stateById(states, selection.stateId);
819
+ if (state) {
820
+ detailContent = /* @__PURE__ */ jsxs4(Fragment, { children: [
821
+ /* @__PURE__ */ jsx5("p", { className: "sf-caption", children: "State" }),
822
+ /* @__PURE__ */ jsx5("p", { className: "sf-overview-legend-title", children: /* @__PURE__ */ jsx5("strong", { children: state.name }) }),
823
+ /* @__PURE__ */ jsx5("p", { className: "sf-overview-legend-detail", children: state.description })
824
+ ] });
825
+ }
826
+ }
827
+ }
828
+ const zoomOut = () => setZoom((current) => clampZoom(current - ZOOM_STEP));
829
+ const zoomIn = () => setZoom((current) => clampZoom(current + ZOOM_STEP));
830
+ const resetView = () => {
831
+ setZoom(ZOOM_DEFAULT);
832
+ setPan({ x: 0, y: 0 });
833
+ setSelection(null);
834
+ };
835
+ const isDiagramHitTarget = (target) => {
836
+ if (!(target instanceof Element)) {
837
+ return false;
838
+ }
839
+ return Boolean(
840
+ target.closest(".overview-state-hit") || target.closest(".overview-edge-hit")
841
+ );
842
+ };
843
+ const onViewportPointerDown = (event) => {
844
+ const middleButton = event.button === 1;
845
+ const primaryButton = event.button === 0;
846
+ if (!middleButton && !primaryButton) {
847
+ return;
848
+ }
849
+ if (primaryButton && isDiagramHitTarget(event.target)) {
850
+ return;
851
+ }
852
+ event.preventDefault();
853
+ panSessionRef.current = {
854
+ pointerId: event.pointerId,
855
+ originX: event.clientX,
856
+ originY: event.clientY,
857
+ startPanX: pan.x,
858
+ startPanY: pan.y
859
+ };
860
+ setIsPanning(true);
861
+ event.currentTarget.setPointerCapture(event.pointerId);
862
+ };
863
+ const onViewportPointerMove = (event) => {
864
+ const session = panSessionRef.current;
865
+ if (!session || session.pointerId !== event.pointerId) {
866
+ return;
867
+ }
868
+ const deltaX = event.clientX - session.originX;
869
+ const deltaY = event.clientY - session.originY;
870
+ if (Math.abs(deltaX) > PAN_CLICK_THRESHOLD_PX || Math.abs(deltaY) > PAN_CLICK_THRESHOLD_PX) {
871
+ suppressSelectionClickRef.current = true;
872
+ }
873
+ setPan({
874
+ x: session.startPanX + deltaX,
875
+ y: session.startPanY + deltaY
876
+ });
877
+ };
878
+ const endPan = (event) => {
879
+ const session = panSessionRef.current;
880
+ if (!session || session.pointerId !== event.pointerId) {
881
+ return;
882
+ }
883
+ panSessionRef.current = null;
884
+ setIsPanning(false);
885
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
886
+ event.currentTarget.releasePointerCapture(event.pointerId);
887
+ }
888
+ };
889
+ const overviewLabel = detailLayout === "split" ? "State flow" : "Overview";
890
+ const diagramColumn = /* @__PURE__ */ jsxs4("div", { className: "sf-diagram-overview", children: [
891
+ /* @__PURE__ */ jsxs4("div", { className: "sf-diagram-overview-header", children: [
892
+ /* @__PURE__ */ jsxs4("div", { children: [
893
+ /* @__PURE__ */ jsx5("p", { className: "sf-diagram-overview-label", children: overviewLabel }),
894
+ /* @__PURE__ */ jsx5("p", { className: "sf-diagram-overview-hint", children: "Drag empty space (or middle-click drag) to pan; click a state or transition to read it." })
895
+ ] }),
896
+ /* @__PURE__ */ jsxs4(
897
+ "div",
898
+ {
899
+ className: "sf-diagram-overview-zoom",
900
+ role: "group",
901
+ "aria-label": "Diagram view",
902
+ children: [
903
+ /* @__PURE__ */ jsx5(
904
+ "button",
905
+ {
906
+ type: "button",
907
+ className: "sf-diagram-zoom-btn",
908
+ "aria-label": "Zoom out",
909
+ disabled: zoom <= ZOOM_MIN + 1e-6,
910
+ onClick: zoomOut,
911
+ children: "\u2212"
912
+ }
913
+ ),
914
+ /* @__PURE__ */ jsxs4("span", { className: "sf-diagram-zoom-level", "aria-live": "polite", children: [
915
+ Math.round(zoom * 100),
916
+ "%"
917
+ ] }),
918
+ /* @__PURE__ */ jsx5(
919
+ "button",
920
+ {
921
+ type: "button",
922
+ className: "sf-diagram-zoom-btn",
923
+ "aria-label": "Zoom in",
924
+ disabled: zoom >= ZOOM_MAX - 1e-6,
925
+ onClick: zoomIn,
926
+ children: "+"
927
+ }
928
+ ),
929
+ /* @__PURE__ */ jsx5(
930
+ "button",
931
+ {
932
+ type: "button",
933
+ className: "sf-diagram-zoom-btn sf-diagram-reset-btn",
934
+ "aria-label": "Reset zoom and pan",
935
+ onClick: resetView,
936
+ children: "Reset"
937
+ }
938
+ )
939
+ ]
940
+ }
941
+ )
942
+ ] }),
943
+ /* @__PURE__ */ jsx5(
944
+ "div",
945
+ {
946
+ className: `sf-diagram-overview-viewport${isPanning ? " is-panning" : ""}`,
947
+ ref: viewportRef,
948
+ onPointerDown: onViewportPointerDown,
949
+ onPointerMove: onViewportPointerMove,
950
+ onPointerUp: endPan,
951
+ onPointerCancel: endPan,
952
+ children: /* @__PURE__ */ jsx5(
953
+ "div",
954
+ {
955
+ className: "sf-diagram-overview-figure",
956
+ ref: figureRef,
957
+ style: {
958
+ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`
959
+ }
960
+ }
961
+ )
962
+ }
963
+ ),
964
+ detailLayout === "inline" && selection !== null && detailContent ? /* @__PURE__ */ jsx5(
965
+ "div",
966
+ {
967
+ className: `sf-overview-edge-detail${guardBlockedActive ? " is-guard-blocked" : ""}`,
968
+ ref: detailRef,
969
+ role: "region",
970
+ "aria-live": "polite",
971
+ "data-selection-kind": selection.kind === "edge" ? "transition" : "state",
972
+ children: detailContent
973
+ }
974
+ ) : null
975
+ ] });
976
+ if (detailLayout === "split") {
977
+ return /* @__PURE__ */ jsxs4("div", { className: "sf-diagram-workspace-wrap", children: [
978
+ /* @__PURE__ */ jsxs4("div", { className: "sf-diagram-workspace", children: [
979
+ /* @__PURE__ */ jsx5("div", { className: "sf-diagram-workspace-primary", children: diagramColumn }),
980
+ /* @__PURE__ */ jsx5(
981
+ "aside",
982
+ {
983
+ className: "sf-diagram-workspace-aside",
984
+ ref: detailRef,
985
+ role: "region",
986
+ "aria-live": "polite",
987
+ children: detailAside
988
+ }
989
+ )
990
+ ] }),
991
+ /* @__PURE__ */ jsx5(DiagramLegend, {})
992
+ ] });
993
+ }
994
+ return /* @__PURE__ */ jsxs4(Fragment, { children: [
995
+ diagramColumn,
996
+ /* @__PURE__ */ jsx5(DiagramLegend, {})
997
+ ] });
998
+ }
999
+
1000
+ // src/diagram-svg.ts
1001
+ function escapeXml(value) {
1002
+ return value.replace(/[&<>"']/g, (character) => {
1003
+ return {
1004
+ "&": "&amp;",
1005
+ "<": "&lt;",
1006
+ ">": "&gt;",
1007
+ '"': "&quot;",
1008
+ "'": "&apos;"
1009
+ }[character];
1010
+ });
1011
+ }
1012
+ function wrapLabel(name) {
1013
+ const words = name.match(/\S{1,22}/g) ?? [""];
1014
+ const lines = [""];
1015
+ for (const word of words) {
1016
+ const last = lines.length - 1;
1017
+ if (lines[last] && lines[last].length + word.length + 1 > 22)
1018
+ lines.push(word);
1019
+ else lines[last] += `${lines[last] ? " " : ""}${word}`;
1020
+ }
1021
+ return lines;
1022
+ }
1023
+ function wrapEdgeLabel(name) {
1024
+ if (name.length <= 18) {
1025
+ return [name];
1026
+ }
1027
+ const segments = name.split("-");
1028
+ const lines = [];
1029
+ let current = "";
1030
+ for (const segment of segments) {
1031
+ const candidate = current ? `${current}-${segment}` : segment;
1032
+ if (candidate.length > 16 && current) {
1033
+ lines.push(current);
1034
+ current = segment;
1035
+ } else {
1036
+ current = candidate;
1037
+ }
1038
+ }
1039
+ if (current) {
1040
+ lines.push(current);
1041
+ }
1042
+ if (lines.length <= 2) {
1043
+ return lines;
1044
+ }
1045
+ const remainder = lines.slice(1).join("-");
1046
+ const tail = remainder.length > 15 ? `${remainder.slice(0, 14)}\u2026` : remainder;
1047
+ return [lines[0], tail];
1048
+ }
1049
+ function stateIconMarkup(stateId, x, iconY) {
1050
+ const key = stateId.toLowerCase();
1051
+ const stroke = "var(--sf-accent)";
1052
+ const common = `stroke="${stroke}" stroke-width="1.6" stroke-linecap="round" fill="none"`;
1053
+ if (key.includes("guest")) {
1054
+ return `<g class="overview-state-icon" pointer-events="none" transform="translate(${x - 78} ${iconY - 4})"><circle cx="10" cy="6" r="3.2" ${common}/><path d="M4 16c0-2.8 2.7-5 6-5s6 2.2 6 5" ${common}/></g>`;
1055
+ }
1056
+ if (key.includes("unauth")) {
1057
+ return `<g class="overview-state-icon" pointer-events="none" transform="translate(${x - 78} ${iconY - 4})"><ellipse cx="10" cy="10" rx="6.5" ry="4.8" ${common}/><path d="M4 6l12 8M16 6 4 14" ${common}/></g>`;
1058
+ }
1059
+ if (key.includes("auth")) {
1060
+ return `<g class="overview-state-icon" pointer-events="none" transform="translate(${x - 78} ${iconY - 4})"><rect x="5" y="8.5" width="10" height="8" rx="1.5" ${common}/><path d="M7 8.5V6.8a3 3 0 0 1 6 0V8.5" ${common}/></g>`;
1061
+ }
1062
+ return `<g class="overview-state-icon" pointer-events="none" transform="translate(${x - 78} ${iconY - 4})"><rect x="6" y="6" width="8" height="8" rx="2" ${common}/></g>`;
1063
+ }
1064
+ function orthogonalEdgePath(start, end, attachY, laneY) {
1065
+ return `M ${start} ${attachY} L ${start} ${laneY} L ${end} ${laneY} L ${end} ${attachY}`;
1066
+ }
1067
+ var STATE_BOX_HALF_WIDTH = 95;
1068
+ var SELF_LOOP_BAND_HEIGHT = 42;
1069
+ function layoutSelfLoop(cx, top, slot, slotsOnState) {
1070
+ const innerLeft = cx - STATE_BOX_HALF_WIDTH + 18;
1071
+ const innerRight = cx + STATE_BOX_HALF_WIDTH - 18;
1072
+ const band = innerRight - innerLeft;
1073
+ const loopSpan = Math.min(
1074
+ 92,
1075
+ Math.max(52, (band - 12 * Math.max(0, slotsOnState - 1)) / slotsOnState)
1076
+ );
1077
+ const stride = slotsOnState > 1 ? (band - loopSpan) / (slotsOnState - 1) : 0;
1078
+ const left = innerLeft + slot * stride;
1079
+ const right = left + loopSpan;
1080
+ const laneY = top - 26;
1081
+ const path = `M ${roundCoord(left)} ${top} L ${roundCoord(left)} ${laneY} L ${roundCoord(right)} ${laneY} L ${roundCoord(right)} ${top}`;
1082
+ return { path, labelCx: (left + right) / 2, labelCy: laneY };
1083
+ }
1084
+ function roundCoord(value) {
1085
+ return Math.round(value * 10) / 10;
1086
+ }
1087
+ function countSelfLoopsPerState(flow) {
1088
+ const counts = /* @__PURE__ */ new Map();
1089
+ for (const edge of flow.edges) {
1090
+ if (edge.fromId !== edge.toId) {
1091
+ continue;
1092
+ }
1093
+ counts.set(edge.fromId, (counts.get(edge.fromId) ?? 0) + 1);
1094
+ }
1095
+ return counts;
1096
+ }
1097
+ function maxSelfLoopsOnOneState(counts) {
1098
+ let max = 0;
1099
+ for (const count of counts.values()) {
1100
+ max = Math.max(max, count);
1101
+ }
1102
+ return max;
1103
+ }
1104
+ function wrapBranchLabel(text) {
1105
+ const words = text.trim().split(/\s+/);
1106
+ const lines = [""];
1107
+ for (const word of words) {
1108
+ const last = lines.length - 1;
1109
+ if (lines[last] && lines[last].length + word.length + 1 > 20) {
1110
+ if (lines.length === 2) {
1111
+ lines[last] = `${lines[last].slice(0, 18)}\u2026`;
1112
+ return lines;
1113
+ }
1114
+ lines.push(word);
1115
+ } else {
1116
+ lines[last] += `${lines[last] ? " " : ""}${word}`;
1117
+ }
1118
+ }
1119
+ return lines;
1120
+ }
1121
+ function transitionLabelBadge(options) {
1122
+ const branch = options.branchLabel !== void 0;
1123
+ const lines = branch ? wrapBranchLabel(options.branchLabel) : wrapEdgeLabel(options.edgeName);
1124
+ const charWidth = 6.35;
1125
+ const lineHeight = 12;
1126
+ const padX = 10;
1127
+ const padY = 6;
1128
+ const width = Math.min(
1129
+ 152,
1130
+ Math.max(40, ...lines.map((line) => line.length * charWidth + padX * 2))
1131
+ );
1132
+ const height = lines.length * lineHeight + padY * 2;
1133
+ const x = options.cx - width / 2;
1134
+ const y = options.cy - height / 2;
1135
+ const textAnchorY = options.cy - (lines.length - 1) * lineHeight / 2 + 3.5;
1136
+ const textContent = lines.map(
1137
+ (line, lineIndex) => `<tspan x="${options.cx}" y="${textAnchorY + lineIndex * lineHeight}">${escapeXml(line)}</tspan>`
1138
+ ).join("");
1139
+ const label = branch ? `${options.edgeName}: ${options.branchLabel}` : options.edgeName;
1140
+ return `<g class="overview-edge-hit${branch ? " overview-branch-hit" : ""}" data-edge-index="${options.index}" role="button" tabindex="0" aria-label="${escapeXml(label)}"><path d="${options.path}" fill="none" stroke="transparent" stroke-width="22"/><rect class="overview-edge-hit-area" x="${x - 6}" y="${y - 6}" width="${width + 12}" height="${height + 12}" rx="${(height + 12) / 2}" fill="transparent"/><rect class="overview-edge-badge${branch ? " overview-branch-badge" : ""}" x="${x}" y="${y}" width="${width}" height="${height}" rx="${branch ? 6 : height / 2}" fill="${branch ? "var(--sf-panel)" : "var(--sf-selected)"}" stroke="var(--sf-accent)" stroke-width="${branch ? 1.5 : 2}"${branch ? ' stroke-dasharray="4 3"' : ""}/><text text-anchor="middle" fill="var(--sf-ink)" font-size="10" font-weight="${branch ? 500 : 600}" ${branch ? 'font-style="italic" ' : ""}pointer-events="none">${textContent}</text></g>`;
1141
+ }
1142
+ function splitNodeMarkup(options) {
1143
+ const r = 7;
1144
+ const { x, y } = options;
1145
+ return `<g class="overview-edge-hit overview-split-hit" data-edge-index="${options.index}" role="button" tabindex="0" aria-label="${escapeXml(`${options.edgeName} (branches)`)}"><path class="overview-split-node" d="M ${x} ${y - r} L ${x + r} ${y} L ${x} ${y + r} L ${x - r} ${y} Z" fill="var(--sf-accent)" stroke="var(--sf-panel)" stroke-width="1.5"/></g>`;
1146
+ }
1147
+ function diagramSvg(flow, title, instanceId) {
1148
+ const templates = flow.states.map(
1149
+ (state) => (state.composition?.regions ?? []).filter((region) => region.kind !== "inline").map(
1150
+ (region) => region.kind === "list" ? `${region.name}: ${region.flow}[ ]` : `${region.name}: ${region.flow}`
1151
+ )
1152
+ );
1153
+ const labels = flow.states.map((state, index) => [
1154
+ ...wrapLabel(state.name),
1155
+ ...templates[index]
1156
+ ]);
1157
+ const boxHeight = Math.max(
1158
+ 54,
1159
+ ...labels.map((lines) => lines.length * 17 + 24)
1160
+ );
1161
+ const width = Math.max(280, flow.states.length * 260 + 24);
1162
+ const markerId = escapeXml(`${instanceId}-arrow`);
1163
+ const centers = new Map(
1164
+ flow.states.map((state, index) => [state.id, 142 + index * 260])
1165
+ );
1166
+ const routes = flow.edges.map((edge, index) => {
1167
+ const from = centers.get(edge.fromId);
1168
+ const to = centers.get(edge.toId);
1169
+ return {
1170
+ index,
1171
+ from,
1172
+ to,
1173
+ above: from <= to,
1174
+ start: from,
1175
+ end: to,
1176
+ lane: 0
1177
+ };
1178
+ });
1179
+ for (const center of centers.values()) {
1180
+ for (const above of [true, false]) {
1181
+ const ports = routes.filter((route) => {
1182
+ const edge = flow.edges[route.index];
1183
+ return edge.fromId !== edge.toId && route.above === above;
1184
+ }).flatMap((route) => [
1185
+ ...route.from === center ? [{ route, key: "start", neighbor: route.to }] : [],
1186
+ ...route.to === center ? [{ route, key: "end", neighbor: route.from }] : []
1187
+ ]).sort(
1188
+ (a, b) => a.neighbor - b.neighbor || a.route.index - b.route.index
1189
+ );
1190
+ ports.forEach((port, index) => {
1191
+ port.route[port.key] = center - 78 + 156 * (index + 1) / (ports.length + 1);
1192
+ });
1193
+ }
1194
+ }
1195
+ const sharedStart = /* @__PURE__ */ new Map();
1196
+ for (const route of routes) {
1197
+ const edge = flow.edges[route.index];
1198
+ if (edge.destinationCount < 2 || edge.fromId === edge.toId) continue;
1199
+ const key = `${edge.name}:${route.above}`;
1200
+ const shared = sharedStart.get(key);
1201
+ if (shared === void 0) sharedStart.set(key, route.start);
1202
+ else route.start = shared;
1203
+ }
1204
+ const selfLoopCounts = countSelfLoopsPerState(flow);
1205
+ const maxSelfLoopsOnState = maxSelfLoopsOnOneState(selfLoopCounts);
1206
+ const selfLoopReserved = maxSelfLoopsOnState > 0 ? SELF_LOOP_BAND_HEIGHT + (maxSelfLoopsOnState - 1) * 4 : 0;
1207
+ const laneCounts = { above: 0, below: 0 };
1208
+ for (const above of [true, false]) {
1209
+ const lanes = [];
1210
+ const sorted = routes.filter((route) => {
1211
+ const edge = flow.edges[route.index];
1212
+ return edge.fromId !== edge.toId && route.above === above;
1213
+ }).sort(
1214
+ (a, b) => Math.abs(a.end - a.start) - Math.abs(b.end - b.start) || a.index - b.index
1215
+ );
1216
+ for (const route of sorted) {
1217
+ const left = Math.min(route.start, route.end) - 16;
1218
+ const right = Math.max(route.start, route.end) + 16;
1219
+ let lane = lanes.findIndex(
1220
+ (intervals) => intervals.every(([l, r]) => right < l || left > r)
1221
+ );
1222
+ if (lane === -1) {
1223
+ lane = lanes.length;
1224
+ lanes.push([]);
1225
+ }
1226
+ lanes[lane].push([left, right]);
1227
+ route.lane = lane;
1228
+ }
1229
+ laneCounts[above ? "above" : "below"] = lanes.length;
1230
+ }
1231
+ const top = 32 + (laneCounts.above ? 64 + (laneCounts.above - 1) * 44 : 0) + selfLoopReserved;
1232
+ const bottom = top + boxHeight;
1233
+ const paths = [];
1234
+ const badges = [];
1235
+ const splits = [];
1236
+ const drawnSplits = /* @__PURE__ */ new Set();
1237
+ const selfLoopSlot = /* @__PURE__ */ new Map();
1238
+ const edgeClass = (edge) => edge.destinationCount > 1 ? "flow-edge flow-edge-branch" : "flow-edge";
1239
+ flow.edges.forEach((edge, index) => {
1240
+ const branching = edge.destinationCount > 1;
1241
+ if (edge.fromId === edge.toId) {
1242
+ const cx = centers.get(edge.fromId);
1243
+ const slotsOnState = selfLoopCounts.get(edge.fromId) ?? 1;
1244
+ const slot = selfLoopSlot.get(edge.fromId) ?? 0;
1245
+ selfLoopSlot.set(edge.fromId, slot + 1);
1246
+ const layout = layoutSelfLoop(cx, top, slot, slotsOnState);
1247
+ paths.push(
1248
+ `<path class="${edgeClass(edge)} flow-edge-self" data-edge-index="${index}" d="${layout.path}" fill="none" stroke="var(--sf-accent)" stroke-width="2.5" data-transition="${escapeXml(edge.name)}" marker-end="url(#${markerId})"/>`
1249
+ );
1250
+ badges.push(
1251
+ transitionLabelBadge({
1252
+ edgeName: edge.name,
1253
+ cx: layout.labelCx,
1254
+ cy: layout.labelCy,
1255
+ index,
1256
+ path: layout.path,
1257
+ ...branching ? { branchLabel: edge.destinationDescription } : {}
1258
+ })
1259
+ );
1260
+ return;
1261
+ }
1262
+ const { start, end, above, lane: laneIndex } = routes[index];
1263
+ const lane = above ? top - 64 - laneIndex * 44 : bottom + 64 + laneIndex * 44;
1264
+ const y = above ? top : bottom;
1265
+ const path = orthogonalEdgePath(start, end, y, lane);
1266
+ paths.push(
1267
+ `<path class="${edgeClass(edge)}" data-edge-index="${index}" d="${path}" fill="none" stroke="var(--sf-accent)" stroke-width="2.5" data-transition="${escapeXml(edge.name)}" marker-end="url(#${markerId})"/>`
1268
+ );
1269
+ if (branching) {
1270
+ const splitKey = `${edge.name}:${above}`;
1271
+ const splitY = above ? y - 32 : y + 32;
1272
+ if (!drawnSplits.has(splitKey)) {
1273
+ const firstSplit = ![...drawnSplits].some(
1274
+ (key) => key.startsWith(`${edge.name}:`)
1275
+ );
1276
+ drawnSplits.add(splitKey);
1277
+ splits.push(
1278
+ splitNodeMarkup({ edgeName: edge.name, x: start, y: splitY, index })
1279
+ );
1280
+ if (firstSplit) {
1281
+ badges.push(
1282
+ transitionLabelBadge({
1283
+ edgeName: edge.name,
1284
+ cx: start,
1285
+ cy: (y + splitY) / 2,
1286
+ index,
1287
+ path: `M ${start} ${y} L ${start} ${splitY}`
1288
+ }).replace(
1289
+ 'class="overview-edge-hit"',
1290
+ 'class="overview-edge-hit overview-split-label"'
1291
+ )
1292
+ );
1293
+ }
1294
+ }
1295
+ }
1296
+ badges.push(
1297
+ transitionLabelBadge({
1298
+ edgeName: edge.name,
1299
+ cx: (start + end) / 2,
1300
+ cy: lane,
1301
+ index,
1302
+ path,
1303
+ ...branching ? { branchLabel: edge.destinationDescription } : {}
1304
+ })
1305
+ );
1306
+ });
1307
+ const brackets = [];
1308
+ for (const parent of flow.states) {
1309
+ for (const region of parent.composition?.regions ?? []) {
1310
+ if (region.kind !== "inline") continue;
1311
+ const children = flow.states.filter(
1312
+ (state) => state.parent?.state === parent.name && state.parent.region === region.name
1313
+ );
1314
+ if (children.length === 0) continue;
1315
+ const xs = children.map((state) => centers.get(state.id));
1316
+ const left = Math.min(...xs) - 95;
1317
+ const right = Math.max(...xs) + 95;
1318
+ const y = bottom + 14;
1319
+ const label = parent.composition.kind === "compound" ? `inside ${parent.name}` : `inside ${parent.name} \u203A ${region.name}`;
1320
+ brackets.push(
1321
+ `<g class="overview-region" pointer-events="none"><path d="M ${left} ${y - 6} L ${left} ${y} L ${right} ${y} L ${right} ${y - 6}" fill="none" stroke="var(--sf-accent)" stroke-width="1.2" stroke-dasharray="4 3"/><text x="${(left + right) / 2}" y="${y + 13}" text-anchor="middle" fill="var(--sf-ink)" font-size="10" font-style="italic">${escapeXml(label)}</text></g>`
1322
+ );
1323
+ }
1324
+ }
1325
+ const nodes = flow.states.map((state, index) => {
1326
+ const x = centers.get(state.id);
1327
+ const initial = state.id === flow.initialId;
1328
+ const lines = labels[index];
1329
+ const final = state.kind === "final";
1330
+ const kindTag = final ? null : state.composition?.kind === "parallel" ? "PARALLEL" : state.composition ? "COMPOUND" : null;
1331
+ const textY = top + boxHeight / 2 - (lines.length - 1) * 8.5 + (final || kindTag ? 9 : 4);
1332
+ const iconY = top + 22;
1333
+ const labelX = x + 8;
1334
+ const stroke = final ? "var(--sf-final)" : "var(--sf-accent)";
1335
+ const finalMarkup = final ? `<rect class="overview-state-final-ring" x="${x - 90}" y="${top + 5}" width="180" height="${boxHeight - 10}" rx="9" fill="none" stroke="${stroke}" stroke-width="1.2" pointer-events="none"/><text class="overview-state-kind" x="${x}" y="${top + 17}" text-anchor="middle" fill="${stroke}" font-size="9" font-weight="700" letter-spacing="0.08em" pointer-events="none">FINAL</text>` : kindTag ? `<text class="overview-state-kind" x="${x}" y="${top + 17}" text-anchor="middle" fill="${stroke}" font-size="9" font-weight="700" letter-spacing="0.08em" pointer-events="none">${kindTag}</text>` : "";
1336
+ return `${initial ? `<circle cx="${x - 108}" cy="${top + boxHeight / 2}" r="7" fill="var(--sf-accent)"/>` : ""}${final ? "" : stateIconMarkup(state.id, x, iconY)}<g class="overview-state-hit${initial ? " is-initial-state" : ""}${final ? " is-final-state" : ""}" data-state-id="${escapeXml(state.id)}" data-state-kind="${state.kind}" role="button" tabindex="0" aria-label="${escapeXml(final ? `${state.name} (final state)` : state.name)}"><rect class="overview-state-box" x="${x - 95}" y="${top}" width="190" height="${boxHeight}" rx="12" fill="${final ? "var(--sf-final-fill)" : "var(--sf-panel)"}" stroke="${stroke}" stroke-width="${initial ? 3 : 2}"/>${finalMarkup}<text text-anchor="middle" fill="var(--sf-ink)" font-size="13" font-weight="500" pointer-events="none">${lines.map((line, lineIndex) => `<tspan x="${labelX}" y="${textY + lineIndex * 17}">${escapeXml(line)}</tspan>`).join("")}</text></g>`;
1337
+ });
1338
+ const height = bottom + (brackets.length > 0 ? 40 : 32) + (laneCounts.below ? 64 + (laneCounts.below - 1) * 44 : 0);
1339
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" class="sf-flow-svg" style="font-family:system-ui,sans-serif"><title>${escapeXml(title)}</title><defs><marker id="${markerId}" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M 0 1 L 9 5 L 0 9 z" fill="var(--sf-accent)"/></marker></defs>${paths.join("")}${brackets.join("")}${splits.join("")}${badges.join("")}${nodes.join("")}</svg>`;
1340
+ }
1341
+
1342
+ // src/parse-flow.ts
1343
+ import {
1344
+ compileFlow
1345
+ } from "@ai-digital-factory/state-flow-definition";
1346
+
1347
+ // src/extract-flow-definition.ts
1348
+ import {
1349
+ Children,
1350
+ isValidElement
1351
+ } from "react";
1352
+ var FLOW_DISPLAY_NAME = "StateFlowFlow";
1353
+ var STATE_DISPLAY_NAME = "StateFlowState";
1354
+ var FINAL_STATE_DISPLAY_NAME = "StateFlowFinalState";
1355
+ var PARALLEL_STATE_DISPLAY_NAME = "StateFlowParallelState";
1356
+ var REGION_DISPLAY_NAME = "StateFlowRegion";
1357
+ var FLOW_LIST_DISPLAY_NAME = "StateFlowFlowList";
1358
+ var TRANSITION_DISPLAY_NAME = "StateFlowTransition";
1359
+ var TO_DISPLAY_NAME = "StateFlowTo";
1360
+ function elementDisplayName(type) {
1361
+ if (typeof type === "function" || typeof type === "object") {
1362
+ if (type && "displayName" in type) {
1363
+ const displayName = type.displayName;
1364
+ if (displayName) {
1365
+ return displayName;
1366
+ }
1367
+ }
1368
+ if (typeof type === "function" && type.name) {
1369
+ return type.name;
1370
+ }
1371
+ }
1372
+ return void 0;
1373
+ }
1374
+ function isElement(node, displayName) {
1375
+ return isValidElement(node) && elementDisplayName(node.type) === displayName;
1376
+ }
1377
+ function isFlowElement(node) {
1378
+ return isElement(node, FLOW_DISPLAY_NAME);
1379
+ }
1380
+ function stateKind(node) {
1381
+ if (isElement(node, STATE_DISPLAY_NAME)) return "state";
1382
+ if (isElement(node, FINAL_STATE_DISPLAY_NAME)) return "final";
1383
+ if (isElement(node, PARALLEL_STATE_DISPLAY_NAME)) return "parallel";
1384
+ return null;
1385
+ }
1386
+ function transitionsOf(children) {
1387
+ const transitions = [];
1388
+ Children.forEach(children, (transitionChild) => {
1389
+ if (!isElement(transitionChild, TRANSITION_DISPLAY_NAME)) {
1390
+ return;
1391
+ }
1392
+ const destinations = [];
1393
+ Children.forEach(transitionChild.props.children, (toChild) => {
1394
+ if (isElement(toChild, TO_DISPLAY_NAME)) {
1395
+ destinations.push({
1396
+ state: toChild.props.state,
1397
+ description: toChild.props.description
1398
+ });
1399
+ }
1400
+ });
1401
+ const { childWork, trigger } = transitionChild.props;
1402
+ transitions.push({
1403
+ name: transitionChild.props.name,
1404
+ description: transitionChild.props.description,
1405
+ destinations,
1406
+ ...childWork !== void 0 ? { childWork } : {},
1407
+ ...trigger !== void 0 && trigger !== "dispatch" ? { trigger } : {}
1408
+ });
1409
+ });
1410
+ return transitions;
1411
+ }
1412
+ function statesOf(children) {
1413
+ const states = [];
1414
+ Children.forEach(children, (child) => {
1415
+ const kind = stateKind(child);
1416
+ if (kind === null) return;
1417
+ states.push(stateOf(child, kind));
1418
+ });
1419
+ return states;
1420
+ }
1421
+ function flowListRegion(node) {
1422
+ return {
1423
+ kind: "list",
1424
+ name: node.props.name,
1425
+ description: node.props.description,
1426
+ flow: node.props.flow
1427
+ };
1428
+ }
1429
+ function stateOf(element, kind) {
1430
+ const props = element.props;
1431
+ const regions = [];
1432
+ if (kind === "parallel") {
1433
+ Children.forEach(props.children, (child) => {
1434
+ if (isElement(child, REGION_DISPLAY_NAME)) {
1435
+ const { name, description, flow } = child.props;
1436
+ regions.push(
1437
+ flow !== void 0 ? { kind: "flow", name, description, flow } : {
1438
+ kind: "inline",
1439
+ name,
1440
+ description,
1441
+ default: child.props.default ?? "",
1442
+ states: statesOf(child.props.children)
1443
+ }
1444
+ );
1445
+ } else if (isElement(child, FLOW_LIST_DISPLAY_NAME)) {
1446
+ regions.push(flowListRegion(child));
1447
+ }
1448
+ });
1449
+ } else {
1450
+ const nested = statesOf(props.children);
1451
+ if (nested.length > 0 || props.default !== void 0) {
1452
+ regions.push({
1453
+ kind: "inline",
1454
+ name: props.name,
1455
+ description: props.description,
1456
+ default: props.default ?? "",
1457
+ states: nested
1458
+ });
1459
+ }
1460
+ if (props.flow !== void 0) {
1461
+ regions.push({
1462
+ kind: "flow",
1463
+ name: props.name,
1464
+ description: props.description,
1465
+ flow: props.flow
1466
+ });
1467
+ }
1468
+ Children.forEach(props.children, (child) => {
1469
+ if (isElement(child, FLOW_LIST_DISPLAY_NAME)) {
1470
+ regions.push(flowListRegion(child));
1471
+ }
1472
+ });
1473
+ }
1474
+ const composite = kind === "parallel" || regions.length > 0;
1475
+ return {
1476
+ name: props.name,
1477
+ description: props.description,
1478
+ kind: kind === "final" ? "final" : "state",
1479
+ transitions: transitionsOf(props.children),
1480
+ ...composite ? {
1481
+ composition: {
1482
+ kind: kind === "parallel" ? "parallel" : "compound",
1483
+ regions
1484
+ }
1485
+ } : {}
1486
+ };
1487
+ }
1488
+ function extractFlowDefinition(node) {
1489
+ if (!isFlowElement(node)) {
1490
+ throw new Error("Application flow module must render a Flow root");
1491
+ }
1492
+ const { name, description, default: defaultState, children } = node.props;
1493
+ return {
1494
+ name,
1495
+ description,
1496
+ default: defaultState,
1497
+ states: statesOf(children)
1498
+ };
1499
+ }
1500
+ function extractFlowDefinitionFromComponent(component) {
1501
+ return extractFlowDefinition(component());
1502
+ }
1503
+ function explicitStateIds(children) {
1504
+ const ids = /* @__PURE__ */ new Map();
1505
+ const visit = (nodes) => {
1506
+ Children.forEach(nodes, (child) => {
1507
+ if (!isValidElement(child)) return;
1508
+ const display = elementDisplayName(child.type);
1509
+ if (display === STATE_DISPLAY_NAME || display === FINAL_STATE_DISPLAY_NAME || display === PARALLEL_STATE_DISPLAY_NAME) {
1510
+ if (child.props.id !== void 0)
1511
+ ids.set(child.props.name, child.props.id);
1512
+ visit(child.props.children);
1513
+ } else if (display === REGION_DISPLAY_NAME) {
1514
+ visit(child.props.children);
1515
+ }
1516
+ });
1517
+ };
1518
+ visit(children);
1519
+ return ids;
1520
+ }
1521
+
1522
+ // src/parse-flow.ts
1523
+ function parseFlow(children, defaultStateName) {
1524
+ const parsed = compileFlow({
1525
+ name: "",
1526
+ description: "",
1527
+ default: defaultStateName,
1528
+ states: statesOf(children)
1529
+ });
1530
+ const explicit = explicitStateIds(children);
1531
+ if (explicit.size === 0) return parsed;
1532
+ const remap = new Map(
1533
+ parsed.states.map((state) => [
1534
+ state.id,
1535
+ explicit.get(state.name) ?? state.id
1536
+ ])
1537
+ );
1538
+ const ids = [...remap.values()];
1539
+ const duplicate = ids.find((id, index) => ids.indexOf(id) !== index);
1540
+ if (duplicate !== void 0) {
1541
+ throw new Error(`Duplicate state id: ${duplicate}`);
1542
+ }
1543
+ return {
1544
+ states: parsed.states.map((state) => ({
1545
+ ...state,
1546
+ id: remap.get(state.id)
1547
+ })),
1548
+ edges: parsed.edges.map((edge) => ({
1549
+ ...edge,
1550
+ fromId: remap.get(edge.fromId),
1551
+ toId: remap.get(edge.toId)
1552
+ })),
1553
+ initialId: remap.get(parsed.initialId)
1554
+ };
1555
+ }
1556
+
1557
+ // src/Flow.tsx
1558
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1559
+ function titleCaseName(name) {
1560
+ if (!name) {
1561
+ return name;
1562
+ }
1563
+ return name.split("-").map((part) => part ? part.charAt(0).toUpperCase() + part.slice(1) : part).join(" ");
1564
+ }
1565
+ function Flow({
1566
+ name,
1567
+ description,
1568
+ default: defaultStateName,
1569
+ children
1570
+ }) {
1571
+ const flow = useMemo(
1572
+ () => parseFlow(children, defaultStateName),
1573
+ [children, defaultStateName]
1574
+ );
1575
+ const instanceId = useId();
1576
+ const flowTitle = titleCaseName(name);
1577
+ const svgHtml = diagramSvg(flow, flowTitle, instanceId);
1578
+ return /* @__PURE__ */ jsxs5("article", { className: "sf-root", children: [
1579
+ /* @__PURE__ */ jsx6(
1580
+ FlowPageHeader,
1581
+ {
1582
+ title: flowTitle,
1583
+ description,
1584
+ flowId: name
1585
+ }
1586
+ ),
1587
+ svgHtml ? /* @__PURE__ */ jsx6(
1588
+ DiagramOverview,
1589
+ {
1590
+ svgHtml,
1591
+ states: flow.states,
1592
+ edges: flow.edges,
1593
+ viewPersistenceKey: name,
1594
+ detailLayout: "split",
1595
+ initialStateId: flow.initialId
1596
+ }
1597
+ ) : /* @__PURE__ */ jsx6("p", { className: "sf-notice", children: "No overview diagram is available for this flow." })
1598
+ ] });
1599
+ }
1600
+ Flow.displayName = "StateFlowFlow";
1601
+
1602
+ // src/State.tsx
1603
+ function State(_props) {
1604
+ void _props;
1605
+ return null;
1606
+ }
1607
+ State.displayName = "StateFlowState";
1608
+
1609
+ // src/ParallelState.tsx
1610
+ function ParallelState(_props) {
1611
+ void _props;
1612
+ return null;
1613
+ }
1614
+ ParallelState.displayName = "StateFlowParallelState";
1615
+
1616
+ // src/Region.tsx
1617
+ function Region(_props) {
1618
+ void _props;
1619
+ return null;
1620
+ }
1621
+ Region.displayName = "StateFlowRegion";
1622
+
1623
+ // src/FlowList.tsx
1624
+ function FlowList(_props) {
1625
+ void _props;
1626
+ return null;
1627
+ }
1628
+ FlowList.displayName = "StateFlowFlowList";
1629
+
1630
+ // src/FinalState.tsx
1631
+ function FinalState(_props) {
1632
+ void _props;
1633
+ return null;
1634
+ }
1635
+ FinalState.displayName = "StateFlowFinalState";
1636
+
1637
+ // src/Transition.tsx
1638
+ function Transition(_props) {
1639
+ void _props;
1640
+ return null;
1641
+ }
1642
+ Transition.displayName = "StateFlowTransition";
1643
+
1644
+ // src/To.tsx
1645
+ function To(_props) {
1646
+ void _props;
1647
+ return null;
1648
+ }
1649
+ To.displayName = "StateFlowTo";
1650
+
1651
+ // src/FlowRenderer.tsx
1652
+ import { useId as useId2, useMemo as useMemo2 } from "react";
1653
+ import {
1654
+ compileFlow as compileFlow2
1655
+ } from "@ai-digital-factory/state-flow-definition";
1656
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1657
+ function titleCaseName2(name) {
1658
+ if (!name) {
1659
+ return name;
1660
+ }
1661
+ return name.charAt(0).toUpperCase() + name.slice(1);
1662
+ }
1663
+ function FlowRenderer({ flow }) {
1664
+ const parsed = useMemo2(() => compileFlow2(flow), [flow]);
1665
+ const instanceId = useId2();
1666
+ const flowTitle = titleCaseName2(flow.name);
1667
+ const svgHtml = diagramSvg(parsed, flowTitle, instanceId);
1668
+ return /* @__PURE__ */ jsxs6("article", { className: "sf-root", children: [
1669
+ /* @__PURE__ */ jsx7(
1670
+ FlowPageHeader,
1671
+ {
1672
+ title: flowTitle,
1673
+ description: flow.description,
1674
+ flowId: flow.name
1675
+ }
1676
+ ),
1677
+ svgHtml ? /* @__PURE__ */ jsx7(
1678
+ DiagramOverview,
1679
+ {
1680
+ svgHtml,
1681
+ states: parsed.states,
1682
+ edges: parsed.edges,
1683
+ viewPersistenceKey: flow.name,
1684
+ detailLayout: "split",
1685
+ initialStateId: parsed.initialId
1686
+ }
1687
+ ) : /* @__PURE__ */ jsx7("p", { className: "sf-notice", children: "No overview diagram is available for this flow." })
1688
+ ] });
1689
+ }
1690
+
1691
+ // src/index.tsx
1692
+ import {
1693
+ DEFAULT_TRANSITION_NAME,
1694
+ STATE_HANDLER_EXPORT_NAMES,
1695
+ TRANSITION_HANDLER_EXPORT_NAMES
1696
+ } from "@ai-digital-factory/state-flow-definition";
1697
+ export {
1698
+ DEFAULT_TRANSITION_NAME,
1699
+ DiagramOverview,
1700
+ FinalState,
1701
+ Flow,
1702
+ FlowHeaderIcon,
1703
+ FlowList,
1704
+ FlowPageHeader,
1705
+ FlowRenderer,
1706
+ ParallelState,
1707
+ Region,
1708
+ STATE_HANDLER_EXPORT_NAMES,
1709
+ State,
1710
+ StateFlowPlaceholder,
1711
+ TRANSITION_HANDLER_EXPORT_NAMES,
1712
+ To,
1713
+ Transition,
1714
+ diagramSvg,
1715
+ extractFlowDefinition,
1716
+ extractFlowDefinitionFromComponent,
1717
+ flowNavIcon,
1718
+ mergeScrollYIntoSearch,
1719
+ parseScrollYFromSearch,
1720
+ replaceBrowserUrlSearch
1721
+ };