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