flowchart-sequence-designer 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui/index.cjs CHANGED
@@ -415,12 +415,37 @@ function arrowColor(isDark) {
415
415
  }
416
416
  function variantAccent(variant, isDark) {
417
417
  if (variant === "question") {
418
- 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 };
418
+ return isDark ? {
419
+ color: ACCENT.amberDark,
420
+ fill: ACCENT.amberDarkLight,
421
+ border: ACCENT.amberDarkBorder,
422
+ glow: ACCENT.amberGlow
423
+ } : {
424
+ color: ACCENT.amber,
425
+ fill: ACCENT.amberLight,
426
+ border: ACCENT.amberBorder,
427
+ glow: ACCENT.amberGlow
428
+ };
419
429
  }
420
430
  if (variant === "journey") {
421
- 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 };
431
+ return isDark ? {
432
+ color: ACCENT.emeraldDark,
433
+ fill: ACCENT.emeraldDarkLight,
434
+ border: ACCENT.emeraldDarkBorder,
435
+ glow: ACCENT.emeraldGlow
436
+ } : {
437
+ color: ACCENT.emerald,
438
+ fill: ACCENT.emeraldLight,
439
+ border: "#6ee7b7",
440
+ glow: ACCENT.emeraldGlow
441
+ };
422
442
  }
423
- 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 };
443
+ return isDark ? {
444
+ color: "#818cf8",
445
+ fill: "rgba(79,70,229,0.12)",
446
+ border: "rgba(79,70,229,0.3)",
447
+ glow: ACCENT.indigoGlow
448
+ } : { color: ACCENT.indigo, fill: "#f5f3ff", border: "#c7d2fe", glow: ACCENT.indigoGlow };
424
449
  }
425
450
 
426
451
  // src/ui/Toolbar.tsx
@@ -447,17 +472,19 @@ function Toolbar({ onExport, onImport, allowedExports, allowImport = true }) {
447
472
  allowImport && onImport && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: () => setImportOpen(true), "aria-label": "Import diagram", style: ghostBtn, children: "\u2191 Import" }),
448
473
  formats.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
449
474
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: 11, color: darkTheme.inputText, margin: "0 4px" }, children: "Export \u2192" }),
450
- formats.map((f) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: () => onExport(f.key), "aria-label": `Export as ${f.label}`, style: exportBtn, children: f.label }, f.key))
475
+ formats.map((f) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
476
+ "button",
477
+ {
478
+ onClick: () => onExport(f.key),
479
+ "aria-label": `Export as ${f.label}`,
480
+ style: exportBtn,
481
+ children: f.label
482
+ },
483
+ f.key
484
+ ))
451
485
  ] })
452
486
  ] }),
453
- onImport && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
454
- ImportDialog,
455
- {
456
- open: importOpen,
457
- onClose: () => setImportOpen(false),
458
- onImport
459
- }
460
- )
487
+ onImport && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ImportDialog, { open: importOpen, onClose: () => setImportOpen(false), onImport })
461
488
  ] });
462
489
  }
463
490
  var bar = {
@@ -549,7 +576,15 @@ var SHAPES = [
549
576
  { key: "circle", label: "Circle", icon: "\u25CB" },
550
577
  { key: "parallelogram", label: "I/O", icon: "\u25B1" }
551
578
  ];
552
- function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDark = false, t, acc }) {
579
+ function StepEditor({
580
+ nodeId,
581
+ model,
582
+ onModelChange,
583
+ variant = "flowchart",
584
+ isDark = false,
585
+ t,
586
+ acc
587
+ }) {
553
588
  const isQuestion2 = variant === "question";
554
589
  const branchTerm = isQuestion2 ? "Answer" : "Branch";
555
590
  const tt = t ?? (isDark ? darkTheme : lightTheme);
@@ -583,27 +618,57 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
583
618
  const answers = node.metadata?.answers ?? [];
584
619
  const commitLabel = () => {
585
620
  if (label === node.label || !label.trim()) return;
586
- onModelChange({ ...model, nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, label: label.trim() } : n) });
621
+ onModelChange({
622
+ ...model,
623
+ nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, label: label.trim() } : n)
624
+ });
587
625
  };
588
626
  const setShape = (shape) => {
589
- onModelChange({ ...model, nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, shape } : n) });
627
+ onModelChange({
628
+ ...model,
629
+ nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, shape } : n)
630
+ });
590
631
  };
591
632
  const removeEdge = (edgeId) => {
592
633
  onModelChange({ ...model, edges: model.edges.filter((e) => e.id !== edgeId) });
593
634
  };
594
635
  const updateEdgeLabel = (edgeId, val) => {
595
- onModelChange({ ...model, edges: model.edges.map((e) => e.id === edgeId ? { ...e, label: val || void 0 } : e) });
636
+ onModelChange({
637
+ ...model,
638
+ edges: model.edges.map((e) => e.id === edgeId ? { ...e, label: val || void 0 } : e)
639
+ });
596
640
  };
597
641
  const addBranch = () => {
598
642
  if (branchMode === "new") {
599
643
  if (!branchLabel.trim()) return;
600
644
  const newId = nextId("node", model.nodes);
601
- const newNode = { id: newId, label: branchLabel.trim(), shape: "rectangle", x: (node.x ?? 0) + 200, y: (node.y ?? 0) + 20 + outEdges.length * 100 };
602
- const newEdge = { id: nextId("e", model.edges), from: nodeId, to: newId, label: branchEdgeLabel.trim() || void 0 };
603
- onModelChange({ ...model, nodes: [...model.nodes, newNode], edges: [...model.edges, newEdge] });
645
+ const newNode = {
646
+ id: newId,
647
+ label: branchLabel.trim(),
648
+ shape: "rectangle",
649
+ x: (node.x ?? 0) + 200,
650
+ y: (node.y ?? 0) + 20 + outEdges.length * 100
651
+ };
652
+ const newEdge = {
653
+ id: nextId("e", model.edges),
654
+ from: nodeId,
655
+ to: newId,
656
+ label: branchEdgeLabel.trim() || void 0
657
+ };
658
+ onModelChange({
659
+ ...model,
660
+ nodes: [...model.nodes, newNode],
661
+ edges: [...model.edges, newEdge]
662
+ });
604
663
  } else {
605
- if (!branchTarget || model.edges.some((e) => e.from === nodeId && e.to === branchTarget)) return;
606
- const newEdge = { id: nextId("e", model.edges), from: nodeId, to: branchTarget, label: branchEdgeLabel.trim() || void 0 };
664
+ if (!branchTarget || model.edges.some((e) => e.from === nodeId && e.to === branchTarget))
665
+ return;
666
+ const newEdge = {
667
+ id: nextId("e", model.edges),
668
+ from: nodeId,
669
+ to: branchTarget,
670
+ label: branchEdgeLabel.trim() || void 0
671
+ };
607
672
  onModelChange({ ...model, edges: [...model.edges, newEdge] });
608
673
  }
609
674
  setBranchLabel("");
@@ -615,7 +680,12 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
615
680
  const trimmed = newAnswer.trim();
616
681
  if (!trimmed || answers.includes(trimmed)) return;
617
682
  const updated = [...answers, trimmed];
618
- onModelChange({ ...model, nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, metadata: { ...n.metadata ?? {}, answers: updated } } : n) });
683
+ onModelChange({
684
+ ...model,
685
+ nodes: model.nodes.map(
686
+ (n) => n.id === nodeId ? { ...n, metadata: { ...n.metadata ?? {}, answers: updated } } : n
687
+ )
688
+ });
619
689
  setNewAnswer("");
620
690
  setAddingAnswer(false);
621
691
  };
@@ -624,7 +694,9 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
624
694
  const updatedEdges = model.edges.filter((e) => !(e.from === nodeId && e.label === ans));
625
695
  onModelChange({
626
696
  ...model,
627
- nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, metadata: { ...n.metadata ?? {}, answers: updated } } : n),
697
+ nodes: model.nodes.map(
698
+ (n) => n.id === nodeId ? { ...n, metadata: { ...n.metadata ?? {}, answers: updated } } : n
699
+ ),
628
700
  edges: updatedEdges
629
701
  });
630
702
  };
@@ -633,7 +705,12 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
633
705
  if (next < 0 || next >= answers.length) return;
634
706
  const arr = [...answers];
635
707
  [arr[idx], arr[next]] = [arr[next], arr[idx]];
636
- onModelChange({ ...model, nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, metadata: { ...n.metadata ?? {}, answers: arr } } : n) });
708
+ onModelChange({
709
+ ...model,
710
+ nodes: model.nodes.map(
711
+ (n) => n.id === nodeId ? { ...n, metadata: { ...n.metadata ?? {}, answers: arr } } : n
712
+ )
713
+ });
637
714
  };
638
715
  const inputStyle = {
639
716
  width: "100%",
@@ -688,167 +765,600 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
688
765
  fontFamily: "inherit",
689
766
  transition: "background 0.15s, border-color 0.15s"
690
767
  };
691
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { width: 272, minWidth: 272, background: tt.panelBg, borderLeft: `1px solid ${tt.panelBorder}`, display: "flex", flexDirection: "column", overflow: "hidden" }, children: [
692
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
693
- padding: "12px 16px",
694
- fontWeight: 700,
695
- fontSize: 12,
696
- letterSpacing: 0.8,
697
- textTransform: "uppercase",
698
- color: accentColor,
699
- borderBottom: `1px solid ${accentBorder}`,
700
- background: accentLight,
701
- display: "flex",
702
- alignItems: "center",
703
- gap: 8
704
- }, children: [
705
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { width: 6, height: 6, borderRadius: "50%", background: accentColor } }),
706
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: isQuestion2 ? "Question Editor" : variant === "journey" ? "Step Editor" : "Step Editor" })
707
- ] }),
708
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { flex: 1, overflowY: "auto", display: "flex", flexDirection: "column" }, children: [
709
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}` }, children: [
710
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: { display: "block", fontSize: 10, fontWeight: 700, color: tt.labelText, marginBottom: 8, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Name" }),
711
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
712
- "input",
768
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
769
+ "div",
770
+ {
771
+ style: {
772
+ width: 272,
773
+ minWidth: 272,
774
+ background: tt.panelBg,
775
+ borderLeft: `1px solid ${tt.panelBorder}`,
776
+ display: "flex",
777
+ flexDirection: "column",
778
+ overflow: "hidden"
779
+ },
780
+ children: [
781
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
782
+ "div",
713
783
  {
714
- ref: inputRef,
715
- value: label,
716
- onChange: (e) => setLabel(e.target.value),
717
- onBlur: commitLabel,
718
- onKeyDown: (e) => e.key === "Enter" && commitLabel(),
719
- style: inputStyle,
720
- placeholder: "Step name\u2026"
721
- }
722
- )
723
- ] }),
724
- !isQuestion2 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}` }, children: [
725
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: { display: "block", fontSize: 10, fontWeight: 700, color: tt.labelText, marginBottom: 8, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Shape" }),
726
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6 }, children: SHAPES.map((s2) => {
727
- const active = (node.shape ?? "rectangle") === s2.key;
728
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { onClick: () => setShape(s2.key), "aria-pressed": active, style: {
729
- display: "flex",
730
- flexDirection: "column",
731
- alignItems: "center",
732
- gap: 4,
733
- padding: "8px 6px",
734
- borderRadius: 8,
735
- cursor: "pointer",
736
- transition: "all 0.15s",
737
- background: active ? accentColor : tt.shapeBtnBg,
738
- color: active ? "#fff" : tt.textSecondary,
739
- border: active ? `1.5px solid ${accentColor}` : `1.5px solid ${tt.shapeBtnBorder}`
740
- }, children: [
741
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: 16, lineHeight: 1 }, children: s2.icon }),
742
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: 11, fontWeight: 500 }, children: s2.label })
743
- ] }, s2.key);
744
- }) })
745
- ] }),
746
- isQuestion2 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}`, flex: 1 }, children: [
747
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 10 }, children: [
748
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: { display: "block", fontSize: 10, fontWeight: 700, color: tt.labelText, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Answers" }),
749
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: 11, color: tt.textMuted, background: isDark ? "#0f172a" : "#f1f5f9", padding: "1px 7px", borderRadius: 99, fontWeight: 600 }, children: answers.length })
750
- ] }),
751
- answers.length === 0 && !addingAnswer && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 12, color: tt.textMuted, textAlign: "center", padding: "16px 0", fontStyle: "italic" }, children: "No answers yet \u2014 add one below" }),
752
- answers.map((ans, i) => {
753
- const connected = model.edges.some((e) => e.from === nodeId && e.label === ans);
754
- const targetNode = model.nodes.find((n) => {
755
- const e = model.edges.find((ex) => ex.from === nodeId && ex.label === ans);
756
- return e && n.id === e.to;
757
- });
758
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", alignItems: "flex-start", gap: 0, marginBottom: 8, borderRadius: 12, border: `1px solid ${tt.cardBorder}`, overflow: "hidden", background: tt.cardBg, boxShadow: isDark ? "none" : "0 1px 2px rgba(15,23,42,0.04)" }, children: [
759
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { width: 4, alignSelf: "stretch", background: accentColor, flexShrink: 0 } }),
760
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { flex: 1, minWidth: 0, padding: "8px 10px" }, children: [
761
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 12, fontWeight: 600, color: tt.textPrimary, marginBottom: connected ? 3 : 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: ans }),
762
- connected && targetNode && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: 11, color: accentColor, opacity: 0.85 }, children: [
763
- "\u2192 ",
764
- targetNode.label
765
- ] }),
766
- !connected && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 10, color: tt.textMuted, fontStyle: "italic" }, children: "drag port to connect" })
767
- ] }),
768
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", padding: "4px 2px", gap: 2 }, children: [
769
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => moveAnswer(i, -1), disabled: i === 0, style: { background: "none", border: "none", color: tt.textMuted, cursor: "pointer", fontSize: 11, padding: "2px 4px", opacity: i === 0 ? 0.3 : 1 }, children: "\u2191" }),
770
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => moveAnswer(i, 1), disabled: i === answers.length - 1, style: { background: "none", border: "none", color: tt.textMuted, cursor: "pointer", fontSize: 11, padding: "2px 4px", opacity: i === answers.length - 1 ? 0.3 : 1 }, children: "\u2193" })
771
- ] }),
772
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => removeAnswer(ans), style: { background: "none", border: "none", color: tt.textMuted, cursor: "pointer", fontSize: 12, padding: "8px 10px", flexShrink: 0 }, title: "Remove", children: "\u2715" })
773
- ] }, ans + i);
774
- }),
775
- addingAnswer ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { role: "group", "aria-label": "Add answer form", onKeyDown: (e) => {
776
- if (e.key === "Escape") {
777
- setAddingAnswer(false);
778
- setNewAnswer("");
784
+ style: {
785
+ padding: "12px 16px",
786
+ fontWeight: 700,
787
+ fontSize: 12,
788
+ letterSpacing: 0.8,
789
+ textTransform: "uppercase",
790
+ color: accentColor,
791
+ borderBottom: `1px solid ${accentBorder}`,
792
+ background: accentLight,
793
+ display: "flex",
794
+ alignItems: "center",
795
+ gap: 8
796
+ },
797
+ children: [
798
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { width: 6, height: 6, borderRadius: "50%", background: accentColor } }),
799
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: isQuestion2 ? "Question Editor" : variant === "journey" ? "Step Editor" : "Step Editor" })
800
+ ]
779
801
  }
780
- }, style: { marginTop: 10, background: tt.addFormBg, borderRadius: 10, padding: 12, border: `1.5px solid ${accentBorder}` }, children: [
781
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { autoFocus: true, value: newAnswer, onChange: (e) => setNewAnswer(e.target.value), onKeyDown: (e) => e.key === "Enter" && addAnswer(), placeholder: "Answer text\u2026", style: { ...inputStyle, marginBottom: 8 } }),
782
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: 6 }, children: [
783
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: addAnswer, style: addBtnStyle, children: "Add Answer" }),
784
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => {
785
- setAddingAnswer(false);
786
- setNewAnswer("");
787
- }, style: cancelBtnStyle, children: "Cancel" })
788
- ] })
789
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { onClick: () => setAddingAnswer(true), style: addTriggerStyle, children: [
790
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: 16, lineHeight: 1 }, children: "+" }),
791
- " Add Answer"
792
- ] }),
793
- answers.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginTop: 12, padding: "8px 10px", background: isDark ? "rgba(251,191,36,0.06)" : "#fef9f0", borderRadius: 8, border: `1px solid ${accentBorder}` }, children: [
794
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 10, fontWeight: 700, color: accentColor, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 4 }, children: "How to connect" }),
795
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 11, color: tt.textSecondary, lineHeight: 1.5 }, children: "Hover the question node on the canvas \u2014 drag an answer's port dot to any other node." })
796
- ] })
797
- ] }),
798
- !isQuestion2 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}`, flex: 1 }, children: [
799
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 10 }, children: [
800
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: { display: "block", fontSize: 10, fontWeight: 700, color: tt.labelText, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Branches" }),
801
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: 11, color: tt.textMuted, background: isDark ? "#0f172a" : "#f1f5f9", padding: "1px 7px", borderRadius: 99, fontWeight: 600 }, children: outEdges.length })
802
- ] }),
803
- outEdges.length === 0 && !addingBranch && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 12, color: tt.textMuted, textAlign: "center", padding: "16px 0", fontStyle: "italic" }, children: "No outgoing connections yet" }),
804
- outEdges.map((edge) => {
805
- const target = model.nodes.find((n) => n.id === edge.to);
806
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", alignItems: "flex-start", gap: 0, marginBottom: 8, borderRadius: 12, border: `1px solid ${tt.cardBorder}`, overflow: "hidden", background: tt.cardBg, boxShadow: isDark ? "none" : "0 1px 2px rgba(15,23,42,0.04)" }, children: [
807
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { width: 4, alignSelf: "stretch", background: accentColor, flexShrink: 0 } }),
808
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { flex: 1, minWidth: 0, padding: "8px 10px" }, children: [
809
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: 12, fontWeight: 600, color: tt.textPrimary, marginBottom: 5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [
810
- "\u2192 ",
811
- target?.label ?? edge.to
812
- ] }),
813
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { value: edge.label ?? "", onChange: (e) => updateEdgeLabel(edge.id, e.target.value), placeholder: "Edge label (optional)", style: { ...inputStyle, fontSize: 11, padding: "4px 8px" } })
814
- ] }),
815
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => removeEdge(edge.id), style: { background: "none", border: "none", color: tt.textMuted, cursor: "pointer", fontSize: 12, padding: "8px 10px", flexShrink: 0 }, title: "Remove", children: "\u2715" })
816
- ] }, edge.id);
817
- }),
818
- addingBranch ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { role: "group", "aria-label": "Add branch form", onKeyDown: (e) => {
819
- if (e.key === "Escape") setAddingBranch(false);
820
- }, style: { marginTop: 10, background: tt.addFormBg, borderRadius: 10, padding: 12, border: `1.5px solid ${accentBorder}` }, children: [
821
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "flex", gap: 6, marginBottom: 10 }, children: ["new", "existing"].map((mode) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => setBranchMode(mode), style: {
822
- flex: 1,
823
- padding: "5px 0",
824
- border: "none",
825
- borderRadius: 6,
826
- cursor: "pointer",
827
- fontSize: 11,
828
- fontWeight: 600,
829
- background: branchMode === mode ? accentColor : tt.btnSecBg,
830
- color: branchMode === mode ? "#fff" : tt.btnSecText
831
- }, children: mode === "new" ? `+ New step` : "Existing step" }, mode)) }),
832
- branchMode === "new" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { autoFocus: true, value: branchLabel, onChange: (e) => setBranchLabel(e.target.value), onKeyDown: (e) => e.key === "Enter" && addBranch(), placeholder: "New step name\u2026", style: { ...inputStyle, marginBottom: 6 } }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("select", { value: branchTarget, onChange: (e) => setBranchTarget(e.target.value), style: { ...inputStyle, marginBottom: 6, appearance: "none" }, children: [
833
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "", children: "Choose a step\u2026" }),
834
- otherNodes.map((n) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: n.id, children: n.label }, n.id))
802
+ ),
803
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { flex: 1, overflowY: "auto", display: "flex", flexDirection: "column" }, children: [
804
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}` }, children: [
805
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
806
+ "label",
807
+ {
808
+ style: {
809
+ display: "block",
810
+ fontSize: 10,
811
+ fontWeight: 700,
812
+ color: tt.labelText,
813
+ marginBottom: 8,
814
+ textTransform: "uppercase",
815
+ letterSpacing: 0.8
816
+ },
817
+ children: "Name"
818
+ }
819
+ ),
820
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
821
+ "input",
822
+ {
823
+ ref: inputRef,
824
+ value: label,
825
+ onChange: (e) => setLabel(e.target.value),
826
+ onBlur: commitLabel,
827
+ onKeyDown: (e) => e.key === "Enter" && commitLabel(),
828
+ style: inputStyle,
829
+ placeholder: "Step name\u2026"
830
+ }
831
+ )
835
832
  ] }),
836
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { value: branchEdgeLabel, onChange: (e) => setBranchEdgeLabel(e.target.value), placeholder: "Edge label (optional)", style: { ...inputStyle, marginBottom: 10 } }),
837
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: 6 }, children: [
838
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { onClick: addBranch, style: addBtnStyle, children: [
839
- "Add ",
840
- branchTerm
841
- ] }),
842
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => setAddingBranch(false), style: cancelBtnStyle, children: "Cancel" })
843
- ] })
844
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { onClick: () => setAddingBranch(true), style: addTriggerStyle, children: [
845
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: 16, lineHeight: 1 }, children: "+" }),
846
- " Add ",
847
- branchTerm
833
+ !isQuestion2 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}` }, children: [
834
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
835
+ "label",
836
+ {
837
+ style: {
838
+ display: "block",
839
+ fontSize: 10,
840
+ fontWeight: 700,
841
+ color: tt.labelText,
842
+ marginBottom: 8,
843
+ textTransform: "uppercase",
844
+ letterSpacing: 0.8
845
+ },
846
+ children: "Shape"
847
+ }
848
+ ),
849
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6 }, children: SHAPES.map((s2) => {
850
+ const active = (node.shape ?? "rectangle") === s2.key;
851
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
852
+ "button",
853
+ {
854
+ onClick: () => setShape(s2.key),
855
+ "aria-pressed": active,
856
+ style: {
857
+ display: "flex",
858
+ flexDirection: "column",
859
+ alignItems: "center",
860
+ gap: 4,
861
+ padding: "8px 6px",
862
+ borderRadius: 8,
863
+ cursor: "pointer",
864
+ transition: "all 0.15s",
865
+ background: active ? accentColor : tt.shapeBtnBg,
866
+ color: active ? "#fff" : tt.textSecondary,
867
+ border: active ? `1.5px solid ${accentColor}` : `1.5px solid ${tt.shapeBtnBorder}`
868
+ },
869
+ children: [
870
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: 16, lineHeight: 1 }, children: s2.icon }),
871
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: 11, fontWeight: 500 }, children: s2.label })
872
+ ]
873
+ },
874
+ s2.key
875
+ );
876
+ }) })
877
+ ] }),
878
+ isQuestion2 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
879
+ "section",
880
+ {
881
+ style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}`, flex: 1 },
882
+ children: [
883
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
884
+ "div",
885
+ {
886
+ style: {
887
+ display: "flex",
888
+ alignItems: "center",
889
+ justifyContent: "space-between",
890
+ marginBottom: 10
891
+ },
892
+ children: [
893
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
894
+ "label",
895
+ {
896
+ style: {
897
+ display: "block",
898
+ fontSize: 10,
899
+ fontWeight: 700,
900
+ color: tt.labelText,
901
+ textTransform: "uppercase",
902
+ letterSpacing: 0.8
903
+ },
904
+ children: "Answers"
905
+ }
906
+ ),
907
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
908
+ "span",
909
+ {
910
+ style: {
911
+ fontSize: 11,
912
+ color: tt.textMuted,
913
+ background: isDark ? "#0f172a" : "#f1f5f9",
914
+ padding: "1px 7px",
915
+ borderRadius: 99,
916
+ fontWeight: 600
917
+ },
918
+ children: answers.length
919
+ }
920
+ )
921
+ ]
922
+ }
923
+ ),
924
+ answers.length === 0 && !addingAnswer && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
925
+ "div",
926
+ {
927
+ style: {
928
+ fontSize: 12,
929
+ color: tt.textMuted,
930
+ textAlign: "center",
931
+ padding: "16px 0",
932
+ fontStyle: "italic"
933
+ },
934
+ children: "No answers yet \u2014 add one below"
935
+ }
936
+ ),
937
+ answers.map((ans, i) => {
938
+ const connected = model.edges.some((e) => e.from === nodeId && e.label === ans);
939
+ const targetNode = model.nodes.find((n) => {
940
+ const e = model.edges.find((ex) => ex.from === nodeId && ex.label === ans);
941
+ return e && n.id === e.to;
942
+ });
943
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
944
+ "div",
945
+ {
946
+ style: {
947
+ display: "flex",
948
+ alignItems: "flex-start",
949
+ gap: 0,
950
+ marginBottom: 8,
951
+ borderRadius: 12,
952
+ border: `1px solid ${tt.cardBorder}`,
953
+ overflow: "hidden",
954
+ background: tt.cardBg,
955
+ boxShadow: isDark ? "none" : "0 1px 2px rgba(15,23,42,0.04)"
956
+ },
957
+ children: [
958
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
959
+ "div",
960
+ {
961
+ style: {
962
+ width: 4,
963
+ alignSelf: "stretch",
964
+ background: accentColor,
965
+ flexShrink: 0
966
+ }
967
+ }
968
+ ),
969
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { flex: 1, minWidth: 0, padding: "8px 10px" }, children: [
970
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
971
+ "div",
972
+ {
973
+ style: {
974
+ fontSize: 12,
975
+ fontWeight: 600,
976
+ color: tt.textPrimary,
977
+ marginBottom: connected ? 3 : 0,
978
+ overflow: "hidden",
979
+ textOverflow: "ellipsis",
980
+ whiteSpace: "nowrap"
981
+ },
982
+ children: ans
983
+ }
984
+ ),
985
+ connected && targetNode && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: 11, color: accentColor, opacity: 0.85 }, children: [
986
+ "\u2192 ",
987
+ targetNode.label
988
+ ] }),
989
+ !connected && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 10, color: tt.textMuted, fontStyle: "italic" }, children: "drag port to connect" })
990
+ ] }),
991
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
992
+ "div",
993
+ {
994
+ style: { display: "flex", flexDirection: "column", padding: "4px 2px", gap: 2 },
995
+ children: [
996
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
997
+ "button",
998
+ {
999
+ onClick: () => moveAnswer(i, -1),
1000
+ disabled: i === 0,
1001
+ style: {
1002
+ background: "none",
1003
+ border: "none",
1004
+ color: tt.textMuted,
1005
+ cursor: "pointer",
1006
+ fontSize: 11,
1007
+ padding: "2px 4px",
1008
+ opacity: i === 0 ? 0.3 : 1
1009
+ },
1010
+ children: "\u2191"
1011
+ }
1012
+ ),
1013
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1014
+ "button",
1015
+ {
1016
+ onClick: () => moveAnswer(i, 1),
1017
+ disabled: i === answers.length - 1,
1018
+ style: {
1019
+ background: "none",
1020
+ border: "none",
1021
+ color: tt.textMuted,
1022
+ cursor: "pointer",
1023
+ fontSize: 11,
1024
+ padding: "2px 4px",
1025
+ opacity: i === answers.length - 1 ? 0.3 : 1
1026
+ },
1027
+ children: "\u2193"
1028
+ }
1029
+ )
1030
+ ]
1031
+ }
1032
+ ),
1033
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1034
+ "button",
1035
+ {
1036
+ onClick: () => removeAnswer(ans),
1037
+ style: {
1038
+ background: "none",
1039
+ border: "none",
1040
+ color: tt.textMuted,
1041
+ cursor: "pointer",
1042
+ fontSize: 12,
1043
+ padding: "8px 10px",
1044
+ flexShrink: 0
1045
+ },
1046
+ title: "Remove",
1047
+ children: "\u2715"
1048
+ }
1049
+ )
1050
+ ]
1051
+ },
1052
+ ans + i
1053
+ );
1054
+ }),
1055
+ addingAnswer ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1056
+ "div",
1057
+ {
1058
+ role: "group",
1059
+ "aria-label": "Add answer form",
1060
+ onKeyDown: (e) => {
1061
+ if (e.key === "Escape") {
1062
+ setAddingAnswer(false);
1063
+ setNewAnswer("");
1064
+ }
1065
+ },
1066
+ style: {
1067
+ marginTop: 10,
1068
+ background: tt.addFormBg,
1069
+ borderRadius: 10,
1070
+ padding: 12,
1071
+ border: `1.5px solid ${accentBorder}`
1072
+ },
1073
+ children: [
1074
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1075
+ "input",
1076
+ {
1077
+ autoFocus: true,
1078
+ value: newAnswer,
1079
+ onChange: (e) => setNewAnswer(e.target.value),
1080
+ onKeyDown: (e) => e.key === "Enter" && addAnswer(),
1081
+ placeholder: "Answer text\u2026",
1082
+ style: { ...inputStyle, marginBottom: 8 }
1083
+ }
1084
+ ),
1085
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: 6 }, children: [
1086
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: addAnswer, style: addBtnStyle, children: "Add Answer" }),
1087
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1088
+ "button",
1089
+ {
1090
+ onClick: () => {
1091
+ setAddingAnswer(false);
1092
+ setNewAnswer("");
1093
+ },
1094
+ style: cancelBtnStyle,
1095
+ children: "Cancel"
1096
+ }
1097
+ )
1098
+ ] })
1099
+ ]
1100
+ }
1101
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { onClick: () => setAddingAnswer(true), style: addTriggerStyle, children: [
1102
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: 16, lineHeight: 1 }, children: "+" }),
1103
+ " Add Answer"
1104
+ ] }),
1105
+ answers.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1106
+ "div",
1107
+ {
1108
+ style: {
1109
+ marginTop: 12,
1110
+ padding: "8px 10px",
1111
+ background: isDark ? "rgba(251,191,36,0.06)" : "#fef9f0",
1112
+ borderRadius: 8,
1113
+ border: `1px solid ${accentBorder}`
1114
+ },
1115
+ children: [
1116
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1117
+ "div",
1118
+ {
1119
+ style: {
1120
+ fontSize: 10,
1121
+ fontWeight: 700,
1122
+ color: accentColor,
1123
+ textTransform: "uppercase",
1124
+ letterSpacing: 0.6,
1125
+ marginBottom: 4
1126
+ },
1127
+ children: "How to connect"
1128
+ }
1129
+ ),
1130
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 11, color: tt.textSecondary, lineHeight: 1.5 }, children: "Hover the question node on the canvas \u2014 drag an answer's port dot to any other node." })
1131
+ ]
1132
+ }
1133
+ )
1134
+ ]
1135
+ }
1136
+ ),
1137
+ !isQuestion2 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1138
+ "section",
1139
+ {
1140
+ style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}`, flex: 1 },
1141
+ children: [
1142
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1143
+ "div",
1144
+ {
1145
+ style: {
1146
+ display: "flex",
1147
+ alignItems: "center",
1148
+ justifyContent: "space-between",
1149
+ marginBottom: 10
1150
+ },
1151
+ children: [
1152
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1153
+ "label",
1154
+ {
1155
+ style: {
1156
+ display: "block",
1157
+ fontSize: 10,
1158
+ fontWeight: 700,
1159
+ color: tt.labelText,
1160
+ textTransform: "uppercase",
1161
+ letterSpacing: 0.8
1162
+ },
1163
+ children: "Branches"
1164
+ }
1165
+ ),
1166
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1167
+ "span",
1168
+ {
1169
+ style: {
1170
+ fontSize: 11,
1171
+ color: tt.textMuted,
1172
+ background: isDark ? "#0f172a" : "#f1f5f9",
1173
+ padding: "1px 7px",
1174
+ borderRadius: 99,
1175
+ fontWeight: 600
1176
+ },
1177
+ children: outEdges.length
1178
+ }
1179
+ )
1180
+ ]
1181
+ }
1182
+ ),
1183
+ outEdges.length === 0 && !addingBranch && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1184
+ "div",
1185
+ {
1186
+ style: {
1187
+ fontSize: 12,
1188
+ color: tt.textMuted,
1189
+ textAlign: "center",
1190
+ padding: "16px 0",
1191
+ fontStyle: "italic"
1192
+ },
1193
+ children: "No outgoing connections yet"
1194
+ }
1195
+ ),
1196
+ outEdges.map((edge) => {
1197
+ const target = model.nodes.find((n) => n.id === edge.to);
1198
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1199
+ "div",
1200
+ {
1201
+ style: {
1202
+ display: "flex",
1203
+ alignItems: "flex-start",
1204
+ gap: 0,
1205
+ marginBottom: 8,
1206
+ borderRadius: 12,
1207
+ border: `1px solid ${tt.cardBorder}`,
1208
+ overflow: "hidden",
1209
+ background: tt.cardBg,
1210
+ boxShadow: isDark ? "none" : "0 1px 2px rgba(15,23,42,0.04)"
1211
+ },
1212
+ children: [
1213
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1214
+ "div",
1215
+ {
1216
+ style: {
1217
+ width: 4,
1218
+ alignSelf: "stretch",
1219
+ background: accentColor,
1220
+ flexShrink: 0
1221
+ }
1222
+ }
1223
+ ),
1224
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { flex: 1, minWidth: 0, padding: "8px 10px" }, children: [
1225
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1226
+ "div",
1227
+ {
1228
+ style: {
1229
+ fontSize: 12,
1230
+ fontWeight: 600,
1231
+ color: tt.textPrimary,
1232
+ marginBottom: 5,
1233
+ overflow: "hidden",
1234
+ textOverflow: "ellipsis",
1235
+ whiteSpace: "nowrap"
1236
+ },
1237
+ children: [
1238
+ "\u2192 ",
1239
+ target?.label ?? edge.to
1240
+ ]
1241
+ }
1242
+ ),
1243
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1244
+ "input",
1245
+ {
1246
+ value: edge.label ?? "",
1247
+ onChange: (e) => updateEdgeLabel(edge.id, e.target.value),
1248
+ placeholder: "Edge label (optional)",
1249
+ style: { ...inputStyle, fontSize: 11, padding: "4px 8px" }
1250
+ }
1251
+ )
1252
+ ] }),
1253
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1254
+ "button",
1255
+ {
1256
+ onClick: () => removeEdge(edge.id),
1257
+ style: {
1258
+ background: "none",
1259
+ border: "none",
1260
+ color: tt.textMuted,
1261
+ cursor: "pointer",
1262
+ fontSize: 12,
1263
+ padding: "8px 10px",
1264
+ flexShrink: 0
1265
+ },
1266
+ title: "Remove",
1267
+ children: "\u2715"
1268
+ }
1269
+ )
1270
+ ]
1271
+ },
1272
+ edge.id
1273
+ );
1274
+ }),
1275
+ addingBranch ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1276
+ "div",
1277
+ {
1278
+ role: "group",
1279
+ "aria-label": "Add branch form",
1280
+ onKeyDown: (e) => {
1281
+ if (e.key === "Escape") setAddingBranch(false);
1282
+ },
1283
+ style: {
1284
+ marginTop: 10,
1285
+ background: tt.addFormBg,
1286
+ borderRadius: 10,
1287
+ padding: 12,
1288
+ border: `1.5px solid ${accentBorder}`
1289
+ },
1290
+ children: [
1291
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "flex", gap: 6, marginBottom: 10 }, children: ["new", "existing"].map((mode) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1292
+ "button",
1293
+ {
1294
+ onClick: () => setBranchMode(mode),
1295
+ style: {
1296
+ flex: 1,
1297
+ padding: "5px 0",
1298
+ border: "none",
1299
+ borderRadius: 6,
1300
+ cursor: "pointer",
1301
+ fontSize: 11,
1302
+ fontWeight: 600,
1303
+ background: branchMode === mode ? accentColor : tt.btnSecBg,
1304
+ color: branchMode === mode ? "#fff" : tt.btnSecText
1305
+ },
1306
+ children: mode === "new" ? `+ New step` : "Existing step"
1307
+ },
1308
+ mode
1309
+ )) }),
1310
+ branchMode === "new" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1311
+ "input",
1312
+ {
1313
+ autoFocus: true,
1314
+ value: branchLabel,
1315
+ onChange: (e) => setBranchLabel(e.target.value),
1316
+ onKeyDown: (e) => e.key === "Enter" && addBranch(),
1317
+ placeholder: "New step name\u2026",
1318
+ style: { ...inputStyle, marginBottom: 6 }
1319
+ }
1320
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1321
+ "select",
1322
+ {
1323
+ value: branchTarget,
1324
+ onChange: (e) => setBranchTarget(e.target.value),
1325
+ style: { ...inputStyle, marginBottom: 6, appearance: "none" },
1326
+ children: [
1327
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "", children: "Choose a step\u2026" }),
1328
+ otherNodes.map((n) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: n.id, children: n.label }, n.id))
1329
+ ]
1330
+ }
1331
+ ),
1332
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1333
+ "input",
1334
+ {
1335
+ value: branchEdgeLabel,
1336
+ onChange: (e) => setBranchEdgeLabel(e.target.value),
1337
+ placeholder: "Edge label (optional)",
1338
+ style: { ...inputStyle, marginBottom: 10 }
1339
+ }
1340
+ ),
1341
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: 6 }, children: [
1342
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { onClick: addBranch, style: addBtnStyle, children: [
1343
+ "Add ",
1344
+ branchTerm
1345
+ ] }),
1346
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => setAddingBranch(false), style: cancelBtnStyle, children: "Cancel" })
1347
+ ] })
1348
+ ]
1349
+ }
1350
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { onClick: () => setAddingBranch(true), style: addTriggerStyle, children: [
1351
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: 16, lineHeight: 1 }, children: "+" }),
1352
+ " Add ",
1353
+ branchTerm
1354
+ ] })
1355
+ ]
1356
+ }
1357
+ )
848
1358
  ] })
