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

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