flowchart-sequence-designer 1.0.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.
@@ -0,0 +1,4596 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/ui/index.ts
21
+ var ui_exports = {};
22
+ __export(ui_exports, {
23
+ DiagramEditor: () => DiagramEditor,
24
+ SequenceEditor: () => SequenceEditor,
25
+ StepEditor: () => StepEditor,
26
+ Toolbar: () => Toolbar,
27
+ emptyModel: () => emptyModel,
28
+ presetFlowchartModel: () => presetFlowchartModel,
29
+ presetSequenceModel: () => presetSequenceModel
30
+ });
31
+ module.exports = __toCommonJS(ui_exports);
32
+
33
+ // src/ui/DiagramEditor.tsx
34
+ var import_react17 = require("react");
35
+
36
+ // src/ui/Toolbar.tsx
37
+ var import_react2 = require("react");
38
+
39
+ // src/ui/ImportDialog.tsx
40
+ var import_react = require("react");
41
+ var import_jsx_runtime = require("react/jsx-runtime");
42
+ function ImportDialog({ open, onClose, onImport }) {
43
+ const [text, setText] = (0, import_react.useState)("");
44
+ const [fileName, setFileName] = (0, import_react.useState)(null);
45
+ const [error, setError] = (0, import_react.useState)(null);
46
+ const textareaRef = (0, import_react.useRef)(null);
47
+ const fileInputRef = (0, import_react.useRef)(null);
48
+ const dialogRef = (0, import_react.useRef)(null);
49
+ (0, import_react.useEffect)(() => {
50
+ if (!open) return;
51
+ setText("");
52
+ setFileName(null);
53
+ setError(null);
54
+ const id = requestAnimationFrame(() => textareaRef.current?.focus());
55
+ return () => cancelAnimationFrame(id);
56
+ }, [open]);
57
+ (0, import_react.useEffect)(() => {
58
+ if (!open) return;
59
+ const onKey = (e) => {
60
+ if (e.key === "Escape") {
61
+ e.preventDefault();
62
+ onClose();
63
+ return;
64
+ }
65
+ if (e.key !== "Tab") return;
66
+ const root = dialogRef.current;
67
+ if (!root) return;
68
+ const focusables = root.querySelectorAll(
69
+ 'button:not([disabled]), textarea, input:not([type="file"])'
70
+ );
71
+ if (focusables.length === 0) return;
72
+ const first = focusables[0];
73
+ const last = focusables[focusables.length - 1];
74
+ const active = document.activeElement;
75
+ if (e.shiftKey && active === first) {
76
+ e.preventDefault();
77
+ last.focus();
78
+ } else if (!e.shiftKey && active === last) {
79
+ e.preventDefault();
80
+ first.focus();
81
+ }
82
+ };
83
+ window.addEventListener("keydown", onKey);
84
+ return () => window.removeEventListener("keydown", onKey);
85
+ }, [open, onClose]);
86
+ const onFile = (0, import_react.useCallback)((file) => {
87
+ setError(null);
88
+ setFileName(file.name);
89
+ const reader = new FileReader();
90
+ reader.onload = () => {
91
+ const result = typeof reader.result === "string" ? reader.result : "";
92
+ setText(result);
93
+ };
94
+ reader.onerror = () => setError(`Could not read ${file.name}`);
95
+ reader.readAsText(file);
96
+ }, []);
97
+ const onSubmit = (0, import_react.useCallback)(() => {
98
+ const trimmed2 = text.trim();
99
+ if (!trimmed2) {
100
+ setError("Paste a diagram or pick a file first.");
101
+ return;
102
+ }
103
+ try {
104
+ onImport(trimmed2);
105
+ onClose();
106
+ } catch (e) {
107
+ setError(e.message);
108
+ }
109
+ }, [text, onImport, onClose]);
110
+ if (!open) return null;
111
+ const trimmed = text.trim();
112
+ const detected = !trimmed ? null : trimmed.startsWith("{") ? "JSON" : "Mermaid";
113
+ const canSubmit = trimmed.length > 0;
114
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "presentation", onClick: onClose, style: s.backdrop, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
115
+ "div",
116
+ {
117
+ ref: dialogRef,
118
+ role: "dialog",
119
+ "aria-modal": "true",
120
+ "aria-labelledby": "fsd-import-title",
121
+ onClick: (e) => e.stopPropagation(),
122
+ style: s.dialog,
123
+ children: [
124
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { style: s.header, children: [
125
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: s.brandDot }),
126
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { id: "fsd-import-title", style: s.title, children: "Import diagram" }),
127
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: s.headerHint, children: "Mermaid or JSON" })
128
+ ] }),
129
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: s.body, children: [
130
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "fsd-import-textarea", style: s.label, children: "Paste source" }),
131
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
132
+ "textarea",
133
+ {
134
+ id: "fsd-import-textarea",
135
+ ref: textareaRef,
136
+ value: text,
137
+ onChange: (e) => {
138
+ setText(e.target.value);
139
+ setError(null);
140
+ },
141
+ placeholder: PLACEHOLDER,
142
+ spellCheck: false,
143
+ style: s.textarea
144
+ }
145
+ ),
146
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: s.fileRow, children: [
147
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
148
+ "input",
149
+ {
150
+ ref: fileInputRef,
151
+ type: "file",
152
+ accept: ".json,.mmd,.mermaid,.txt,application/json,text/plain",
153
+ style: s.hiddenFile,
154
+ onChange: (e) => {
155
+ const f = e.target.files?.[0];
156
+ if (f) onFile(f);
157
+ e.target.value = "";
158
+ }
159
+ }
160
+ ),
161
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => fileInputRef.current?.click(), style: s.fileBtn, children: "Choose file\u2026" }),
162
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: s.fileName, children: fileName ?? ".json, .mmd, .mermaid, .txt" })
163
+ ] }),
164
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: s.status, "aria-live": "polite", children: error ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: s.error, children: [
165
+ "! ",
166
+ error
167
+ ] }) : detected ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: s.ok, children: [
168
+ "Detected: ",
169
+ detected
170
+ ] }) : null })
171
+ ] }),
172
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("footer", { style: s.footer, children: [
173
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: onClose, style: s.cancelBtn, children: "Cancel" }),
174
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
175
+ "button",
176
+ {
177
+ type: "button",
178
+ onClick: onSubmit,
179
+ disabled: !canSubmit,
180
+ style: { ...s.submitBtn, ...canSubmit ? null : s.submitBtnDisabled },
181
+ children: "Import"
182
+ }
183
+ )
184
+ ] })
185
+ ]
186
+ }
187
+ ) });
188
+ }
189
+ var PLACEHOLDER = "flowchart TD\n A[Start] --> B{Choice?}\n B -->|yes| C[Done]\n B -->|no| A";
190
+ var s = {
191
+ backdrop: {
192
+ position: "fixed",
193
+ inset: 0,
194
+ zIndex: 100,
195
+ background: "rgba(15, 23, 42, 0.65)",
196
+ backdropFilter: "blur(2px)",
197
+ display: "flex",
198
+ alignItems: "center",
199
+ justifyContent: "center",
200
+ padding: 24,
201
+ fontFamily: "ui-sans-serif,system-ui,sans-serif"
202
+ },
203
+ dialog: {
204
+ width: "min(560px, 100%)",
205
+ maxHeight: "90vh",
206
+ background: "#1e293b",
207
+ color: "#f1f5f9",
208
+ border: "1px solid #334155",
209
+ borderRadius: 12,
210
+ boxShadow: "0 24px 64px rgba(0,0,0,0.45)",
211
+ display: "flex",
212
+ flexDirection: "column",
213
+ overflow: "hidden"
214
+ },
215
+ header: {
216
+ padding: "14px 18px",
217
+ borderBottom: "1px solid #334155",
218
+ display: "flex",
219
+ alignItems: "center",
220
+ gap: 10
221
+ },
222
+ brandDot: {
223
+ width: 8,
224
+ height: 8,
225
+ borderRadius: "50%",
226
+ background: "#4f46e5",
227
+ boxShadow: "0 0 8px #818cf8"
228
+ },
229
+ title: {
230
+ margin: 0,
231
+ fontSize: 14,
232
+ fontWeight: 700,
233
+ letterSpacing: 0.2,
234
+ color: "#f1f5f9",
235
+ fontFamily: "ui-monospace,monospace"
236
+ },
237
+ headerHint: { marginLeft: "auto", fontSize: 11, color: "#64748b" },
238
+ body: { padding: 18, display: "flex", flexDirection: "column", gap: 12, overflow: "auto" },
239
+ label: {
240
+ fontSize: 11,
241
+ fontWeight: 600,
242
+ color: "#94a3b8",
243
+ letterSpacing: 0.4,
244
+ textTransform: "uppercase"
245
+ },
246
+ textarea: {
247
+ width: "100%",
248
+ minHeight: 180,
249
+ padding: "10px 12px",
250
+ borderRadius: 8,
251
+ background: "#0f172a",
252
+ color: "#e2e8f0",
253
+ border: "1px solid #334155",
254
+ outline: "none",
255
+ fontFamily: "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",
256
+ fontSize: 12,
257
+ lineHeight: 1.55,
258
+ resize: "vertical",
259
+ boxSizing: "border-box"
260
+ },
261
+ fileRow: {
262
+ display: "flex",
263
+ alignItems: "center",
264
+ gap: 10,
265
+ padding: "10px 12px",
266
+ borderRadius: 8,
267
+ background: "rgba(79, 70, 229, 0.08)",
268
+ border: "1px dashed rgba(79, 70, 229, 0.4)"
269
+ },
270
+ hiddenFile: { display: "none" },
271
+ fileBtn: {
272
+ padding: "6px 12px",
273
+ borderRadius: 6,
274
+ cursor: "pointer",
275
+ background: "#4f46e5",
276
+ color: "#fff",
277
+ border: "none",
278
+ fontSize: 11,
279
+ fontWeight: 600,
280
+ fontFamily: "inherit",
281
+ letterSpacing: 0.2
282
+ },
283
+ fileName: {
284
+ fontSize: 11,
285
+ color: "#cbd5e1",
286
+ flex: 1,
287
+ overflow: "hidden",
288
+ textOverflow: "ellipsis",
289
+ whiteSpace: "nowrap"
290
+ },
291
+ status: { display: "flex", alignItems: "center", minHeight: 16 },
292
+ error: { fontSize: 11, color: "#fca5a5", fontWeight: 500 },
293
+ ok: { fontSize: 11, color: "#86efac", fontWeight: 500 },
294
+ footer: {
295
+ padding: "12px 18px",
296
+ borderTop: "1px solid #334155",
297
+ display: "flex",
298
+ justifyContent: "flex-end",
299
+ gap: 8,
300
+ background: "#0f172a"
301
+ },
302
+ cancelBtn: {
303
+ padding: "6px 14px",
304
+ borderRadius: 6,
305
+ cursor: "pointer",
306
+ background: "transparent",
307
+ color: "#cbd5e1",
308
+ border: "1px solid #334155",
309
+ fontSize: 12,
310
+ fontWeight: 500,
311
+ fontFamily: "inherit"
312
+ },
313
+ submitBtn: {
314
+ padding: "6px 14px",
315
+ borderRadius: 6,
316
+ cursor: "pointer",
317
+ background: "#4f46e5",
318
+ color: "#fff",
319
+ border: "none",
320
+ fontSize: 12,
321
+ fontWeight: 600,
322
+ fontFamily: "inherit",
323
+ letterSpacing: 0.2
324
+ },
325
+ submitBtnDisabled: { cursor: "not-allowed", background: "rgba(79,70,229,0.35)" }
326
+ };
327
+
328
+ // src/ui/theme.ts
329
+ var lightTheme = {
330
+ canvas: "#fafbfc",
331
+ dot: "#dbe3ee",
332
+ nodeFill: "#ffffff",
333
+ nodeStroke: "#cbd5e1",
334
+ nodeSelectedFill: "#eef2ff",
335
+ edgeColor: "#94a3b8",
336
+ textPrimary: "#1e293b",
337
+ textSecondary: "#475569",
338
+ textMuted: "#94a3b8",
339
+ panelBg: "#ffffff",
340
+ panelBorder: "#e2e8f0",
341
+ ctrlsBg: "#ffffff",
342
+ ctrlsBorder: "#cbd5e1",
343
+ inputBg: "#f8fafc",
344
+ inputBorder: "#e2e8f0",
345
+ inputText: "#1e293b",
346
+ cardBg: "#f8fafc",
347
+ cardBorder: "#e2e8f0",
348
+ sectionBorder: "#f1f5f9",
349
+ labelText: "#94a3b8",
350
+ hintText: "#94a3b8",
351
+ statusBg: "#ffffff",
352
+ btnSecBg: "#e2e8f0",
353
+ btnSecText: "#475569",
354
+ shapeBtnBg: "#f1f5f9",
355
+ shapeBtnBorder: "#e2e8f0",
356
+ addFormBg: "#f5f3ff",
357
+ bannerBg: "#f8fafc"
358
+ };
359
+ var darkTheme = {
360
+ canvas: "#0f172a",
361
+ dot: "#1e293b",
362
+ nodeFill: "#1e293b",
363
+ nodeStroke: "#334155",
364
+ nodeSelectedFill: "#1e1b4b",
365
+ edgeColor: "#475569",
366
+ textPrimary: "#f1f5f9",
367
+ textSecondary: "#94a3b8",
368
+ textMuted: "#475569",
369
+ panelBg: "#1e293b",
370
+ panelBorder: "#334155",
371
+ ctrlsBg: "#0f172a",
372
+ ctrlsBorder: "#1e293b",
373
+ inputBg: "#0f172a",
374
+ inputBorder: "#334155",
375
+ inputText: "#e2e8f0",
376
+ cardBg: "#0f172a",
377
+ cardBorder: "#334155",
378
+ sectionBorder: "#0f172a",
379
+ labelText: "#475569",
380
+ hintText: "#475569",
381
+ statusBg: "#0f172a",
382
+ btnSecBg: "#334155",
383
+ btnSecText: "#94a3b8",
384
+ shapeBtnBg: "#0f172a",
385
+ shapeBtnBorder: "#334155",
386
+ addFormBg: "#1e1b4b",
387
+ bannerBg: "#1e293b"
388
+ };
389
+ var ACCENT = {
390
+ indigo: "#4f46e5",
391
+ indigoGlow: "rgba(79,70,229,0.22)",
392
+ indigoLight: "#818cf8",
393
+ indigoText: "#a5b4fc",
394
+ indigoSoftBg: "rgba(79,70,229,0.15)",
395
+ indigoSoftBorder: "rgba(79,70,229,0.3)",
396
+ amber: "#d97706",
397
+ amberLight: "#fef3c7",
398
+ amberBorder: "#fcd34d",
399
+ amberGlow: "rgba(217,119,6,0.25)",
400
+ amberDark: "#fbbf24",
401
+ amberDarkLight: "rgba(251,191,36,0.12)",
402
+ amberDarkBorder: "rgba(251,191,36,0.3)",
403
+ emerald: "#059669",
404
+ emeraldLight: "#ecfdf5",
405
+ emeraldGlow: "rgba(5,150,105,0.2)",
406
+ emeraldDark: "#10b981",
407
+ emeraldDarkLight: "rgba(16,185,129,0.12)",
408
+ emeraldDarkBorder: "rgba(16,185,129,0.3)"
409
+ };
410
+ function variantAccent(variant, isDark) {
411
+ if (variant === "question") {
412
+ return isDark ? { color: ACCENT.amberDark, fill: ACCENT.amberDarkLight, border: ACCENT.amberDarkBorder, glow: ACCENT.amberGlow } : { color: ACCENT.amber, fill: ACCENT.amberLight, border: ACCENT.amberBorder, glow: ACCENT.amberGlow };
413
+ }
414
+ if (variant === "journey") {
415
+ return isDark ? { color: ACCENT.emeraldDark, fill: ACCENT.emeraldDarkLight, border: ACCENT.emeraldDarkBorder, glow: ACCENT.emeraldGlow } : { color: ACCENT.emerald, fill: ACCENT.emeraldLight, border: "#6ee7b7", glow: ACCENT.emeraldGlow };
416
+ }
417
+ return isDark ? { color: "#818cf8", fill: "rgba(79,70,229,0.12)", border: "rgba(79,70,229,0.3)", glow: ACCENT.indigoGlow } : { color: ACCENT.indigo, fill: "#f5f3ff", border: "#c7d2fe", glow: ACCENT.indigoGlow };
418
+ }
419
+
420
+ // src/ui/Toolbar.tsx
421
+ var import_jsx_runtime2 = require("react/jsx-runtime");
422
+ var ALL_FORMATS = [
423
+ { key: "mermaid", label: "Mermaid" },
424
+ { key: "plantuml", label: "PlantUML" },
425
+ { key: "json", label: "JSON" },
426
+ { key: "svg", label: "SVG" },
427
+ { key: "png", label: "PNG" }
428
+ ];
429
+ function Toolbar({ onExport, onImport, allowedExports, allowImport = true }) {
430
+ const [importOpen, setImportOpen] = (0, import_react2.useState)(false);
431
+ const formats = allowedExports ? ALL_FORMATS.filter((f) => allowedExports.includes(f.key)) : ALL_FORMATS;
432
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: bar, children: [
433
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: brand, children: [
434
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: brandDot }),
435
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "flowchart" }),
436
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: darkTheme.textSecondary, fontWeight: 400 }, children: "/" }),
437
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: ACCENT.indigo }, children: "designer" })
438
+ ] }),
439
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: divider }),
440
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 4, alignItems: "center" }, children: [
441
+ allowImport && onImport && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: () => setImportOpen(true), style: ghostBtn, children: "\u2191 Import" }),
442
+ formats.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
443
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: 11, color: darkTheme.inputText, margin: "0 4px" }, children: "Export \u2192" }),
444
+ formats.map((f) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: () => onExport(f.key), style: exportBtn, children: f.label }, f.key))
445
+ ] })
446
+ ] }),
447
+ onImport && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
448
+ ImportDialog,
449
+ {
450
+ open: importOpen,
451
+ onClose: () => setImportOpen(false),
452
+ onImport
453
+ }
454
+ )
455
+ ] });
456
+ }
457
+ var bar = {
458
+ display: "flex",
459
+ alignItems: "center",
460
+ gap: 10,
461
+ padding: "0 14px",
462
+ height: 44,
463
+ background: darkTheme.panelBg,
464
+ borderBottom: `1px solid ${darkTheme.panelBorder}`,
465
+ flexShrink: 0
466
+ };
467
+ var brand = {
468
+ display: "flex",
469
+ alignItems: "center",
470
+ gap: 5,
471
+ fontSize: 13,
472
+ fontWeight: 700,
473
+ color: darkTheme.textPrimary,
474
+ letterSpacing: 0.2,
475
+ fontFamily: "ui-monospace,monospace"
476
+ };
477
+ var brandDot = {
478
+ width: 7,
479
+ height: 7,
480
+ borderRadius: "50%",
481
+ background: ACCENT.indigo,
482
+ boxShadow: `0 0 6px ${ACCENT.indigoLight}`
483
+ };
484
+ var divider = {
485
+ width: 1,
486
+ height: 20,
487
+ background: darkTheme.panelBorder,
488
+ margin: "0 4px"
489
+ };
490
+ var ghostBtn = {
491
+ padding: "4px 10px",
492
+ background: "transparent",
493
+ color: darkTheme.textSecondary,
494
+ border: `1px solid ${darkTheme.panelBorder}`,
495
+ borderRadius: 6,
496
+ cursor: "pointer",
497
+ fontSize: 11,
498
+ fontWeight: 500,
499
+ fontFamily: "inherit",
500
+ letterSpacing: 0.2
501
+ };
502
+ var exportBtn = {
503
+ padding: "4px 10px",
504
+ background: ACCENT.indigoSoftBg,
505
+ color: ACCENT.indigoText,
506
+ border: `1px solid ${ACCENT.indigoSoftBorder}`,
507
+ borderRadius: 6,
508
+ cursor: "pointer",
509
+ fontSize: 11,
510
+ fontWeight: 600,
511
+ fontFamily: "ui-monospace,monospace",
512
+ letterSpacing: 0.3
513
+ };
514
+
515
+ // src/ui/StepEditor.tsx
516
+ var import_react3 = require("react");
517
+
518
+ // src/core/ids.ts
519
+ function makeIdSource(prefix, existing) {
520
+ const first = nextId(prefix, existing);
521
+ let counter = parseInt(first.slice(prefix.length), 10);
522
+ return () => `${prefix}${counter++}`;
523
+ }
524
+ function nextId(prefix, existing) {
525
+ const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
526
+ const re = new RegExp(`^${escaped}(\\d+)$`);
527
+ let max = 0;
528
+ for (const item of existing) {
529
+ const match = re.exec(item.id);
530
+ if (match) {
531
+ const n = parseInt(match[1], 10);
532
+ if (n > max) max = n;
533
+ }
534
+ }
535
+ return `${prefix}${max + 1}`;
536
+ }
537
+
538
+ // src/ui/StepEditor.tsx
539
+ var import_jsx_runtime3 = require("react/jsx-runtime");
540
+ var SHAPES = [
541
+ { key: "rectangle", label: "Box", icon: "\u25AD" },
542
+ { key: "diamond", label: "Decision", icon: "\u25C7" },
543
+ { key: "circle", label: "Circle", icon: "\u25CB" },
544
+ { key: "parallelogram", label: "I/O", icon: "\u25B1" }
545
+ ];
546
+ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDark = false, t, acc }) {
547
+ const isQuestion2 = variant === "question";
548
+ const branchTerm = isQuestion2 ? "Answer" : "Branch";
549
+ const tt = t ?? (isDark ? darkTheme : lightTheme);
550
+ const aa = acc ?? variantAccent(variant, isDark);
551
+ const accentColor = aa.color;
552
+ const accentLight = aa.fill;
553
+ const accentBorder = aa.border;
554
+ const node = model.nodes.find((n) => n.id === nodeId);
555
+ const [label, setLabel] = (0, import_react3.useState)(node?.label ?? "");
556
+ const [addingBranch, setAddingBranch] = (0, import_react3.useState)(false);
557
+ const [branchMode, setBranchMode] = (0, import_react3.useState)("new");
558
+ const [branchLabel, setBranchLabel] = (0, import_react3.useState)("");
559
+ const [branchEdgeLabel, setBranchEdgeLabel] = (0, import_react3.useState)("");
560
+ const [branchTarget, setBranchTarget] = (0, import_react3.useState)("");
561
+ const [newAnswer, setNewAnswer] = (0, import_react3.useState)("");
562
+ const [addingAnswer, setAddingAnswer] = (0, import_react3.useState)(false);
563
+ const inputRef = (0, import_react3.useRef)(null);
564
+ (0, import_react3.useEffect)(() => {
565
+ setLabel(node?.label ?? "");
566
+ setAddingBranch(false);
567
+ setAddingAnswer(false);
568
+ setBranchLabel("");
569
+ setBranchEdgeLabel("");
570
+ setBranchTarget("");
571
+ setNewAnswer("");
572
+ setTimeout(() => inputRef.current?.focus(), 50);
573
+ }, [nodeId]);
574
+ if (!node) return null;
575
+ const outEdges = model.edges.filter((e) => e.from === nodeId);
576
+ const otherNodes = model.nodes.filter((n) => n.id !== nodeId);
577
+ const answers = node.metadata?.answers ?? [];
578
+ const commitLabel = () => {
579
+ if (label === node.label || !label.trim()) return;
580
+ onModelChange({ ...model, nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, label: label.trim() } : n) });
581
+ };
582
+ const setShape = (shape) => {
583
+ onModelChange({ ...model, nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, shape } : n) });
584
+ };
585
+ const removeEdge = (edgeId) => {
586
+ onModelChange({ ...model, edges: model.edges.filter((e) => e.id !== edgeId) });
587
+ };
588
+ const updateEdgeLabel = (edgeId, val) => {
589
+ onModelChange({ ...model, edges: model.edges.map((e) => e.id === edgeId ? { ...e, label: val || void 0 } : e) });
590
+ };
591
+ const addBranch = () => {
592
+ if (branchMode === "new") {
593
+ if (!branchLabel.trim()) return;
594
+ const newId = nextId("node", model.nodes);
595
+ const newNode = { id: newId, label: branchLabel.trim(), shape: "rectangle", x: (node.x ?? 0) + 200, y: (node.y ?? 0) + 20 + outEdges.length * 100 };
596
+ const newEdge = { id: nextId("e", model.edges), from: nodeId, to: newId, label: branchEdgeLabel.trim() || void 0 };
597
+ onModelChange({ ...model, nodes: [...model.nodes, newNode], edges: [...model.edges, newEdge] });
598
+ } else {
599
+ if (!branchTarget || model.edges.some((e) => e.from === nodeId && e.to === branchTarget)) return;
600
+ const newEdge = { id: nextId("e", model.edges), from: nodeId, to: branchTarget, label: branchEdgeLabel.trim() || void 0 };
601
+ onModelChange({ ...model, edges: [...model.edges, newEdge] });
602
+ }
603
+ setBranchLabel("");
604
+ setBranchEdgeLabel("");
605
+ setBranchTarget("");
606
+ setAddingBranch(false);
607
+ };
608
+ const addAnswer = () => {
609
+ const trimmed = newAnswer.trim();
610
+ if (!trimmed || answers.includes(trimmed)) return;
611
+ const updated = [...answers, trimmed];
612
+ onModelChange({ ...model, nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, metadata: { ...n.metadata ?? {}, answers: updated } } : n) });
613
+ setNewAnswer("");
614
+ setAddingAnswer(false);
615
+ };
616
+ const removeAnswer = (ans) => {
617
+ const updated = answers.filter((a) => a !== ans);
618
+ const updatedEdges = model.edges.filter((e) => !(e.from === nodeId && e.label === ans));
619
+ onModelChange({
620
+ ...model,
621
+ nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, metadata: { ...n.metadata ?? {}, answers: updated } } : n),
622
+ edges: updatedEdges
623
+ });
624
+ };
625
+ const moveAnswer = (idx, dir) => {
626
+ const next = idx + dir;
627
+ if (next < 0 || next >= answers.length) return;
628
+ const arr = [...answers];
629
+ [arr[idx], arr[next]] = [arr[next], arr[idx]];
630
+ onModelChange({ ...model, nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, metadata: { ...n.metadata ?? {}, answers: arr } } : n) });
631
+ };
632
+ const inputStyle = {
633
+ width: "100%",
634
+ padding: "7px 10px",
635
+ border: `1.5px solid ${tt.inputBorder}`,
636
+ borderRadius: 8,
637
+ fontSize: 13,
638
+ outline: "none",
639
+ boxSizing: "border-box",
640
+ fontFamily: "inherit",
641
+ color: tt.inputText,
642
+ background: tt.inputBg,
643
+ transition: "border-color 0.15s"
644
+ };
645
+ const addBtnStyle = {
646
+ flex: 1,
647
+ padding: "7px 0",
648
+ background: accentColor,
649
+ color: "#fff",
650
+ border: "none",
651
+ borderRadius: 7,
652
+ cursor: "pointer",
653
+ fontSize: 12,
654
+ fontWeight: 600,
655
+ fontFamily: "inherit"
656
+ };
657
+ const cancelBtnStyle = {
658
+ padding: "7px 14px",
659
+ background: tt.btnSecBg,
660
+ color: tt.btnSecText,
661
+ border: "none",
662
+ borderRadius: 7,
663
+ cursor: "pointer",
664
+ fontSize: 12,
665
+ fontFamily: "inherit"
666
+ };
667
+ const addTriggerStyle = {
668
+ display: "flex",
669
+ alignItems: "center",
670
+ justifyContent: "center",
671
+ gap: 6,
672
+ marginTop: 10,
673
+ width: "100%",
674
+ padding: "9px 0",
675
+ background: "transparent",
676
+ color: accentColor,
677
+ border: `1.5px dashed ${accentBorder}`,
678
+ borderRadius: 10,
679
+ cursor: "pointer",
680
+ fontSize: 12,
681
+ fontWeight: 600,
682
+ fontFamily: "inherit",
683
+ transition: "background 0.15s, border-color 0.15s"
684
+ };
685
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { width: 272, minWidth: 272, background: tt.panelBg, borderLeft: `1px solid ${tt.panelBorder}`, display: "flex", flexDirection: "column", overflow: "hidden" }, children: [
686
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
687
+ padding: "12px 16px",
688
+ fontWeight: 700,
689
+ fontSize: 12,
690
+ letterSpacing: 0.8,
691
+ textTransform: "uppercase",
692
+ color: accentColor,
693
+ borderBottom: `1px solid ${accentBorder}`,
694
+ background: accentLight,
695
+ display: "flex",
696
+ alignItems: "center",
697
+ gap: 8
698
+ }, children: [
699
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { width: 6, height: 6, borderRadius: "50%", background: accentColor } }),
700
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: isQuestion2 ? "Question Editor" : variant === "journey" ? "Step Editor" : "Step Editor" })
701
+ ] }),
702
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { flex: 1, overflowY: "auto", display: "flex", flexDirection: "column" }, children: [
703
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}` }, children: [
704
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: { display: "block", fontSize: 10, fontWeight: 700, color: tt.labelText, marginBottom: 8, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Name" }),
705
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
706
+ "input",
707
+ {
708
+ ref: inputRef,
709
+ value: label,
710
+ onChange: (e) => setLabel(e.target.value),
711
+ onBlur: commitLabel,
712
+ onKeyDown: (e) => e.key === "Enter" && commitLabel(),
713
+ style: inputStyle,
714
+ placeholder: "Step name\u2026"
715
+ }
716
+ )
717
+ ] }),
718
+ !isQuestion2 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}` }, children: [
719
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: { display: "block", fontSize: 10, fontWeight: 700, color: tt.labelText, marginBottom: 8, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Shape" }),
720
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6 }, children: SHAPES.map((s2) => {
721
+ const active = (node.shape ?? "rectangle") === s2.key;
722
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { onClick: () => setShape(s2.key), style: {
723
+ display: "flex",
724
+ flexDirection: "column",
725
+ alignItems: "center",
726
+ gap: 4,
727
+ padding: "8px 6px",
728
+ borderRadius: 8,
729
+ cursor: "pointer",
730
+ transition: "all 0.15s",
731
+ background: active ? accentColor : tt.shapeBtnBg,
732
+ color: active ? "#fff" : tt.textSecondary,
733
+ border: active ? `1.5px solid ${accentColor}` : `1.5px solid ${tt.shapeBtnBorder}`
734
+ }, children: [
735
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: 16, lineHeight: 1 }, children: s2.icon }),
736
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: 11, fontWeight: 500 }, children: s2.label })
737
+ ] }, s2.key);
738
+ }) })
739
+ ] }),
740
+ isQuestion2 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}`, flex: 1 }, children: [
741
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 10 }, children: [
742
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: { display: "block", fontSize: 10, fontWeight: 700, color: tt.labelText, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Answers" }),
743
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: 11, color: tt.textMuted, background: isDark ? "#0f172a" : "#f1f5f9", padding: "1px 7px", borderRadius: 99, fontWeight: 600 }, children: answers.length })
744
+ ] }),
745
+ answers.length === 0 && !addingAnswer && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 12, color: tt.textMuted, textAlign: "center", padding: "16px 0", fontStyle: "italic" }, children: "No answers yet \u2014 add one below" }),
746
+ answers.map((ans, i) => {
747
+ const connected = model.edges.some((e) => e.from === nodeId && e.label === ans);
748
+ const targetNode = model.nodes.find((n) => {
749
+ const e = model.edges.find((ex) => ex.from === nodeId && ex.label === ans);
750
+ return e && n.id === e.to;
751
+ });
752
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", alignItems: "flex-start", gap: 0, marginBottom: 8, borderRadius: 12, border: `1px solid ${tt.cardBorder}`, overflow: "hidden", background: tt.cardBg, boxShadow: isDark ? "none" : "0 1px 2px rgba(15,23,42,0.04)" }, children: [
753
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { width: 4, alignSelf: "stretch", background: accentColor, flexShrink: 0 } }),
754
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { flex: 1, minWidth: 0, padding: "8px 10px" }, children: [
755
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 12, fontWeight: 600, color: tt.textPrimary, marginBottom: connected ? 3 : 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: ans }),
756
+ connected && targetNode && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: 11, color: accentColor, opacity: 0.85 }, children: [
757
+ "\u2192 ",
758
+ targetNode.label
759
+ ] }),
760
+ !connected && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 10, color: tt.textMuted, fontStyle: "italic" }, children: "drag port to connect" })
761
+ ] }),
762
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", padding: "4px 2px", gap: 2 }, children: [
763
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => moveAnswer(i, -1), disabled: i === 0, style: { background: "none", border: "none", color: tt.textMuted, cursor: "pointer", fontSize: 11, padding: "2px 4px", opacity: i === 0 ? 0.3 : 1 }, children: "\u2191" }),
764
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => moveAnswer(i, 1), disabled: i === answers.length - 1, style: { background: "none", border: "none", color: tt.textMuted, cursor: "pointer", fontSize: 11, padding: "2px 4px", opacity: i === answers.length - 1 ? 0.3 : 1 }, children: "\u2193" })
765
+ ] }),
766
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => removeAnswer(ans), style: { background: "none", border: "none", color: tt.textMuted, cursor: "pointer", fontSize: 12, padding: "8px 10px", flexShrink: 0 }, title: "Remove", children: "\u2715" })
767
+ ] }, ans + i);
768
+ }),
769
+ addingAnswer ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginTop: 10, background: tt.addFormBg, borderRadius: 10, padding: 12, border: `1.5px solid ${accentBorder}` }, children: [
770
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { autoFocus: true, value: newAnswer, onChange: (e) => setNewAnswer(e.target.value), onKeyDown: (e) => e.key === "Enter" && addAnswer(), placeholder: "Answer text\u2026", style: { ...inputStyle, marginBottom: 8 } }),
771
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: 6 }, children: [
772
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: addAnswer, style: addBtnStyle, children: "Add Answer" }),
773
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => {
774
+ setAddingAnswer(false);
775
+ setNewAnswer("");
776
+ }, style: cancelBtnStyle, children: "Cancel" })
777
+ ] })
778
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { onClick: () => setAddingAnswer(true), style: addTriggerStyle, children: [
779
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: 16, lineHeight: 1 }, children: "+" }),
780
+ " Add Answer"
781
+ ] }),
782
+ answers.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginTop: 12, padding: "8px 10px", background: isDark ? "rgba(251,191,36,0.06)" : "#fef9f0", borderRadius: 8, border: `1px solid ${accentBorder}` }, children: [
783
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 10, fontWeight: 700, color: accentColor, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 4 }, children: "How to connect" }),
784
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 11, color: tt.textSecondary, lineHeight: 1.5 }, children: "Hover the question node on the canvas \u2014 drag an answer's port dot to any other node." })
785
+ ] })
786
+ ] }),
787
+ !isQuestion2 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}`, flex: 1 }, children: [
788
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 10 }, children: [
789
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: { display: "block", fontSize: 10, fontWeight: 700, color: tt.labelText, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Branches" }),
790
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: 11, color: tt.textMuted, background: isDark ? "#0f172a" : "#f1f5f9", padding: "1px 7px", borderRadius: 99, fontWeight: 600 }, children: outEdges.length })
791
+ ] }),
792
+ outEdges.length === 0 && !addingBranch && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 12, color: tt.textMuted, textAlign: "center", padding: "16px 0", fontStyle: "italic" }, children: "No outgoing connections yet" }),
793
+ outEdges.map((edge) => {
794
+ const target = model.nodes.find((n) => n.id === edge.to);
795
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", alignItems: "flex-start", gap: 0, marginBottom: 8, borderRadius: 12, border: `1px solid ${tt.cardBorder}`, overflow: "hidden", background: tt.cardBg, boxShadow: isDark ? "none" : "0 1px 2px rgba(15,23,42,0.04)" }, children: [
796
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { width: 4, alignSelf: "stretch", background: accentColor, flexShrink: 0 } }),
797
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { flex: 1, minWidth: 0, padding: "8px 10px" }, children: [
798
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: 12, fontWeight: 600, color: tt.textPrimary, marginBottom: 5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [
799
+ "\u2192 ",
800
+ target?.label ?? edge.to
801
+ ] }),
802
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { value: edge.label ?? "", onChange: (e) => updateEdgeLabel(edge.id, e.target.value), placeholder: "Edge label (optional)", style: { ...inputStyle, fontSize: 11, padding: "4px 8px" } })
803
+ ] }),
804
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => removeEdge(edge.id), style: { background: "none", border: "none", color: tt.textMuted, cursor: "pointer", fontSize: 12, padding: "8px 10px", flexShrink: 0 }, title: "Remove", children: "\u2715" })
805
+ ] }, edge.id);
806
+ }),
807
+ addingBranch ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginTop: 10, background: tt.addFormBg, borderRadius: 10, padding: 12, border: `1.5px solid ${accentBorder}` }, children: [
808
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "flex", gap: 6, marginBottom: 10 }, children: ["new", "existing"].map((mode) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => setBranchMode(mode), style: {
809
+ flex: 1,
810
+ padding: "5px 0",
811
+ border: "none",
812
+ borderRadius: 6,
813
+ cursor: "pointer",
814
+ fontSize: 11,
815
+ fontWeight: 600,
816
+ background: branchMode === mode ? accentColor : tt.btnSecBg,
817
+ color: branchMode === mode ? "#fff" : tt.btnSecText
818
+ }, children: mode === "new" ? `+ New step` : "Existing step" }, mode)) }),
819
+ branchMode === "new" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { autoFocus: true, value: branchLabel, onChange: (e) => setBranchLabel(e.target.value), onKeyDown: (e) => e.key === "Enter" && addBranch(), placeholder: "New step name\u2026", style: { ...inputStyle, marginBottom: 6 } }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("select", { value: branchTarget, onChange: (e) => setBranchTarget(e.target.value), style: { ...inputStyle, marginBottom: 6, appearance: "none" }, children: [
820
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "", children: "Choose a step\u2026" }),
821
+ otherNodes.map((n) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: n.id, children: n.label }, n.id))
822
+ ] }),
823
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { value: branchEdgeLabel, onChange: (e) => setBranchEdgeLabel(e.target.value), placeholder: "Edge label (optional)", style: { ...inputStyle, marginBottom: 10 } }),
824
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: 6 }, children: [
825
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { onClick: addBranch, style: addBtnStyle, children: [
826
+ "Add ",
827
+ branchTerm
828
+ ] }),
829
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => setAddingBranch(false), style: cancelBtnStyle, children: "Cancel" })
830
+ ] })
831
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { onClick: () => setAddingBranch(true), style: addTriggerStyle, children: [
832
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: 16, lineHeight: 1 }, children: "+" }),
833
+ " Add ",
834
+ branchTerm
835
+ ] })
836
+ ] })
837
+ ] })
838
+ ] });
839
+ }
840
+
841
+ // src/ui/SequenceEditor.tsx
842
+ var import_react8 = require("react");
843
+
844
+ // src/ui/hooks/useEditorTheme.ts
845
+ var import_react5 = require("react");
846
+
847
+ // src/ui/hooks/useSystemTheme.ts
848
+ var import_react4 = require("react");
849
+ function useIsDark(theme) {
850
+ const [sysDark, setSysDark] = (0, import_react4.useState)(
851
+ () => typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)").matches : false
852
+ );
853
+ (0, import_react4.useEffect)(() => {
854
+ if (theme !== "auto" || typeof window === "undefined") return;
855
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
856
+ const handler = (e) => setSysDark(e.matches);
857
+ mq.addEventListener("change", handler);
858
+ return () => mq.removeEventListener("change", handler);
859
+ }, [theme]);
860
+ return theme === "dark" || theme === "auto" && sysDark;
861
+ }
862
+ function useIsCoarsePointer() {
863
+ const [coarse, setCoarse] = (0, import_react4.useState)(
864
+ () => typeof window !== "undefined" ? window.matchMedia("(pointer: coarse)").matches : false
865
+ );
866
+ (0, import_react4.useEffect)(() => {
867
+ if (typeof window === "undefined") return;
868
+ const mq = window.matchMedia("(pointer: coarse)");
869
+ const handler = (e) => setCoarse(e.matches);
870
+ mq.addEventListener("change", handler);
871
+ return () => mq.removeEventListener("change", handler);
872
+ }, []);
873
+ return coarse;
874
+ }
875
+ function usePrefersReducedMotion() {
876
+ const [reduced, setReduced] = (0, import_react4.useState)(
877
+ () => typeof window !== "undefined" ? window.matchMedia("(prefers-reduced-motion: reduce)").matches : false
878
+ );
879
+ (0, import_react4.useEffect)(() => {
880
+ if (typeof window === "undefined") return;
881
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
882
+ const handler = (e) => setReduced(e.matches);
883
+ mq.addEventListener("change", handler);
884
+ return () => mq.removeEventListener("change", handler);
885
+ }, []);
886
+ return reduced;
887
+ }
888
+
889
+ // src/ui/hooks/useEditorTheme.ts
890
+ function useEditorTheme(theme, overrides, palettes) {
891
+ const isDark = useIsDark(theme);
892
+ const t = (0, import_react5.useMemo)(
893
+ () => ({ ...isDark ? palettes.dark : palettes.light, ...overrides ?? {} }),
894
+ // palettes is a stable module-level constant in every caller, so it is
895
+ // deliberately omitted from the dep array to keep the memo key tight.
896
+ // eslint-disable-next-line react-hooks/exhaustive-deps
897
+ [isDark, overrides]
898
+ );
899
+ return { t, isDark };
900
+ }
901
+
902
+ // src/ui/hooks/useExporters.ts
903
+ var import_react6 = require("react");
904
+
905
+ // src/exporters/mermaid.ts
906
+ var SHAPE_OPEN = {
907
+ rectangle: "[",
908
+ diamond: "{",
909
+ circle: "((",
910
+ parallelogram: "[/"
911
+ };
912
+ var SHAPE_CLOSE = {
913
+ rectangle: "]",
914
+ diamond: "}",
915
+ circle: "))",
916
+ parallelogram: "/]"
917
+ };
918
+ function formatNode(node) {
919
+ const shape = node.shape ?? "rectangle";
920
+ const open = SHAPE_OPEN[shape] ?? "[";
921
+ const close = SHAPE_CLOSE[shape] ?? "]";
922
+ return ` ${node.id}${open}"${node.label}"${close}`;
923
+ }
924
+ function edgeArrow(edge) {
925
+ const style = edge.style ?? "solid";
926
+ const arrowhead = edge.arrowhead ?? "arrow";
927
+ if (style === "dashed" || style === "dotted") return arrowhead === "none" ? "-.-" : "-.->";
928
+ return arrowhead === "none" ? "---" : "-->";
929
+ }
930
+ function formatEdge(edge) {
931
+ const arrow = edgeArrow(edge);
932
+ return edge.label ? ` ${edge.from} ${arrow}|"${edge.label}"| ${edge.to}` : ` ${edge.from} ${arrow} ${edge.to}`;
933
+ }
934
+ function exportFlowchart(model) {
935
+ const lines = ["graph TD"];
936
+ if (model.title) lines.unshift(`---
937
+ title: ${model.title}
938
+ ---`);
939
+ for (const node of model.nodes) lines.push(formatNode(node));
940
+ for (const edge of model.edges) lines.push(formatEdge(edge));
941
+ return lines.join("\n");
942
+ }
943
+ function msgArrow(msg) {
944
+ return msg.style === "dashed" ? "-->>" : "->>";
945
+ }
946
+ function exportSequence(model) {
947
+ const lines = ["sequenceDiagram"];
948
+ if (model.title) lines.unshift(`---
949
+ title: ${model.title}
950
+ ---`);
951
+ for (const actor of model.actors ?? []) lines.push(` participant ${actor}`);
952
+ for (const msg of model.messages ?? []) {
953
+ lines.push(` ${msg.from}${msgArrow(msg)}${msg.to}: ${msg.label}`);
954
+ }
955
+ return lines.join("\n");
956
+ }
957
+ function toMermaid(model) {
958
+ return model.type === "sequence" ? exportSequence(model) : exportFlowchart(model);
959
+ }
960
+
961
+ // src/exporters/plantuml.ts
962
+ function nodeShape(node) {
963
+ switch (node.shape) {
964
+ case "diamond":
965
+ return ["<>", "<>"];
966
+ case "circle":
967
+ return ["(", ")"];
968
+ case "parallelogram":
969
+ return ["/", "/"];
970
+ default:
971
+ return ["[", "]"];
972
+ }
973
+ }
974
+ function exportFlowchart2(model) {
975
+ const lines = ["@startuml"];
976
+ if (model.title) lines.push(`title ${model.title}`);
977
+ lines.push("");
978
+ for (const node of model.nodes) {
979
+ const [open, close] = nodeShape(node);
980
+ lines.push(`state "${node.label}" as ${node.id} ${open}${close}`);
981
+ }
982
+ lines.push("");
983
+ for (const edge of model.edges) {
984
+ const arrow = edge.style === "dashed" ? "-[dashed]->" : edge.style === "dotted" ? "-[dotted]->" : "-->";
985
+ const label = edge.label ? ` : ${edge.label}` : "";
986
+ lines.push(`${edge.from} ${arrow} ${edge.to}${label}`);
987
+ }
988
+ lines.push("@enduml");
989
+ return lines.join("\n");
990
+ }
991
+ function msgArrow2(msg) {
992
+ return msg.style === "dashed" ? "-->" : "->";
993
+ }
994
+ function exportSequence2(model) {
995
+ const lines = ["@startuml"];
996
+ if (model.title) lines.push(`title ${model.title}`);
997
+ lines.push("");
998
+ for (const actor of model.actors ?? []) {
999
+ lines.push(`participant ${actor}`);
1000
+ }
1001
+ lines.push("");
1002
+ for (const msg of model.messages ?? []) {
1003
+ lines.push(`${msg.from} ${msgArrow2(msg)} ${msg.to} : ${msg.label}`);
1004
+ }
1005
+ lines.push("@enduml");
1006
+ return lines.join("\n");
1007
+ }
1008
+ function toPlantUML(model) {
1009
+ return model.type === "sequence" ? exportSequence2(model) : exportFlowchart2(model);
1010
+ }
1011
+
1012
+ // src/exporters/json.ts
1013
+ function toJSON(model) {
1014
+ return JSON.stringify(model, null, 2);
1015
+ }
1016
+
1017
+ // src/exporters/svg.ts
1018
+ var NODE_H = 48;
1019
+ var Q_BASE_H = 68;
1020
+ var Q_ANS_ROW_H = 80;
1021
+ var Q_CARD_PAD = 8;
1022
+ var MIN_NODE_W = 120;
1023
+ var MAX_NODE_W = 320;
1024
+ var MIN_Q_W = 220;
1025
+ var PADDING = 48;
1026
+ var H_GAP = 80;
1027
+ var V_GAP = 96;
1028
+ function estimateTextW(text, pxPerChar = 7.5) {
1029
+ return text.length * pxPerChar;
1030
+ }
1031
+ function nodeWidth(label) {
1032
+ return Math.min(MAX_NODE_W, Math.max(MIN_NODE_W, Math.ceil(estimateTextW(label) + 48)));
1033
+ }
1034
+ function answerCardW(ans) {
1035
+ return Math.max(86, Math.ceil(Math.max(estimateTextW(ans, 7.5) + 20, 56) + 32));
1036
+ }
1037
+ function questionNodeW(node) {
1038
+ const answers = node.metadata?.answers ?? [];
1039
+ const headerW = estimateTextW(node.label, 8) + 80;
1040
+ if (answers.length === 0) return Math.max(MIN_Q_W, Math.ceil(headerW));
1041
+ const cardsW = answers.reduce((s2, a) => s2 + answerCardW(a), 0) + (answers.length - 1) * Q_CARD_PAD + 2 * Q_CARD_PAD;
1042
+ return Math.max(MIN_Q_W, Math.ceil(Math.max(headerW, cardsW)));
1043
+ }
1044
+ function questionNodeH(answers) {
1045
+ return Q_BASE_H + (answers.length === 0 ? 48 : Q_ANS_ROW_H);
1046
+ }
1047
+ function bezierPath(x1, y1, x2, y2) {
1048
+ const dy = y2 - y1;
1049
+ const dyAbs = Math.abs(dy);
1050
+ const dxAbs = Math.abs(x2 - x1);
1051
+ const base = dy > 0 ? dyAbs * 0.55 : Math.max(90, dyAbs * 0.5 + dxAbs * 0.28);
1052
+ const curve = Math.max(36, Math.min(220, base));
1053
+ return `M ${x1} ${y1} C ${x1} ${y1 + curve}, ${x2} ${y2 - curve}, ${x2} ${y2}`;
1054
+ }
1055
+ function isQuestion(node, variant) {
1056
+ return variant === "question" && !!node.metadata?.answers;
1057
+ }
1058
+ function computeLayout(model) {
1059
+ const boxes = /* @__PURE__ */ new Map();
1060
+ const sized = model.nodes.map((n) => {
1061
+ const w = isQuestion(n, model.variant) ? questionNodeW(n) : nodeWidth(n.label);
1062
+ const h = isQuestion(n, model.variant) ? questionNodeH(n.metadata?.answers ?? []) : NODE_H;
1063
+ return { node: n, w, h };
1064
+ });
1065
+ const allPositioned = sized.every((s2) => typeof s2.node.x === "number" && typeof s2.node.y === "number");
1066
+ if (allPositioned) {
1067
+ for (const s2 of sized) {
1068
+ boxes.set(s2.node.id, { x: s2.node.x, y: s2.node.y, w: s2.w, h: s2.h });
1069
+ }
1070
+ return boxes;
1071
+ }
1072
+ const inDeg = new Map(model.nodes.map((n) => [n.id, 0]));
1073
+ for (const e of model.edges) inDeg.set(e.to, (inDeg.get(e.to) ?? 0) + 1);
1074
+ const layers = /* @__PURE__ */ new Map();
1075
+ const queue = model.nodes.filter((n) => (inDeg.get(n.id) ?? 0) === 0).map((n) => n.id);
1076
+ for (const id of queue) layers.set(id, 0);
1077
+ let head = 0;
1078
+ while (head < queue.length) {
1079
+ const cur = queue[head++];
1080
+ const layer = layers.get(cur) ?? 0;
1081
+ for (const e of model.edges) {
1082
+ if (e.from === cur) {
1083
+ const next = layers.get(e.to) ?? -1;
1084
+ if (next < layer + 1) {
1085
+ layers.set(e.to, layer + 1);
1086
+ queue.push(e.to);
1087
+ }
1088
+ }
1089
+ }
1090
+ }
1091
+ model.nodes.forEach((n) => {
1092
+ if (!layers.has(n.id)) layers.set(n.id, 0);
1093
+ });
1094
+ const byLayer = /* @__PURE__ */ new Map();
1095
+ for (const s2 of sized) {
1096
+ const layer = layers.get(s2.node.id) ?? 0;
1097
+ if (!byLayer.has(layer)) byLayer.set(layer, []);
1098
+ byLayer.get(layer).push(s2);
1099
+ }
1100
+ let y = PADDING;
1101
+ for (const layer of [...byLayer.keys()].sort((a, b) => a - b)) {
1102
+ const row = byLayer.get(layer);
1103
+ let x = PADDING;
1104
+ let maxH = 0;
1105
+ for (const s2 of row) {
1106
+ boxes.set(s2.node.id, { x, y, w: s2.w, h: s2.h });
1107
+ x += s2.w + H_GAP;
1108
+ maxH = Math.max(maxH, s2.h);
1109
+ }
1110
+ y += maxH + V_GAP;
1111
+ }
1112
+ return boxes;
1113
+ }
1114
+ function escapeXML(s2) {
1115
+ return s2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1116
+ }
1117
+ var COLORS = {
1118
+ bg: "#fafbfc",
1119
+ dot: "#dbe3ee",
1120
+ nodeFill: "#ffffff",
1121
+ nodeStroke: "#cbd5e1",
1122
+ edge: "#94a3b8",
1123
+ text: "#1e293b",
1124
+ textSub: "#94a3b8",
1125
+ amber: "#d97706",
1126
+ amberSoft: "#fef9ee",
1127
+ amberLine: "#fde68a",
1128
+ amberCardBg: "#fffdf7"
1129
+ };
1130
+ function renderStandardNode(node, box) {
1131
+ const cx = box.x + box.w / 2;
1132
+ const cy = box.y + box.h / 2;
1133
+ const shape = node.shape ?? "rectangle";
1134
+ const label = `<text x="${cx}" y="${cy + 4.5}" text-anchor="middle" font-family="ui-sans-serif,system-ui,-apple-system,sans-serif" font-size="13" font-weight="500" fill="${COLORS.text}">${escapeXML(node.label)}</text>`;
1135
+ let shapeEl = "";
1136
+ if (shape === "diamond") {
1137
+ const pts = `${cx},${box.y} ${box.x + box.w},${cy} ${cx},${box.y + box.h} ${box.x},${cy}`;
1138
+ shapeEl = `<polygon points="${pts}" fill="${COLORS.nodeFill}" stroke="${COLORS.nodeStroke}" stroke-width="1.25" filter="url(#nodeShadow)"/>`;
1139
+ } else if (shape === "circle") {
1140
+ const r = Math.min(box.w, box.h) / 2 - 1;
1141
+ shapeEl = `<circle cx="${cx}" cy="${cy}" r="${r}" fill="${COLORS.nodeFill}" stroke="${COLORS.nodeStroke}" stroke-width="1.25" filter="url(#nodeShadow)"/>`;
1142
+ } else if (shape === "parallelogram") {
1143
+ const pts = `${box.x + 14},${box.y} ${box.x + box.w},${box.y} ${box.x + box.w - 14},${box.y + box.h} ${box.x},${box.y + box.h}`;
1144
+ shapeEl = `<polygon points="${pts}" fill="${COLORS.nodeFill}" stroke="${COLORS.nodeStroke}" stroke-width="1.25" filter="url(#nodeShadow)"/>`;
1145
+ } else {
1146
+ shapeEl = `<rect x="${box.x}" y="${box.y}" width="${box.w}" height="${box.h}" rx="14" fill="${COLORS.nodeFill}" stroke="${COLORS.nodeStroke}" stroke-width="1.25" filter="url(#nodeShadow)"/>`;
1147
+ }
1148
+ return shapeEl + label;
1149
+ }
1150
+ function renderQuestionNode(node, box) {
1151
+ const answers = node.metadata?.answers ?? [];
1152
+ const clipId = `qhdr-${node.id.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
1153
+ const x = box.x, y = box.y, w = box.w, h = box.h;
1154
+ const parts = [];
1155
+ parts.push(`<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="14" fill="${COLORS.nodeFill}" stroke="${COLORS.amberLine}" stroke-width="1.5" filter="url(#nodeShadow)"/>`);
1156
+ parts.push(`<defs><clipPath id="${clipId}"><rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" rx="14"/></clipPath></defs>`);
1157
+ parts.push(`<rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" fill="${COLORS.amberSoft}" clip-path="url(#${clipId})"/>`);
1158
+ parts.push(`<rect x="${x}" y="${y}" width="4" height="${Q_BASE_H}" rx="2" fill="${COLORS.amber}"/>`);
1159
+ parts.push(`<rect x="${x + 12}" y="${y + 14}" width="28" height="28" rx="8" fill="${COLORS.amber}"/>`);
1160
+ parts.push(`<text x="${x + 26}" y="${y + 33}" text-anchor="middle" font-size="15" font-weight="900" fill="white">?</text>`);
1161
+ parts.push(`<text x="${x + 50}" y="${y + 27}" font-family="ui-sans-serif,system-ui,sans-serif" font-size="9" font-weight="700" fill="${COLORS.textSub}" letter-spacing="0.6">QUESTION</text>`);
1162
+ parts.push(`<text x="${x + 50}" y="${y + 42}" font-family="ui-sans-serif,system-ui,sans-serif" font-size="13" font-weight="700" fill="${COLORS.text}">${escapeXML(node.label)}</text>`);
1163
+ parts.push(`<line x1="${x}" y1="${y + Q_BASE_H}" x2="${x + w}" y2="${y + Q_BASE_H}" stroke="${COLORS.amberLine}" stroke-width="1"/>`);
1164
+ if (answers.length === 0) {
1165
+ parts.push(`<text x="${x + w / 2}" y="${y + Q_BASE_H + 22}" text-anchor="middle" font-size="10" fill="${COLORS.amber}" opacity="0.4" font-weight="600">No answers yet</text>`);
1166
+ } else {
1167
+ const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1168
+ answers.forEach((ans, i) => {
1169
+ const prevW = answers.slice(0, i).reduce((s2, a) => s2 + answerCardW(a) + Q_CARD_PAD, 0);
1170
+ const cW = answerCardW(ans);
1171
+ const cardX = x + Q_CARD_PAD + prevW;
1172
+ const cardY = y + Q_BASE_H + 7;
1173
+ const cardH = Q_ANS_ROW_H - 20;
1174
+ const cx = cardX + cW / 2;
1175
+ const letter = i < 26 ? letters[i] : `${i + 1}`;
1176
+ const maxChars = Math.max(2, Math.floor((cW - 20) / 7.5));
1177
+ const displayAns = ans.length > maxChars ? ans.slice(0, maxChars - 1) + "\u2026" : ans;
1178
+ parts.push(`<rect x="${cardX}" y="${cardY}" width="${cW}" height="${cardH}" rx="8" fill="${COLORS.amberCardBg}" stroke="${COLORS.amberLine}" stroke-width="1"/>`);
1179
+ parts.push(`<rect x="${cx - 11}" y="${cardY + 7}" width="22" height="22" rx="6" fill="#fef3c7"/>`);
1180
+ parts.push(`<text x="${cx}" y="${cardY + 22}" text-anchor="middle" font-size="10" font-weight="800" fill="${COLORS.amber}">${escapeXML(letter)}</text>`);
1181
+ parts.push(`<text x="${cx}" y="${cardY + 46}" text-anchor="middle" font-size="11" font-weight="500" fill="#374151" font-family="ui-sans-serif,system-ui,sans-serif">${escapeXML(displayAns)}</text>`);
1182
+ });
1183
+ }
1184
+ return parts.join("");
1185
+ }
1186
+ function renderEdge(edge, boxes, variant, nodes) {
1187
+ const fromBox = boxes.get(edge.from);
1188
+ const toBox = boxes.get(edge.to);
1189
+ if (!fromBox || !toBox) return "";
1190
+ let x1, y1;
1191
+ const fromNode = nodes.find((n) => n.id === edge.from);
1192
+ if (fromNode && isQuestion(fromNode, variant)) {
1193
+ const answers = fromNode.metadata?.answers ?? [];
1194
+ const idx = answers.indexOf(edge.label ?? "");
1195
+ if (idx >= 0) {
1196
+ const prevW = answers.slice(0, idx).reduce((s2, a) => s2 + answerCardW(a) + Q_CARD_PAD, 0);
1197
+ const cW = answerCardW(answers[idx]);
1198
+ x1 = fromBox.x + Q_CARD_PAD + prevW + cW / 2;
1199
+ y1 = fromBox.y + Q_BASE_H + Q_ANS_ROW_H - 8;
1200
+ } else {
1201
+ x1 = fromBox.x + fromBox.w / 2;
1202
+ y1 = fromBox.y + fromBox.h;
1203
+ }
1204
+ } else {
1205
+ x1 = fromBox.x + fromBox.w / 2;
1206
+ y1 = fromBox.y + fromBox.h;
1207
+ }
1208
+ const x2 = toBox.x + toBox.w / 2;
1209
+ const y2 = toBox.y;
1210
+ const dash = edge.style === "dashed" ? ' stroke-dasharray="6,4"' : edge.style === "dotted" ? ' stroke-dasharray="2,3"' : "";
1211
+ const marker = edge.arrowhead === "none" ? "" : ' marker-end="url(#arrow)"';
1212
+ const d = bezierPath(x1, y1, x2, y2);
1213
+ let out = `<path d="${d}" fill="none" stroke="${COLORS.edge}" stroke-width="1.5"${dash}${marker}/>`;
1214
+ if (edge.label) {
1215
+ const midX = (x1 + x2) / 2;
1216
+ const midY = (y1 + y2) / 2;
1217
+ const labelW = estimateTextW(edge.label, 7) + 14;
1218
+ out += `<rect x="${midX - labelW / 2}" y="${midY - 11}" width="${labelW}" height="18" rx="9" fill="${COLORS.bg}" stroke="${COLORS.nodeStroke}" stroke-width="1"/>`;
1219
+ out += `<text x="${midX}" y="${midY + 2}" text-anchor="middle" font-family="ui-sans-serif,system-ui,sans-serif" font-size="11" fill="${COLORS.text}">${escapeXML(edge.label)}</text>`;
1220
+ }
1221
+ return out;
1222
+ }
1223
+ function toSVG(model) {
1224
+ const boxes = computeLayout(model);
1225
+ let maxX = 0, maxY = 0;
1226
+ for (const b of boxes.values()) {
1227
+ maxX = Math.max(maxX, b.x + b.w);
1228
+ maxY = Math.max(maxY, b.y + b.h);
1229
+ }
1230
+ const width = maxX + PADDING;
1231
+ const height = maxY + PADDING + (model.title ? 32 : 0);
1232
+ const defs = [
1233
+ `<defs>`,
1234
+ `<pattern id="dotgrid" x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse">`,
1235
+ `<circle cx="12" cy="12" r="1.1" fill="${COLORS.dot}"/>`,
1236
+ `</pattern>`,
1237
+ `<filter id="nodeShadow" x="-20%" y="-20%" width="140%" height="140%">`,
1238
+ `<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="rgba(15,23,42,0.09)"/>`,
1239
+ `</filter>`,
1240
+ `<marker id="arrow" markerWidth="9" markerHeight="7" refX="8.5" refY="3.5" orient="auto" markerUnits="strokeWidth">`,
1241
+ `<path d="M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z" fill="${COLORS.edge}"/>`,
1242
+ `</marker>`,
1243
+ `</defs>`
1244
+ ].join("");
1245
+ const titleEl = model.title ? `<text x="${width / 2}" y="22" text-anchor="middle" font-family="ui-sans-serif,system-ui,sans-serif" font-size="15" font-weight="700" fill="${COLORS.text}">${escapeXML(model.title)}</text>` : "";
1246
+ const edges = model.edges.map((e) => renderEdge(e, boxes, model.variant, model.nodes)).join("\n");
1247
+ const nodes = model.nodes.map((n) => {
1248
+ const b = boxes.get(n.id);
1249
+ return isQuestion(n, model.variant) ? renderQuestionNode(n, b) : renderStandardNode(n, b);
1250
+ }).join("\n");
1251
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
1252
+ ${defs}
1253
+ <rect width="${width}" height="${height}" fill="${COLORS.bg}"/>
1254
+ <rect width="${width}" height="${height}" fill="url(#dotgrid)"/>
1255
+ ${titleEl}
1256
+ ${edges}
1257
+ ${nodes}
1258
+ </svg>`;
1259
+ }
1260
+ async function toPNG(model) {
1261
+ if (typeof document === "undefined") {
1262
+ throw new Error("toPNG requires a browser environment. For Node/Bun server use, pipe toSVG() through @resvg/resvg-js.");
1263
+ }
1264
+ const svg = toSVG(model);
1265
+ const blob = new Blob([svg], { type: "image/svg+xml" });
1266
+ const url = URL.createObjectURL(blob);
1267
+ return new Promise((resolve, reject) => {
1268
+ const img = new Image();
1269
+ img.onload = () => {
1270
+ const canvas = document.createElement("canvas");
1271
+ const scale = window.devicePixelRatio || 2;
1272
+ canvas.width = img.naturalWidth * scale;
1273
+ canvas.height = img.naturalHeight * scale;
1274
+ const ctx = canvas.getContext("2d");
1275
+ ctx.scale(scale, scale);
1276
+ ctx.drawImage(img, 0, 0);
1277
+ URL.revokeObjectURL(url);
1278
+ canvas.toBlob((b) => b ? resolve(b) : reject(new Error("Canvas toBlob failed")), "image/png");
1279
+ };
1280
+ img.onerror = () => {
1281
+ URL.revokeObjectURL(url);
1282
+ reject(new Error("SVG image load failed"));
1283
+ };
1284
+ img.src = url;
1285
+ });
1286
+ }
1287
+
1288
+ // src/ui/hooks/useExporters.ts
1289
+ function useExporters(model, onExport, filename = "diagram") {
1290
+ return (0, import_react6.useCallback)(async (format) => {
1291
+ let content;
1292
+ switch (format) {
1293
+ case "mermaid":
1294
+ content = toMermaid(model);
1295
+ break;
1296
+ case "plantuml":
1297
+ content = toPlantUML(model);
1298
+ break;
1299
+ case "json":
1300
+ content = toJSON(model);
1301
+ break;
1302
+ case "svg":
1303
+ content = toSVG(model);
1304
+ break;
1305
+ case "png":
1306
+ content = await toPNG(model);
1307
+ break;
1308
+ default:
1309
+ return;
1310
+ }
1311
+ if (onExport) {
1312
+ onExport(format, content);
1313
+ return;
1314
+ }
1315
+ const url = content instanceof Blob ? URL.createObjectURL(content) : URL.createObjectURL(new Blob([content], { type: "text/plain" }));
1316
+ const a = document.createElement("a");
1317
+ a.href = url;
1318
+ a.download = `${filename}.${format === "plantuml" ? "puml" : format}`;
1319
+ a.click();
1320
+ URL.revokeObjectURL(url);
1321
+ }, [model, onExport, filename]);
1322
+ }
1323
+
1324
+ // src/ui/hooks/useImporter.ts
1325
+ var import_react7 = require("react");
1326
+
1327
+ // src/core/model.ts
1328
+ var Model = class _Model {
1329
+ data;
1330
+ /**
1331
+ * Create an empty model.
1332
+ *
1333
+ * @param type Top-level kind — `flowchart` or `sequence`.
1334
+ * @param title Optional human-readable title.
1335
+ * @param variant Optional UI variant (flowchart models only).
1336
+ */
1337
+ constructor(type, title, variant) {
1338
+ this.data = { type, ...variant ? { variant } : {}, title, nodes: [], edges: [], actors: [], messages: [] };
1339
+ }
1340
+ /**
1341
+ * Rehydrate a `Model` from a previously serialized `DiagramModel`. The
1342
+ * incoming data is deep-cloned, so future mutations on the returned `Model`
1343
+ * do not affect the caller's object.
1344
+ */
1345
+ static fromData(data) {
1346
+ const m = new _Model(data.type, data.title, data.variant);
1347
+ m.data = structuredClone(data);
1348
+ return m;
1349
+ }
1350
+ /** Set the UI variant. No-op semantics for sequence models. */
1351
+ setVariant(variant) {
1352
+ this.data.variant = variant;
1353
+ return this;
1354
+ }
1355
+ /**
1356
+ * Append a node. Throws if a node with the same id already exists. The
1357
+ * input is shallow-cloned, so later mutations of the caller's object do
1358
+ * not leak in.
1359
+ */
1360
+ addNode(node) {
1361
+ if (this.data.nodes.find((n) => n.id === node.id)) {
1362
+ throw new Error(`Node with id "${node.id}" already exists`);
1363
+ }
1364
+ this.data.nodes.push({ ...node });
1365
+ return this;
1366
+ }
1367
+ /**
1368
+ * Patch an existing node in place. Throws if the id is not found. The id
1369
+ * field itself cannot be patched — to rename, remove + re-add.
1370
+ */
1371
+ updateNode(id, patch) {
1372
+ const node = this.data.nodes.find((n) => n.id === id);
1373
+ if (!node) throw new Error(`Node "${id}" not found`);
1374
+ Object.assign(node, patch);
1375
+ return this;
1376
+ }
1377
+ /**
1378
+ * Remove a node and every edge that referenced it as `from` or `to`. Safe
1379
+ * to call on a missing id (no-op).
1380
+ */
1381
+ removeNode(id) {
1382
+ this.data.nodes = this.data.nodes.filter((n) => n.id !== id);
1383
+ this.data.edges = this.data.edges.filter((e) => e.from !== id && e.to !== id);
1384
+ return this;
1385
+ }
1386
+ /**
1387
+ * Append an edge. Throws on duplicate id or if either endpoint references
1388
+ * an unknown node — the model never holds dangling edges from this entry
1389
+ * point. (Importers can still construct dangling edges; call `validate()`
1390
+ * to detect them.)
1391
+ */
1392
+ addEdge(edge) {
1393
+ if (this.data.edges.find((e) => e.id === edge.id)) {
1394
+ throw new Error(`Edge with id "${edge.id}" already exists`);
1395
+ }
1396
+ if (!this.data.nodes.find((n) => n.id === edge.from)) {
1397
+ throw new Error(`Edge "${edge.id}" references unknown source node "${edge.from}"`);
1398
+ }
1399
+ if (!this.data.nodes.find((n) => n.id === edge.to)) {
1400
+ throw new Error(`Edge "${edge.id}" references unknown target node "${edge.to}"`);
1401
+ }
1402
+ this.data.edges.push({ ...edge });
1403
+ return this;
1404
+ }
1405
+ /**
1406
+ * Surface structural problems without throwing. Returns an array of
1407
+ * `ValidationError`s; empty array means the model is well-formed. Used by
1408
+ * the editor's status banner and by external tooling.
1409
+ */
1410
+ validate() {
1411
+ const errors = [];
1412
+ const nodeIds = /* @__PURE__ */ new Set();
1413
+ for (const n of this.data.nodes) {
1414
+ if (nodeIds.has(n.id)) errors.push({ kind: "duplicate-node-id", id: n.id, message: `Duplicate node id "${n.id}"` });
1415
+ nodeIds.add(n.id);
1416
+ }
1417
+ const edgeIds = /* @__PURE__ */ new Set();
1418
+ for (const e of this.data.edges) {
1419
+ if (edgeIds.has(e.id)) errors.push({ kind: "duplicate-edge-id", id: e.id, message: `Duplicate edge id "${e.id}"` });
1420
+ edgeIds.add(e.id);
1421
+ if (!nodeIds.has(e.from)) errors.push({ kind: "dangling-from", id: e.id, message: `Edge "${e.id}" references unknown source node "${e.from}"` });
1422
+ if (!nodeIds.has(e.to)) errors.push({ kind: "dangling-to", id: e.id, message: `Edge "${e.id}" references unknown target node "${e.to}"` });
1423
+ }
1424
+ return errors;
1425
+ }
1426
+ /** Remove an edge by id. Safe to call on a missing id (no-op). */
1427
+ removeEdge(id) {
1428
+ this.data.edges = this.data.edges.filter((e) => e.id !== id);
1429
+ return this;
1430
+ }
1431
+ /** Append a sequence actor. Duplicate names are silently ignored. */
1432
+ addActor(name) {
1433
+ if (!this.data.actors.includes(name)) {
1434
+ this.data.actors.push(name);
1435
+ }
1436
+ return this;
1437
+ }
1438
+ /**
1439
+ * Append a sequence message. The actors referenced by `from`/`to` are not
1440
+ * validated here — callers are expected to register them via `addActor()`
1441
+ * first.
1442
+ */
1443
+ addMessage(message) {
1444
+ this.data.messages.push({ ...message });
1445
+ return this;
1446
+ }
1447
+ /**
1448
+ * Return a deep-cloned plain `DiagramModel`. Safe to mutate by the caller;
1449
+ * mutations do not flow back into this `Model`.
1450
+ */
1451
+ toJSON() {
1452
+ return structuredClone(this.data);
1453
+ }
1454
+ };
1455
+
1456
+ // src/importers/mermaid.ts
1457
+ function parseNodeDecl(raw) {
1458
+ const patterns = [
1459
+ [/^(\w+)\{\{?"?(.+?)"?\}?\}$/, "diamond"],
1460
+ [/^(\w+)\(\("?(.+?)"?\)\)$/, "circle"],
1461
+ [/^(\w+)\[\/(.+?)\/\]$/, "parallelogram"],
1462
+ [/^(\w+)\[["']?(.+?)["']?\]$/, "rectangle"],
1463
+ [/^(\w+)\("?(.+?)"?\)$/, "rectangle"]
1464
+ ];
1465
+ for (const [re, shape] of patterns) {
1466
+ const m = raw.match(re);
1467
+ if (m) return { id: m[1], label: m[2].replace(/^["']|["']$/g, ""), shape };
1468
+ }
1469
+ return null;
1470
+ }
1471
+ var EDGE_RE = /^(.+?)\s*(-\.->|-\.-|-->|---)(?:\|(.+?)\|)?\s*(.+)$/;
1472
+ function detectStyle(connector) {
1473
+ return connector.startsWith("-.") ? "dashed" : "solid";
1474
+ }
1475
+ function detectArrowhead(connector) {
1476
+ return connector.endsWith(">") ? "arrow" : "none";
1477
+ }
1478
+ function parseFlowchart(lines) {
1479
+ const model = new Model("flowchart");
1480
+ const nodeMap = /* @__PURE__ */ new Map();
1481
+ const groupStack = [];
1482
+ const ensureNode = (id, group) => {
1483
+ if (!nodeMap.has(id)) {
1484
+ nodeMap.set(id, true);
1485
+ const metadata = group ? { group } : void 0;
1486
+ model.addNode({ id, label: id, shape: "rectangle", ...metadata ? { metadata } : {} });
1487
+ }
1488
+ };
1489
+ for (const line of lines) {
1490
+ const trimmed = line.trim();
1491
+ if (!trimmed) continue;
1492
+ if (trimmed.startsWith("%%") || trimmed.startsWith("graph") || trimmed.startsWith("flowchart") || trimmed.startsWith("click ") || trimmed.startsWith("classDef ") || trimmed.startsWith("class ") || trimmed.startsWith("style ") || trimmed.startsWith("linkStyle ")) continue;
1493
+ const subgraphOpen = trimmed.match(/^subgraph\s+(\S+)/i);
1494
+ if (subgraphOpen) {
1495
+ groupStack.push(subgraphOpen[1]);
1496
+ continue;
1497
+ }
1498
+ if (/^end\b/i.test(trimmed)) {
1499
+ groupStack.pop();
1500
+ continue;
1501
+ }
1502
+ const currentGroup = groupStack[groupStack.length - 1];
1503
+ const edgeMatch = trimmed.match(EDGE_RE);
1504
+ if (edgeMatch) {
1505
+ const fromRaw = edgeMatch[1].trim();
1506
+ const connector = edgeMatch[2];
1507
+ const label = edgeMatch[3]?.replace(/^["']|["']$/g, "");
1508
+ const toRaw = edgeMatch[4].trim();
1509
+ const style = detectStyle(connector);
1510
+ const arrowhead = detectArrowhead(connector);
1511
+ const fromNode = parseNodeDecl(fromRaw);
1512
+ const toNode = parseNodeDecl(toRaw);
1513
+ if (fromNode && !nodeMap.has(fromNode.id)) {
1514
+ nodeMap.set(fromNode.id, true);
1515
+ const metadata = currentGroup ? { group: currentGroup } : void 0;
1516
+ model.addNode({ ...fromNode, ...metadata ? { metadata } : {} });
1517
+ } else if (!fromNode) {
1518
+ ensureNode(fromRaw.replace(/\W.*/, ""), currentGroup);
1519
+ }
1520
+ if (toNode && !nodeMap.has(toNode.id)) {
1521
+ nodeMap.set(toNode.id, true);
1522
+ const metadata = currentGroup ? { group: currentGroup } : void 0;
1523
+ model.addNode({ ...toNode, ...metadata ? { metadata } : {} });
1524
+ } else if (!toNode) {
1525
+ ensureNode(toRaw.replace(/\W.*/, ""), currentGroup);
1526
+ }
1527
+ const fromId = fromNode?.id ?? fromRaw.replace(/\W.*/, "");
1528
+ const toId = toNode?.id ?? toRaw.replace(/\W.*/, "");
1529
+ model.addEdge({
1530
+ id: nextId("e", model.toJSON().edges),
1531
+ from: fromId,
1532
+ to: toId,
1533
+ ...label ? { label } : {},
1534
+ style,
1535
+ ...arrowhead === "none" ? { arrowhead } : {}
1536
+ });
1537
+ continue;
1538
+ }
1539
+ const nodeDecl = parseNodeDecl(trimmed);
1540
+ if (nodeDecl && !nodeMap.has(nodeDecl.id)) {
1541
+ nodeMap.set(nodeDecl.id, true);
1542
+ const metadata = currentGroup ? { group: currentGroup } : void 0;
1543
+ model.addNode({ ...nodeDecl, ...metadata ? { metadata } : {} });
1544
+ }
1545
+ }
1546
+ return model;
1547
+ }
1548
+ function parseSequence(lines, title) {
1549
+ const model = new Model("sequence", title);
1550
+ for (const line of lines) {
1551
+ const trimmed = line.trim();
1552
+ if (!trimmed || trimmed.startsWith("sequenceDiagram") || trimmed.startsWith("%%")) continue;
1553
+ const participantMatch = trimmed.match(/^participant\s+(.+)$/i);
1554
+ if (participantMatch) {
1555
+ model.addActor(participantMatch[1].trim());
1556
+ continue;
1557
+ }
1558
+ const actorMatch = trimmed.match(/^actor\s+(.+)$/i);
1559
+ if (actorMatch) {
1560
+ model.addActor(actorMatch[1].trim());
1561
+ continue;
1562
+ }
1563
+ const msgMatch = trimmed.match(/^(.+?)\s*(-->>|->>|-->|->)\s*(.+?):\s*(.+)$/);
1564
+ if (msgMatch) {
1565
+ const from = msgMatch[1].trim();
1566
+ const arrow = msgMatch[2];
1567
+ const to = msgMatch[3].trim();
1568
+ const label = msgMatch[4].trim();
1569
+ model.addActor(from);
1570
+ model.addActor(to);
1571
+ const messages = model.toJSON().messages ?? [];
1572
+ model.addMessage({ id: nextId("m", messages), from, to, label, style: arrow.startsWith("--") ? "dashed" : "solid" });
1573
+ }
1574
+ }
1575
+ return model;
1576
+ }
1577
+ function fromMermaid(mermaid) {
1578
+ const cleaned = mermaid.replace(/mermaid\.initialize\([\s\S]*?\)\s*;?/g, "");
1579
+ const rawLines = cleaned.split("\n");
1580
+ let startIdx = 0;
1581
+ let title;
1582
+ if (rawLines[0]?.trim() === "---") {
1583
+ const endFm = rawLines.findIndex((l, i) => i > 0 && l.trim() === "---");
1584
+ if (endFm !== -1) {
1585
+ const fmLines = rawLines.slice(1, endFm);
1586
+ for (const fl of fmLines) {
1587
+ const tm = fl.match(/^title:\s*(.+)$/);
1588
+ if (tm) title = tm[1].trim();
1589
+ }
1590
+ startIdx = endFm + 1;
1591
+ }
1592
+ }
1593
+ const lines = rawLines.slice(startIdx);
1594
+ const firstContent = lines.find((l) => l.trim());
1595
+ if (firstContent?.trim().startsWith("sequenceDiagram")) {
1596
+ const m2 = parseSequence(lines, title);
1597
+ return m2;
1598
+ }
1599
+ const m = parseFlowchart(lines);
1600
+ if (title) {
1601
+ const data = m.toJSON();
1602
+ data.title = title;
1603
+ return Model.fromData(data);
1604
+ }
1605
+ return m;
1606
+ }
1607
+
1608
+ // src/importers/json.ts
1609
+ function fromJSON(json) {
1610
+ const data = typeof json === "string" ? JSON.parse(json) : json;
1611
+ if (!data.type || !Array.isArray(data.nodes) || !Array.isArray(data.edges)) {
1612
+ throw new Error("Invalid DiagramModel JSON");
1613
+ }
1614
+ return Model.fromData(data);
1615
+ }
1616
+
1617
+ // src/ui/hooks/useImporter.ts
1618
+ function useImporter(applyAndPush, options = {}) {
1619
+ const { expectedType, transform } = options;
1620
+ return (0, import_react7.useCallback)((text) => {
1621
+ try {
1622
+ const parsed = text.trim().startsWith("{") ? fromJSON(text).toJSON() : fromMermaid(text).toJSON();
1623
+ if (expectedType && parsed.type !== expectedType) {
1624
+ alert(`Imported diagram is not a ${expectedType} diagram.`);
1625
+ return;
1626
+ }
1627
+ applyAndPush(transform ? transform(parsed) : parsed);
1628
+ } catch (err) {
1629
+ alert(`Import failed: ${err.message}`);
1630
+ }
1631
+ }, [applyAndPush, expectedType, transform]);
1632
+ }
1633
+
1634
+ // src/ui/presets.ts
1635
+ function emptyModel(type, variant) {
1636
+ if (type === "sequence") {
1637
+ return { type: "sequence", nodes: [], edges: [], actors: [], messages: [] };
1638
+ }
1639
+ return { type: "flowchart", variant: variant ?? "flowchart", nodes: [], edges: [] };
1640
+ }
1641
+ var FLOWCHART_PRESET = {
1642
+ type: "flowchart",
1643
+ variant: "flowchart",
1644
+ nodes: [
1645
+ { id: "start", label: "Start", shape: "circle", x: 240, y: 60 },
1646
+ { id: "place", label: "Place order", shape: "rectangle", x: 216, y: 180 },
1647
+ { id: "check", label: "Payment valid?", shape: "diamond", x: 200, y: 300 },
1648
+ { id: "confirm", label: "Send confirmation", shape: "rectangle", x: 60, y: 460 },
1649
+ { id: "retry", label: "Notify failure", shape: "rectangle", x: 380, y: 460 },
1650
+ { id: "done", label: "Done", shape: "circle", x: 240, y: 580 }
1651
+ ],
1652
+ edges: [
1653
+ { id: "e1", from: "start", to: "place" },
1654
+ { id: "e2", from: "place", to: "check" },
1655
+ { id: "e3", from: "check", to: "confirm", label: "yes" },
1656
+ { id: "e4", from: "check", to: "retry", label: "no", style: "dashed" },
1657
+ { id: "e5", from: "confirm", to: "done" },
1658
+ { id: "e6", from: "retry", to: "done", style: "dashed" }
1659
+ ]
1660
+ };
1661
+ var QUESTION_PRESET = {
1662
+ type: "flowchart",
1663
+ variant: "question",
1664
+ nodes: [
1665
+ {
1666
+ id: "role",
1667
+ label: "What is your role?",
1668
+ shape: "rectangle",
1669
+ x: 220,
1670
+ y: 60,
1671
+ metadata: { answers: ["Engineer", "Designer", "PM"] }
1672
+ },
1673
+ { id: "eng", label: "Engineering docs", shape: "rectangle", x: 40, y: 320 },
1674
+ { id: "design", label: "Design system", shape: "rectangle", x: 280, y: 320 },
1675
+ { id: "pm", label: "Product roadmap", shape: "rectangle", x: 520, y: 320 }
1676
+ ],
1677
+ edges: [
1678
+ { id: "q1", from: "role", to: "eng", label: "Engineer" },
1679
+ { id: "q2", from: "role", to: "design", label: "Designer" },
1680
+ { id: "q3", from: "role", to: "pm", label: "PM" }
1681
+ ]
1682
+ };
1683
+ var JOURNEY_PRESET = {
1684
+ type: "flowchart",
1685
+ variant: "journey",
1686
+ nodes: [
1687
+ { id: "j1", label: "Sign up", shape: "rectangle", x: 60, y: 60 },
1688
+ { id: "j2", label: "Verify email", shape: "rectangle", x: 60, y: 180 },
1689
+ { id: "j3", label: "Complete profile", shape: "rectangle", x: 60, y: 300 },
1690
+ { id: "j4", label: "Invite team", shape: "rectangle", x: 60, y: 420 },
1691
+ { id: "j5", label: "Launch project", shape: "rectangle", x: 60, y: 540 }
1692
+ ],
1693
+ edges: [
1694
+ { id: "je1", from: "j1", to: "j2" },
1695
+ { id: "je2", from: "j2", to: "j3" },
1696
+ { id: "je3", from: "j3", to: "j4" },
1697
+ { id: "je4", from: "j4", to: "j5" }
1698
+ ]
1699
+ };
1700
+ var SEQUENCE_PRESET = {
1701
+ type: "sequence",
1702
+ nodes: [],
1703
+ edges: [],
1704
+ actors: ["User", "App", "Server"],
1705
+ messages: [
1706
+ { id: "m1", from: "User", to: "App", label: 'Tap "Log in"' },
1707
+ { id: "m2", from: "App", to: "Server", label: "POST /login" },
1708
+ { id: "m3", from: "Server", to: "App", label: "200 OK + token", style: "dashed" },
1709
+ { id: "m4", from: "App", to: "User", label: "Show dashboard", style: "dashed" }
1710
+ ]
1711
+ };
1712
+ function presetFlowchartModel(variant = "flowchart") {
1713
+ if (variant === "question") return cloneModel(QUESTION_PRESET);
1714
+ if (variant === "journey") return cloneModel(JOURNEY_PRESET);
1715
+ return cloneModel(FLOWCHART_PRESET);
1716
+ }
1717
+ function presetSequenceModel() {
1718
+ return cloneModel(SEQUENCE_PRESET);
1719
+ }
1720
+ function cloneModel(m) {
1721
+ return {
1722
+ ...m,
1723
+ nodes: m.nodes.map((n) => ({ ...n, metadata: n.metadata ? { ...n.metadata } : void 0 })),
1724
+ edges: m.edges.map((e) => ({ ...e })),
1725
+ actors: m.actors ? [...m.actors] : void 0,
1726
+ messages: m.messages?.map((msg) => ({ ...msg }))
1727
+ };
1728
+ }
1729
+
1730
+ // src/ui/SequenceEditor.tsx
1731
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1732
+ var INDIGO = "#4f46e5";
1733
+ var INDIGO_SOFT = "#eef2ff";
1734
+ var lightTheme2 = {
1735
+ canvas: "#fafbfc",
1736
+ dot: "#dbe3ee",
1737
+ panelBg: "#ffffff",
1738
+ panelBorder: "#e2e8f0",
1739
+ ctrlsBg: "#ffffff",
1740
+ ctrlsBorder: "#cbd5e1",
1741
+ inputBg: "#f8fafc",
1742
+ inputBorder: "#e2e8f0",
1743
+ inputText: "#1e293b",
1744
+ textPrimary: "#1e293b",
1745
+ textSecondary: "#475569",
1746
+ textMuted: "#94a3b8",
1747
+ cardBg: "#ffffff",
1748
+ cardBorder: "#e2e8f0",
1749
+ lifeline: "#cbd5e1",
1750
+ arrow: "#64748b",
1751
+ actorFill: "#f5f3ff",
1752
+ actorStroke: "#c7d2fe",
1753
+ actorText: "#4338ca"
1754
+ };
1755
+ var darkTheme2 = {
1756
+ canvas: "#0f172a",
1757
+ dot: "#1e293b",
1758
+ panelBg: "#1e293b",
1759
+ panelBorder: "#334155",
1760
+ ctrlsBg: "#0f172a",
1761
+ ctrlsBorder: "#1e293b",
1762
+ inputBg: "#0f172a",
1763
+ inputBorder: "#334155",
1764
+ inputText: "#e2e8f0",
1765
+ textPrimary: "#f1f5f9",
1766
+ textSecondary: "#94a3b8",
1767
+ textMuted: "#475569",
1768
+ cardBg: "#0f172a",
1769
+ cardBorder: "#334155",
1770
+ lifeline: "#334155",
1771
+ arrow: "#64748b",
1772
+ actorFill: "#1e1b4b",
1773
+ actorStroke: "rgba(99,102,241,0.45)",
1774
+ actorText: "#a5b4fc"
1775
+ };
1776
+ var HEADER_H = 64;
1777
+ var HEADER_PAD = 24;
1778
+ var COL_MIN = 160;
1779
+ var ROW_H = 64;
1780
+ var SIDE_PAD = 40;
1781
+ var DRAG_THRESHOLD = 5;
1782
+ function ensureSequenceModel(m) {
1783
+ if (m && m.type === "sequence") {
1784
+ return { ...m, actors: m.actors ?? [], messages: m.messages ?? [] };
1785
+ }
1786
+ return presetSequenceModel();
1787
+ }
1788
+ function SequenceEditor({
1789
+ initialModel,
1790
+ onChange,
1791
+ onExport,
1792
+ height = 600,
1793
+ allowedExports,
1794
+ allowImport = true,
1795
+ theme = "auto",
1796
+ themeOverrides
1797
+ }) {
1798
+ const [model, setModel] = (0, import_react8.useState)(() => ensureSequenceModel(initialModel));
1799
+ const [selected, setSelected] = (0, import_react8.useState)(null);
1800
+ const [drag, setDrag] = (0, import_react8.useState)(null);
1801
+ const [editingId, setEditingId] = (0, import_react8.useState)(null);
1802
+ const [editLabel, setEditLabel] = (0, import_react8.useState)("");
1803
+ const historyRef = (0, import_react8.useRef)([ensureSequenceModel(initialModel)]);
1804
+ const historyIdxRef = (0, import_react8.useRef)(0);
1805
+ const svgRef = (0, import_react8.useRef)(null);
1806
+ const { t, isDark } = useEditorTheme(theme, themeOverrides, { light: lightTheme2, dark: darkTheme2 });
1807
+ const actors = model.actors ?? [];
1808
+ const messages = model.messages ?? [];
1809
+ const colW = (0, import_react8.useMemo)(() => {
1810
+ const longest = actors.reduce((m, a) => Math.max(m, a.length), 6);
1811
+ return Math.max(COL_MIN, longest * 9 + 40);
1812
+ }, [actors]);
1813
+ const totalW = SIDE_PAD * 2 + Math.max(1, actors.length) * colW;
1814
+ const totalH = HEADER_PAD + HEADER_H + 32 + messages.length * ROW_H + 48;
1815
+ const actorX = (name) => {
1816
+ const idx = actors.indexOf(name);
1817
+ if (idx < 0) return SIDE_PAD + colW / 2;
1818
+ return SIDE_PAD + idx * colW + colW / 2;
1819
+ };
1820
+ const msgY = (idx) => HEADER_PAD + HEADER_H + 40 + idx * ROW_H;
1821
+ const pushHistory = (0, import_react8.useCallback)((m) => {
1822
+ const stack = historyRef.current.slice(0, historyIdxRef.current + 1);
1823
+ stack.push(m);
1824
+ if (stack.length > 80) stack.shift();
1825
+ historyRef.current = stack;
1826
+ historyIdxRef.current = stack.length - 1;
1827
+ }, []);
1828
+ const applyAndPush = (0, import_react8.useCallback)((m) => {
1829
+ setModel(m);
1830
+ onChange?.(m);
1831
+ pushHistory(m);
1832
+ }, [onChange, pushHistory]);
1833
+ const undo = (0, import_react8.useCallback)(() => {
1834
+ if (historyIdxRef.current <= 0) return;
1835
+ historyIdxRef.current--;
1836
+ const m = historyRef.current[historyIdxRef.current];
1837
+ setModel(m);
1838
+ onChange?.(m);
1839
+ }, [onChange]);
1840
+ const redo = (0, import_react8.useCallback)(() => {
1841
+ if (historyIdxRef.current >= historyRef.current.length - 1) return;
1842
+ historyIdxRef.current++;
1843
+ const m = historyRef.current[historyIdxRef.current];
1844
+ setModel(m);
1845
+ onChange?.(m);
1846
+ }, [onChange]);
1847
+ const addActor = () => {
1848
+ const name = `Actor${actors.length + 1}`;
1849
+ applyAndPush({ ...model, actors: [...actors, name] });
1850
+ };
1851
+ const renameActor = (oldName, newName) => {
1852
+ if (!newName || newName === oldName || actors.includes(newName)) return;
1853
+ applyAndPush({
1854
+ ...model,
1855
+ actors: actors.map((a) => a === oldName ? newName : a),
1856
+ messages: messages.map((m) => ({
1857
+ ...m,
1858
+ from: m.from === oldName ? newName : m.from,
1859
+ to: m.to === oldName ? newName : m.to
1860
+ }))
1861
+ });
1862
+ };
1863
+ const removeActor = (name) => {
1864
+ applyAndPush({
1865
+ ...model,
1866
+ actors: actors.filter((a) => a !== name),
1867
+ messages: messages.filter((m) => m.from !== name && m.to !== name)
1868
+ });
1869
+ };
1870
+ const addMessage = () => {
1871
+ if (actors.length < 1) {
1872
+ const a = `Actor${actors.length + 1}`;
1873
+ const b = `Actor${actors.length + 2}`;
1874
+ applyAndPush({
1875
+ ...model,
1876
+ actors: [...actors, a, b],
1877
+ messages: [...messages, { id: nextId("m", messages), from: a, to: b, label: "message", style: "solid" }]
1878
+ });
1879
+ return;
1880
+ }
1881
+ const from = actors[0];
1882
+ const to = actors[Math.min(1, actors.length - 1)] ?? from;
1883
+ applyAndPush({
1884
+ ...model,
1885
+ messages: [...messages, { id: nextId("m", messages), from, to, label: "message", style: "solid" }]
1886
+ });
1887
+ };
1888
+ const updateMessage = (id, patch) => {
1889
+ applyAndPush({
1890
+ ...model,
1891
+ messages: messages.map((m) => m.id === id ? { ...m, ...patch } : m)
1892
+ });
1893
+ };
1894
+ const removeMessage = (id) => {
1895
+ applyAndPush({ ...model, messages: messages.filter((m) => m.id !== id) });
1896
+ if (selected === id) setSelected(null);
1897
+ };
1898
+ const reorderMessage = (0, import_react8.useCallback)((id, toIdx) => {
1899
+ const fromIdx = messages.findIndex((m) => m.id === id);
1900
+ if (fromIdx < 0 || toIdx === fromIdx) return;
1901
+ const next = messages.slice();
1902
+ const [moved] = next.splice(fromIdx, 1);
1903
+ next.splice(toIdx, 0, moved);
1904
+ applyAndPush({ ...model, messages: next });
1905
+ }, [messages, model, applyAndPush]);
1906
+ (0, import_react8.useEffect)(() => {
1907
+ const onKey = (e) => {
1908
+ const tgt = e.target;
1909
+ if (tgt && (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA" || tgt.isContentEditable)) return;
1910
+ const ctrl = e.ctrlKey || e.metaKey;
1911
+ if (ctrl && e.key === "z") {
1912
+ e.preventDefault();
1913
+ undo();
1914
+ return;
1915
+ }
1916
+ if (ctrl && (e.key === "y" || e.shiftKey && e.key === "z")) {
1917
+ e.preventDefault();
1918
+ redo();
1919
+ return;
1920
+ }
1921
+ if (e.key === "Escape") {
1922
+ setSelected(null);
1923
+ setEditingId(null);
1924
+ return;
1925
+ }
1926
+ if ((e.key === "Delete" || e.key === "Backspace") && selected) {
1927
+ e.preventDefault();
1928
+ removeMessage(selected);
1929
+ }
1930
+ };
1931
+ window.addEventListener("keydown", onKey);
1932
+ return () => window.removeEventListener("keydown", onKey);
1933
+ }, [undo, redo, selected]);
1934
+ const handleExport = useExporters(model, onExport, "sequence");
1935
+ const handleImport = useImporter(applyAndPush, {
1936
+ expectedType: "sequence",
1937
+ transform: ensureSequenceModel
1938
+ });
1939
+ const onRowMouseDown = (e, id) => {
1940
+ const tag = e.target.tagName;
1941
+ if (tag === "INPUT" || tag === "BUTTON" || tag === "SELECT") return;
1942
+ const idx = messages.findIndex((m) => m.id === id);
1943
+ if (idx < 0) return;
1944
+ e.preventDefault();
1945
+ setSelected(id);
1946
+ setDrag({ id, startY: e.clientY, originalIdx: idx, targetIdx: idx, active: false });
1947
+ };
1948
+ (0, import_react8.useEffect)(() => {
1949
+ if (!drag) return;
1950
+ const baseY = HEADER_PAD + HEADER_H + 40;
1951
+ const onMove = (ev) => {
1952
+ const dy = ev.clientY - drag.startY;
1953
+ if (!drag.active && Math.abs(dy) < DRAG_THRESHOLD) return;
1954
+ const svg = svgRef.current;
1955
+ if (!svg) return;
1956
+ const rect = svg.getBoundingClientRect();
1957
+ const yInSvg = ev.clientY - rect.top;
1958
+ const raw = Math.floor((yInSvg - baseY + ROW_H / 2) / ROW_H);
1959
+ const next = Math.max(0, Math.min(messages.length - 1, raw));
1960
+ if (next === drag.targetIdx && drag.active) return;
1961
+ setDrag({ ...drag, active: true, targetIdx: next });
1962
+ };
1963
+ const onUp = () => {
1964
+ if (drag.active && drag.targetIdx !== drag.originalIdx) {
1965
+ reorderMessage(drag.id, drag.targetIdx);
1966
+ }
1967
+ setDrag(null);
1968
+ };
1969
+ window.addEventListener("mousemove", onMove);
1970
+ window.addEventListener("mouseup", onUp);
1971
+ return () => {
1972
+ window.removeEventListener("mousemove", onMove);
1973
+ window.removeEventListener("mouseup", onUp);
1974
+ };
1975
+ }, [drag, messages.length, reorderMessage]);
1976
+ const visualMessages = (0, import_react8.useMemo)(() => {
1977
+ if (!drag?.active) return messages;
1978
+ const idx = messages.findIndex((m) => m.id === drag.id);
1979
+ if (idx < 0) return messages;
1980
+ const next = messages.slice();
1981
+ const [moved] = next.splice(idx, 1);
1982
+ next.splice(drag.targetIdx, 0, moved);
1983
+ return next;
1984
+ }, [messages, drag]);
1985
+ const selectedMsg = selected ? messages.find((m) => m.id === selected) : null;
1986
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
1987
+ display: "flex",
1988
+ flexDirection: "column",
1989
+ height,
1990
+ width: "100%",
1991
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
1992
+ background: t.ctrlsBg
1993
+ }, children: [
1994
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
1995
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
1996
+ display: "flex",
1997
+ gap: 8,
1998
+ padding: "7px 14px",
1999
+ background: t.ctrlsBg,
2000
+ borderBottom: `1px solid ${t.ctrlsBorder}`,
2001
+ alignItems: "center",
2002
+ flexWrap: "wrap"
2003
+ }, children: [
2004
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: addActor, style: primaryBtn(), children: "+ Actor" }),
2005
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: addMessage, style: primaryBtn(), children: "+ Message" }),
2006
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { width: 1, height: 18, background: t.ctrlsBorder, margin: "0 4px" } }),
2007
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: undo, style: ghostBtn2(t), title: "Undo (Ctrl+Z)", children: "\u21B6" }),
2008
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: redo, style: ghostBtn2(t), title: "Redo (Ctrl+Y)", children: "\u21B7" }),
2009
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
2010
+ actors.length,
2011
+ " actor",
2012
+ actors.length === 1 ? "" : "s",
2013
+ " \xB7 ",
2014
+ messages.length,
2015
+ " message",
2016
+ messages.length === 1 ? "" : "s",
2017
+ " \xB7 drag a row to reorder"
2018
+ ] })
2019
+ ] }),
2020
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [
2021
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { flex: 1, overflow: "auto", background: t.canvas, position: "relative" }, children: actors.length === 0 && messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
2022
+ position: "absolute",
2023
+ inset: 0,
2024
+ display: "flex",
2025
+ flexDirection: "column",
2026
+ alignItems: "center",
2027
+ justifyContent: "center",
2028
+ gap: 10,
2029
+ color: t.textMuted,
2030
+ pointerEvents: "none"
2031
+ }, children: [
2032
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 36, opacity: 0.15, color: t.textPrimary }, children: "\u2194" }),
2033
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { fontSize: 13, fontWeight: 500 }, children: [
2034
+ "Click ",
2035
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { style: { color: INDIGO }, children: "+ Actor" }),
2036
+ " then ",
2037
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { style: { color: INDIGO }, children: "+ Message" }),
2038
+ " to start"
2039
+ ] })
2040
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2041
+ "svg",
2042
+ {
2043
+ ref: svgRef,
2044
+ width: totalW,
2045
+ height: totalH,
2046
+ style: { display: "block", cursor: drag?.active ? "grabbing" : "default", userSelect: "none" },
2047
+ children: [
2048
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("defs", { children: [
2049
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pattern", { id: "seqdots", x: "0", y: "0", width: "24", height: "24", patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { cx: 12, cy: 12, r: 1.1, fill: t.dot }) }),
2050
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("filter", { id: "seqShadow", x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("feDropShadow", { dx: 0, dy: 3, stdDeviation: 5, floodColor: isDark ? "rgba(0,0,0,0.5)" : "rgba(15,23,42,0.09)" }) }),
2051
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("marker", { id: "seqArrow", markerWidth: 9, markerHeight: 7, refX: 8.5, refY: 3.5, orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: t.arrow }) })
2052
+ ] }),
2053
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { width: totalW, height: totalH, fill: "url(#seqdots)" }),
2054
+ actors.map((name) => {
2055
+ const x = actorX(name);
2056
+ const top = HEADER_PAD + HEADER_H;
2057
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2058
+ "line",
2059
+ {
2060
+ x1: x,
2061
+ x2: x,
2062
+ y1: top + 4,
2063
+ y2: totalH - 24,
2064
+ stroke: t.lifeline,
2065
+ strokeWidth: 1.25,
2066
+ strokeDasharray: "5 5"
2067
+ },
2068
+ `life-${name}`
2069
+ );
2070
+ }),
2071
+ visualMessages.map((msg, idx) => {
2072
+ const y = msgY(idx);
2073
+ const fromX = actorX(msg.from);
2074
+ const toX = actorX(msg.to);
2075
+ const selectedHere = selected === msg.id;
2076
+ const isDragging = drag?.active && drag.id === msg.id;
2077
+ const isSelf = msg.from === msg.to;
2078
+ const stroke = selectedHere ? INDIGO : t.arrow;
2079
+ const dash = msg.style === "dashed" ? "6,4" : void 0;
2080
+ const cursor = drag?.active ? "grabbing" : "grab";
2081
+ const groupOpacity = isDragging ? 0.85 : 1;
2082
+ if (isSelf) {
2083
+ const startX = fromX;
2084
+ const loopW = 36;
2085
+ const loopY = y - 6;
2086
+ const d = `M ${startX} ${loopY} C ${startX + loopW} ${loopY}, ${startX + loopW} ${loopY + 24}, ${startX} ${loopY + 24}`;
2087
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { onMouseDown: (e) => onRowMouseDown(e, msg.id), style: { cursor, opacity: groupOpacity }, children: [
2088
+ (selectedHere || isDragging) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2089
+ "rect",
2090
+ {
2091
+ x: SIDE_PAD - 8,
2092
+ y: y - 22,
2093
+ width: totalW - (SIDE_PAD - 8) * 2,
2094
+ height: ROW_H - 12,
2095
+ rx: 10,
2096
+ fill: INDIGO_SOFT,
2097
+ opacity: isDark ? 0.18 : 0.6
2098
+ }
2099
+ ),
2100
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d, fill: "none", stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
2101
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("text", { x: startX + loopW + 8, y: loopY + 16, fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
2102
+ ] }, msg.id);
2103
+ }
2104
+ const labelX = (fromX + toX) / 2;
2105
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { onMouseDown: (e) => onRowMouseDown(e, msg.id), style: { cursor, opacity: groupOpacity }, children: [
2106
+ (selectedHere || isDragging) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2107
+ "rect",
2108
+ {
2109
+ x: SIDE_PAD - 8,
2110
+ y: y - 22,
2111
+ width: totalW - (SIDE_PAD - 8) * 2,
2112
+ height: ROW_H - 12,
2113
+ rx: 10,
2114
+ fill: INDIGO_SOFT,
2115
+ opacity: isDark ? 0.18 : 0.6
2116
+ }
2117
+ ),
2118
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: fromX, y1: y, x2: toX, y2: y, stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
2119
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2120
+ "rect",
2121
+ {
2122
+ x: labelX - estimateW(msg.label) / 2 - 6,
2123
+ y: y - 18,
2124
+ width: estimateW(msg.label) + 12,
2125
+ height: 18,
2126
+ rx: 6,
2127
+ fill: t.canvas,
2128
+ stroke: selectedHere ? INDIGO : t.cardBorder,
2129
+ strokeWidth: selectedHere ? 1.25 : 1
2130
+ }
2131
+ ),
2132
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("text", { x: labelX, y: y - 5, textAnchor: "middle", fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
2133
+ ] }, msg.id);
2134
+ }),
2135
+ actors.map((name) => {
2136
+ const x = actorX(name);
2137
+ const w = colW - 24;
2138
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { children: [
2139
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2140
+ "rect",
2141
+ {
2142
+ x: x - w / 2,
2143
+ y: HEADER_PAD,
2144
+ width: w,
2145
+ height: HEADER_H,
2146
+ rx: 12,
2147
+ fill: t.actorFill,
2148
+ stroke: t.actorStroke,
2149
+ strokeWidth: 1.25,
2150
+ filter: "url(#seqShadow)"
2151
+ }
2152
+ ),
2153
+ editingId === name ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("foreignObject", { x: x - w / 2 + 8, y: HEADER_PAD + 16, width: w - 16, height: 32, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2154
+ "input",
2155
+ {
2156
+ autoFocus: true,
2157
+ defaultValue: name,
2158
+ onBlur: (e) => {
2159
+ renameActor(name, e.currentTarget.value.trim());
2160
+ setEditingId(null);
2161
+ },
2162
+ onKeyDown: (e) => {
2163
+ if (e.key === "Enter") {
2164
+ renameActor(name, e.target.value.trim());
2165
+ setEditingId(null);
2166
+ }
2167
+ if (e.key === "Escape") setEditingId(null);
2168
+ },
2169
+ style: {
2170
+ width: "100%",
2171
+ height: "100%",
2172
+ border: "none",
2173
+ borderRadius: 6,
2174
+ outline: `2px solid ${INDIGO}`,
2175
+ textAlign: "center",
2176
+ fontSize: 13,
2177
+ fontWeight: 600,
2178
+ background: t.inputBg,
2179
+ color: t.inputText,
2180
+ boxSizing: "border-box",
2181
+ padding: "0 6px",
2182
+ fontFamily: "inherit"
2183
+ }
2184
+ }
2185
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2186
+ "text",
2187
+ {
2188
+ x,
2189
+ y: HEADER_PAD + HEADER_H / 2 + 4,
2190
+ textAnchor: "middle",
2191
+ fontSize: 13,
2192
+ fontWeight: 700,
2193
+ fill: t.actorText,
2194
+ style: { cursor: "pointer", userSelect: "none" },
2195
+ onDoubleClick: () => setEditingId(name),
2196
+ children: name
2197
+ }
2198
+ ),
2199
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2200
+ "circle",
2201
+ {
2202
+ cx: x + w / 2 - 12,
2203
+ cy: HEADER_PAD + 14,
2204
+ r: 9,
2205
+ fill: "transparent",
2206
+ style: { cursor: "pointer" },
2207
+ onClick: () => removeActor(name),
2208
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("title", { children: "Remove actor" })
2209
+ }
2210
+ ),
2211
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2212
+ "text",
2213
+ {
2214
+ x: x + w / 2 - 12,
2215
+ y: HEADER_PAD + 18,
2216
+ textAnchor: "middle",
2217
+ fontSize: 12,
2218
+ fill: t.textMuted,
2219
+ style: { pointerEvents: "none", userSelect: "none" },
2220
+ children: "\xD7"
2221
+ }
2222
+ )
2223
+ ] }, `hdr-${name}`);
2224
+ })
2225
+ ]
2226
+ }
2227
+ ) }),
2228
+ selectedMsg && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
2229
+ width: 280,
2230
+ flexShrink: 0,
2231
+ background: t.panelBg,
2232
+ borderLeft: `1px solid ${t.panelBorder}`,
2233
+ padding: "14px 16px",
2234
+ overflowY: "auto"
2235
+ }, children: [
2236
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.7, marginBottom: 10 }, children: "Message" }),
2237
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Label, { t, children: "Label" }),
2238
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2239
+ "input",
2240
+ {
2241
+ value: editLabel || selectedMsg.label,
2242
+ onChange: (e) => setEditLabel(e.target.value),
2243
+ onFocus: () => setEditLabel(selectedMsg.label),
2244
+ onBlur: () => {
2245
+ if (editLabel && editLabel !== selectedMsg.label) updateMessage(selectedMsg.id, { label: editLabel });
2246
+ setEditLabel("");
2247
+ },
2248
+ onKeyDown: (e) => {
2249
+ if (e.key === "Enter") e.target.blur();
2250
+ },
2251
+ style: input(t)
2252
+ }
2253
+ ),
2254
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Label, { t, children: "From" }),
2255
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { value: selectedMsg.from, onChange: (e) => updateMessage(selectedMsg.id, { from: e.target.value }), style: input(t), children: actors.map((a) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: a, children: a }, a)) }),
2256
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Label, { t, children: "To" }),
2257
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { value: selectedMsg.to, onChange: (e) => updateMessage(selectedMsg.id, { to: e.target.value }), style: input(t), children: actors.map((a) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: a, children: a }, a)) }),
2258
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Label, { t, children: "Style" }),
2259
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { display: "flex", gap: 6 }, children: ["solid", "dashed"].map((s2) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2260
+ "button",
2261
+ {
2262
+ onClick: () => updateMessage(selectedMsg.id, { style: s2 }),
2263
+ style: {
2264
+ flex: 1,
2265
+ padding: "6px 10px",
2266
+ border: `1.5px solid ${selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO : t.inputBorder}`,
2267
+ background: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO_SOFT : t.inputBg,
2268
+ color: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO : t.textPrimary,
2269
+ borderRadius: 8,
2270
+ fontSize: 12,
2271
+ fontWeight: 600,
2272
+ cursor: "pointer",
2273
+ fontFamily: "inherit"
2274
+ },
2275
+ children: s2 === "solid" ? "\u2500\u2500 solid" : "\u2500 \u2500 dashed"
2276
+ },
2277
+ s2
2278
+ )) }),
2279
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { height: 14 } }),
2280
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2281
+ "button",
2282
+ {
2283
+ onClick: () => removeMessage(selectedMsg.id),
2284
+ style: { ...ghostBtn2(t), width: "100%", color: "#ef4444", border: `1px solid ${isDark ? "#7f1d1d" : "#fca5a5"}` },
2285
+ children: "Delete message"
2286
+ }
2287
+ )
2288
+ ] })
2289
+ ] }),
2290
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
2291
+ padding: "4px 14px",
2292
+ fontSize: 11,
2293
+ color: t.textMuted,
2294
+ background: t.canvas,
2295
+ borderTop: `1px solid ${t.ctrlsBorder}`,
2296
+ display: "flex",
2297
+ gap: 16
2298
+ }, children: [
2299
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
2300
+ actors.length,
2301
+ " actors"
2302
+ ] }),
2303
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
2304
+ messages.length,
2305
+ " messages"
2306
+ ] }),
2307
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { marginLeft: "auto" }, children: "double-click actor to rename \xB7 drag a row to reorder" })
2308
+ ] })
2309
+ ] });
2310
+ }
2311
+ function estimateW(text, pxPerChar = 7) {
2312
+ return text.length * pxPerChar;
2313
+ }
2314
+ function primaryBtn() {
2315
+ return {
2316
+ padding: "6px 12px",
2317
+ background: INDIGO,
2318
+ color: "#fff",
2319
+ border: "none",
2320
+ borderRadius: 8,
2321
+ cursor: "pointer",
2322
+ fontSize: 12,
2323
+ fontWeight: 600,
2324
+ fontFamily: "inherit"
2325
+ };
2326
+ }
2327
+ function ghostBtn2(t) {
2328
+ return {
2329
+ padding: "5px 10px",
2330
+ background: "transparent",
2331
+ color: t.textSecondary,
2332
+ border: `1px solid ${t.ctrlsBorder}`,
2333
+ borderRadius: 7,
2334
+ cursor: "pointer",
2335
+ fontSize: 12,
2336
+ fontWeight: 500,
2337
+ fontFamily: "inherit"
2338
+ };
2339
+ }
2340
+ function input(t) {
2341
+ return {
2342
+ width: "100%",
2343
+ boxSizing: "border-box",
2344
+ padding: "6px 10px",
2345
+ border: `1.5px solid ${t.inputBorder}`,
2346
+ borderRadius: 7,
2347
+ background: t.inputBg,
2348
+ color: t.inputText,
2349
+ fontSize: 12,
2350
+ fontFamily: "inherit",
2351
+ outline: "none",
2352
+ marginBottom: 12
2353
+ };
2354
+ }
2355
+ function Label({ t, children }) {
2356
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 4 }, children });
2357
+ }
2358
+
2359
+ // src/ui/Minimap.tsx
2360
+ var import_react9 = require("react");
2361
+ var import_jsx_runtime5 = require("react/jsx-runtime");
2362
+ var W = 168;
2363
+ var H = 112;
2364
+ var PAD = 18;
2365
+ function Minimap({
2366
+ model,
2367
+ viewportW,
2368
+ viewportH,
2369
+ transform,
2370
+ measureNode,
2371
+ onCenterOn,
2372
+ isDark,
2373
+ accentColor
2374
+ }) {
2375
+ const dragRef = (0, import_react9.useRef)(null);
2376
+ const boxes = model.nodes.map((n) => {
2377
+ const { w, h } = measureNode(n);
2378
+ return { id: n.id, x: n.x ?? 0, y: n.y ?? 0, w, h };
2379
+ });
2380
+ if (boxes.length === 0) return null;
2381
+ const vx = -transform.x / transform.scale;
2382
+ const vy = -transform.y / transform.scale;
2383
+ const vw = viewportW / transform.scale;
2384
+ const vh = viewportH / transform.scale;
2385
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
2386
+ for (const b of boxes) {
2387
+ minX = Math.min(minX, b.x);
2388
+ minY = Math.min(minY, b.y);
2389
+ maxX = Math.max(maxX, b.x + b.w);
2390
+ maxY = Math.max(maxY, b.y + b.h);
2391
+ }
2392
+ minX = Math.min(minX, vx);
2393
+ minY = Math.min(minY, vy);
2394
+ maxX = Math.max(maxX, vx + vw);
2395
+ maxY = Math.max(maxY, vy + vh);
2396
+ const contentW = Math.max(1, maxX - minX);
2397
+ const contentH = Math.max(1, maxY - minY);
2398
+ const scale = Math.min((W - PAD * 2) / contentW, (H - PAD * 2) / contentH);
2399
+ const offsetX = (W - contentW * scale) / 2 - minX * scale;
2400
+ const offsetY = (H - contentH * scale) / 2 - minY * scale;
2401
+ const project = (x, y) => ({
2402
+ x: offsetX + x * scale,
2403
+ y: offsetY + y * scale
2404
+ });
2405
+ const unproject = (mx, my) => ({
2406
+ x: (mx - offsetX) / scale,
2407
+ y: (my - offsetY) / scale
2408
+ });
2409
+ const panTo = (0, import_react9.useCallback)((e) => {
2410
+ const rect = e.currentTarget.getBoundingClientRect();
2411
+ const mx = e.clientX - rect.left;
2412
+ const my = e.clientY - rect.top;
2413
+ const { x, y } = unproject(mx, my);
2414
+ onCenterOn(x, y);
2415
+ }, [onCenterOn, scale, offsetX, offsetY]);
2416
+ const onMouseDown = (e) => {
2417
+ e.stopPropagation();
2418
+ dragRef.current = { active: true };
2419
+ panTo(e);
2420
+ };
2421
+ const onMouseMove = (e) => {
2422
+ if (!dragRef.current?.active) return;
2423
+ panTo(e);
2424
+ };
2425
+ const onMouseUp = () => {
2426
+ dragRef.current = null;
2427
+ };
2428
+ const bg = isDark ? "rgba(15,23,42,0.92)" : "rgba(255,255,255,0.94)";
2429
+ const border = isDark ? "#334155" : "#e2e8f0";
2430
+ const nodeFill = isDark ? "#475569" : "#cbd5e1";
2431
+ const viewStroke = accentColor;
2432
+ const viewFill = `${accentColor}22`;
2433
+ const vp1 = project(vx, vy);
2434
+ const vp2 = project(vx + vw, vy + vh);
2435
+ const vpRect = {
2436
+ x: Math.max(0, Math.min(W, vp1.x)),
2437
+ y: Math.max(0, Math.min(H, vp1.y)),
2438
+ w: Math.max(2, Math.min(W, vp2.x) - Math.max(0, vp1.x)),
2439
+ h: Math.max(2, Math.min(H, vp2.y) - Math.max(0, vp1.y))
2440
+ };
2441
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2442
+ "div",
2443
+ {
2444
+ style: {
2445
+ position: "absolute",
2446
+ bottom: 14,
2447
+ right: 14,
2448
+ background: bg,
2449
+ border: `1px solid ${border}`,
2450
+ borderRadius: 10,
2451
+ padding: 6,
2452
+ boxShadow: isDark ? "0 8px 20px rgba(0,0,0,0.45)" : "0 6px 18px rgba(15,23,42,0.08)",
2453
+ backdropFilter: "blur(6px)"
2454
+ },
2455
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2456
+ "svg",
2457
+ {
2458
+ width: W,
2459
+ height: H,
2460
+ style: { display: "block", cursor: "grab", borderRadius: 6 },
2461
+ onMouseDown,
2462
+ onMouseMove,
2463
+ onMouseUp,
2464
+ onMouseLeave: onMouseUp,
2465
+ children: [
2466
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { width: W, height: H, rx: 6, fill: isDark ? "#0f172a" : "#fafbfc" }),
2467
+ boxes.map((b) => {
2468
+ const p = project(b.x, b.y);
2469
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2470
+ "rect",
2471
+ {
2472
+ x: p.x,
2473
+ y: p.y,
2474
+ width: Math.max(2, b.w * scale),
2475
+ height: Math.max(2, b.h * scale),
2476
+ rx: 2,
2477
+ fill: nodeFill
2478
+ },
2479
+ b.id
2480
+ );
2481
+ }),
2482
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2483
+ "rect",
2484
+ {
2485
+ x: vpRect.x,
2486
+ y: vpRect.y,
2487
+ width: vpRect.w,
2488
+ height: vpRect.h,
2489
+ rx: 3,
2490
+ fill: viewFill,
2491
+ stroke: viewStroke,
2492
+ strokeWidth: 1.25
2493
+ }
2494
+ )
2495
+ ]
2496
+ }
2497
+ )
2498
+ }
2499
+ );
2500
+ }
2501
+
2502
+ // src/ui/NodeNavigator.tsx
2503
+ var import_react10 = require("react");
2504
+ var import_jsx_runtime6 = require("react/jsx-runtime");
2505
+ function NodeNavigator({
2506
+ model,
2507
+ selected,
2508
+ variant,
2509
+ isDark,
2510
+ t,
2511
+ acc,
2512
+ open,
2513
+ onToggle,
2514
+ onSelect
2515
+ }) {
2516
+ const [search, setSearch] = (0, import_react10.useState)("");
2517
+ const shapeIcon = (node) => {
2518
+ if (variant === "question") return "?";
2519
+ if (variant === "journey") return "\u2197";
2520
+ switch (node.shape) {
2521
+ case "diamond":
2522
+ return "\u25C7";
2523
+ case "circle":
2524
+ return "\u25CB";
2525
+ case "parallelogram":
2526
+ return "\u25B1";
2527
+ default:
2528
+ return "\u25AD";
2529
+ }
2530
+ };
2531
+ const filtered = model.nodes.filter(
2532
+ (n) => n.label.toLowerCase().includes(search.toLowerCase())
2533
+ );
2534
+ const inEdges = (id) => model.edges.filter((e) => e.to === id).length;
2535
+ const outEdges = (id) => model.edges.filter((e) => e.from === id).length;
2536
+ if (!open) {
2537
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: {
2538
+ width: 36,
2539
+ flexShrink: 0,
2540
+ background: t.panelBg,
2541
+ borderRight: `1px solid ${t.panelBorder}`,
2542
+ display: "flex",
2543
+ flexDirection: "column",
2544
+ alignItems: "center",
2545
+ paddingTop: 8,
2546
+ gap: 6
2547
+ }, children: [
2548
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2549
+ "button",
2550
+ {
2551
+ onClick: onToggle,
2552
+ title: "Open node list",
2553
+ style: { background: "none", border: "none", cursor: "pointer", color: t.textMuted, padding: 6, borderRadius: 6, fontSize: 14, lineHeight: 1 },
2554
+ children: "\u2630"
2555
+ }
2556
+ ),
2557
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 10, color: t.textMuted, fontWeight: 700, writingMode: "vertical-rl", transform: "rotate(180deg)", letterSpacing: 0.5 }, children: model.nodes.length })
2558
+ ] });
2559
+ }
2560
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: {
2561
+ width: 216,
2562
+ flexShrink: 0,
2563
+ background: t.panelBg,
2564
+ borderRight: `1px solid ${t.panelBorder}`,
2565
+ display: "flex",
2566
+ flexDirection: "column",
2567
+ overflow: "hidden"
2568
+ }, children: [
2569
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: {
2570
+ display: "flex",
2571
+ alignItems: "center",
2572
+ justifyContent: "space-between",
2573
+ padding: "10px 12px",
2574
+ borderBottom: `1px solid ${t.panelBorder}`,
2575
+ flexShrink: 0
2576
+ }, children: [
2577
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
2578
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { fontSize: 11, fontWeight: 700, color: t.textSecondary, textTransform: "uppercase", letterSpacing: 0.7 }, children: variant === "question" ? "Questions" : variant === "journey" ? "Steps" : "Nodes" }),
2579
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: {
2580
+ fontSize: 10,
2581
+ fontWeight: 700,
2582
+ color: t.textMuted,
2583
+ background: isDark ? "#0f172a" : "#f1f5f9",
2584
+ padding: "1px 6px",
2585
+ borderRadius: 99
2586
+ }, children: model.nodes.length })
2587
+ ] }),
2588
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2589
+ "button",
2590
+ {
2591
+ onClick: onToggle,
2592
+ style: { background: "none", border: "none", cursor: "pointer", color: t.textMuted, padding: "2px 4px", borderRadius: 4, fontSize: 13, lineHeight: 1 },
2593
+ title: "Collapse",
2594
+ children: "\u2039"
2595
+ }
2596
+ )
2597
+ ] }),
2598
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { padding: "8px 10px", borderBottom: `1px solid ${t.sectionBorder}`, flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { position: "relative" }, children: [
2599
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { position: "absolute", left: 8, top: "50%", transform: "translateY(-50%)", fontSize: 11, color: t.textMuted, pointerEvents: "none" }, children: "\u2315" }),
2600
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2601
+ "input",
2602
+ {
2603
+ value: search,
2604
+ onChange: (e) => setSearch(e.target.value),
2605
+ placeholder: "Search\u2026",
2606
+ style: {
2607
+ width: "100%",
2608
+ padding: "5px 8px 5px 24px",
2609
+ border: `1.5px solid ${t.inputBorder}`,
2610
+ borderRadius: 7,
2611
+ fontSize: 12,
2612
+ background: t.inputBg,
2613
+ color: t.inputText,
2614
+ outline: "none",
2615
+ boxSizing: "border-box",
2616
+ fontFamily: "inherit"
2617
+ }
2618
+ }
2619
+ )
2620
+ ] }) }),
2621
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { flex: 1, overflowY: "auto", padding: "6px 8px", display: "flex", flexDirection: "column", gap: 2 }, children: [
2622
+ filtered.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { textAlign: "center", padding: "20px 0", fontSize: 12, color: t.textMuted, fontStyle: "italic" }, children: model.nodes.length === 0 ? "No nodes yet" : "No matches" }),
2623
+ filtered.map((node, idx) => {
2624
+ const isSelected = selected === node.id;
2625
+ const answers = node.metadata?.answers ?? [];
2626
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2627
+ "button",
2628
+ {
2629
+ onClick: () => onSelect(node.id),
2630
+ style: {
2631
+ display: "flex",
2632
+ alignItems: "center",
2633
+ gap: 8,
2634
+ width: "100%",
2635
+ padding: "7px 8px",
2636
+ textAlign: "left",
2637
+ background: isSelected ? acc.fill : "transparent",
2638
+ border: isSelected ? `1.5px solid ${acc.border}` : "1.5px solid transparent",
2639
+ borderRadius: 8,
2640
+ cursor: "pointer",
2641
+ fontFamily: "inherit",
2642
+ transition: "background 0.1s"
2643
+ },
2644
+ onMouseEnter: (e) => {
2645
+ if (!isSelected) e.currentTarget.style.background = isDark ? "#334155" : "#f1f5f9";
2646
+ },
2647
+ onMouseLeave: (e) => {
2648
+ if (!isSelected) e.currentTarget.style.background = "transparent";
2649
+ },
2650
+ children: [
2651
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: {
2652
+ width: 22,
2653
+ height: 22,
2654
+ borderRadius: 6,
2655
+ flexShrink: 0,
2656
+ background: isSelected ? acc.color : isDark ? "#334155" : "#e2e8f0",
2657
+ color: isSelected ? "#fff" : t.textMuted,
2658
+ display: "flex",
2659
+ alignItems: "center",
2660
+ justifyContent: "center",
2661
+ fontSize: variant === "journey" ? 9 : 11,
2662
+ fontWeight: 700
2663
+ }, children: variant === "journey" ? idx + 1 : shapeIcon(node) }),
2664
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [
2665
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: {
2666
+ fontSize: 12,
2667
+ fontWeight: isSelected ? 600 : 400,
2668
+ color: isSelected ? acc.color : t.textPrimary,
2669
+ overflow: "hidden",
2670
+ textOverflow: "ellipsis",
2671
+ whiteSpace: "nowrap",
2672
+ lineHeight: 1.3
2673
+ }, children: node.label }),
2674
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 10, color: t.textMuted, lineHeight: 1.2, marginTop: 1 }, children: variant === "question" ? `${answers.length} answer${answers.length !== 1 ? "s" : ""}` : `${inEdges(node.id)}\u2193 ${outEdges(node.id)}\u2192` })
2675
+ ] }),
2676
+ isSelected && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { fontSize: 10, color: acc.color, flexShrink: 0 }, children: "\u25C9" })
2677
+ ]
2678
+ },
2679
+ node.id
2680
+ );
2681
+ })
2682
+ ] })
2683
+ ] });
2684
+ }
2685
+
2686
+ // src/ui/ContextMenu.tsx
2687
+ var import_react11 = require("react");
2688
+ var import_jsx_runtime7 = require("react/jsx-runtime");
2689
+ function ContextMenu({
2690
+ x,
2691
+ y,
2692
+ nodeId,
2693
+ edgeId,
2694
+ isDark,
2695
+ t,
2696
+ acc,
2697
+ canUndo,
2698
+ canRedo,
2699
+ onUndo,
2700
+ onRedo,
2701
+ onReCenter,
2702
+ onAddNode,
2703
+ onDuplicate,
2704
+ onRename,
2705
+ onDelete,
2706
+ onDisconnect,
2707
+ onEdgeRename,
2708
+ onEdgeStyle,
2709
+ onEdgeArrowhead,
2710
+ onEdgeDelete,
2711
+ onEdgeResetRouting,
2712
+ currentEdgeStyle,
2713
+ currentEdgeArrow,
2714
+ edgeHasWaypoint,
2715
+ containerRef
2716
+ }) {
2717
+ const menuRef = (0, import_react11.useRef)(null);
2718
+ const [pos, setPos] = (0, import_react11.useState)({ x, y });
2719
+ (0, import_react11.useEffect)(() => {
2720
+ if (!menuRef.current || !containerRef.current) return;
2721
+ const m = menuRef.current.getBoundingClientRect();
2722
+ const c = containerRef.current.getBoundingClientRect();
2723
+ let nx = x, ny = y;
2724
+ if (nx + m.width > c.right - 8) nx = x - m.width;
2725
+ if (ny + m.height > c.bottom - 8) ny = y - m.height;
2726
+ setPos({ x: nx, y: ny });
2727
+ }, [x, y, containerRef]);
2728
+ const bg = isDark ? "#1e293b" : "#ffffff";
2729
+ const border = isDark ? "#334155" : "#e2e8f0";
2730
+ const hoverBg = isDark ? "#334155" : "#f1f5f9";
2731
+ const dividerColor = isDark ? "#334155" : "#f1f5f9";
2732
+ const text = t.textPrimary;
2733
+ const muted = t.textMuted;
2734
+ const item = (label, onClick, color, disabled) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2735
+ "button",
2736
+ {
2737
+ onClick: disabled ? void 0 : onClick,
2738
+ style: {
2739
+ display: "flex",
2740
+ alignItems: "center",
2741
+ gap: 10,
2742
+ width: "100%",
2743
+ padding: "7px 14px",
2744
+ background: "none",
2745
+ border: "none",
2746
+ textAlign: "left",
2747
+ cursor: disabled ? "default" : "pointer",
2748
+ fontSize: 12,
2749
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
2750
+ color: disabled ? muted : color ?? text,
2751
+ opacity: disabled ? 0.4 : 1,
2752
+ borderRadius: 6
2753
+ },
2754
+ onMouseEnter: (e) => {
2755
+ if (!disabled) e.currentTarget.style.background = hoverBg;
2756
+ },
2757
+ onMouseLeave: (e) => {
2758
+ e.currentTarget.style.background = "none";
2759
+ },
2760
+ children: label
2761
+ },
2762
+ label
2763
+ );
2764
+ const divider2 = /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { height: 1, background: dividerColor, margin: "4px 0" } });
2765
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2766
+ "div",
2767
+ {
2768
+ ref: menuRef,
2769
+ onMouseDown: (e) => e.stopPropagation(),
2770
+ style: {
2771
+ position: "fixed",
2772
+ left: pos.x,
2773
+ top: pos.y,
2774
+ zIndex: 9999,
2775
+ background: bg,
2776
+ border: `1px solid ${border}`,
2777
+ borderRadius: 10,
2778
+ padding: "5px 0",
2779
+ minWidth: 180,
2780
+ boxShadow: isDark ? "0 8px 32px rgba(0,0,0,0.5)" : "0 8px 32px rgba(0,0,0,0.12)",
2781
+ fontFamily: "ui-sans-serif,system-ui,sans-serif"
2782
+ },
2783
+ children: edgeId ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
2784
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Edge" }),
2785
+ item("Rename label (dbl-click)", () => onEdgeRename?.()),
2786
+ divider2,
2787
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { padding: "4px 14px 2px", fontSize: 9, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Style" }),
2788
+ item(`Solid${currentEdgeStyle === "solid" || !currentEdgeStyle ? " \u2713" : ""}`, () => onEdgeStyle?.("solid")),
2789
+ item(`Dashed${currentEdgeStyle === "dashed" ? " \u2713" : ""}`, () => onEdgeStyle?.("dashed")),
2790
+ item(`Dotted${currentEdgeStyle === "dotted" ? " \u2713" : ""}`, () => onEdgeStyle?.("dotted")),
2791
+ divider2,
2792
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { padding: "4px 14px 2px", fontSize: 9, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Arrowhead" }),
2793
+ item(`Arrow${currentEdgeArrow !== "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("arrow")),
2794
+ item(`None${currentEdgeArrow === "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("none")),
2795
+ divider2,
2796
+ item("Reset routing", () => onEdgeResetRouting?.(), void 0, !edgeHasWaypoint),
2797
+ item("Delete edge", () => onEdgeDelete?.(), "#ef4444")
2798
+ ] }) : nodeId ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
2799
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Node" }),
2800
+ item("Rename (dbl-click)", onRename),
2801
+ item("Duplicate", onDuplicate),
2802
+ item("Disconnect all edges", onDisconnect),
2803
+ divider2,
2804
+ item("Delete node", onDelete, "#ef4444")
2805
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
2806
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Canvas" }),
2807
+ item("Add node here", onAddNode, acc.color),
2808
+ item("Re-center (Ctrl+0)", onReCenter),
2809
+ divider2,
2810
+ item("Undo (Ctrl+Z)", onUndo, void 0, !canUndo),
2811
+ item("Redo (Ctrl+Y)", onRedo, void 0, !canRedo)
2812
+ ] })
2813
+ }
2814
+ );
2815
+ }
2816
+
2817
+ // src/ui/render.tsx
2818
+ var import_react12 = require("react");
2819
+
2820
+ // src/ui/layout.ts
2821
+ var NODE_H2 = 48;
2822
+ var Q_BASE_H2 = 68;
2823
+ var Q_ANS_ROW_H2 = 80;
2824
+ var GRID = 24;
2825
+ var Q_CARD_PAD2 = 8;
2826
+ var MIN_NODE_W2 = 120;
2827
+ var MAX_NODE_W2 = 320;
2828
+ var MIN_Q_W2 = 220;
2829
+ function estimateTextW2(text, pxPerChar = 7.5) {
2830
+ return text.length * pxPerChar;
2831
+ }
2832
+ function nodeWidth2(label) {
2833
+ return Math.min(MAX_NODE_W2, Math.max(MIN_NODE_W2, Math.ceil(estimateTextW2(label) + 48)));
2834
+ }
2835
+ function answerCardW2(ans) {
2836
+ return Math.max(86, Math.ceil(Math.max(estimateTextW2(ans, 7.5) + 20, 56) + 32));
2837
+ }
2838
+ function questionNodeW2(node) {
2839
+ const answers = node.metadata?.answers ?? [];
2840
+ const headerW = estimateTextW2(node.label, 8) + 80;
2841
+ if (answers.length === 0) return Math.max(MIN_Q_W2, Math.ceil(headerW));
2842
+ const cardsW = answers.reduce((s2, a) => s2 + answerCardW2(a), 0) + (answers.length - 1) * Q_CARD_PAD2 + 2 * Q_CARD_PAD2;
2843
+ return Math.max(MIN_Q_W2, Math.ceil(Math.max(headerW, cardsW)));
2844
+ }
2845
+ function questionNodeH2(answers) {
2846
+ return Q_BASE_H2 + (answers.length === 0 ? 48 : Q_ANS_ROW_H2);
2847
+ }
2848
+ function nodeDims(node, variant) {
2849
+ if (variant === "question") {
2850
+ const answers = node.metadata?.answers ?? [];
2851
+ return { w: questionNodeW2(node), h: questionNodeH2(answers) };
2852
+ }
2853
+ return { w: nodeWidth2(node.label), h: NODE_H2 };
2854
+ }
2855
+ function snap(v) {
2856
+ return Math.round(v / GRID) * GRID;
2857
+ }
2858
+ function bezierPath2(x1, y1, x2, y2, exitDir = "bottom") {
2859
+ if (exitDir === "right") {
2860
+ const dx = Math.abs(x2 - x1), dy2 = Math.abs(y2 - y1);
2861
+ const c = Math.max(60, (dx + dy2) * 0.45);
2862
+ return `M ${x1} ${y1} C ${x1 + c} ${y1}, ${x2} ${y2 - c * 0.5}, ${x2} ${y2}`;
2863
+ }
2864
+ if (exitDir === "left") {
2865
+ const dx = Math.abs(x2 - x1), dy2 = Math.abs(y2 - y1);
2866
+ const c = Math.max(60, (dx + dy2) * 0.45);
2867
+ return `M ${x1} ${y1} C ${x1 - c} ${y1}, ${x2} ${y2 - c * 0.5}, ${x2} ${y2}`;
2868
+ }
2869
+ const dy = y2 - y1;
2870
+ const dyAbs = Math.abs(dy);
2871
+ const dxAbs = Math.abs(x2 - x1);
2872
+ const base = dy > 0 ? dyAbs * 0.55 : Math.max(90, dyAbs * 0.5 + dxAbs * 0.28);
2873
+ const curve = Math.max(36, Math.min(220, base));
2874
+ return `M ${x1} ${y1} C ${x1} ${y1 + curve}, ${x2} ${y2 - curve}, ${x2} ${y2}`;
2875
+ }
2876
+ function bezierPathVia(x1, y1, wx, wy, x2, y2) {
2877
+ const seg1 = bezierPath2(x1, y1, wx, wy, "bottom");
2878
+ const seg2 = bezierPath2(wx, wy, x2, y2, "bottom");
2879
+ const seg2NoM = seg2.replace(/^M\s+-?[\d.]+\s+-?[\d.]+\s+/, "");
2880
+ return seg1 + " " + seg2NoM;
2881
+ }
2882
+
2883
+ // src/ui/render.tsx
2884
+ var import_jsx_runtime8 = require("react/jsx-runtime");
2885
+ var STYLE_LABEL = { pointerEvents: "none", userSelect: "none" };
2886
+ var STYLE_BLUR = { filter: "blur(4px)" };
2887
+ var STYLE_EDGE_HIT = { cursor: "pointer" };
2888
+ var STYLE_NO_EVENTS = { pointerEvents: "none" };
2889
+ var STYLE_PORT_HOVER = { cursor: "crosshair", filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.18))" };
2890
+ var STYLE_WAYPOINT = { cursor: "grab", filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.25))" };
2891
+ var STYLE_EDGE_LABEL_HIT = { cursor: "text" };
2892
+ function NodeShape({ node, selected, variant, stepNumber, t, isDark, w }) {
2893
+ const acc = variantAccent(variant, isDark);
2894
+ const cx = w / 2, cy = NODE_H2 / 2;
2895
+ const stroke = selected ? acc.color : t.nodeStroke;
2896
+ const fill = selected ? t.nodeSelectedFill : t.nodeFill;
2897
+ const sw = selected ? 1.75 : 1.25;
2898
+ const glow = selected && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_jsx_runtime8.Fragment, { children: node.shape === "circle" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2899
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx, cy, r: NODE_H2 / 2 + 3, fill: "none", stroke: acc.color, strokeWidth: 6, opacity: 0.18, style: STYLE_BLUR }),
2900
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx, cy, r: NODE_H2 / 2 + 1.5, fill: "none", stroke: acc.color, strokeWidth: 1, opacity: 0.55 })
2901
+ ] }) : node.shape === "diamond" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2902
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2903
+ "polygon",
2904
+ {
2905
+ points: `${cx},${-5} ${w + 5},${cy} ${cx},${NODE_H2 + 5} ${-5},${cy}`,
2906
+ fill: "none",
2907
+ stroke: acc.color,
2908
+ strokeWidth: 6,
2909
+ opacity: 0.18,
2910
+ style: STYLE_BLUR
2911
+ }
2912
+ ),
2913
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2914
+ "polygon",
2915
+ {
2916
+ points: `${cx},${-2} ${w + 2},${cy} ${cx},${NODE_H2 + 2} ${-2},${cy}`,
2917
+ fill: "none",
2918
+ stroke: acc.color,
2919
+ strokeWidth: 1,
2920
+ opacity: 0.55
2921
+ }
2922
+ )
2923
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2924
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2925
+ "rect",
2926
+ {
2927
+ x: -4,
2928
+ y: -4,
2929
+ width: w + 8,
2930
+ height: NODE_H2 + 8,
2931
+ rx: 18,
2932
+ fill: "none",
2933
+ stroke: acc.color,
2934
+ strokeWidth: 6,
2935
+ opacity: 0.18,
2936
+ style: STYLE_BLUR
2937
+ }
2938
+ ),
2939
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2940
+ "rect",
2941
+ {
2942
+ x: -1.5,
2943
+ y: -1.5,
2944
+ width: w + 3,
2945
+ height: NODE_H2 + 3,
2946
+ rx: 15.5,
2947
+ fill: "none",
2948
+ stroke: acc.color,
2949
+ strokeWidth: 1,
2950
+ opacity: 0.5
2951
+ }
2952
+ )
2953
+ ] }) });
2954
+ const badgeColor = isDark ? ACCENT.emeraldDark : ACCENT.emerald;
2955
+ const badge = variant === "journey" && stepNumber !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2956
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: 14, cy: 14, r: 10, fill: badgeColor }),
2957
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("text", { x: 14, y: 18, textAnchor: "middle", fontSize: 9, fill: "white", fontWeight: "700", style: STYLE_LABEL, children: stepNumber })
2958
+ ] });
2959
+ switch (node.shape) {
2960
+ case "diamond": {
2961
+ const pts = `${cx},0 ${w},${cy} ${cx},${NODE_H2} 0,${cy}`;
2962
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2963
+ glow,
2964
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polygon", { points: pts, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
2965
+ badge
2966
+ ] });
2967
+ }
2968
+ case "circle":
2969
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2970
+ glow,
2971
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx, cy, r: NODE_H2 / 2 - 1, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
2972
+ badge
2973
+ ] });
2974
+ case "parallelogram":
2975
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2976
+ glow,
2977
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polygon", { points: `14,0 ${w},0 ${w - 14},${NODE_H2} 0,${NODE_H2}`, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
2978
+ badge
2979
+ ] });
2980
+ default:
2981
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2982
+ glow,
2983
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { width: w, height: NODE_H2, rx: 14, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
2984
+ badge
2985
+ ] });
2986
+ }
2987
+ }
2988
+ var ANSWER_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
2989
+ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
2990
+ const answers = node.metadata?.answers ?? [];
2991
+ const totalH = questionNodeH2(answers);
2992
+ const amber = isDark ? ACCENT.amberDark : ACCENT.amber;
2993
+ const amberSoft = isDark ? "rgba(251,191,36,0.14)" : "#fef9ee";
2994
+ const amberLine = isDark ? "rgba(251,191,36,0.18)" : "#fde68a";
2995
+ const nodeBg = isDark ? "#1e293b" : "#ffffff";
2996
+ const nodeBorder = selected ? amber : isDark ? "rgba(251,191,36,0.25)" : "#fde68a";
2997
+ const cardBg = isDark ? "#0f172a" : "#fffdf7";
2998
+ const cardBgConnected = isDark ? "rgba(251,191,36,0.12)" : "#fef3c7";
2999
+ const cardBorder = isDark ? "#1e293b" : "#fde68a";
3000
+ const textMain = isDark ? "#f1f5f9" : "#1e293b";
3001
+ const textSub = isDark ? "#64748b" : "#94a3b8";
3002
+ const textAns = isDark ? "#cbd5e1" : "#374151";
3003
+ const portRowY = Q_BASE_H2 + Q_ANS_ROW_H2 - 8;
3004
+ const glow = selected && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
3005
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3006
+ "rect",
3007
+ {
3008
+ x: -4,
3009
+ y: -4,
3010
+ width: qW + 8,
3011
+ height: totalH + 8,
3012
+ rx: 18,
3013
+ fill: "none",
3014
+ stroke: amber,
3015
+ strokeWidth: 6,
3016
+ opacity: 0.2,
3017
+ style: STYLE_BLUR
3018
+ }
3019
+ ),
3020
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3021
+ "rect",
3022
+ {
3023
+ x: -1.5,
3024
+ y: -1.5,
3025
+ width: qW + 3,
3026
+ height: totalH + 3,
3027
+ rx: 15.5,
3028
+ fill: "none",
3029
+ stroke: amber,
3030
+ strokeWidth: 1,
3031
+ opacity: 0.55
3032
+ }
3033
+ )
3034
+ ] });
3035
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
3036
+ glow,
3037
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { width: qW, height: totalH, rx: 14, fill: nodeBg, stroke: nodeBorder, strokeWidth: selected ? 2 : 1.5, filter: "url(#nodeShadow)" }),
3038
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("clipPath", { id: `qhdr-${node.id}`, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { width: qW, height: Q_BASE_H2, rx: 14 }) }),
3039
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { width: qW, height: Q_BASE_H2, fill: amberSoft, clipPath: `url(#qhdr-${node.id})` }),
3040
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { x: 0, y: 0, width: 4, height: Q_BASE_H2, rx: 2, fill: amber }),
3041
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { x: 12, y: 14, width: 28, height: 28, rx: 8, fill: amber }),
3042
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("text", { x: 26, y: 33, textAnchor: "middle", fontSize: 15, fontWeight: "900", fill: "white", style: STYLE_LABEL, children: "?" }),
3043
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
3044
+ "text",
3045
+ {
3046
+ style: STYLE_LABEL,
3047
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
3048
+ children: [
3049
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("tspan", { x: 50, y: 27, fontSize: 9, fontWeight: 700, fill: textSub, letterSpacing: 0.6, textAnchor: "start", children: "QUESTION" }),
3050
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("tspan", { x: 50, dy: 15, fontSize: 13, fontWeight: 700, fill: selected ? amber : textMain, textAnchor: "start", children: node.label })
3051
+ ]
3052
+ }
3053
+ ),
3054
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: 0, y1: Q_BASE_H2, x2: qW, y2: Q_BASE_H2, stroke: amberLine, strokeWidth: 1 }),
3055
+ answers.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
3056
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("text", { x: qW / 2, y: Q_BASE_H2 + 22, textAnchor: "middle", fontSize: 10, fill: amber, opacity: 0.4, fontWeight: 600, style: STYLE_LABEL, children: "No answers yet" }),
3057
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("text", { x: qW / 2, y: Q_BASE_H2 + 36, textAnchor: "middle", fontSize: 9, fill: textSub, opacity: 0.7, style: STYLE_LABEL, children: "Open panel \u2192 Add Answer" })
3058
+ ] }),
3059
+ answers.map((ans, i) => {
3060
+ const prevW = answers.slice(0, i).reduce((s2, a) => s2 + answerCardW2(a) + Q_CARD_PAD2, 0);
3061
+ const cW = answerCardW2(ans);
3062
+ const cardX = Q_CARD_PAD2 + prevW;
3063
+ const cardY = Q_BASE_H2 + 7;
3064
+ const cardH = Q_ANS_ROW_H2 - 20;
3065
+ const cx = cardX + cW / 2;
3066
+ const connected = edges.some((e) => e.from === node.id && e.label === ans);
3067
+ const letter = i < 26 ? ANSWER_LETTERS[i] : `${i + 1}`;
3068
+ const maxChars = Math.max(2, Math.floor((cW - 20) / 7.5));
3069
+ const displayAns = ans.length > maxChars ? ans.slice(0, maxChars - 1) + "\u2026" : ans;
3070
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("g", { children: [
3071
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3072
+ "rect",
3073
+ {
3074
+ x: cardX,
3075
+ y: cardY,
3076
+ width: cW,
3077
+ height: cardH,
3078
+ rx: 8,
3079
+ fill: connected ? cardBgConnected : cardBg,
3080
+ stroke: connected ? amber : cardBorder,
3081
+ strokeWidth: connected ? 1.5 : 1
3082
+ }
3083
+ ),
3084
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3085
+ "rect",
3086
+ {
3087
+ x: cx - 11,
3088
+ y: cardY + 7,
3089
+ width: 22,
3090
+ height: 22,
3091
+ rx: 6,
3092
+ fill: connected ? amber : isDark ? "#1e293b" : "#fef3c7"
3093
+ }
3094
+ ),
3095
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3096
+ "text",
3097
+ {
3098
+ x: cx,
3099
+ y: cardY + 22,
3100
+ textAnchor: "middle",
3101
+ fontSize: 10,
3102
+ fontWeight: 800,
3103
+ fill: connected ? "#fff" : amber,
3104
+ style: STYLE_LABEL,
3105
+ children: letter
3106
+ }
3107
+ ),
3108
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3109
+ "text",
3110
+ {
3111
+ x: cx,
3112
+ y: cardY + 46,
3113
+ textAnchor: "middle",
3114
+ fontSize: 11,
3115
+ fontWeight: 500,
3116
+ fill: connected ? isDark ? "#fef3c7" : "#92400e" : textAns,
3117
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
3118
+ style: STYLE_LABEL,
3119
+ children: displayAns
3120
+ }
3121
+ ),
3122
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3123
+ "circle",
3124
+ {
3125
+ cx,
3126
+ cy: portRowY,
3127
+ r: 7,
3128
+ fill: connected ? amber : isDark ? "#0f172a" : "#fff",
3129
+ stroke: amber,
3130
+ strokeWidth: 1.5,
3131
+ style: STYLE_PORT_HOVER,
3132
+ onMouseDown: (e) => onAnswerPortDown(e, node.id, ans, cx, portRowY)
3133
+ }
3134
+ ),
3135
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3136
+ "path",
3137
+ {
3138
+ d: `M ${cx - 3} ${portRowY - 2} L ${cx} ${portRowY + 2} L ${cx + 3} ${portRowY - 2}`,
3139
+ fill: "none",
3140
+ stroke: connected ? "#fff" : amber,
3141
+ strokeWidth: 1.5,
3142
+ strokeLinecap: "round",
3143
+ strokeLinejoin: "round",
3144
+ style: STYLE_NO_EVENTS
3145
+ }
3146
+ )
3147
+ ] }, ans + i);
3148
+ })
3149
+ ] });
3150
+ }
3151
+ function EdgeLine({ edge, nodes, variant, t, isDark, acc, editing, editValue, onEditChange, onEditCommit, onEditCancel, onDoubleClick, onContextMenu, onWaypointDown }) {
3152
+ const [hovered, setHovered] = (0, import_react12.useState)(false);
3153
+ const from = nodes.find((n) => n.id === edge.from);
3154
+ const to = nodes.find((n) => n.id === edge.to);
3155
+ if (!from || !to) return null;
3156
+ let x1, y1, exitDir = "bottom";
3157
+ const amberColor = isDark ? ACCENT.amberDark : ACCENT.amber;
3158
+ if (variant === "question") {
3159
+ const answers = from.metadata?.answers ?? [];
3160
+ const idx = answers.indexOf(edge.label ?? "");
3161
+ if (idx >= 0) {
3162
+ const prevW = answers.slice(0, idx).reduce((s2, a) => s2 + answerCardW2(a) + Q_CARD_PAD2, 0);
3163
+ const cW = answerCardW2(answers[idx]);
3164
+ x1 = (from.x ?? 0) + Q_CARD_PAD2 + prevW + cW / 2;
3165
+ y1 = (from.y ?? 0) + Q_BASE_H2 + Q_ANS_ROW_H2 - 8;
3166
+ exitDir = "bottom";
3167
+ } else {
3168
+ const fqW = questionNodeW2(from);
3169
+ x1 = (from.x ?? 0) + fqW / 2;
3170
+ y1 = (from.y ?? 0) + questionNodeH2(answers);
3171
+ }
3172
+ } else {
3173
+ const fnW = nodeWidth2(from.label);
3174
+ x1 = (from.x ?? 0) + fnW / 2;
3175
+ y1 = (from.y ?? 0) + NODE_H2;
3176
+ }
3177
+ const toW = variant === "question" ? questionNodeW2(to) : nodeWidth2(to.label);
3178
+ const x2 = (to.x ?? 0) + toW / 2;
3179
+ const y2 = to.y ?? 0;
3180
+ const wp = edge.waypoint;
3181
+ const d = wp ? bezierPathVia(x1, y1, wp.x, wp.y, x2, y2) : bezierPath2(x1, y1, x2, y2, exitDir);
3182
+ const hx = wp ? wp.x : (x1 + x2) / 2;
3183
+ const hy = wp ? wp.y : (y1 + y2) / 2;
3184
+ const mx = hx, my = hy - 8;
3185
+ const dash = edge.style === "dashed" ? "7 4" : edge.style === "dotted" ? "2 4" : void 0;
3186
+ const edgeClr = variant === "question" ? amberColor : t.edgeColor;
3187
+ const isAmber = variant === "question";
3188
+ const labelW = edge.label ? Math.max(60, Math.ceil(estimateTextW2(edge.label, 7) + 18)) : 60;
3189
+ const showHandle = !!onWaypointDown && (hovered || !!wp);
3190
+ const flowClass = dash ? void 0 : isAmber ? "edge-flow-amber" : "edge-flow";
3191
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
3192
+ "g",
3193
+ {
3194
+ onDoubleClick: (e) => {
3195
+ e.stopPropagation();
3196
+ onDoubleClick?.(edge.id);
3197
+ },
3198
+ onContextMenu: (e) => {
3199
+ onContextMenu?.(e, edge.id);
3200
+ },
3201
+ onMouseEnter: () => setHovered(true),
3202
+ onMouseLeave: () => setHovered(false),
3203
+ children: [
3204
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d, fill: "none", stroke: "transparent", strokeWidth: 14, style: STYLE_EDGE_HIT }),
3205
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3206
+ "path",
3207
+ {
3208
+ d,
3209
+ fill: "none",
3210
+ stroke: edgeClr,
3211
+ strokeWidth: isAmber ? 2 : 1.5,
3212
+ strokeLinecap: "round",
3213
+ className: flowClass,
3214
+ strokeDasharray: dash,
3215
+ markerEnd: isAmber ? "url(#arrowAmber)" : "url(#arrowhead)",
3216
+ opacity: isAmber ? 0.85 : 0.9,
3217
+ style: STYLE_NO_EVENTS
3218
+ }
3219
+ ),
3220
+ showHandle && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3221
+ "circle",
3222
+ {
3223
+ cx: hx,
3224
+ cy: hy,
3225
+ r: wp ? 5 : 4,
3226
+ fill: wp ? acc.color : isDark ? "#1e293b" : "#fff",
3227
+ stroke: acc.color,
3228
+ strokeWidth: 1.5,
3229
+ style: STYLE_WAYPOINT,
3230
+ onMouseDown: (e) => {
3231
+ e.stopPropagation();
3232
+ onWaypointDown?.(e, edge.id);
3233
+ }
3234
+ }
3235
+ ),
3236
+ editing && !isAmber ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("foreignObject", { x: mx - labelW / 2, y: my - 12, width: labelW, height: 22, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3237
+ "input",
3238
+ {
3239
+ autoFocus: true,
3240
+ value: editValue ?? "",
3241
+ onChange: (e) => onEditChange?.(e.target.value),
3242
+ onBlur: () => onEditCommit?.(),
3243
+ onKeyDown: (e) => {
3244
+ if (e.key === "Enter") {
3245
+ e.preventDefault();
3246
+ onEditCommit?.();
3247
+ }
3248
+ if (e.key === "Escape") {
3249
+ e.preventDefault();
3250
+ onEditCancel?.();
3251
+ }
3252
+ },
3253
+ onMouseDown: (e) => e.stopPropagation(),
3254
+ style: {
3255
+ width: "100%",
3256
+ height: "100%",
3257
+ border: "none",
3258
+ borderRadius: 6,
3259
+ outline: `2px solid ${acc.color}`,
3260
+ textAlign: "center",
3261
+ fontSize: 10,
3262
+ fontWeight: 500,
3263
+ background: t.inputBg,
3264
+ color: t.inputText,
3265
+ boxSizing: "border-box",
3266
+ padding: "0 6px",
3267
+ fontFamily: "inherit"
3268
+ }
3269
+ }
3270
+ ) }) : edge.label && !isAmber ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
3271
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3272
+ "rect",
3273
+ {
3274
+ x: mx - labelW / 2,
3275
+ y: my - 11,
3276
+ width: labelW,
3277
+ height: 19,
3278
+ rx: 5,
3279
+ fill: t.panelBg,
3280
+ stroke: t.cardBorder,
3281
+ strokeWidth: 1,
3282
+ style: STYLE_EDGE_LABEL_HIT
3283
+ }
3284
+ ),
3285
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3286
+ "text",
3287
+ {
3288
+ x: mx,
3289
+ y: my + 4,
3290
+ textAnchor: "middle",
3291
+ fontSize: 10,
3292
+ fill: t.textSecondary,
3293
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
3294
+ fontWeight: "500",
3295
+ style: STYLE_LABEL,
3296
+ children: edge.label
3297
+ }
3298
+ )
3299
+ ] }) : null
3300
+ ]
3301
+ }
3302
+ );
3303
+ }
3304
+
3305
+ // src/ui/hooks/useHistory.ts
3306
+ var import_react13 = require("react");
3307
+ var MAX_HISTORY = 80;
3308
+ function useHistory(initial, onChange) {
3309
+ const [state, setState] = (0, import_react13.useState)(initial);
3310
+ const stackRef = (0, import_react13.useRef)([initial]);
3311
+ const idxRef = (0, import_react13.useRef)(0);
3312
+ const [, setTick] = (0, import_react13.useState)(0);
3313
+ const bump = () => setTick((n) => n + 1);
3314
+ const apply = (0, import_react13.useCallback)(
3315
+ (next) => {
3316
+ setState(next);
3317
+ onChange?.(next);
3318
+ },
3319
+ [onChange]
3320
+ );
3321
+ const applyAndPush = (0, import_react13.useCallback)(
3322
+ (next) => {
3323
+ const stack = stackRef.current.slice(0, idxRef.current + 1);
3324
+ stack.push(next);
3325
+ if (stack.length > MAX_HISTORY) stack.shift();
3326
+ stackRef.current = stack;
3327
+ idxRef.current = stack.length - 1;
3328
+ setState(next);
3329
+ onChange?.(next);
3330
+ bump();
3331
+ },
3332
+ [onChange]
3333
+ );
3334
+ const undo = (0, import_react13.useCallback)(() => {
3335
+ if (idxRef.current <= 0) return;
3336
+ idxRef.current--;
3337
+ const next = stackRef.current[idxRef.current];
3338
+ setState(next);
3339
+ onChange?.(next);
3340
+ bump();
3341
+ }, [onChange]);
3342
+ const redo = (0, import_react13.useCallback)(() => {
3343
+ if (idxRef.current >= stackRef.current.length - 1) return;
3344
+ idxRef.current++;
3345
+ const next = stackRef.current[idxRef.current];
3346
+ setState(next);
3347
+ onChange?.(next);
3348
+ bump();
3349
+ }, [onChange]);
3350
+ return {
3351
+ state,
3352
+ apply,
3353
+ applyAndPush,
3354
+ undo,
3355
+ redo,
3356
+ canUndo: idxRef.current > 0,
3357
+ canRedo: idxRef.current < stackRef.current.length - 1
3358
+ };
3359
+ }
3360
+
3361
+ // src/ui/hooks/useCanvasWheel.ts
3362
+ var import_react14 = require("react");
3363
+ function useCanvasWheel(ref, setTransform, options = {}) {
3364
+ const { min = 0.15, max = 3, factor = 0.1 } = options;
3365
+ (0, import_react14.useEffect)(() => {
3366
+ const el = ref.current;
3367
+ if (!el) return;
3368
+ const onWheel = (e) => {
3369
+ e.preventDefault();
3370
+ const rect = el.getBoundingClientRect();
3371
+ const px = e.clientX - rect.left;
3372
+ const py = e.clientY - rect.top;
3373
+ const delta = e.deltaY > 0 ? 1 - factor : 1 + factor;
3374
+ setTransform((tr) => {
3375
+ const scale = Math.min(max, Math.max(min, tr.scale * delta));
3376
+ return {
3377
+ scale,
3378
+ x: px - (px - tr.x) * (scale / tr.scale),
3379
+ y: py - (py - tr.y) * (scale / tr.scale)
3380
+ };
3381
+ });
3382
+ };
3383
+ el.addEventListener("wheel", onWheel, { passive: false });
3384
+ return () => el.removeEventListener("wheel", onWheel);
3385
+ }, [ref, setTransform, min, max, factor]);
3386
+ }
3387
+
3388
+ // src/ui/hooks/useCanvasTouch.ts
3389
+ var import_react15 = require("react");
3390
+ function useCanvasTouch(ref, {
3391
+ transform,
3392
+ setTransform,
3393
+ onLongPress,
3394
+ minScale = 0.15,
3395
+ maxScale = 3,
3396
+ longPressMs = 550,
3397
+ longPressSlop = 8
3398
+ }) {
3399
+ (0, import_react15.useEffect)(() => {
3400
+ const el = ref.current;
3401
+ if (!el) return;
3402
+ let touchPan = null;
3403
+ let pinch = null;
3404
+ let longPressTimer = null;
3405
+ let longPressStart = null;
3406
+ let longPressFired = false;
3407
+ const dist = (a, b) => Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
3408
+ const cancelLongPress = () => {
3409
+ if (longPressTimer) {
3410
+ clearTimeout(longPressTimer);
3411
+ longPressTimer = null;
3412
+ }
3413
+ longPressStart = null;
3414
+ };
3415
+ const onStart = (e) => {
3416
+ if (e.touches.length === 2) {
3417
+ e.preventDefault();
3418
+ cancelLongPress();
3419
+ const [a, b] = [e.touches[0], e.touches[1]];
3420
+ const rect = el.getBoundingClientRect();
3421
+ pinch = {
3422
+ dist: dist(a, b),
3423
+ cx: (a.clientX + b.clientX) / 2 - rect.left,
3424
+ cy: (a.clientY + b.clientY) / 2 - rect.top,
3425
+ scale: transform.scale,
3426
+ tx: transform.x,
3427
+ ty: transform.y
3428
+ };
3429
+ touchPan = null;
3430
+ return;
3431
+ }
3432
+ if (e.touches.length === 1) {
3433
+ const target = e.target;
3434
+ const t0 = e.touches[0];
3435
+ longPressFired = false;
3436
+ longPressStart = { x: t0.clientX, y: t0.clientY };
3437
+ longPressTimer = setTimeout(() => {
3438
+ if (!longPressStart) return;
3439
+ longPressFired = true;
3440
+ touchPan = null;
3441
+ onLongPress(longPressStart.x, longPressStart.y);
3442
+ }, longPressMs);
3443
+ if (target?.dataset.bg !== "1" && target !== el) return;
3444
+ touchPan = { ox: t0.clientX, oy: t0.clientY, tx: transform.x, ty: transform.y };
3445
+ }
3446
+ };
3447
+ const onMove = (e) => {
3448
+ if (pinch && e.touches.length === 2) {
3449
+ e.preventDefault();
3450
+ const [a, b] = [e.touches[0], e.touches[1]];
3451
+ const ratio = dist(a, b) / pinch.dist;
3452
+ const scale = Math.min(maxScale, Math.max(minScale, pinch.scale * ratio));
3453
+ setTransform({
3454
+ scale,
3455
+ x: pinch.cx - (pinch.cx - pinch.tx) * (scale / pinch.scale),
3456
+ y: pinch.cy - (pinch.cy - pinch.ty) * (scale / pinch.scale)
3457
+ });
3458
+ return;
3459
+ }
3460
+ if (e.touches.length === 1) {
3461
+ const t0 = e.touches[0];
3462
+ if (longPressStart && (Math.abs(t0.clientX - longPressStart.x) > longPressSlop || Math.abs(t0.clientY - longPressStart.y) > longPressSlop)) {
3463
+ cancelLongPress();
3464
+ }
3465
+ if (touchPan) {
3466
+ e.preventDefault();
3467
+ const pan = touchPan;
3468
+ setTransform((tr) => ({
3469
+ ...tr,
3470
+ x: pan.tx + (t0.clientX - pan.ox),
3471
+ y: pan.ty + (t0.clientY - pan.oy)
3472
+ }));
3473
+ }
3474
+ }
3475
+ };
3476
+ const onEnd = (e) => {
3477
+ cancelLongPress();
3478
+ if (longPressFired) {
3479
+ e.preventDefault();
3480
+ longPressFired = false;
3481
+ }
3482
+ if (e.touches.length === 0) {
3483
+ touchPan = null;
3484
+ pinch = null;
3485
+ }
3486
+ if (e.touches.length === 1) pinch = null;
3487
+ };
3488
+ el.addEventListener("touchstart", onStart, { passive: false });
3489
+ el.addEventListener("touchmove", onMove, { passive: false });
3490
+ el.addEventListener("touchend", onEnd);
3491
+ el.addEventListener("touchcancel", onEnd);
3492
+ return () => {
3493
+ cancelLongPress();
3494
+ el.removeEventListener("touchstart", onStart);
3495
+ el.removeEventListener("touchmove", onMove);
3496
+ el.removeEventListener("touchend", onEnd);
3497
+ el.removeEventListener("touchcancel", onEnd);
3498
+ };
3499
+ }, [ref, transform.scale, transform.x, transform.y, setTransform, onLongPress, minScale, maxScale, longPressMs, longPressSlop]);
3500
+ }
3501
+
3502
+ // src/ui/hooks/useElementSize.ts
3503
+ var import_react16 = require("react");
3504
+ function useElementSize(ref) {
3505
+ const [size, setSize] = (0, import_react16.useState)({ w: 0, h: 0 });
3506
+ (0, import_react16.useEffect)(() => {
3507
+ const el = ref.current;
3508
+ if (!el || typeof ResizeObserver === "undefined") return;
3509
+ const measure = () => {
3510
+ const r = el.getBoundingClientRect();
3511
+ setSize({ w: r.width, h: r.height });
3512
+ };
3513
+ const ro = new ResizeObserver(measure);
3514
+ ro.observe(el);
3515
+ measure();
3516
+ return () => ro.disconnect();
3517
+ }, [ref]);
3518
+ return size;
3519
+ }
3520
+
3521
+ // src/ui/alignment.ts
3522
+ var ALIGN_SNAP_THRESHOLD = 4;
3523
+ function findSiblingSnap(dragged, others, threshold = ALIGN_SNAP_THRESHOLD) {
3524
+ const dL = dragged.x;
3525
+ const dC = dragged.x + dragged.w / 2;
3526
+ const dR = dragged.x + dragged.w;
3527
+ const dT = dragged.y;
3528
+ const dM = dragged.y + dragged.h / 2;
3529
+ const dB = dragged.y + dragged.h;
3530
+ let bestX = null;
3531
+ let bestY = null;
3532
+ others.forEach((o, idx) => {
3533
+ const oL = o.x;
3534
+ const oC = o.x + o.w / 2;
3535
+ const oR = o.x + o.w;
3536
+ const oT = o.y;
3537
+ const oM = o.y + o.h / 2;
3538
+ const oB = o.y + o.h;
3539
+ const xCandidates = [
3540
+ { delta: oL - dL, pos: oL },
3541
+ // left -> left
3542
+ { delta: oC - dC, pos: oC },
3543
+ // center -> center
3544
+ { delta: oR - dR, pos: oR }
3545
+ // right -> right
3546
+ ];
3547
+ for (const c of xCandidates) {
3548
+ if (Math.abs(c.delta) < threshold && (!bestX || Math.abs(c.delta) < Math.abs(bestX.delta))) {
3549
+ bestX = { delta: c.delta, pos: c.pos, otherIdx: idx };
3550
+ }
3551
+ }
3552
+ const yCandidates = [
3553
+ { delta: oT - dT, pos: oT },
3554
+ // top -> top
3555
+ { delta: oM - dM, pos: oM },
3556
+ // middle -> middle
3557
+ { delta: oB - dB, pos: oB }
3558
+ // bottom -> bottom
3559
+ ];
3560
+ for (const c of yCandidates) {
3561
+ if (Math.abs(c.delta) < threshold && (!bestY || Math.abs(c.delta) < Math.abs(bestY.delta))) {
3562
+ bestY = { delta: c.delta, pos: c.pos, otherIdx: idx };
3563
+ }
3564
+ }
3565
+ });
3566
+ let x = dragged.x;
3567
+ let y = dragged.y;
3568
+ let guideX;
3569
+ let guideY;
3570
+ if (bestX) {
3571
+ const bx = bestX;
3572
+ x = dragged.x + bx.delta;
3573
+ const o = others[bx.otherIdx];
3574
+ guideX = {
3575
+ pos: bx.pos,
3576
+ minY: Math.min(y, o.y) - 12,
3577
+ maxY: Math.max(y + dragged.h, o.y + o.h) + 12
3578
+ };
3579
+ }
3580
+ if (bestY) {
3581
+ const by = bestY;
3582
+ y = dragged.y + by.delta;
3583
+ const o = others[by.otherIdx];
3584
+ guideY = {
3585
+ pos: by.pos,
3586
+ minX: Math.min(x, o.x) - 12,
3587
+ maxX: Math.max(x + dragged.w, o.x + o.w) + 12
3588
+ };
3589
+ }
3590
+ return { x, y, guideX, guideY };
3591
+ }
3592
+
3593
+ // src/ui/traversal.ts
3594
+ function nearestInDirection(fromX, fromY, dir, candidates) {
3595
+ const matches = candidates.filter((c) => {
3596
+ const dx = c.x - fromX;
3597
+ const dy = c.y - fromY;
3598
+ if (dx === 0 && dy === 0) return false;
3599
+ if (dir === "right") return dx > 0 && Math.abs(dy) <= Math.abs(dx);
3600
+ if (dir === "left") return dx < 0 && Math.abs(dy) <= Math.abs(dx);
3601
+ if (dir === "down") return dy > 0 && Math.abs(dx) <= Math.abs(dy);
3602
+ return dy < 0 && Math.abs(dx) <= Math.abs(dy);
3603
+ });
3604
+ if (matches.length === 0) return null;
3605
+ matches.sort(
3606
+ (a, b) => Math.hypot(a.x - fromX, a.y - fromY) - Math.hypot(b.x - fromX, b.y - fromY)
3607
+ );
3608
+ return matches[0].id;
3609
+ }
3610
+
3611
+ // src/ui/DiagramEditor.tsx
3612
+ var import_jsx_runtime9 = require("react/jsx-runtime");
3613
+ var STYLE_LABEL2 = { pointerEvents: "none", userSelect: "none" };
3614
+ var STYLE_LIVE_PORT = { opacity: 0.85, pointerEvents: "none" };
3615
+ var STYLE_SR_ONLY = { position: "absolute", width: 1, height: 1, padding: 0, margin: -1, overflow: "hidden", clip: "rect(0 0 0 0)", whiteSpace: "nowrap", border: 0 };
3616
+ var STYLE_FLEX_ROW = { flex: 1, display: "flex", overflow: "hidden" };
3617
+ function DiagramEditor(props) {
3618
+ if (props.initialModel?.type === "sequence") {
3619
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3620
+ SequenceEditor,
3621
+ {
3622
+ initialModel: props.initialModel,
3623
+ onChange: props.onChange,
3624
+ onExport: props.onExport,
3625
+ height: props.height,
3626
+ allowedExports: props.allowedExports,
3627
+ allowImport: props.allowImport,
3628
+ theme: props.theme,
3629
+ themeOverrides: props.themeOverrides
3630
+ }
3631
+ );
3632
+ }
3633
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(FlowchartEditor, { ...props });
3634
+ }
3635
+ function FlowchartEditor({
3636
+ initialModel,
3637
+ onChange,
3638
+ onExport,
3639
+ height = 600,
3640
+ allowedExports,
3641
+ allowImport = true,
3642
+ variant = "flowchart",
3643
+ theme = "auto",
3644
+ themeOverrides
3645
+ }) {
3646
+ const base = initialModel ? { ...initialModel, variant: initialModel.variant ?? variant } : presetFlowchartModel(variant);
3647
+ const notify = (0, import_react17.useCallback)((m) => onChange?.(m), [onChange]);
3648
+ const history = useHistory(base, notify);
3649
+ const { state: model, apply: applyModel, applyAndPush, undo, redo } = history;
3650
+ const [transform, setTransform] = (0, import_react17.useState)({ x: 60, y: 60, scale: 1 });
3651
+ const [selected, setSelected] = (0, import_react17.useState)(null);
3652
+ const [selectedSet, setSelectedSet] = (0, import_react17.useState)(() => /* @__PURE__ */ new Set());
3653
+ const [drag, setDrag] = (0, import_react17.useState)(null);
3654
+ const [pan, setPan] = (0, import_react17.useState)(null);
3655
+ const [boxSel, setBoxSel] = (0, import_react17.useState)(null);
3656
+ const [liveEdge, setLiveEdge] = (0, import_react17.useState)(null);
3657
+ const [alignGuides, setAlignGuides] = (0, import_react17.useState)(null);
3658
+ const [waypointDrag, setWaypointDrag] = (0, import_react17.useState)(null);
3659
+ const groupDragOriginsRef = (0, import_react17.useRef)(null);
3660
+ const clipboardRef = (0, import_react17.useRef)(null);
3661
+ const selectOne = (0, import_react17.useCallback)((id) => {
3662
+ setSelected(id);
3663
+ setSelectedSet(id ? /* @__PURE__ */ new Set([id]) : /* @__PURE__ */ new Set());
3664
+ }, []);
3665
+ const toggleSelect = (0, import_react17.useCallback)((id) => {
3666
+ setSelectedSet((prev) => {
3667
+ const next = new Set(prev);
3668
+ if (next.has(id)) {
3669
+ next.delete(id);
3670
+ const last = next.size ? Array.from(next)[next.size - 1] : null;
3671
+ setSelected(last);
3672
+ } else {
3673
+ next.add(id);
3674
+ setSelected(id);
3675
+ }
3676
+ return next;
3677
+ });
3678
+ }, []);
3679
+ const clearSelection = (0, import_react17.useCallback)(() => {
3680
+ setSelected(null);
3681
+ setSelectedSet(/* @__PURE__ */ new Set());
3682
+ }, []);
3683
+ const [editingId, setEditingId] = (0, import_react17.useState)(null);
3684
+ const [editLabel, setEditLabel] = (0, import_react17.useState)("");
3685
+ const [editingEdgeId, setEditingEdgeId] = (0, import_react17.useState)(null);
3686
+ const [editEdgeLabel, setEditEdgeLabel] = (0, import_react17.useState)("");
3687
+ const [hoveredId, setHoveredId] = (0, import_react17.useState)(null);
3688
+ const [ctxMenu, setCtxMenu] = (0, import_react17.useState)(null);
3689
+ const [navOpen, setNavOpen] = (0, import_react17.useState)(true);
3690
+ const [announcement, setAnnouncement] = (0, import_react17.useState)("");
3691
+ const svgRef = (0, import_react17.useRef)(null);
3692
+ const containerRef = (0, import_react17.useRef)(null);
3693
+ const reducedMotion = usePrefersReducedMotion();
3694
+ const { t, isDark } = useEditorTheme(theme, themeOverrides, { light: lightTheme, dark: darkTheme });
3695
+ const isCoarse = useIsCoarsePointer();
3696
+ const portR = isCoarse ? 9 : 6;
3697
+ const viewport = useElementSize(svgRef);
3698
+ const reCenter = (0, import_react17.useCallback)(() => {
3699
+ if (!svgRef.current) return;
3700
+ const rect = svgRef.current.getBoundingClientRect();
3701
+ const W2 = rect.width, H2 = rect.height;
3702
+ if (model.nodes.length === 0) {
3703
+ setTransform({ x: W2 / 2, y: H2 / 2, scale: 1 });
3704
+ return;
3705
+ }
3706
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3707
+ for (const n of model.nodes) {
3708
+ const nx = n.x ?? 0, ny = n.y ?? 0;
3709
+ const { w: nw, h: nh } = nodeDims(n, variant);
3710
+ minX = Math.min(minX, nx);
3711
+ minY = Math.min(minY, ny);
3712
+ maxX = Math.max(maxX, nx + nw);
3713
+ maxY = Math.max(maxY, ny + nh);
3714
+ }
3715
+ const pad = 48;
3716
+ const scaleX = (W2 - pad * 2) / (maxX - minX || 1);
3717
+ const scaleY = (H2 - pad * 2) / (maxY - minY || 1);
3718
+ const scale = Math.min(1.5, Math.max(0.2, Math.min(scaleX, scaleY)));
3719
+ const cx = (minX + maxX) / 2, cy = (minY + maxY) / 2;
3720
+ setTransform({ scale, x: W2 / 2 - cx * scale, y: H2 / 2 - cy * scale });
3721
+ }, [model.nodes, variant]);
3722
+ const jumpToNode = (0, import_react17.useCallback)((nodeId) => {
3723
+ const node = model.nodes.find((n) => n.id === nodeId);
3724
+ if (!node || !svgRef.current) return;
3725
+ const rect = svgRef.current.getBoundingClientRect();
3726
+ const { w: nw, h: nh } = nodeDims(node, variant);
3727
+ const cx = (node.x ?? 0) + nw / 2;
3728
+ const cy = (node.y ?? 0) + nh / 2;
3729
+ const scale = Math.min(Math.max(transform.scale, 0.8), 1.4);
3730
+ setTransform({ scale, x: rect.width / 2 - cx * scale, y: rect.height / 2 - cy * scale });
3731
+ selectOne(nodeId);
3732
+ }, [model.nodes, variant, transform.scale, selectOne]);
3733
+ const duplicateIds = (0, import_react17.useCallback)((ids) => {
3734
+ if (ids.length === 0) return;
3735
+ const idSet = new Set(ids);
3736
+ const idMap = /* @__PURE__ */ new Map();
3737
+ const nextNode = makeIdSource("node", model.nodes);
3738
+ const nextEdge = makeIdSource("e", model.edges);
3739
+ const newNodes = [];
3740
+ for (const oldId of ids) {
3741
+ const n = model.nodes.find((x) => x.id === oldId);
3742
+ if (!n) continue;
3743
+ const newId = nextNode();
3744
+ idMap.set(oldId, newId);
3745
+ newNodes.push({
3746
+ ...n,
3747
+ id: newId,
3748
+ label: ids.length === 1 ? n.label + " (copy)" : n.label,
3749
+ x: (n.x ?? 0) + 32,
3750
+ y: (n.y ?? 0) + 32
3751
+ });
3752
+ }
3753
+ const newEdges = [];
3754
+ for (const e of model.edges) {
3755
+ if (idSet.has(e.from) && idSet.has(e.to)) {
3756
+ newEdges.push({ ...e, id: nextEdge(), from: idMap.get(e.from), to: idMap.get(e.to) });
3757
+ }
3758
+ }
3759
+ const m = { ...model, nodes: [...model.nodes, ...newNodes], edges: [...model.edges, ...newEdges] };
3760
+ applyAndPush(m);
3761
+ const newIds = newNodes.map((n) => n.id);
3762
+ setSelected(newIds[newIds.length - 1] ?? null);
3763
+ setSelectedSet(new Set(newIds));
3764
+ }, [model, applyAndPush]);
3765
+ const duplicateNode = (0, import_react17.useCallback)((nodeId) => {
3766
+ duplicateIds([nodeId]);
3767
+ }, [duplicateIds]);
3768
+ (0, import_react17.useEffect)(() => {
3769
+ if (!ctxMenu) return;
3770
+ const close = () => setCtxMenu(null);
3771
+ window.addEventListener("mousedown", close);
3772
+ return () => window.removeEventListener("mousedown", close);
3773
+ }, [ctxMenu]);
3774
+ (0, import_react17.useEffect)(() => {
3775
+ const onKey = (e) => {
3776
+ const tgt = e.target;
3777
+ if (tgt && (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA" || tgt.isContentEditable)) return;
3778
+ const ctrl = e.ctrlKey || e.metaKey;
3779
+ if (ctrl && e.key === "z") {
3780
+ e.preventDefault();
3781
+ undo();
3782
+ return;
3783
+ }
3784
+ if (ctrl && (e.key === "y" || e.shiftKey && e.key === "z")) {
3785
+ e.preventDefault();
3786
+ redo();
3787
+ return;
3788
+ }
3789
+ if (ctrl && e.key === "0") {
3790
+ e.preventDefault();
3791
+ reCenter();
3792
+ return;
3793
+ }
3794
+ if (ctrl && (e.key === "d" || e.key === "D")) {
3795
+ if (selectedSet.size > 0) {
3796
+ e.preventDefault();
3797
+ duplicateIds(Array.from(selectedSet));
3798
+ }
3799
+ return;
3800
+ }
3801
+ if (ctrl && (e.key === "c" || e.key === "C")) {
3802
+ if (selectedSet.size > 0) {
3803
+ e.preventDefault();
3804
+ const ids = new Set(selectedSet);
3805
+ const nodes = model.nodes.filter((n) => ids.has(n.id));
3806
+ const edges = model.edges.filter((ed) => ids.has(ed.from) && ids.has(ed.to));
3807
+ clipboardRef.current = {
3808
+ nodes: nodes.map((n) => ({ ...n })),
3809
+ edges: edges.map((ed) => ({ ...ed }))
3810
+ };
3811
+ }
3812
+ return;
3813
+ }
3814
+ if (ctrl && (e.key === "v" || e.key === "V")) {
3815
+ const clip = clipboardRef.current;
3816
+ if (clip && clip.nodes.length > 0) {
3817
+ e.preventDefault();
3818
+ const idMap = /* @__PURE__ */ new Map();
3819
+ const nextNode = makeIdSource("node", model.nodes);
3820
+ const nextEdge = makeIdSource("e", model.edges);
3821
+ const newNodes = clip.nodes.map((n) => {
3822
+ const newId = nextNode();
3823
+ idMap.set(n.id, newId);
3824
+ return { ...n, id: newId, x: (n.x ?? 0) + 24, y: (n.y ?? 0) + 24 };
3825
+ });
3826
+ const newEdges = clip.edges.map((ed) => ({
3827
+ ...ed,
3828
+ id: nextEdge(),
3829
+ from: idMap.get(ed.from) ?? ed.from,
3830
+ to: idMap.get(ed.to) ?? ed.to
3831
+ }));
3832
+ const m = { ...model, nodes: [...model.nodes, ...newNodes], edges: [...model.edges, ...newEdges] };
3833
+ applyAndPush(m);
3834
+ const newIds = newNodes.map((n) => n.id);
3835
+ setSelected(newIds[newIds.length - 1]);
3836
+ setSelectedSet(new Set(newIds));
3837
+ setAnnouncement(`Pasted ${newIds.length} ${variantLabel.toLowerCase()}${newIds.length === 1 ? "" : "s"}.`);
3838
+ }
3839
+ return;
3840
+ }
3841
+ if (e.key === "Escape") {
3842
+ if (ctxMenu) setCtxMenu(null);
3843
+ if (liveEdge) setLiveEdge(null);
3844
+ if (editingId) setEditingId(null);
3845
+ if (boxSel) setBoxSel(null);
3846
+ if (selectedSet.size > 0) clearSelection();
3847
+ return;
3848
+ }
3849
+ if ((e.key === "Delete" || e.key === "Backspace") && selectedSet.size > 0) {
3850
+ e.preventDefault();
3851
+ const ids = new Set(selectedSet);
3852
+ const updated = {
3853
+ ...model,
3854
+ nodes: model.nodes.filter((n) => !ids.has(n.id)),
3855
+ edges: model.edges.filter((ed) => !ids.has(ed.from) && !ids.has(ed.to))
3856
+ };
3857
+ applyAndPush(updated);
3858
+ clearSelection();
3859
+ setAnnouncement(`Deleted ${ids.size} ${variantLabel.toLowerCase()}${ids.size === 1 ? "" : "s"}.`);
3860
+ return;
3861
+ }
3862
+ if (selectedSet.size > 0 && (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight")) {
3863
+ const dirKey = e.key === "ArrowLeft" ? "left" : e.key === "ArrowRight" ? "right" : e.key === "ArrowUp" ? "up" : "down";
3864
+ if (e.altKey && selected) {
3865
+ e.preventDefault();
3866
+ const origin = model.nodes.find((n) => n.id === selected);
3867
+ if (!origin) return;
3868
+ const od = nodeDims(origin, variant);
3869
+ const ox = (origin.x ?? 0) + od.w / 2;
3870
+ const oy = (origin.y ?? 0) + od.h / 2;
3871
+ const candidates = model.nodes.filter((n) => n.id !== selected).map((n) => {
3872
+ const d = nodeDims(n, variant);
3873
+ return { id: n.id, x: (n.x ?? 0) + d.w / 2, y: (n.y ?? 0) + d.h / 2 };
3874
+ });
3875
+ const nextId2 = nearestInDirection(ox, oy, dirKey, candidates);
3876
+ if (nextId2) {
3877
+ selectOne(nextId2);
3878
+ setAnnouncement(`Selected ${model.nodes.find((n) => n.id === nextId2)?.label ?? ""}.`);
3879
+ }
3880
+ return;
3881
+ }
3882
+ e.preventDefault();
3883
+ const step = e.shiftKey ? GRID * 4 : GRID;
3884
+ const dx = dirKey === "left" ? -step : dirKey === "right" ? step : 0;
3885
+ const dy = dirKey === "up" ? -step : dirKey === "down" ? step : 0;
3886
+ const ids = selectedSet;
3887
+ const updated = {
3888
+ ...model,
3889
+ nodes: model.nodes.map((n) => ids.has(n.id) ? { ...n, x: snap((n.x ?? 0) + dx), y: snap((n.y ?? 0) + dy) } : n)
3890
+ };
3891
+ applyAndPush(updated);
3892
+ }
3893
+ };
3894
+ window.addEventListener("keydown", onKey);
3895
+ return () => window.removeEventListener("keydown", onKey);
3896
+ }, [undo, redo, reCenter, selected, selectedSet, ctxMenu, liveEdge, editingId, boxSel, model, applyAndPush, duplicateNode, clearSelection]);
3897
+ const toCanvas = (0, import_react17.useCallback)((clientX, clientY) => {
3898
+ const rect = svgRef.current.getBoundingClientRect();
3899
+ return { x: (clientX - rect.left - transform.x) / transform.scale, y: (clientY - rect.top - transform.y) / transform.scale };
3900
+ }, [transform]);
3901
+ useCanvasWheel(svgRef, setTransform);
3902
+ const onCanvasLongPress = (0, import_react17.useCallback)((x, y) => {
3903
+ setCtxMenu({ x, y, nodeId: null });
3904
+ }, []);
3905
+ useCanvasTouch(svgRef, { transform, setTransform, onLongPress: onCanvasLongPress });
3906
+ const onPortMouseDown = (e, nodeId) => {
3907
+ e.stopPropagation();
3908
+ const node = model.nodes.find((n) => n.id === nodeId);
3909
+ const { x, y } = toCanvas(e.clientX, e.clientY);
3910
+ const nW = nodeWidth2(node.label);
3911
+ setLiveEdge({ fromId: nodeId, fromX: (node.x ?? 0) + nW / 2, fromY: (node.y ?? 0) + NODE_H2, exitDir: "bottom", toX: x, toY: y });
3912
+ };
3913
+ const onAnswerPortDown = (e, nodeId, answer, portXInNode, portYInNode) => {
3914
+ e.stopPropagation();
3915
+ const node = model.nodes.find((n) => n.id === nodeId);
3916
+ const { x, y } = toCanvas(e.clientX, e.clientY);
3917
+ setLiveEdge({ fromId: nodeId, fromX: (node.x ?? 0) + portXInNode, fromY: (node.y ?? 0) + portYInNode, exitDir: "bottom", answerLabel: answer, toX: x, toY: y });
3918
+ };
3919
+ const onNodeMouseDown = (e, id) => {
3920
+ e.stopPropagation();
3921
+ if (liveEdge) return;
3922
+ const node = model.nodes.find((n) => n.id === id);
3923
+ if (e.shiftKey) {
3924
+ toggleSelect(id);
3925
+ return;
3926
+ }
3927
+ const inSet = selectedSet.has(id);
3928
+ if (inSet && selectedSet.size > 1) {
3929
+ setSelected(id);
3930
+ const origins = /* @__PURE__ */ new Map();
3931
+ for (const sid of selectedSet) {
3932
+ const n = model.nodes.find((x) => x.id === sid);
3933
+ if (!n) continue;
3934
+ origins.set(sid, {
3935
+ ox: e.clientX - (transform.x + (n.x ?? 0) * transform.scale),
3936
+ oy: e.clientY - (transform.y + (n.y ?? 0) * transform.scale)
3937
+ });
3938
+ }
3939
+ groupDragOriginsRef.current = origins;
3940
+ } else {
3941
+ selectOne(id);
3942
+ groupDragOriginsRef.current = null;
3943
+ }
3944
+ setDrag({ nodeId: id, ox: e.clientX - (transform.x + (node.x ?? 0) * transform.scale), oy: e.clientY - (transform.y + (node.y ?? 0) * transform.scale) });
3945
+ };
3946
+ const onNodeMouseUp = (e, targetId) => {
3947
+ if (!liveEdge || liveEdge.fromId === targetId) return;
3948
+ e.stopPropagation();
3949
+ const label = liveEdge.answerLabel;
3950
+ let updated;
3951
+ if (label) {
3952
+ const existing = model.edges.find((ex) => ex.from === liveEdge.fromId && ex.label === label);
3953
+ if (existing) {
3954
+ updated = { ...model, edges: model.edges.map((ex) => ex.id === existing.id ? { ...ex, to: targetId } : ex) };
3955
+ } else {
3956
+ updated = { ...model, edges: [...model.edges, { id: nextId("e", model.edges), from: liveEdge.fromId, to: targetId, label }] };
3957
+ }
3958
+ } else {
3959
+ updated = { ...model, edges: [...model.edges, { id: nextId("e", model.edges), from: liveEdge.fromId, to: targetId }] };
3960
+ }
3961
+ applyAndPush(updated);
3962
+ setLiveEdge(null);
3963
+ };
3964
+ const onSvgMouseDown = (e) => {
3965
+ if (ctxMenu) {
3966
+ setCtxMenu(null);
3967
+ return;
3968
+ }
3969
+ if (e.target.dataset.bg === "1" || e.target === svgRef.current) {
3970
+ if (e.shiftKey) {
3971
+ setBoxSel({ sx: e.clientX, sy: e.clientY, cx: e.clientX, cy: e.clientY, additive: true });
3972
+ } else {
3973
+ clearSelection();
3974
+ setPan({ ox: e.clientX, oy: e.clientY, tx: transform.x, ty: transform.y });
3975
+ }
3976
+ }
3977
+ };
3978
+ const onSvgContextMenu = (e) => {
3979
+ e.preventDefault();
3980
+ setCtxMenu({ x: e.clientX, y: e.clientY, nodeId: null });
3981
+ };
3982
+ const onNodeContextMenu = (e, nodeId) => {
3983
+ e.preventDefault();
3984
+ e.stopPropagation();
3985
+ if (!selectedSet.has(nodeId)) selectOne(nodeId);
3986
+ setCtxMenu({ x: e.clientX, y: e.clientY, nodeId });
3987
+ };
3988
+ const onMouseMove = (e) => {
3989
+ if (liveEdge) {
3990
+ const { x, y } = toCanvas(e.clientX, e.clientY);
3991
+ setLiveEdge((le) => le ? { ...le, toX: x, toY: y } : null);
3992
+ return;
3993
+ }
3994
+ if (waypointDrag) {
3995
+ const { x, y } = toCanvas(e.clientX, e.clientY);
3996
+ const wx = snap(x), wy = snap(y);
3997
+ const updated = {
3998
+ ...model,
3999
+ edges: model.edges.map((ed) => ed.id === waypointDrag ? { ...ed, waypoint: { x: wx, y: wy } } : ed)
4000
+ };
4001
+ applyModel(updated);
4002
+ return;
4003
+ }
4004
+ if (drag) {
4005
+ const dx = snap((e.clientX - drag.ox - transform.x) / transform.scale);
4006
+ const dy = snap((e.clientY - drag.oy - transform.y) / transform.scale);
4007
+ const origins = groupDragOriginsRef.current;
4008
+ if (origins && origins.size > 1) {
4009
+ const updated = {
4010
+ ...model,
4011
+ nodes: model.nodes.map((n) => {
4012
+ const o = origins.get(n.id);
4013
+ if (!o) return n;
4014
+ return {
4015
+ ...n,
4016
+ x: snap((e.clientX - o.ox - transform.x) / transform.scale),
4017
+ y: snap((e.clientY - o.oy - transform.y) / transform.scale)
4018
+ };
4019
+ })
4020
+ };
4021
+ applyModel(updated);
4022
+ } else {
4023
+ const dragged = model.nodes.find((n) => n.id === drag.nodeId);
4024
+ if (!dragged) return;
4025
+ const { w: dW, h: dH } = nodeDims(dragged, variant);
4026
+ const others = model.nodes.filter((n) => n.id !== drag.nodeId).map((n) => {
4027
+ const d = nodeDims(n, variant);
4028
+ return { x: n.x ?? 0, y: n.y ?? 0, w: d.w, h: d.h };
4029
+ });
4030
+ const snapResult = findSiblingSnap({ x: dx, y: dy, w: dW, h: dH }, others);
4031
+ setAlignGuides(snapResult.guideX || snapResult.guideY ? { x: snapResult.guideX, y: snapResult.guideY } : null);
4032
+ const updated = { ...model, nodes: model.nodes.map((n) => n.id === drag.nodeId ? { ...n, x: snapResult.x, y: snapResult.y } : n) };
4033
+ applyModel(updated);
4034
+ }
4035
+ } else if (pan) {
4036
+ setTransform((tr) => ({ ...tr, x: pan.tx + (e.clientX - pan.ox), y: pan.ty + (e.clientY - pan.oy) }));
4037
+ } else if (boxSel) {
4038
+ setBoxSel((b) => b ? { ...b, cx: e.clientX, cy: e.clientY } : null);
4039
+ }
4040
+ };
4041
+ const onMouseUp = () => {
4042
+ if (boxSel) {
4043
+ const dragged = Math.abs(boxSel.cx - boxSel.sx) > 3 || Math.abs(boxSel.cy - boxSel.sy) > 3;
4044
+ if (dragged && svgRef.current) {
4045
+ const rect = svgRef.current.getBoundingClientRect();
4046
+ const x1 = Math.min(boxSel.sx, boxSel.cx) - rect.left;
4047
+ const y1 = Math.min(boxSel.sy, boxSel.cy) - rect.top;
4048
+ const x2 = Math.max(boxSel.sx, boxSel.cx) - rect.left;
4049
+ const y2 = Math.max(boxSel.sy, boxSel.cy) - rect.top;
4050
+ const cx1 = (x1 - transform.x) / transform.scale;
4051
+ const cy1 = (y1 - transform.y) / transform.scale;
4052
+ const cx2 = (x2 - transform.x) / transform.scale;
4053
+ const cy2 = (y2 - transform.y) / transform.scale;
4054
+ const hits = new Set(boxSel.additive ? selectedSet : []);
4055
+ for (const n of model.nodes) {
4056
+ const nx = n.x ?? 0, ny = n.y ?? 0;
4057
+ const { w: nw, h: nh } = nodeDims(n, variant);
4058
+ if (nx + nw >= cx1 && nx <= cx2 && ny + nh >= cy1 && ny <= cy2) hits.add(n.id);
4059
+ }
4060
+ const arr = Array.from(hits);
4061
+ setSelectedSet(hits);
4062
+ setSelected(arr.length ? arr[arr.length - 1] : null);
4063
+ }
4064
+ setBoxSel(null);
4065
+ }
4066
+ if (drag) applyAndPush(model);
4067
+ if (waypointDrag) {
4068
+ applyAndPush(model);
4069
+ setWaypointDrag(null);
4070
+ }
4071
+ groupDragOriginsRef.current = null;
4072
+ setAlignGuides(null);
4073
+ setDrag(null);
4074
+ setPan(null);
4075
+ if (liveEdge) setLiveEdge(null);
4076
+ };
4077
+ const onNodeDblClick = (e, id) => {
4078
+ e.stopPropagation();
4079
+ const node = model.nodes.find((n) => n.id === id);
4080
+ setEditingId(id);
4081
+ setEditLabel(node.label);
4082
+ };
4083
+ const commitEdit = () => {
4084
+ if (!editingId) return;
4085
+ const up = { ...model, nodes: model.nodes.map((n) => n.id === editingId ? { ...n, label: editLabel } : n) };
4086
+ applyAndPush(up);
4087
+ setEditingId(null);
4088
+ };
4089
+ const addNode = (atCanvasPos) => {
4090
+ const id = nextId("node", model.nodes);
4091
+ const p = atCanvasPos ? { x: snap(atCanvasPos.x), y: snap(atCanvasPos.y) } : { x: snap(100 + Math.random() * 240), y: snap(100 + Math.random() * 180) };
4092
+ const label = variant === "question" ? "New Question" : variant === "journey" ? `Step ${model.nodes.length + 1}` : "New Step";
4093
+ const metadata = variant === "question" ? { answers: [] } : void 0;
4094
+ const updated = { ...model, nodes: [...model.nodes, { id, label, shape: "rectangle", metadata, ...p }] };
4095
+ applyAndPush(updated);
4096
+ selectOne(id);
4097
+ setAnnouncement(`Added ${variantLabel.toLowerCase()} "${label}".`);
4098
+ };
4099
+ const deleteNode = (nodeId) => {
4100
+ const node = model.nodes.find((n) => n.id === nodeId);
4101
+ const updated = { ...model, nodes: model.nodes.filter((n) => n.id !== nodeId), edges: model.edges.filter((e) => e.from !== nodeId && e.to !== nodeId) };
4102
+ applyAndPush(updated);
4103
+ if (selectedSet.has(nodeId)) {
4104
+ const next = new Set(selectedSet);
4105
+ next.delete(nodeId);
4106
+ setSelectedSet(next);
4107
+ if (selected === nodeId) setSelected(next.size ? Array.from(next)[next.size - 1] : null);
4108
+ }
4109
+ if (node) setAnnouncement(`Deleted ${variantLabel.toLowerCase()} "${node.label}".`);
4110
+ };
4111
+ const deleteSelected = () => {
4112
+ if (selectedSet.size === 0) return;
4113
+ if (selectedSet.size === 1 && selected) {
4114
+ deleteNode(selected);
4115
+ return;
4116
+ }
4117
+ const ids = new Set(selectedSet);
4118
+ const updated = {
4119
+ ...model,
4120
+ nodes: model.nodes.filter((n) => !ids.has(n.id)),
4121
+ edges: model.edges.filter((ed) => !ids.has(ed.from) && !ids.has(ed.to))
4122
+ };
4123
+ applyAndPush(updated);
4124
+ clearSelection();
4125
+ setAnnouncement(`Deleted ${ids.size} ${variantLabel.toLowerCase()}s.`);
4126
+ };
4127
+ const beginEditEdge = (edgeId) => {
4128
+ const edge = model.edges.find((e) => e.id === edgeId);
4129
+ if (!edge) return;
4130
+ if (variant === "question") return;
4131
+ setEditingEdgeId(edgeId);
4132
+ setEditEdgeLabel(edge.label ?? "");
4133
+ };
4134
+ const commitEdgeEdit = () => {
4135
+ if (!editingEdgeId) return;
4136
+ const next = editEdgeLabel.trim();
4137
+ const updated = {
4138
+ ...model,
4139
+ edges: model.edges.map((e) => e.id === editingEdgeId ? { ...e, ...next ? { label: next } : { label: void 0 } } : e)
4140
+ };
4141
+ applyAndPush(updated);
4142
+ setEditingEdgeId(null);
4143
+ };
4144
+ const onEdgeContextMenu = (e, edgeId) => {
4145
+ e.preventDefault();
4146
+ e.stopPropagation();
4147
+ setCtxMenu({ x: e.clientX, y: e.clientY, nodeId: null, edgeId });
4148
+ };
4149
+ const setEdgeStyle = (edgeId, style) => {
4150
+ const updated = { ...model, edges: model.edges.map((e) => e.id === edgeId ? { ...e, style } : e) };
4151
+ applyAndPush(updated);
4152
+ };
4153
+ const setEdgeArrowhead = (edgeId, arrowhead) => {
4154
+ const updated = { ...model, edges: model.edges.map((e) => e.id === edgeId ? { ...e, arrowhead } : e) };
4155
+ applyAndPush(updated);
4156
+ };
4157
+ const deleteEdge = (edgeId) => {
4158
+ const updated = { ...model, edges: model.edges.filter((e) => e.id !== edgeId) };
4159
+ applyAndPush(updated);
4160
+ };
4161
+ const resetEdgeRouting = (edgeId) => {
4162
+ const updated = {
4163
+ ...model,
4164
+ edges: model.edges.map((e) => {
4165
+ if (e.id !== edgeId) return e;
4166
+ const { waypoint: _ignored, ...rest } = e;
4167
+ void _ignored;
4168
+ return rest;
4169
+ })
4170
+ };
4171
+ applyAndPush(updated);
4172
+ };
4173
+ const handleExport = useExporters(model, onExport, "diagram");
4174
+ const positionFlowchartNodes = (0, import_react17.useCallback)((m) => ({
4175
+ ...m,
4176
+ nodes: m.nodes.map((n, i) => ({
4177
+ ...n,
4178
+ x: n.x ?? snap(80 + i % 4 * 200),
4179
+ y: n.y ?? snap(80 + Math.floor(i / 4) * 140)
4180
+ }))
4181
+ }), []);
4182
+ const handleImport = useImporter(applyAndPush, { transform: positionFlowchartNodes });
4183
+ const acc = variantAccent(variant, isDark);
4184
+ const variantLabel = variant === "question" ? "Question" : variant === "journey" ? "Step" : "Node";
4185
+ const shadowColor = isDark ? "rgba(0,0,0,0.55)" : "rgba(15,23,42,0.09)";
4186
+ const arrowColor = isDark ? "#64748b" : "#94a3b8";
4187
+ const amberArrow = isDark ? ACCENT.amberDark : ACCENT.amber;
4188
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "fsd-editor", style: { display: "flex", flexDirection: "column", height, width: "100%", fontFamily: "ui-sans-serif,system-ui,sans-serif", boxSizing: "border-box", background: t.ctrlsBg }, children: [
4189
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("style", { children: `
4190
+ .fsd-editor button:focus-visible,
4191
+ .fsd-editor input:focus-visible,
4192
+ .fsd-editor textarea:focus-visible,
4193
+ .fsd-editor select:focus-visible,
4194
+ .fsd-editor [role="button"]:focus-visible {
4195
+ outline: 2px solid ${acc.color};
4196
+ outline-offset: 2px;
4197
+ border-radius: 6px;
4198
+ }
4199
+ .fsd-editor svg[role="application"]:focus-visible {
4200
+ outline: 2px solid ${acc.color};
4201
+ outline-offset: -2px;
4202
+ }
4203
+ ` }),
4204
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4205
+ "div",
4206
+ {
4207
+ role: "status",
4208
+ "aria-live": "polite",
4209
+ "aria-atomic": "true",
4210
+ style: STYLE_SR_ONLY,
4211
+ children: announcement
4212
+ }
4213
+ ),
4214
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
4215
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { display: "flex", gap: 6, padding: "7px 14px", background: t.ctrlsBg, borderBottom: `1px solid ${t.ctrlsBorder}`, alignItems: "center", flexWrap: "wrap" }, children: [
4216
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("button", { onClick: () => addNode(), style: ctrlBtn(acc.color, isDark), children: [
4217
+ "+ ",
4218
+ variantLabel
4219
+ ] }),
4220
+ selectedSet.size > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
4221
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { width: 1, height: 20, background: t.ctrlsBorder, margin: "0 2px" } }),
4222
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { onClick: deleteSelected, style: { ...ctrlBtn("transparent", isDark), color: "#ef4444", border: `1px solid ${isDark ? "#7f1d1d" : "#fca5a5"}` }, children: selectedSet.size > 1 ? `Delete (${selectedSet.size})` : "Delete" })
4223
+ ] }),
4224
+ liveEdge && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: { fontSize: 11, color: acc.color, fontWeight: 600, marginLeft: 6 }, children: [
4225
+ liveEdge.answerLabel ? `Routing "${liveEdge.answerLabel}" \u2192` : "Drop on a node to connect",
4226
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: { fontWeight: 400, color: t.textMuted, marginLeft: 6 }, children: "release to cancel" })
4227
+ ] }),
4228
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
4229
+ variant === "question" ? "drag answer port to connect \xB7 " : "drag port dot \xB7 ",
4230
+ "scroll to zoom \xB7 drag to pan"
4231
+ ] })
4232
+ ] }),
4233
+ variant !== "flowchart" && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { padding: "3px 14px", background: acc.fill, borderBottom: `1px solid ${acc.border}`, fontSize: 11, color: acc.color, fontWeight: 600 }, children: variant === "question" ? "? Question Flow \u2014 add answers in the panel, drag their port to connect" : "\u2197 Journey Map \u2014 numbered steps, drag port to sequence" }),
4234
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: STYLE_FLEX_ROW, children: [
4235
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4236
+ NodeNavigator,
4237
+ {
4238
+ model,
4239
+ selected,
4240
+ variant,
4241
+ isDark,
4242
+ t,
4243
+ acc,
4244
+ open: navOpen,
4245
+ onToggle: () => setNavOpen((v) => !v),
4246
+ onSelect: jumpToNode
4247
+ }
4248
+ ),
4249
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { ref: containerRef, style: { flex: 1, overflow: "hidden", position: "relative", background: t.canvas }, children: [
4250
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
4251
+ "svg",
4252
+ {
4253
+ ref: svgRef,
4254
+ width: "100%",
4255
+ height: "100%",
4256
+ role: "application",
4257
+ "aria-label": `${variantLabel} diagram editor. ${model.nodes.length} ${variantLabel.toLowerCase()}s, ${model.edges.length} connections. Scroll to zoom, drag to pan, click a ${variantLabel.toLowerCase()} to select.`,
4258
+ tabIndex: 0,
4259
+ style: { display: "block", cursor: pan ? "grabbing" : drag ? "grabbing" : liveEdge ? "crosshair" : "default", userSelect: "none", outline: "none" },
4260
+ onMouseDown: onSvgMouseDown,
4261
+ onMouseMove,
4262
+ onMouseUp,
4263
+ onMouseLeave: onMouseUp,
4264
+ onContextMenu: onSvgContextMenu,
4265
+ children: [
4266
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("defs", { children: [
4267
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("style", { children: reducedMotion ? `
4268
+ .edge-flow { stroke-dasharray: 0; }
4269
+ .edge-flow-amber { stroke-dasharray: 0; }
4270
+ .edge-live { stroke-dasharray: 4 4; }
4271
+ ` : `
4272
+ @keyframes edgeFlow { to { stroke-dashoffset: -13; } }
4273
+ @keyframes edgeFlowFast { to { stroke-dashoffset: -13; } }
4274
+ .edge-flow { stroke-dasharray: 8 5; animation: edgeFlow 0.9s linear infinite; }
4275
+ .edge-flow-amber { stroke-dasharray: 6 4; animation: edgeFlowFast 0.65s linear infinite; }
4276
+ .edge-live { stroke-dasharray: 7 5; animation: edgeFlow 0.55s linear infinite; }
4277
+ ` }),
4278
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("pattern", { id: "dots", width: GRID, height: GRID, patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("circle", { cx: GRID / 2, cy: GRID / 2, r: 1.1, fill: t.dot }) }),
4279
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("filter", { id: "nodeShadow", x: "-25%", y: "-25%", width: "150%", height: "160%", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("feDropShadow", { dx: "0", dy: "3", stdDeviation: "5", floodColor: shadowColor, floodOpacity: "1" }) }),
4280
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("marker", { id: "arrowhead", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: arrowColor }) }),
4281
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("marker", { id: "arrowAmber", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: amberArrow }) }),
4282
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("marker", { id: "arrowLive", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: acc.color }) })
4283
+ ] }),
4284
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("rect", { width: "100%", height: "100%", fill: "url(#dots)", "data-bg": "1" }),
4285
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("g", { transform: `translate(${transform.x},${transform.y}) scale(${transform.scale})`, children: [
4286
+ model.edges.map((e) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4287
+ EdgeLine,
4288
+ {
4289
+ edge: e,
4290
+ nodes: model.nodes,
4291
+ variant,
4292
+ t,
4293
+ isDark,
4294
+ acc,
4295
+ editing: editingEdgeId === e.id,
4296
+ editValue: editEdgeLabel,
4297
+ onEditChange: setEditEdgeLabel,
4298
+ onEditCommit: commitEdgeEdit,
4299
+ onEditCancel: () => setEditingEdgeId(null),
4300
+ onDoubleClick: beginEditEdge,
4301
+ onContextMenu: onEdgeContextMenu,
4302
+ onWaypointDown: (ev, edgeId) => setWaypointDrag(edgeId)
4303
+ },
4304
+ e.id
4305
+ )),
4306
+ liveEdge && (() => {
4307
+ const d = bezierPath2(liveEdge.fromX, liveEdge.fromY, liveEdge.toX, liveEdge.toY, liveEdge.exitDir);
4308
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d, fill: "none", stroke: acc.color, strokeWidth: 2, strokeLinecap: "round", className: "edge-live", opacity: 0.8, markerEnd: "url(#arrowLive)" });
4309
+ })(),
4310
+ alignGuides?.x && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4311
+ "line",
4312
+ {
4313
+ x1: alignGuides.x.pos,
4314
+ x2: alignGuides.x.pos,
4315
+ y1: alignGuides.x.minY,
4316
+ y2: alignGuides.x.maxY,
4317
+ stroke: acc.color,
4318
+ strokeWidth: 1 / transform.scale,
4319
+ strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
4320
+ opacity: 0.85,
4321
+ pointerEvents: "none"
4322
+ }
4323
+ ),
4324
+ alignGuides?.y && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4325
+ "line",
4326
+ {
4327
+ y1: alignGuides.y.pos,
4328
+ y2: alignGuides.y.pos,
4329
+ x1: alignGuides.y.minX,
4330
+ x2: alignGuides.y.maxX,
4331
+ stroke: acc.color,
4332
+ strokeWidth: 1 / transform.scale,
4333
+ strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
4334
+ opacity: 0.85,
4335
+ pointerEvents: "none"
4336
+ }
4337
+ ),
4338
+ model.nodes.map((node, idx) => {
4339
+ const isHovered = hoveredId === node.id;
4340
+ const isQuestion2 = variant === "question";
4341
+ const { w: nW, h: nH } = nodeDims(node, variant);
4342
+ const isSelected = selectedSet.has(node.id);
4343
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
4344
+ "g",
4345
+ {
4346
+ transform: `translate(${node.x ?? 0},${node.y ?? 0})`,
4347
+ role: "button",
4348
+ "aria-label": `${variantLabel} ${variant === "journey" ? idx + 1 + ": " : ""}${node.label}${isSelected ? ", selected" : ""}`,
4349
+ style: { cursor: drag?.nodeId === node.id ? "grabbing" : "grab" },
4350
+ onMouseDown: (e) => onNodeMouseDown(e, node.id),
4351
+ onMouseUp: (e) => onNodeMouseUp(e, node.id),
4352
+ onDoubleClick: (e) => onNodeDblClick(e, node.id),
4353
+ onContextMenu: (e) => onNodeContextMenu(e, node.id),
4354
+ onMouseEnter: () => setHoveredId(node.id),
4355
+ onMouseLeave: () => setHoveredId(null),
4356
+ children: [
4357
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("title", { children: `${variantLabel}: ${node.label}` }),
4358
+ isQuestion2 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(QuestionNode, { node, selected: isSelected, edges: model.edges, isDark, onAnswerPortDown, qW: nW }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
4359
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(NodeShape, { node, selected: isSelected, variant, stepNumber: variant === "journey" ? idx + 1 : void 0, t, isDark, w: nW }),
4360
+ editingId === node.id ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("foreignObject", { x: 6, y: 6, width: nW - 12, height: NODE_H2 - 12, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4361
+ "input",
4362
+ {
4363
+ autoFocus: true,
4364
+ value: editLabel,
4365
+ onChange: (e) => setEditLabel(e.target.value),
4366
+ onBlur: commitEdit,
4367
+ onKeyDown: (e) => {
4368
+ if (e.key === "Enter") commitEdit();
4369
+ if (e.key === "Escape") setEditingId(null);
4370
+ },
4371
+ style: { width: "100%", height: "100%", border: "none", borderRadius: 6, outline: `2px solid ${acc.color}`, textAlign: "center", fontSize: 13, fontWeight: 500, background: t.inputBg, boxSizing: "border-box", padding: "0 6px", fontFamily: "inherit", color: t.inputText }
4372
+ }
4373
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("text", { x: nW / 2, y: NODE_H2 / 2 + 5, textAnchor: "middle", fontSize: 13, fontWeight: "500", fontFamily: "ui-sans-serif,system-ui,sans-serif", fill: isSelected ? acc.color : t.textPrimary, style: STYLE_LABEL2, children: node.label }),
4374
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4375
+ "circle",
4376
+ {
4377
+ cx: nW / 2,
4378
+ cy: NODE_H2 + 1,
4379
+ r: portR,
4380
+ fill: acc.color,
4381
+ stroke: isDark ? "#0f172a" : "white",
4382
+ strokeWidth: 2,
4383
+ style: { cursor: "crosshair", opacity: isHovered || isCoarse ? 1 : 0, transition: "opacity 0.15s", pointerEvents: isHovered || isCoarse ? "all" : "none", filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.25))" },
4384
+ onMouseDown: (e) => onPortMouseDown(e, node.id)
4385
+ }
4386
+ )
4387
+ ] }),
4388
+ liveEdge && liveEdge.fromId !== node.id && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("circle", { cx: nW / 2, cy: -1, r: portR, fill: acc.color, stroke: isDark ? "#0f172a" : "white", strokeWidth: 2, style: STYLE_LIVE_PORT })
4389
+ ]
4390
+ },
4391
+ node.id
4392
+ );
4393
+ })
4394
+ ] })
4395
+ ]
4396
+ }
4397
+ ),
4398
+ boxSel && Math.abs(boxSel.cx - boxSel.sx) + Math.abs(boxSel.cy - boxSel.sy) > 4 && containerRef.current && (() => {
4399
+ const rect = containerRef.current.getBoundingClientRect();
4400
+ const left = Math.min(boxSel.sx, boxSel.cx) - rect.left;
4401
+ const top = Math.min(boxSel.sy, boxSel.cy) - rect.top;
4402
+ const w = Math.abs(boxSel.cx - boxSel.sx);
4403
+ const h = Math.abs(boxSel.cy - boxSel.sy);
4404
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4405
+ "div",
4406
+ {
4407
+ style: {
4408
+ position: "absolute",
4409
+ left,
4410
+ top,
4411
+ width: w,
4412
+ height: h,
4413
+ border: `1px dashed ${acc.color}`,
4414
+ background: isDark ? "rgba(99,102,241,0.10)" : "rgba(99,102,241,0.08)",
4415
+ pointerEvents: "none",
4416
+ borderRadius: 4
4417
+ }
4418
+ }
4419
+ );
4420
+ })(),
4421
+ model.nodes.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", pointerEvents: "none", gap: 8 }, children: [
4422
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { fontSize: 36, opacity: 0.1, color: t.textPrimary }, children: variant === "question" ? "?" : variant === "journey" ? "\u2197" : "\u2B21" }),
4423
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { fontSize: 13, color: t.textMuted, fontWeight: 500 }, children: [
4424
+ "Click ",
4425
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("strong", { style: { color: acc.color }, children: [
4426
+ "+ ",
4427
+ variantLabel
4428
+ ] }),
4429
+ " to start"
4430
+ ] })
4431
+ ] }),
4432
+ model.nodes.length > 0 && viewport.w > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4433
+ Minimap,
4434
+ {
4435
+ model,
4436
+ viewportW: viewport.w,
4437
+ viewportH: viewport.h,
4438
+ transform,
4439
+ isDark,
4440
+ accentColor: acc.color,
4441
+ measureNode: (n) => nodeDims(n, variant),
4442
+ onCenterOn: (cx, cy) => {
4443
+ setTransform((tr) => ({ ...tr, x: viewport.w / 2 - cx * tr.scale, y: viewport.h / 2 - cy * tr.scale }));
4444
+ }
4445
+ }
4446
+ ),
4447
+ ctxMenu && (() => {
4448
+ const ctxEdge = ctxMenu.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0;
4449
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4450
+ ContextMenu,
4451
+ {
4452
+ x: ctxMenu.x,
4453
+ y: ctxMenu.y,
4454
+ nodeId: ctxMenu.nodeId,
4455
+ edgeId: ctxMenu.edgeId,
4456
+ isDark,
4457
+ t,
4458
+ acc,
4459
+ canUndo: history.canUndo,
4460
+ canRedo: history.canRedo,
4461
+ onUndo: () => {
4462
+ undo();
4463
+ setCtxMenu(null);
4464
+ },
4465
+ onRedo: () => {
4466
+ redo();
4467
+ setCtxMenu(null);
4468
+ },
4469
+ onReCenter: () => {
4470
+ reCenter();
4471
+ setCtxMenu(null);
4472
+ },
4473
+ onAddNode: () => {
4474
+ const rect = svgRef.current.getBoundingClientRect();
4475
+ const cx = (ctxMenu.x - rect.left - transform.x) / transform.scale;
4476
+ const cy = (ctxMenu.y - rect.top - transform.y) / transform.scale;
4477
+ addNode({ x: cx, y: cy });
4478
+ setCtxMenu(null);
4479
+ },
4480
+ onDuplicate: () => {
4481
+ if (ctxMenu.nodeId) {
4482
+ duplicateNode(ctxMenu.nodeId);
4483
+ setCtxMenu(null);
4484
+ }
4485
+ },
4486
+ onRename: () => {
4487
+ if (ctxMenu.nodeId) {
4488
+ const node = model.nodes.find((n) => n.id === ctxMenu.nodeId);
4489
+ setEditingId(ctxMenu.nodeId);
4490
+ setEditLabel(node.label);
4491
+ setCtxMenu(null);
4492
+ }
4493
+ },
4494
+ onDelete: () => {
4495
+ if (ctxMenu.nodeId) {
4496
+ deleteNode(ctxMenu.nodeId);
4497
+ setCtxMenu(null);
4498
+ }
4499
+ },
4500
+ onDisconnect: () => {
4501
+ if (ctxMenu.nodeId) {
4502
+ const m = { ...model, edges: model.edges.filter((e) => e.from !== ctxMenu.nodeId && e.to !== ctxMenu.nodeId) };
4503
+ applyAndPush(m);
4504
+ setCtxMenu(null);
4505
+ }
4506
+ },
4507
+ currentEdgeStyle: ctxEdge?.style ?? "solid",
4508
+ currentEdgeArrow: ctxEdge?.arrowhead ?? "arrow",
4509
+ edgeHasWaypoint: !!ctxEdge?.waypoint,
4510
+ onEdgeRename: () => {
4511
+ if (ctxMenu.edgeId) {
4512
+ beginEditEdge(ctxMenu.edgeId);
4513
+ setCtxMenu(null);
4514
+ }
4515
+ },
4516
+ onEdgeStyle: (s2) => {
4517
+ if (ctxMenu.edgeId) {
4518
+ setEdgeStyle(ctxMenu.edgeId, s2);
4519
+ setCtxMenu(null);
4520
+ }
4521
+ },
4522
+ onEdgeArrowhead: (a) => {
4523
+ if (ctxMenu.edgeId) {
4524
+ setEdgeArrowhead(ctxMenu.edgeId, a);
4525
+ setCtxMenu(null);
4526
+ }
4527
+ },
4528
+ onEdgeDelete: () => {
4529
+ if (ctxMenu.edgeId) {
4530
+ deleteEdge(ctxMenu.edgeId);
4531
+ setCtxMenu(null);
4532
+ }
4533
+ },
4534
+ onEdgeResetRouting: () => {
4535
+ if (ctxMenu.edgeId) {
4536
+ resetEdgeRouting(ctxMenu.edgeId);
4537
+ setCtxMenu(null);
4538
+ }
4539
+ },
4540
+ containerRef
4541
+ }
4542
+ );
4543
+ })()
4544
+ ] }),
4545
+ selected && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(StepEditor, { nodeId: selected, model, onModelChange: (m) => {
4546
+ applyAndPush(m);
4547
+ }, variant, isDark, t, acc }, selected)
4548
+ ] }),
4549
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { padding: "4px 14px", fontSize: 11, color: t.textMuted, background: t.statusBg, borderTop: `1px solid ${t.ctrlsBorder}`, display: "flex", gap: 16 }, children: [
4550
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { children: [
4551
+ model.nodes.length,
4552
+ " ",
4553
+ variantLabel.toLowerCase(),
4554
+ "s"
4555
+ ] }),
4556
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { children: [
4557
+ model.edges.length,
4558
+ " connections"
4559
+ ] }),
4560
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { children: [
4561
+ Math.round(transform.scale * 100),
4562
+ "% zoom"
4563
+ ] }),
4564
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: { marginLeft: "auto" }, children: "Ctrl+Z undo \xB7 Ctrl+Y redo \xB7 Ctrl+0 fit \xB7 Alt+Arrow traverse" }),
4565
+ selected && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: { color: acc.color }, children: model.nodes.find((n) => n.id === selected)?.label })
4566
+ ] })
4567
+ ] });
4568
+ }
4569
+ function ctrlBtn(accent, isDark) {
4570
+ const isTransparent = accent === "transparent";
4571
+ return {
4572
+ display: "inline-flex",
4573
+ alignItems: "center",
4574
+ gap: 5,
4575
+ padding: "5px 12px",
4576
+ background: isTransparent ? "transparent" : accent,
4577
+ color: isTransparent ? "#ef4444" : "#fff",
4578
+ border: isTransparent ? `1px solid ${isDark ? "#7f1d1d" : "#fca5a5"}` : "none",
4579
+ borderRadius: 6,
4580
+ cursor: "pointer",
4581
+ fontSize: 12,
4582
+ fontWeight: 500,
4583
+ fontFamily: "inherit"
4584
+ };
4585
+ }
4586
+ // Annotate the CommonJS export names for ESM import in node:
4587
+ 0 && (module.exports = {
4588
+ DiagramEditor,
4589
+ SequenceEditor,
4590
+ StepEditor,
4591
+ Toolbar,
4592
+ emptyModel,
4593
+ presetFlowchartModel,
4594
+ presetSequenceModel
4595
+ });
4596
+ //# sourceMappingURL=index.cjs.map