849
- ] })
850
- ] })
851
- ] });
1359
+ ]
1360
+ }
1361
+ );
852
1362
  }
853
1363
 
854
1364
  // src/ui/SequenceEditor.tsx
@@ -903,26 +1413,33 @@ function SequenceCanvas(props) {
903
1413
  return next;
904
1414
  }, [messages, drag]);
905
1415
  if (actors.length === 0 && messages.length === 0) {
906
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
907
- position: "absolute",
908
- inset: 0,
909
- display: "flex",
910
- flexDirection: "column",
911
- alignItems: "center",
912
- justifyContent: "center",
913
- gap: 10,
914
- color: t.textMuted,
915
- pointerEvents: "none"
916
- }, children: [
917
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 36, opacity: 0.15, color: t.textPrimary }, children: "\u2194" }),
918
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { fontSize: 13, fontWeight: 500 }, children: [
919
- "Click ",
920
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { style: { color: INDIGO }, children: "+ Actor" }),
921
- " then ",
922
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { style: { color: INDIGO }, children: "+ Message" }),
923
- " to start"
924
- ] })
925
- ] });
1416
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1417
+ "div",
1418
+ {
1419
+ style: {
1420
+ position: "absolute",
1421
+ inset: 0,
1422
+ display: "flex",
1423
+ flexDirection: "column",
1424
+ alignItems: "center",
1425
+ justifyContent: "center",
1426
+ gap: 10,
1427
+ color: t.textMuted,
1428
+ pointerEvents: "none"
1429
+ },
1430
+ children: [
1431
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 36, opacity: 0.15, color: t.textPrimary }, children: "\u2194" }),
1432
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { fontSize: 13, fontWeight: 500 }, children: [
1433
+ "Click ",
1434
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { style: { color: INDIGO }, children: "+ Actor" }),
1435
+ " then",
1436
+ " ",
1437
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { style: { color: INDIGO }, children: "+ Message" }),
1438
+ " to start"
1439
+ ] })
1440
+ ]
1441
+ }
1442
+ );
926
1443
  }
927
1444
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
928
1445
  "svg",
@@ -930,12 +1447,28 @@ function SequenceCanvas(props) {
930
1447
  ref: svgRef,
931
1448
  width: totalW,
932
1449
  height: totalH,
933
- style: { display: "block", cursor: drag?.active ? "grabbing" : "default", userSelect: "none" },
1450
+ style: {
1451
+ display: "block",
1452
+ cursor: drag?.active ? "grabbing" : "default",
1453
+ userSelect: "none"
1454
+ },
934
1455
  children: [
935
1456
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("defs", { children: [
936
1457
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pattern", { id: "seqdots", x: "0", y: "0", width: "24", height: "24", patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { cx: 12, cy: 12, r: 1.1, fill: t.dot }) }),
937
1458
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("filter", { id: "seqShadow", x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("feDropShadow", { dx: 0, dy: 3, stdDeviation: 5, floodColor: shadowColor(isDark) }) }),
938
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("marker", { id: "seqArrow", markerWidth: 9, markerHeight: 7, refX: 8.5, refY: 3.5, orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: t.arrow }) })
1459
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1460
+ "marker",
1461
+ {
1462
+ id: "seqArrow",
1463
+ markerWidth: 9,
1464
+ markerHeight: 7,
1465
+ refX: 8.5,
1466
+ refY: 3.5,
1467
+ orient: "auto",
1468
+ markerUnits: "strokeWidth",
1469
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: t.arrow })
1470
+ }
1471
+ )
939
1472
  ] }),
940
1473
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { width: totalW, height: totalH, fill: "url(#seqdots)" }),
941
1474
  actors.map((name) => {
@@ -984,8 +1517,28 @@ function SequenceCanvas(props) {
984
1517
  opacity: isDark ? 0.18 : 0.6
985
1518
  }
986
1519
  ),
987
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d, fill: "none", stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
988
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("text", { x: startX + loopW + 8, y: loopY + 16, fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
1520
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1521
+ "path",
1522
+ {
1523
+ d,
1524
+ fill: "none",
1525
+ stroke,
1526
+ strokeWidth: 1.5,
1527
+ strokeDasharray: dash,
1528
+ markerEnd: "url(#seqArrow)"
1529
+ }
1530
+ ),
1531
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1532
+ "text",
1533
+ {
1534
+ x: startX + loopW + 8,
1535
+ y: loopY + 16,
1536
+ fontSize: 11,
1537
+ fill: selectedHere ? INDIGO : t.textPrimary,
1538
+ fontWeight: 500,
1539
+ children: msg.label
1540
+ }
1541
+ )
989
1542
  ] }, msg.id);
990
1543
  }
991
1544
  const labelX = (fromX + toX) / 2;
@@ -1002,7 +1555,19 @@ function SequenceCanvas(props) {
1002
1555
  opacity: isDark ? 0.18 : 0.6
1003
1556
  }
1004
1557
  ),
1005
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: fromX, y1: y, x2: toX, y2: y, stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
1558
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1559
+ "line",
1560
+ {
1561
+ x1: fromX,
1562
+ y1: y,
1563
+ x2: toX,
1564
+ y2: y,
1565
+ stroke,
1566
+ strokeWidth: 1.5,
1567
+ strokeDasharray: dash,
1568
+ markerEnd: "url(#seqArrow)"
1569
+ }
1570
+ ),
1006
1571
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1007
1572
  "rect",
1008
1573
  {
@@ -1016,7 +1581,18 @@ function SequenceCanvas(props) {
1016
1581
  strokeWidth: selectedHere ? 1.25 : 1
1017
1582
  }
1018
1583
  ),
1019
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("text", { x: labelX, y: y - 5, textAnchor: "middle", fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
1584
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1585
+ "text",
1586
+ {
1587
+ x: labelX,
1588
+ y: y - 5,
1589
+ textAnchor: "middle",
1590
+ fontSize: 11,
1591
+ fill: selectedHere ? INDIGO : t.textPrimary,
1592
+ fontWeight: 500,
1593
+ children: msg.label
1594
+ }
1595
+ )
1020
1596
  ] }, msg.id);
1021
1597
  }),
