@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/LICENSE +18 -0
- package/README.md +24 -2
- package/dist/index.d.ts +106 -0
- package/dist/index.js +1475 -0
- package/dist/state-flow.css +716 -0
- package/package.json +65 -3
- package/tsconfig.charts.json +11 -0
- package/types/index.d.ts +46 -0
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
|
+
"&": "&",
|
|
975
|
+
"<": "<",
|
|
976
|
+
">": ">",
|
|
977
|
+
'"': """,
|
|
978
|
+
"'": "'"
|
|
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
|
+
};
|