1022
1598
  actors.map((name) => {
@@ -1356,7 +1932,9 @@ function computeLayout(model) {
1356
1932
  const h = isQuestion(n, model.variant) ? questionNodeH(n.metadata?.answers ?? []) : NODE_H;
1357
1933
  return { node: n, w, h };
1358
1934
  });
1359
- const allPositioned = sized.every((s2) => typeof s2.node.x === "number" && typeof s2.node.y === "number");
1935
+ const allPositioned = sized.every(
1936
+ (s2) => typeof s2.node.x === "number" && typeof s2.node.y === "number"
1937
+ );
1360
1938
  if (allPositioned) {
1361
1939
  for (const s2 of sized) {
1362
1940
  boxes.set(s2.node.id, { x: s2.node.x, y: s2.node.y, w: s2.w, h: s2.h });
@@ -1406,7 +1984,15 @@ function computeLayout(model) {
1406
1984
  return boxes;
1407
1985
  }
1408
1986
  function escapeXML(s2) {
1409
- return s2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1987
+ return s2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1988
+ }
1989
+ function sanitizeForSVG(s2) {
1990
+ let clean = s2;
1991
+ clean = clean.replace(/<\/?[a-zA-Z][^>]*>/g, "");
1992
+ clean = clean.replace(/\b(?:javascript|data|vbscript)\s*:/gi, "");
1993
+ clean = clean.replace(/\bon[a-z]+\s*=/gi, "");
1994
+ clean = clean.replace(/\x00/g, "");
1995
+ return escapeXML(clean);
1410
1996
  }
1411
1997
  var COLORS = {
1412
1998
  bg: "#fafbfc",
@@ -1425,8 +2011,8 @@ function renderStandardNode(node, box) {
1425
2011
  const cx = box.x + box.w / 2;
1426
2012
  const cy = box.y + box.h / 2;
1427
2013
  const shape = node.shape ?? "rectangle";
1428
- 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>`;
1429
- let shapeEl = "";
2014
+ 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}">${sanitizeForSVG(node.label)}</text>`;
2015
+ let shapeEl;
1430
2016
  if (shape === "diamond") {
1431
2017
  const pts = `${cx},${box.y} ${box.x + box.w},${cy} ${cx},${box.y + box.h} ${box.x},${cy}`;
1432
2018
  shapeEl = `<polygon points="${pts}" fill="${COLORS.nodeFill}" stroke="${COLORS.nodeStroke}" stroke-width="1.25" filter="url(#nodeShadow)"/>`;
@@ -1446,17 +2032,37 @@ function renderQuestionNode(node, box) {
1446
2032
  const clipId = `qhdr-${node.id.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
1447
2033
  const x = box.x, y = box.y, w = box.w, h = box.h;
1448
2034
  const parts = [];
1449
- 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)"/>`);
1450
- parts.push(`<defs><clipPath id="${clipId}"><rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" rx="14"/></clipPath></defs>`);
1451
- parts.push(`<rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" fill="${COLORS.amberSoft}" clip-path="url(#${clipId})"/>`);
1452
- parts.push(`<rect x="${x}" y="${y}" width="4" height="${Q_BASE_H}" rx="2" fill="${COLORS.amber}"/>`);
1453
- parts.push(`<rect x="${x + 12}" y="${y + 14}" width="28" height="28" rx="8" fill="${COLORS.amber}"/>`);
1454
- parts.push(`<text x="${x + 26}" y="${y + 33}" text-anchor="middle" font-size="15" font-weight="900" fill="white">?</text>`);
1455
- 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>`);
1456
- 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>`);
1457
- parts.push(`<line x1="${x}" y1="${y + Q_BASE_H}" x2="${x + w}" y2="${y + Q_BASE_H}" stroke="${COLORS.amberLine}" stroke-width="1"/>`);
2035
+ parts.push(
2036
+ `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="14" fill="${COLORS.nodeFill}" stroke="${COLORS.amberLine}" stroke-width="1.5" filter="url(#nodeShadow)"/>`
2037
+ );
2038
+ parts.push(
2039
+ `<defs><clipPath id="${clipId}"><rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" rx="14"/></clipPath></defs>`
2040
+ );
2041
+ parts.push(
2042
+ `<rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" fill="${COLORS.amberSoft}" clip-path="url(#${clipId})"/>`
2043
+ );
2044
+ parts.push(
2045
+ `<rect x="${x}" y="${y}" width="4" height="${Q_BASE_H}" rx="2" fill="${COLORS.amber}"/>`
2046
+ );
2047
+ parts.push(
2048
+ `<rect x="${x + 12}" y="${y + 14}" width="28" height="28" rx="8" fill="${COLORS.amber}"/>`
2049
+ );
2050
+ parts.push(
2051
+ `<text x="${x + 26}" y="${y + 33}" text-anchor="middle" font-size="15" font-weight="900" fill="white">?</text>`
2052
+ );
2053
+ parts.push(
2054
+ `<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>`
2055
+ );
2056
+ parts.push(
2057
+ `<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}">${sanitizeForSVG(node.label)}</text>`
2058
+ );
2059
+ parts.push(
2060
+ `<line x1="${x}" y1="${y + Q_BASE_H}" x2="${x + w}" y2="${y + Q_BASE_H}" stroke="${COLORS.amberLine}" stroke-width="1"/>`
2061
+ );
1458
2062
  if (answers.length === 0) {
1459
- 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>`);
2063
+ parts.push(
2064
+ `<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>`
2065
+ );
1460
2066
  } else {
1461
2067
  const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1462
2068
  answers.forEach((ans, i) => {
@@ -1469,10 +2075,18 @@ function renderQuestionNode(node, box) {
1469
2075
  const letter = i < 26 ? letters[i] : `${i + 1}`;
1470
2076
  const maxChars = Math.max(2, Math.floor((cW - 20) / 7.5));
1471
2077
  const displayAns = ans.length > maxChars ? ans.slice(0, maxChars - 1) + "\u2026" : ans;
1472
- parts.push(`<rect x="${cardX}" y="${cardY}" width="${cW}" height="${cardH}" rx="8" fill="${COLORS.amberCardBg}" stroke="${COLORS.amberLine}" stroke-width="1"/>`);
1473
- parts.push(`<rect x="${cx - 11}" y="${cardY + 7}" width="22" height="22" rx="6" fill="#fef3c7"/>`);
1474
- parts.push(`<text x="${cx}" y="${cardY + 22}" text-anchor="middle" font-size="10" font-weight="800" fill="${COLORS.amber}">${escapeXML(letter)}</text>`);
1475
- 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>`);
2078
+ parts.push(
2079
+ `<rect x="${cardX}" y="${cardY}" width="${cW}" height="${cardH}" rx="8" fill="${COLORS.amberCardBg}" stroke="${COLORS.amberLine}" stroke-width="1"/>`
2080
+ );
2081
+ parts.push(
2082
+ `<rect x="${cx - 11}" y="${cardY + 7}" width="22" height="22" rx="6" fill="#fef3c7"/>`
2083
+ );
2084
+ parts.push(
2085
+ `<text x="${cx}" y="${cardY + 22}" text-anchor="middle" font-size="10" font-weight="800" fill="${COLORS.amber}">${sanitizeForSVG(letter)}</text>`
2086
+ );
2087
+ parts.push(
2088
+ `<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">${sanitizeForSVG(displayAns)}</text>`
2089
+ );
1476
2090
  });
1477
2091
  }
1478
2092
  return parts.join("");
@@ -1510,7 +2124,7 @@ function renderEdge(edge, boxes, variant, nodes) {
1510
2124
  const midY = (y1 + y2) / 2;
1511
2125
  const labelW = estimateTextW(edge.label, 7) + 14;
1512
2126
  out += `<rect x="${midX - labelW / 2}" y="${midY - 11}" width="${labelW}" height="18" rx="9" fill="${COLORS.bg}" stroke="${COLORS.nodeStroke}" stroke-width="1"/>`;
1513
- 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>`;
2127
+ 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}">${sanitizeForSVG(edge.label)}</text>`;
1514
2128
  }
1515
2129
  return out;
1516
2130
  }
@@ -1536,7 +2150,7 @@ function toSVG(model) {
1536
2150
  `</marker>`,
1537
2151
  `</defs>`
1538
2152
  ].join("");
1539
- 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>` : "";
2153
+ 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}">${sanitizeForSVG(model.title)}</text>` : "";
1540
2154
  const edges = model.edges.map((e) => renderEdge(e, boxes, model.variant, model.nodes)).join("\n");
1541
2155
  const nodes = model.nodes.map((n) => {
1542
2156
  const b = boxes.get(n.id);
@@ -1553,7 +2167,9 @@ ${nodes}
1553
2167
  }
1554
2168
  async function toPNG(model) {
1555
2169
  if (typeof document === "undefined") {
1556
- throw new Error("toPNG requires a browser environment. For Node/Bun server use, pipe toSVG() through @resvg/resvg-js.");
2170
+ throw new Error(
2171
+ "toPNG requires a browser environment. For Node/Bun server use, pipe toSVG() through @resvg/resvg-js."
2172
+ );
1557
2173
  }
1558
2174
  const svg = toSVG(model);
1559
2175
  const blob = new Blob([svg], { type: "image/svg+xml" });
@@ -1569,7 +2185,10 @@ async function toPNG(model) {
1569
2185
  ctx.scale(scale, scale);
1570
2186
  ctx.drawImage(img, 0, 0);
1571
2187
  URL.revokeObjectURL(url);
1572
- canvas.toBlob((b) => b ? resolve(b) : reject(new Error("Canvas toBlob failed")), "image/png");
2188
+ canvas.toBlob(
2189
+ (b) => b ? resolve(b) : reject(new Error("Canvas toBlob failed")),
2190
+ "image/png"
2191
+ );
1573
2192
  };
1574
2193
  img.onerror = () => {
1575
2194
  URL.revokeObjectURL(url);
@@ -1581,40 +2200,43 @@ async function toPNG(model) {
1581
2200
 
1582
2201
  // src/ui/hooks/useExporters.ts
1583
2202
  function useExporters(model, onExport, filename = "diagram", onSuccess) {
1584
- return (0, import_react7.useCallback)(async (format) => {
1585
- let content;
1586
- switch (format) {
1587
- case "mermaid":
1588
- content = toMermaid(model);
1589
- break;
1590
- case "plantuml":
1591
- content = toPlantUML(model);
1592
- break;
1593
- case "json":
1594
- content = toJSON(model);
1595
- break;
1596
- case "svg":
1597
- content = toSVG(model);
1598
- break;
1599
- case "png":
1600
- content = await toPNG(model);
1601
- break;
1602
- default:
2203
+ return (0, import_react7.useCallback)(
2204
+ async (format) => {
2205
+ let content;
2206
+ switch (format) {
2207
+ case "mermaid":
2208
+ content = toMermaid(model);
2209
+ break;
2210
+ case "plantuml":
2211
+ content = toPlantUML(model);
2212
+ break;
2213
+ case "json":
2214
+ content = toJSON(model);
2215
+ break;
2216
+ case "svg":
2217
+ content = toSVG(model);
2218
+ break;
2219
+ case "png":
2220
+ content = await toPNG(model);
2221
+ break;
2222
+ default:
2223
+ return;
2224
+ }
2225
+ if (onExport) {
2226
+ onExport(format, content);
2227
+ onSuccess?.(`Exported as ${format.toUpperCase()}`);
1603
2228
  return;
1604
- }
1605
- if (onExport) {
1606
- onExport(format, content);
1607
- onSuccess?.(`Exported as ${format.toUpperCase()}`);
1608
- return;
1609
- }
1610
- const url = content instanceof Blob ? URL.createObjectURL(content) : URL.createObjectURL(new Blob([content], { type: "text/plain" }));
1611
- const a = document.createElement("a");
1612
- a.href = url;
1613
- a.download = `${filename}.${format === "plantuml" ? "puml" : format}`;
1614
- a.click();
1615
- URL.revokeObjectURL(url);
1616
- onSuccess?.(`Downloaded ${a.download}`);
1617
- }, [model, onExport, filename, onSuccess]);
2229
+ }
2230
+ const url = content instanceof Blob ? URL.createObjectURL(content) : URL.createObjectURL(new Blob([content], { type: "text/plain" }));
2231
+ const a = document.createElement("a");
2232
+ a.href = url;
2233
+ a.download = `${filename}.${format === "plantuml" ? "puml" : format}`;
2234
+ a.click();
2235
+ URL.revokeObjectURL(url);
2236
+ onSuccess?.(`Downloaded ${a.download}`);
2237
+ },
2238
+ [model, onExport, filename, onSuccess]
2239
+ );
1618
2240
  }
1619
2241
 
1620
2242
  // src/ui/hooks/useImporter.ts
@@ -1631,7 +2253,15 @@ var Model = class _Model {
1631
2253
  * @param variant Optional UI variant (flowchart models only).
1632
2254
  */
1633
2255
  constructor(type, title, variant) {
1634
- this.data = { type, ...variant ? { variant } : {}, title, nodes: [], edges: [], actors: [], messages: [] };
2256
+ this.data = {
2257
+ type,
2258
+ ...variant ? { variant } : {},
2259
+ title,
2260
+ nodes: [],
2261
+ edges: [],
2262
+ actors: [],
2263
+ messages: []
2264
+ };
1635
2265
  }
1636
2266
  /**
1637
2267
  * Rehydrate a `Model` from a previously serialized `DiagramModel`. The
@@ -1667,7 +2297,8 @@ var Model = class _Model {
1667
2297
  updateNode(id, patch) {
1668
2298
  const node = this.data.nodes.find((n) => n.id === id);
1669
2299
  if (!node) throw new Error(`Node "${id}" not found`);
1670
- Object.assign(node, patch);
2300
+ const { __proto__, constructor, ...safe } = patch;
2301
+ Object.assign(node, safe);
1671
2302
  return this;
1672
2303
  }
1673
2304
  /**
@@ -1707,15 +2338,35 @@ var Model = class _Model {
1707
2338
  const errors = [];
1708
2339
  const nodeIds = /* @__PURE__ */ new Set();
1709
2340
  for (const n of this.data.nodes) {
1710
- if (nodeIds.has(n.id)) errors.push({ kind: "duplicate-node-id", id: n.id, message: `Duplicate node id "${n.id}"` });
2341
+ if (nodeIds.has(n.id))
2342
+ errors.push({
2343
+ kind: "duplicate-node-id",
2344
+ id: n.id,
2345
+ message: `Duplicate node id "${n.id}"`
2346
+ });
1711
2347
  nodeIds.add(n.id);
1712
2348
  }
1713
2349
  const edgeIds = /* @__PURE__ */ new Set();
1714
2350
  for (const e of this.data.edges) {
1715
- if (edgeIds.has(e.id)) errors.push({ kind: "duplicate-edge-id", id: e.id, message: `Duplicate edge id "${e.id}"` });
2351
+ if (edgeIds.has(e.id))
2352
+ errors.push({
2353
+ kind: "duplicate-edge-id",
2354
+ id: e.id,
2355
+ message: `Duplicate edge id "${e.id}"`
2356
+ });
1716
2357
  edgeIds.add(e.id);
1717
- if (!nodeIds.has(e.from)) errors.push({ kind: "dangling-from", id: e.id, message: `Edge "${e.id}" references unknown source node "${e.from}"` });
1718
- if (!nodeIds.has(e.to)) errors.push({ kind: "dangling-to", id: e.id, message: `Edge "${e.id}" references unknown target node "${e.to}"` });
2358
+ if (!nodeIds.has(e.from))
2359
+ errors.push({
2360
+ kind: "dangling-from",
2361
+ id: e.id,
2362
+ message: `Edge "${e.id}" references unknown source node "${e.from}"`
2363
+ });
2364
+ if (!nodeIds.has(e.to))
2365
+ errors.push({
2366
+ kind: "dangling-to",
2367
+ id: e.id,
2368
+ message: `Edge "${e.id}" references unknown target node "${e.to}"`
2369
+ });
1719
2370
  }
1720
2371
  return errors;
1721
2372
  }
@@ -1749,6 +2400,25 @@ var Model = class _Model {
1749
2400
  }
1750
2401
  };
1751
2402
 
2403
+ // src/core/sanitize.ts
2404
+ var MAX_LABEL_LENGTH = 2e3;
2405
+ var MAX_NODES = 500;
2406
+ var MAX_EDGES = 2e3;
2407
+ var MAX_ACTORS = 100;
2408
+ var MAX_MESSAGES = 2e3;
2409
+ var MAX_IMPORT_LENGTH = 2 * 1024 * 1024;
2410
+ function sanitizeLabel(raw) {
2411
+ let s2 = raw;
2412
+ s2 = s2.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
2413
+ s2 = s2.replace(/<\/?[a-zA-Z][^>]*>/g, "");
2414
+ s2 = s2.replace(/\b(?:javascript|data|vbscript)\s*:/gi, "");
2415
+ s2 = s2.replace(/\bon[a-z]+\s*=/gi, "");
2416
+ if (s2.length > MAX_LABEL_LENGTH) {
2417
+ s2 = s2.slice(0, MAX_LABEL_LENGTH);
2418
+ }
2419
+ return s2;
2420
+ }
2421
+
1752
2422
  // src/importers/mermaid.ts
1753
2423
  function parseNodeDecl(raw) {
1754
2424
  const patterns = [
@@ -1760,7 +2430,7 @@ function parseNodeDecl(raw) {
1760
2430
  ];
1761
2431
  for (const [re, shape] of patterns) {
1762
2432
  const m = raw.match(re);
1763
- if (m) return { id: m[1], label: m[2].replace(/^["']|["']$/g, ""), shape };
2433
+ if (m) return { id: m[1], label: sanitizeLabel(m[2].replace(/^["']|["']$/g, "")), shape };
1764
2434
  }
1765
2435
  return null;
1766
2436
  }
@@ -1775,8 +2445,11 @@ function parseFlowchart(lines) {
1775
2445
  const model = new Model("flowchart");
1776
2446
  const nodeMap = /* @__PURE__ */ new Map();
1777
2447
  const groupStack = [];
2448
+ let edgeCount = 0;
1778
2449
  const ensureNode = (id, group) => {
1779
2450
  if (!nodeMap.has(id)) {
2451
+ if (nodeMap.size >= MAX_NODES)
2452
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
1780
2453
  nodeMap.set(id, true);
1781
2454
  const metadata = group ? { group } : void 0;
1782
2455
  model.addNode({ id, label: id, shape: "rectangle", ...metadata ? { metadata } : {} });
@@ -1785,7 +2458,8 @@ function parseFlowchart(lines) {
1785
2458
  for (const line of lines) {
1786
2459
  const trimmed = line.trim();
1787
2460
  if (!trimmed) continue;
1788
- 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;
2461
+ if (trimmed.startsWith("%%") || trimmed.startsWith("graph") || trimmed.startsWith("flowchart") || trimmed.startsWith("click ") || trimmed.startsWith("classDef ") || trimmed.startsWith("class ") || trimmed.startsWith("style ") || trimmed.startsWith("linkStyle "))
2462
+ continue;
1789
2463
  const subgraphOpen = trimmed.match(/^subgraph\s+(\S+)/i);
1790
2464
  if (subgraphOpen) {
1791
2465
  groupStack.push(subgraphOpen[1]);
@@ -1801,12 +2475,15 @@ function parseFlowchart(lines) {
1801
2475
  const fromRaw = edgeMatch[1].trim();
1802
2476
  const connector = edgeMatch[2];
1803
2477
  const label = edgeMatch[3]?.replace(/^["']|["']$/g, "");
2478
+ const sanitizedLabel = label ? sanitizeLabel(label) : void 0;
1804
2479
  const toRaw = edgeMatch[4].trim();
1805
2480
  const style = detectStyle(connector);
1806
2481
  const arrowhead = detectArrowhead(connector);
1807
2482
  const fromNode = parseNodeDecl(fromRaw);
1808
2483
  const toNode = parseNodeDecl(toRaw);
1809
2484
  if (fromNode && !nodeMap.has(fromNode.id)) {
2485
+ if (nodeMap.size >= MAX_NODES)
2486
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
1810
2487
  nodeMap.set(fromNode.id, true);
1811
2488
  const metadata = currentGroup ? { group: currentGroup } : void 0;
1812
2489
  model.addNode({ ...fromNode, ...metadata ? { metadata } : {} });
@@ -1814,19 +2491,24 @@ function parseFlowchart(lines) {
1814
2491
  ensureNode(fromRaw.replace(/\W.*/, ""), currentGroup);
1815
2492
  }
1816
2493
  if (toNode && !nodeMap.has(toNode.id)) {
2494
+ if (nodeMap.size >= MAX_NODES)
2495
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
1817
2496
  nodeMap.set(toNode.id, true);
1818
2497
  const metadata = currentGroup ? { group: currentGroup } : void 0;
1819
2498
  model.addNode({ ...toNode, ...metadata ? { metadata } : {} });
1820
2499
  } else if (!toNode) {
1821
2500
  ensureNode(toRaw.replace(/\W.*/, ""), currentGroup);
1822
2501
  }
2502
+ if (edgeCount >= MAX_EDGES)
2503
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_EDGES} edges`);
2504
+ edgeCount++;
1823
2505
  const fromId = fromNode?.id ?? fromRaw.replace(/\W.*/, "");
1824
2506
  const toId = toNode?.id ?? toRaw.replace(/\W.*/, "");
1825
2507
  model.addEdge({
1826
2508
  id: nextId("e", model.toJSON().edges),
1827
2509
  from: fromId,
1828
2510
  to: toId,
1829
- ...label ? { label } : {},
2511
+ ...sanitizedLabel ? { label: sanitizedLabel } : {},
1830
2512
  style,
1831
2513
  ...arrowhead === "none" ? { arrowhead } : {}
1832
2514
  });
@@ -1834,6 +2516,8 @@ function parseFlowchart(lines) {
1834
2516
  }
1835
2517
  const nodeDecl = parseNodeDecl(trimmed);
1836
2518
  if (nodeDecl && !nodeMap.has(nodeDecl.id)) {
2519
+ if (nodeMap.size >= MAX_NODES)
2520
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
1837
2521
  nodeMap.set(nodeDecl.id, true);
1838
2522
  const metadata = currentGroup ? { group: currentGroup } : void 0;
1839
2523
  model.addNode({ ...nodeDecl, ...metadata ? { metadata } : {} });
@@ -1843,34 +2527,56 @@ function parseFlowchart(lines) {
1843
2527
  }
1844
2528
  function parseSequence(lines, title) {
1845
2529
  const model = new Model("sequence", title);
2530
+ let actorCount = 0;
2531
+ let messageCount = 0;
2532
+ const safeAddActor = (name) => {
2533
+ const safeName = sanitizeLabel(name);
2534
+ if (!model.toJSON().actors?.includes(safeName)) {
2535
+ if (actorCount >= MAX_ACTORS)
2536
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_ACTORS} actors`);
2537
+ actorCount++;
2538
+ }
2539
+ model.addActor(safeName);
2540
+ return safeName;
2541
+ };
1846
2542
  for (const line of lines) {
1847
2543
  const trimmed = line.trim();
1848
2544
  if (!trimmed || trimmed.startsWith("sequenceDiagram") || trimmed.startsWith("%%")) continue;
1849
2545
  const participantMatch = trimmed.match(/^participant\s+(.+)$/i);
1850
2546
  if (participantMatch) {
1851
- model.addActor(participantMatch[1].trim());
2547
+ safeAddActor(participantMatch[1].trim());
1852
2548
  continue;
1853
2549
  }
1854
2550
  const actorMatch = trimmed.match(/^actor\s+(.+)$/i);
1855
2551
  if (actorMatch) {
1856
- model.addActor(actorMatch[1].trim());
2552
+ safeAddActor(actorMatch[1].trim());
1857
2553
  continue;
1858
2554
  }
1859
2555
  const msgMatch = trimmed.match(/^(.+?)\s*(-->>|->>|-->|->)\s*(.+?):\s*(.+)$/);
1860
2556
  if (msgMatch) {
1861
- const from = msgMatch[1].trim();
2557
+ const from = safeAddActor(msgMatch[1].trim());
1862
2558
  const arrow = msgMatch[2];
1863
- const to = msgMatch[3].trim();
1864
- const label = msgMatch[4].trim();
1865
- model.addActor(from);
1866
- model.addActor(to);
2559
+ const to = safeAddActor(msgMatch[3].trim());
2560
+ const label = sanitizeLabel(msgMatch[4].trim());
2561
+ if (messageCount >= MAX_MESSAGES)
2562
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_MESSAGES} messages`);
2563
+ messageCount++;
1867
2564
  const messages = model.toJSON().messages ?? [];
1868
- model.addMessage({ id: nextId("m", messages), from, to, label, style: arrow.startsWith("--") ? "dashed" : "solid" });
2565
+ model.addMessage({
2566
+ id: nextId("m", messages),
2567
+ from,
2568
+ to,
2569
+ label,
2570
+ style: arrow.startsWith("--") ? "dashed" : "solid"
2571
+ });
1869
2572
  }
1870
2573
  }
1871
2574
  return model;
1872
2575
  }
1873
2576
  function fromMermaid(mermaid) {
2577
+ if (mermaid.length > MAX_IMPORT_LENGTH) {
2578
+ throw new Error(`Import aborted: input exceeds the maximum of ${MAX_IMPORT_LENGTH} characters`);
2579
+ }
1874
2580
  const cleaned = mermaid.replace(/mermaid\.initialize\([\s\S]*?\)\s*;?/g, "");
1875
2581
  const rawLines = cleaned.split("\n");
1876
2582
  let startIdx = 0;
@@ -1902,10 +2608,74 @@ function fromMermaid(mermaid) {
1902
2608
  }
1903
2609
 
1904
2610
  // src/importers/json.ts
2611
+ function stripDangerousKeys(obj) {
2612
+ if (Array.isArray(obj)) return obj.map(stripDangerousKeys);
2613
+ if (obj !== null && typeof obj === "object") {
2614
+ const clean = {};
2615
+ for (const [key, val] of Object.entries(obj)) {
2616
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
2617
+ clean[key] = stripDangerousKeys(val);
2618
+ }
2619
+ return clean;
2620
+ }
2621
+ return obj;
2622
+ }
1905
2623
  function fromJSON(json) {
1906
- const data = typeof json === "string" ? JSON.parse(json) : json;
1907
- if (!data.type || !Array.isArray(data.nodes) || !Array.isArray(data.edges)) {
1908
- throw new Error("Invalid DiagramModel JSON");
2624
+ if (typeof json === "string" && json.length > MAX_IMPORT_LENGTH) {
2625
+ throw new Error(`Import aborted: input exceeds the maximum of ${MAX_IMPORT_LENGTH} characters`);
2626
+ }
2627
+ const raw = typeof json === "string" ? JSON.parse(json) : json;
2628
+ const data = stripDangerousKeys(raw);
2629
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
2630
+ throw new Error("Invalid DiagramModel JSON: expected an object");
2631
+ }
2632
+ if (data.type !== "flowchart" && data.type !== "sequence") {
2633
+ throw new Error(`Invalid DiagramModel JSON: unknown type "${data.type}"`);
2634
+ }
2635
+ if (!Array.isArray(data.nodes) || !Array.isArray(data.edges)) {
2636
+ throw new Error("Invalid DiagramModel JSON: nodes and edges must be arrays");
2637
+ }
2638
+ if (data.nodes.length > MAX_NODES) {
2639
+ throw new Error(
2640
+ `Import aborted: diagram has ${data.nodes.length} nodes, maximum is ${MAX_NODES}`
2641
+ );
2642
+ }
2643
+ if (data.edges.length > MAX_EDGES) {
2644
+ throw new Error(
2645
+ `Import aborted: diagram has ${data.edges.length} edges, maximum is ${MAX_EDGES}`
2646
+ );
2647
+ }
2648
+ if (data.actors && data.actors.length > MAX_ACTORS) {
2649
+ throw new Error(
2650
+ `Import aborted: diagram has ${data.actors.length} actors, maximum is ${MAX_ACTORS}`
2651
+ );
2652
+ }
2653
+ if (data.messages && data.messages.length > MAX_MESSAGES) {
2654
+ throw new Error(
2655
+ `Import aborted: diagram has ${data.messages.length} messages, maximum is ${MAX_MESSAGES}`
2656
+ );
2657
+ }
2658
+ for (const node of data.nodes) {
2659
+ if (typeof node !== "object" || node === null || typeof node.id !== "string" || typeof node.label !== "string") {
2660
+ throw new Error("Invalid DiagramModel JSON: each node must have string id and label");
2661
+ }
2662
+ node.label = sanitizeLabel(node.label);
2663
+ }
2664
+ for (const edge of data.edges) {
2665
+ if (typeof edge !== "object" || edge === null || typeof edge.id !== "string" || typeof edge.from !== "string" || typeof edge.to !== "string") {
2666
+ throw new Error("Invalid DiagramModel JSON: each edge must have string id, from, and to");
2667
+ }
2668
+ if (edge.label) edge.label = sanitizeLabel(edge.label);
2669
+ }
2670
+ if (data.actors) {
2671
+ data.actors = data.actors.map((a) => typeof a === "string" ? sanitizeLabel(a) : a);
2672
+ }
2673
+ if (data.messages) {
2674
+ for (const msg of data.messages) {
2675
+ if (typeof msg === "object" && msg !== null && typeof msg.label === "string") {
2676
+ msg.label = sanitizeLabel(msg.label);
2677
+ }
2678
+ }
1909
2679
  }
1910
2680
  return Model.fromData(data);
1911
2681
  }
@@ -1914,19 +2684,22 @@ function fromJSON(json) {
1914
2684
  function useImporter(applyAndPush, options = {}) {
1915
2685
  const { expectedType, transform, onSuccess, onError } = options;
1916
2686
  const reportError = onError ?? ((msg) => alert(msg));
1917
- return (0, import_react8.useCallback)((text) => {
1918
- try {
1919
- const parsed = text.trim().startsWith("{") ? fromJSON(text).toJSON() : fromMermaid(text).toJSON();
1920
- if (expectedType && parsed.type !== expectedType) {
1921
- reportError(`Imported diagram is not a ${expectedType} diagram.`);
1922
- return;
2687
+ return (0, import_react8.useCallback)(
2688
+ (text) => {
2689
+ try {
2690
+ const parsed = text.trim().startsWith("{") ? fromJSON(text).toJSON() : fromMermaid(text).toJSON();
2691
+ if (expectedType && parsed.type !== expectedType) {
2692
+ reportError(`Imported diagram is not a ${expectedType} diagram.`);
2693
+ return;
2694
+ }
2695
+ applyAndPush(transform ? transform(parsed) : parsed);
2696
+ onSuccess?.("Diagram imported successfully");
2697
+ } catch (err) {
2698
+ reportError(`Import failed: ${err.message}`);
1923
2699
  }
1924
- applyAndPush(transform ? transform(parsed) : parsed);
1925
- onSuccess?.("Diagram imported successfully");
1926
- } catch (err) {
1927
- reportError(`Import failed: ${err.message}`);
1928
- }
1929
- }, [applyAndPush, expectedType, transform, onSuccess, onError]);
2700
+ },
2701
+ [applyAndPush, expectedType, transform, onSuccess, onError]
2702
+ );
1930
2703
  }
1931
2704
 
1932
2705
  // src/ui/hooks/useToast.ts
@@ -1964,7 +2737,10 @@ var containerStyle = {
1964
2737
  zIndex: 9999,
1965
2738
  pointerEvents: "none"
1966
2739
  };
1967
- function ToastContainer({ toasts, onDismiss }) {
2740
+ function ToastContainer({
2741
+ toasts,
2742
+ onDismiss
2743
+ }) {
1968
2744
  if (toasts.length === 0) return null;
1969
2745
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: containerStyle, children: toasts.map((t) => {
1970
2746
  const c = TOAST_COLORS[t.type];
@@ -2195,7 +2971,10 @@ function SequenceEditor({
2195
2971
  const historyRef = (0, import_react11.useRef)([ensureSequenceModel(initialModel)]);
2196
2972
  const historyIdxRef = (0, import_react11.useRef)(0);
2197
2973
  const svgRef = (0, import_react11.useRef)(null);
2198
- const { t, isDark } = useEditorTheme(theme, themeOverrides, { light: lightTheme2, dark: darkTheme2 });
2974
+ const { t, isDark } = useEditorTheme(theme, themeOverrides, {
2975
+ light: lightTheme2,
2976
+ dark: darkTheme2
2977
+ });
2199
2978
  const actors = model.actors ?? [];
2200
2979
  const messages = model.messages ?? [];
2201
2980
  const colW = (0, import_react11.useMemo)(() => {
@@ -2217,11 +2996,14 @@ function SequenceEditor({
2217
2996
  historyRef.current = stack;
2218
2997
  historyIdxRef.current = stack.length - 1;
2219
2998
  }, []);
2220
- const applyAndPush = (0, import_react11.useCallback)((m) => {
2221
- setModel(m);
2222
- onChange?.(m);
2223
- pushHistory(m);
2224
- }, [onChange, pushHistory]);
2999
+ const applyAndPush = (0, import_react11.useCallback)(
3000
+ (m) => {
3001
+ setModel(m);
3002
+ onChange?.(m);
3003
+ pushHistory(m);
3004
+ },
3005
+ [onChange, pushHistory]
3006
+ );
2225
3007
  const undo = (0, import_react11.useCallback)(() => {
2226
3008
  if (historyIdxRef.current <= 0) return;
2227
3009
  historyIdxRef.current--;
@@ -2266,7 +3048,10 @@ function SequenceEditor({
2266
3048
  applyAndPush({
2267
3049
  ...model,
2268
3050
  actors: [...actors, a, b],
2269
- messages: [...messages, { id: nextId("m", messages), from: a, to: b, label: "message", style: "solid" }]
3051
+ messages: [
3052
+ ...messages,
3053
+ { id: nextId("m", messages), from: a, to: b, label: "message", style: "solid" }
3054
+ ]
2270
3055
  });
2271
3056
  return;
2272
3057
  }
@@ -2274,7 +3059,10 @@ function SequenceEditor({
2274
3059
  const to = actors[Math.min(1, actors.length - 1)] ?? from;
2275
3060
  applyAndPush({
2276
3061
  ...model,
2277
- messages: [...messages, { id: nextId("m", messages), from, to, label: "message", style: "solid" }]
3062
+ messages: [
3063
+ ...messages,
3064
+ { id: nextId("m", messages), from, to, label: "message", style: "solid" }
3065
+ ]
2278
3066
  });
2279
3067
  };
2280
3068
  const updateMessage = (id, patch) => {
@@ -2287,37 +3075,57 @@ function SequenceEditor({
2287
3075
  applyAndPush({ ...model, messages: messages.filter((m) => m.id !== id) });
2288
3076
  if (selected === id) setSelected(null);
2289
3077
  };
2290
- const reorderMessage = (0, import_react11.useCallback)((id, toIdx) => {
2291
- const fromIdx = messages.findIndex((m) => m.id === id);
2292
- if (fromIdx < 0 || toIdx === fromIdx) return;
2293
- const next = messages.slice();
2294
- const [moved] = next.splice(fromIdx, 1);
2295
- next.splice(toIdx, 0, moved);
2296
- applyAndPush({ ...model, messages: next });
2297
- }, [messages, model, applyAndPush]);
3078
+ const reorderMessage = (0, import_react11.useCallback)(
3079
+ (id, toIdx) => {
3080
+ const fromIdx = messages.findIndex((m) => m.id === id);
3081
+ if (fromIdx < 0 || toIdx === fromIdx) return;
3082
+ const next = messages.slice();
3083
+ const [moved] = next.splice(fromIdx, 1);
3084
+ next.splice(toIdx, 0, moved);
3085
+ applyAndPush({ ...model, messages: next });
3086
+ },
3087
+ [messages, model, applyAndPush]
3088
+ );
2298
3089
  const keyCommands = [
2299
- { match: (e) => (e.ctrlKey || e.metaKey) && e.key === "z", run: () => {
2300
- undo();
2301
- return true;
2302
- } },
2303
- { match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "y" || e.shiftKey && e.key === "z"), run: () => {
2304
- redo();
2305
- return true;
2306
- } },
2307
- { match: (e) => e.key === "Escape", run: () => {
2308
- setSelected(null);
2309
- setEditingId(null);
2310
- return true;
2311
- } },
2312
- { match: (e) => (e.key === "Delete" || e.key === "Backspace") && !!selected, run: () => {
2313
- removeMessage(selected);
2314
- return true;
2315
- } }
2316
- ];
2317
- useEditorKeyboard(keyCommands, [undo, redo, selected]);
2318
- const handleExport = useExporters(model, onExport, "sequence", (msg) => showToast(msg, "success"));
2319
- const handleImport = useImporter(applyAndPush, {
2320
- expectedType: "sequence",
3090
+ {
3091
+ match: (e) => (e.ctrlKey || e.metaKey) && e.key === "z",
3092
+ run: () => {
3093
+ undo();
3094
+ return true;
3095
+ }
3096
+ },
3097
+ {
3098
+ match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "y" || e.shiftKey && e.key === "z"),
3099
+ run: () => {
3100
+ redo();
3101
+ return true;
3102
+ }
3103
+ },
3104
+ {
3105
+ match: (e) => e.key === "Escape",
3106
+ run: () => {
3107
+ setSelected(null);
3108
+ setEditingId(null);
3109
+ return true;
3110
+ }
3111
+ },
3112
+ {
3113
+ match: (e) => (e.key === "Delete" || e.key === "Backspace") && !!selected,
3114
+ run: () => {
3115
+ removeMessage(selected);
3116
+ return true;
3117
+ }
3118
+ }
3119
+ ];
3120
+ useEditorKeyboard(keyCommands, [undo, redo, selected]);
3121
+ const handleExport = useExporters(
3122
+ model,
3123
+ onExport,
3124
+ "sequence",
3125
+ (msg) => showToast(msg, "success")
3126
+ );
3127
+ const handleImport = useImporter(applyAndPush, {
3128
+ expectedType: "sequence",
2321
3129
  transform: ensureSequenceModel,
2322
3130
  onSuccess: (msg) => showToast(msg, "success"),
2323
3131
  onError: (msg) => showToast(msg, "error")
@@ -2360,17 +3168,22 @@ function SequenceEditor({
2360
3168
  };
2361
3169
  }, [drag, messages.length, reorderMessage]);
2362
3170
  const selectedMsg = selected ? messages.find((m) => m.id === selected) : null;
2363
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "fsd-seq-editor", style: {
2364
- display: "flex",
2365
- flexDirection: "column",
2366
- height,
2367
- width: "100%",
2368
- fontFamily: "ui-sans-serif,system-ui,sans-serif",
2369
- background: t.ctrlsBg,
2370
- position: "relative"
2371
- }, children: [
2372
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ToastContainer, { toasts, onDismiss: dismissToast }),
2373
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("style", { children: `
3171
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3172
+ "div",
3173
+ {
3174
+ className: "fsd-seq-editor",
3175
+ style: {
3176
+ display: "flex",
3177
+ flexDirection: "column",
3178
+ height,
3179
+ width: "100%",
3180
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
3181
+ background: t.ctrlsBg,
3182
+ position: "relative"
3183
+ },
3184
+ children: [
3185
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ToastContainer, { toasts, onDismiss: dismissToast }),
3186
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("style", { children: `
2374
3187
  .fsd-seq-editor [role="button"]:focus-visible {
2375
3188
  outline: 2px solid ${t.actorText};
2376
3189
  outline-offset: 2px;
@@ -2382,139 +3195,202 @@ function SequenceEditor({
2382
3195
  border-radius: 4px;
2383
3196
  }
2384
3197
  ` }),
2385
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
2386
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: {
2387
- display: "flex",
2388
- gap: 8,
2389
- padding: "7px 14px",
2390
- background: t.ctrlsBg,
2391
- borderBottom: `1px solid ${t.ctrlsBorder}`,
2392
- alignItems: "center",
2393
- flexWrap: "wrap"
2394
- }, children: [
2395
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { onClick: addActor, style: primaryBtn(), children: "+ Actor" }),
2396
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { onClick: addMessage, style: primaryBtn(), children: "+ Message" }),
2397
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { width: 1, height: 18, background: t.ctrlsBorder, margin: "0 4px" } }),
2398
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { onClick: undo, style: ghostBtn2(t), title: "Undo (Ctrl+Z)", children: "\u21B6" }),
2399
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { onClick: redo, style: ghostBtn2(t), title: "Redo (Ctrl+Y)", children: "\u21B7" }),
2400
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
2401
- actors.length,
2402
- " actor",
2403
- actors.length === 1 ? "" : "s",
2404
- " \xB7 ",
2405
- messages.length,
2406
- " message",
2407
- messages.length === 1 ? "" : "s",
2408
- " \xB7 drag a row to reorder"
2409
- ] })
2410
- ] }),
2411
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [
2412
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { flex: 1, overflow: "auto", background: t.canvas, position: "relative" }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2413
- SequenceCanvas,
2414
- {
2415
- model,
2416
- actors,
2417
- messages,
2418
- t,
2419
- isDark,
2420
- colW,
2421
- totalW,
2422
- totalH,
2423
- actorX,
2424
- msgY,
2425
- selected,
2426
- editingId,
2427
- setEditingId,
2428
- drag,
2429
- onRowMouseDown,
2430
- renameActor,
2431
- removeActor,
2432
- svgRef
2433
- }
2434
- ) }),
2435
- selectedMsg && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: {
2436
- width: 280,
2437
- maxWidth: "40vw",
2438
- flexShrink: 0,
2439
- background: t.panelBg,
2440
- borderLeft: `1px solid ${t.panelBorder}`,
2441
- padding: "14px 16px",
2442
- overflowY: "auto"
2443
- }, children: [
2444
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.7, marginBottom: 10 }, children: "Message" }),
2445
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Label, { t, children: "Label" }),
2446
3198
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2447
- "input",
3199
+ Toolbar,
2448
3200
  {
2449
- value: editLabel || selectedMsg.label,
2450
- onChange: (e) => setEditLabel(e.target.value),
2451
- onFocus: () => setEditLabel(selectedMsg.label),
2452
- onBlur: () => {
2453
- if (editLabel && editLabel !== selectedMsg.label) updateMessage(selectedMsg.id, { label: editLabel });
2454
- setEditLabel("");
2455
- },
2456
- onKeyDown: (e) => {
2457
- if (e.key === "Enter") e.target.blur();
2458
- },
2459
- style: input(t)
3201
+ onExport: handleExport,
3202
+ onImport: allowImport ? handleImport : void 0,
3203
+ allowedExports,
3204
+ allowImport
2460
3205
  }
2461
3206
  ),
2462
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Label, { t, children: "From" }),
2463
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("select", { value: selectedMsg.from, onChange: (e) => updateMessage(selectedMsg.id, { from: e.target.value }), style: input(t), children: actors.map((a) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: a, children: a }, a)) }),
2464
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Label, { t, children: "To" }),
2465
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("select", { value: selectedMsg.to, onChange: (e) => updateMessage(selectedMsg.id, { to: e.target.value }), style: input(t), children: actors.map((a) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: a, children: a }, a)) }),
2466
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Label, { t, children: "Style" }),
2467
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { display: "flex", gap: 6 }, children: ["solid", "dashed"].map((s2) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2468
- "button",
3207
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3208
+ "div",
2469
3209
  {
2470
- onClick: () => updateMessage(selectedMsg.id, { style: s2 }),
2471
3210
  style: {
2472
- flex: 1,
2473
- padding: "6px 10px",
2474
- border: `1.5px solid ${selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO2 : t.inputBorder}`,
2475
- background: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO_SOFT2 : t.inputBg,
2476
- color: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO2 : t.textPrimary,
2477
- borderRadius: 8,
2478
- fontSize: 12,
2479
- fontWeight: 600,
2480
- cursor: "pointer",
2481
- fontFamily: "inherit"
3211
+ display: "flex",
3212
+ gap: 8,
3213
+ padding: "7px 14px",
3214
+ background: t.ctrlsBg,
3215
+ borderBottom: `1px solid ${t.ctrlsBorder}`,
3216
+ alignItems: "center",
3217
+ flexWrap: "wrap"
2482
3218
  },
2483
- children: s2 === "solid" ? "\u2500\u2500 solid" : "\u2500 \u2500 dashed"
2484
- },
2485
- s2
2486
- )) }),
2487
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { height: 14 } }),
2488
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2489
- "button",
3219
+ children: [
3220
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { onClick: addActor, style: primaryBtn(), children: "+ Actor" }),
3221
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { onClick: addMessage, style: primaryBtn(), children: "+ Message" }),
3222
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { width: 1, height: 18, background: t.ctrlsBorder, margin: "0 4px" } }),
3223
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { onClick: undo, style: ghostBtn2(t), title: "Undo (Ctrl+Z)", children: "\u21B6" }),
3224
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { onClick: redo, style: ghostBtn2(t), title: "Redo (Ctrl+Y)", children: "\u21B7" }),
3225
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
3226
+ actors.length,
3227
+ " actor",
3228
+ actors.length === 1 ? "" : "s",
3229
+ " \xB7 ",
3230
+ messages.length,
3231
+ " message",
3232
+ messages.length === 1 ? "" : "s",
3233
+ " \xB7 drag a row to reorder"
3234
+ ] })
3235
+ ]
3236
+ }
3237
+ ),
3238
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [
3239
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { flex: 1, overflow: "auto", background: t.canvas, position: "relative" }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3240
+ SequenceCanvas,
3241
+ {
3242
+ model,
3243
+ actors,
3244
+ messages,
3245
+ t,
3246
+ isDark,
3247
+ colW,
3248
+ totalW,
3249
+ totalH,
3250
+ actorX,
3251
+ msgY,
3252
+ selected,
3253
+ editingId,
3254
+ setEditingId,
3255
+ drag,
3256
+ onRowMouseDown,
3257
+ renameActor,
3258
+ removeActor,
3259
+ svgRef
3260
+ }
3261
+ ) }),
3262
+ selectedMsg && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3263
+ "div",
3264
+ {
3265
+ style: {
3266
+ width: 280,
3267
+ maxWidth: "40vw",
3268
+ flexShrink: 0,
3269
+ background: t.panelBg,
3270
+ borderLeft: `1px solid ${t.panelBorder}`,
3271
+ padding: "14px 16px",
3272
+ overflowY: "auto"
3273
+ },
3274
+ children: [
3275
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3276
+ "div",
3277
+ {
3278
+ style: {
3279
+ fontSize: 10,
3280
+ fontWeight: 700,
3281
+ color: t.textMuted,
3282
+ textTransform: "uppercase",
3283
+ letterSpacing: 0.7,
3284
+ marginBottom: 10
3285
+ },
3286
+ children: "Message"
3287
+ }
3288
+ ),
3289
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Label, { t, children: "Label" }),
3290
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3291
+ "input",
3292
+ {
3293
+ value: editLabel || selectedMsg.label,
3294
+ onChange: (e) => setEditLabel(e.target.value),
3295
+ onFocus: () => setEditLabel(selectedMsg.label),
3296
+ onBlur: () => {
3297
+ if (editLabel && editLabel !== selectedMsg.label)
3298
+ updateMessage(selectedMsg.id, { label: editLabel });
3299
+ setEditLabel("");
3300
+ },
3301
+ onKeyDown: (e) => {
3302
+ if (e.key === "Enter") e.target.blur();
3303
+ },
3304
+ style: input(t)
3305
+ }
3306
+ ),
3307
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Label, { t, children: "From" }),
3308
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3309
+ "select",
3310
+ {
3311
+ value: selectedMsg.from,
3312
+ onChange: (e) => updateMessage(selectedMsg.id, { from: e.target.value }),
3313
+ style: input(t),
3314
+ children: actors.map((a) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: a, children: a }, a))
3315
+ }
3316
+ ),
3317
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Label, { t, children: "To" }),
3318
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3319
+ "select",
3320
+ {
3321
+ value: selectedMsg.to,
3322
+ onChange: (e) => updateMessage(selectedMsg.id, { to: e.target.value }),
3323
+ style: input(t),
3324
+ children: actors.map((a) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: a, children: a }, a))
3325
+ }
3326
+ ),
3327
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Label, { t, children: "Style" }),
3328
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { display: "flex", gap: 6 }, children: ["solid", "dashed"].map((s2) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3329
+ "button",
3330
+ {
3331
+ onClick: () => updateMessage(selectedMsg.id, { style: s2 }),
3332
+ style: {
3333
+ flex: 1,
3334
+ padding: "6px 10px",
3335
+ border: `1.5px solid ${selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO2 : t.inputBorder}`,
3336
+ background: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO_SOFT2 : t.inputBg,
3337
+ color: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO2 : t.textPrimary,
3338
+ borderRadius: 8,
3339
+ fontSize: 12,
3340
+ fontWeight: 600,
3341
+ cursor: "pointer",
3342
+ fontFamily: "inherit"
3343
+ },
3344
+ children: s2 === "solid" ? "\u2500\u2500 solid" : "\u2500 \u2500 dashed"
3345
+ },
3346
+ s2
3347
+ )) }),
3348
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { height: 14 } }),
3349
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3350
+ "button",
3351
+ {
3352
+ onClick: () => removeMessage(selectedMsg.id),
3353
+ style: {
3354
+ ...ghostBtn2(t),
3355
+ width: "100%",
3356
+ color: "#ef4444",
3357
+ border: `1px solid ${isDark ? "#7f1d1d" : "#fca5a5"}`
3358
+ },
3359
+ children: "Delete message"
3360
+ }
3361
+ )
3362
+ ]
3363
+ }
3364
+ )
3365
+ ] }),
3366
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3367
+ "div",
2490
3368
  {
2491
- onClick: () => removeMessage(selectedMsg.id),
2492
- style: { ...ghostBtn2(t), width: "100%", color: "#ef4444", border: `1px solid ${isDark ? "#7f1d1d" : "#fca5a5"}` },
2493
- children: "Delete message"
3369
+ style: {
3370
+ padding: "4px 14px",
3371
+ fontSize: 11,
3372
+ color: t.textMuted,
3373
+ background: t.canvas,
3374
+ borderTop: `1px solid ${t.ctrlsBorder}`,
3375
+ display: "flex",
3376
+ gap: 16
3377
+ },
3378
+ children: [
3379
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
3380
+ actors.length,
3381
+ " actors"
3382
+ ] }),
3383
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
3384
+ messages.length,
3385
+ " messages"
3386
+ ] }),
3387
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { marginLeft: "auto" }, children: "double-click actor to rename \xB7 drag a row to reorder" })
3388
+ ]
2494
3389
  }
2495
3390
  )
2496
- ] })
2497
- ] }),
2498
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: {
2499
- padding: "4px 14px",
2500
- fontSize: 11,
2501
- color: t.textMuted,
2502
- background: t.canvas,
2503
- borderTop: `1px solid ${t.ctrlsBorder}`,
2504
- display: "flex",
2505
- gap: 16
2506
- }, children: [
2507
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
2508
- actors.length,
2509
- " actors"
2510
- ] }),
2511
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
2512
- messages.length,
2513
- " messages"
2514
- ] }),
2515
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { marginLeft: "auto" }, children: "double-click actor to rename \xB7 drag a row to reorder" })
2516
- ] })
2517
- ] });
3391
+ ]
3392
+ }
3393
+ );
2518
3394
  }
2519
3395
  function primaryBtn() {
2520
3396
  return {
@@ -2558,7 +3434,20 @@ function input(t) {
2558
3434
  };
2559
3435
  }
2560
3436
  function Label({ t, children }) {
2561
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 4 }, children });
3437
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3438
+ "div",
3439
+ {
3440
+ style: {
3441
+ fontSize: 10,
3442
+ fontWeight: 700,
3443
+ color: t.textMuted,
3444
+ textTransform: "uppercase",
3445
+ letterSpacing: 0.6,
3446
+ marginBottom: 4
3447
+ },
3448
+ children
3449
+ }
3450
+ );
2562
3451
  }
2563
3452
 
2564
3453
  // src/ui/NodeNavigator.tsx
@@ -2590,163 +3479,286 @@ function NodeNavigator({
2590
3479
  return "\u25AD";
2591
3480
  }
2592
3481
  };
2593
- const filtered = model.nodes.filter(
2594
- (n) => n.label.toLowerCase().includes(search.toLowerCase())
2595
- );
3482
+ const filtered = model.nodes.filter((n) => n.label.toLowerCase().includes(search.toLowerCase()));
2596
3483
  const inEdges = (id) => model.edges.filter((e) => e.to === id).length;
2597
3484
  const outEdges = (id) => model.edges.filter((e) => e.from === id).length;
2598
3485
  if (!open) {
2599
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: {
2600
- width: 36,
2601
- flexShrink: 0,
2602
- background: t.panelBg,
2603
- borderRight: `1px solid ${t.panelBorder}`,
2604
- display: "flex",
2605
- flexDirection: "column",
2606
- alignItems: "center",
2607
- paddingTop: 8,
2608
- gap: 6
2609
- }, children: [
2610
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2611
- "button",
2612
- {
2613
- onClick: onToggle,
2614
- title: "Open node list",
2615
- "aria-expanded": false,
2616
- "aria-label": "Open node list",
2617
- style: { background: "none", border: "none", cursor: "pointer", color: t.textMuted, padding: 6, borderRadius: 6, fontSize: 14, lineHeight: 1 },
2618
- children: "\u2630"
2619
- }
2620
- ),
2621
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontSize: 10, color: t.textMuted, fontWeight: 700, writingMode: "vertical-rl", transform: "rotate(180deg)", letterSpacing: 0.5 }, children: model.nodes.length })
2622
- ] });
3486
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3487
+ "div",
3488
+ {
3489
+ style: {
3490
+ width: 36,
3491
+ flexShrink: 0,
3492
+ background: t.panelBg,
3493
+ borderRight: `1px solid ${t.panelBorder}`,
3494
+ display: "flex",
3495
+ flexDirection: "column",
3496
+ alignItems: "center",
3497
+ paddingTop: 8,
3498
+ gap: 6
3499
+ },
3500
+ children: [
3501
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3502
+ "button",
3503
+ {
3504
+ onClick: onToggle,
3505
+ title: "Open node list",
3506
+ "aria-expanded": false,
3507
+ "aria-label": "Open node list",
3508
+ style: {
3509
+ background: "none",
3510
+ border: "none",
3511
+ cursor: "pointer",
3512
+ color: t.textMuted,
3513
+ padding: 6,
3514
+ borderRadius: 6,
3515
+ fontSize: 14,
3516
+ lineHeight: 1
3517
+ },
3518
+ children: "\u2630"
3519
+ }
3520
+ ),
3521
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3522
+ "div",
3523
+ {
3524
+ style: {
3525
+ fontSize: 10,
3526
+ color: t.textMuted,
3527
+ fontWeight: 700,
3528
+ writingMode: "vertical-rl",
3529
+ transform: "rotate(180deg)",
3530
+ letterSpacing: 0.5
3531
+ },
3532
+ children: model.nodes.length
3533
+ }
3534
+ )
3535
+ ]
3536
+ }
3537
+ );
2623
3538
  }
2624
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: {
2625
- width: 216,
2626
- flexShrink: 0,
2627
- background: t.panelBg,
2628
- borderRight: `1px solid ${t.panelBorder}`,
2629
- display: "flex",
2630
- flexDirection: "column",
2631
- overflow: "hidden"
2632
- }, children: [
2633
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: {
2634
- display: "flex",
2635
- alignItems: "center",
2636
- justifyContent: "space-between",
2637
- padding: "10px 12px",
2638
- borderBottom: `1px solid ${t.panelBorder}`,
2639
- flexShrink: 0
2640
- }, children: [
2641
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
2642
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontSize: 11, fontWeight: 700, color: t.textSecondary, textTransform: "uppercase", letterSpacing: 0.7 }, children: variant === "question" ? "Questions" : variant === "journey" ? "Steps" : "Nodes" }),
2643
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: {
2644
- fontSize: 10,
2645
- fontWeight: 700,
2646
- color: t.textMuted,
2647
- background: isDark ? "#0f172a" : "#f1f5f9",
2648
- padding: "1px 6px",
2649
- borderRadius: 99
2650
- }, children: model.nodes.length })
2651
- ] }),
2652
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2653
- "button",
2654
- {
2655
- onClick: onToggle,
2656
- style: { background: "none", border: "none", cursor: "pointer", color: t.textMuted, padding: "2px 4px", borderRadius: 4, fontSize: 13, lineHeight: 1 },
2657
- title: "Collapse",
2658
- "aria-expanded": true,
2659
- "aria-label": "Collapse node list",
2660
- children: "\u2039"
2661
- }
2662
- )
2663
- ] }),
2664
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { padding: "8px 10px", borderBottom: `1px solid ${t.sectionBorder}`, flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { position: "relative" }, children: [
2665
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { position: "absolute", left: 8, top: "50%", transform: "translateY(-50%)", fontSize: 11, color: t.textMuted, pointerEvents: "none" }, children: "\u2315" }),
2666
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2667
- "input",
2668
- {
2669
- value: search,
2670
- onChange: (e) => setSearch(e.target.value),
2671
- placeholder: "Search\u2026",
2672
- style: {
2673
- width: "100%",
2674
- padding: "5px 8px 5px 24px",
2675
- border: `1.5px solid ${t.inputBorder}`,
2676
- borderRadius: 7,
2677
- fontSize: 12,
2678
- background: t.inputBg,
2679
- color: t.inputText,
2680
- outline: "none",
2681
- boxSizing: "border-box",
2682
- fontFamily: "inherit"
2683
- }
2684
- }
2685
- )
2686
- ] }) }),
2687
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { flex: 1, overflowY: "auto", padding: "6px 8px", display: "flex", flexDirection: "column", gap: 2 }, children: [
2688
- filtered.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { textAlign: "center", padding: "20px 0", fontSize: 12, color: t.textMuted, fontStyle: "italic" }, children: model.nodes.length === 0 ? "No nodes yet" : "No matches" }),
2689
- filtered.map((node, idx) => {
2690
- const isSelected = selected === node.id;
2691
- const answers = node.metadata?.answers ?? [];
2692
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2693
- "button",
3539
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3540
+ "div",
3541
+ {
3542
+ style: {
3543
+ width: 216,
3544
+ flexShrink: 0,
3545
+ background: t.panelBg,
3546
+ borderRight: `1px solid ${t.panelBorder}`,
3547
+ display: "flex",
3548
+ flexDirection: "column",
3549
+ overflow: "hidden"
3550
+ },
3551
+ children: [
3552
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3553
+ "div",
2694
3554
  {
2695
- onClick: () => onSelect(node.id),
2696
3555
  style: {
2697
3556
  display: "flex",
2698
3557
  alignItems: "center",
2699
- gap: 8,
2700
- width: "100%",
2701
- padding: "7px 8px",
2702
- textAlign: "left",
2703
- background: isSelected ? acc.fill : "transparent",
2704
- border: isSelected ? `1.5px solid ${acc.border}` : "1.5px solid transparent",
2705
- borderRadius: 8,
2706
- cursor: "pointer",
2707
- fontFamily: "inherit",
2708
- transition: "background 0.1s"
2709
- },
2710
- onMouseEnter: (e) => {
2711
- if (!isSelected) e.currentTarget.style.background = isDark ? "#334155" : "#f1f5f9";
2712
- },
2713
- onMouseLeave: (e) => {
2714
- if (!isSelected) e.currentTarget.style.background = "transparent";
3558
+ justifyContent: "space-between",
3559
+ padding: "10px 12px",
3560
+ borderBottom: `1px solid ${t.panelBorder}`,
3561
+ flexShrink: 0
2715
3562
  },
2716
3563
  children: [
2717
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: {
2718
- width: 22,
2719
- height: 22,
2720
- borderRadius: 6,
2721
- flexShrink: 0,
2722
- background: isSelected ? acc.color : isDark ? "#334155" : "#e2e8f0",
2723
- color: isSelected ? "#fff" : t.textMuted,
2724
- display: "flex",
2725
- alignItems: "center",
2726
- justifyContent: "center",
2727
- fontSize: variant === "journey" ? 9 : 11,
2728
- fontWeight: 700
2729
- }, children: variant === "journey" ? idx + 1 : shapeIcon(node) }),
2730
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [
2731
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: {
2732
- fontSize: 12,
2733
- fontWeight: isSelected ? 600 : 400,
2734
- color: isSelected ? acc.color : t.textPrimary,
2735
- overflow: "hidden",
2736
- textOverflow: "ellipsis",
2737
- whiteSpace: "nowrap",
2738
- lineHeight: 1.3
2739
- }, children: node.label }),
2740
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontSize: 10, color: t.textMuted, lineHeight: 1.2, marginTop: 1 }, children: variant === "question" ? `${answers.length} answer${answers.length !== 1 ? "s" : ""}` : `${inEdges(node.id)}\u2193 ${outEdges(node.id)}\u2192` })
3564
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
3565
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3566
+ "span",
3567
+ {
3568
+ style: {
3569
+ fontSize: 11,
3570
+ fontWeight: 700,
3571
+ color: t.textSecondary,
3572
+ textTransform: "uppercase",
3573
+ letterSpacing: 0.7
3574
+ },
3575
+ children: variant === "question" ? "Questions" : variant === "journey" ? "Steps" : "Nodes"
3576
+ }
3577
+ ),
3578
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3579
+ "span",
3580
+ {
3581
+ style: {
3582
+ fontSize: 10,
3583
+ fontWeight: 700,
3584
+ color: t.textMuted,
3585
+ background: isDark ? "#0f172a" : "#f1f5f9",
3586
+ padding: "1px 6px",
3587
+ borderRadius: 99
3588
+ },
3589
+ children: model.nodes.length
3590
+ }
3591
+ )
2741
3592
  ] }),
2742
- isSelected && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontSize: 10, color: acc.color, flexShrink: 0 }, children: "\u25C9" })
3593
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3594
+ "button",
3595
+ {
3596
+ onClick: onToggle,
3597
+ style: {
3598
+ background: "none",
3599
+ border: "none",
3600
+ cursor: "pointer",
3601
+ color: t.textMuted,
3602
+ padding: "2px 4px",
3603
+ borderRadius: 4,
3604
+ fontSize: 13,
3605
+ lineHeight: 1
3606
+ },
3607
+ title: "Collapse",
3608
+ "aria-expanded": true,
3609
+ "aria-label": "Collapse node list",
3610
+ children: "\u2039"
3611
+ }
3612
+ )
2743
3613
  ]
2744
- },
2745
- node.id
2746
- );
2747
- })
2748
- ] })
2749
- ] });
3614
+ }
3615
+ ),
3616
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3617
+ "div",
3618
+ {
3619
+ style: { padding: "8px 10px", borderBottom: `1px solid ${t.sectionBorder}`, flexShrink: 0 },
3620
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { position: "relative" }, children: [
3621
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3622
+ "span",
3623
+ {
3624
+ style: {
3625
+ position: "absolute",
3626
+ left: 8,
3627
+ top: "50%",
3628
+ transform: "translateY(-50%)",
3629
+ fontSize: 11,
3630
+ color: t.textMuted,
3631
+ pointerEvents: "none"
3632
+ },
3633
+ children: "\u2315"
3634
+ }
3635
+ ),
3636
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3637
+ "input",
3638
+ {
3639
+ value: search,
3640
+ onChange: (e) => setSearch(e.target.value),
3641
+ placeholder: "Search\u2026",
3642
+ style: {
3643
+ width: "100%",
3644
+ padding: "5px 8px 5px 24px",
3645
+ border: `1.5px solid ${t.inputBorder}`,
3646
+ borderRadius: 7,
3647
+ fontSize: 12,
3648
+ background: t.inputBg,
3649
+ color: t.inputText,
3650
+ outline: "none",
3651
+ boxSizing: "border-box",
3652
+ fontFamily: "inherit"
3653
+ }
3654
+ }
3655
+ )
3656
+ ] })
3657
+ }
3658
+ ),
3659
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3660
+ "div",
3661
+ {
3662
+ style: {
3663
+ flex: 1,
3664
+ overflowY: "auto",
3665
+ padding: "6px 8px",
3666
+ display: "flex",
3667
+ flexDirection: "column",
3668
+ gap: 2
3669
+ },
3670
+ children: [
3671
+ filtered.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3672
+ "div",
3673
+ {
3674
+ style: {
3675
+ textAlign: "center",
3676
+ padding: "20px 0",
3677
+ fontSize: 12,
3678
+ color: t.textMuted,
3679
+ fontStyle: "italic"
3680
+ },
3681
+ children: model.nodes.length === 0 ? "No nodes yet" : "No matches"
3682
+ }
3683
+ ),
3684
+ filtered.map((node, idx) => {
3685
+ const isSelected = selected === node.id;
3686
+ const answers = node.metadata?.answers ?? [];
3687
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3688
+ "button",
3689
+ {
3690
+ onClick: () => onSelect(node.id),
3691
+ style: {
3692
+ display: "flex",
3693
+ alignItems: "center",
3694
+ gap: 8,
3695
+ width: "100%",
3696
+ padding: "7px 8px",
3697
+ textAlign: "left",
3698
+ background: isSelected ? acc.fill : "transparent",
3699
+ border: isSelected ? `1.5px solid ${acc.border}` : "1.5px solid transparent",
3700
+ borderRadius: 8,
3701
+ cursor: "pointer",
3702
+ fontFamily: "inherit",
3703
+ transition: "background 0.1s"
3704
+ },
3705
+ onMouseEnter: (e) => {
3706
+ if (!isSelected)
3707
+ e.currentTarget.style.background = isDark ? "#334155" : "#f1f5f9";
3708
+ },
3709
+ onMouseLeave: (e) => {
3710
+ if (!isSelected) e.currentTarget.style.background = "transparent";
3711
+ },
3712
+ children: [
3713
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3714
+ "div",
3715
+ {
3716
+ style: {
3717
+ width: 22,
3718
+ height: 22,
3719
+ borderRadius: 6,
3720
+ flexShrink: 0,
3721
+ background: isSelected ? acc.color : isDark ? "#334155" : "#e2e8f0",
3722
+ color: isSelected ? "#fff" : t.textMuted,
3723
+ display: "flex",
3724
+ alignItems: "center",
3725
+ justifyContent: "center",
3726
+ fontSize: variant === "journey" ? 9 : 11,
3727
+ fontWeight: 700
3728
+ },
3729
+ children: variant === "journey" ? idx + 1 : shapeIcon(node)
3730
+ }
3731
+ ),
3732
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [
3733
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3734
+ "div",
3735
+ {
3736
+ style: {
3737
+ fontSize: 12,
3738
+ fontWeight: isSelected ? 600 : 400,
3739
+ color: isSelected ? acc.color : t.textPrimary,
3740
+ overflow: "hidden",
3741
+ textOverflow: "ellipsis",
3742
+ whiteSpace: "nowrap",
3743
+ lineHeight: 1.3
3744
+ },
3745
+ children: node.label
3746
+ }
3747
+ ),
3748
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontSize: 10, color: t.textMuted, lineHeight: 1.2, marginTop: 1 }, children: variant === "question" ? `${answers.length} answer${answers.length !== 1 ? "s" : ""}` : `${inEdges(node.id)}\u2193 ${outEdges(node.id)}\u2192` })
3749
+ ] }),
3750
+ isSelected && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontSize: 10, color: acc.color, flexShrink: 0 }, children: "\u25C9" })
3751
+ ]
3752
+ },
3753
+ node.id
3754
+ );
3755
+ })
3756
+ ]
3757
+ }
3758
+ )
3759
+ ]
3760
+ }
3761
+ );
2750
3762
  }
2751
3763
 
2752
3764
  // src/ui/render.tsx
@@ -2821,18 +3833,55 @@ var STYLE_LABEL = { pointerEvents: "none", userSelect: "none" };
2821
3833
  var STYLE_BLUR = { filter: "blur(4px)" };
2822
3834
  var STYLE_EDGE_HIT = { cursor: "pointer" };
2823
3835
  var STYLE_NO_EVENTS = { pointerEvents: "none" };
2824
- var STYLE_PORT_HOVER = { cursor: "crosshair", filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.18))" };
2825
- var STYLE_WAYPOINT = { cursor: "grab", filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.25))" };
3836
+ var STYLE_PORT_HOVER = {
3837
+ cursor: "crosshair",
3838
+ filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.18))"
3839
+ };
3840
+ var STYLE_WAYPOINT = {
3841
+ cursor: "grab",
3842
+ filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.25))"
3843
+ };
2826
3844
  var STYLE_EDGE_LABEL_HIT = { cursor: "text" };
2827
- function NodeShape({ node, selected, variant, stepNumber, t, isDark, w }) {
3845
+ function NodeShape({
3846
+ node,
3847
+ selected,
3848
+ variant,
3849
+ stepNumber,
3850
+ t,
3851
+ isDark,
3852
+ w
3853
+ }) {
2828
3854
  const acc = variantAccent(variant, isDark);
2829
3855
  const cx = w / 2, cy = NODE_H2 / 2;
2830
3856
  const stroke = selected ? acc.color : t.nodeStroke;
2831
3857
  const fill = selected ? t.nodeSelectedFill : t.nodeFill;
2832
3858
  const sw = selected ? 1.75 : 1.25;
2833
3859
  const glow = selected && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_jsx_runtime8.Fragment, { children: node.shape === "circle" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2834
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx, cy, r: NODE_H2 / 2 + 3, fill: "none", stroke: acc.color, strokeWidth: 6, opacity: 0.18, style: STYLE_BLUR }),
2835
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx, cy, r: NODE_H2 / 2 + 1.5, fill: "none", stroke: acc.color, strokeWidth: 1, opacity: 0.55 })
3860
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3861
+ "circle",
3862
+ {
3863
+ cx,
3864
+ cy,
3865
+ r: NODE_H2 / 2 + 3,
3866
+ fill: "none",
3867
+ stroke: acc.color,
3868
+ strokeWidth: 6,
3869
+ opacity: 0.18,
3870
+ style: STYLE_BLUR
3871
+ }
3872
+ ),
3873
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3874
+ "circle",
3875
+ {
3876
+ cx,
3877
+ cy,
3878
+ r: NODE_H2 / 2 + 1.5,
3879
+ fill: "none",
3880
+ stroke: acc.color,
3881
+ strokeWidth: 1,
3882
+ opacity: 0.55
3883
+ }
3884
+ )
2836
3885
  ] }) : node.shape === "diamond" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2837
3886
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2838
3887
  "polygon",
@@ -2889,39 +3938,98 @@ function NodeShape({ node, selected, variant, stepNumber, t, isDark, w }) {
2889
3938
  const badgeColor = isDark ? ACCENT.emeraldDark : ACCENT.emerald;
2890
3939
  const badge = variant === "journey" && stepNumber !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2891
3940
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: 14, cy: 14, r: 10, fill: badgeColor }),
2892
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("text", { x: 14, y: 18, textAnchor: "middle", fontSize: 9, fill: "white", fontWeight: "700", style: STYLE_LABEL, children: stepNumber })
3941
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3942
+ "text",
3943
+ {
3944
+ x: 14,
3945
+ y: 18,
3946
+ textAnchor: "middle",
3947
+ fontSize: 9,
3948
+ fill: "white",
3949
+ fontWeight: "700",
3950
+ style: STYLE_LABEL,
3951
+ children: stepNumber
3952
+ }
3953
+ )
2893
3954
  ] });
2894
3955
  switch (node.shape) {
2895
3956
  case "diamond": {
2896
3957
  const pts = `${cx},0 ${w},${cy} ${cx},${NODE_H2} 0,${cy}`;
2897
3958
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2898
3959
  glow,
2899
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polygon", { points: pts, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
3960
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3961
+ "polygon",
3962
+ {
3963
+ points: pts,
3964
+ fill,
3965
+ stroke,
3966
+ strokeWidth: sw,
3967
+ filter: "url(#nodeShadow)"
3968
+ }
3969
+ ),
2900
3970
  badge
2901
3971
  ] });
2902
3972
  }
2903
3973
  case "circle":
2904
3974
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2905
3975
  glow,
2906
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx, cy, r: NODE_H2 / 2 - 1, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
3976
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3977
+ "circle",
3978
+ {
3979
+ cx,
3980
+ cy,
3981
+ r: NODE_H2 / 2 - 1,
3982
+ fill,
3983
+ stroke,
3984
+ strokeWidth: sw,
3985
+ filter: "url(#nodeShadow)"
3986
+ }
3987
+ ),
2907
3988
  badge
2908
3989
  ] });
2909
3990
  case "parallelogram":
2910
3991
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2911
3992
  glow,
2912
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("polygon", { points: `14,0 ${w},0 ${w - 14},${NODE_H2} 0,${NODE_H2}`, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
3993
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3994
+ "polygon",
3995
+ {
3996
+ points: `14,0 ${w},0 ${w - 14},${NODE_H2} 0,${NODE_H2}`,
3997
+ fill,
3998
+ stroke,
3999
+ strokeWidth: sw,
4000
+ filter: "url(#nodeShadow)"
4001
+ }
4002
+ ),
2913
4003
  badge
2914
4004
  ] });
2915
4005
  default:
2916
4006
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2917
4007
  glow,
2918
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { width: w, height: NODE_H2, rx: 14, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
4008
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4009
+ "rect",
4010
+ {
4011
+ width: w,
4012
+ height: NODE_H2,
4013
+ rx: 14,
4014
+ fill,
4015
+ stroke,
4016
+ strokeWidth: sw,
4017
+ filter: "url(#nodeShadow)"
4018
+ }
4019
+ ),
2919
4020
  badge
2920
4021
  ] });
2921
4022
  }
2922
4023
  }
2923
4024
  var ANSWER_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
2924
- function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
4025
+ function QuestionNode({
4026
+ node,
4027
+ selected,
4028
+ edges,
4029
+ isDark,
4030
+ onAnswerPortDown,
4031
+ qW
4032
+ }) {
2925
4033
  const answers = node.metadata?.answers ?? [];
2926
4034
  const totalH = questionNodeH2(answers);
2927
4035
  const amber = isDark ? ACCENT.amberDark : ACCENT.amber;
@@ -2969,27 +4077,91 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
2969
4077
  ] });
2970
4078
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2971
4079
  glow,
2972
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { width: qW, height: totalH, rx: 14, fill: nodeBg, stroke: nodeBorder, strokeWidth: selected ? 2 : 1.5, filter: "url(#nodeShadow)" }),
4080
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4081
+ "rect",
4082
+ {
4083
+ width: qW,
4084
+ height: totalH,
4085
+ rx: 14,
4086
+ fill: nodeBg,
4087
+ stroke: nodeBorder,
4088
+ strokeWidth: selected ? 2 : 1.5,
4089
+ filter: "url(#nodeShadow)"
4090
+ }
4091
+ ),
2973
4092
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("clipPath", { id: `qhdr-${node.id}`, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { width: qW, height: Q_BASE_H2, rx: 14 }) }),
2974
4093
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { width: qW, height: Q_BASE_H2, fill: amberSoft, clipPath: `url(#qhdr-${node.id})` }),
2975
4094
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { x: 0, y: 0, width: 4, height: Q_BASE_H2, rx: 2, fill: amber }),
2976
4095
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { x: 12, y: 14, width: 28, height: 28, rx: 8, fill: amber }),
2977
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("text", { x: 26, y: 33, textAnchor: "middle", fontSize: 15, fontWeight: "900", fill: "white", style: STYLE_LABEL, children: "?" }),
2978
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
4096
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2979
4097
  "text",
2980
4098
  {
4099
+ x: 26,
4100
+ y: 33,
4101
+ textAnchor: "middle",
4102
+ fontSize: 15,
4103
+ fontWeight: "900",
4104
+ fill: "white",
2981
4105
  style: STYLE_LABEL,
2982
- fontFamily: "ui-sans-serif,system-ui,sans-serif",
2983
- children: [
2984
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("tspan", { x: 50, y: 27, fontSize: 9, fontWeight: 700, fill: textSub, letterSpacing: 0.6, textAnchor: "start", children: "QUESTION" }),
2985
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("tspan", { x: 50, dy: 15, fontSize: 13, fontWeight: 700, fill: selected ? amber : textMain, textAnchor: "start", children: node.label })
2986
- ]
4106
+ children: "?"
2987
4107
  }
2988
4108
  ),
4109
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("text", { style: STYLE_LABEL, fontFamily: "ui-sans-serif,system-ui,sans-serif", children: [
4110
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4111
+ "tspan",
4112
+ {
4113
+ x: 50,
4114
+ y: 27,
4115
+ fontSize: 9,
4116
+ fontWeight: 700,
4117
+ fill: textSub,
4118
+ letterSpacing: 0.6,
4119
+ textAnchor: "start",
4120
+ children: "QUESTION"
4121
+ }
4122
+ ),
4123
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4124
+ "tspan",
4125
+ {
4126
+ x: 50,
4127
+ dy: 15,
4128
+ fontSize: 13,
4129
+ fontWeight: 700,
4130
+ fill: selected ? amber : textMain,
4131
+ textAnchor: "start",
4132
+ children: node.label
4133
+ }
4134
+ )
4135
+ ] }),
2989
4136
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("line", { x1: 0, y1: Q_BASE_H2, x2: qW, y2: Q_BASE_H2, stroke: amberLine, strokeWidth: 1 }),
2990
4137
  answers.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2991
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("text", { x: qW / 2, y: Q_BASE_H2 + 22, textAnchor: "middle", fontSize: 10, fill: amber, opacity: 0.4, fontWeight: 600, style: STYLE_LABEL, children: "No answers yet" }),
2992
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("text", { x: qW / 2, y: Q_BASE_H2 + 36, textAnchor: "middle", fontSize: 9, fill: textSub, opacity: 0.7, style: STYLE_LABEL, children: "Open panel \u2192 Add Answer" })
4138
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4139
+ "text",
4140
+ {
4141
+ x: qW / 2,
4142
+ y: Q_BASE_H2 + 22,
4143
+ textAnchor: "middle",
4144
+ fontSize: 10,
4145
+ fill: amber,
4146
+ opacity: 0.4,
4147
+ fontWeight: 600,
4148
+ style: STYLE_LABEL,
4149
+ children: "No answers yet"
4150
+ }
4151
+ ),
4152
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4153
+ "text",
4154
+ {
4155
+ x: qW / 2,
4156
+ y: Q_BASE_H2 + 36,
4157
+ textAnchor: "middle",
4158
+ fontSize: 9,
4159
+ fill: textSub,
4160
+ opacity: 0.7,
4161
+ style: STYLE_LABEL,
4162
+ children: "Open panel \u2192 Add Answer"
4163
+ }
4164
+ )
2993
4165
  ] }),
2994
4166
  answers.map((ans, i) => {
2995
4167
  const prevW = answers.slice(0, i).reduce((s2, a) => s2 + answerCardW2(a) + Q_CARD_PAD2, 0);
@@ -3083,7 +4255,22 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
3083
4255
  })
3084
4256
  ] });
3085
4257
  }
3086
- function EdgeLine({ edge, nodes, variant, t, isDark, acc, editing, editValue, onEditChange, onEditCommit, onEditCancel, onDoubleClick, onContextMenu, onWaypointDown }) {
4258
+ function EdgeLine({
4259
+ edge,
4260
+ nodes,
4261
+ variant,
4262
+ t,
4263
+ isDark,
4264
+ acc,
4265
+ editing,
4266
+ editValue,
4267
+ onEditChange,
4268
+ onEditCommit,
4269
+ onEditCancel,
4270
+ onDoubleClick,
4271
+ onContextMenu,
4272
+ onWaypointDown
4273
+ }) {
3087
4274
  const [hovered, setHovered] = (0, import_react13.useState)(false);
3088
4275
  const from = nodes.find((n) => n.id === edge.from);
3089
4276
  const to = nodes.find((n) => n.id === edge.to);
@@ -3287,13 +4474,13 @@ function Minimap({
3287
4474
  x: (mx - offsetX) / scale,
3288
4475
  y: (my - offsetY) / scale
3289
4476
  });
3290
- const panTo = (0, import_react14.useCallback)((e) => {
4477
+ const panTo = (e) => {
3291
4478
  const rect = e.currentTarget.getBoundingClientRect();
3292
4479
  const mx = e.clientX - rect.left;
3293
4480
  const my = e.clientY - rect.top;
3294
4481
  const { x, y } = unproject(mx, my);
3295
4482
  onCenterOn(x, y);
3296
- }, [onCenterOn, scale, offsetX, offsetY]);
4483
+ };
3297
4484
  const onMouseDown = (e) => {
3298
4485
  e.stopPropagation();
3299
4486
  dragRef.current = { active: true };
@@ -3480,29 +4667,106 @@ function ContextMenu({
3480
4667
  fontFamily: "ui-sans-serif,system-ui,sans-serif"
3481
4668
  },
3482
4669
  children: edgeId ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
3483
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Edge" }),
4670
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
4671
+ "div",
4672
+ {
4673
+ style: {
4674
+ padding: "4px 14px 6px",
4675
+ fontSize: 10,
4676
+ fontWeight: 700,
4677
+ color: muted,
4678
+ textTransform: "uppercase",
4679
+ letterSpacing: 0.8
4680
+ },
4681
+ children: "Edge"
4682
+ }
4683
+ ),
3484
4684
  item("Rename label (dbl-click)", () => onEdgeRename?.()),
3485
4685
  divider2,
3486
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { padding: "4px 14px 2px", fontSize: 9, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Style" }),
3487
- item(`Solid${currentEdgeStyle === "solid" || !currentEdgeStyle ? " \u2713" : ""}`, () => onEdgeStyle?.("solid")),
3488
- item(`Dashed${currentEdgeStyle === "dashed" ? " \u2713" : ""}`, () => onEdgeStyle?.("dashed")),
3489
- item(`Dotted${currentEdgeStyle === "dotted" ? " \u2713" : ""}`, () => onEdgeStyle?.("dotted")),
4686
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
4687
+ "div",
4688
+ {
4689
+ style: {
4690
+ padding: "4px 14px 2px",
4691
+ fontSize: 9,
4692
+ fontWeight: 700,
4693
+ color: muted,
4694
+ textTransform: "uppercase",
4695
+ letterSpacing: 0.8
4696
+ },
4697
+ children: "Style"
4698
+ }
4699
+ ),
4700
+ item(
4701
+ `Solid${currentEdgeStyle === "solid" || !currentEdgeStyle ? " \u2713" : ""}`,
4702
+ () => onEdgeStyle?.("solid")
4703
+ ),
4704
+ item(
4705
+ `Dashed${currentEdgeStyle === "dashed" ? " \u2713" : ""}`,
4706
+ () => onEdgeStyle?.("dashed")
4707
+ ),
4708
+ item(
4709
+ `Dotted${currentEdgeStyle === "dotted" ? " \u2713" : ""}`,
4710
+ () => onEdgeStyle?.("dotted")
4711
+ ),
3490
4712
  divider2,
3491
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { padding: "4px 14px 2px", fontSize: 9, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Arrowhead" }),
3492
- item(`Arrow${currentEdgeArrow !== "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("arrow")),
4713
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
4714
+ "div",
4715
+ {
4716
+ style: {
4717
+ padding: "4px 14px 2px",
4718
+ fontSize: 9,
4719
+ fontWeight: 700,
4720
+ color: muted,
4721
+ textTransform: "uppercase",
4722
+ letterSpacing: 0.8
4723
+ },
4724
+ children: "Arrowhead"
4725
+ }
4726
+ ),
4727
+ item(
4728
+ `Arrow${currentEdgeArrow !== "none" ? " \u2713" : ""}`,
4729
+ () => onEdgeArrowhead?.("arrow")
4730
+ ),
3493
4731
  item(`None${currentEdgeArrow === "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("none")),
3494
4732
  divider2,
3495
4733
  item("Reset routing", () => onEdgeResetRouting?.(), void 0, !edgeHasWaypoint),
3496
4734
  item("Delete edge", () => onEdgeDelete?.(), "#ef4444")
3497
4735
  ] }) : nodeId ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
3498
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Node" }),
4736
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
4737
+ "div",
4738
+ {
4739
+ style: {
4740
+ padding: "4px 14px 6px",
4741
+ fontSize: 10,
4742
+ fontWeight: 700,
4743
+ color: muted,
4744
+ textTransform: "uppercase",
4745
+ letterSpacing: 0.8
4746
+ },
4747
+ children: "Node"
4748
+ }
4749
+ ),
3499
4750
  item("Rename (dbl-click)", onRename),
3500
4751
  item("Duplicate", onDuplicate),
3501
4752
  item("Disconnect all edges", onDisconnect),
3502
4753
  divider2,
3503
4754
  item("Delete node", onDelete, "#ef4444")
3504
4755
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
3505
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Canvas" }),
4756
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
4757
+ "div",
4758
+ {
4759
+ style: {
4760
+ padding: "4px 14px 6px",
4761
+ fontSize: 10,
4762
+ fontWeight: 700,
4763
+ color: muted,
4764
+ textTransform: "uppercase",
4765
+ letterSpacing: 0.8
4766
+ },
4767
+ children: "Canvas"
4768
+ }
4769
+ ),
3506
4770
  item("Add node here", onAddNode, acc.color),
3507
4771
  item("Re-center (Ctrl+0)", onReCenter),
3508
4772
  divider2,
@@ -3519,8 +4783,20 @@ var STYLE_LABEL2 = { pointerEvents: "none", userSelect: "none" };
3519
4783
  var STYLE_LIVE_PORT = { opacity: 0.85, pointerEvents: "none" };
3520
4784
  var STYLE_NODE_GRAB = { cursor: "grab" };
3521
4785
  var STYLE_NODE_GRABBING = { cursor: "grabbing" };
3522
- var STYLE_PORT_VISIBLE = { cursor: "crosshair", opacity: 1, transition: "opacity 0.15s", pointerEvents: "all", filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.25))" };
3523
- var STYLE_PORT_HIDDEN = { cursor: "crosshair", opacity: 0, transition: "opacity 0.15s", pointerEvents: "none", filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.25))" };
4786
+ var STYLE_PORT_VISIBLE = {
4787
+ cursor: "crosshair",
4788
+ opacity: 1,
4789
+ transition: "opacity 0.15s",
4790
+ pointerEvents: "all",
4791
+ filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.25))"
4792
+ };
4793
+ var STYLE_PORT_HIDDEN = {
4794
+ cursor: "crosshair",
4795
+ opacity: 0,
4796
+ transition: "opacity 0.15s",
4797
+ pointerEvents: "none",
4798
+ filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.25))"
4799
+ };
3524
4800
  function DiagramCanvas(props) {
3525
4801
  const {
3526
4802
  model,
@@ -3591,25 +4867,35 @@ function DiagramCanvas(props) {
3591
4867
  onCtxEdgeDelete,
3592
4868
  onCtxEdgeResetRouting
3593
4869
  } = props;
3594
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { ref: containerRef, style: { flex: 1, overflow: "hidden", position: "relative", background: t.canvas }, children: [
3595
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
3596
- "svg",
3597
- {
3598
- ref: svgRef,
3599
- width: "100%",
3600
- height: "100%",
3601
- role: "application",
3602
- "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.`,
3603
- tabIndex: 0,
3604
- style: { display: "block", cursor: pan ? "grabbing" : drag ? "grabbing" : liveEdge ? "crosshair" : "default", userSelect: "none", outline: "none" },
3605
- onMouseDown: onSvgMouseDown,
3606
- onMouseMove,
3607
- onMouseUp,
3608
- onMouseLeave: onMouseUp,
3609
- onContextMenu: onSvgContextMenu,
3610
- children: [
3611
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("defs", { children: [
3612
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("style", { children: reducedMotion ? `
4870
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
4871
+ "div",
4872
+ {
4873
+ ref: containerRef,
4874
+ style: { flex: 1, overflow: "hidden", position: "relative", background: t.canvas },
4875
+ children: [
4876
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
4877
+ "svg",
4878
+ {
4879
+ ref: svgRef,
4880
+ width: "100%",
4881
+ height: "100%",
4882
+ role: "application",
4883
+ "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.`,
4884
+ tabIndex: 0,
4885
+ style: {
4886
+ display: "block",
4887
+ cursor: pan ? "grabbing" : drag ? "grabbing" : liveEdge ? "crosshair" : "default",
4888
+ userSelect: "none",
4889
+ outline: "none"
4890
+ },
4891
+ onMouseDown: onSvgMouseDown,
4892
+ onMouseMove,
4893
+ onMouseUp,
4894
+ onMouseLeave: onMouseUp,
4895
+ onContextMenu: onSvgContextMenu,
4896
+ children: [
4897
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("defs", { children: [
4898
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("style", { children: reducedMotion ? `
3613
4899
  .edge-flow { stroke-dasharray: 0; }
3614
4900
  .edge-flow-amber { stroke-dasharray: 0; }
3615
4901
  .edge-live { stroke-dasharray: 4 4; }
@@ -3620,217 +4906,351 @@ function DiagramCanvas(props) {
3620
4906
  .edge-flow-amber { stroke-dasharray: 6 4; animation: edgeFlowFast 0.65s linear infinite; }
3621
4907
  .edge-live { stroke-dasharray: 7 5; animation: edgeFlow 0.55s linear infinite; }
3622
4908
  ` }),
3623
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("pattern", { id: "dots", width: GRID, height: GRID, patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("circle", { cx: GRID / 2, cy: GRID / 2, r: 1.1, fill: t.dot }) }),
3624
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("filter", { id: "nodeShadow", x: "-25%", y: "-25%", width: "150%", height: "160%", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("feDropShadow", { dx: "0", dy: "3", stdDeviation: "5", floodColor: shadowClr, floodOpacity: "1" }) }),
3625
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("marker", { id: "arrowhead", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: arrowClr }) }),
3626
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("marker", { id: "arrowAmber", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: amberArrow }) }),
3627
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("marker", { id: "arrowLive", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: acc.color }) })
3628
- ] }),
3629
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { width: "100%", height: "100%", fill: "url(#dots)", "data-bg": "1" }),
3630
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("g", { transform: `translate(${transform.x},${transform.y}) scale(${transform.scale})`, children: [
3631
- model.edges.map((e) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3632
- EdgeLine,
3633
- {
3634
- edge: e,
3635
- nodes: model.nodes,
3636
- variant,
3637
- t,
3638
- isDark,
3639
- acc,
3640
- editing: editingEdgeId === e.id,
3641
- editValue: editEdgeLabel,
3642
- onEditChange: setEditEdgeLabel,
3643
- onEditCommit: commitEdgeEdit,
3644
- onEditCancel: () => setEditingEdgeId(null),
3645
- onDoubleClick: beginEditEdge,
3646
- onContextMenu: onEdgeContextMenu,
3647
- onWaypointDown: (ev, edgeId) => setWaypointDrag(edgeId)
3648
- },
3649
- e.id
3650
- )),
3651
- liveEdge && (() => {
3652
- const d = bezierPath2(liveEdge.fromX, liveEdge.fromY, liveEdge.toX, liveEdge.toY, liveEdge.exitDir);
3653
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d, fill: "none", stroke: acc.color, strokeWidth: 2, strokeLinecap: "round", className: "edge-live", opacity: 0.8, markerEnd: "url(#arrowLive)" });
3654
- })(),
3655
- alignGuides?.x && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3656
- "line",
3657
- {
3658
- x1: alignGuides.x.pos,
3659
- x2: alignGuides.x.pos,
3660
- y1: alignGuides.x.minY,
3661
- y2: alignGuides.x.maxY,
3662
- stroke: acc.color,
3663
- strokeWidth: 1 / transform.scale,
3664
- strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
3665
- opacity: 0.85,
3666
- pointerEvents: "none"
3667
- }
3668
- ),
3669
- alignGuides?.y && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3670
- "line",
3671
- {
3672
- y1: alignGuides.y.pos,
3673
- y2: alignGuides.y.pos,
3674
- x1: alignGuides.y.minX,
3675
- x2: alignGuides.y.maxX,
3676
- stroke: acc.color,
3677
- strokeWidth: 1 / transform.scale,
3678
- strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
3679
- opacity: 0.85,
3680
- pointerEvents: "none"
3681
- }
3682
- ),
3683
- model.nodes.map((node, idx) => {
3684
- const isHovered = hoveredId === node.id;
3685
- const isQuestion2 = variant === "question";
3686
- const { w: nW } = nodeDims(node, variant);
3687
- const isSelected = selectedSet.has(node.id);
3688
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
3689
- "g",
3690
- {
3691
- transform: `translate(${node.x ?? 0},${node.y ?? 0})`,
3692
- role: "button",
3693
- tabIndex: 0,
3694
- "aria-label": `${variantLabel} ${variant === "journey" ? idx + 1 + ": " : ""}${node.label}${isSelected ? ", selected" : ""}`,
3695
- style: drag?.nodeId === node.id ? STYLE_NODE_GRABBING : STYLE_NODE_GRAB,
3696
- onMouseDown: (e) => onNodeMouseDown(e, node.id),
3697
- onMouseUp: (e) => onNodeMouseUp(e, node.id),
3698
- onDoubleClick: (e) => onNodeDblClick(e, node.id),
3699
- onContextMenu: (e) => onNodeContextMenu(e, node.id),
3700
- onMouseEnter: () => setHoveredId(node.id),
3701
- onMouseLeave: () => setHoveredId(null),
3702
- onFocus: () => setHoveredId(node.id),
3703
- onBlur: () => setHoveredId(null),
3704
- onKeyDown: (e) => {
3705
- if (e.key === "F2" || e.key === "Enter" && !e.ctrlKey && !e.metaKey) {
3706
- e.preventDefault();
3707
- setEditingId(node.id);
3708
- setEditLabel(node.label);
3709
- }
3710
- },
3711
- children: [
3712
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("title", { children: `${variantLabel}: ${node.label}` }),
3713
- isQuestion2 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(QuestionNode, { node, selected: isSelected, edges: model.edges, isDark, onAnswerPortDown, qW: nW }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
3714
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(NodeShape, { node, selected: isSelected, variant, stepNumber: variant === "journey" ? idx + 1 : void 0, t, isDark, w: nW }),
3715
- editingId === node.id ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("foreignObject", { x: 6, y: 6, width: nW - 12, height: NODE_H2 - 12, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3716
- "input",
3717
- {
3718
- autoFocus: true,
3719
- value: editLabel,
3720
- onChange: (e) => setEditLabel(e.target.value),
3721
- onBlur: commitEdit,
3722
- onKeyDown: (e) => {
3723
- if (e.key === "Enter") commitEdit();
3724
- if (e.key === "Escape") setEditingId(null);
3725
- },
3726
- 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 }
3727
- }
3728
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("text", { x: nW / 2, y: NODE_H2 / 2 + 5, textAnchor: "middle", fontSize: 13, fontWeight: "500", fontFamily: "ui-sans-serif,system-ui,sans-serif", fill: isSelected ? acc.color : t.textPrimary, style: STYLE_LABEL2, children: node.label }),
3729
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3730
- "circle",
3731
- {
3732
- cx: nW / 2,
3733
- cy: NODE_H2 + 1,
3734
- r: portR,
3735
- fill: acc.color,
3736
- stroke: isDark ? "#0f172a" : "white",
3737
- strokeWidth: 2,
3738
- style: isHovered || isCoarse ? STYLE_PORT_VISIBLE : STYLE_PORT_HIDDEN,
3739
- onMouseDown: (e) => onPortMouseDown(e, node.id)
3740
- }
3741
- )
3742
- ] }),
3743
- liveEdge && liveEdge.fromId !== node.id && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("circle", { cx: nW / 2, cy: -1, r: portR, fill: acc.color, stroke: isDark ? "#0f172a" : "white", strokeWidth: 2, style: STYLE_LIVE_PORT })
3744
- ]
3745
- },
3746
- node.id
3747
- );
3748
- })
3749
- ] })
3750
- ]
3751
- }
3752
- ),
3753
- boxSel && Math.abs(boxSel.cx - boxSel.sx) + Math.abs(boxSel.cy - boxSel.sy) > 4 && containerRef.current && (() => {
3754
- const rect = containerRef.current.getBoundingClientRect();
3755
- const left = Math.min(boxSel.sx, boxSel.cx) - rect.left;
3756
- const top = Math.min(boxSel.sy, boxSel.cy) - rect.top;
3757
- const w = Math.abs(boxSel.cx - boxSel.sx);
3758
- const h = Math.abs(boxSel.cy - boxSel.sy);
3759
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3760
- "div",
3761
- {
3762
- style: {
3763
- position: "absolute",
3764
- left,
3765
- top,
3766
- width: w,
3767
- height: h,
3768
- border: `1px dashed ${acc.color}`,
3769
- background: isDark ? "rgba(99,102,241,0.10)" : "rgba(99,102,241,0.08)",
3770
- pointerEvents: "none",
3771
- borderRadius: 4
4909
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("pattern", { id: "dots", width: GRID, height: GRID, patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("circle", { cx: GRID / 2, cy: GRID / 2, r: 1.1, fill: t.dot }) }),
4910
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("filter", { id: "nodeShadow", x: "-25%", y: "-25%", width: "150%", height: "160%", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("feDropShadow", { dx: "0", dy: "3", stdDeviation: "5", floodColor: shadowClr, floodOpacity: "1" }) }),
4911
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
4912
+ "marker",
4913
+ {
4914
+ id: "arrowhead",
4915
+ markerWidth: "9",
4916
+ markerHeight: "7",
4917
+ refX: "8",
4918
+ refY: "3.5",
4919
+ orient: "auto",
4920
+ markerUnits: "strokeWidth",
4921
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: arrowClr })
4922
+ }
4923
+ ),
4924
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
4925
+ "marker",
4926
+ {
4927
+ id: "arrowAmber",
4928
+ markerWidth: "9",
4929
+ markerHeight: "7",
4930
+ refX: "8",
4931
+ refY: "3.5",
4932
+ orient: "auto",
4933
+ markerUnits: "strokeWidth",
4934
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: amberArrow })
4935
+ }
4936
+ ),
4937
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
4938
+ "marker",
4939
+ {
4940
+ id: "arrowLive",
4941
+ markerWidth: "9",
4942
+ markerHeight: "7",
4943
+ refX: "8",
4944
+ refY: "3.5",
4945
+ orient: "auto",
4946
+ markerUnits: "strokeWidth",
4947
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: acc.color })
4948
+ }
4949
+ )
4950
+ ] }),
4951
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { width: "100%", height: "100%", fill: "url(#dots)", "data-bg": "1" }),
4952
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("g", { transform: `translate(${transform.x},${transform.y}) scale(${transform.scale})`, children: [
4953
+ model.edges.map((e) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
4954
+ EdgeLine,
4955
+ {
4956
+ edge: e,
4957
+ nodes: model.nodes,
4958
+ variant,
4959
+ t,
4960
+ isDark,
4961
+ acc,
4962
+ editing: editingEdgeId === e.id,
4963
+ editValue: editEdgeLabel,
4964
+ onEditChange: setEditEdgeLabel,
4965
+ onEditCommit: commitEdgeEdit,
4966
+ onEditCancel: () => setEditingEdgeId(null),
4967
+ onDoubleClick: beginEditEdge,
4968
+ onContextMenu: onEdgeContextMenu,
4969
+ onWaypointDown: (ev, edgeId) => setWaypointDrag(edgeId)
4970
+ },
4971
+ e.id
4972
+ )),
4973
+ liveEdge && (() => {
4974
+ const d = bezierPath2(
4975
+ liveEdge.fromX,
4976
+ liveEdge.fromY,
4977
+ liveEdge.toX,
4978
+ liveEdge.toY,
4979
+ liveEdge.exitDir
4980
+ );
4981
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
4982
+ "path",
4983
+ {
4984
+ d,
4985
+ fill: "none",
4986
+ stroke: acc.color,
4987
+ strokeWidth: 2,
4988
+ strokeLinecap: "round",
4989
+ className: "edge-live",
4990
+ opacity: 0.8,
4991
+ markerEnd: "url(#arrowLive)"
4992
+ }
4993
+ );
4994
+ })(),
4995
+ alignGuides?.x && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
4996
+ "line",
4997
+ {
4998
+ x1: alignGuides.x.pos,
4999
+ x2: alignGuides.x.pos,
5000
+ y1: alignGuides.x.minY,
5001
+ y2: alignGuides.x.maxY,
5002
+ stroke: acc.color,
5003
+ strokeWidth: 1 / transform.scale,
5004
+ strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
5005
+ opacity: 0.85,
5006
+ pointerEvents: "none"
5007
+ }
5008
+ ),
5009
+ alignGuides?.y && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
5010
+ "line",
5011
+ {
5012
+ y1: alignGuides.y.pos,
5013
+ y2: alignGuides.y.pos,
5014
+ x1: alignGuides.y.minX,
5015
+ x2: alignGuides.y.maxX,
5016
+ stroke: acc.color,
5017
+ strokeWidth: 1 / transform.scale,
5018
+ strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
5019
+ opacity: 0.85,
5020
+ pointerEvents: "none"
5021
+ }
5022
+ ),
5023
+ model.nodes.map((node, idx) => {
5024
+ const isHovered = hoveredId === node.id;
5025
+ const isQuestion2 = variant === "question";
5026
+ const { w: nW } = nodeDims(node, variant);
5027
+ const isSelected = selectedSet.has(node.id);
5028
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
5029
+ "g",
5030
+ {
5031
+ transform: `translate(${node.x ?? 0},${node.y ?? 0})`,
5032
+ role: "button",
5033
+ tabIndex: 0,
5034
+ "aria-label": `${variantLabel} ${variant === "journey" ? idx + 1 + ": " : ""}${node.label}${isSelected ? ", selected" : ""}`,
5035
+ style: drag?.nodeId === node.id ? STYLE_NODE_GRABBING : STYLE_NODE_GRAB,
5036
+ onMouseDown: (e) => onNodeMouseDown(e, node.id),
5037
+ onMouseUp: (e) => onNodeMouseUp(e, node.id),
5038
+ onDoubleClick: (e) => onNodeDblClick(e, node.id),
5039
+ onContextMenu: (e) => onNodeContextMenu(e, node.id),
5040
+ onMouseEnter: () => setHoveredId(node.id),
5041
+ onMouseLeave: () => setHoveredId(null),
5042
+ onFocus: () => setHoveredId(node.id),
5043
+ onBlur: () => setHoveredId(null),
5044
+ onKeyDown: (e) => {
5045
+ if (e.key === "F2" || e.key === "Enter" && !e.ctrlKey && !e.metaKey) {
5046
+ e.preventDefault();
5047
+ setEditingId(node.id);
5048
+ setEditLabel(node.label);
5049
+ }
5050
+ },
5051
+ children: [
5052
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("title", { children: `${variantLabel}: ${node.label}` }),
5053
+ isQuestion2 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
5054
+ QuestionNode,
5055
+ {
5056
+ node,
5057
+ selected: isSelected,
5058
+ edges: model.edges,
5059
+ isDark,
5060
+ onAnswerPortDown,
5061
+ qW: nW
5062
+ }
5063
+ ) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
5064
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
5065
+ NodeShape,
5066
+ {
5067
+ node,
5068
+ selected: isSelected,
5069
+ variant,
5070
+ stepNumber: variant === "journey" ? idx + 1 : void 0,
5071
+ t,
5072
+ isDark,
5073
+ w: nW
5074
+ }
5075
+ ),
5076
+ editingId === node.id ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("foreignObject", { x: 6, y: 6, width: nW - 12, height: NODE_H2 - 12, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
5077
+ "input",
5078
+ {
5079
+ autoFocus: true,
5080
+ value: editLabel,
5081
+ onChange: (e) => setEditLabel(e.target.value),
5082
+ onBlur: commitEdit,
5083
+ onKeyDown: (e) => {
5084
+ if (e.key === "Enter") commitEdit();
5085
+ if (e.key === "Escape") setEditingId(null);
5086
+ },
5087
+ style: {
5088
+ width: "100%",
5089
+ height: "100%",
5090
+ border: "none",
5091
+ borderRadius: 6,
5092
+ outline: `2px solid ${acc.color}`,
5093
+ textAlign: "center",
5094
+ fontSize: 13,
5095
+ fontWeight: 500,
5096
+ background: t.inputBg,
5097
+ boxSizing: "border-box",
5098
+ padding: "0 6px",
5099
+ fontFamily: "inherit",
5100
+ color: t.inputText
5101
+ }
5102
+ }
5103
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
5104
+ "text",
5105
+ {
5106
+ x: nW / 2,
5107
+ y: NODE_H2 / 2 + 5,
5108
+ textAnchor: "middle",
5109
+ fontSize: 13,
5110
+ fontWeight: "500",
5111
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
5112
+ fill: isSelected ? acc.color : t.textPrimary,
5113
+ style: STYLE_LABEL2,
5114
+ children: node.label
5115
+ }
5116
+ ),
5117
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
5118
+ "circle",
5119
+ {
5120
+ cx: nW / 2,
5121
+ cy: NODE_H2 + 1,
5122
+ r: portR,
5123
+ fill: acc.color,
5124
+ stroke: isDark ? "#0f172a" : "white",
5125
+ strokeWidth: 2,
5126
+ style: isHovered || isCoarse ? STYLE_PORT_VISIBLE : STYLE_PORT_HIDDEN,
5127
+ onMouseDown: (e) => onPortMouseDown(e, node.id)
5128
+ }
5129
+ )
5130
+ ] }),
5131
+ liveEdge && liveEdge.fromId !== node.id && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
5132
+ "circle",
5133
+ {
5134
+ cx: nW / 2,
5135
+ cy: -1,
5136
+ r: portR,
5137
+ fill: acc.color,
5138
+ stroke: isDark ? "#0f172a" : "white",
5139
+ strokeWidth: 2,
5140
+ style: STYLE_LIVE_PORT
5141
+ }
5142
+ )
5143
+ ]
5144
+ },
5145
+ node.id
5146
+ );
5147
+ })
5148
+ ] })
5149
+ ]
3772
5150
  }
3773
- }
3774
- );
3775
- })(),
3776
- model.nodes.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: { position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", pointerEvents: "none", gap: 8 }, children: [
3777
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: { fontSize: 36, opacity: 0.1, color: t.textPrimary }, children: variant === "question" ? "?" : variant === "journey" ? "\u2197" : "\u2B21" }),
3778
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: { fontSize: 13, color: t.textMuted, fontWeight: 500 }, children: [
3779
- "Click ",
3780
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("strong", { style: { color: acc.color }, children: [
3781
- "+ ",
3782
- variantLabel
3783
- ] }),
3784
- " to start"
3785
- ] })
3786
- ] }),
3787
- model.nodes.length > 0 && viewport.w > 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3788
- Minimap,
3789
- {
3790
- model,
3791
- viewportW: viewport.w,
3792
- viewportH: viewport.h,
3793
- transform,
3794
- isDark,
3795
- accentColor: acc.color,
3796
- measureNode: (n) => nodeDims(n, variant),
3797
- onCenterOn: (cx, cy) => {
3798
- setTransform((tr) => ({ ...tr, x: viewport.w / 2 - cx * tr.scale, y: viewport.h / 2 - cy * tr.scale }));
3799
- }
3800
- }
3801
- ),
3802
- ctxMenu && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3803
- ContextMenu,
3804
- {
3805
- x: ctxMenu.x,
3806
- y: ctxMenu.y,
3807
- nodeId: ctxMenu.nodeId,
3808
- edgeId: ctxMenu.edgeId,
3809
- isDark,
3810
- t,
3811
- acc,
3812
- canUndo: history.canUndo,
3813
- canRedo: history.canRedo,
3814
- onUndo: onCtxUndo,
3815
- onRedo: onCtxRedo,
3816
- onReCenter: onCtxReCenter,
3817
- onAddNode: onCtxAddNode,
3818
- onDuplicate: onCtxDuplicate,
3819
- onRename: onCtxRename,
3820
- onDelete: onCtxDelete,
3821
- onDisconnect: onCtxDisconnect,
3822
- currentEdgeStyle: ctxEdgeStyle,
3823
- currentEdgeArrow: ctxEdgeArrow,
3824
- edgeHasWaypoint: ctxEdgeHasWaypoint,
3825
- onEdgeRename: onCtxEdgeRename,
3826
- onEdgeStyle: onCtxEdgeStyle,
3827
- onEdgeArrowhead: onCtxEdgeArrowhead,
3828
- onEdgeDelete: onCtxEdgeDelete,
3829
- onEdgeResetRouting: onCtxEdgeResetRouting,
3830
- containerRef
3831
- }
3832
- )
3833
- ] });
5151
+ ),
5152
+ boxSel && Math.abs(boxSel.cx - boxSel.sx) + Math.abs(boxSel.cy - boxSel.sy) > 4 && containerRef.current && (() => {
5153
+ const rect = containerRef.current.getBoundingClientRect();
5154
+ const left = Math.min(boxSel.sx, boxSel.cx) - rect.left;
5155
+ const top = Math.min(boxSel.sy, boxSel.cy) - rect.top;
5156
+ const w = Math.abs(boxSel.cx - boxSel.sx);
5157
+ const h = Math.abs(boxSel.cy - boxSel.sy);
5158
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
5159
+ "div",
5160
+ {
5161
+ style: {
5162
+ position: "absolute",
5163
+ left,
5164
+ top,
5165
+ width: w,
5166
+ height: h,
5167
+ border: `1px dashed ${acc.color}`,
5168
+ background: isDark ? "rgba(99,102,241,0.10)" : "rgba(99,102,241,0.08)",
5169
+ pointerEvents: "none",
5170
+ borderRadius: 4
5171
+ }
5172
+ }
5173
+ );
5174
+ })(),
5175
+ model.nodes.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
5176
+ "div",
5177
+ {
5178
+ style: {
5179
+ position: "absolute",
5180
+ inset: 0,
5181
+ display: "flex",
5182
+ flexDirection: "column",
5183
+ alignItems: "center",
5184
+ justifyContent: "center",
5185
+ pointerEvents: "none",
5186
+ gap: 8
5187
+ },
5188
+ children: [
5189
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: { fontSize: 36, opacity: 0.1, color: t.textPrimary }, children: variant === "question" ? "?" : variant === "journey" ? "\u2197" : "\u2B21" }),
5190
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: { fontSize: 13, color: t.textMuted, fontWeight: 500 }, children: [
5191
+ "Click ",
5192
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("strong", { style: { color: acc.color }, children: [
5193
+ "+ ",
5194
+ variantLabel
5195
+ ] }),
5196
+ " to start"
5197
+ ] })
5198
+ ]
5199
+ }
5200
+ ),
5201
+ model.nodes.length > 0 && viewport.w > 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
5202
+ Minimap,
5203
+ {
5204
+ model,
5205
+ viewportW: viewport.w,
5206
+ viewportH: viewport.h,
5207
+ transform,
5208
+ isDark,
5209
+ accentColor: acc.color,
5210
+ measureNode: (n) => nodeDims(n, variant),
5211
+ onCenterOn: (cx, cy) => {
5212
+ setTransform((tr) => ({
5213
+ ...tr,
5214
+ x: viewport.w / 2 - cx * tr.scale,
5215
+ y: viewport.h / 2 - cy * tr.scale
5216
+ }));
5217
+ }
5218
+ }
5219
+ ),
5220
+ ctxMenu && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
5221
+ ContextMenu,
5222
+ {
5223
+ x: ctxMenu.x,
5224
+ y: ctxMenu.y,
5225
+ nodeId: ctxMenu.nodeId,
5226
+ edgeId: ctxMenu.edgeId,
5227
+ isDark,
5228
+ t,
5229
+ acc,
5230
+ canUndo: history.canUndo,
5231
+ canRedo: history.canRedo,
5232
+ onUndo: onCtxUndo,
5233
+ onRedo: onCtxRedo,
5234
+ onReCenter: onCtxReCenter,
5235
+ onAddNode: onCtxAddNode,
5236
+ onDuplicate: onCtxDuplicate,
5237
+ onRename: onCtxRename,
5238
+ onDelete: onCtxDelete,
5239
+ onDisconnect: onCtxDisconnect,
5240
+ currentEdgeStyle: ctxEdgeStyle,
5241
+ currentEdgeArrow: ctxEdgeArrow,
5242
+ edgeHasWaypoint: ctxEdgeHasWaypoint,
5243
+ onEdgeRename: onCtxEdgeRename,
5244
+ onEdgeStyle: onCtxEdgeStyle,
5245
+ onEdgeArrowhead: onCtxEdgeArrowhead,
5246
+ onEdgeDelete: onCtxEdgeDelete,
5247
+ onEdgeResetRouting: onCtxEdgeResetRouting,
5248
+ containerRef
5249
+ }
5250
+ )
5251
+ ]
5252
+ }
5253
+ );
3834
5254
  }
3835
5255
 
3836
5256
  // src/ui/hooks/useHistory.ts
@@ -4027,7 +5447,18 @@ function useCanvasTouch(ref, {
4027
5447
  el.removeEventListener("touchend", onEnd);
4028
5448
  el.removeEventListener("touchcancel", onEnd);
4029
5449
  };
4030
- }, [ref, transform.scale, transform.x, transform.y, setTransform, onLongPress, minScale, maxScale, longPressMs, longPressSlop]);
5450
+ }, [
5451
+ ref,
5452
+ transform.scale,
5453
+ transform.x,
5454
+ transform.y,
5455
+ setTransform,
5456
+ onLongPress,
5457
+ minScale,
5458
+ maxScale,
5459
+ longPressMs,
5460
+ longPressSlop
5461
+ ]);
4031
5462
  }
4032
5463
 
4033
5464
  // src/ui/hooks/useElementSize.ts
@@ -4141,7 +5572,17 @@ function nearestInDirection(fromX, fromY, dir, candidates) {
4141
5572
 
4142
5573
  // src/ui/DiagramEditor.tsx
4143
5574
  var import_jsx_runtime12 = require("react/jsx-runtime");
4144
- 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 };
5575
+ var STYLE_SR_ONLY = {
5576
+ position: "absolute",
5577
+ width: 1,
5578
+ height: 1,
5579
+ padding: 0,
5580
+ margin: -1,
5581
+ overflow: "hidden",
5582
+ clip: "rect(0 0 0 0)",
5583
+ whiteSpace: "nowrap",
5584
+ border: 0
5585
+ };
4145
5586
  var STYLE_FLEX_ROW = { flex: 1, display: "flex", overflow: "hidden" };
4146
5587
  function DiagramEditor(props) {
4147
5588
  if (props.initialModel?.type === "sequence") {
@@ -4197,7 +5638,7 @@ function FlowchartEditor({
4197
5638
  const next = new Set(prev);
4198
5639
  if (next.has(id)) {
4199
5640
  next.delete(id);
4200
- const last = next.size ? Array.from(next)[next.size - 1] : null;
5641
+ const last = next.size ? Array.from(next)[next.size - 1] ?? null : null;
4201
5642
  setSelected(last);
4202
5643
  } else {
4203
5644
  next.add(id);
@@ -4221,7 +5662,10 @@ function FlowchartEditor({
4221
5662
  const svgRef = (0, import_react20.useRef)(null);
4222
5663
  const containerRef = (0, import_react20.useRef)(null);
4223
5664
  const reducedMotion = usePrefersReducedMotion();
4224
- const { t, isDark } = useEditorTheme(theme, themeOverrides, { light: lightTheme, dark: darkTheme });
5665
+ const { t, isDark } = useEditorTheme(theme, themeOverrides, {
5666
+ light: lightTheme,
5667
+ dark: darkTheme
5668
+ });
4225
5669
  const isCoarse = useIsCoarsePointer();
4226
5670
  const portR = isCoarse ? 9 : 6;
4227
5671
  const viewport = useElementSize(svgRef);
@@ -4249,52 +5693,65 @@ function FlowchartEditor({
4249
5693
  const cx = (minX + maxX) / 2, cy = (minY + maxY) / 2;
4250
5694
  setTransform({ scale, x: W2 / 2 - cx * scale, y: H2 / 2 - cy * scale });
4251
5695
  }, [model.nodes, variant]);
4252
- const jumpToNode = (0, import_react20.useCallback)((nodeId) => {
4253
- const node = model.nodes.find((n) => n.id === nodeId);
4254
- if (!node || !svgRef.current) return;
4255
- const rect = svgRef.current.getBoundingClientRect();
4256
- const { w: nw, h: nh } = nodeDims(node, variant);
4257
- const cx = (node.x ?? 0) + nw / 2;
4258
- const cy = (node.y ?? 0) + nh / 2;
4259
- const scale = Math.min(Math.max(transform.scale, 0.8), 1.4);
4260
- setTransform({ scale, x: rect.width / 2 - cx * scale, y: rect.height / 2 - cy * scale });
4261
- selectOne(nodeId);
4262
- }, [model.nodes, variant, transform.scale, selectOne]);
4263
- const duplicateIds = (0, import_react20.useCallback)((ids) => {
4264
- if (ids.length === 0) return;
4265
- const idSet = new Set(ids);
4266
- const idMap = /* @__PURE__ */ new Map();
4267
- const nextNode = makeIdSource("node", model.nodes);
4268
- const nextEdge = makeIdSource("e", model.edges);
4269
- const newNodes = [];
4270
- for (const oldId of ids) {
4271
- const n = model.nodes.find((x) => x.id === oldId);
4272
- if (!n) continue;
4273
- const newId = nextNode();
4274
- idMap.set(oldId, newId);
4275
- newNodes.push({
4276
- ...n,
4277
- id: newId,
4278
- label: ids.length === 1 ? n.label + " (copy)" : n.label,
4279
- x: (n.x ?? 0) + 32,
4280
- y: (n.y ?? 0) + 32
4281
- });
4282
- }
4283
- const newEdges = [];
4284
- for (const e of model.edges) {
4285
- if (idSet.has(e.from) && idSet.has(e.to)) {
4286
- newEdges.push({ ...e, id: nextEdge(), from: idMap.get(e.from), to: idMap.get(e.to) });
5696
+ const jumpToNode = (0, import_react20.useCallback)(
5697
+ (nodeId) => {
5698
+ const node = model.nodes.find((n) => n.id === nodeId);
5699
+ if (!node || !svgRef.current) return;
5700
+ const rect = svgRef.current.getBoundingClientRect();
5701
+ const { w: nw, h: nh } = nodeDims(node, variant);
5702
+ const cx = (node.x ?? 0) + nw / 2;
5703
+ const cy = (node.y ?? 0) + nh / 2;
5704
+ const scale = Math.min(Math.max(transform.scale, 0.8), 1.4);
5705
+ setTransform({ scale, x: rect.width / 2 - cx * scale, y: rect.height / 2 - cy * scale });
5706
+ selectOne(nodeId);
5707
+ },
5708
+ [model.nodes, variant, transform.scale, selectOne]
5709
+ );
5710
+ const duplicateIds = (0, import_react20.useCallback)(
5711
+ (ids) => {
5712
+ if (ids.length === 0) return;
5713
+ const idSet = new Set(ids);
5714
+ const idMap = /* @__PURE__ */ new Map();
5715
+ const nextNode = makeIdSource("node", model.nodes);
5716
+ const nextEdge = makeIdSource("e", model.edges);
5717
+ const newNodes = [];
5718
+ for (const oldId of ids) {
5719
+ const n = model.nodes.find((x) => x.id === oldId);
5720
+ if (!n) continue;
5721
+ const newId = nextNode();
5722
+ idMap.set(oldId, newId);
5723
+ newNodes.push({
5724
+ ...n,
5725
+ id: newId,
5726
+ label: ids.length === 1 ? n.label + " (copy)" : n.label,
5727
+ x: (n.x ?? 0) + 32,
5728
+ y: (n.y ?? 0) + 32
5729
+ });
4287
5730
  }
4288
- }
4289
- const m = { ...model, nodes: [...model.nodes, ...newNodes], edges: [...model.edges, ...newEdges] };
4290
- applyAndPush(m);
4291
- const newIds = newNodes.map((n) => n.id);
4292
- setSelected(newIds[newIds.length - 1] ?? null);
4293
- setSelectedSet(new Set(newIds));
4294
- }, [model, applyAndPush]);
4295
- const duplicateNode = (0, import_react20.useCallback)((nodeId) => {
4296
- duplicateIds([nodeId]);
4297
- }, [duplicateIds]);
5731
+ const newEdges = [];
5732
+ for (const e of model.edges) {
5733
+ if (idSet.has(e.from) && idSet.has(e.to)) {
5734
+ newEdges.push({ ...e, id: nextEdge(), from: idMap.get(e.from), to: idMap.get(e.to) });
5735
+ }
5736
+ }
5737
+ const m = {
5738
+ ...model,
5739
+ nodes: [...model.nodes, ...newNodes],
5740
+ edges: [...model.edges, ...newEdges]
5741
+ };
5742
+ applyAndPush(m);
5743
+ const newIds = newNodes.map((n) => n.id);
5744
+ setSelected(newIds[newIds.length - 1] ?? null);
5745
+ setSelectedSet(new Set(newIds));
5746
+ },
5747
+ [model, applyAndPush]
5748
+ );
5749
+ const duplicateNode = (0, import_react20.useCallback)(
5750
+ (nodeId) => {
5751
+ duplicateIds([nodeId]);
5752
+ },
5753
+ [duplicateIds]
5754
+ );
4298
5755
  (0, import_react20.useEffect)(() => {
4299
5756
  if (!ctxMenu) return;
4300
5757
  const close = () => setCtxMenu(null);
@@ -4302,18 +5759,27 @@ function FlowchartEditor({
4302
5759
  return () => window.removeEventListener("mousedown", close);
4303
5760
  }, [ctxMenu]);
4304
5761
  const keyCommands = [
4305
- { match: (e) => (e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey, run: () => {
4306
- undo();
4307
- return true;
4308
- } },
4309
- { match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "y" || e.shiftKey && e.key === "z"), run: () => {
4310
- redo();
4311
- return true;
4312
- } },
4313
- { match: (e) => (e.ctrlKey || e.metaKey) && e.key === "0", run: () => {
4314
- reCenter();
4315
- return true;
4316
- } },
5762
+ {
5763
+ match: (e) => (e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey,
5764
+ run: () => {
5765
+ undo();
5766
+ return true;
5767
+ }
5768
+ },
5769
+ {
5770
+ match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "y" || e.shiftKey && e.key === "z"),
5771
+ run: () => {
5772
+ redo();
5773
+ return true;
5774
+ }
5775
+ },
5776
+ {
5777
+ match: (e) => (e.ctrlKey || e.metaKey) && e.key === "0",
5778
+ run: () => {
5779
+ reCenter();
5780
+ return true;
5781
+ }
5782
+ },
4317
5783
  {
4318
5784
  match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "d" || e.key === "D") && selectedSet.size > 0,
4319
5785
  run: () => {
@@ -4353,12 +5819,18 @@ function FlowchartEditor({
4353
5819
  from: idMap.get(ed.from) ?? ed.from,
4354
5820
  to: idMap.get(ed.to) ?? ed.to
4355
5821
  }));
4356
- const m = { ...model, nodes: [...model.nodes, ...newNodes], edges: [...model.edges, ...newEdges] };
5822
+ const m = {
5823
+ ...model,
5824
+ nodes: [...model.nodes, ...newNodes],
5825
+ edges: [...model.edges, ...newEdges]
5826
+ };
4357
5827
  applyAndPush(m);
4358
5828
  const newIds = newNodes.map((n) => n.id);
4359
- setSelected(newIds[newIds.length - 1]);
5829
+ setSelected(newIds[newIds.length - 1] ?? null);
4360
5830
  setSelectedSet(new Set(newIds));
4361
- setAnnouncement(`Pasted ${newIds.length} ${variantLabel.toLowerCase()}${newIds.length === 1 ? "" : "s"}.`);
5831
+ setAnnouncement(
5832
+ `Pasted ${newIds.length} ${variantLabel.toLowerCase()}${newIds.length === 1 ? "" : "s"}.`
5833
+ );
4362
5834
  return true;
4363
5835
  }
4364
5836
  },
@@ -4384,7 +5856,9 @@ function FlowchartEditor({
4384
5856
  };
4385
5857
  applyAndPush(updated);
4386
5858
  clearSelection();
4387
- setAnnouncement(`Deleted ${ids.size} ${variantLabel.toLowerCase()}${ids.size === 1 ? "" : "s"}.`);
5859
+ setAnnouncement(
5860
+ `Deleted ${ids.size} ${variantLabel.toLowerCase()}${ids.size === 1 ? "" : "s"}.`
5861
+ );
4388
5862
  return true;
4389
5863
  }
4390
5864
  },
@@ -4419,18 +5893,40 @@ function FlowchartEditor({
4419
5893
  const ids = selectedSet;
4420
5894
  const updated = {
4421
5895
  ...model,
4422
- nodes: model.nodes.map((n) => ids.has(n.id) ? { ...n, x: snap((n.x ?? 0) + dx), y: snap((n.y ?? 0) + dy) } : n)
5896
+ nodes: model.nodes.map(
5897
+ (n) => ids.has(n.id) ? { ...n, x: snap((n.x ?? 0) + dx), y: snap((n.y ?? 0) + dy) } : n
5898
+ )
4423
5899
  };
4424
5900
  applyAndPush(updated);
4425
5901
  return true;
4426
5902
  }
4427
5903
  }
4428
5904
  ];
4429
- useEditorKeyboard(keyCommands, [undo, redo, reCenter, selected, selectedSet, ctxMenu, liveEdge, editingId, boxSel, model, applyAndPush, duplicateNode, clearSelection]);
4430
- const toCanvas = (0, import_react20.useCallback)((clientX, clientY) => {
4431
- const rect = svgRef.current.getBoundingClientRect();
4432
- return { x: (clientX - rect.left - transform.x) / transform.scale, y: (clientY - rect.top - transform.y) / transform.scale };
4433
- }, [transform]);
5905
+ useEditorKeyboard(keyCommands, [
5906
+ undo,
5907
+ redo,
5908
+ reCenter,
5909
+ selected,
5910
+ selectedSet,
5911
+ ctxMenu,
5912
+ liveEdge,
5913
+ editingId,
5914
+ boxSel,
5915
+ model,
5916
+ applyAndPush,
5917
+ duplicateNode,
5918
+ clearSelection
5919
+ ]);
5920
+ const toCanvas = (0, import_react20.useCallback)(
5921
+ (clientX, clientY) => {
5922
+ const rect = svgRef.current.getBoundingClientRect();
5923
+ return {
5924
+ x: (clientX - rect.left - transform.x) / transform.scale,
5925
+ y: (clientY - rect.top - transform.y) / transform.scale
5926
+ };
5927
+ },
5928
+ [transform]
5929
+ );
4434
5930
  useCanvasWheel(svgRef, setTransform);
4435
5931
  const onCanvasLongPress = (0, import_react20.useCallback)((x, y) => {
4436
5932
  setCtxMenu({ x, y, nodeId: null });
@@ -4441,13 +5937,28 @@ function FlowchartEditor({
4441
5937
  const node = model.nodes.find((n) => n.id === nodeId);
4442
5938
  const { x, y } = toCanvas(e.clientX, e.clientY);
4443
5939
  const nW = nodeWidth2(node.label);
4444
- setLiveEdge({ fromId: nodeId, fromX: (node.x ?? 0) + nW / 2, fromY: (node.y ?? 0) + NODE_H2, exitDir: "bottom", toX: x, toY: y });
5940
+ setLiveEdge({
5941
+ fromId: nodeId,
5942
+ fromX: (node.x ?? 0) + nW / 2,
5943
+ fromY: (node.y ?? 0) + NODE_H2,
5944
+ exitDir: "bottom",
5945
+ toX: x,
5946
+ toY: y
5947
+ });
4445
5948
  };
4446
5949
  const onAnswerPortDown = (e, nodeId, answer, portXInNode, portYInNode) => {
4447
5950
  e.stopPropagation();
4448
5951
  const node = model.nodes.find((n) => n.id === nodeId);
4449
5952
  const { x, y } = toCanvas(e.clientX, e.clientY);
4450
- setLiveEdge({ fromId: nodeId, fromX: (node.x ?? 0) + portXInNode, fromY: (node.y ?? 0) + portYInNode, exitDir: "bottom", answerLabel: answer, toX: x, toY: y });
5953
+ setLiveEdge({
5954
+ fromId: nodeId,
5955
+ fromX: (node.x ?? 0) + portXInNode,
5956
+ fromY: (node.y ?? 0) + portYInNode,
5957
+ exitDir: "bottom",
5958
+ answerLabel: answer,
5959
+ toX: x,
5960
+ toY: y
5961
+ });
4451
5962
  };
4452
5963
  const onNodeMouseDown = (e, id) => {
4453
5964
  e.stopPropagation();
@@ -4474,7 +5985,11 @@ function FlowchartEditor({
4474
5985
  selectOne(id);
4475
5986
  groupDragOriginsRef.current = null;
4476
5987
  }
4477
- setDrag({ nodeId: id, ox: e.clientX - (transform.x + (node.x ?? 0) * transform.scale), oy: e.clientY - (transform.y + (node.y ?? 0) * transform.scale) });
5988
+ setDrag({
5989
+ nodeId: id,
5990
+ ox: e.clientX - (transform.x + (node.x ?? 0) * transform.scale),
5991
+ oy: e.clientY - (transform.y + (node.y ?? 0) * transform.scale)
5992
+ });
4478
5993
  };
4479
5994
  const onNodeMouseUp = (e, targetId) => {
4480
5995
  if (!liveEdge || liveEdge.fromId === targetId) return;
@@ -4484,12 +5999,27 @@ function FlowchartEditor({
4484
5999
  if (label) {
4485
6000
  const existing = model.edges.find((ex) => ex.from === liveEdge.fromId && ex.label === label);
4486
6001
  if (existing) {
4487
- updated = { ...model, edges: model.edges.map((ex) => ex.id === existing.id ? { ...ex, to: targetId } : ex) };
6002
+ updated = {
6003
+ ...model,
6004
+ edges: model.edges.map((ex) => ex.id === existing.id ? { ...ex, to: targetId } : ex)
6005
+ };
4488
6006
  } else {
4489
- updated = { ...model, edges: [...model.edges, { id: nextId("e", model.edges), from: liveEdge.fromId, to: targetId, label }] };
6007
+ updated = {
6008
+ ...model,
6009
+ edges: [
6010
+ ...model.edges,
6011
+ { id: nextId("e", model.edges), from: liveEdge.fromId, to: targetId, label }
6012
+ ]
6013
+ };
4490
6014
  }
4491
6015
  } else {
4492
- updated = { ...model, edges: [...model.edges, { id: nextId("e", model.edges), from: liveEdge.fromId, to: targetId }] };
6016
+ updated = {
6017
+ ...model,
6018
+ edges: [
6019
+ ...model.edges,
6020
+ { id: nextId("e", model.edges), from: liveEdge.fromId, to: targetId }
6021
+ ]
6022
+ };
4493
6023
  }
4494
6024
  applyAndPush(updated);
4495
6025
  setLiveEdge(null);
@@ -4529,7 +6059,9 @@ function FlowchartEditor({
4529
6059
  const wx = snap(x), wy = snap(y);
4530
6060
  const updated = {
4531
6061
  ...model,
4532
- edges: model.edges.map((ed) => ed.id === waypointDrag ? { ...ed, waypoint: { x: wx, y: wy } } : ed)
6062
+ edges: model.edges.map(
6063
+ (ed) => ed.id === waypointDrag ? { ...ed, waypoint: { x: wx, y: wy } } : ed
6064
+ )
4533
6065
  };
4534
6066
  applyModel(updated);
4535
6067
  return;
@@ -4561,12 +6093,23 @@ function FlowchartEditor({
4561
6093
  return { x: n.x ?? 0, y: n.y ?? 0, w: d.w, h: d.h };
4562
6094
  });
4563
6095
  const snapResult = findSiblingSnap({ x: dx, y: dy, w: dW, h: dH }, others);
4564
- setAlignGuides(snapResult.guideX || snapResult.guideY ? { x: snapResult.guideX, y: snapResult.guideY } : null);
4565
- const updated = { ...model, nodes: model.nodes.map((n) => n.id === drag.nodeId ? { ...n, x: snapResult.x, y: snapResult.y } : n) };
6096
+ setAlignGuides(
6097
+ snapResult.guideX || snapResult.guideY ? { x: snapResult.guideX, y: snapResult.guideY } : null
6098
+ );
6099
+ const updated = {
6100
+ ...model,
6101
+ nodes: model.nodes.map(
6102
+ (n) => n.id === drag.nodeId ? { ...n, x: snapResult.x, y: snapResult.y } : n
6103
+ )
6104
+ };
4566
6105
  applyModel(updated);
4567
6106
  }
4568
6107
  } else if (pan) {
4569
- setTransform((tr) => ({ ...tr, x: pan.tx + (e.clientX - pan.ox), y: pan.ty + (e.clientY - pan.oy) }));
6108
+ setTransform((tr) => ({
6109
+ ...tr,
6110
+ x: pan.tx + (e.clientX - pan.ox),
6111
+ y: pan.ty + (e.clientY - pan.oy)
6112
+ }));
4570
6113
  } else if (boxSel) {
4571
6114
  setBoxSel((b) => b ? { ...b, cx: e.clientX, cy: e.clientY } : null);
4572
6115
  }
@@ -4592,7 +6135,7 @@ function FlowchartEditor({
4592
6135
  }
4593
6136
  const arr = Array.from(hits);
4594
6137
  setSelectedSet(hits);
4595
- setSelected(arr.length ? arr[arr.length - 1] : null);
6138
+ setSelected(arr.length ? arr[arr.length - 1] ?? null : null);
4596
6139
  }
4597
6140
  setBoxSel(null);
4598
6141
  }
@@ -4615,7 +6158,10 @@ function FlowchartEditor({
4615
6158
  };
4616
6159
  const commitEdit = () => {
4617
6160
  if (!editingId) return;
4618
- const up = { ...model, nodes: model.nodes.map((n) => n.id === editingId ? { ...n, label: editLabel } : n) };
6161
+ const up = {
6162
+ ...model,
6163
+ nodes: model.nodes.map((n) => n.id === editingId ? { ...n, label: editLabel } : n)
6164
+ };
4619
6165
  applyAndPush(up);
4620
6166
  setEditingId(null);
4621
6167
  };
@@ -4624,20 +6170,28 @@ function FlowchartEditor({
4624
6170
  const p = atCanvasPos ? { x: snap(atCanvasPos.x), y: snap(atCanvasPos.y) } : { x: snap(100 + Math.random() * 240), y: snap(100 + Math.random() * 180) };
4625
6171
  const label = variant === "question" ? "New Question" : variant === "journey" ? `Step ${model.nodes.length + 1}` : "New Step";
4626
6172
  const metadata = variant === "question" ? { answers: [] } : void 0;
4627
- const updated = { ...model, nodes: [...model.nodes, { id, label, shape: "rectangle", metadata, ...p }] };
6173
+ const updated = {
6174
+ ...model,
6175
+ nodes: [...model.nodes, { id, label, shape: "rectangle", metadata, ...p }]
6176
+ };
4628
6177
  applyAndPush(updated);
4629
6178
  selectOne(id);
4630
6179
  setAnnouncement(`Added ${variantLabel.toLowerCase()} "${label}".`);
4631
6180
  };
4632
6181
  const deleteNode = (nodeId) => {
4633
6182
  const node = model.nodes.find((n) => n.id === nodeId);
4634
- const updated = { ...model, nodes: model.nodes.filter((n) => n.id !== nodeId), edges: model.edges.filter((e) => e.from !== nodeId && e.to !== nodeId) };
6183
+ const updated = {
6184
+ ...model,
6185
+ nodes: model.nodes.filter((n) => n.id !== nodeId),
6186
+ edges: model.edges.filter((e) => e.from !== nodeId && e.to !== nodeId)
6187
+ };
4635
6188
  applyAndPush(updated);
4636
6189
  if (selectedSet.has(nodeId)) {
4637
6190
  const next = new Set(selectedSet);
4638
6191
  next.delete(nodeId);
4639
6192
  setSelectedSet(next);
4640
- if (selected === nodeId) setSelected(next.size ? Array.from(next)[next.size - 1] : null);
6193
+ if (selected === nodeId)
6194
+ setSelected(next.size ? Array.from(next)[next.size - 1] ?? null : null);
4641
6195
  }
4642
6196
  if (node) setAnnouncement(`Deleted ${variantLabel.toLowerCase()} "${node.label}".`);
4643
6197
  };
@@ -4669,7 +6223,9 @@ function FlowchartEditor({
4669
6223
  const next = editEdgeLabel.trim();
4670
6224
  const updated = {
4671
6225
  ...model,
4672
- edges: model.edges.map((e) => e.id === editingEdgeId ? { ...e, ...next ? { label: next } : { label: void 0 } } : e)
6226
+ edges: model.edges.map(
6227
+ (e) => e.id === editingEdgeId ? { ...e, ...next ? { label: next } : { label: void 0 } } : e
6228
+ )
4673
6229
  };
4674
6230
  applyAndPush(updated);
4675
6231
  setEditingEdgeId(null);
@@ -4680,11 +6236,17 @@ function FlowchartEditor({
4680
6236
  setCtxMenu({ x: e.clientX, y: e.clientY, nodeId: null, edgeId });
4681
6237
  };
4682
6238
  const setEdgeStyle = (edgeId, style) => {
4683
- const updated = { ...model, edges: model.edges.map((e) => e.id === edgeId ? { ...e, style } : e) };
6239
+ const updated = {
6240
+ ...model,
6241
+ edges: model.edges.map((e) => e.id === edgeId ? { ...e, style } : e)
6242
+ };
4684
6243
  applyAndPush(updated);
4685
6244
  };
4686
6245
  const setEdgeArrowhead = (edgeId, arrowhead) => {
4687
- const updated = { ...model, edges: model.edges.map((e) => e.id === edgeId ? { ...e, arrowhead } : e) };
6246
+ const updated = {
6247
+ ...model,
6248
+ edges: model.edges.map((e) => e.id === edgeId ? { ...e, arrowhead } : e)
6249
+ };
4688
6250
  applyAndPush(updated);
4689
6251
  };
4690
6252
  const deleteEdge = (edgeId) => {
@@ -4704,14 +6266,17 @@ function FlowchartEditor({
4704
6266
  applyAndPush(updated);
4705
6267
  };
4706
6268
  const handleExport = useExporters(model, onExport, "diagram", (msg) => showToast(msg, "success"));
4707
- const positionFlowchartNodes = (0, import_react20.useCallback)((m) => ({
4708
- ...m,
4709
- nodes: m.nodes.map((n, i) => ({
4710
- ...n,
4711
- x: n.x ?? snap(80 + i % 4 * 200),
4712
- y: n.y ?? snap(80 + Math.floor(i / 4) * 140)
4713
- }))
4714
- }), []);
6269
+ const positionFlowchartNodes = (0, import_react20.useCallback)(
6270
+ (m) => ({
6271
+ ...m,
6272
+ nodes: m.nodes.map((n, i) => ({
6273
+ ...n,
6274
+ x: n.x ?? snap(80 + i % 4 * 200),
6275
+ y: n.y ?? snap(80 + Math.floor(i / 4) * 140)
6276
+ }))
6277
+ }),
6278
+ []
6279
+ );
4715
6280
  const handleImport = useImporter(applyAndPush, {
4716
6281
  transform: positionFlowchartNodes,
4717
6282
  onSuccess: (msg) => showToast(msg, "success"),
@@ -4722,9 +6287,23 @@ function FlowchartEditor({
4722
6287
  const shadowClr = shadowColor(isDark);
4723
6288
  const arrowClr = arrowColor(isDark);
4724
6289
  const amberArrow = isDark ? ACCENT.amberDark : ACCENT.amber;
4725
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "fsd-editor", style: { display: "flex", flexDirection: "column", height, width: "100%", fontFamily: "ui-sans-serif,system-ui,sans-serif", boxSizing: "border-box", background: t.ctrlsBg, position: "relative" }, children: [
4726
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ToastContainer, { toasts, onDismiss: dismissToast }),
4727
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("style", { children: `
6290
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
6291
+ "div",
6292
+ {
6293
+ className: "fsd-editor",
6294
+ style: {
6295
+ display: "flex",
6296
+ flexDirection: "column",
6297
+ height,
6298
+ width: "100%",
6299
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
6300
+ boxSizing: "border-box",
6301
+ background: t.ctrlsBg,
6302
+ position: "relative"
6303
+ },
6304
+ children: [
6305
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ToastContainer, { toasts, onDismiss: dismissToast }),
6306
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("style", { children: `
4728
6307
  .fsd-editor button:focus-visible,
4729
6308
  .fsd-editor input:focus-visible,
4730
6309
  .fsd-editor textarea:focus-visible,
@@ -4743,209 +6322,293 @@ function FlowchartEditor({
4743
6322
  outline-offset: -2px;
4744
6323
  }
4745
6324
  ` }),
4746
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
4747
- "div",
4748
- {
4749
- role: "status",
4750
- "aria-live": "polite",
4751
- "aria-atomic": "true",
4752
- style: STYLE_SR_ONLY,
4753
- children: announcement
4754
- }
4755
- ),
4756
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
4757
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { display: "flex", gap: 6, padding: "7px 14px", background: t.ctrlsBg, borderBottom: `1px solid ${t.ctrlsBorder}`, alignItems: "center", flexWrap: "wrap" }, children: [
4758
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("button", { onClick: () => addNode(), style: ctrlBtn(acc.color, isDark), children: [
4759
- "+ ",
4760
- variantLabel
4761
- ] }),
4762
- selectedSet.size > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
4763
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: { width: 1, height: 20, background: t.ctrlsBorder, margin: "0 2px" } }),
4764
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { onClick: deleteSelected, style: { ...ctrlBtn("transparent", isDark), color: "#ef4444", border: `1px solid ${isDark ? "#7f1d1d" : "#fca5a5"}` }, children: selectedSet.size > 1 ? `Delete (${selectedSet.size})` : "Delete" })
4765
- ] }),
4766
- liveEdge && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { style: { fontSize: 11, color: acc.color, fontWeight: 600, marginLeft: 6 }, children: [
4767
- liveEdge.answerLabel ? `Routing "${liveEdge.answerLabel}" \u2192` : "Drop on a node to connect",
4768
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: { fontWeight: 400, color: t.textMuted, marginLeft: 6 }, children: "release to cancel" })
4769
- ] }),
4770
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
4771
- variant === "question" ? "drag answer port to connect \xB7 " : "drag port dot \xB7 ",
4772
- "scroll to zoom \xB7 drag to pan"
4773
- ] })
4774
- ] }),
4775
- variant !== "flowchart" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: { padding: "3px 14px", background: acc.fill, borderBottom: `1px solid ${acc.border}`, fontSize: 11, color: acc.color, fontWeight: 600 }, children: variant === "question" ? "? Question Flow \u2014 add answers in the panel, drag their port to connect" : "\u2197 Journey Map \u2014 numbered steps, drag port to sequence" }),
4776
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: STYLE_FLEX_ROW, children: [
4777
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
4778
- NodeNavigator,
4779
- {
4780
- model,
4781
- selected,
4782
- variant,
4783
- isDark,
4784
- t,
4785
- acc,
4786
- open: navOpen,
4787
- onToggle: () => setNavOpen((v) => !v),
4788
- onSelect: jumpToNode
4789
- }
4790
- ),
4791
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
4792
- DiagramCanvas,
4793
- {
4794
- model,
4795
- variant,
4796
- variantLabel,
4797
- t,
4798
- isDark,
4799
- acc,
4800
- transform,
4801
- setTransform,
4802
- selected,
4803
- selectedSet,
4804
- hoveredId,
4805
- setHoveredId,
4806
- drag,
4807
- pan,
4808
- liveEdge,
4809
- boxSel,
4810
- alignGuides,
4811
- editingEdgeId,
4812
- editEdgeLabel,
4813
- setEditEdgeLabel,
4814
- commitEdgeEdit,
4815
- setEditingEdgeId,
4816
- beginEditEdge,
4817
- onEdgeContextMenu,
4818
- setWaypointDrag,
4819
- editingId,
4820
- editLabel,
4821
- setEditLabel,
4822
- commitEdit,
4823
- setEditingId,
4824
- onNodeMouseDown,
4825
- onNodeMouseUp,
4826
- onNodeDblClick,
4827
- onNodeContextMenu,
4828
- onPortMouseDown,
4829
- onAnswerPortDown,
4830
- onSvgMouseDown,
4831
- onMouseMove,
4832
- onMouseUp,
4833
- onSvgContextMenu,
4834
- reducedMotion,
4835
- isCoarse,
4836
- portR,
4837
- shadowClr,
4838
- arrowClr,
4839
- amberArrow,
4840
- viewport,
4841
- svgRef,
4842
- containerRef,
4843
- ctxMenu,
4844
- history,
4845
- ctxEdgeStyle: (ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.style ?? "solid",
4846
- ctxEdgeArrow: (ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.arrowhead ?? "arrow",
4847
- ctxEdgeHasWaypoint: !!(ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.waypoint,
4848
- onCtxUndo: () => {
4849
- undo();
4850
- setCtxMenu(null);
4851
- },
4852
- onCtxRedo: () => {
4853
- redo();
4854
- setCtxMenu(null);
4855
- },
4856
- onCtxReCenter: () => {
4857
- reCenter();
4858
- setCtxMenu(null);
4859
- },
4860
- onCtxAddNode: () => {
4861
- const rect = svgRef.current.getBoundingClientRect();
4862
- const cx = (ctxMenu.x - rect.left - transform.x) / transform.scale;
4863
- const cy = (ctxMenu.y - rect.top - transform.y) / transform.scale;
4864
- addNode({ x: cx, y: cy });
4865
- setCtxMenu(null);
4866
- },
4867
- onCtxDuplicate: () => {
4868
- if (ctxMenu?.nodeId) {
4869
- duplicateNode(ctxMenu.nodeId);
4870
- setCtxMenu(null);
4871
- }
4872
- },
4873
- onCtxRename: () => {
4874
- if (ctxMenu?.nodeId) {
4875
- const node = model.nodes.find((n) => n.id === ctxMenu.nodeId);
4876
- setEditingId(ctxMenu.nodeId);
4877
- setEditLabel(node.label);
4878
- setCtxMenu(null);
4879
- }
4880
- },
4881
- onCtxDelete: () => {
4882
- if (ctxMenu?.nodeId) {
4883
- deleteNode(ctxMenu.nodeId);
4884
- setCtxMenu(null);
4885
- }
4886
- },
4887
- onCtxDisconnect: () => {
4888
- if (ctxMenu?.nodeId) {
4889
- const m = { ...model, edges: model.edges.filter((e) => e.from !== ctxMenu.nodeId && e.to !== ctxMenu.nodeId) };
4890
- applyAndPush(m);
4891
- setCtxMenu(null);
4892
- }
4893
- },
4894
- onCtxEdgeRename: () => {
4895
- if (ctxMenu?.edgeId) {
4896
- beginEditEdge(ctxMenu.edgeId);
4897
- setCtxMenu(null);
4898
- }
4899
- },
4900
- onCtxEdgeStyle: (s2) => {
4901
- if (ctxMenu?.edgeId) {
4902
- setEdgeStyle(ctxMenu.edgeId, s2);
4903
- setCtxMenu(null);
4904
- }
4905
- },
4906
- onCtxEdgeArrowhead: (a) => {
4907
- if (ctxMenu?.edgeId) {
4908
- setEdgeArrowhead(ctxMenu.edgeId, a);
4909
- setCtxMenu(null);
4910
- }
4911
- },
4912
- onCtxEdgeDelete: () => {
4913
- if (ctxMenu?.edgeId) {
4914
- deleteEdge(ctxMenu.edgeId);
4915
- setCtxMenu(null);
6325
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", style: STYLE_SR_ONLY, children: announcement }),
6326
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
6327
+ Toolbar,
6328
+ {
6329
+ onExport: handleExport,
6330
+ onImport: allowImport ? handleImport : void 0,
6331
+ allowedExports,
6332
+ allowImport
6333
+ }
6334
+ ),
6335
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
6336
+ "div",
6337
+ {
6338
+ style: {
6339
+ display: "flex",
6340
+ gap: 6,
6341
+ padding: "7px 14px",
6342
+ background: t.ctrlsBg,
6343
+ borderBottom: `1px solid ${t.ctrlsBorder}`,
6344
+ alignItems: "center",
6345
+ flexWrap: "wrap"
6346
+ },
6347
+ children: [
6348
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("button", { onClick: () => addNode(), style: ctrlBtn(acc.color, isDark), children: [
6349
+ "+ ",
6350
+ variantLabel
6351
+ ] }),
6352
+ selectedSet.size > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
6353
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: { width: 1, height: 20, background: t.ctrlsBorder, margin: "0 2px" } }),
6354
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
6355
+ "button",
6356
+ {
6357
+ onClick: deleteSelected,
6358
+ style: {
6359
+ ...ctrlBtn("transparent", isDark),
6360
+ color: "#ef4444",
6361
+ border: `1px solid ${isDark ? "#7f1d1d" : "#fca5a5"}`
6362
+ },
6363
+ children: selectedSet.size > 1 ? `Delete (${selectedSet.size})` : "Delete"
6364
+ }
6365
+ )
6366
+ ] }),
6367
+ liveEdge && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { style: { fontSize: 11, color: acc.color, fontWeight: 600, marginLeft: 6 }, children: [
6368
+ liveEdge.answerLabel ? `Routing "${liveEdge.answerLabel}" \u2192` : "Drop on a node to connect",
6369
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: { fontWeight: 400, color: t.textMuted, marginLeft: 6 }, children: "release to cancel" })
6370
+ ] }),
6371
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
6372
+ variant === "question" ? "drag answer port to connect \xB7 " : "drag port dot \xB7 ",
6373
+ "scroll to zoom \xB7 drag to pan"
6374
+ ] })
6375
+ ]
6376
+ }
6377
+ ),
6378
+ variant !== "flowchart" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
6379
+ "div",
6380
+ {
6381
+ style: {
6382
+ padding: "3px 14px",
6383
+ background: acc.fill,
6384
+ borderBottom: `1px solid ${acc.border}`,
6385
+ fontSize: 11,
6386
+ color: acc.color,
6387
+ fontWeight: 600
6388
+ },
6389
+ 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"
6390
+ }
6391
+ ),
6392
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: STYLE_FLEX_ROW, children: [
6393
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
6394
+ NodeNavigator,
6395
+ {
6396
+ model,
6397
+ selected,
6398
+ variant,
6399
+ isDark,
6400
+ t,
6401
+ acc,
6402
+ open: navOpen,
6403
+ onToggle: () => setNavOpen((v) => !v),
6404
+ onSelect: jumpToNode
4916
6405
  }
4917
- },
4918
- onCtxEdgeResetRouting: () => {
4919
- if (ctxMenu?.edgeId) {
4920
- resetEdgeRouting(ctxMenu.edgeId);
4921
- setCtxMenu(null);
6406
+ ),
6407
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
6408
+ DiagramCanvas,
6409
+ {
6410
+ model,
6411
+ variant,
6412
+ variantLabel,
6413
+ t,
6414
+ isDark,
6415
+ acc,
6416
+ transform,
6417
+ setTransform,
6418
+ selected,
6419
+ selectedSet,
6420
+ hoveredId,
6421
+ setHoveredId,
6422
+ drag,
6423
+ pan,
6424
+ liveEdge,
6425
+ boxSel,
6426
+ alignGuides,
6427
+ editingEdgeId,
6428
+ editEdgeLabel,
6429
+ setEditEdgeLabel,
6430
+ commitEdgeEdit,
6431
+ setEditingEdgeId,
6432
+ beginEditEdge,
6433
+ onEdgeContextMenu,
6434
+ setWaypointDrag,
6435
+ editingId,
6436
+ editLabel,
6437
+ setEditLabel,
6438
+ commitEdit,
6439
+ setEditingId,
6440
+ onNodeMouseDown,
6441
+ onNodeMouseUp,
6442
+ onNodeDblClick,
6443
+ onNodeContextMenu,
6444
+ onPortMouseDown,
6445
+ onAnswerPortDown,
6446
+ onSvgMouseDown,
6447
+ onMouseMove,
6448
+ onMouseUp,
6449
+ onSvgContextMenu,
6450
+ reducedMotion,
6451
+ isCoarse,
6452
+ portR,
6453
+ shadowClr,
6454
+ arrowClr,
6455
+ amberArrow,
6456
+ viewport,
6457
+ svgRef,
6458
+ containerRef,
6459
+ ctxMenu,
6460
+ history,
6461
+ ctxEdgeStyle: (ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.style ?? "solid",
6462
+ ctxEdgeArrow: (ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.arrowhead ?? "arrow",
6463
+ ctxEdgeHasWaypoint: !!(ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.waypoint,
6464
+ onCtxUndo: () => {
6465
+ undo();
6466
+ setCtxMenu(null);
6467
+ },
6468
+ onCtxRedo: () => {
6469
+ redo();
6470
+ setCtxMenu(null);
6471
+ },
6472
+ onCtxReCenter: () => {
6473
+ reCenter();
6474
+ setCtxMenu(null);
6475
+ },
6476
+ onCtxAddNode: () => {
6477
+ const rect = svgRef.current.getBoundingClientRect();
6478
+ const cx = (ctxMenu.x - rect.left - transform.x) / transform.scale;
6479
+ const cy = (ctxMenu.y - rect.top - transform.y) / transform.scale;
6480
+ addNode({ x: cx, y: cy });
6481
+ setCtxMenu(null);
6482
+ },
6483
+ onCtxDuplicate: () => {
6484
+ if (ctxMenu?.nodeId) {
6485
+ duplicateNode(ctxMenu.nodeId);
6486
+ setCtxMenu(null);
6487
+ }
6488
+ },
6489
+ onCtxRename: () => {
6490
+ if (ctxMenu?.nodeId) {
6491
+ const node = model.nodes.find((n) => n.id === ctxMenu.nodeId);
6492
+ setEditingId(ctxMenu.nodeId);
6493
+ setEditLabel(node.label);
6494
+ setCtxMenu(null);
6495
+ }
6496
+ },
6497
+ onCtxDelete: () => {
6498
+ if (ctxMenu?.nodeId) {
6499
+ deleteNode(ctxMenu.nodeId);
6500
+ setCtxMenu(null);
6501
+ }
6502
+ },
6503
+ onCtxDisconnect: () => {
6504
+ if (ctxMenu?.nodeId) {
6505
+ const m = {
6506
+ ...model,
6507
+ edges: model.edges.filter(
6508
+ (e) => e.from !== ctxMenu.nodeId && e.to !== ctxMenu.nodeId
6509
+ )
6510
+ };
6511
+ applyAndPush(m);
6512
+ setCtxMenu(null);
6513
+ }
6514
+ },
6515
+ onCtxEdgeRename: () => {
6516
+ if (ctxMenu?.edgeId) {
6517
+ beginEditEdge(ctxMenu.edgeId);
6518
+ setCtxMenu(null);
6519
+ }
6520
+ },
6521
+ onCtxEdgeStyle: (s2) => {
6522
+ if (ctxMenu?.edgeId) {
6523
+ setEdgeStyle(ctxMenu.edgeId, s2);
6524
+ setCtxMenu(null);
6525
+ }
6526
+ },
6527
+ onCtxEdgeArrowhead: (a) => {
6528
+ if (ctxMenu?.edgeId) {
6529
+ setEdgeArrowhead(ctxMenu.edgeId, a);
6530
+ setCtxMenu(null);
6531
+ }
6532
+ },
6533
+ onCtxEdgeDelete: () => {
6534
+ if (ctxMenu?.edgeId) {
6535
+ deleteEdge(ctxMenu.edgeId);
6536
+ setCtxMenu(null);
6537
+ }
6538
+ },
6539
+ onCtxEdgeResetRouting: () => {
6540
+ if (ctxMenu?.edgeId) {
6541
+ resetEdgeRouting(ctxMenu.edgeId);
6542
+ setCtxMenu(null);
6543
+ }
6544
+ }
4922
6545
  }
6546
+ ),
6547
+ selected && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
6548
+ StepEditor,
6549
+ {
6550
+ nodeId: selected,
6551
+ model,
6552
+ onModelChange: (m) => {
6553
+ applyAndPush(m);
6554
+ },
6555
+ variant,
6556
+ isDark,
6557
+ t,
6558
+ acc
6559
+ },
6560
+ selected
6561
+ )
6562
+ ] }),
6563
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
6564
+ "div",
6565
+ {
6566
+ style: {
6567
+ padding: "4px 14px",
6568
+ fontSize: 11,
6569
+ color: t.textMuted,
6570
+ background: t.statusBg,
6571
+ borderTop: `1px solid ${t.ctrlsBorder}`,
6572
+ display: "flex",
6573
+ gap: 16,
6574
+ flexWrap: "wrap",
6575
+ overflow: "hidden",
6576
+ maxHeight: 28
6577
+ },
6578
+ children: [
6579
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
6580
+ model.nodes.length,
6581
+ " ",
6582
+ variantLabel.toLowerCase(),
6583
+ "s"
6584
+ ] }),
6585
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
6586
+ model.edges.length,
6587
+ " connections"
6588
+ ] }),
6589
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
6590
+ Math.round(transform.scale * 100),
6591
+ "% zoom"
6592
+ ] }),
6593
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
6594
+ "span",
6595
+ {
6596
+ style: {
6597
+ marginLeft: "auto",
6598
+ whiteSpace: "nowrap",
6599
+ overflow: "hidden",
6600
+ textOverflow: "ellipsis"
6601
+ },
6602
+ children: "Ctrl+Z undo \xB7 Ctrl+Y redo \xB7 Ctrl+0 fit \xB7 Alt+Arrow traverse"
6603
+ }
6604
+ ),
6605
+ selected && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: { color: acc.color }, children: model.nodes.find((n) => n.id === selected)?.label })
6606
+ ]
4923
6607
  }
4924
- }
4925
- ),
4926
- selected && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(StepEditor, { nodeId: selected, model, onModelChange: (m) => {
4927
- applyAndPush(m);
4928
- }, variant, isDark, t, acc }, selected)
4929
- ] }),
4930
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { padding: "4px 14px", fontSize: 11, color: t.textMuted, background: t.statusBg, borderTop: `1px solid ${t.ctrlsBorder}`, display: "flex", gap: 16, flexWrap: "wrap", overflow: "hidden", maxHeight: 28 }, children: [
4931
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
4932
- model.nodes.length,
4933
- " ",
4934
- variantLabel.toLowerCase(),
4935
- "s"
4936
- ] }),
4937
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
4938
- model.edges.length,
4939
- " connections"
4940
- ] }),
4941
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
4942
- Math.round(transform.scale * 100),
4943
- "% zoom"
4944
- ] }),
4945
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: { marginLeft: "auto", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: "Ctrl+Z undo \xB7 Ctrl+Y redo \xB7 Ctrl+0 fit \xB7 Alt+Arrow traverse" }),
4946
- selected && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: { color: acc.color }, children: model.nodes.find((n) => n.id === selected)?.label })
4947
- ] })
4948
- ] });
6608
+ )
6609
+ ]
6610
+ }
6611
+ );
4949
6612
  }
4950
6613
  function ctrlBtn(accent, isDark) {
4951
6614
  const isTransparent = accent === "transparent";