flowchart-sequence-designer 1.1.0 → 1.2.1

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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/ui/DiagramEditor.tsx
2
- import { useCallback as useCallback8, useEffect as useEffect10, useRef as useRef7, useState as useState12 } from "react";
2
+ import { useCallback as useCallback7, useEffect as useEffect10, useRef as useRef7, useState as useState12 } from "react";
3
3
 
4
4
  // src/ui/Toolbar.tsx
5
5
  import { useState as useState2 } from "react";
@@ -383,12 +383,37 @@ function arrowColor(isDark) {
383
383
  }
384
384
  function variantAccent(variant, isDark) {
385
385
  if (variant === "question") {
386
- 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 };
386
+ return isDark ? {
387
+ color: ACCENT.amberDark,
388
+ fill: ACCENT.amberDarkLight,
389
+ border: ACCENT.amberDarkBorder,
390
+ glow: ACCENT.amberGlow
391
+ } : {
392
+ color: ACCENT.amber,
393
+ fill: ACCENT.amberLight,
394
+ border: ACCENT.amberBorder,
395
+ glow: ACCENT.amberGlow
396
+ };
387
397
  }
388
398
  if (variant === "journey") {
389
- 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 };
399
+ return isDark ? {
400
+ color: ACCENT.emeraldDark,
401
+ fill: ACCENT.emeraldDarkLight,
402
+ border: ACCENT.emeraldDarkBorder,
403
+ glow: ACCENT.emeraldGlow
404
+ } : {
405
+ color: ACCENT.emerald,
406
+ fill: ACCENT.emeraldLight,
407
+ border: "#6ee7b7",
408
+ glow: ACCENT.emeraldGlow
409
+ };
390
410
  }
391
- 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 };
411
+ return isDark ? {
412
+ color: "#818cf8",
413
+ fill: "rgba(79,70,229,0.12)",
414
+ border: "rgba(79,70,229,0.3)",
415
+ glow: ACCENT.indigoGlow
416
+ } : { color: ACCENT.indigo, fill: "#f5f3ff", border: "#c7d2fe", glow: ACCENT.indigoGlow };
392
417
  }
393
418
 
394
419
  // src/ui/Toolbar.tsx
@@ -415,17 +440,19 @@ function Toolbar({ onExport, onImport, allowedExports, allowImport = true }) {
415
440
  allowImport && onImport && /* @__PURE__ */ jsx2("button", { onClick: () => setImportOpen(true), "aria-label": "Import diagram", style: ghostBtn, children: "\u2191 Import" }),
416
441
  formats.length > 0 && /* @__PURE__ */ jsxs2(Fragment, { children: [
417
442
  /* @__PURE__ */ jsx2("span", { style: { fontSize: 11, color: darkTheme.inputText, margin: "0 4px" }, children: "Export \u2192" }),
418
- formats.map((f) => /* @__PURE__ */ jsx2("button", { onClick: () => onExport(f.key), "aria-label": `Export as ${f.label}`, style: exportBtn, children: f.label }, f.key))
443
+ formats.map((f) => /* @__PURE__ */ jsx2(
444
+ "button",
445
+ {
446
+ onClick: () => onExport(f.key),
447
+ "aria-label": `Export as ${f.label}`,
448
+ style: exportBtn,
449
+ children: f.label
450
+ },
451
+ f.key
452
+ ))
419
453
  ] })
420
454
  ] }),
421
- onImport && /* @__PURE__ */ jsx2(
422
- ImportDialog,
423
- {
424
- open: importOpen,
425
- onClose: () => setImportOpen(false),
426
- onImport
427
- }
428
- )
455
+ onImport && /* @__PURE__ */ jsx2(ImportDialog, { open: importOpen, onClose: () => setImportOpen(false), onImport })
429
456
  ] });
430
457
  }
431
458
  var bar = {
@@ -517,7 +544,15 @@ var SHAPES = [
517
544
  { key: "circle", label: "Circle", icon: "\u25CB" },
518
545
  { key: "parallelogram", label: "I/O", icon: "\u25B1" }
519
546
  ];
520
- function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDark = false, t, acc }) {
547
+ function StepEditor({
548
+ nodeId,
549
+ model,
550
+ onModelChange,
551
+ variant = "flowchart",
552
+ isDark = false,
553
+ t,
554
+ acc
555
+ }) {
521
556
  const isQuestion2 = variant === "question";
522
557
  const branchTerm = isQuestion2 ? "Answer" : "Branch";
523
558
  const tt = t ?? (isDark ? darkTheme : lightTheme);
@@ -551,27 +586,57 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
551
586
  const answers = node.metadata?.answers ?? [];
552
587
  const commitLabel = () => {
553
588
  if (label === node.label || !label.trim()) return;
554
- onModelChange({ ...model, nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, label: label.trim() } : n) });
589
+ onModelChange({
590
+ ...model,
591
+ nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, label: label.trim() } : n)
592
+ });
555
593
  };
556
594
  const setShape = (shape) => {
557
- onModelChange({ ...model, nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, shape } : n) });
595
+ onModelChange({
596
+ ...model,
597
+ nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, shape } : n)
598
+ });
558
599
  };
559
600
  const removeEdge = (edgeId) => {
560
601
  onModelChange({ ...model, edges: model.edges.filter((e) => e.id !== edgeId) });
561
602
  };
562
603
  const updateEdgeLabel = (edgeId, val) => {
563
- onModelChange({ ...model, edges: model.edges.map((e) => e.id === edgeId ? { ...e, label: val || void 0 } : e) });
604
+ onModelChange({
605
+ ...model,
606
+ edges: model.edges.map((e) => e.id === edgeId ? { ...e, label: val || void 0 } : e)
607
+ });
564
608
  };
565
609
  const addBranch = () => {
566
610
  if (branchMode === "new") {
567
611
  if (!branchLabel.trim()) return;
568
612
  const newId = nextId("node", model.nodes);
569
- const newNode = { id: newId, label: branchLabel.trim(), shape: "rectangle", x: (node.x ?? 0) + 200, y: (node.y ?? 0) + 20 + outEdges.length * 100 };
570
- const newEdge = { id: nextId("e", model.edges), from: nodeId, to: newId, label: branchEdgeLabel.trim() || void 0 };
571
- onModelChange({ ...model, nodes: [...model.nodes, newNode], edges: [...model.edges, newEdge] });
613
+ const newNode = {
614
+ id: newId,
615
+ label: branchLabel.trim(),
616
+ shape: "rectangle",
617
+ x: (node.x ?? 0) + 200,
618
+ y: (node.y ?? 0) + 20 + outEdges.length * 100
619
+ };
620
+ const newEdge = {
621
+ id: nextId("e", model.edges),
622
+ from: nodeId,
623
+ to: newId,
624
+ label: branchEdgeLabel.trim() || void 0
625
+ };
626
+ onModelChange({
627
+ ...model,
628
+ nodes: [...model.nodes, newNode],
629
+ edges: [...model.edges, newEdge]
630
+ });
572
631
  } else {
573
- if (!branchTarget || model.edges.some((e) => e.from === nodeId && e.to === branchTarget)) return;
574
- const newEdge = { id: nextId("e", model.edges), from: nodeId, to: branchTarget, label: branchEdgeLabel.trim() || void 0 };
632
+ if (!branchTarget || model.edges.some((e) => e.from === nodeId && e.to === branchTarget))
633
+ return;
634
+ const newEdge = {
635
+ id: nextId("e", model.edges),
636
+ from: nodeId,
637
+ to: branchTarget,
638
+ label: branchEdgeLabel.trim() || void 0
639
+ };
575
640
  onModelChange({ ...model, edges: [...model.edges, newEdge] });
576
641
  }
577
642
  setBranchLabel("");
@@ -583,7 +648,12 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
583
648
  const trimmed = newAnswer.trim();
584
649
  if (!trimmed || answers.includes(trimmed)) return;
585
650
  const updated = [...answers, trimmed];
586
- onModelChange({ ...model, nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, metadata: { ...n.metadata ?? {}, answers: updated } } : n) });
651
+ onModelChange({
652
+ ...model,
653
+ nodes: model.nodes.map(
654
+ (n) => n.id === nodeId ? { ...n, metadata: { ...n.metadata ?? {}, answers: updated } } : n
655
+ )
656
+ });
587
657
  setNewAnswer("");
588
658
  setAddingAnswer(false);
589
659
  };
@@ -592,7 +662,9 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
592
662
  const updatedEdges = model.edges.filter((e) => !(e.from === nodeId && e.label === ans));
593
663
  onModelChange({
594
664
  ...model,
595
- nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, metadata: { ...n.metadata ?? {}, answers: updated } } : n),
665
+ nodes: model.nodes.map(
666
+ (n) => n.id === nodeId ? { ...n, metadata: { ...n.metadata ?? {}, answers: updated } } : n
667
+ ),
596
668
  edges: updatedEdges
597
669
  });
598
670
  };
@@ -601,7 +673,12 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
601
673
  if (next < 0 || next >= answers.length) return;
602
674
  const arr = [...answers];
603
675
  [arr[idx], arr[next]] = [arr[next], arr[idx]];
604
- onModelChange({ ...model, nodes: model.nodes.map((n) => n.id === nodeId ? { ...n, metadata: { ...n.metadata ?? {}, answers: arr } } : n) });
676
+ onModelChange({
677
+ ...model,
678
+ nodes: model.nodes.map(
679
+ (n) => n.id === nodeId ? { ...n, metadata: { ...n.metadata ?? {}, answers: arr } } : n
680
+ )
681
+ });
605
682
  };
606
683
  const inputStyle = {
607
684
  width: "100%",
@@ -656,167 +733,600 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
656
733
  fontFamily: "inherit",
657
734
  transition: "background 0.15s, border-color 0.15s"
658
735
  };
659
- return /* @__PURE__ */ jsxs3("div", { style: { width: 272, minWidth: 272, background: tt.panelBg, borderLeft: `1px solid ${tt.panelBorder}`, display: "flex", flexDirection: "column", overflow: "hidden" }, children: [
660
- /* @__PURE__ */ jsxs3("div", { style: {
661
- padding: "12px 16px",
662
- fontWeight: 700,
663
- fontSize: 12,
664
- letterSpacing: 0.8,
665
- textTransform: "uppercase",
666
- color: accentColor,
667
- borderBottom: `1px solid ${accentBorder}`,
668
- background: accentLight,
669
- display: "flex",
670
- alignItems: "center",
671
- gap: 8
672
- }, children: [
673
- /* @__PURE__ */ jsx3("div", { style: { width: 6, height: 6, borderRadius: "50%", background: accentColor } }),
674
- /* @__PURE__ */ jsx3("span", { children: isQuestion2 ? "Question Editor" : variant === "journey" ? "Step Editor" : "Step Editor" })
675
- ] }),
676
- /* @__PURE__ */ jsxs3("div", { style: { flex: 1, overflowY: "auto", display: "flex", flexDirection: "column" }, children: [
677
- /* @__PURE__ */ jsxs3("section", { style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}` }, children: [
678
- /* @__PURE__ */ jsx3("label", { style: { display: "block", fontSize: 10, fontWeight: 700, color: tt.labelText, marginBottom: 8, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Name" }),
679
- /* @__PURE__ */ jsx3(
680
- "input",
736
+ return /* @__PURE__ */ jsxs3(
737
+ "div",
738
+ {
739
+ style: {
740
+ width: 272,
741
+ minWidth: 272,
742
+ background: tt.panelBg,
743
+ borderLeft: `1px solid ${tt.panelBorder}`,
744
+ display: "flex",
745
+ flexDirection: "column",
746
+ overflow: "hidden"
747
+ },
748
+ children: [
749
+ /* @__PURE__ */ jsxs3(
750
+ "div",
681
751
  {
682
- ref: inputRef,
683
- value: label,
684
- onChange: (e) => setLabel(e.target.value),
685
- onBlur: commitLabel,
686
- onKeyDown: (e) => e.key === "Enter" && commitLabel(),
687
- style: inputStyle,
688
- placeholder: "Step name\u2026"
689
- }
690
- )
691
- ] }),
692
- !isQuestion2 && /* @__PURE__ */ jsxs3("section", { style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}` }, children: [
693
- /* @__PURE__ */ jsx3("label", { style: { display: "block", fontSize: 10, fontWeight: 700, color: tt.labelText, marginBottom: 8, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Shape" }),
694
- /* @__PURE__ */ jsx3("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6 }, children: SHAPES.map((s2) => {
695
- const active = (node.shape ?? "rectangle") === s2.key;
696
- return /* @__PURE__ */ jsxs3("button", { onClick: () => setShape(s2.key), "aria-pressed": active, style: {
697
- display: "flex",
698
- flexDirection: "column",
699
- alignItems: "center",
700
- gap: 4,
701
- padding: "8px 6px",
702
- borderRadius: 8,
703
- cursor: "pointer",
704
- transition: "all 0.15s",
705
- background: active ? accentColor : tt.shapeBtnBg,
706
- color: active ? "#fff" : tt.textSecondary,
707
- border: active ? `1.5px solid ${accentColor}` : `1.5px solid ${tt.shapeBtnBorder}`
708
- }, children: [
709
- /* @__PURE__ */ jsx3("span", { style: { fontSize: 16, lineHeight: 1 }, children: s2.icon }),
710
- /* @__PURE__ */ jsx3("span", { style: { fontSize: 11, fontWeight: 500 }, children: s2.label })
711
- ] }, s2.key);
712
- }) })
713
- ] }),
714
- isQuestion2 && /* @__PURE__ */ jsxs3("section", { style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}`, flex: 1 }, children: [
715
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 10 }, children: [
716
- /* @__PURE__ */ jsx3("label", { style: { display: "block", fontSize: 10, fontWeight: 700, color: tt.labelText, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Answers" }),
717
- /* @__PURE__ */ jsx3("span", { style: { fontSize: 11, color: tt.textMuted, background: isDark ? "#0f172a" : "#f1f5f9", padding: "1px 7px", borderRadius: 99, fontWeight: 600 }, children: answers.length })
718
- ] }),
719
- answers.length === 0 && !addingAnswer && /* @__PURE__ */ jsx3("div", { style: { fontSize: 12, color: tt.textMuted, textAlign: "center", padding: "16px 0", fontStyle: "italic" }, children: "No answers yet \u2014 add one below" }),
720
- answers.map((ans, i) => {
721
- const connected = model.edges.some((e) => e.from === nodeId && e.label === ans);
722
- const targetNode = model.nodes.find((n) => {
723
- const e = model.edges.find((ex) => ex.from === nodeId && ex.label === ans);
724
- return e && n.id === e.to;
725
- });
726
- return /* @__PURE__ */ jsxs3("div", { style: { display: "flex", alignItems: "flex-start", gap: 0, marginBottom: 8, borderRadius: 12, border: `1px solid ${tt.cardBorder}`, overflow: "hidden", background: tt.cardBg, boxShadow: isDark ? "none" : "0 1px 2px rgba(15,23,42,0.04)" }, children: [
727
- /* @__PURE__ */ jsx3("div", { style: { width: 4, alignSelf: "stretch", background: accentColor, flexShrink: 0 } }),
728
- /* @__PURE__ */ jsxs3("div", { style: { flex: 1, minWidth: 0, padding: "8px 10px" }, children: [
729
- /* @__PURE__ */ jsx3("div", { style: { fontSize: 12, fontWeight: 600, color: tt.textPrimary, marginBottom: connected ? 3 : 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: ans }),
730
- connected && targetNode && /* @__PURE__ */ jsxs3("div", { style: { fontSize: 11, color: accentColor, opacity: 0.85 }, children: [
731
- "\u2192 ",
732
- targetNode.label
733
- ] }),
734
- !connected && /* @__PURE__ */ jsx3("div", { style: { fontSize: 10, color: tt.textMuted, fontStyle: "italic" }, children: "drag port to connect" })
735
- ] }),
736
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", padding: "4px 2px", gap: 2 }, children: [
737
- /* @__PURE__ */ jsx3("button", { onClick: () => moveAnswer(i, -1), disabled: i === 0, style: { background: "none", border: "none", color: tt.textMuted, cursor: "pointer", fontSize: 11, padding: "2px 4px", opacity: i === 0 ? 0.3 : 1 }, children: "\u2191" }),
738
- /* @__PURE__ */ jsx3("button", { onClick: () => moveAnswer(i, 1), disabled: i === answers.length - 1, style: { background: "none", border: "none", color: tt.textMuted, cursor: "pointer", fontSize: 11, padding: "2px 4px", opacity: i === answers.length - 1 ? 0.3 : 1 }, children: "\u2193" })
739
- ] }),
740
- /* @__PURE__ */ jsx3("button", { onClick: () => removeAnswer(ans), style: { background: "none", border: "none", color: tt.textMuted, cursor: "pointer", fontSize: 12, padding: "8px 10px", flexShrink: 0 }, title: "Remove", children: "\u2715" })
741
- ] }, ans + i);
742
- }),
743
- addingAnswer ? /* @__PURE__ */ jsxs3("div", { role: "group", "aria-label": "Add answer form", onKeyDown: (e) => {
744
- if (e.key === "Escape") {
745
- setAddingAnswer(false);
746
- setNewAnswer("");
752
+ style: {
753
+ padding: "12px 16px",
754
+ fontWeight: 700,
755
+ fontSize: 12,
756
+ letterSpacing: 0.8,
757
+ textTransform: "uppercase",
758
+ color: accentColor,
759
+ borderBottom: `1px solid ${accentBorder}`,
760
+ background: accentLight,
761
+ display: "flex",
762
+ alignItems: "center",
763
+ gap: 8
764
+ },
765
+ children: [
766
+ /* @__PURE__ */ jsx3("div", { style: { width: 6, height: 6, borderRadius: "50%", background: accentColor } }),
767
+ /* @__PURE__ */ jsx3("span", { children: isQuestion2 ? "Question Editor" : variant === "journey" ? "Step Editor" : "Step Editor" })
768
+ ]
747
769
  }
748
- }, style: { marginTop: 10, background: tt.addFormBg, borderRadius: 10, padding: 12, border: `1.5px solid ${accentBorder}` }, children: [
749
- /* @__PURE__ */ jsx3("input", { autoFocus: true, value: newAnswer, onChange: (e) => setNewAnswer(e.target.value), onKeyDown: (e) => e.key === "Enter" && addAnswer(), placeholder: "Answer text\u2026", style: { ...inputStyle, marginBottom: 8 } }),
750
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 6 }, children: [
751
- /* @__PURE__ */ jsx3("button", { onClick: addAnswer, style: addBtnStyle, children: "Add Answer" }),
752
- /* @__PURE__ */ jsx3("button", { onClick: () => {
753
- setAddingAnswer(false);
754
- setNewAnswer("");
755
- }, style: cancelBtnStyle, children: "Cancel" })
756
- ] })
757
- ] }) : /* @__PURE__ */ jsxs3("button", { onClick: () => setAddingAnswer(true), style: addTriggerStyle, children: [
758
- /* @__PURE__ */ jsx3("span", { style: { fontSize: 16, lineHeight: 1 }, children: "+" }),
759
- " Add Answer"
760
- ] }),
761
- answers.length > 0 && /* @__PURE__ */ jsxs3("div", { style: { marginTop: 12, padding: "8px 10px", background: isDark ? "rgba(251,191,36,0.06)" : "#fef9f0", borderRadius: 8, border: `1px solid ${accentBorder}` }, children: [
762
- /* @__PURE__ */ jsx3("div", { style: { fontSize: 10, fontWeight: 700, color: accentColor, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 4 }, children: "How to connect" }),
763
- /* @__PURE__ */ jsx3("div", { style: { fontSize: 11, color: tt.textSecondary, lineHeight: 1.5 }, children: "Hover the question node on the canvas \u2014 drag an answer's port dot to any other node." })
764
- ] })
765
- ] }),
766
- !isQuestion2 && /* @__PURE__ */ jsxs3("section", { style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}`, flex: 1 }, children: [
767
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 10 }, children: [
768
- /* @__PURE__ */ jsx3("label", { style: { display: "block", fontSize: 10, fontWeight: 700, color: tt.labelText, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Branches" }),
769
- /* @__PURE__ */ jsx3("span", { style: { fontSize: 11, color: tt.textMuted, background: isDark ? "#0f172a" : "#f1f5f9", padding: "1px 7px", borderRadius: 99, fontWeight: 600 }, children: outEdges.length })
770
- ] }),
771
- outEdges.length === 0 && !addingBranch && /* @__PURE__ */ jsx3("div", { style: { fontSize: 12, color: tt.textMuted, textAlign: "center", padding: "16px 0", fontStyle: "italic" }, children: "No outgoing connections yet" }),
772
- outEdges.map((edge) => {
773
- const target = model.nodes.find((n) => n.id === edge.to);
774
- return /* @__PURE__ */ jsxs3("div", { style: { display: "flex", alignItems: "flex-start", gap: 0, marginBottom: 8, borderRadius: 12, border: `1px solid ${tt.cardBorder}`, overflow: "hidden", background: tt.cardBg, boxShadow: isDark ? "none" : "0 1px 2px rgba(15,23,42,0.04)" }, children: [
775
- /* @__PURE__ */ jsx3("div", { style: { width: 4, alignSelf: "stretch", background: accentColor, flexShrink: 0 } }),
776
- /* @__PURE__ */ jsxs3("div", { style: { flex: 1, minWidth: 0, padding: "8px 10px" }, children: [
777
- /* @__PURE__ */ jsxs3("div", { style: { fontSize: 12, fontWeight: 600, color: tt.textPrimary, marginBottom: 5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [
778
- "\u2192 ",
779
- target?.label ?? edge.to
780
- ] }),
781
- /* @__PURE__ */ jsx3("input", { value: edge.label ?? "", onChange: (e) => updateEdgeLabel(edge.id, e.target.value), placeholder: "Edge label (optional)", style: { ...inputStyle, fontSize: 11, padding: "4px 8px" } })
782
- ] }),
783
- /* @__PURE__ */ jsx3("button", { onClick: () => removeEdge(edge.id), style: { background: "none", border: "none", color: tt.textMuted, cursor: "pointer", fontSize: 12, padding: "8px 10px", flexShrink: 0 }, title: "Remove", children: "\u2715" })
784
- ] }, edge.id);
785
- }),
786
- addingBranch ? /* @__PURE__ */ jsxs3("div", { role: "group", "aria-label": "Add branch form", onKeyDown: (e) => {
787
- if (e.key === "Escape") setAddingBranch(false);
788
- }, style: { marginTop: 10, background: tt.addFormBg, borderRadius: 10, padding: 12, border: `1.5px solid ${accentBorder}` }, children: [
789
- /* @__PURE__ */ jsx3("div", { style: { display: "flex", gap: 6, marginBottom: 10 }, children: ["new", "existing"].map((mode) => /* @__PURE__ */ jsx3("button", { onClick: () => setBranchMode(mode), style: {
790
- flex: 1,
791
- padding: "5px 0",
792
- border: "none",
793
- borderRadius: 6,
794
- cursor: "pointer",
795
- fontSize: 11,
796
- fontWeight: 600,
797
- background: branchMode === mode ? accentColor : tt.btnSecBg,
798
- color: branchMode === mode ? "#fff" : tt.btnSecText
799
- }, children: mode === "new" ? `+ New step` : "Existing step" }, mode)) }),
800
- branchMode === "new" ? /* @__PURE__ */ jsx3("input", { autoFocus: true, value: branchLabel, onChange: (e) => setBranchLabel(e.target.value), onKeyDown: (e) => e.key === "Enter" && addBranch(), placeholder: "New step name\u2026", style: { ...inputStyle, marginBottom: 6 } }) : /* @__PURE__ */ jsxs3("select", { value: branchTarget, onChange: (e) => setBranchTarget(e.target.value), style: { ...inputStyle, marginBottom: 6, appearance: "none" }, children: [
801
- /* @__PURE__ */ jsx3("option", { value: "", children: "Choose a step\u2026" }),
802
- otherNodes.map((n) => /* @__PURE__ */ jsx3("option", { value: n.id, children: n.label }, n.id))
770
+ ),
771
+ /* @__PURE__ */ jsxs3("div", { style: { flex: 1, overflowY: "auto", display: "flex", flexDirection: "column" }, children: [
772
+ /* @__PURE__ */ jsxs3("section", { style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}` }, children: [
773
+ /* @__PURE__ */ jsx3(
774
+ "label",
775
+ {
776
+ style: {
777
+ display: "block",
778
+ fontSize: 10,
779
+ fontWeight: 700,
780
+ color: tt.labelText,
781
+ marginBottom: 8,
782
+ textTransform: "uppercase",
783
+ letterSpacing: 0.8
784
+ },
785
+ children: "Name"
786
+ }
787
+ ),
788
+ /* @__PURE__ */ jsx3(
789
+ "input",
790
+ {
791
+ ref: inputRef,
792
+ value: label,
793
+ onChange: (e) => setLabel(e.target.value),
794
+ onBlur: commitLabel,
795
+ onKeyDown: (e) => e.key === "Enter" && commitLabel(),
796
+ style: inputStyle,
797
+ placeholder: "Step name\u2026"
798
+ }
799
+ )
803
800
  ] }),
804
- /* @__PURE__ */ jsx3("input", { value: branchEdgeLabel, onChange: (e) => setBranchEdgeLabel(e.target.value), placeholder: "Edge label (optional)", style: { ...inputStyle, marginBottom: 10 } }),
805
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 6 }, children: [
806
- /* @__PURE__ */ jsxs3("button", { onClick: addBranch, style: addBtnStyle, children: [
807
- "Add ",
808
- branchTerm
809
- ] }),
810
- /* @__PURE__ */ jsx3("button", { onClick: () => setAddingBranch(false), style: cancelBtnStyle, children: "Cancel" })
811
- ] })
812
- ] }) : /* @__PURE__ */ jsxs3("button", { onClick: () => setAddingBranch(true), style: addTriggerStyle, children: [
813
- /* @__PURE__ */ jsx3("span", { style: { fontSize: 16, lineHeight: 1 }, children: "+" }),
814
- " Add ",
815
- branchTerm
801
+ !isQuestion2 && /* @__PURE__ */ jsxs3("section", { style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}` }, children: [
802
+ /* @__PURE__ */ jsx3(
803
+ "label",
804
+ {
805
+ style: {
806
+ display: "block",
807
+ fontSize: 10,
808
+ fontWeight: 700,
809
+ color: tt.labelText,
810
+ marginBottom: 8,
811
+ textTransform: "uppercase",
812
+ letterSpacing: 0.8
813
+ },
814
+ children: "Shape"
815
+ }
816
+ ),
817
+ /* @__PURE__ */ jsx3("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6 }, children: SHAPES.map((s2) => {
818
+ const active = (node.shape ?? "rectangle") === s2.key;
819
+ return /* @__PURE__ */ jsxs3(
820
+ "button",
821
+ {
822
+ onClick: () => setShape(s2.key),
823
+ "aria-pressed": active,
824
+ style: {
825
+ display: "flex",
826
+ flexDirection: "column",
827
+ alignItems: "center",
828
+ gap: 4,
829
+ padding: "8px 6px",
830
+ borderRadius: 8,
831
+ cursor: "pointer",
832
+ transition: "all 0.15s",
833
+ background: active ? accentColor : tt.shapeBtnBg,
834
+ color: active ? "#fff" : tt.textSecondary,
835
+ border: active ? `1.5px solid ${accentColor}` : `1.5px solid ${tt.shapeBtnBorder}`
836
+ },
837
+ children: [
838
+ /* @__PURE__ */ jsx3("span", { style: { fontSize: 16, lineHeight: 1 }, children: s2.icon }),
839
+ /* @__PURE__ */ jsx3("span", { style: { fontSize: 11, fontWeight: 500 }, children: s2.label })
840
+ ]
841
+ },
842
+ s2.key
843
+ );
844
+ }) })
845
+ ] }),
846
+ isQuestion2 && /* @__PURE__ */ jsxs3(
847
+ "section",
848
+ {
849
+ style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}`, flex: 1 },
850
+ children: [
851
+ /* @__PURE__ */ jsxs3(
852
+ "div",
853
+ {
854
+ style: {
855
+ display: "flex",
856
+ alignItems: "center",
857
+ justifyContent: "space-between",
858
+ marginBottom: 10
859
+ },
860
+ children: [
861
+ /* @__PURE__ */ jsx3(
862
+ "label",
863
+ {
864
+ style: {
865
+ display: "block",
866
+ fontSize: 10,
867
+ fontWeight: 700,
868
+ color: tt.labelText,
869
+ textTransform: "uppercase",
870
+ letterSpacing: 0.8
871
+ },
872
+ children: "Answers"
873
+ }
874
+ ),
875
+ /* @__PURE__ */ jsx3(
876
+ "span",
877
+ {
878
+ style: {
879
+ fontSize: 11,
880
+ color: tt.textMuted,
881
+ background: isDark ? "#0f172a" : "#f1f5f9",
882
+ padding: "1px 7px",
883
+ borderRadius: 99,
884
+ fontWeight: 600
885
+ },
886
+ children: answers.length
887
+ }
888
+ )
889
+ ]
890
+ }
891
+ ),
892
+ answers.length === 0 && !addingAnswer && /* @__PURE__ */ jsx3(
893
+ "div",
894
+ {
895
+ style: {
896
+ fontSize: 12,
897
+ color: tt.textMuted,
898
+ textAlign: "center",
899
+ padding: "16px 0",
900
+ fontStyle: "italic"
901
+ },
902
+ children: "No answers yet \u2014 add one below"
903
+ }
904
+ ),
905
+ answers.map((ans, i) => {
906
+ const connected = model.edges.some((e) => e.from === nodeId && e.label === ans);
907
+ const targetNode = model.nodes.find((n) => {
908
+ const e = model.edges.find((ex) => ex.from === nodeId && ex.label === ans);
909
+ return e && n.id === e.to;
910
+ });
911
+ return /* @__PURE__ */ jsxs3(
912
+ "div",
913
+ {
914
+ style: {
915
+ display: "flex",
916
+ alignItems: "flex-start",
917
+ gap: 0,
918
+ marginBottom: 8,
919
+ borderRadius: 12,
920
+ border: `1px solid ${tt.cardBorder}`,
921
+ overflow: "hidden",
922
+ background: tt.cardBg,
923
+ boxShadow: isDark ? "none" : "0 1px 2px rgba(15,23,42,0.04)"
924
+ },
925
+ children: [
926
+ /* @__PURE__ */ jsx3(
927
+ "div",
928
+ {
929
+ style: {
930
+ width: 4,
931
+ alignSelf: "stretch",
932
+ background: accentColor,
933
+ flexShrink: 0
934
+ }
935
+ }
936
+ ),
937
+ /* @__PURE__ */ jsxs3("div", { style: { flex: 1, minWidth: 0, padding: "8px 10px" }, children: [
938
+ /* @__PURE__ */ jsx3(
939
+ "div",
940
+ {
941
+ style: {
942
+ fontSize: 12,
943
+ fontWeight: 600,
944
+ color: tt.textPrimary,
945
+ marginBottom: connected ? 3 : 0,
946
+ overflow: "hidden",
947
+ textOverflow: "ellipsis",
948
+ whiteSpace: "nowrap"
949
+ },
950
+ children: ans
951
+ }
952
+ ),
953
+ connected && targetNode && /* @__PURE__ */ jsxs3("div", { style: { fontSize: 11, color: accentColor, opacity: 0.85 }, children: [
954
+ "\u2192 ",
955
+ targetNode.label
956
+ ] }),
957
+ !connected && /* @__PURE__ */ jsx3("div", { style: { fontSize: 10, color: tt.textMuted, fontStyle: "italic" }, children: "drag port to connect" })
958
+ ] }),
959
+ /* @__PURE__ */ jsxs3(
960
+ "div",
961
+ {
962
+ style: { display: "flex", flexDirection: "column", padding: "4px 2px", gap: 2 },
963
+ children: [
964
+ /* @__PURE__ */ jsx3(
965
+ "button",
966
+ {
967
+ onClick: () => moveAnswer(i, -1),
968
+ disabled: i === 0,
969
+ style: {
970
+ background: "none",
971
+ border: "none",
972
+ color: tt.textMuted,
973
+ cursor: "pointer",
974
+ fontSize: 11,
975
+ padding: "2px 4px",
976
+ opacity: i === 0 ? 0.3 : 1
977
+ },
978
+ children: "\u2191"
979
+ }
980
+ ),
981
+ /* @__PURE__ */ jsx3(
982
+ "button",
983
+ {
984
+ onClick: () => moveAnswer(i, 1),
985
+ disabled: i === answers.length - 1,
986
+ style: {
987
+ background: "none",
988
+ border: "none",
989
+ color: tt.textMuted,
990
+ cursor: "pointer",
991
+ fontSize: 11,
992
+ padding: "2px 4px",
993
+ opacity: i === answers.length - 1 ? 0.3 : 1
994
+ },
995
+ children: "\u2193"
996
+ }
997
+ )
998
+ ]
999
+ }
1000
+ ),
1001
+ /* @__PURE__ */ jsx3(
1002
+ "button",
1003
+ {
1004
+ onClick: () => removeAnswer(ans),
1005
+ style: {
1006
+ background: "none",
1007
+ border: "none",
1008
+ color: tt.textMuted,
1009
+ cursor: "pointer",
1010
+ fontSize: 12,
1011
+ padding: "8px 10px",
1012
+ flexShrink: 0
1013
+ },
1014
+ title: "Remove",
1015
+ children: "\u2715"
1016
+ }
1017
+ )
1018
+ ]
1019
+ },
1020
+ ans + i
1021
+ );
1022
+ }),
1023
+ addingAnswer ? /* @__PURE__ */ jsxs3(
1024
+ "div",
1025
+ {
1026
+ role: "group",
1027
+ "aria-label": "Add answer form",
1028
+ onKeyDown: (e) => {
1029
+ if (e.key === "Escape") {
1030
+ setAddingAnswer(false);
1031
+ setNewAnswer("");
1032
+ }
1033
+ },
1034
+ style: {
1035
+ marginTop: 10,
1036
+ background: tt.addFormBg,
1037
+ borderRadius: 10,
1038
+ padding: 12,
1039
+ border: `1.5px solid ${accentBorder}`
1040
+ },
1041
+ children: [
1042
+ /* @__PURE__ */ jsx3(
1043
+ "input",
1044
+ {
1045
+ autoFocus: true,
1046
+ value: newAnswer,
1047
+ onChange: (e) => setNewAnswer(e.target.value),
1048
+ onKeyDown: (e) => e.key === "Enter" && addAnswer(),
1049
+ placeholder: "Answer text\u2026",
1050
+ style: { ...inputStyle, marginBottom: 8 }
1051
+ }
1052
+ ),
1053
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 6 }, children: [
1054
+ /* @__PURE__ */ jsx3("button", { onClick: addAnswer, style: addBtnStyle, children: "Add Answer" }),
1055
+ /* @__PURE__ */ jsx3(
1056
+ "button",
1057
+ {
1058
+ onClick: () => {
1059
+ setAddingAnswer(false);
1060
+ setNewAnswer("");
1061
+ },
1062
+ style: cancelBtnStyle,
1063
+ children: "Cancel"
1064
+ }
1065
+ )
1066
+ ] })
1067
+ ]
1068
+ }
1069
+ ) : /* @__PURE__ */ jsxs3("button", { onClick: () => setAddingAnswer(true), style: addTriggerStyle, children: [
1070
+ /* @__PURE__ */ jsx3("span", { style: { fontSize: 16, lineHeight: 1 }, children: "+" }),
1071
+ " Add Answer"
1072
+ ] }),
1073
+ answers.length > 0 && /* @__PURE__ */ jsxs3(
1074
+ "div",
1075
+ {
1076
+ style: {
1077
+ marginTop: 12,
1078
+ padding: "8px 10px",
1079
+ background: isDark ? "rgba(251,191,36,0.06)" : "#fef9f0",
1080
+ borderRadius: 8,
1081
+ border: `1px solid ${accentBorder}`
1082
+ },
1083
+ children: [
1084
+ /* @__PURE__ */ jsx3(
1085
+ "div",
1086
+ {
1087
+ style: {
1088
+ fontSize: 10,
1089
+ fontWeight: 700,
1090
+ color: accentColor,
1091
+ textTransform: "uppercase",
1092
+ letterSpacing: 0.6,
1093
+ marginBottom: 4
1094
+ },
1095
+ children: "How to connect"
1096
+ }
1097
+ ),
1098
+ /* @__PURE__ */ jsx3("div", { style: { fontSize: 11, color: tt.textSecondary, lineHeight: 1.5 }, children: "Hover the question node on the canvas \u2014 drag an answer's port dot to any other node." })
1099
+ ]
1100
+ }
1101
+ )
1102
+ ]
1103
+ }
1104
+ ),
1105
+ !isQuestion2 && /* @__PURE__ */ jsxs3(
1106
+ "section",
1107
+ {
1108
+ style: { padding: "14px 16px", borderBottom: `1px solid ${tt.sectionBorder}`, flex: 1 },
1109
+ children: [
1110
+ /* @__PURE__ */ jsxs3(
1111
+ "div",
1112
+ {
1113
+ style: {
1114
+ display: "flex",
1115
+ alignItems: "center",
1116
+ justifyContent: "space-between",
1117
+ marginBottom: 10
1118
+ },
1119
+ children: [
1120
+ /* @__PURE__ */ jsx3(
1121
+ "label",
1122
+ {
1123
+ style: {
1124
+ display: "block",
1125
+ fontSize: 10,
1126
+ fontWeight: 700,
1127
+ color: tt.labelText,
1128
+ textTransform: "uppercase",
1129
+ letterSpacing: 0.8
1130
+ },
1131
+ children: "Branches"
1132
+ }
1133
+ ),
1134
+ /* @__PURE__ */ jsx3(
1135
+ "span",
1136
+ {
1137
+ style: {
1138
+ fontSize: 11,
1139
+ color: tt.textMuted,
1140
+ background: isDark ? "#0f172a" : "#f1f5f9",
1141
+ padding: "1px 7px",
1142
+ borderRadius: 99,
1143
+ fontWeight: 600
1144
+ },
1145
+ children: outEdges.length
1146
+ }
1147
+ )
1148
+ ]
1149
+ }
1150
+ ),
1151
+ outEdges.length === 0 && !addingBranch && /* @__PURE__ */ jsx3(
1152
+ "div",
1153
+ {
1154
+ style: {
1155
+ fontSize: 12,
1156
+ color: tt.textMuted,
1157
+ textAlign: "center",
1158
+ padding: "16px 0",
1159
+ fontStyle: "italic"
1160
+ },
1161
+ children: "No outgoing connections yet"
1162
+ }
1163
+ ),
1164
+ outEdges.map((edge) => {
1165
+ const target = model.nodes.find((n) => n.id === edge.to);
1166
+ return /* @__PURE__ */ jsxs3(
1167
+ "div",
1168
+ {
1169
+ style: {
1170
+ display: "flex",
1171
+ alignItems: "flex-start",
1172
+ gap: 0,
1173
+ marginBottom: 8,
1174
+ borderRadius: 12,
1175
+ border: `1px solid ${tt.cardBorder}`,
1176
+ overflow: "hidden",
1177
+ background: tt.cardBg,
1178
+ boxShadow: isDark ? "none" : "0 1px 2px rgba(15,23,42,0.04)"
1179
+ },
1180
+ children: [
1181
+ /* @__PURE__ */ jsx3(
1182
+ "div",
1183
+ {
1184
+ style: {
1185
+ width: 4,
1186
+ alignSelf: "stretch",
1187
+ background: accentColor,
1188
+ flexShrink: 0
1189
+ }
1190
+ }
1191
+ ),
1192
+ /* @__PURE__ */ jsxs3("div", { style: { flex: 1, minWidth: 0, padding: "8px 10px" }, children: [
1193
+ /* @__PURE__ */ jsxs3(
1194
+ "div",
1195
+ {
1196
+ style: {
1197
+ fontSize: 12,
1198
+ fontWeight: 600,
1199
+ color: tt.textPrimary,
1200
+ marginBottom: 5,
1201
+ overflow: "hidden",
1202
+ textOverflow: "ellipsis",
1203
+ whiteSpace: "nowrap"
1204
+ },
1205
+ children: [
1206
+ "\u2192 ",
1207
+ target?.label ?? edge.to
1208
+ ]
1209
+ }
1210
+ ),
1211
+ /* @__PURE__ */ jsx3(
1212
+ "input",
1213
+ {
1214
+ value: edge.label ?? "",
1215
+ onChange: (e) => updateEdgeLabel(edge.id, e.target.value),
1216
+ placeholder: "Edge label (optional)",
1217
+ style: { ...inputStyle, fontSize: 11, padding: "4px 8px" }
1218
+ }
1219
+ )
1220
+ ] }),
1221
+ /* @__PURE__ */ jsx3(
1222
+ "button",
1223
+ {
1224
+ onClick: () => removeEdge(edge.id),
1225
+ style: {
1226
+ background: "none",
1227
+ border: "none",
1228
+ color: tt.textMuted,
1229
+ cursor: "pointer",
1230
+ fontSize: 12,
1231
+ padding: "8px 10px",
1232
+ flexShrink: 0
1233
+ },
1234
+ title: "Remove",
1235
+ children: "\u2715"
1236
+ }
1237
+ )
1238
+ ]
1239
+ },
1240
+ edge.id
1241
+ );
1242
+ }),
1243
+ addingBranch ? /* @__PURE__ */ jsxs3(
1244
+ "div",
1245
+ {
1246
+ role: "group",
1247
+ "aria-label": "Add branch form",
1248
+ onKeyDown: (e) => {
1249
+ if (e.key === "Escape") setAddingBranch(false);
1250
+ },
1251
+ style: {
1252
+ marginTop: 10,
1253
+ background: tt.addFormBg,
1254
+ borderRadius: 10,
1255
+ padding: 12,
1256
+ border: `1.5px solid ${accentBorder}`
1257
+ },
1258
+ children: [
1259
+ /* @__PURE__ */ jsx3("div", { style: { display: "flex", gap: 6, marginBottom: 10 }, children: ["new", "existing"].map((mode) => /* @__PURE__ */ jsx3(
1260
+ "button",
1261
+ {
1262
+ onClick: () => setBranchMode(mode),
1263
+ style: {
1264
+ flex: 1,
1265
+ padding: "5px 0",
1266
+ border: "none",
1267
+ borderRadius: 6,
1268
+ cursor: "pointer",
1269
+ fontSize: 11,
1270
+ fontWeight: 600,
1271
+ background: branchMode === mode ? accentColor : tt.btnSecBg,
1272
+ color: branchMode === mode ? "#fff" : tt.btnSecText
1273
+ },
1274
+ children: mode === "new" ? `+ New step` : "Existing step"
1275
+ },
1276
+ mode
1277
+ )) }),
1278
+ branchMode === "new" ? /* @__PURE__ */ jsx3(
1279
+ "input",
1280
+ {
1281
+ autoFocus: true,
1282
+ value: branchLabel,
1283
+ onChange: (e) => setBranchLabel(e.target.value),
1284
+ onKeyDown: (e) => e.key === "Enter" && addBranch(),
1285
+ placeholder: "New step name\u2026",
1286
+ style: { ...inputStyle, marginBottom: 6 }
1287
+ }
1288
+ ) : /* @__PURE__ */ jsxs3(
1289
+ "select",
1290
+ {
1291
+ value: branchTarget,
1292
+ onChange: (e) => setBranchTarget(e.target.value),
1293
+ style: { ...inputStyle, marginBottom: 6, appearance: "none" },
1294
+ children: [
1295
+ /* @__PURE__ */ jsx3("option", { value: "", children: "Choose a step\u2026" }),
1296
+ otherNodes.map((n) => /* @__PURE__ */ jsx3("option", { value: n.id, children: n.label }, n.id))
1297
+ ]
1298
+ }
1299
+ ),
1300
+ /* @__PURE__ */ jsx3(
1301
+ "input",
1302
+ {
1303
+ value: branchEdgeLabel,
1304
+ onChange: (e) => setBranchEdgeLabel(e.target.value),
1305
+ placeholder: "Edge label (optional)",
1306
+ style: { ...inputStyle, marginBottom: 10 }
1307
+ }
1308
+ ),
1309
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 6 }, children: [
1310
+ /* @__PURE__ */ jsxs3("button", { onClick: addBranch, style: addBtnStyle, children: [
1311
+ "Add ",
1312
+ branchTerm
1313
+ ] }),
1314
+ /* @__PURE__ */ jsx3("button", { onClick: () => setAddingBranch(false), style: cancelBtnStyle, children: "Cancel" })
1315
+ ] })
1316
+ ]
1317
+ }
1318
+ ) : /* @__PURE__ */ jsxs3("button", { onClick: () => setAddingBranch(true), style: addTriggerStyle, children: [
1319
+ /* @__PURE__ */ jsx3("span", { style: { fontSize: 16, lineHeight: 1 }, children: "+" }),
1320
+ " Add ",
1321
+ branchTerm
1322
+ ] })
1323
+ ]
1324
+ }
1325
+ )
816
1326
  ] })
817
- ] })
818
- ] })
819
- ] });
1327
+ ]
1328
+ }
1329
+ );
820
1330
  }
821
1331
 
822
1332
  // src/ui/SequenceEditor.tsx
@@ -871,26 +1381,33 @@ function SequenceCanvas(props) {
871
1381
  return next;
872
1382
  }, [messages, drag]);
873
1383
  if (actors.length === 0 && messages.length === 0) {
874
- return /* @__PURE__ */ jsxs4("div", { style: {
875
- position: "absolute",
876
- inset: 0,
877
- display: "flex",
878
- flexDirection: "column",
879
- alignItems: "center",
880
- justifyContent: "center",
881
- gap: 10,
882
- color: t.textMuted,
883
- pointerEvents: "none"
884
- }, children: [
885
- /* @__PURE__ */ jsx4("div", { style: { fontSize: 36, opacity: 0.15, color: t.textPrimary }, children: "\u2194" }),
886
- /* @__PURE__ */ jsxs4("div", { style: { fontSize: 13, fontWeight: 500 }, children: [
887
- "Click ",
888
- /* @__PURE__ */ jsx4("strong", { style: { color: INDIGO }, children: "+ Actor" }),
889
- " then ",
890
- /* @__PURE__ */ jsx4("strong", { style: { color: INDIGO }, children: "+ Message" }),
891
- " to start"
892
- ] })
893
- ] });
1384
+ return /* @__PURE__ */ jsxs4(
1385
+ "div",
1386
+ {
1387
+ style: {
1388
+ position: "absolute",
1389
+ inset: 0,
1390
+ display: "flex",
1391
+ flexDirection: "column",
1392
+ alignItems: "center",
1393
+ justifyContent: "center",
1394
+ gap: 10,
1395
+ color: t.textMuted,
1396
+ pointerEvents: "none"
1397
+ },
1398
+ children: [
1399
+ /* @__PURE__ */ jsx4("div", { style: { fontSize: 36, opacity: 0.15, color: t.textPrimary }, children: "\u2194" }),
1400
+ /* @__PURE__ */ jsxs4("div", { style: { fontSize: 13, fontWeight: 500 }, children: [
1401
+ "Click ",
1402
+ /* @__PURE__ */ jsx4("strong", { style: { color: INDIGO }, children: "+ Actor" }),
1403
+ " then",
1404
+ " ",
1405
+ /* @__PURE__ */ jsx4("strong", { style: { color: INDIGO }, children: "+ Message" }),
1406
+ " to start"
1407
+ ] })
1408
+ ]
1409
+ }
1410
+ );
894
1411
  }
895
1412
  return /* @__PURE__ */ jsxs4(
896
1413
  "svg",
@@ -898,12 +1415,28 @@ function SequenceCanvas(props) {
898
1415
  ref: svgRef,
899
1416
  width: totalW,
900
1417
  height: totalH,
901
- style: { display: "block", cursor: drag?.active ? "grabbing" : "default", userSelect: "none" },
1418
+ style: {
1419
+ display: "block",
1420
+ cursor: drag?.active ? "grabbing" : "default",
1421
+ userSelect: "none"
1422
+ },
902
1423
  children: [
903
1424
  /* @__PURE__ */ jsxs4("defs", { children: [
904
1425
  /* @__PURE__ */ jsx4("pattern", { id: "seqdots", x: "0", y: "0", width: "24", height: "24", patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ jsx4("circle", { cx: 12, cy: 12, r: 1.1, fill: t.dot }) }),
905
1426
  /* @__PURE__ */ jsx4("filter", { id: "seqShadow", x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ jsx4("feDropShadow", { dx: 0, dy: 3, stdDeviation: 5, floodColor: shadowColor(isDark) }) }),
906
- /* @__PURE__ */ jsx4("marker", { id: "seqArrow", markerWidth: 9, markerHeight: 7, refX: 8.5, refY: 3.5, orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ jsx4("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: t.arrow }) })
1427
+ /* @__PURE__ */ jsx4(
1428
+ "marker",
1429
+ {
1430
+ id: "seqArrow",
1431
+ markerWidth: 9,
1432
+ markerHeight: 7,
1433
+ refX: 8.5,
1434
+ refY: 3.5,
1435
+ orient: "auto",
1436
+ markerUnits: "strokeWidth",
1437
+ children: /* @__PURE__ */ jsx4("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: t.arrow })
1438
+ }
1439
+ )
907
1440
  ] }),
908
1441
  /* @__PURE__ */ jsx4("rect", { width: totalW, height: totalH, fill: "url(#seqdots)" }),
909
1442
  actors.map((name) => {
@@ -952,8 +1485,28 @@ function SequenceCanvas(props) {
952
1485
  opacity: isDark ? 0.18 : 0.6
953
1486
  }
954
1487
  ),
955
- /* @__PURE__ */ jsx4("path", { d, fill: "none", stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
956
- /* @__PURE__ */ jsx4("text", { x: startX + loopW + 8, y: loopY + 16, fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
1488
+ /* @__PURE__ */ jsx4(
1489
+ "path",
1490
+ {
1491
+ d,
1492
+ fill: "none",
1493
+ stroke,
1494
+ strokeWidth: 1.5,
1495
+ strokeDasharray: dash,
1496
+ markerEnd: "url(#seqArrow)"
1497
+ }
1498
+ ),
1499
+ /* @__PURE__ */ jsx4(
1500
+ "text",
1501
+ {
1502
+ x: startX + loopW + 8,
1503
+ y: loopY + 16,
1504
+ fontSize: 11,
1505
+ fill: selectedHere ? INDIGO : t.textPrimary,
1506
+ fontWeight: 500,
1507
+ children: msg.label
1508
+ }
1509
+ )
957
1510
  ] }, msg.id);
958
1511
  }
959
1512
  const labelX = (fromX + toX) / 2;
@@ -970,7 +1523,19 @@ function SequenceCanvas(props) {
970
1523
  opacity: isDark ? 0.18 : 0.6
971
1524
  }
972
1525
  ),
973
- /* @__PURE__ */ jsx4("line", { x1: fromX, y1: y, x2: toX, y2: y, stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
1526
+ /* @__PURE__ */ jsx4(
1527
+ "line",
1528
+ {
1529
+ x1: fromX,
1530
+ y1: y,
1531
+ x2: toX,
1532
+ y2: y,
1533
+ stroke,
1534
+ strokeWidth: 1.5,
1535
+ strokeDasharray: dash,
1536
+ markerEnd: "url(#seqArrow)"
1537
+ }
1538
+ ),
974
1539
  /* @__PURE__ */ jsx4(
975
1540
  "rect",
976
1541
  {
@@ -984,7 +1549,18 @@ function SequenceCanvas(props) {
984
1549
  strokeWidth: selectedHere ? 1.25 : 1
985
1550
  }
986
1551
  ),
987
- /* @__PURE__ */ jsx4("text", { x: labelX, y: y - 5, textAnchor: "middle", fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
1552
+ /* @__PURE__ */ jsx4(
1553
+ "text",
1554
+ {
1555
+ x: labelX,
1556
+ y: y - 5,
1557
+ textAnchor: "middle",
1558
+ fontSize: 11,
1559
+ fill: selectedHere ? INDIGO : t.textPrimary,
1560
+ fontWeight: 500,
1561
+ children: msg.label
1562
+ }
1563
+ )
988
1564
  ] }, msg.id);
989
1565
  }),
990
1566
  actors.map((name) => {
@@ -1324,7 +1900,9 @@ function computeLayout(model) {
1324
1900
  const h = isQuestion(n, model.variant) ? questionNodeH(n.metadata?.answers ?? []) : NODE_H;
1325
1901
  return { node: n, w, h };
1326
1902
  });
1327
- const allPositioned = sized.every((s2) => typeof s2.node.x === "number" && typeof s2.node.y === "number");
1903
+ const allPositioned = sized.every(
1904
+ (s2) => typeof s2.node.x === "number" && typeof s2.node.y === "number"
1905
+ );
1328
1906
  if (allPositioned) {
1329
1907
  for (const s2 of sized) {
1330
1908
  boxes.set(s2.node.id, { x: s2.node.x, y: s2.node.y, w: s2.w, h: s2.h });
@@ -1374,7 +1952,15 @@ function computeLayout(model) {
1374
1952
  return boxes;
1375
1953
  }
1376
1954
  function escapeXML(s2) {
1377
- return s2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1955
+ return s2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1956
+ }
1957
+ function sanitizeForSVG(s2) {
1958
+ let clean = s2;
1959
+ clean = clean.replace(/<\/?[a-zA-Z][^>]*>/g, "");
1960
+ clean = clean.replace(/\b(?:javascript|data|vbscript)\s*:/gi, "");
1961
+ clean = clean.replace(/\bon[a-z]+\s*=/gi, "");
1962
+ clean = clean.replace(/\x00/g, "");
1963
+ return escapeXML(clean);
1378
1964
  }
1379
1965
  var COLORS = {
1380
1966
  bg: "#fafbfc",
@@ -1393,8 +1979,8 @@ function renderStandardNode(node, box) {
1393
1979
  const cx = box.x + box.w / 2;
1394
1980
  const cy = box.y + box.h / 2;
1395
1981
  const shape = node.shape ?? "rectangle";
1396
- 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>`;
1397
- let shapeEl = "";
1982
+ 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>`;
1983
+ let shapeEl;
1398
1984
  if (shape === "diamond") {
1399
1985
  const pts = `${cx},${box.y} ${box.x + box.w},${cy} ${cx},${box.y + box.h} ${box.x},${cy}`;
1400
1986
  shapeEl = `<polygon points="${pts}" fill="${COLORS.nodeFill}" stroke="${COLORS.nodeStroke}" stroke-width="1.25" filter="url(#nodeShadow)"/>`;
@@ -1414,17 +2000,37 @@ function renderQuestionNode(node, box) {
1414
2000
  const clipId = `qhdr-${node.id.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
1415
2001
  const x = box.x, y = box.y, w = box.w, h = box.h;
1416
2002
  const parts = [];
1417
- 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)"/>`);
1418
- parts.push(`<defs><clipPath id="${clipId}"><rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" rx="14"/></clipPath></defs>`);
1419
- parts.push(`<rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" fill="${COLORS.amberSoft}" clip-path="url(#${clipId})"/>`);
1420
- parts.push(`<rect x="${x}" y="${y}" width="4" height="${Q_BASE_H}" rx="2" fill="${COLORS.amber}"/>`);
1421
- parts.push(`<rect x="${x + 12}" y="${y + 14}" width="28" height="28" rx="8" fill="${COLORS.amber}"/>`);
1422
- parts.push(`<text x="${x + 26}" y="${y + 33}" text-anchor="middle" font-size="15" font-weight="900" fill="white">?</text>`);
1423
- 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>`);
1424
- 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>`);
1425
- parts.push(`<line x1="${x}" y1="${y + Q_BASE_H}" x2="${x + w}" y2="${y + Q_BASE_H}" stroke="${COLORS.amberLine}" stroke-width="1"/>`);
2003
+ parts.push(
2004
+ `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="14" fill="${COLORS.nodeFill}" stroke="${COLORS.amberLine}" stroke-width="1.5" filter="url(#nodeShadow)"/>`
2005
+ );
2006
+ parts.push(
2007
+ `<defs><clipPath id="${clipId}"><rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" rx="14"/></clipPath></defs>`
2008
+ );
2009
+ parts.push(
2010
+ `<rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" fill="${COLORS.amberSoft}" clip-path="url(#${clipId})"/>`
2011
+ );
2012
+ parts.push(
2013
+ `<rect x="${x}" y="${y}" width="4" height="${Q_BASE_H}" rx="2" fill="${COLORS.amber}"/>`
2014
+ );
2015
+ parts.push(
2016
+ `<rect x="${x + 12}" y="${y + 14}" width="28" height="28" rx="8" fill="${COLORS.amber}"/>`
2017
+ );
2018
+ parts.push(
2019
+ `<text x="${x + 26}" y="${y + 33}" text-anchor="middle" font-size="15" font-weight="900" fill="white">?</text>`
2020
+ );
2021
+ parts.push(
2022
+ `<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>`
2023
+ );
2024
+ parts.push(
2025
+ `<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>`
2026
+ );
2027
+ parts.push(
2028
+ `<line x1="${x}" y1="${y + Q_BASE_H}" x2="${x + w}" y2="${y + Q_BASE_H}" stroke="${COLORS.amberLine}" stroke-width="1"/>`
2029
+ );
1426
2030
  if (answers.length === 0) {
1427
- 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>`);
2031
+ parts.push(
2032
+ `<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>`
2033
+ );
1428
2034
  } else {
1429
2035
  const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1430
2036
  answers.forEach((ans, i) => {
@@ -1437,10 +2043,18 @@ function renderQuestionNode(node, box) {
1437
2043
  const letter = i < 26 ? letters[i] : `${i + 1}`;
1438
2044
  const maxChars = Math.max(2, Math.floor((cW - 20) / 7.5));
1439
2045
  const displayAns = ans.length > maxChars ? ans.slice(0, maxChars - 1) + "\u2026" : ans;
1440
- parts.push(`<rect x="${cardX}" y="${cardY}" width="${cW}" height="${cardH}" rx="8" fill="${COLORS.amberCardBg}" stroke="${COLORS.amberLine}" stroke-width="1"/>`);
1441
- parts.push(`<rect x="${cx - 11}" y="${cardY + 7}" width="22" height="22" rx="6" fill="#fef3c7"/>`);
1442
- parts.push(`<text x="${cx}" y="${cardY + 22}" text-anchor="middle" font-size="10" font-weight="800" fill="${COLORS.amber}">${escapeXML(letter)}</text>`);
1443
- 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>`);
2046
+ parts.push(
2047
+ `<rect x="${cardX}" y="${cardY}" width="${cW}" height="${cardH}" rx="8" fill="${COLORS.amberCardBg}" stroke="${COLORS.amberLine}" stroke-width="1"/>`
2048
+ );
2049
+ parts.push(
2050
+ `<rect x="${cx - 11}" y="${cardY + 7}" width="22" height="22" rx="6" fill="#fef3c7"/>`
2051
+ );
2052
+ parts.push(
2053
+ `<text x="${cx}" y="${cardY + 22}" text-anchor="middle" font-size="10" font-weight="800" fill="${COLORS.amber}">${sanitizeForSVG(letter)}</text>`
2054
+ );
2055
+ parts.push(
2056
+ `<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>`
2057
+ );
1444
2058
  });
1445
2059
  }
1446
2060
  return parts.join("");
@@ -1478,7 +2092,7 @@ function renderEdge(edge, boxes, variant, nodes) {
1478
2092
  const midY = (y1 + y2) / 2;
1479
2093
  const labelW = estimateTextW(edge.label, 7) + 14;
1480
2094
  out += `<rect x="${midX - labelW / 2}" y="${midY - 11}" width="${labelW}" height="18" rx="9" fill="${COLORS.bg}" stroke="${COLORS.nodeStroke}" stroke-width="1"/>`;
1481
- 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>`;
2095
+ 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>`;
1482
2096
  }
1483
2097
  return out;
1484
2098
  }
@@ -1504,7 +2118,7 @@ function toSVG(model) {
1504
2118
  `</marker>`,
1505
2119
  `</defs>`
1506
2120
  ].join("");
1507
- 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>` : "";
2121
+ 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>` : "";
1508
2122
  const edges = model.edges.map((e) => renderEdge(e, boxes, model.variant, model.nodes)).join("\n");
1509
2123
  const nodes = model.nodes.map((n) => {
1510
2124
  const b = boxes.get(n.id);
@@ -1521,7 +2135,9 @@ ${nodes}
1521
2135
  }
1522
2136
  async function toPNG(model) {
1523
2137
  if (typeof document === "undefined") {
1524
- throw new Error("toPNG requires a browser environment. For Node/Bun server use, pipe toSVG() through @resvg/resvg-js.");
2138
+ throw new Error(
2139
+ "toPNG requires a browser environment. For Node/Bun server use, pipe toSVG() through @resvg/resvg-js."
2140
+ );
1525
2141
  }
1526
2142
  const svg = toSVG(model);
1527
2143
  const blob = new Blob([svg], { type: "image/svg+xml" });
@@ -1537,7 +2153,10 @@ async function toPNG(model) {
1537
2153
  ctx.scale(scale, scale);
1538
2154
  ctx.drawImage(img, 0, 0);
1539
2155
  URL.revokeObjectURL(url);
1540
- canvas.toBlob((b) => b ? resolve(b) : reject(new Error("Canvas toBlob failed")), "image/png");
2156
+ canvas.toBlob(
2157
+ (b) => b ? resolve(b) : reject(new Error("Canvas toBlob failed")),
2158
+ "image/png"
2159
+ );
1541
2160
  };
1542
2161
  img.onerror = () => {
1543
2162
  URL.revokeObjectURL(url);
@@ -1549,40 +2168,43 @@ async function toPNG(model) {
1549
2168
 
1550
2169
  // src/ui/hooks/useExporters.ts
1551
2170
  function useExporters(model, onExport, filename = "diagram", onSuccess) {
1552
- return useCallback2(async (format) => {
1553
- let content;
1554
- switch (format) {
1555
- case "mermaid":
1556
- content = toMermaid(model);
1557
- break;
1558
- case "plantuml":
1559
- content = toPlantUML(model);
1560
- break;
1561
- case "json":
1562
- content = toJSON(model);
1563
- break;
1564
- case "svg":
1565
- content = toSVG(model);
1566
- break;
1567
- case "png":
1568
- content = await toPNG(model);
1569
- break;
1570
- default:
2171
+ return useCallback2(
2172
+ async (format) => {
2173
+ let content;
2174
+ switch (format) {
2175
+ case "mermaid":
2176
+ content = toMermaid(model);
2177
+ break;
2178
+ case "plantuml":
2179
+ content = toPlantUML(model);
2180
+ break;
2181
+ case "json":
2182
+ content = toJSON(model);
2183
+ break;
2184
+ case "svg":
2185
+ content = toSVG(model);
2186
+ break;
2187
+ case "png":
2188
+ content = await toPNG(model);
2189
+ break;
2190
+ default:
2191
+ return;
2192
+ }
2193
+ if (onExport) {
2194
+ onExport(format, content);
2195
+ onSuccess?.(`Exported as ${format.toUpperCase()}`);
1571
2196
  return;
1572
- }
1573
- if (onExport) {
1574
- onExport(format, content);
1575
- onSuccess?.(`Exported as ${format.toUpperCase()}`);
1576
- return;
1577
- }
1578
- const url = content instanceof Blob ? URL.createObjectURL(content) : URL.createObjectURL(new Blob([content], { type: "text/plain" }));
1579
- const a = document.createElement("a");
1580
- a.href = url;
1581
- a.download = `${filename}.${format === "plantuml" ? "puml" : format}`;
1582
- a.click();
1583
- URL.revokeObjectURL(url);
1584
- onSuccess?.(`Downloaded ${a.download}`);
1585
- }, [model, onExport, filename, onSuccess]);
2197
+ }
2198
+ const url = content instanceof Blob ? URL.createObjectURL(content) : URL.createObjectURL(new Blob([content], { type: "text/plain" }));
2199
+ const a = document.createElement("a");
2200
+ a.href = url;
2201
+ a.download = `${filename}.${format === "plantuml" ? "puml" : format}`;
2202
+ a.click();
2203
+ URL.revokeObjectURL(url);
2204
+ onSuccess?.(`Downloaded ${a.download}`);
2205
+ },
2206
+ [model, onExport, filename, onSuccess]
2207
+ );
1586
2208
  }
1587
2209
 
1588
2210
  // src/ui/hooks/useImporter.ts
@@ -1599,7 +2221,15 @@ var Model = class _Model {
1599
2221
  * @param variant Optional UI variant (flowchart models only).
1600
2222
  */
1601
2223
  constructor(type, title, variant) {
1602
- this.data = { type, ...variant ? { variant } : {}, title, nodes: [], edges: [], actors: [], messages: [] };
2224
+ this.data = {
2225
+ type,
2226
+ ...variant ? { variant } : {},
2227
+ title,
2228
+ nodes: [],
2229
+ edges: [],
2230
+ actors: [],
2231
+ messages: []
2232
+ };
1603
2233
  }
1604
2234
  /**
1605
2235
  * Rehydrate a `Model` from a previously serialized `DiagramModel`. The
@@ -1635,7 +2265,8 @@ var Model = class _Model {
1635
2265
  updateNode(id, patch) {
1636
2266
  const node = this.data.nodes.find((n) => n.id === id);
1637
2267
  if (!node) throw new Error(`Node "${id}" not found`);
1638
- Object.assign(node, patch);
2268
+ const { __proto__, constructor, ...safe } = patch;
2269
+ Object.assign(node, safe);
1639
2270
  return this;
1640
2271
  }
1641
2272
  /**
@@ -1675,15 +2306,35 @@ var Model = class _Model {
1675
2306
  const errors = [];
1676
2307
  const nodeIds = /* @__PURE__ */ new Set();
1677
2308
  for (const n of this.data.nodes) {
1678
- if (nodeIds.has(n.id)) errors.push({ kind: "duplicate-node-id", id: n.id, message: `Duplicate node id "${n.id}"` });
2309
+ if (nodeIds.has(n.id))
2310
+ errors.push({
2311
+ kind: "duplicate-node-id",
2312
+ id: n.id,
2313
+ message: `Duplicate node id "${n.id}"`
2314
+ });
1679
2315
  nodeIds.add(n.id);
1680
2316
  }
1681
2317
  const edgeIds = /* @__PURE__ */ new Set();
1682
2318
  for (const e of this.data.edges) {
1683
- if (edgeIds.has(e.id)) errors.push({ kind: "duplicate-edge-id", id: e.id, message: `Duplicate edge id "${e.id}"` });
2319
+ if (edgeIds.has(e.id))
2320
+ errors.push({
2321
+ kind: "duplicate-edge-id",
2322
+ id: e.id,
2323
+ message: `Duplicate edge id "${e.id}"`
2324
+ });
1684
2325
  edgeIds.add(e.id);
1685
- if (!nodeIds.has(e.from)) errors.push({ kind: "dangling-from", id: e.id, message: `Edge "${e.id}" references unknown source node "${e.from}"` });
1686
- if (!nodeIds.has(e.to)) errors.push({ kind: "dangling-to", id: e.id, message: `Edge "${e.id}" references unknown target node "${e.to}"` });
2326
+ if (!nodeIds.has(e.from))
2327
+ errors.push({
2328
+ kind: "dangling-from",
2329
+ id: e.id,
2330
+ message: `Edge "${e.id}" references unknown source node "${e.from}"`
2331
+ });
2332
+ if (!nodeIds.has(e.to))
2333
+ errors.push({
2334
+ kind: "dangling-to",
2335
+ id: e.id,
2336
+ message: `Edge "${e.id}" references unknown target node "${e.to}"`
2337
+ });
1687
2338
  }
1688
2339
  return errors;
1689
2340
  }
@@ -1717,6 +2368,25 @@ var Model = class _Model {
1717
2368
  }
1718
2369
  };
1719
2370
 
2371
+ // src/core/sanitize.ts
2372
+ var MAX_LABEL_LENGTH = 2e3;
2373
+ var MAX_NODES = 500;
2374
+ var MAX_EDGES = 2e3;
2375
+ var MAX_ACTORS = 100;
2376
+ var MAX_MESSAGES = 2e3;
2377
+ var MAX_IMPORT_LENGTH = 2 * 1024 * 1024;
2378
+ function sanitizeLabel(raw) {
2379
+ let s2 = raw;
2380
+ s2 = s2.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
2381
+ s2 = s2.replace(/<\/?[a-zA-Z][^>]*>/g, "");
2382
+ s2 = s2.replace(/\b(?:javascript|data|vbscript)\s*:/gi, "");
2383
+ s2 = s2.replace(/\bon[a-z]+\s*=/gi, "");
2384
+ if (s2.length > MAX_LABEL_LENGTH) {
2385
+ s2 = s2.slice(0, MAX_LABEL_LENGTH);
2386
+ }
2387
+ return s2;
2388
+ }
2389
+
1720
2390
  // src/importers/mermaid.ts
1721
2391
  function parseNodeDecl(raw) {
1722
2392
  const patterns = [
@@ -1728,7 +2398,7 @@ function parseNodeDecl(raw) {
1728
2398
  ];
1729
2399
  for (const [re, shape] of patterns) {
1730
2400
  const m = raw.match(re);
1731
- if (m) return { id: m[1], label: m[2].replace(/^["']|["']$/g, ""), shape };
2401
+ if (m) return { id: m[1], label: sanitizeLabel(m[2].replace(/^["']|["']$/g, "")), shape };
1732
2402
  }
1733
2403
  return null;
1734
2404
  }
@@ -1743,8 +2413,11 @@ function parseFlowchart(lines) {
1743
2413
  const model = new Model("flowchart");
1744
2414
  const nodeMap = /* @__PURE__ */ new Map();
1745
2415
  const groupStack = [];
2416
+ let edgeCount = 0;
1746
2417
  const ensureNode = (id, group) => {
1747
2418
  if (!nodeMap.has(id)) {
2419
+ if (nodeMap.size >= MAX_NODES)
2420
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
1748
2421
  nodeMap.set(id, true);
1749
2422
  const metadata = group ? { group } : void 0;
1750
2423
  model.addNode({ id, label: id, shape: "rectangle", ...metadata ? { metadata } : {} });
@@ -1753,7 +2426,8 @@ function parseFlowchart(lines) {
1753
2426
  for (const line of lines) {
1754
2427
  const trimmed = line.trim();
1755
2428
  if (!trimmed) continue;
1756
- 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;
2429
+ if (trimmed.startsWith("%%") || trimmed.startsWith("graph") || trimmed.startsWith("flowchart") || trimmed.startsWith("click ") || trimmed.startsWith("classDef ") || trimmed.startsWith("class ") || trimmed.startsWith("style ") || trimmed.startsWith("linkStyle "))
2430
+ continue;
1757
2431
  const subgraphOpen = trimmed.match(/^subgraph\s+(\S+)/i);
1758
2432
  if (subgraphOpen) {
1759
2433
  groupStack.push(subgraphOpen[1]);
@@ -1769,12 +2443,15 @@ function parseFlowchart(lines) {
1769
2443
  const fromRaw = edgeMatch[1].trim();
1770
2444
  const connector = edgeMatch[2];
1771
2445
  const label = edgeMatch[3]?.replace(/^["']|["']$/g, "");
2446
+ const sanitizedLabel = label ? sanitizeLabel(label) : void 0;
1772
2447
  const toRaw = edgeMatch[4].trim();
1773
2448
  const style = detectStyle(connector);
1774
2449
  const arrowhead = detectArrowhead(connector);
1775
2450
  const fromNode = parseNodeDecl(fromRaw);
1776
2451
  const toNode = parseNodeDecl(toRaw);
1777
2452
  if (fromNode && !nodeMap.has(fromNode.id)) {
2453
+ if (nodeMap.size >= MAX_NODES)
2454
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
1778
2455
  nodeMap.set(fromNode.id, true);
1779
2456
  const metadata = currentGroup ? { group: currentGroup } : void 0;
1780
2457
  model.addNode({ ...fromNode, ...metadata ? { metadata } : {} });
@@ -1782,19 +2459,24 @@ function parseFlowchart(lines) {
1782
2459
  ensureNode(fromRaw.replace(/\W.*/, ""), currentGroup);
1783
2460
  }
1784
2461
  if (toNode && !nodeMap.has(toNode.id)) {
2462
+ if (nodeMap.size >= MAX_NODES)
2463
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
1785
2464
  nodeMap.set(toNode.id, true);
1786
2465
  const metadata = currentGroup ? { group: currentGroup } : void 0;
1787
2466
  model.addNode({ ...toNode, ...metadata ? { metadata } : {} });
1788
2467
  } else if (!toNode) {
1789
2468
  ensureNode(toRaw.replace(/\W.*/, ""), currentGroup);
1790
2469
  }
2470
+ if (edgeCount >= MAX_EDGES)
2471
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_EDGES} edges`);
2472
+ edgeCount++;
1791
2473
  const fromId = fromNode?.id ?? fromRaw.replace(/\W.*/, "");
1792
2474
  const toId = toNode?.id ?? toRaw.replace(/\W.*/, "");
1793
2475
  model.addEdge({
1794
2476
  id: nextId("e", model.toJSON().edges),
1795
2477
  from: fromId,
1796
2478
  to: toId,
1797
- ...label ? { label } : {},
2479
+ ...sanitizedLabel ? { label: sanitizedLabel } : {},
1798
2480
  style,
1799
2481
  ...arrowhead === "none" ? { arrowhead } : {}
1800
2482
  });
@@ -1802,6 +2484,8 @@ function parseFlowchart(lines) {
1802
2484
  }
1803
2485
  const nodeDecl = parseNodeDecl(trimmed);
1804
2486
  if (nodeDecl && !nodeMap.has(nodeDecl.id)) {
2487
+ if (nodeMap.size >= MAX_NODES)
2488
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
1805
2489
  nodeMap.set(nodeDecl.id, true);
1806
2490
  const metadata = currentGroup ? { group: currentGroup } : void 0;
1807
2491
  model.addNode({ ...nodeDecl, ...metadata ? { metadata } : {} });
@@ -1811,34 +2495,56 @@ function parseFlowchart(lines) {
1811
2495
  }
1812
2496
  function parseSequence(lines, title) {
1813
2497
  const model = new Model("sequence", title);
2498
+ let actorCount = 0;
2499
+ let messageCount = 0;
2500
+ const safeAddActor = (name) => {
2501
+ const safeName = sanitizeLabel(name);
2502
+ if (!model.toJSON().actors?.includes(safeName)) {
2503
+ if (actorCount >= MAX_ACTORS)
2504
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_ACTORS} actors`);
2505
+ actorCount++;
2506
+ }
2507
+ model.addActor(safeName);
2508
+ return safeName;
2509
+ };
1814
2510
  for (const line of lines) {
1815
2511
  const trimmed = line.trim();
1816
2512
  if (!trimmed || trimmed.startsWith("sequenceDiagram") || trimmed.startsWith("%%")) continue;
1817
2513
  const participantMatch = trimmed.match(/^participant\s+(.+)$/i);
1818
2514
  if (participantMatch) {
1819
- model.addActor(participantMatch[1].trim());
2515
+ safeAddActor(participantMatch[1].trim());
1820
2516
  continue;
1821
2517
  }
1822
2518
  const actorMatch = trimmed.match(/^actor\s+(.+)$/i);
1823
2519
  if (actorMatch) {
1824
- model.addActor(actorMatch[1].trim());
2520
+ safeAddActor(actorMatch[1].trim());
1825
2521
  continue;
1826
2522
  }
1827
2523
  const msgMatch = trimmed.match(/^(.+?)\s*(-->>|->>|-->|->)\s*(.+?):\s*(.+)$/);
1828
2524
  if (msgMatch) {
1829
- const from = msgMatch[1].trim();
2525
+ const from = safeAddActor(msgMatch[1].trim());
1830
2526
  const arrow = msgMatch[2];
1831
- const to = msgMatch[3].trim();
1832
- const label = msgMatch[4].trim();
1833
- model.addActor(from);
1834
- model.addActor(to);
2527
+ const to = safeAddActor(msgMatch[3].trim());
2528
+ const label = sanitizeLabel(msgMatch[4].trim());
2529
+ if (messageCount >= MAX_MESSAGES)
2530
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_MESSAGES} messages`);
2531
+ messageCount++;
1835
2532
  const messages = model.toJSON().messages ?? [];
1836
- model.addMessage({ id: nextId("m", messages), from, to, label, style: arrow.startsWith("--") ? "dashed" : "solid" });
2533
+ model.addMessage({
2534
+ id: nextId("m", messages),
2535
+ from,
2536
+ to,
2537
+ label,
2538
+ style: arrow.startsWith("--") ? "dashed" : "solid"
2539
+ });
1837
2540
  }
1838
2541
  }
1839
2542
  return model;
1840
2543
  }
1841
2544
  function fromMermaid(mermaid) {
2545
+ if (mermaid.length > MAX_IMPORT_LENGTH) {
2546
+ throw new Error(`Import aborted: input exceeds the maximum of ${MAX_IMPORT_LENGTH} characters`);
2547
+ }
1842
2548
  const cleaned = mermaid.replace(/mermaid\.initialize\([\s\S]*?\)\s*;?/g, "");
1843
2549
  const rawLines = cleaned.split("\n");
1844
2550
  let startIdx = 0;
@@ -1870,10 +2576,74 @@ function fromMermaid(mermaid) {
1870
2576
  }
1871
2577
 
1872
2578
  // src/importers/json.ts
2579
+ function stripDangerousKeys(obj) {
2580
+ if (Array.isArray(obj)) return obj.map(stripDangerousKeys);
2581
+ if (obj !== null && typeof obj === "object") {
2582
+ const clean = {};
2583
+ for (const [key, val] of Object.entries(obj)) {
2584
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
2585
+ clean[key] = stripDangerousKeys(val);
2586
+ }
2587
+ return clean;
2588
+ }
2589
+ return obj;
2590
+ }
1873
2591
  function fromJSON(json) {
1874
- const data = typeof json === "string" ? JSON.parse(json) : json;
1875
- if (!data.type || !Array.isArray(data.nodes) || !Array.isArray(data.edges)) {
1876
- throw new Error("Invalid DiagramModel JSON");
2592
+ if (typeof json === "string" && json.length > MAX_IMPORT_LENGTH) {
2593
+ throw new Error(`Import aborted: input exceeds the maximum of ${MAX_IMPORT_LENGTH} characters`);
2594
+ }
2595
+ const raw = typeof json === "string" ? JSON.parse(json) : json;
2596
+ const data = stripDangerousKeys(raw);
2597
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
2598
+ throw new Error("Invalid DiagramModel JSON: expected an object");
2599
+ }
2600
+ if (data.type !== "flowchart" && data.type !== "sequence") {
2601
+ throw new Error(`Invalid DiagramModel JSON: unknown type "${data.type}"`);
2602
+ }
2603
+ if (!Array.isArray(data.nodes) || !Array.isArray(data.edges)) {
2604
+ throw new Error("Invalid DiagramModel JSON: nodes and edges must be arrays");
2605
+ }
2606
+ if (data.nodes.length > MAX_NODES) {
2607
+ throw new Error(
2608
+ `Import aborted: diagram has ${data.nodes.length} nodes, maximum is ${MAX_NODES}`
2609
+ );
2610
+ }
2611
+ if (data.edges.length > MAX_EDGES) {
2612
+ throw new Error(
2613
+ `Import aborted: diagram has ${data.edges.length} edges, maximum is ${MAX_EDGES}`
2614
+ );
2615
+ }
2616
+ if (data.actors && data.actors.length > MAX_ACTORS) {
2617
+ throw new Error(
2618
+ `Import aborted: diagram has ${data.actors.length} actors, maximum is ${MAX_ACTORS}`
2619
+ );
2620
+ }
2621
+ if (data.messages && data.messages.length > MAX_MESSAGES) {
2622
+ throw new Error(
2623
+ `Import aborted: diagram has ${data.messages.length} messages, maximum is ${MAX_MESSAGES}`
2624
+ );
2625
+ }
2626
+ for (const node of data.nodes) {
2627
+ if (typeof node !== "object" || node === null || typeof node.id !== "string" || typeof node.label !== "string") {
2628
+ throw new Error("Invalid DiagramModel JSON: each node must have string id and label");
2629
+ }
2630
+ node.label = sanitizeLabel(node.label);
2631
+ }
2632
+ for (const edge of data.edges) {
2633
+ if (typeof edge !== "object" || edge === null || typeof edge.id !== "string" || typeof edge.from !== "string" || typeof edge.to !== "string") {
2634
+ throw new Error("Invalid DiagramModel JSON: each edge must have string id, from, and to");
2635
+ }
2636
+ if (edge.label) edge.label = sanitizeLabel(edge.label);
2637
+ }
2638
+ if (data.actors) {
2639
+ data.actors = data.actors.map((a) => typeof a === "string" ? sanitizeLabel(a) : a);
2640
+ }
2641
+ if (data.messages) {
2642
+ for (const msg of data.messages) {
2643
+ if (typeof msg === "object" && msg !== null && typeof msg.label === "string") {
2644
+ msg.label = sanitizeLabel(msg.label);
2645
+ }
2646
+ }
1877
2647
  }
1878
2648
  return Model.fromData(data);
1879
2649
  }
@@ -1882,19 +2652,22 @@ function fromJSON(json) {
1882
2652
  function useImporter(applyAndPush, options = {}) {
1883
2653
  const { expectedType, transform, onSuccess, onError } = options;
1884
2654
  const reportError = onError ?? ((msg) => alert(msg));
1885
- return useCallback3((text) => {
1886
- try {
1887
- const parsed = text.trim().startsWith("{") ? fromJSON(text).toJSON() : fromMermaid(text).toJSON();
1888
- if (expectedType && parsed.type !== expectedType) {
1889
- reportError(`Imported diagram is not a ${expectedType} diagram.`);
1890
- return;
2655
+ return useCallback3(
2656
+ (text) => {
2657
+ try {
2658
+ const parsed = text.trim().startsWith("{") ? fromJSON(text).toJSON() : fromMermaid(text).toJSON();
2659
+ if (expectedType && parsed.type !== expectedType) {
2660
+ reportError(`Imported diagram is not a ${expectedType} diagram.`);
2661
+ return;
2662
+ }
2663
+ applyAndPush(transform ? transform(parsed) : parsed);
2664
+ onSuccess?.("Diagram imported successfully");
2665
+ } catch (err) {
2666
+ reportError(`Import failed: ${err.message}`);
1891
2667
  }
1892
- applyAndPush(transform ? transform(parsed) : parsed);
1893
- onSuccess?.("Diagram imported successfully");
1894
- } catch (err) {
1895
- reportError(`Import failed: ${err.message}`);
1896
- }
1897
- }, [applyAndPush, expectedType, transform, onSuccess, onError]);
2668
+ },
2669
+ [applyAndPush, expectedType, transform, onSuccess, onError]
2670
+ );
1898
2671
  }
1899
2672
 
1900
2673
  // src/ui/hooks/useToast.ts
@@ -1932,7 +2705,10 @@ var containerStyle = {
1932
2705
  zIndex: 9999,
1933
2706
  pointerEvents: "none"
1934
2707
  };
1935
- function ToastContainer({ toasts, onDismiss }) {
2708
+ function ToastContainer({
2709
+ toasts,
2710
+ onDismiss
2711
+ }) {
1936
2712
  if (toasts.length === 0) return null;
1937
2713
  return /* @__PURE__ */ jsx5("div", { style: containerStyle, children: toasts.map((t) => {
1938
2714
  const c = TOAST_COLORS[t.type];
@@ -2163,7 +2939,10 @@ function SequenceEditor({
2163
2939
  const historyRef = useRef3([ensureSequenceModel(initialModel)]);
2164
2940
  const historyIdxRef = useRef3(0);
2165
2941
  const svgRef = useRef3(null);
2166
- const { t, isDark } = useEditorTheme(theme, themeOverrides, { light: lightTheme2, dark: darkTheme2 });
2942
+ const { t, isDark } = useEditorTheme(theme, themeOverrides, {
2943
+ light: lightTheme2,
2944
+ dark: darkTheme2
2945
+ });
2167
2946
  const actors = model.actors ?? [];
2168
2947
  const messages = model.messages ?? [];
2169
2948
  const colW = useMemo3(() => {
@@ -2185,11 +2964,14 @@ function SequenceEditor({
2185
2964
  historyRef.current = stack;
2186
2965
  historyIdxRef.current = stack.length - 1;
2187
2966
  }, []);
2188
- const applyAndPush = useCallback5((m) => {
2189
- setModel(m);
2190
- onChange?.(m);
2191
- pushHistory(m);
2192
- }, [onChange, pushHistory]);
2967
+ const applyAndPush = useCallback5(
2968
+ (m) => {
2969
+ setModel(m);
2970
+ onChange?.(m);
2971
+ pushHistory(m);
2972
+ },
2973
+ [onChange, pushHistory]
2974
+ );
2193
2975
  const undo = useCallback5(() => {
2194
2976
  if (historyIdxRef.current <= 0) return;
2195
2977
  historyIdxRef.current--;
@@ -2234,7 +3016,10 @@ function SequenceEditor({
2234
3016
  applyAndPush({
2235
3017
  ...model,
2236
3018
  actors: [...actors, a, b],
2237
- messages: [...messages, { id: nextId("m", messages), from: a, to: b, label: "message", style: "solid" }]
3019
+ messages: [
3020
+ ...messages,
3021
+ { id: nextId("m", messages), from: a, to: b, label: "message", style: "solid" }
3022
+ ]
2238
3023
  });
2239
3024
  return;
2240
3025
  }
@@ -2242,7 +3027,10 @@ function SequenceEditor({
2242
3027
  const to = actors[Math.min(1, actors.length - 1)] ?? from;
2243
3028
  applyAndPush({
2244
3029
  ...model,
2245
- messages: [...messages, { id: nextId("m", messages), from, to, label: "message", style: "solid" }]
3030
+ messages: [
3031
+ ...messages,
3032
+ { id: nextId("m", messages), from, to, label: "message", style: "solid" }
3033
+ ]
2246
3034
  });
2247
3035
  };
2248
3036
  const updateMessage = (id, patch) => {
@@ -2255,37 +3043,57 @@ function SequenceEditor({
2255
3043
  applyAndPush({ ...model, messages: messages.filter((m) => m.id !== id) });
2256
3044
  if (selected === id) setSelected(null);
2257
3045
  };
2258
- const reorderMessage = useCallback5((id, toIdx) => {
2259
- const fromIdx = messages.findIndex((m) => m.id === id);
2260
- if (fromIdx < 0 || toIdx === fromIdx) return;
2261
- const next = messages.slice();
2262
- const [moved] = next.splice(fromIdx, 1);
2263
- next.splice(toIdx, 0, moved);
2264
- applyAndPush({ ...model, messages: next });
2265
- }, [messages, model, applyAndPush]);
3046
+ const reorderMessage = useCallback5(
3047
+ (id, toIdx) => {
3048
+ const fromIdx = messages.findIndex((m) => m.id === id);
3049
+ if (fromIdx < 0 || toIdx === fromIdx) return;
3050
+ const next = messages.slice();
3051
+ const [moved] = next.splice(fromIdx, 1);
3052
+ next.splice(toIdx, 0, moved);
3053
+ applyAndPush({ ...model, messages: next });
3054
+ },
3055
+ [messages, model, applyAndPush]
3056
+ );
2266
3057
  const keyCommands = [
2267
- { match: (e) => (e.ctrlKey || e.metaKey) && e.key === "z", run: () => {
2268
- undo();
2269
- return true;
2270
- } },
2271
- { match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "y" || e.shiftKey && e.key === "z"), run: () => {
2272
- redo();
2273
- return true;
2274
- } },
2275
- { match: (e) => e.key === "Escape", run: () => {
2276
- setSelected(null);
2277
- setEditingId(null);
2278
- return true;
2279
- } },
2280
- { match: (e) => (e.key === "Delete" || e.key === "Backspace") && !!selected, run: () => {
2281
- removeMessage(selected);
2282
- return true;
2283
- } }
2284
- ];
2285
- useEditorKeyboard(keyCommands, [undo, redo, selected]);
2286
- const handleExport = useExporters(model, onExport, "sequence", (msg) => showToast(msg, "success"));
2287
- const handleImport = useImporter(applyAndPush, {
2288
- expectedType: "sequence",
3058
+ {
3059
+ match: (e) => (e.ctrlKey || e.metaKey) && e.key === "z",
3060
+ run: () => {
3061
+ undo();
3062
+ return true;
3063
+ }
3064
+ },
3065
+ {
3066
+ match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "y" || e.shiftKey && e.key === "z"),
3067
+ run: () => {
3068
+ redo();
3069
+ return true;
3070
+ }
3071
+ },
3072
+ {
3073
+ match: (e) => e.key === "Escape",
3074
+ run: () => {
3075
+ setSelected(null);
3076
+ setEditingId(null);
3077
+ return true;
3078
+ }
3079
+ },
3080
+ {
3081
+ match: (e) => (e.key === "Delete" || e.key === "Backspace") && !!selected,
3082
+ run: () => {
3083
+ removeMessage(selected);
3084
+ return true;
3085
+ }
3086
+ }
3087
+ ];
3088
+ useEditorKeyboard(keyCommands, [undo, redo, selected]);
3089
+ const handleExport = useExporters(
3090
+ model,
3091
+ onExport,
3092
+ "sequence",
3093
+ (msg) => showToast(msg, "success")
3094
+ );
3095
+ const handleImport = useImporter(applyAndPush, {
3096
+ expectedType: "sequence",
2289
3097
  transform: ensureSequenceModel,
2290
3098
  onSuccess: (msg) => showToast(msg, "success"),
2291
3099
  onError: (msg) => showToast(msg, "error")
@@ -2328,17 +3136,22 @@ function SequenceEditor({
2328
3136
  };
2329
3137
  }, [drag, messages.length, reorderMessage]);
2330
3138
  const selectedMsg = selected ? messages.find((m) => m.id === selected) : null;
2331
- return /* @__PURE__ */ jsxs6("div", { className: "fsd-seq-editor", style: {
2332
- display: "flex",
2333
- flexDirection: "column",
2334
- height,
2335
- width: "100%",
2336
- fontFamily: "ui-sans-serif,system-ui,sans-serif",
2337
- background: t.ctrlsBg,
2338
- position: "relative"
2339
- }, children: [
2340
- /* @__PURE__ */ jsx6(ToastContainer, { toasts, onDismiss: dismissToast }),
2341
- /* @__PURE__ */ jsx6("style", { children: `
3139
+ return /* @__PURE__ */ jsxs6(
3140
+ "div",
3141
+ {
3142
+ className: "fsd-seq-editor",
3143
+ style: {
3144
+ display: "flex",
3145
+ flexDirection: "column",
3146
+ height,
3147
+ width: "100%",
3148
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
3149
+ background: t.ctrlsBg,
3150
+ position: "relative"
3151
+ },
3152
+ children: [
3153
+ /* @__PURE__ */ jsx6(ToastContainer, { toasts, onDismiss: dismissToast }),
3154
+ /* @__PURE__ */ jsx6("style", { children: `
2342
3155
  .fsd-seq-editor [role="button"]:focus-visible {
2343
3156
  outline: 2px solid ${t.actorText};
2344
3157
  outline-offset: 2px;
@@ -2350,139 +3163,202 @@ function SequenceEditor({
2350
3163
  border-radius: 4px;
2351
3164
  }
2352
3165
  ` }),
2353
- /* @__PURE__ */ jsx6(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
2354
- /* @__PURE__ */ jsxs6("div", { style: {
2355
- display: "flex",
2356
- gap: 8,
2357
- padding: "7px 14px",
2358
- background: t.ctrlsBg,
2359
- borderBottom: `1px solid ${t.ctrlsBorder}`,
2360
- alignItems: "center",
2361
- flexWrap: "wrap"
2362
- }, children: [
2363
- /* @__PURE__ */ jsx6("button", { onClick: addActor, style: primaryBtn(), children: "+ Actor" }),
2364
- /* @__PURE__ */ jsx6("button", { onClick: addMessage, style: primaryBtn(), children: "+ Message" }),
2365
- /* @__PURE__ */ jsx6("div", { style: { width: 1, height: 18, background: t.ctrlsBorder, margin: "0 4px" } }),
2366
- /* @__PURE__ */ jsx6("button", { onClick: undo, style: ghostBtn2(t), title: "Undo (Ctrl+Z)", children: "\u21B6" }),
2367
- /* @__PURE__ */ jsx6("button", { onClick: redo, style: ghostBtn2(t), title: "Redo (Ctrl+Y)", children: "\u21B7" }),
2368
- /* @__PURE__ */ jsxs6("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
2369
- actors.length,
2370
- " actor",
2371
- actors.length === 1 ? "" : "s",
2372
- " \xB7 ",
2373
- messages.length,
2374
- " message",
2375
- messages.length === 1 ? "" : "s",
2376
- " \xB7 drag a row to reorder"
2377
- ] })
2378
- ] }),
2379
- /* @__PURE__ */ jsxs6("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [
2380
- /* @__PURE__ */ jsx6("div", { style: { flex: 1, overflow: "auto", background: t.canvas, position: "relative" }, children: /* @__PURE__ */ jsx6(
2381
- SequenceCanvas,
2382
- {
2383
- model,
2384
- actors,
2385
- messages,
2386
- t,
2387
- isDark,
2388
- colW,
2389
- totalW,
2390
- totalH,
2391
- actorX,
2392
- msgY,
2393
- selected,
2394
- editingId,
2395
- setEditingId,
2396
- drag,
2397
- onRowMouseDown,
2398
- renameActor,
2399
- removeActor,
2400
- svgRef
2401
- }
2402
- ) }),
2403
- selectedMsg && /* @__PURE__ */ jsxs6("div", { style: {
2404
- width: 280,
2405
- maxWidth: "40vw",
2406
- flexShrink: 0,
2407
- background: t.panelBg,
2408
- borderLeft: `1px solid ${t.panelBorder}`,
2409
- padding: "14px 16px",
2410
- overflowY: "auto"
2411
- }, children: [
2412
- /* @__PURE__ */ jsx6("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.7, marginBottom: 10 }, children: "Message" }),
2413
- /* @__PURE__ */ jsx6(Label, { t, children: "Label" }),
2414
3166
  /* @__PURE__ */ jsx6(
2415
- "input",
3167
+ Toolbar,
2416
3168
  {
2417
- value: editLabel || selectedMsg.label,
2418
- onChange: (e) => setEditLabel(e.target.value),
2419
- onFocus: () => setEditLabel(selectedMsg.label),
2420
- onBlur: () => {
2421
- if (editLabel && editLabel !== selectedMsg.label) updateMessage(selectedMsg.id, { label: editLabel });
2422
- setEditLabel("");
2423
- },
2424
- onKeyDown: (e) => {
2425
- if (e.key === "Enter") e.target.blur();
2426
- },
2427
- style: input(t)
3169
+ onExport: handleExport,
3170
+ onImport: allowImport ? handleImport : void 0,
3171
+ allowedExports,
3172
+ allowImport
2428
3173
  }
2429
3174
  ),
2430
- /* @__PURE__ */ jsx6(Label, { t, children: "From" }),
2431
- /* @__PURE__ */ jsx6("select", { value: selectedMsg.from, onChange: (e) => updateMessage(selectedMsg.id, { from: e.target.value }), style: input(t), children: actors.map((a) => /* @__PURE__ */ jsx6("option", { value: a, children: a }, a)) }),
2432
- /* @__PURE__ */ jsx6(Label, { t, children: "To" }),
2433
- /* @__PURE__ */ jsx6("select", { value: selectedMsg.to, onChange: (e) => updateMessage(selectedMsg.id, { to: e.target.value }), style: input(t), children: actors.map((a) => /* @__PURE__ */ jsx6("option", { value: a, children: a }, a)) }),
2434
- /* @__PURE__ */ jsx6(Label, { t, children: "Style" }),
2435
- /* @__PURE__ */ jsx6("div", { style: { display: "flex", gap: 6 }, children: ["solid", "dashed"].map((s2) => /* @__PURE__ */ jsx6(
2436
- "button",
3175
+ /* @__PURE__ */ jsxs6(
3176
+ "div",
2437
3177
  {
2438
- onClick: () => updateMessage(selectedMsg.id, { style: s2 }),
2439
3178
  style: {
2440
- flex: 1,
2441
- padding: "6px 10px",
2442
- border: `1.5px solid ${selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO2 : t.inputBorder}`,
2443
- background: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO_SOFT2 : t.inputBg,
2444
- color: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO2 : t.textPrimary,
2445
- borderRadius: 8,
2446
- fontSize: 12,
2447
- fontWeight: 600,
2448
- cursor: "pointer",
2449
- fontFamily: "inherit"
3179
+ display: "flex",
3180
+ gap: 8,
3181
+ padding: "7px 14px",
3182
+ background: t.ctrlsBg,
3183
+ borderBottom: `1px solid ${t.ctrlsBorder}`,
3184
+ alignItems: "center",
3185
+ flexWrap: "wrap"
2450
3186
  },
2451
- children: s2 === "solid" ? "\u2500\u2500 solid" : "\u2500 \u2500 dashed"
2452
- },
2453
- s2
2454
- )) }),
2455
- /* @__PURE__ */ jsx6("div", { style: { height: 14 } }),
2456
- /* @__PURE__ */ jsx6(
2457
- "button",
3187
+ children: [
3188
+ /* @__PURE__ */ jsx6("button", { onClick: addActor, style: primaryBtn(), children: "+ Actor" }),
3189
+ /* @__PURE__ */ jsx6("button", { onClick: addMessage, style: primaryBtn(), children: "+ Message" }),
3190
+ /* @__PURE__ */ jsx6("div", { style: { width: 1, height: 18, background: t.ctrlsBorder, margin: "0 4px" } }),
3191
+ /* @__PURE__ */ jsx6("button", { onClick: undo, style: ghostBtn2(t), title: "Undo (Ctrl+Z)", children: "\u21B6" }),
3192
+ /* @__PURE__ */ jsx6("button", { onClick: redo, style: ghostBtn2(t), title: "Redo (Ctrl+Y)", children: "\u21B7" }),
3193
+ /* @__PURE__ */ jsxs6("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
3194
+ actors.length,
3195
+ " actor",
3196
+ actors.length === 1 ? "" : "s",
3197
+ " \xB7 ",
3198
+ messages.length,
3199
+ " message",
3200
+ messages.length === 1 ? "" : "s",
3201
+ " \xB7 drag a row to reorder"
3202
+ ] })
3203
+ ]
3204
+ }
3205
+ ),
3206
+ /* @__PURE__ */ jsxs6("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [
3207
+ /* @__PURE__ */ jsx6("div", { style: { flex: 1, overflow: "auto", background: t.canvas, position: "relative" }, children: /* @__PURE__ */ jsx6(
3208
+ SequenceCanvas,
3209
+ {
3210
+ model,
3211
+ actors,
3212
+ messages,
3213
+ t,
3214
+ isDark,
3215
+ colW,
3216
+ totalW,
3217
+ totalH,
3218
+ actorX,
3219
+ msgY,
3220
+ selected,
3221
+ editingId,
3222
+ setEditingId,
3223
+ drag,
3224
+ onRowMouseDown,
3225
+ renameActor,
3226
+ removeActor,
3227
+ svgRef
3228
+ }
3229
+ ) }),
3230
+ selectedMsg && /* @__PURE__ */ jsxs6(
3231
+ "div",
3232
+ {
3233
+ style: {
3234
+ width: 280,
3235
+ maxWidth: "40vw",
3236
+ flexShrink: 0,
3237
+ background: t.panelBg,
3238
+ borderLeft: `1px solid ${t.panelBorder}`,
3239
+ padding: "14px 16px",
3240
+ overflowY: "auto"
3241
+ },
3242
+ children: [
3243
+ /* @__PURE__ */ jsx6(
3244
+ "div",
3245
+ {
3246
+ style: {
3247
+ fontSize: 10,
3248
+ fontWeight: 700,
3249
+ color: t.textMuted,
3250
+ textTransform: "uppercase",
3251
+ letterSpacing: 0.7,
3252
+ marginBottom: 10
3253
+ },
3254
+ children: "Message"
3255
+ }
3256
+ ),
3257
+ /* @__PURE__ */ jsx6(Label, { t, children: "Label" }),
3258
+ /* @__PURE__ */ jsx6(
3259
+ "input",
3260
+ {
3261
+ value: editLabel || selectedMsg.label,
3262
+ onChange: (e) => setEditLabel(e.target.value),
3263
+ onFocus: () => setEditLabel(selectedMsg.label),
3264
+ onBlur: () => {
3265
+ if (editLabel && editLabel !== selectedMsg.label)
3266
+ updateMessage(selectedMsg.id, { label: editLabel });
3267
+ setEditLabel("");
3268
+ },
3269
+ onKeyDown: (e) => {
3270
+ if (e.key === "Enter") e.target.blur();
3271
+ },
3272
+ style: input(t)
3273
+ }
3274
+ ),
3275
+ /* @__PURE__ */ jsx6(Label, { t, children: "From" }),
3276
+ /* @__PURE__ */ jsx6(
3277
+ "select",
3278
+ {
3279
+ value: selectedMsg.from,
3280
+ onChange: (e) => updateMessage(selectedMsg.id, { from: e.target.value }),
3281
+ style: input(t),
3282
+ children: actors.map((a) => /* @__PURE__ */ jsx6("option", { value: a, children: a }, a))
3283
+ }
3284
+ ),
3285
+ /* @__PURE__ */ jsx6(Label, { t, children: "To" }),
3286
+ /* @__PURE__ */ jsx6(
3287
+ "select",
3288
+ {
3289
+ value: selectedMsg.to,
3290
+ onChange: (e) => updateMessage(selectedMsg.id, { to: e.target.value }),
3291
+ style: input(t),
3292
+ children: actors.map((a) => /* @__PURE__ */ jsx6("option", { value: a, children: a }, a))
3293
+ }
3294
+ ),
3295
+ /* @__PURE__ */ jsx6(Label, { t, children: "Style" }),
3296
+ /* @__PURE__ */ jsx6("div", { style: { display: "flex", gap: 6 }, children: ["solid", "dashed"].map((s2) => /* @__PURE__ */ jsx6(
3297
+ "button",
3298
+ {
3299
+ onClick: () => updateMessage(selectedMsg.id, { style: s2 }),
3300
+ style: {
3301
+ flex: 1,
3302
+ padding: "6px 10px",
3303
+ border: `1.5px solid ${selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO2 : t.inputBorder}`,
3304
+ background: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO_SOFT2 : t.inputBg,
3305
+ color: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO2 : t.textPrimary,
3306
+ borderRadius: 8,
3307
+ fontSize: 12,
3308
+ fontWeight: 600,
3309
+ cursor: "pointer",
3310
+ fontFamily: "inherit"
3311
+ },
3312
+ children: s2 === "solid" ? "\u2500\u2500 solid" : "\u2500 \u2500 dashed"
3313
+ },
3314
+ s2
3315
+ )) }),
3316
+ /* @__PURE__ */ jsx6("div", { style: { height: 14 } }),
3317
+ /* @__PURE__ */ jsx6(
3318
+ "button",
3319
+ {
3320
+ onClick: () => removeMessage(selectedMsg.id),
3321
+ style: {
3322
+ ...ghostBtn2(t),
3323
+ width: "100%",
3324
+ color: "#ef4444",
3325
+ border: `1px solid ${isDark ? "#7f1d1d" : "#fca5a5"}`
3326
+ },
3327
+ children: "Delete message"
3328
+ }
3329
+ )
3330
+ ]
3331
+ }
3332
+ )
3333
+ ] }),
3334
+ /* @__PURE__ */ jsxs6(
3335
+ "div",
2458
3336
  {
2459
- onClick: () => removeMessage(selectedMsg.id),
2460
- style: { ...ghostBtn2(t), width: "100%", color: "#ef4444", border: `1px solid ${isDark ? "#7f1d1d" : "#fca5a5"}` },
2461
- children: "Delete message"
3337
+ style: {
3338
+ padding: "4px 14px",
3339
+ fontSize: 11,
3340
+ color: t.textMuted,
3341
+ background: t.canvas,
3342
+ borderTop: `1px solid ${t.ctrlsBorder}`,
3343
+ display: "flex",
3344
+ gap: 16
3345
+ },
3346
+ children: [
3347
+ /* @__PURE__ */ jsxs6("span", { children: [
3348
+ actors.length,
3349
+ " actors"
3350
+ ] }),
3351
+ /* @__PURE__ */ jsxs6("span", { children: [
3352
+ messages.length,
3353
+ " messages"
3354
+ ] }),
3355
+ /* @__PURE__ */ jsx6("span", { style: { marginLeft: "auto" }, children: "double-click actor to rename \xB7 drag a row to reorder" })
3356
+ ]
2462
3357
  }
2463
3358
  )
2464
- ] })
2465
- ] }),
2466
- /* @__PURE__ */ jsxs6("div", { style: {
2467
- padding: "4px 14px",
2468
- fontSize: 11,
2469
- color: t.textMuted,
2470
- background: t.canvas,
2471
- borderTop: `1px solid ${t.ctrlsBorder}`,
2472
- display: "flex",
2473
- gap: 16
2474
- }, children: [
2475
- /* @__PURE__ */ jsxs6("span", { children: [
2476
- actors.length,
2477
- " actors"
2478
- ] }),
2479
- /* @__PURE__ */ jsxs6("span", { children: [
2480
- messages.length,
2481
- " messages"
2482
- ] }),
2483
- /* @__PURE__ */ jsx6("span", { style: { marginLeft: "auto" }, children: "double-click actor to rename \xB7 drag a row to reorder" })
2484
- ] })
2485
- ] });
3359
+ ]
3360
+ }
3361
+ );
2486
3362
  }
2487
3363
  function primaryBtn() {
2488
3364
  return {
@@ -2526,7 +3402,20 @@ function input(t) {
2526
3402
  };
2527
3403
  }
2528
3404
  function Label({ t, children }) {
2529
- return /* @__PURE__ */ jsx6("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 4 }, children });
3405
+ return /* @__PURE__ */ jsx6(
3406
+ "div",
3407
+ {
3408
+ style: {
3409
+ fontSize: 10,
3410
+ fontWeight: 700,
3411
+ color: t.textMuted,
3412
+ textTransform: "uppercase",
3413
+ letterSpacing: 0.6,
3414
+ marginBottom: 4
3415
+ },
3416
+ children
3417
+ }
3418
+ );
2530
3419
  }
2531
3420
 
2532
3421
  // src/ui/NodeNavigator.tsx
@@ -2558,163 +3447,286 @@ function NodeNavigator({
2558
3447
  return "\u25AD";
2559
3448
  }
2560
3449
  };
2561
- const filtered = model.nodes.filter(
2562
- (n) => n.label.toLowerCase().includes(search.toLowerCase())
2563
- );
3450
+ const filtered = model.nodes.filter((n) => n.label.toLowerCase().includes(search.toLowerCase()));
2564
3451
  const inEdges = (id) => model.edges.filter((e) => e.to === id).length;
2565
3452
  const outEdges = (id) => model.edges.filter((e) => e.from === id).length;
2566
3453
  if (!open) {
2567
- return /* @__PURE__ */ jsxs7("div", { style: {
2568
- width: 36,
2569
- flexShrink: 0,
2570
- background: t.panelBg,
2571
- borderRight: `1px solid ${t.panelBorder}`,
2572
- display: "flex",
2573
- flexDirection: "column",
2574
- alignItems: "center",
2575
- paddingTop: 8,
2576
- gap: 6
2577
- }, children: [
2578
- /* @__PURE__ */ jsx7(
2579
- "button",
2580
- {
2581
- onClick: onToggle,
2582
- title: "Open node list",
2583
- "aria-expanded": false,
2584
- "aria-label": "Open node list",
2585
- style: { background: "none", border: "none", cursor: "pointer", color: t.textMuted, padding: 6, borderRadius: 6, fontSize: 14, lineHeight: 1 },
2586
- children: "\u2630"
2587
- }
2588
- ),
2589
- /* @__PURE__ */ jsx7("div", { style: { fontSize: 10, color: t.textMuted, fontWeight: 700, writingMode: "vertical-rl", transform: "rotate(180deg)", letterSpacing: 0.5 }, children: model.nodes.length })
2590
- ] });
3454
+ return /* @__PURE__ */ jsxs7(
3455
+ "div",
3456
+ {
3457
+ style: {
3458
+ width: 36,
3459
+ flexShrink: 0,
3460
+ background: t.panelBg,
3461
+ borderRight: `1px solid ${t.panelBorder}`,
3462
+ display: "flex",
3463
+ flexDirection: "column",
3464
+ alignItems: "center",
3465
+ paddingTop: 8,
3466
+ gap: 6
3467
+ },
3468
+ children: [
3469
+ /* @__PURE__ */ jsx7(
3470
+ "button",
3471
+ {
3472
+ onClick: onToggle,
3473
+ title: "Open node list",
3474
+ "aria-expanded": false,
3475
+ "aria-label": "Open node list",
3476
+ style: {
3477
+ background: "none",
3478
+ border: "none",
3479
+ cursor: "pointer",
3480
+ color: t.textMuted,
3481
+ padding: 6,
3482
+ borderRadius: 6,
3483
+ fontSize: 14,
3484
+ lineHeight: 1
3485
+ },
3486
+ children: "\u2630"
3487
+ }
3488
+ ),
3489
+ /* @__PURE__ */ jsx7(
3490
+ "div",
3491
+ {
3492
+ style: {
3493
+ fontSize: 10,
3494
+ color: t.textMuted,
3495
+ fontWeight: 700,
3496
+ writingMode: "vertical-rl",
3497
+ transform: "rotate(180deg)",
3498
+ letterSpacing: 0.5
3499
+ },
3500
+ children: model.nodes.length
3501
+ }
3502
+ )
3503
+ ]
3504
+ }
3505
+ );
2591
3506
  }
2592
- return /* @__PURE__ */ jsxs7("div", { style: {
2593
- width: 216,
2594
- flexShrink: 0,
2595
- background: t.panelBg,
2596
- borderRight: `1px solid ${t.panelBorder}`,
2597
- display: "flex",
2598
- flexDirection: "column",
2599
- overflow: "hidden"
2600
- }, children: [
2601
- /* @__PURE__ */ jsxs7("div", { style: {
2602
- display: "flex",
2603
- alignItems: "center",
2604
- justifyContent: "space-between",
2605
- padding: "10px 12px",
2606
- borderBottom: `1px solid ${t.panelBorder}`,
2607
- flexShrink: 0
2608
- }, children: [
2609
- /* @__PURE__ */ jsxs7("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
2610
- /* @__PURE__ */ jsx7("span", { style: { fontSize: 11, fontWeight: 700, color: t.textSecondary, textTransform: "uppercase", letterSpacing: 0.7 }, children: variant === "question" ? "Questions" : variant === "journey" ? "Steps" : "Nodes" }),
2611
- /* @__PURE__ */ jsx7("span", { style: {
2612
- fontSize: 10,
2613
- fontWeight: 700,
2614
- color: t.textMuted,
2615
- background: isDark ? "#0f172a" : "#f1f5f9",
2616
- padding: "1px 6px",
2617
- borderRadius: 99
2618
- }, children: model.nodes.length })
2619
- ] }),
2620
- /* @__PURE__ */ jsx7(
2621
- "button",
2622
- {
2623
- onClick: onToggle,
2624
- style: { background: "none", border: "none", cursor: "pointer", color: t.textMuted, padding: "2px 4px", borderRadius: 4, fontSize: 13, lineHeight: 1 },
2625
- title: "Collapse",
2626
- "aria-expanded": true,
2627
- "aria-label": "Collapse node list",
2628
- children: "\u2039"
2629
- }
2630
- )
2631
- ] }),
2632
- /* @__PURE__ */ jsx7("div", { style: { padding: "8px 10px", borderBottom: `1px solid ${t.sectionBorder}`, flexShrink: 0 }, children: /* @__PURE__ */ jsxs7("div", { style: { position: "relative" }, children: [
2633
- /* @__PURE__ */ jsx7("span", { style: { position: "absolute", left: 8, top: "50%", transform: "translateY(-50%)", fontSize: 11, color: t.textMuted, pointerEvents: "none" }, children: "\u2315" }),
2634
- /* @__PURE__ */ jsx7(
2635
- "input",
2636
- {
2637
- value: search,
2638
- onChange: (e) => setSearch(e.target.value),
2639
- placeholder: "Search\u2026",
2640
- style: {
2641
- width: "100%",
2642
- padding: "5px 8px 5px 24px",
2643
- border: `1.5px solid ${t.inputBorder}`,
2644
- borderRadius: 7,
2645
- fontSize: 12,
2646
- background: t.inputBg,
2647
- color: t.inputText,
2648
- outline: "none",
2649
- boxSizing: "border-box",
2650
- fontFamily: "inherit"
2651
- }
2652
- }
2653
- )
2654
- ] }) }),
2655
- /* @__PURE__ */ jsxs7("div", { style: { flex: 1, overflowY: "auto", padding: "6px 8px", display: "flex", flexDirection: "column", gap: 2 }, children: [
2656
- filtered.length === 0 && /* @__PURE__ */ jsx7("div", { style: { textAlign: "center", padding: "20px 0", fontSize: 12, color: t.textMuted, fontStyle: "italic" }, children: model.nodes.length === 0 ? "No nodes yet" : "No matches" }),
2657
- filtered.map((node, idx) => {
2658
- const isSelected = selected === node.id;
2659
- const answers = node.metadata?.answers ?? [];
2660
- return /* @__PURE__ */ jsxs7(
2661
- "button",
3507
+ return /* @__PURE__ */ jsxs7(
3508
+ "div",
3509
+ {
3510
+ style: {
3511
+ width: 216,
3512
+ flexShrink: 0,
3513
+ background: t.panelBg,
3514
+ borderRight: `1px solid ${t.panelBorder}`,
3515
+ display: "flex",
3516
+ flexDirection: "column",
3517
+ overflow: "hidden"
3518
+ },
3519
+ children: [
3520
+ /* @__PURE__ */ jsxs7(
3521
+ "div",
2662
3522
  {
2663
- onClick: () => onSelect(node.id),
2664
3523
  style: {
2665
3524
  display: "flex",
2666
3525
  alignItems: "center",
2667
- gap: 8,
2668
- width: "100%",
2669
- padding: "7px 8px",
2670
- textAlign: "left",
2671
- background: isSelected ? acc.fill : "transparent",
2672
- border: isSelected ? `1.5px solid ${acc.border}` : "1.5px solid transparent",
2673
- borderRadius: 8,
2674
- cursor: "pointer",
2675
- fontFamily: "inherit",
2676
- transition: "background 0.1s"
2677
- },
2678
- onMouseEnter: (e) => {
2679
- if (!isSelected) e.currentTarget.style.background = isDark ? "#334155" : "#f1f5f9";
2680
- },
2681
- onMouseLeave: (e) => {
2682
- if (!isSelected) e.currentTarget.style.background = "transparent";
3526
+ justifyContent: "space-between",
3527
+ padding: "10px 12px",
3528
+ borderBottom: `1px solid ${t.panelBorder}`,
3529
+ flexShrink: 0
2683
3530
  },
2684
3531
  children: [
2685
- /* @__PURE__ */ jsx7("div", { style: {
2686
- width: 22,
2687
- height: 22,
2688
- borderRadius: 6,
2689
- flexShrink: 0,
2690
- background: isSelected ? acc.color : isDark ? "#334155" : "#e2e8f0",
2691
- color: isSelected ? "#fff" : t.textMuted,
2692
- display: "flex",
2693
- alignItems: "center",
2694
- justifyContent: "center",
2695
- fontSize: variant === "journey" ? 9 : 11,
2696
- fontWeight: 700
2697
- }, children: variant === "journey" ? idx + 1 : shapeIcon(node) }),
2698
- /* @__PURE__ */ jsxs7("div", { style: { flex: 1, minWidth: 0 }, children: [
2699
- /* @__PURE__ */ jsx7("div", { style: {
2700
- fontSize: 12,
2701
- fontWeight: isSelected ? 600 : 400,
2702
- color: isSelected ? acc.color : t.textPrimary,
2703
- overflow: "hidden",
2704
- textOverflow: "ellipsis",
2705
- whiteSpace: "nowrap",
2706
- lineHeight: 1.3
2707
- }, children: node.label }),
2708
- /* @__PURE__ */ jsx7("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` })
3532
+ /* @__PURE__ */ jsxs7("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
3533
+ /* @__PURE__ */ jsx7(
3534
+ "span",
3535
+ {
3536
+ style: {
3537
+ fontSize: 11,
3538
+ fontWeight: 700,
3539
+ color: t.textSecondary,
3540
+ textTransform: "uppercase",
3541
+ letterSpacing: 0.7
3542
+ },
3543
+ children: variant === "question" ? "Questions" : variant === "journey" ? "Steps" : "Nodes"
3544
+ }
3545
+ ),
3546
+ /* @__PURE__ */ jsx7(
3547
+ "span",
3548
+ {
3549
+ style: {
3550
+ fontSize: 10,
3551
+ fontWeight: 700,
3552
+ color: t.textMuted,
3553
+ background: isDark ? "#0f172a" : "#f1f5f9",
3554
+ padding: "1px 6px",
3555
+ borderRadius: 99
3556
+ },
3557
+ children: model.nodes.length
3558
+ }
3559
+ )
2709
3560
  ] }),
2710
- isSelected && /* @__PURE__ */ jsx7("span", { style: { fontSize: 10, color: acc.color, flexShrink: 0 }, children: "\u25C9" })
3561
+ /* @__PURE__ */ jsx7(
3562
+ "button",
3563
+ {
3564
+ onClick: onToggle,
3565
+ style: {
3566
+ background: "none",
3567
+ border: "none",
3568
+ cursor: "pointer",
3569
+ color: t.textMuted,
3570
+ padding: "2px 4px",
3571
+ borderRadius: 4,
3572
+ fontSize: 13,
3573
+ lineHeight: 1
3574
+ },
3575
+ title: "Collapse",
3576
+ "aria-expanded": true,
3577
+ "aria-label": "Collapse node list",
3578
+ children: "\u2039"
3579
+ }
3580
+ )
2711
3581
  ]
2712
- },
2713
- node.id
2714
- );
2715
- })
2716
- ] })
2717
- ] });
3582
+ }
3583
+ ),
3584
+ /* @__PURE__ */ jsx7(
3585
+ "div",
3586
+ {
3587
+ style: { padding: "8px 10px", borderBottom: `1px solid ${t.sectionBorder}`, flexShrink: 0 },
3588
+ children: /* @__PURE__ */ jsxs7("div", { style: { position: "relative" }, children: [
3589
+ /* @__PURE__ */ jsx7(
3590
+ "span",
3591
+ {
3592
+ style: {
3593
+ position: "absolute",
3594
+ left: 8,
3595
+ top: "50%",
3596
+ transform: "translateY(-50%)",
3597
+ fontSize: 11,
3598
+ color: t.textMuted,
3599
+ pointerEvents: "none"
3600
+ },
3601
+ children: "\u2315"
3602
+ }
3603
+ ),
3604
+ /* @__PURE__ */ jsx7(
3605
+ "input",
3606
+ {
3607
+ value: search,
3608
+ onChange: (e) => setSearch(e.target.value),
3609
+ placeholder: "Search\u2026",
3610
+ style: {
3611
+ width: "100%",
3612
+ padding: "5px 8px 5px 24px",
3613
+ border: `1.5px solid ${t.inputBorder}`,
3614
+ borderRadius: 7,
3615
+ fontSize: 12,
3616
+ background: t.inputBg,
3617
+ color: t.inputText,
3618
+ outline: "none",
3619
+ boxSizing: "border-box",
3620
+ fontFamily: "inherit"
3621
+ }
3622
+ }
3623
+ )
3624
+ ] })
3625
+ }
3626
+ ),
3627
+ /* @__PURE__ */ jsxs7(
3628
+ "div",
3629
+ {
3630
+ style: {
3631
+ flex: 1,
3632
+ overflowY: "auto",
3633
+ padding: "6px 8px",
3634
+ display: "flex",
3635
+ flexDirection: "column",
3636
+ gap: 2
3637
+ },
3638
+ children: [
3639
+ filtered.length === 0 && /* @__PURE__ */ jsx7(
3640
+ "div",
3641
+ {
3642
+ style: {
3643
+ textAlign: "center",
3644
+ padding: "20px 0",
3645
+ fontSize: 12,
3646
+ color: t.textMuted,
3647
+ fontStyle: "italic"
3648
+ },
3649
+ children: model.nodes.length === 0 ? "No nodes yet" : "No matches"
3650
+ }
3651
+ ),
3652
+ filtered.map((node, idx) => {
3653
+ const isSelected = selected === node.id;
3654
+ const answers = node.metadata?.answers ?? [];
3655
+ return /* @__PURE__ */ jsxs7(
3656
+ "button",
3657
+ {
3658
+ onClick: () => onSelect(node.id),
3659
+ style: {
3660
+ display: "flex",
3661
+ alignItems: "center",
3662
+ gap: 8,
3663
+ width: "100%",
3664
+ padding: "7px 8px",
3665
+ textAlign: "left",
3666
+ background: isSelected ? acc.fill : "transparent",
3667
+ border: isSelected ? `1.5px solid ${acc.border}` : "1.5px solid transparent",
3668
+ borderRadius: 8,
3669
+ cursor: "pointer",
3670
+ fontFamily: "inherit",
3671
+ transition: "background 0.1s"
3672
+ },
3673
+ onMouseEnter: (e) => {
3674
+ if (!isSelected)
3675
+ e.currentTarget.style.background = isDark ? "#334155" : "#f1f5f9";
3676
+ },
3677
+ onMouseLeave: (e) => {
3678
+ if (!isSelected) e.currentTarget.style.background = "transparent";
3679
+ },
3680
+ children: [
3681
+ /* @__PURE__ */ jsx7(
3682
+ "div",
3683
+ {
3684
+ style: {
3685
+ width: 22,
3686
+ height: 22,
3687
+ borderRadius: 6,
3688
+ flexShrink: 0,
3689
+ background: isSelected ? acc.color : isDark ? "#334155" : "#e2e8f0",
3690
+ color: isSelected ? "#fff" : t.textMuted,
3691
+ display: "flex",
3692
+ alignItems: "center",
3693
+ justifyContent: "center",
3694
+ fontSize: variant === "journey" ? 9 : 11,
3695
+ fontWeight: 700
3696
+ },
3697
+ children: variant === "journey" ? idx + 1 : shapeIcon(node)
3698
+ }
3699
+ ),
3700
+ /* @__PURE__ */ jsxs7("div", { style: { flex: 1, minWidth: 0 }, children: [
3701
+ /* @__PURE__ */ jsx7(
3702
+ "div",
3703
+ {
3704
+ style: {
3705
+ fontSize: 12,
3706
+ fontWeight: isSelected ? 600 : 400,
3707
+ color: isSelected ? acc.color : t.textPrimary,
3708
+ overflow: "hidden",
3709
+ textOverflow: "ellipsis",
3710
+ whiteSpace: "nowrap",
3711
+ lineHeight: 1.3
3712
+ },
3713
+ children: node.label
3714
+ }
3715
+ ),
3716
+ /* @__PURE__ */ jsx7("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` })
3717
+ ] }),
3718
+ isSelected && /* @__PURE__ */ jsx7("span", { style: { fontSize: 10, color: acc.color, flexShrink: 0 }, children: "\u25C9" })
3719
+ ]
3720
+ },
3721
+ node.id
3722
+ );
3723
+ })
3724
+ ]
3725
+ }
3726
+ )
3727
+ ]
3728
+ }
3729
+ );
2718
3730
  }
2719
3731
 
2720
3732
  // src/ui/render.tsx
@@ -2789,18 +3801,55 @@ var STYLE_LABEL = { pointerEvents: "none", userSelect: "none" };
2789
3801
  var STYLE_BLUR = { filter: "blur(4px)" };
2790
3802
  var STYLE_EDGE_HIT = { cursor: "pointer" };
2791
3803
  var STYLE_NO_EVENTS = { pointerEvents: "none" };
2792
- var STYLE_PORT_HOVER = { cursor: "crosshair", filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.18))" };
2793
- var STYLE_WAYPOINT = { cursor: "grab", filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.25))" };
3804
+ var STYLE_PORT_HOVER = {
3805
+ cursor: "crosshair",
3806
+ filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.18))"
3807
+ };
3808
+ var STYLE_WAYPOINT = {
3809
+ cursor: "grab",
3810
+ filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.25))"
3811
+ };
2794
3812
  var STYLE_EDGE_LABEL_HIT = { cursor: "text" };
2795
- function NodeShape({ node, selected, variant, stepNumber, t, isDark, w }) {
3813
+ function NodeShape({
3814
+ node,
3815
+ selected,
3816
+ variant,
3817
+ stepNumber,
3818
+ t,
3819
+ isDark,
3820
+ w
3821
+ }) {
2796
3822
  const acc = variantAccent(variant, isDark);
2797
3823
  const cx = w / 2, cy = NODE_H2 / 2;
2798
3824
  const stroke = selected ? acc.color : t.nodeStroke;
2799
3825
  const fill = selected ? t.nodeSelectedFill : t.nodeFill;
2800
3826
  const sw = selected ? 1.75 : 1.25;
2801
3827
  const glow = selected && /* @__PURE__ */ jsx8(Fragment2, { children: node.shape === "circle" ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
2802
- /* @__PURE__ */ jsx8("circle", { cx, cy, r: NODE_H2 / 2 + 3, fill: "none", stroke: acc.color, strokeWidth: 6, opacity: 0.18, style: STYLE_BLUR }),
2803
- /* @__PURE__ */ jsx8("circle", { cx, cy, r: NODE_H2 / 2 + 1.5, fill: "none", stroke: acc.color, strokeWidth: 1, opacity: 0.55 })
3828
+ /* @__PURE__ */ jsx8(
3829
+ "circle",
3830
+ {
3831
+ cx,
3832
+ cy,
3833
+ r: NODE_H2 / 2 + 3,
3834
+ fill: "none",
3835
+ stroke: acc.color,
3836
+ strokeWidth: 6,
3837
+ opacity: 0.18,
3838
+ style: STYLE_BLUR
3839
+ }
3840
+ ),
3841
+ /* @__PURE__ */ jsx8(
3842
+ "circle",
3843
+ {
3844
+ cx,
3845
+ cy,
3846
+ r: NODE_H2 / 2 + 1.5,
3847
+ fill: "none",
3848
+ stroke: acc.color,
3849
+ strokeWidth: 1,
3850
+ opacity: 0.55
3851
+ }
3852
+ )
2804
3853
  ] }) : node.shape === "diamond" ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
2805
3854
  /* @__PURE__ */ jsx8(
2806
3855
  "polygon",
@@ -2857,39 +3906,98 @@ function NodeShape({ node, selected, variant, stepNumber, t, isDark, w }) {
2857
3906
  const badgeColor = isDark ? ACCENT.emeraldDark : ACCENT.emerald;
2858
3907
  const badge = variant === "journey" && stepNumber !== void 0 && /* @__PURE__ */ jsxs8(Fragment2, { children: [
2859
3908
  /* @__PURE__ */ jsx8("circle", { cx: 14, cy: 14, r: 10, fill: badgeColor }),
2860
- /* @__PURE__ */ jsx8("text", { x: 14, y: 18, textAnchor: "middle", fontSize: 9, fill: "white", fontWeight: "700", style: STYLE_LABEL, children: stepNumber })
3909
+ /* @__PURE__ */ jsx8(
3910
+ "text",
3911
+ {
3912
+ x: 14,
3913
+ y: 18,
3914
+ textAnchor: "middle",
3915
+ fontSize: 9,
3916
+ fill: "white",
3917
+ fontWeight: "700",
3918
+ style: STYLE_LABEL,
3919
+ children: stepNumber
3920
+ }
3921
+ )
2861
3922
  ] });
2862
3923
  switch (node.shape) {
2863
3924
  case "diamond": {
2864
3925
  const pts = `${cx},0 ${w},${cy} ${cx},${NODE_H2} 0,${cy}`;
2865
3926
  return /* @__PURE__ */ jsxs8(Fragment2, { children: [
2866
3927
  glow,
2867
- /* @__PURE__ */ jsx8("polygon", { points: pts, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
3928
+ /* @__PURE__ */ jsx8(
3929
+ "polygon",
3930
+ {
3931
+ points: pts,
3932
+ fill,
3933
+ stroke,
3934
+ strokeWidth: sw,
3935
+ filter: "url(#nodeShadow)"
3936
+ }
3937
+ ),
2868
3938
  badge
2869
3939
  ] });
2870
3940
  }
2871
3941
  case "circle":
2872
3942
  return /* @__PURE__ */ jsxs8(Fragment2, { children: [
2873
3943
  glow,
2874
- /* @__PURE__ */ jsx8("circle", { cx, cy, r: NODE_H2 / 2 - 1, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
3944
+ /* @__PURE__ */ jsx8(
3945
+ "circle",
3946
+ {
3947
+ cx,
3948
+ cy,
3949
+ r: NODE_H2 / 2 - 1,
3950
+ fill,
3951
+ stroke,
3952
+ strokeWidth: sw,
3953
+ filter: "url(#nodeShadow)"
3954
+ }
3955
+ ),
2875
3956
  badge
2876
3957
  ] });
2877
3958
  case "parallelogram":
2878
3959
  return /* @__PURE__ */ jsxs8(Fragment2, { children: [
2879
3960
  glow,
2880
- /* @__PURE__ */ jsx8("polygon", { points: `14,0 ${w},0 ${w - 14},${NODE_H2} 0,${NODE_H2}`, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
3961
+ /* @__PURE__ */ jsx8(
3962
+ "polygon",
3963
+ {
3964
+ points: `14,0 ${w},0 ${w - 14},${NODE_H2} 0,${NODE_H2}`,
3965
+ fill,
3966
+ stroke,
3967
+ strokeWidth: sw,
3968
+ filter: "url(#nodeShadow)"
3969
+ }
3970
+ ),
2881
3971
  badge
2882
3972
  ] });
2883
3973
  default:
2884
3974
  return /* @__PURE__ */ jsxs8(Fragment2, { children: [
2885
3975
  glow,
2886
- /* @__PURE__ */ jsx8("rect", { width: w, height: NODE_H2, rx: 14, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
3976
+ /* @__PURE__ */ jsx8(
3977
+ "rect",
3978
+ {
3979
+ width: w,
3980
+ height: NODE_H2,
3981
+ rx: 14,
3982
+ fill,
3983
+ stroke,
3984
+ strokeWidth: sw,
3985
+ filter: "url(#nodeShadow)"
3986
+ }
3987
+ ),
2887
3988
  badge
2888
3989
  ] });
2889
3990
  }
2890
3991
  }
2891
3992
  var ANSWER_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
2892
- function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
3993
+ function QuestionNode({
3994
+ node,
3995
+ selected,
3996
+ edges,
3997
+ isDark,
3998
+ onAnswerPortDown,
3999
+ qW
4000
+ }) {
2893
4001
  const answers = node.metadata?.answers ?? [];
2894
4002
  const totalH = questionNodeH2(answers);
2895
4003
  const amber = isDark ? ACCENT.amberDark : ACCENT.amber;
@@ -2937,27 +4045,91 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
2937
4045
  ] });
2938
4046
  return /* @__PURE__ */ jsxs8(Fragment2, { children: [
2939
4047
  glow,
2940
- /* @__PURE__ */ jsx8("rect", { width: qW, height: totalH, rx: 14, fill: nodeBg, stroke: nodeBorder, strokeWidth: selected ? 2 : 1.5, filter: "url(#nodeShadow)" }),
4048
+ /* @__PURE__ */ jsx8(
4049
+ "rect",
4050
+ {
4051
+ width: qW,
4052
+ height: totalH,
4053
+ rx: 14,
4054
+ fill: nodeBg,
4055
+ stroke: nodeBorder,
4056
+ strokeWidth: selected ? 2 : 1.5,
4057
+ filter: "url(#nodeShadow)"
4058
+ }
4059
+ ),
2941
4060
  /* @__PURE__ */ jsx8("clipPath", { id: `qhdr-${node.id}`, children: /* @__PURE__ */ jsx8("rect", { width: qW, height: Q_BASE_H2, rx: 14 }) }),
2942
4061
  /* @__PURE__ */ jsx8("rect", { width: qW, height: Q_BASE_H2, fill: amberSoft, clipPath: `url(#qhdr-${node.id})` }),
2943
4062
  /* @__PURE__ */ jsx8("rect", { x: 0, y: 0, width: 4, height: Q_BASE_H2, rx: 2, fill: amber }),
2944
4063
  /* @__PURE__ */ jsx8("rect", { x: 12, y: 14, width: 28, height: 28, rx: 8, fill: amber }),
2945
- /* @__PURE__ */ jsx8("text", { x: 26, y: 33, textAnchor: "middle", fontSize: 15, fontWeight: "900", fill: "white", style: STYLE_LABEL, children: "?" }),
2946
- /* @__PURE__ */ jsxs8(
4064
+ /* @__PURE__ */ jsx8(
2947
4065
  "text",
2948
4066
  {
4067
+ x: 26,
4068
+ y: 33,
4069
+ textAnchor: "middle",
4070
+ fontSize: 15,
4071
+ fontWeight: "900",
4072
+ fill: "white",
2949
4073
  style: STYLE_LABEL,
2950
- fontFamily: "ui-sans-serif,system-ui,sans-serif",
2951
- children: [
2952
- /* @__PURE__ */ jsx8("tspan", { x: 50, y: 27, fontSize: 9, fontWeight: 700, fill: textSub, letterSpacing: 0.6, textAnchor: "start", children: "QUESTION" }),
2953
- /* @__PURE__ */ jsx8("tspan", { x: 50, dy: 15, fontSize: 13, fontWeight: 700, fill: selected ? amber : textMain, textAnchor: "start", children: node.label })
2954
- ]
4074
+ children: "?"
2955
4075
  }
2956
4076
  ),
4077
+ /* @__PURE__ */ jsxs8("text", { style: STYLE_LABEL, fontFamily: "ui-sans-serif,system-ui,sans-serif", children: [
4078
+ /* @__PURE__ */ jsx8(
4079
+ "tspan",
4080
+ {
4081
+ x: 50,
4082
+ y: 27,
4083
+ fontSize: 9,
4084
+ fontWeight: 700,
4085
+ fill: textSub,
4086
+ letterSpacing: 0.6,
4087
+ textAnchor: "start",
4088
+ children: "QUESTION"
4089
+ }
4090
+ ),
4091
+ /* @__PURE__ */ jsx8(
4092
+ "tspan",
4093
+ {
4094
+ x: 50,
4095
+ dy: 15,
4096
+ fontSize: 13,
4097
+ fontWeight: 700,
4098
+ fill: selected ? amber : textMain,
4099
+ textAnchor: "start",
4100
+ children: node.label
4101
+ }
4102
+ )
4103
+ ] }),
2957
4104
  /* @__PURE__ */ jsx8("line", { x1: 0, y1: Q_BASE_H2, x2: qW, y2: Q_BASE_H2, stroke: amberLine, strokeWidth: 1 }),
2958
4105
  answers.length === 0 && /* @__PURE__ */ jsxs8(Fragment2, { children: [
2959
- /* @__PURE__ */ jsx8("text", { x: qW / 2, y: Q_BASE_H2 + 22, textAnchor: "middle", fontSize: 10, fill: amber, opacity: 0.4, fontWeight: 600, style: STYLE_LABEL, children: "No answers yet" }),
2960
- /* @__PURE__ */ jsx8("text", { x: qW / 2, y: Q_BASE_H2 + 36, textAnchor: "middle", fontSize: 9, fill: textSub, opacity: 0.7, style: STYLE_LABEL, children: "Open panel \u2192 Add Answer" })
4106
+ /* @__PURE__ */ jsx8(
4107
+ "text",
4108
+ {
4109
+ x: qW / 2,
4110
+ y: Q_BASE_H2 + 22,
4111
+ textAnchor: "middle",
4112
+ fontSize: 10,
4113
+ fill: amber,
4114
+ opacity: 0.4,
4115
+ fontWeight: 600,
4116
+ style: STYLE_LABEL,
4117
+ children: "No answers yet"
4118
+ }
4119
+ ),
4120
+ /* @__PURE__ */ jsx8(
4121
+ "text",
4122
+ {
4123
+ x: qW / 2,
4124
+ y: Q_BASE_H2 + 36,
4125
+ textAnchor: "middle",
4126
+ fontSize: 9,
4127
+ fill: textSub,
4128
+ opacity: 0.7,
4129
+ style: STYLE_LABEL,
4130
+ children: "Open panel \u2192 Add Answer"
4131
+ }
4132
+ )
2961
4133
  ] }),
2962
4134
  answers.map((ans, i) => {
2963
4135
  const prevW = answers.slice(0, i).reduce((s2, a) => s2 + answerCardW2(a) + Q_CARD_PAD2, 0);
@@ -3051,7 +4223,22 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
3051
4223
  })
3052
4224
  ] });
3053
4225
  }
3054
- function EdgeLine({ edge, nodes, variant, t, isDark, acc, editing, editValue, onEditChange, onEditCommit, onEditCancel, onDoubleClick, onContextMenu, onWaypointDown }) {
4226
+ function EdgeLine({
4227
+ edge,
4228
+ nodes,
4229
+ variant,
4230
+ t,
4231
+ isDark,
4232
+ acc,
4233
+ editing,
4234
+ editValue,
4235
+ onEditChange,
4236
+ onEditCommit,
4237
+ onEditCancel,
4238
+ onDoubleClick,
4239
+ onContextMenu,
4240
+ onWaypointDown
4241
+ }) {
3055
4242
  const [hovered, setHovered] = useState8(false);
3056
4243
  const from = nodes.find((n) => n.id === edge.from);
3057
4244
  const to = nodes.find((n) => n.id === edge.to);
@@ -3206,7 +4393,7 @@ function EdgeLine({ edge, nodes, variant, t, isDark, acc, editing, editValue, on
3206
4393
  }
3207
4394
 
3208
4395
  // src/ui/Minimap.tsx
3209
- import { useCallback as useCallback6, useRef as useRef4 } from "react";
4396
+ import { useRef as useRef4 } from "react";
3210
4397
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
3211
4398
  var W = 168;
3212
4399
  var H = 112;
@@ -3255,13 +4442,13 @@ function Minimap({
3255
4442
  x: (mx - offsetX) / scale,
3256
4443
  y: (my - offsetY) / scale
3257
4444
  });
3258
- const panTo = useCallback6((e) => {
4445
+ const panTo = (e) => {
3259
4446
  const rect = e.currentTarget.getBoundingClientRect();
3260
4447
  const mx = e.clientX - rect.left;
3261
4448
  const my = e.clientY - rect.top;
3262
4449
  const { x, y } = unproject(mx, my);
3263
4450
  onCenterOn(x, y);
3264
- }, [onCenterOn, scale, offsetX, offsetY]);
4451
+ };
3265
4452
  const onMouseDown = (e) => {
3266
4453
  e.stopPropagation();
3267
4454
  dragRef.current = { active: true };
@@ -3448,29 +4635,106 @@ function ContextMenu({
3448
4635
  fontFamily: "ui-sans-serif,system-ui,sans-serif"
3449
4636
  },
3450
4637
  children: edgeId ? /* @__PURE__ */ jsxs10(Fragment3, { children: [
3451
- /* @__PURE__ */ jsx10("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Edge" }),
4638
+ /* @__PURE__ */ jsx10(
4639
+ "div",
4640
+ {
4641
+ style: {
4642
+ padding: "4px 14px 6px",
4643
+ fontSize: 10,
4644
+ fontWeight: 700,
4645
+ color: muted,
4646
+ textTransform: "uppercase",
4647
+ letterSpacing: 0.8
4648
+ },
4649
+ children: "Edge"
4650
+ }
4651
+ ),
3452
4652
  item("Rename label (dbl-click)", () => onEdgeRename?.()),
3453
4653
  divider2,
3454
- /* @__PURE__ */ jsx10("div", { style: { padding: "4px 14px 2px", fontSize: 9, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Style" }),
3455
- item(`Solid${currentEdgeStyle === "solid" || !currentEdgeStyle ? " \u2713" : ""}`, () => onEdgeStyle?.("solid")),
3456
- item(`Dashed${currentEdgeStyle === "dashed" ? " \u2713" : ""}`, () => onEdgeStyle?.("dashed")),
3457
- item(`Dotted${currentEdgeStyle === "dotted" ? " \u2713" : ""}`, () => onEdgeStyle?.("dotted")),
4654
+ /* @__PURE__ */ jsx10(
4655
+ "div",
4656
+ {
4657
+ style: {
4658
+ padding: "4px 14px 2px",
4659
+ fontSize: 9,
4660
+ fontWeight: 700,
4661
+ color: muted,
4662
+ textTransform: "uppercase",
4663
+ letterSpacing: 0.8
4664
+ },
4665
+ children: "Style"
4666
+ }
4667
+ ),
4668
+ item(
4669
+ `Solid${currentEdgeStyle === "solid" || !currentEdgeStyle ? " \u2713" : ""}`,
4670
+ () => onEdgeStyle?.("solid")
4671
+ ),
4672
+ item(
4673
+ `Dashed${currentEdgeStyle === "dashed" ? " \u2713" : ""}`,
4674
+ () => onEdgeStyle?.("dashed")
4675
+ ),
4676
+ item(
4677
+ `Dotted${currentEdgeStyle === "dotted" ? " \u2713" : ""}`,
4678
+ () => onEdgeStyle?.("dotted")
4679
+ ),
3458
4680
  divider2,
3459
- /* @__PURE__ */ jsx10("div", { style: { padding: "4px 14px 2px", fontSize: 9, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Arrowhead" }),
3460
- item(`Arrow${currentEdgeArrow !== "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("arrow")),
4681
+ /* @__PURE__ */ jsx10(
4682
+ "div",
4683
+ {
4684
+ style: {
4685
+ padding: "4px 14px 2px",
4686
+ fontSize: 9,
4687
+ fontWeight: 700,
4688
+ color: muted,
4689
+ textTransform: "uppercase",
4690
+ letterSpacing: 0.8
4691
+ },
4692
+ children: "Arrowhead"
4693
+ }
4694
+ ),
4695
+ item(
4696
+ `Arrow${currentEdgeArrow !== "none" ? " \u2713" : ""}`,
4697
+ () => onEdgeArrowhead?.("arrow")
4698
+ ),
3461
4699
  item(`None${currentEdgeArrow === "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("none")),
3462
4700
  divider2,
3463
4701
  item("Reset routing", () => onEdgeResetRouting?.(), void 0, !edgeHasWaypoint),
3464
4702
  item("Delete edge", () => onEdgeDelete?.(), "#ef4444")
3465
4703
  ] }) : nodeId ? /* @__PURE__ */ jsxs10(Fragment3, { children: [
3466
- /* @__PURE__ */ jsx10("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Node" }),
4704
+ /* @__PURE__ */ jsx10(
4705
+ "div",
4706
+ {
4707
+ style: {
4708
+ padding: "4px 14px 6px",
4709
+ fontSize: 10,
4710
+ fontWeight: 700,
4711
+ color: muted,
4712
+ textTransform: "uppercase",
4713
+ letterSpacing: 0.8
4714
+ },
4715
+ children: "Node"
4716
+ }
4717
+ ),
3467
4718
  item("Rename (dbl-click)", onRename),
3468
4719
  item("Duplicate", onDuplicate),
3469
4720
  item("Disconnect all edges", onDisconnect),
3470
4721
  divider2,
3471
4722
  item("Delete node", onDelete, "#ef4444")
3472
4723
  ] }) : /* @__PURE__ */ jsxs10(Fragment3, { children: [
3473
- /* @__PURE__ */ jsx10("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Canvas" }),
4724
+ /* @__PURE__ */ jsx10(
4725
+ "div",
4726
+ {
4727
+ style: {
4728
+ padding: "4px 14px 6px",
4729
+ fontSize: 10,
4730
+ fontWeight: 700,
4731
+ color: muted,
4732
+ textTransform: "uppercase",
4733
+ letterSpacing: 0.8
4734
+ },
4735
+ children: "Canvas"
4736
+ }
4737
+ ),
3474
4738
  item("Add node here", onAddNode, acc.color),
3475
4739
  item("Re-center (Ctrl+0)", onReCenter),
3476
4740
  divider2,
@@ -3487,8 +4751,20 @@ var STYLE_LABEL2 = { pointerEvents: "none", userSelect: "none" };
3487
4751
  var STYLE_LIVE_PORT = { opacity: 0.85, pointerEvents: "none" };
3488
4752
  var STYLE_NODE_GRAB = { cursor: "grab" };
3489
4753
  var STYLE_NODE_GRABBING = { cursor: "grabbing" };
3490
- 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))" };
3491
- 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))" };
4754
+ var STYLE_PORT_VISIBLE = {
4755
+ cursor: "crosshair",
4756
+ opacity: 1,
4757
+ transition: "opacity 0.15s",
4758
+ pointerEvents: "all",
4759
+ filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.25))"
4760
+ };
4761
+ var STYLE_PORT_HIDDEN = {
4762
+ cursor: "crosshair",
4763
+ opacity: 0,
4764
+ transition: "opacity 0.15s",
4765
+ pointerEvents: "none",
4766
+ filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.25))"
4767
+ };
3492
4768
  function DiagramCanvas(props) {
3493
4769
  const {
3494
4770
  model,
@@ -3559,25 +4835,35 @@ function DiagramCanvas(props) {
3559
4835
  onCtxEdgeDelete,
3560
4836
  onCtxEdgeResetRouting
3561
4837
  } = props;
3562
- return /* @__PURE__ */ jsxs11("div", { ref: containerRef, style: { flex: 1, overflow: "hidden", position: "relative", background: t.canvas }, children: [
3563
- /* @__PURE__ */ jsxs11(
3564
- "svg",
3565
- {
3566
- ref: svgRef,
3567
- width: "100%",
3568
- height: "100%",
3569
- role: "application",
3570
- "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.`,
3571
- tabIndex: 0,
3572
- style: { display: "block", cursor: pan ? "grabbing" : drag ? "grabbing" : liveEdge ? "crosshair" : "default", userSelect: "none", outline: "none" },
3573
- onMouseDown: onSvgMouseDown,
3574
- onMouseMove,
3575
- onMouseUp,
3576
- onMouseLeave: onMouseUp,
3577
- onContextMenu: onSvgContextMenu,
3578
- children: [
3579
- /* @__PURE__ */ jsxs11("defs", { children: [
3580
- /* @__PURE__ */ jsx11("style", { children: reducedMotion ? `
4838
+ return /* @__PURE__ */ jsxs11(
4839
+ "div",
4840
+ {
4841
+ ref: containerRef,
4842
+ style: { flex: 1, overflow: "hidden", position: "relative", background: t.canvas },
4843
+ children: [
4844
+ /* @__PURE__ */ jsxs11(
4845
+ "svg",
4846
+ {
4847
+ ref: svgRef,
4848
+ width: "100%",
4849
+ height: "100%",
4850
+ role: "application",
4851
+ "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.`,
4852
+ tabIndex: 0,
4853
+ style: {
4854
+ display: "block",
4855
+ cursor: pan ? "grabbing" : drag ? "grabbing" : liveEdge ? "crosshair" : "default",
4856
+ userSelect: "none",
4857
+ outline: "none"
4858
+ },
4859
+ onMouseDown: onSvgMouseDown,
4860
+ onMouseMove,
4861
+ onMouseUp,
4862
+ onMouseLeave: onMouseUp,
4863
+ onContextMenu: onSvgContextMenu,
4864
+ children: [
4865
+ /* @__PURE__ */ jsxs11("defs", { children: [
4866
+ /* @__PURE__ */ jsx11("style", { children: reducedMotion ? `
3581
4867
  .edge-flow { stroke-dasharray: 0; }
3582
4868
  .edge-flow-amber { stroke-dasharray: 0; }
3583
4869
  .edge-live { stroke-dasharray: 4 4; }
@@ -3588,221 +4874,355 @@ function DiagramCanvas(props) {
3588
4874
  .edge-flow-amber { stroke-dasharray: 6 4; animation: edgeFlowFast 0.65s linear infinite; }
3589
4875
  .edge-live { stroke-dasharray: 7 5; animation: edgeFlow 0.55s linear infinite; }
3590
4876
  ` }),
3591
- /* @__PURE__ */ jsx11("pattern", { id: "dots", width: GRID, height: GRID, patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ jsx11("circle", { cx: GRID / 2, cy: GRID / 2, r: 1.1, fill: t.dot }) }),
3592
- /* @__PURE__ */ jsx11("filter", { id: "nodeShadow", x: "-25%", y: "-25%", width: "150%", height: "160%", children: /* @__PURE__ */ jsx11("feDropShadow", { dx: "0", dy: "3", stdDeviation: "5", floodColor: shadowClr, floodOpacity: "1" }) }),
3593
- /* @__PURE__ */ jsx11("marker", { id: "arrowhead", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ jsx11("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: arrowClr }) }),
3594
- /* @__PURE__ */ jsx11("marker", { id: "arrowAmber", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ jsx11("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: amberArrow }) }),
3595
- /* @__PURE__ */ jsx11("marker", { id: "arrowLive", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ jsx11("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: acc.color }) })
3596
- ] }),
3597
- /* @__PURE__ */ jsx11("rect", { width: "100%", height: "100%", fill: "url(#dots)", "data-bg": "1" }),
3598
- /* @__PURE__ */ jsxs11("g", { transform: `translate(${transform.x},${transform.y}) scale(${transform.scale})`, children: [
3599
- model.edges.map((e) => /* @__PURE__ */ jsx11(
3600
- EdgeLine,
3601
- {
3602
- edge: e,
3603
- nodes: model.nodes,
3604
- variant,
3605
- t,
3606
- isDark,
3607
- acc,
3608
- editing: editingEdgeId === e.id,
3609
- editValue: editEdgeLabel,
3610
- onEditChange: setEditEdgeLabel,
3611
- onEditCommit: commitEdgeEdit,
3612
- onEditCancel: () => setEditingEdgeId(null),
3613
- onDoubleClick: beginEditEdge,
3614
- onContextMenu: onEdgeContextMenu,
3615
- onWaypointDown: (ev, edgeId) => setWaypointDrag(edgeId)
3616
- },
3617
- e.id
3618
- )),
3619
- liveEdge && (() => {
3620
- const d = bezierPath2(liveEdge.fromX, liveEdge.fromY, liveEdge.toX, liveEdge.toY, liveEdge.exitDir);
3621
- return /* @__PURE__ */ jsx11("path", { d, fill: "none", stroke: acc.color, strokeWidth: 2, strokeLinecap: "round", className: "edge-live", opacity: 0.8, markerEnd: "url(#arrowLive)" });
3622
- })(),
3623
- alignGuides?.x && /* @__PURE__ */ jsx11(
3624
- "line",
3625
- {
3626
- x1: alignGuides.x.pos,
3627
- x2: alignGuides.x.pos,
3628
- y1: alignGuides.x.minY,
3629
- y2: alignGuides.x.maxY,
3630
- stroke: acc.color,
3631
- strokeWidth: 1 / transform.scale,
3632
- strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
3633
- opacity: 0.85,
3634
- pointerEvents: "none"
3635
- }
3636
- ),
3637
- alignGuides?.y && /* @__PURE__ */ jsx11(
3638
- "line",
3639
- {
3640
- y1: alignGuides.y.pos,
3641
- y2: alignGuides.y.pos,
3642
- x1: alignGuides.y.minX,
3643
- x2: alignGuides.y.maxX,
3644
- stroke: acc.color,
3645
- strokeWidth: 1 / transform.scale,
3646
- strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
3647
- opacity: 0.85,
3648
- pointerEvents: "none"
3649
- }
3650
- ),
3651
- model.nodes.map((node, idx) => {
3652
- const isHovered = hoveredId === node.id;
3653
- const isQuestion2 = variant === "question";
3654
- const { w: nW } = nodeDims(node, variant);
3655
- const isSelected = selectedSet.has(node.id);
3656
- return /* @__PURE__ */ jsxs11(
3657
- "g",
3658
- {
3659
- transform: `translate(${node.x ?? 0},${node.y ?? 0})`,
3660
- role: "button",
3661
- tabIndex: 0,
3662
- "aria-label": `${variantLabel} ${variant === "journey" ? idx + 1 + ": " : ""}${node.label}${isSelected ? ", selected" : ""}`,
3663
- style: drag?.nodeId === node.id ? STYLE_NODE_GRABBING : STYLE_NODE_GRAB,
3664
- onMouseDown: (e) => onNodeMouseDown(e, node.id),
3665
- onMouseUp: (e) => onNodeMouseUp(e, node.id),
3666
- onDoubleClick: (e) => onNodeDblClick(e, node.id),
3667
- onContextMenu: (e) => onNodeContextMenu(e, node.id),
3668
- onMouseEnter: () => setHoveredId(node.id),
3669
- onMouseLeave: () => setHoveredId(null),
3670
- onFocus: () => setHoveredId(node.id),
3671
- onBlur: () => setHoveredId(null),
3672
- onKeyDown: (e) => {
3673
- if (e.key === "F2" || e.key === "Enter" && !e.ctrlKey && !e.metaKey) {
3674
- e.preventDefault();
3675
- setEditingId(node.id);
3676
- setEditLabel(node.label);
3677
- }
3678
- },
3679
- children: [
3680
- /* @__PURE__ */ jsx11("title", { children: `${variantLabel}: ${node.label}` }),
3681
- isQuestion2 ? /* @__PURE__ */ jsx11(QuestionNode, { node, selected: isSelected, edges: model.edges, isDark, onAnswerPortDown, qW: nW }) : /* @__PURE__ */ jsxs11(Fragment4, { children: [
3682
- /* @__PURE__ */ jsx11(NodeShape, { node, selected: isSelected, variant, stepNumber: variant === "journey" ? idx + 1 : void 0, t, isDark, w: nW }),
3683
- editingId === node.id ? /* @__PURE__ */ jsx11("foreignObject", { x: 6, y: 6, width: nW - 12, height: NODE_H2 - 12, children: /* @__PURE__ */ jsx11(
3684
- "input",
3685
- {
3686
- autoFocus: true,
3687
- value: editLabel,
3688
- onChange: (e) => setEditLabel(e.target.value),
3689
- onBlur: commitEdit,
3690
- onKeyDown: (e) => {
3691
- if (e.key === "Enter") commitEdit();
3692
- if (e.key === "Escape") setEditingId(null);
3693
- },
3694
- 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 }
3695
- }
3696
- ) }) : /* @__PURE__ */ jsx11("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 }),
3697
- /* @__PURE__ */ jsx11(
3698
- "circle",
3699
- {
3700
- cx: nW / 2,
3701
- cy: NODE_H2 + 1,
3702
- r: portR,
3703
- fill: acc.color,
3704
- stroke: isDark ? "#0f172a" : "white",
3705
- strokeWidth: 2,
3706
- style: isHovered || isCoarse ? STYLE_PORT_VISIBLE : STYLE_PORT_HIDDEN,
3707
- onMouseDown: (e) => onPortMouseDown(e, node.id)
3708
- }
3709
- )
3710
- ] }),
3711
- liveEdge && liveEdge.fromId !== node.id && /* @__PURE__ */ jsx11("circle", { cx: nW / 2, cy: -1, r: portR, fill: acc.color, stroke: isDark ? "#0f172a" : "white", strokeWidth: 2, style: STYLE_LIVE_PORT })
3712
- ]
3713
- },
3714
- node.id
3715
- );
3716
- })
3717
- ] })
3718
- ]
3719
- }
3720
- ),
3721
- boxSel && Math.abs(boxSel.cx - boxSel.sx) + Math.abs(boxSel.cy - boxSel.sy) > 4 && containerRef.current && (() => {
3722
- const rect = containerRef.current.getBoundingClientRect();
3723
- const left = Math.min(boxSel.sx, boxSel.cx) - rect.left;
3724
- const top = Math.min(boxSel.sy, boxSel.cy) - rect.top;
3725
- const w = Math.abs(boxSel.cx - boxSel.sx);
3726
- const h = Math.abs(boxSel.cy - boxSel.sy);
3727
- return /* @__PURE__ */ jsx11(
3728
- "div",
3729
- {
3730
- style: {
3731
- position: "absolute",
3732
- left,
3733
- top,
3734
- width: w,
3735
- height: h,
3736
- border: `1px dashed ${acc.color}`,
3737
- background: isDark ? "rgba(99,102,241,0.10)" : "rgba(99,102,241,0.08)",
3738
- pointerEvents: "none",
3739
- borderRadius: 4
4877
+ /* @__PURE__ */ jsx11("pattern", { id: "dots", width: GRID, height: GRID, patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ jsx11("circle", { cx: GRID / 2, cy: GRID / 2, r: 1.1, fill: t.dot }) }),
4878
+ /* @__PURE__ */ jsx11("filter", { id: "nodeShadow", x: "-25%", y: "-25%", width: "150%", height: "160%", children: /* @__PURE__ */ jsx11("feDropShadow", { dx: "0", dy: "3", stdDeviation: "5", floodColor: shadowClr, floodOpacity: "1" }) }),
4879
+ /* @__PURE__ */ jsx11(
4880
+ "marker",
4881
+ {
4882
+ id: "arrowhead",
4883
+ markerWidth: "9",
4884
+ markerHeight: "7",
4885
+ refX: "8",
4886
+ refY: "3.5",
4887
+ orient: "auto",
4888
+ markerUnits: "strokeWidth",
4889
+ children: /* @__PURE__ */ jsx11("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: arrowClr })
4890
+ }
4891
+ ),
4892
+ /* @__PURE__ */ jsx11(
4893
+ "marker",
4894
+ {
4895
+ id: "arrowAmber",
4896
+ markerWidth: "9",
4897
+ markerHeight: "7",
4898
+ refX: "8",
4899
+ refY: "3.5",
4900
+ orient: "auto",
4901
+ markerUnits: "strokeWidth",
4902
+ children: /* @__PURE__ */ jsx11("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: amberArrow })
4903
+ }
4904
+ ),
4905
+ /* @__PURE__ */ jsx11(
4906
+ "marker",
4907
+ {
4908
+ id: "arrowLive",
4909
+ markerWidth: "9",
4910
+ markerHeight: "7",
4911
+ refX: "8",
4912
+ refY: "3.5",
4913
+ orient: "auto",
4914
+ markerUnits: "strokeWidth",
4915
+ children: /* @__PURE__ */ jsx11("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: acc.color })
4916
+ }
4917
+ )
4918
+ ] }),
4919
+ /* @__PURE__ */ jsx11("rect", { width: "100%", height: "100%", fill: "url(#dots)", "data-bg": "1" }),
4920
+ /* @__PURE__ */ jsxs11("g", { transform: `translate(${transform.x},${transform.y}) scale(${transform.scale})`, children: [
4921
+ model.edges.map((e) => /* @__PURE__ */ jsx11(
4922
+ EdgeLine,
4923
+ {
4924
+ edge: e,
4925
+ nodes: model.nodes,
4926
+ variant,
4927
+ t,
4928
+ isDark,
4929
+ acc,
4930
+ editing: editingEdgeId === e.id,
4931
+ editValue: editEdgeLabel,
4932
+ onEditChange: setEditEdgeLabel,
4933
+ onEditCommit: commitEdgeEdit,
4934
+ onEditCancel: () => setEditingEdgeId(null),
4935
+ onDoubleClick: beginEditEdge,
4936
+ onContextMenu: onEdgeContextMenu,
4937
+ onWaypointDown: (ev, edgeId) => setWaypointDrag(edgeId)
4938
+ },
4939
+ e.id
4940
+ )),
4941
+ liveEdge && (() => {
4942
+ const d = bezierPath2(
4943
+ liveEdge.fromX,
4944
+ liveEdge.fromY,
4945
+ liveEdge.toX,
4946
+ liveEdge.toY,
4947
+ liveEdge.exitDir
4948
+ );
4949
+ return /* @__PURE__ */ jsx11(
4950
+ "path",
4951
+ {
4952
+ d,
4953
+ fill: "none",
4954
+ stroke: acc.color,
4955
+ strokeWidth: 2,
4956
+ strokeLinecap: "round",
4957
+ className: "edge-live",
4958
+ opacity: 0.8,
4959
+ markerEnd: "url(#arrowLive)"
4960
+ }
4961
+ );
4962
+ })(),
4963
+ alignGuides?.x && /* @__PURE__ */ jsx11(
4964
+ "line",
4965
+ {
4966
+ x1: alignGuides.x.pos,
4967
+ x2: alignGuides.x.pos,
4968
+ y1: alignGuides.x.minY,
4969
+ y2: alignGuides.x.maxY,
4970
+ stroke: acc.color,
4971
+ strokeWidth: 1 / transform.scale,
4972
+ strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
4973
+ opacity: 0.85,
4974
+ pointerEvents: "none"
4975
+ }
4976
+ ),
4977
+ alignGuides?.y && /* @__PURE__ */ jsx11(
4978
+ "line",
4979
+ {
4980
+ y1: alignGuides.y.pos,
4981
+ y2: alignGuides.y.pos,
4982
+ x1: alignGuides.y.minX,
4983
+ x2: alignGuides.y.maxX,
4984
+ stroke: acc.color,
4985
+ strokeWidth: 1 / transform.scale,
4986
+ strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
4987
+ opacity: 0.85,
4988
+ pointerEvents: "none"
4989
+ }
4990
+ ),
4991
+ model.nodes.map((node, idx) => {
4992
+ const isHovered = hoveredId === node.id;
4993
+ const isQuestion2 = variant === "question";
4994
+ const { w: nW } = nodeDims(node, variant);
4995
+ const isSelected = selectedSet.has(node.id);
4996
+ return /* @__PURE__ */ jsxs11(
4997
+ "g",
4998
+ {
4999
+ transform: `translate(${node.x ?? 0},${node.y ?? 0})`,
5000
+ role: "button",
5001
+ tabIndex: 0,
5002
+ "aria-label": `${variantLabel} ${variant === "journey" ? idx + 1 + ": " : ""}${node.label}${isSelected ? ", selected" : ""}`,
5003
+ style: drag?.nodeId === node.id ? STYLE_NODE_GRABBING : STYLE_NODE_GRAB,
5004
+ onMouseDown: (e) => onNodeMouseDown(e, node.id),
5005
+ onMouseUp: (e) => onNodeMouseUp(e, node.id),
5006
+ onDoubleClick: (e) => onNodeDblClick(e, node.id),
5007
+ onContextMenu: (e) => onNodeContextMenu(e, node.id),
5008
+ onMouseEnter: () => setHoveredId(node.id),
5009
+ onMouseLeave: () => setHoveredId(null),
5010
+ onFocus: () => setHoveredId(node.id),
5011
+ onBlur: () => setHoveredId(null),
5012
+ onKeyDown: (e) => {
5013
+ if (e.key === "F2" || e.key === "Enter" && !e.ctrlKey && !e.metaKey) {
5014
+ e.preventDefault();
5015
+ setEditingId(node.id);
5016
+ setEditLabel(node.label);
5017
+ }
5018
+ },
5019
+ children: [
5020
+ /* @__PURE__ */ jsx11("title", { children: `${variantLabel}: ${node.label}` }),
5021
+ isQuestion2 ? /* @__PURE__ */ jsx11(
5022
+ QuestionNode,
5023
+ {
5024
+ node,
5025
+ selected: isSelected,
5026
+ edges: model.edges,
5027
+ isDark,
5028
+ onAnswerPortDown,
5029
+ qW: nW
5030
+ }
5031
+ ) : /* @__PURE__ */ jsxs11(Fragment4, { children: [
5032
+ /* @__PURE__ */ jsx11(
5033
+ NodeShape,
5034
+ {
5035
+ node,
5036
+ selected: isSelected,
5037
+ variant,
5038
+ stepNumber: variant === "journey" ? idx + 1 : void 0,
5039
+ t,
5040
+ isDark,
5041
+ w: nW
5042
+ }
5043
+ ),
5044
+ editingId === node.id ? /* @__PURE__ */ jsx11("foreignObject", { x: 6, y: 6, width: nW - 12, height: NODE_H2 - 12, children: /* @__PURE__ */ jsx11(
5045
+ "input",
5046
+ {
5047
+ autoFocus: true,
5048
+ value: editLabel,
5049
+ onChange: (e) => setEditLabel(e.target.value),
5050
+ onBlur: commitEdit,
5051
+ onKeyDown: (e) => {
5052
+ if (e.key === "Enter") commitEdit();
5053
+ if (e.key === "Escape") setEditingId(null);
5054
+ },
5055
+ style: {
5056
+ width: "100%",
5057
+ height: "100%",
5058
+ border: "none",
5059
+ borderRadius: 6,
5060
+ outline: `2px solid ${acc.color}`,
5061
+ textAlign: "center",
5062
+ fontSize: 13,
5063
+ fontWeight: 500,
5064
+ background: t.inputBg,
5065
+ boxSizing: "border-box",
5066
+ padding: "0 6px",
5067
+ fontFamily: "inherit",
5068
+ color: t.inputText
5069
+ }
5070
+ }
5071
+ ) }) : /* @__PURE__ */ jsx11(
5072
+ "text",
5073
+ {
5074
+ x: nW / 2,
5075
+ y: NODE_H2 / 2 + 5,
5076
+ textAnchor: "middle",
5077
+ fontSize: 13,
5078
+ fontWeight: "500",
5079
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
5080
+ fill: isSelected ? acc.color : t.textPrimary,
5081
+ style: STYLE_LABEL2,
5082
+ children: node.label
5083
+ }
5084
+ ),
5085
+ /* @__PURE__ */ jsx11(
5086
+ "circle",
5087
+ {
5088
+ cx: nW / 2,
5089
+ cy: NODE_H2 + 1,
5090
+ r: portR,
5091
+ fill: acc.color,
5092
+ stroke: isDark ? "#0f172a" : "white",
5093
+ strokeWidth: 2,
5094
+ style: isHovered || isCoarse ? STYLE_PORT_VISIBLE : STYLE_PORT_HIDDEN,
5095
+ onMouseDown: (e) => onPortMouseDown(e, node.id)
5096
+ }
5097
+ )
5098
+ ] }),
5099
+ liveEdge && liveEdge.fromId !== node.id && /* @__PURE__ */ jsx11(
5100
+ "circle",
5101
+ {
5102
+ cx: nW / 2,
5103
+ cy: -1,
5104
+ r: portR,
5105
+ fill: acc.color,
5106
+ stroke: isDark ? "#0f172a" : "white",
5107
+ strokeWidth: 2,
5108
+ style: STYLE_LIVE_PORT
5109
+ }
5110
+ )
5111
+ ]
5112
+ },
5113
+ node.id
5114
+ );
5115
+ })
5116
+ ] })
5117
+ ]
3740
5118
  }
3741
- }
3742
- );
3743
- })(),
3744
- model.nodes.length === 0 && /* @__PURE__ */ jsxs11("div", { style: { position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", pointerEvents: "none", gap: 8 }, children: [
3745
- /* @__PURE__ */ jsx11("div", { style: { fontSize: 36, opacity: 0.1, color: t.textPrimary }, children: variant === "question" ? "?" : variant === "journey" ? "\u2197" : "\u2B21" }),
3746
- /* @__PURE__ */ jsxs11("div", { style: { fontSize: 13, color: t.textMuted, fontWeight: 500 }, children: [
3747
- "Click ",
3748
- /* @__PURE__ */ jsxs11("strong", { style: { color: acc.color }, children: [
3749
- "+ ",
3750
- variantLabel
3751
- ] }),
3752
- " to start"
3753
- ] })
3754
- ] }),
3755
- model.nodes.length > 0 && viewport.w > 0 && /* @__PURE__ */ jsx11(
3756
- Minimap,
3757
- {
3758
- model,
3759
- viewportW: viewport.w,
3760
- viewportH: viewport.h,
3761
- transform,
3762
- isDark,
3763
- accentColor: acc.color,
3764
- measureNode: (n) => nodeDims(n, variant),
3765
- onCenterOn: (cx, cy) => {
3766
- setTransform((tr) => ({ ...tr, x: viewport.w / 2 - cx * tr.scale, y: viewport.h / 2 - cy * tr.scale }));
3767
- }
3768
- }
3769
- ),
3770
- ctxMenu && /* @__PURE__ */ jsx11(
3771
- ContextMenu,
3772
- {
3773
- x: ctxMenu.x,
3774
- y: ctxMenu.y,
3775
- nodeId: ctxMenu.nodeId,
3776
- edgeId: ctxMenu.edgeId,
3777
- isDark,
3778
- t,
3779
- acc,
3780
- canUndo: history.canUndo,
3781
- canRedo: history.canRedo,
3782
- onUndo: onCtxUndo,
3783
- onRedo: onCtxRedo,
3784
- onReCenter: onCtxReCenter,
3785
- onAddNode: onCtxAddNode,
3786
- onDuplicate: onCtxDuplicate,
3787
- onRename: onCtxRename,
3788
- onDelete: onCtxDelete,
3789
- onDisconnect: onCtxDisconnect,
3790
- currentEdgeStyle: ctxEdgeStyle,
3791
- currentEdgeArrow: ctxEdgeArrow,
3792
- edgeHasWaypoint: ctxEdgeHasWaypoint,
3793
- onEdgeRename: onCtxEdgeRename,
3794
- onEdgeStyle: onCtxEdgeStyle,
3795
- onEdgeArrowhead: onCtxEdgeArrowhead,
3796
- onEdgeDelete: onCtxEdgeDelete,
3797
- onEdgeResetRouting: onCtxEdgeResetRouting,
3798
- containerRef
3799
- }
3800
- )
3801
- ] });
5119
+ ),
5120
+ boxSel && Math.abs(boxSel.cx - boxSel.sx) + Math.abs(boxSel.cy - boxSel.sy) > 4 && containerRef.current && (() => {
5121
+ const rect = containerRef.current.getBoundingClientRect();
5122
+ const left = Math.min(boxSel.sx, boxSel.cx) - rect.left;
5123
+ const top = Math.min(boxSel.sy, boxSel.cy) - rect.top;
5124
+ const w = Math.abs(boxSel.cx - boxSel.sx);
5125
+ const h = Math.abs(boxSel.cy - boxSel.sy);
5126
+ return /* @__PURE__ */ jsx11(
5127
+ "div",
5128
+ {
5129
+ style: {
5130
+ position: "absolute",
5131
+ left,
5132
+ top,
5133
+ width: w,
5134
+ height: h,
5135
+ border: `1px dashed ${acc.color}`,
5136
+ background: isDark ? "rgba(99,102,241,0.10)" : "rgba(99,102,241,0.08)",
5137
+ pointerEvents: "none",
5138
+ borderRadius: 4
5139
+ }
5140
+ }
5141
+ );
5142
+ })(),
5143
+ model.nodes.length === 0 && /* @__PURE__ */ jsxs11(
5144
+ "div",
5145
+ {
5146
+ style: {
5147
+ position: "absolute",
5148
+ inset: 0,
5149
+ display: "flex",
5150
+ flexDirection: "column",
5151
+ alignItems: "center",
5152
+ justifyContent: "center",
5153
+ pointerEvents: "none",
5154
+ gap: 8
5155
+ },
5156
+ children: [
5157
+ /* @__PURE__ */ jsx11("div", { style: { fontSize: 36, opacity: 0.1, color: t.textPrimary }, children: variant === "question" ? "?" : variant === "journey" ? "\u2197" : "\u2B21" }),
5158
+ /* @__PURE__ */ jsxs11("div", { style: { fontSize: 13, color: t.textMuted, fontWeight: 500 }, children: [
5159
+ "Click ",
5160
+ /* @__PURE__ */ jsxs11("strong", { style: { color: acc.color }, children: [
5161
+ "+ ",
5162
+ variantLabel
5163
+ ] }),
5164
+ " to start"
5165
+ ] })
5166
+ ]
5167
+ }
5168
+ ),
5169
+ model.nodes.length > 0 && viewport.w > 0 && /* @__PURE__ */ jsx11(
5170
+ Minimap,
5171
+ {
5172
+ model,
5173
+ viewportW: viewport.w,
5174
+ viewportH: viewport.h,
5175
+ transform,
5176
+ isDark,
5177
+ accentColor: acc.color,
5178
+ measureNode: (n) => nodeDims(n, variant),
5179
+ onCenterOn: (cx, cy) => {
5180
+ setTransform((tr) => ({
5181
+ ...tr,
5182
+ x: viewport.w / 2 - cx * tr.scale,
5183
+ y: viewport.h / 2 - cy * tr.scale
5184
+ }));
5185
+ }
5186
+ }
5187
+ ),
5188
+ ctxMenu && /* @__PURE__ */ jsx11(
5189
+ ContextMenu,
5190
+ {
5191
+ x: ctxMenu.x,
5192
+ y: ctxMenu.y,
5193
+ nodeId: ctxMenu.nodeId,
5194
+ edgeId: ctxMenu.edgeId,
5195
+ isDark,
5196
+ t,
5197
+ acc,
5198
+ canUndo: history.canUndo,
5199
+ canRedo: history.canRedo,
5200
+ onUndo: onCtxUndo,
5201
+ onRedo: onCtxRedo,
5202
+ onReCenter: onCtxReCenter,
5203
+ onAddNode: onCtxAddNode,
5204
+ onDuplicate: onCtxDuplicate,
5205
+ onRename: onCtxRename,
5206
+ onDelete: onCtxDelete,
5207
+ onDisconnect: onCtxDisconnect,
5208
+ currentEdgeStyle: ctxEdgeStyle,
5209
+ currentEdgeArrow: ctxEdgeArrow,
5210
+ edgeHasWaypoint: ctxEdgeHasWaypoint,
5211
+ onEdgeRename: onCtxEdgeRename,
5212
+ onEdgeStyle: onCtxEdgeStyle,
5213
+ onEdgeArrowhead: onCtxEdgeArrowhead,
5214
+ onEdgeDelete: onCtxEdgeDelete,
5215
+ onEdgeResetRouting: onCtxEdgeResetRouting,
5216
+ containerRef
5217
+ }
5218
+ )
5219
+ ]
5220
+ }
5221
+ );
3802
5222
  }
3803
5223
 
3804
5224
  // src/ui/hooks/useHistory.ts
3805
- import { useCallback as useCallback7, useRef as useRef6, useState as useState10 } from "react";
5225
+ import { useCallback as useCallback6, useRef as useRef6, useState as useState10 } from "react";
3806
5226
  var MAX_HISTORY = 80;
3807
5227
  function useHistory(initial, onChange) {
3808
5228
  const [state, setState] = useState10(initial);
@@ -3810,14 +5230,14 @@ function useHistory(initial, onChange) {
3810
5230
  const idxRef = useRef6(0);
3811
5231
  const [, setTick] = useState10(0);
3812
5232
  const bump = () => setTick((n) => n + 1);
3813
- const apply = useCallback7(
5233
+ const apply = useCallback6(
3814
5234
  (next) => {
3815
5235
  setState(next);
3816
5236
  onChange?.(next);
3817
5237
  },
3818
5238
  [onChange]
3819
5239
  );
3820
- const applyAndPush = useCallback7(
5240
+ const applyAndPush = useCallback6(
3821
5241
  (next) => {
3822
5242
  const stack = stackRef.current.slice(0, idxRef.current + 1);
3823
5243
  stack.push(next);
@@ -3830,7 +5250,7 @@ function useHistory(initial, onChange) {
3830
5250
  },
3831
5251
  [onChange]
3832
5252
  );
3833
- const undo = useCallback7(() => {
5253
+ const undo = useCallback6(() => {
3834
5254
  if (idxRef.current <= 0) return;
3835
5255
  idxRef.current--;
3836
5256
  const next = stackRef.current[idxRef.current];
@@ -3838,7 +5258,7 @@ function useHistory(initial, onChange) {
3838
5258
  onChange?.(next);
3839
5259
  bump();
3840
5260
  }, [onChange]);
3841
- const redo = useCallback7(() => {
5261
+ const redo = useCallback6(() => {
3842
5262
  if (idxRef.current >= stackRef.current.length - 1) return;
3843
5263
  idxRef.current++;
3844
5264
  const next = stackRef.current[idxRef.current];
@@ -3995,7 +5415,18 @@ function useCanvasTouch(ref, {
3995
5415
  el.removeEventListener("touchend", onEnd);
3996
5416
  el.removeEventListener("touchcancel", onEnd);
3997
5417
  };
3998
- }, [ref, transform.scale, transform.x, transform.y, setTransform, onLongPress, minScale, maxScale, longPressMs, longPressSlop]);
5418
+ }, [
5419
+ ref,
5420
+ transform.scale,
5421
+ transform.x,
5422
+ transform.y,
5423
+ setTransform,
5424
+ onLongPress,
5425
+ minScale,
5426
+ maxScale,
5427
+ longPressMs,
5428
+ longPressSlop
5429
+ ]);
3999
5430
  }
4000
5431
 
4001
5432
  // src/ui/hooks/useElementSize.ts
@@ -4109,7 +5540,17 @@ function nearestInDirection(fromX, fromY, dir, candidates) {
4109
5540
 
4110
5541
  // src/ui/DiagramEditor.tsx
4111
5542
  import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
4112
- 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 };
5543
+ var STYLE_SR_ONLY = {
5544
+ position: "absolute",
5545
+ width: 1,
5546
+ height: 1,
5547
+ padding: 0,
5548
+ margin: -1,
5549
+ overflow: "hidden",
5550
+ clip: "rect(0 0 0 0)",
5551
+ whiteSpace: "nowrap",
5552
+ border: 0
5553
+ };
4113
5554
  var STYLE_FLEX_ROW = { flex: 1, display: "flex", overflow: "hidden" };
4114
5555
  function DiagramEditor(props) {
4115
5556
  if (props.initialModel?.type === "sequence") {
@@ -4141,7 +5582,7 @@ function FlowchartEditor({
4141
5582
  themeOverrides
4142
5583
  }) {
4143
5584
  const base = initialModel ? { ...initialModel, variant: initialModel.variant ?? variant } : presetFlowchartModel(variant);
4144
- const notify = useCallback8((m) => onChange?.(m), [onChange]);
5585
+ const notify = useCallback7((m) => onChange?.(m), [onChange]);
4145
5586
  const history = useHistory(base, notify);
4146
5587
  const { state: model, apply: applyModel, applyAndPush, undo, redo } = history;
4147
5588
  const { toasts, showToast, dismissToast } = useToast();
@@ -4156,16 +5597,16 @@ function FlowchartEditor({
4156
5597
  const [waypointDrag, setWaypointDrag] = useState12(null);
4157
5598
  const groupDragOriginsRef = useRef7(null);
4158
5599
  const clipboardRef = useRef7(null);
4159
- const selectOne = useCallback8((id) => {
5600
+ const selectOne = useCallback7((id) => {
4160
5601
  setSelected(id);
4161
5602
  setSelectedSet(id ? /* @__PURE__ */ new Set([id]) : /* @__PURE__ */ new Set());
4162
5603
  }, []);
4163
- const toggleSelect = useCallback8((id) => {
5604
+ const toggleSelect = useCallback7((id) => {
4164
5605
  setSelectedSet((prev) => {
4165
5606
  const next = new Set(prev);
4166
5607
  if (next.has(id)) {
4167
5608
  next.delete(id);
4168
- const last = next.size ? Array.from(next)[next.size - 1] : null;
5609
+ const last = next.size ? Array.from(next)[next.size - 1] ?? null : null;
4169
5610
  setSelected(last);
4170
5611
  } else {
4171
5612
  next.add(id);
@@ -4174,7 +5615,7 @@ function FlowchartEditor({
4174
5615
  return next;
4175
5616
  });
4176
5617
  }, []);
4177
- const clearSelection = useCallback8(() => {
5618
+ const clearSelection = useCallback7(() => {
4178
5619
  setSelected(null);
4179
5620
  setSelectedSet(/* @__PURE__ */ new Set());
4180
5621
  }, []);
@@ -4189,11 +5630,14 @@ function FlowchartEditor({
4189
5630
  const svgRef = useRef7(null);
4190
5631
  const containerRef = useRef7(null);
4191
5632
  const reducedMotion = usePrefersReducedMotion();
4192
- const { t, isDark } = useEditorTheme(theme, themeOverrides, { light: lightTheme, dark: darkTheme });
5633
+ const { t, isDark } = useEditorTheme(theme, themeOverrides, {
5634
+ light: lightTheme,
5635
+ dark: darkTheme
5636
+ });
4193
5637
  const isCoarse = useIsCoarsePointer();
4194
5638
  const portR = isCoarse ? 9 : 6;
4195
5639
  const viewport = useElementSize(svgRef);
4196
- const reCenter = useCallback8(() => {
5640
+ const reCenter = useCallback7(() => {
4197
5641
  if (!svgRef.current) return;
4198
5642
  const rect = svgRef.current.getBoundingClientRect();
4199
5643
  const W2 = rect.width, H2 = rect.height;
@@ -4217,52 +5661,65 @@ function FlowchartEditor({
4217
5661
  const cx = (minX + maxX) / 2, cy = (minY + maxY) / 2;
4218
5662
  setTransform({ scale, x: W2 / 2 - cx * scale, y: H2 / 2 - cy * scale });
4219
5663
  }, [model.nodes, variant]);
4220
- const jumpToNode = useCallback8((nodeId) => {
4221
- const node = model.nodes.find((n) => n.id === nodeId);
4222
- if (!node || !svgRef.current) return;
4223
- const rect = svgRef.current.getBoundingClientRect();
4224
- const { w: nw, h: nh } = nodeDims(node, variant);
4225
- const cx = (node.x ?? 0) + nw / 2;
4226
- const cy = (node.y ?? 0) + nh / 2;
4227
- const scale = Math.min(Math.max(transform.scale, 0.8), 1.4);
4228
- setTransform({ scale, x: rect.width / 2 - cx * scale, y: rect.height / 2 - cy * scale });
4229
- selectOne(nodeId);
4230
- }, [model.nodes, variant, transform.scale, selectOne]);
4231
- const duplicateIds = useCallback8((ids) => {
4232
- if (ids.length === 0) return;
4233
- const idSet = new Set(ids);
4234
- const idMap = /* @__PURE__ */ new Map();
4235
- const nextNode = makeIdSource("node", model.nodes);
4236
- const nextEdge = makeIdSource("e", model.edges);
4237
- const newNodes = [];
4238
- for (const oldId of ids) {
4239
- const n = model.nodes.find((x) => x.id === oldId);
4240
- if (!n) continue;
4241
- const newId = nextNode();
4242
- idMap.set(oldId, newId);
4243
- newNodes.push({
4244
- ...n,
4245
- id: newId,
4246
- label: ids.length === 1 ? n.label + " (copy)" : n.label,
4247
- x: (n.x ?? 0) + 32,
4248
- y: (n.y ?? 0) + 32
4249
- });
4250
- }
4251
- const newEdges = [];
4252
- for (const e of model.edges) {
4253
- if (idSet.has(e.from) && idSet.has(e.to)) {
4254
- newEdges.push({ ...e, id: nextEdge(), from: idMap.get(e.from), to: idMap.get(e.to) });
5664
+ const jumpToNode = useCallback7(
5665
+ (nodeId) => {
5666
+ const node = model.nodes.find((n) => n.id === nodeId);
5667
+ if (!node || !svgRef.current) return;
5668
+ const rect = svgRef.current.getBoundingClientRect();
5669
+ const { w: nw, h: nh } = nodeDims(node, variant);
5670
+ const cx = (node.x ?? 0) + nw / 2;
5671
+ const cy = (node.y ?? 0) + nh / 2;
5672
+ const scale = Math.min(Math.max(transform.scale, 0.8), 1.4);
5673
+ setTransform({ scale, x: rect.width / 2 - cx * scale, y: rect.height / 2 - cy * scale });
5674
+ selectOne(nodeId);
5675
+ },
5676
+ [model.nodes, variant, transform.scale, selectOne]
5677
+ );
5678
+ const duplicateIds = useCallback7(
5679
+ (ids) => {
5680
+ if (ids.length === 0) return;
5681
+ const idSet = new Set(ids);
5682
+ const idMap = /* @__PURE__ */ new Map();
5683
+ const nextNode = makeIdSource("node", model.nodes);
5684
+ const nextEdge = makeIdSource("e", model.edges);
5685
+ const newNodes = [];
5686
+ for (const oldId of ids) {
5687
+ const n = model.nodes.find((x) => x.id === oldId);
5688
+ if (!n) continue;
5689
+ const newId = nextNode();
5690
+ idMap.set(oldId, newId);
5691
+ newNodes.push({
5692
+ ...n,
5693
+ id: newId,
5694
+ label: ids.length === 1 ? n.label + " (copy)" : n.label,
5695
+ x: (n.x ?? 0) + 32,
5696
+ y: (n.y ?? 0) + 32
5697
+ });
4255
5698
  }
4256
- }
4257
- const m = { ...model, nodes: [...model.nodes, ...newNodes], edges: [...model.edges, ...newEdges] };
4258
- applyAndPush(m);
4259
- const newIds = newNodes.map((n) => n.id);
4260
- setSelected(newIds[newIds.length - 1] ?? null);
4261
- setSelectedSet(new Set(newIds));
4262
- }, [model, applyAndPush]);
4263
- const duplicateNode = useCallback8((nodeId) => {
4264
- duplicateIds([nodeId]);
4265
- }, [duplicateIds]);
5699
+ const newEdges = [];
5700
+ for (const e of model.edges) {
5701
+ if (idSet.has(e.from) && idSet.has(e.to)) {
5702
+ newEdges.push({ ...e, id: nextEdge(), from: idMap.get(e.from), to: idMap.get(e.to) });
5703
+ }
5704
+ }
5705
+ const m = {
5706
+ ...model,
5707
+ nodes: [...model.nodes, ...newNodes],
5708
+ edges: [...model.edges, ...newEdges]
5709
+ };
5710
+ applyAndPush(m);
5711
+ const newIds = newNodes.map((n) => n.id);
5712
+ setSelected(newIds[newIds.length - 1] ?? null);
5713
+ setSelectedSet(new Set(newIds));
5714
+ },
5715
+ [model, applyAndPush]
5716
+ );
5717
+ const duplicateNode = useCallback7(
5718
+ (nodeId) => {
5719
+ duplicateIds([nodeId]);
5720
+ },
5721
+ [duplicateIds]
5722
+ );
4266
5723
  useEffect10(() => {
4267
5724
  if (!ctxMenu) return;
4268
5725
  const close = () => setCtxMenu(null);
@@ -4270,18 +5727,27 @@ function FlowchartEditor({
4270
5727
  return () => window.removeEventListener("mousedown", close);
4271
5728
  }, [ctxMenu]);
4272
5729
  const keyCommands = [
4273
- { match: (e) => (e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey, run: () => {
4274
- undo();
4275
- return true;
4276
- } },
4277
- { match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "y" || e.shiftKey && e.key === "z"), run: () => {
4278
- redo();
4279
- return true;
4280
- } },
4281
- { match: (e) => (e.ctrlKey || e.metaKey) && e.key === "0", run: () => {
4282
- reCenter();
4283
- return true;
4284
- } },
5730
+ {
5731
+ match: (e) => (e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey,
5732
+ run: () => {
5733
+ undo();
5734
+ return true;
5735
+ }
5736
+ },
5737
+ {
5738
+ match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "y" || e.shiftKey && e.key === "z"),
5739
+ run: () => {
5740
+ redo();
5741
+ return true;
5742
+ }
5743
+ },
5744
+ {
5745
+ match: (e) => (e.ctrlKey || e.metaKey) && e.key === "0",
5746
+ run: () => {
5747
+ reCenter();
5748
+ return true;
5749
+ }
5750
+ },
4285
5751
  {
4286
5752
  match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "d" || e.key === "D") && selectedSet.size > 0,
4287
5753
  run: () => {
@@ -4321,12 +5787,18 @@ function FlowchartEditor({
4321
5787
  from: idMap.get(ed.from) ?? ed.from,
4322
5788
  to: idMap.get(ed.to) ?? ed.to
4323
5789
  }));
4324
- const m = { ...model, nodes: [...model.nodes, ...newNodes], edges: [...model.edges, ...newEdges] };
5790
+ const m = {
5791
+ ...model,
5792
+ nodes: [...model.nodes, ...newNodes],
5793
+ edges: [...model.edges, ...newEdges]
5794
+ };
4325
5795
  applyAndPush(m);
4326
5796
  const newIds = newNodes.map((n) => n.id);
4327
- setSelected(newIds[newIds.length - 1]);
5797
+ setSelected(newIds[newIds.length - 1] ?? null);
4328
5798
  setSelectedSet(new Set(newIds));
4329
- setAnnouncement(`Pasted ${newIds.length} ${variantLabel.toLowerCase()}${newIds.length === 1 ? "" : "s"}.`);
5799
+ setAnnouncement(
5800
+ `Pasted ${newIds.length} ${variantLabel.toLowerCase()}${newIds.length === 1 ? "" : "s"}.`
5801
+ );
4330
5802
  return true;
4331
5803
  }
4332
5804
  },
@@ -4352,7 +5824,9 @@ function FlowchartEditor({
4352
5824
  };
4353
5825
  applyAndPush(updated);
4354
5826
  clearSelection();
4355
- setAnnouncement(`Deleted ${ids.size} ${variantLabel.toLowerCase()}${ids.size === 1 ? "" : "s"}.`);
5827
+ setAnnouncement(
5828
+ `Deleted ${ids.size} ${variantLabel.toLowerCase()}${ids.size === 1 ? "" : "s"}.`
5829
+ );
4356
5830
  return true;
4357
5831
  }
4358
5832
  },
@@ -4387,20 +5861,42 @@ function FlowchartEditor({
4387
5861
  const ids = selectedSet;
4388
5862
  const updated = {
4389
5863
  ...model,
4390
- nodes: model.nodes.map((n) => ids.has(n.id) ? { ...n, x: snap((n.x ?? 0) + dx), y: snap((n.y ?? 0) + dy) } : n)
5864
+ nodes: model.nodes.map(
5865
+ (n) => ids.has(n.id) ? { ...n, x: snap((n.x ?? 0) + dx), y: snap((n.y ?? 0) + dy) } : n
5866
+ )
4391
5867
  };
4392
5868
  applyAndPush(updated);
4393
5869
  return true;
4394
5870
  }
4395
5871
  }
4396
5872
  ];
4397
- useEditorKeyboard(keyCommands, [undo, redo, reCenter, selected, selectedSet, ctxMenu, liveEdge, editingId, boxSel, model, applyAndPush, duplicateNode, clearSelection]);
4398
- const toCanvas = useCallback8((clientX, clientY) => {
4399
- const rect = svgRef.current.getBoundingClientRect();
4400
- return { x: (clientX - rect.left - transform.x) / transform.scale, y: (clientY - rect.top - transform.y) / transform.scale };
4401
- }, [transform]);
5873
+ useEditorKeyboard(keyCommands, [
5874
+ undo,
5875
+ redo,
5876
+ reCenter,
5877
+ selected,
5878
+ selectedSet,
5879
+ ctxMenu,
5880
+ liveEdge,
5881
+ editingId,
5882
+ boxSel,
5883
+ model,
5884
+ applyAndPush,
5885
+ duplicateNode,
5886
+ clearSelection
5887
+ ]);
5888
+ const toCanvas = useCallback7(
5889
+ (clientX, clientY) => {
5890
+ const rect = svgRef.current.getBoundingClientRect();
5891
+ return {
5892
+ x: (clientX - rect.left - transform.x) / transform.scale,
5893
+ y: (clientY - rect.top - transform.y) / transform.scale
5894
+ };
5895
+ },
5896
+ [transform]
5897
+ );
4402
5898
  useCanvasWheel(svgRef, setTransform);
4403
- const onCanvasLongPress = useCallback8((x, y) => {
5899
+ const onCanvasLongPress = useCallback7((x, y) => {
4404
5900
  setCtxMenu({ x, y, nodeId: null });
4405
5901
  }, []);
4406
5902
  useCanvasTouch(svgRef, { transform, setTransform, onLongPress: onCanvasLongPress });
@@ -4409,13 +5905,28 @@ function FlowchartEditor({
4409
5905
  const node = model.nodes.find((n) => n.id === nodeId);
4410
5906
  const { x, y } = toCanvas(e.clientX, e.clientY);
4411
5907
  const nW = nodeWidth2(node.label);
4412
- setLiveEdge({ fromId: nodeId, fromX: (node.x ?? 0) + nW / 2, fromY: (node.y ?? 0) + NODE_H2, exitDir: "bottom", toX: x, toY: y });
5908
+ setLiveEdge({
5909
+ fromId: nodeId,
5910
+ fromX: (node.x ?? 0) + nW / 2,
5911
+ fromY: (node.y ?? 0) + NODE_H2,
5912
+ exitDir: "bottom",
5913
+ toX: x,
5914
+ toY: y
5915
+ });
4413
5916
  };
4414
5917
  const onAnswerPortDown = (e, nodeId, answer, portXInNode, portYInNode) => {
4415
5918
  e.stopPropagation();
4416
5919
  const node = model.nodes.find((n) => n.id === nodeId);
4417
5920
  const { x, y } = toCanvas(e.clientX, e.clientY);
4418
- setLiveEdge({ fromId: nodeId, fromX: (node.x ?? 0) + portXInNode, fromY: (node.y ?? 0) + portYInNode, exitDir: "bottom", answerLabel: answer, toX: x, toY: y });
5921
+ setLiveEdge({
5922
+ fromId: nodeId,
5923
+ fromX: (node.x ?? 0) + portXInNode,
5924
+ fromY: (node.y ?? 0) + portYInNode,
5925
+ exitDir: "bottom",
5926
+ answerLabel: answer,
5927
+ toX: x,
5928
+ toY: y
5929
+ });
4419
5930
  };
4420
5931
  const onNodeMouseDown = (e, id) => {
4421
5932
  e.stopPropagation();
@@ -4442,7 +5953,11 @@ function FlowchartEditor({
4442
5953
  selectOne(id);
4443
5954
  groupDragOriginsRef.current = null;
4444
5955
  }
4445
- setDrag({ nodeId: id, ox: e.clientX - (transform.x + (node.x ?? 0) * transform.scale), oy: e.clientY - (transform.y + (node.y ?? 0) * transform.scale) });
5956
+ setDrag({
5957
+ nodeId: id,
5958
+ ox: e.clientX - (transform.x + (node.x ?? 0) * transform.scale),
5959
+ oy: e.clientY - (transform.y + (node.y ?? 0) * transform.scale)
5960
+ });
4446
5961
  };
4447
5962
  const onNodeMouseUp = (e, targetId) => {
4448
5963
  if (!liveEdge || liveEdge.fromId === targetId) return;
@@ -4452,12 +5967,27 @@ function FlowchartEditor({
4452
5967
  if (label) {
4453
5968
  const existing = model.edges.find((ex) => ex.from === liveEdge.fromId && ex.label === label);
4454
5969
  if (existing) {
4455
- updated = { ...model, edges: model.edges.map((ex) => ex.id === existing.id ? { ...ex, to: targetId } : ex) };
5970
+ updated = {
5971
+ ...model,
5972
+ edges: model.edges.map((ex) => ex.id === existing.id ? { ...ex, to: targetId } : ex)
5973
+ };
4456
5974
  } else {
4457
- updated = { ...model, edges: [...model.edges, { id: nextId("e", model.edges), from: liveEdge.fromId, to: targetId, label }] };
5975
+ updated = {
5976
+ ...model,
5977
+ edges: [
5978
+ ...model.edges,
5979
+ { id: nextId("e", model.edges), from: liveEdge.fromId, to: targetId, label }
5980
+ ]
5981
+ };
4458
5982
  }
4459
5983
  } else {
4460
- updated = { ...model, edges: [...model.edges, { id: nextId("e", model.edges), from: liveEdge.fromId, to: targetId }] };
5984
+ updated = {
5985
+ ...model,
5986
+ edges: [
5987
+ ...model.edges,
5988
+ { id: nextId("e", model.edges), from: liveEdge.fromId, to: targetId }
5989
+ ]
5990
+ };
4461
5991
  }
4462
5992
  applyAndPush(updated);
4463
5993
  setLiveEdge(null);
@@ -4497,7 +6027,9 @@ function FlowchartEditor({
4497
6027
  const wx = snap(x), wy = snap(y);
4498
6028
  const updated = {
4499
6029
  ...model,
4500
- edges: model.edges.map((ed) => ed.id === waypointDrag ? { ...ed, waypoint: { x: wx, y: wy } } : ed)
6030
+ edges: model.edges.map(
6031
+ (ed) => ed.id === waypointDrag ? { ...ed, waypoint: { x: wx, y: wy } } : ed
6032
+ )
4501
6033
  };
4502
6034
  applyModel(updated);
4503
6035
  return;
@@ -4529,12 +6061,23 @@ function FlowchartEditor({
4529
6061
  return { x: n.x ?? 0, y: n.y ?? 0, w: d.w, h: d.h };
4530
6062
  });
4531
6063
  const snapResult = findSiblingSnap({ x: dx, y: dy, w: dW, h: dH }, others);
4532
- setAlignGuides(snapResult.guideX || snapResult.guideY ? { x: snapResult.guideX, y: snapResult.guideY } : null);
4533
- const updated = { ...model, nodes: model.nodes.map((n) => n.id === drag.nodeId ? { ...n, x: snapResult.x, y: snapResult.y } : n) };
6064
+ setAlignGuides(
6065
+ snapResult.guideX || snapResult.guideY ? { x: snapResult.guideX, y: snapResult.guideY } : null
6066
+ );
6067
+ const updated = {
6068
+ ...model,
6069
+ nodes: model.nodes.map(
6070
+ (n) => n.id === drag.nodeId ? { ...n, x: snapResult.x, y: snapResult.y } : n
6071
+ )
6072
+ };
4534
6073
  applyModel(updated);
4535
6074
  }
4536
6075
  } else if (pan) {
4537
- setTransform((tr) => ({ ...tr, x: pan.tx + (e.clientX - pan.ox), y: pan.ty + (e.clientY - pan.oy) }));
6076
+ setTransform((tr) => ({
6077
+ ...tr,
6078
+ x: pan.tx + (e.clientX - pan.ox),
6079
+ y: pan.ty + (e.clientY - pan.oy)
6080
+ }));
4538
6081
  } else if (boxSel) {
4539
6082
  setBoxSel((b) => b ? { ...b, cx: e.clientX, cy: e.clientY } : null);
4540
6083
  }
@@ -4560,7 +6103,7 @@ function FlowchartEditor({
4560
6103
  }
4561
6104
  const arr = Array.from(hits);
4562
6105
  setSelectedSet(hits);
4563
- setSelected(arr.length ? arr[arr.length - 1] : null);
6106
+ setSelected(arr.length ? arr[arr.length - 1] ?? null : null);
4564
6107
  }
4565
6108
  setBoxSel(null);
4566
6109
  }
@@ -4583,7 +6126,10 @@ function FlowchartEditor({
4583
6126
  };
4584
6127
  const commitEdit = () => {
4585
6128
  if (!editingId) return;
4586
- const up = { ...model, nodes: model.nodes.map((n) => n.id === editingId ? { ...n, label: editLabel } : n) };
6129
+ const up = {
6130
+ ...model,
6131
+ nodes: model.nodes.map((n) => n.id === editingId ? { ...n, label: editLabel } : n)
6132
+ };
4587
6133
  applyAndPush(up);
4588
6134
  setEditingId(null);
4589
6135
  };
@@ -4592,20 +6138,28 @@ function FlowchartEditor({
4592
6138
  const p = atCanvasPos ? { x: snap(atCanvasPos.x), y: snap(atCanvasPos.y) } : { x: snap(100 + Math.random() * 240), y: snap(100 + Math.random() * 180) };
4593
6139
  const label = variant === "question" ? "New Question" : variant === "journey" ? `Step ${model.nodes.length + 1}` : "New Step";
4594
6140
  const metadata = variant === "question" ? { answers: [] } : void 0;
4595
- const updated = { ...model, nodes: [...model.nodes, { id, label, shape: "rectangle", metadata, ...p }] };
6141
+ const updated = {
6142
+ ...model,
6143
+ nodes: [...model.nodes, { id, label, shape: "rectangle", metadata, ...p }]
6144
+ };
4596
6145
  applyAndPush(updated);
4597
6146
  selectOne(id);
4598
6147
  setAnnouncement(`Added ${variantLabel.toLowerCase()} "${label}".`);
4599
6148
  };
4600
6149
  const deleteNode = (nodeId) => {
4601
6150
  const node = model.nodes.find((n) => n.id === nodeId);
4602
- const updated = { ...model, nodes: model.nodes.filter((n) => n.id !== nodeId), edges: model.edges.filter((e) => e.from !== nodeId && e.to !== nodeId) };
6151
+ const updated = {
6152
+ ...model,
6153
+ nodes: model.nodes.filter((n) => n.id !== nodeId),
6154
+ edges: model.edges.filter((e) => e.from !== nodeId && e.to !== nodeId)
6155
+ };
4603
6156
  applyAndPush(updated);
4604
6157
  if (selectedSet.has(nodeId)) {
4605
6158
  const next = new Set(selectedSet);
4606
6159
  next.delete(nodeId);
4607
6160
  setSelectedSet(next);
4608
- if (selected === nodeId) setSelected(next.size ? Array.from(next)[next.size - 1] : null);
6161
+ if (selected === nodeId)
6162
+ setSelected(next.size ? Array.from(next)[next.size - 1] ?? null : null);
4609
6163
  }
4610
6164
  if (node) setAnnouncement(`Deleted ${variantLabel.toLowerCase()} "${node.label}".`);
4611
6165
  };
@@ -4637,7 +6191,9 @@ function FlowchartEditor({
4637
6191
  const next = editEdgeLabel.trim();
4638
6192
  const updated = {
4639
6193
  ...model,
4640
- edges: model.edges.map((e) => e.id === editingEdgeId ? { ...e, ...next ? { label: next } : { label: void 0 } } : e)
6194
+ edges: model.edges.map(
6195
+ (e) => e.id === editingEdgeId ? { ...e, ...next ? { label: next } : { label: void 0 } } : e
6196
+ )
4641
6197
  };
4642
6198
  applyAndPush(updated);
4643
6199
  setEditingEdgeId(null);
@@ -4648,11 +6204,17 @@ function FlowchartEditor({
4648
6204
  setCtxMenu({ x: e.clientX, y: e.clientY, nodeId: null, edgeId });
4649
6205
  };
4650
6206
  const setEdgeStyle = (edgeId, style) => {
4651
- const updated = { ...model, edges: model.edges.map((e) => e.id === edgeId ? { ...e, style } : e) };
6207
+ const updated = {
6208
+ ...model,
6209
+ edges: model.edges.map((e) => e.id === edgeId ? { ...e, style } : e)
6210
+ };
4652
6211
  applyAndPush(updated);
4653
6212
  };
4654
6213
  const setEdgeArrowhead = (edgeId, arrowhead) => {
4655
- const updated = { ...model, edges: model.edges.map((e) => e.id === edgeId ? { ...e, arrowhead } : e) };
6214
+ const updated = {
6215
+ ...model,
6216
+ edges: model.edges.map((e) => e.id === edgeId ? { ...e, arrowhead } : e)
6217
+ };
4656
6218
  applyAndPush(updated);
4657
6219
  };
4658
6220
  const deleteEdge = (edgeId) => {
@@ -4672,14 +6234,17 @@ function FlowchartEditor({
4672
6234
  applyAndPush(updated);
4673
6235
  };
4674
6236
  const handleExport = useExporters(model, onExport, "diagram", (msg) => showToast(msg, "success"));
4675
- const positionFlowchartNodes = useCallback8((m) => ({
4676
- ...m,
4677
- nodes: m.nodes.map((n, i) => ({
4678
- ...n,
4679
- x: n.x ?? snap(80 + i % 4 * 200),
4680
- y: n.y ?? snap(80 + Math.floor(i / 4) * 140)
4681
- }))
4682
- }), []);
6237
+ const positionFlowchartNodes = useCallback7(
6238
+ (m) => ({
6239
+ ...m,
6240
+ nodes: m.nodes.map((n, i) => ({
6241
+ ...n,
6242
+ x: n.x ?? snap(80 + i % 4 * 200),
6243
+ y: n.y ?? snap(80 + Math.floor(i / 4) * 140)
6244
+ }))
6245
+ }),
6246
+ []
6247
+ );
4683
6248
  const handleImport = useImporter(applyAndPush, {
4684
6249
  transform: positionFlowchartNodes,
4685
6250
  onSuccess: (msg) => showToast(msg, "success"),
@@ -4690,9 +6255,23 @@ function FlowchartEditor({
4690
6255
  const shadowClr = shadowColor(isDark);
4691
6256
  const arrowClr = arrowColor(isDark);
4692
6257
  const amberArrow = isDark ? ACCENT.amberDark : ACCENT.amber;
4693
- return /* @__PURE__ */ jsxs12("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: [
4694
- /* @__PURE__ */ jsx12(ToastContainer, { toasts, onDismiss: dismissToast }),
4695
- /* @__PURE__ */ jsx12("style", { children: `
6258
+ return /* @__PURE__ */ jsxs12(
6259
+ "div",
6260
+ {
6261
+ className: "fsd-editor",
6262
+ style: {
6263
+ display: "flex",
6264
+ flexDirection: "column",
6265
+ height,
6266
+ width: "100%",
6267
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
6268
+ boxSizing: "border-box",
6269
+ background: t.ctrlsBg,
6270
+ position: "relative"
6271
+ },
6272
+ children: [
6273
+ /* @__PURE__ */ jsx12(ToastContainer, { toasts, onDismiss: dismissToast }),
6274
+ /* @__PURE__ */ jsx12("style", { children: `
4696
6275
  .fsd-editor button:focus-visible,
4697
6276
  .fsd-editor input:focus-visible,
4698
6277
  .fsd-editor textarea:focus-visible,
@@ -4711,209 +6290,293 @@ function FlowchartEditor({
4711
6290
  outline-offset: -2px;
4712
6291
  }
4713
6292
  ` }),
4714
- /* @__PURE__ */ jsx12(
4715
- "div",
4716
- {
4717
- role: "status",
4718
- "aria-live": "polite",
4719
- "aria-atomic": "true",
4720
- style: STYLE_SR_ONLY,
4721
- children: announcement
4722
- }
4723
- ),
4724
- /* @__PURE__ */ jsx12(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
4725
- /* @__PURE__ */ jsxs12("div", { style: { display: "flex", gap: 6, padding: "7px 14px", background: t.ctrlsBg, borderBottom: `1px solid ${t.ctrlsBorder}`, alignItems: "center", flexWrap: "wrap" }, children: [
4726
- /* @__PURE__ */ jsxs12("button", { onClick: () => addNode(), style: ctrlBtn(acc.color, isDark), children: [
4727
- "+ ",
4728
- variantLabel
4729
- ] }),
4730
- selectedSet.size > 0 && /* @__PURE__ */ jsxs12(Fragment5, { children: [
4731
- /* @__PURE__ */ jsx12("div", { style: { width: 1, height: 20, background: t.ctrlsBorder, margin: "0 2px" } }),
4732
- /* @__PURE__ */ jsx12("button", { onClick: deleteSelected, style: { ...ctrlBtn("transparent", isDark), color: "#ef4444", border: `1px solid ${isDark ? "#7f1d1d" : "#fca5a5"}` }, children: selectedSet.size > 1 ? `Delete (${selectedSet.size})` : "Delete" })
4733
- ] }),
4734
- liveEdge && /* @__PURE__ */ jsxs12("span", { style: { fontSize: 11, color: acc.color, fontWeight: 600, marginLeft: 6 }, children: [
4735
- liveEdge.answerLabel ? `Routing "${liveEdge.answerLabel}" \u2192` : "Drop on a node to connect",
4736
- /* @__PURE__ */ jsx12("span", { style: { fontWeight: 400, color: t.textMuted, marginLeft: 6 }, children: "release to cancel" })
4737
- ] }),
4738
- /* @__PURE__ */ jsxs12("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
4739
- variant === "question" ? "drag answer port to connect \xB7 " : "drag port dot \xB7 ",
4740
- "scroll to zoom \xB7 drag to pan"
4741
- ] })
4742
- ] }),
4743
- variant !== "flowchart" && /* @__PURE__ */ jsx12("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" }),
4744
- /* @__PURE__ */ jsxs12("div", { style: STYLE_FLEX_ROW, children: [
4745
- /* @__PURE__ */ jsx12(
4746
- NodeNavigator,
4747
- {
4748
- model,
4749
- selected,
4750
- variant,
4751
- isDark,
4752
- t,
4753
- acc,
4754
- open: navOpen,
4755
- onToggle: () => setNavOpen((v) => !v),
4756
- onSelect: jumpToNode
4757
- }
4758
- ),
4759
- /* @__PURE__ */ jsx12(
4760
- DiagramCanvas,
4761
- {
4762
- model,
4763
- variant,
4764
- variantLabel,
4765
- t,
4766
- isDark,
4767
- acc,
4768
- transform,
4769
- setTransform,
4770
- selected,
4771
- selectedSet,
4772
- hoveredId,
4773
- setHoveredId,
4774
- drag,
4775
- pan,
4776
- liveEdge,
4777
- boxSel,
4778
- alignGuides,
4779
- editingEdgeId,
4780
- editEdgeLabel,
4781
- setEditEdgeLabel,
4782
- commitEdgeEdit,
4783
- setEditingEdgeId,
4784
- beginEditEdge,
4785
- onEdgeContextMenu,
4786
- setWaypointDrag,
4787
- editingId,
4788
- editLabel,
4789
- setEditLabel,
4790
- commitEdit,
4791
- setEditingId,
4792
- onNodeMouseDown,
4793
- onNodeMouseUp,
4794
- onNodeDblClick,
4795
- onNodeContextMenu,
4796
- onPortMouseDown,
4797
- onAnswerPortDown,
4798
- onSvgMouseDown,
4799
- onMouseMove,
4800
- onMouseUp,
4801
- onSvgContextMenu,
4802
- reducedMotion,
4803
- isCoarse,
4804
- portR,
4805
- shadowClr,
4806
- arrowClr,
4807
- amberArrow,
4808
- viewport,
4809
- svgRef,
4810
- containerRef,
4811
- ctxMenu,
4812
- history,
4813
- ctxEdgeStyle: (ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.style ?? "solid",
4814
- ctxEdgeArrow: (ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.arrowhead ?? "arrow",
4815
- ctxEdgeHasWaypoint: !!(ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.waypoint,
4816
- onCtxUndo: () => {
4817
- undo();
4818
- setCtxMenu(null);
4819
- },
4820
- onCtxRedo: () => {
4821
- redo();
4822
- setCtxMenu(null);
4823
- },
4824
- onCtxReCenter: () => {
4825
- reCenter();
4826
- setCtxMenu(null);
4827
- },
4828
- onCtxAddNode: () => {
4829
- const rect = svgRef.current.getBoundingClientRect();
4830
- const cx = (ctxMenu.x - rect.left - transform.x) / transform.scale;
4831
- const cy = (ctxMenu.y - rect.top - transform.y) / transform.scale;
4832
- addNode({ x: cx, y: cy });
4833
- setCtxMenu(null);
4834
- },
4835
- onCtxDuplicate: () => {
4836
- if (ctxMenu?.nodeId) {
4837
- duplicateNode(ctxMenu.nodeId);
4838
- setCtxMenu(null);
4839
- }
4840
- },
4841
- onCtxRename: () => {
4842
- if (ctxMenu?.nodeId) {
4843
- const node = model.nodes.find((n) => n.id === ctxMenu.nodeId);
4844
- setEditingId(ctxMenu.nodeId);
4845
- setEditLabel(node.label);
4846
- setCtxMenu(null);
4847
- }
4848
- },
4849
- onCtxDelete: () => {
4850
- if (ctxMenu?.nodeId) {
4851
- deleteNode(ctxMenu.nodeId);
4852
- setCtxMenu(null);
4853
- }
4854
- },
4855
- onCtxDisconnect: () => {
4856
- if (ctxMenu?.nodeId) {
4857
- const m = { ...model, edges: model.edges.filter((e) => e.from !== ctxMenu.nodeId && e.to !== ctxMenu.nodeId) };
4858
- applyAndPush(m);
4859
- setCtxMenu(null);
4860
- }
4861
- },
4862
- onCtxEdgeRename: () => {
4863
- if (ctxMenu?.edgeId) {
4864
- beginEditEdge(ctxMenu.edgeId);
4865
- setCtxMenu(null);
4866
- }
4867
- },
4868
- onCtxEdgeStyle: (s2) => {
4869
- if (ctxMenu?.edgeId) {
4870
- setEdgeStyle(ctxMenu.edgeId, s2);
4871
- setCtxMenu(null);
4872
- }
4873
- },
4874
- onCtxEdgeArrowhead: (a) => {
4875
- if (ctxMenu?.edgeId) {
4876
- setEdgeArrowhead(ctxMenu.edgeId, a);
4877
- setCtxMenu(null);
4878
- }
4879
- },
4880
- onCtxEdgeDelete: () => {
4881
- if (ctxMenu?.edgeId) {
4882
- deleteEdge(ctxMenu.edgeId);
4883
- setCtxMenu(null);
6293
+ /* @__PURE__ */ jsx12("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", style: STYLE_SR_ONLY, children: announcement }),
6294
+ /* @__PURE__ */ jsx12(
6295
+ Toolbar,
6296
+ {
6297
+ onExport: handleExport,
6298
+ onImport: allowImport ? handleImport : void 0,
6299
+ allowedExports,
6300
+ allowImport
6301
+ }
6302
+ ),
6303
+ /* @__PURE__ */ jsxs12(
6304
+ "div",
6305
+ {
6306
+ style: {
6307
+ display: "flex",
6308
+ gap: 6,
6309
+ padding: "7px 14px",
6310
+ background: t.ctrlsBg,
6311
+ borderBottom: `1px solid ${t.ctrlsBorder}`,
6312
+ alignItems: "center",
6313
+ flexWrap: "wrap"
6314
+ },
6315
+ children: [
6316
+ /* @__PURE__ */ jsxs12("button", { onClick: () => addNode(), style: ctrlBtn(acc.color, isDark), children: [
6317
+ "+ ",
6318
+ variantLabel
6319
+ ] }),
6320
+ selectedSet.size > 0 && /* @__PURE__ */ jsxs12(Fragment5, { children: [
6321
+ /* @__PURE__ */ jsx12("div", { style: { width: 1, height: 20, background: t.ctrlsBorder, margin: "0 2px" } }),
6322
+ /* @__PURE__ */ jsx12(
6323
+ "button",
6324
+ {
6325
+ onClick: deleteSelected,
6326
+ style: {
6327
+ ...ctrlBtn("transparent", isDark),
6328
+ color: "#ef4444",
6329
+ border: `1px solid ${isDark ? "#7f1d1d" : "#fca5a5"}`
6330
+ },
6331
+ children: selectedSet.size > 1 ? `Delete (${selectedSet.size})` : "Delete"
6332
+ }
6333
+ )
6334
+ ] }),
6335
+ liveEdge && /* @__PURE__ */ jsxs12("span", { style: { fontSize: 11, color: acc.color, fontWeight: 600, marginLeft: 6 }, children: [
6336
+ liveEdge.answerLabel ? `Routing "${liveEdge.answerLabel}" \u2192` : "Drop on a node to connect",
6337
+ /* @__PURE__ */ jsx12("span", { style: { fontWeight: 400, color: t.textMuted, marginLeft: 6 }, children: "release to cancel" })
6338
+ ] }),
6339
+ /* @__PURE__ */ jsxs12("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
6340
+ variant === "question" ? "drag answer port to connect \xB7 " : "drag port dot \xB7 ",
6341
+ "scroll to zoom \xB7 drag to pan"
6342
+ ] })
6343
+ ]
6344
+ }
6345
+ ),
6346
+ variant !== "flowchart" && /* @__PURE__ */ jsx12(
6347
+ "div",
6348
+ {
6349
+ style: {
6350
+ padding: "3px 14px",
6351
+ background: acc.fill,
6352
+ borderBottom: `1px solid ${acc.border}`,
6353
+ fontSize: 11,
6354
+ color: acc.color,
6355
+ fontWeight: 600
6356
+ },
6357
+ 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"
6358
+ }
6359
+ ),
6360
+ /* @__PURE__ */ jsxs12("div", { style: STYLE_FLEX_ROW, children: [
6361
+ /* @__PURE__ */ jsx12(
6362
+ NodeNavigator,
6363
+ {
6364
+ model,
6365
+ selected,
6366
+ variant,
6367
+ isDark,
6368
+ t,
6369
+ acc,
6370
+ open: navOpen,
6371
+ onToggle: () => setNavOpen((v) => !v),
6372
+ onSelect: jumpToNode
4884
6373
  }
4885
- },
4886
- onCtxEdgeResetRouting: () => {
4887
- if (ctxMenu?.edgeId) {
4888
- resetEdgeRouting(ctxMenu.edgeId);
4889
- setCtxMenu(null);
6374
+ ),
6375
+ /* @__PURE__ */ jsx12(
6376
+ DiagramCanvas,
6377
+ {
6378
+ model,
6379
+ variant,
6380
+ variantLabel,
6381
+ t,
6382
+ isDark,
6383
+ acc,
6384
+ transform,
6385
+ setTransform,
6386
+ selected,
6387
+ selectedSet,
6388
+ hoveredId,
6389
+ setHoveredId,
6390
+ drag,
6391
+ pan,
6392
+ liveEdge,
6393
+ boxSel,
6394
+ alignGuides,
6395
+ editingEdgeId,
6396
+ editEdgeLabel,
6397
+ setEditEdgeLabel,
6398
+ commitEdgeEdit,
6399
+ setEditingEdgeId,
6400
+ beginEditEdge,
6401
+ onEdgeContextMenu,
6402
+ setWaypointDrag,
6403
+ editingId,
6404
+ editLabel,
6405
+ setEditLabel,
6406
+ commitEdit,
6407
+ setEditingId,
6408
+ onNodeMouseDown,
6409
+ onNodeMouseUp,
6410
+ onNodeDblClick,
6411
+ onNodeContextMenu,
6412
+ onPortMouseDown,
6413
+ onAnswerPortDown,
6414
+ onSvgMouseDown,
6415
+ onMouseMove,
6416
+ onMouseUp,
6417
+ onSvgContextMenu,
6418
+ reducedMotion,
6419
+ isCoarse,
6420
+ portR,
6421
+ shadowClr,
6422
+ arrowClr,
6423
+ amberArrow,
6424
+ viewport,
6425
+ svgRef,
6426
+ containerRef,
6427
+ ctxMenu,
6428
+ history,
6429
+ ctxEdgeStyle: (ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.style ?? "solid",
6430
+ ctxEdgeArrow: (ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.arrowhead ?? "arrow",
6431
+ ctxEdgeHasWaypoint: !!(ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.waypoint,
6432
+ onCtxUndo: () => {
6433
+ undo();
6434
+ setCtxMenu(null);
6435
+ },
6436
+ onCtxRedo: () => {
6437
+ redo();
6438
+ setCtxMenu(null);
6439
+ },
6440
+ onCtxReCenter: () => {
6441
+ reCenter();
6442
+ setCtxMenu(null);
6443
+ },
6444
+ onCtxAddNode: () => {
6445
+ const rect = svgRef.current.getBoundingClientRect();
6446
+ const cx = (ctxMenu.x - rect.left - transform.x) / transform.scale;
6447
+ const cy = (ctxMenu.y - rect.top - transform.y) / transform.scale;
6448
+ addNode({ x: cx, y: cy });
6449
+ setCtxMenu(null);
6450
+ },
6451
+ onCtxDuplicate: () => {
6452
+ if (ctxMenu?.nodeId) {
6453
+ duplicateNode(ctxMenu.nodeId);
6454
+ setCtxMenu(null);
6455
+ }
6456
+ },
6457
+ onCtxRename: () => {
6458
+ if (ctxMenu?.nodeId) {
6459
+ const node = model.nodes.find((n) => n.id === ctxMenu.nodeId);
6460
+ setEditingId(ctxMenu.nodeId);
6461
+ setEditLabel(node.label);
6462
+ setCtxMenu(null);
6463
+ }
6464
+ },
6465
+ onCtxDelete: () => {
6466
+ if (ctxMenu?.nodeId) {
6467
+ deleteNode(ctxMenu.nodeId);
6468
+ setCtxMenu(null);
6469
+ }
6470
+ },
6471
+ onCtxDisconnect: () => {
6472
+ if (ctxMenu?.nodeId) {
6473
+ const m = {
6474
+ ...model,
6475
+ edges: model.edges.filter(
6476
+ (e) => e.from !== ctxMenu.nodeId && e.to !== ctxMenu.nodeId
6477
+ )
6478
+ };
6479
+ applyAndPush(m);
6480
+ setCtxMenu(null);
6481
+ }
6482
+ },
6483
+ onCtxEdgeRename: () => {
6484
+ if (ctxMenu?.edgeId) {
6485
+ beginEditEdge(ctxMenu.edgeId);
6486
+ setCtxMenu(null);
6487
+ }
6488
+ },
6489
+ onCtxEdgeStyle: (s2) => {
6490
+ if (ctxMenu?.edgeId) {
6491
+ setEdgeStyle(ctxMenu.edgeId, s2);
6492
+ setCtxMenu(null);
6493
+ }
6494
+ },
6495
+ onCtxEdgeArrowhead: (a) => {
6496
+ if (ctxMenu?.edgeId) {
6497
+ setEdgeArrowhead(ctxMenu.edgeId, a);
6498
+ setCtxMenu(null);
6499
+ }
6500
+ },
6501
+ onCtxEdgeDelete: () => {
6502
+ if (ctxMenu?.edgeId) {
6503
+ deleteEdge(ctxMenu.edgeId);
6504
+ setCtxMenu(null);
6505
+ }
6506
+ },
6507
+ onCtxEdgeResetRouting: () => {
6508
+ if (ctxMenu?.edgeId) {
6509
+ resetEdgeRouting(ctxMenu.edgeId);
6510
+ setCtxMenu(null);
6511
+ }
6512
+ }
4890
6513
  }
6514
+ ),
6515
+ selected && /* @__PURE__ */ jsx12(
6516
+ StepEditor,
6517
+ {
6518
+ nodeId: selected,
6519
+ model,
6520
+ onModelChange: (m) => {
6521
+ applyAndPush(m);
6522
+ },
6523
+ variant,
6524
+ isDark,
6525
+ t,
6526
+ acc
6527
+ },
6528
+ selected
6529
+ )
6530
+ ] }),
6531
+ /* @__PURE__ */ jsxs12(
6532
+ "div",
6533
+ {
6534
+ style: {
6535
+ padding: "4px 14px",
6536
+ fontSize: 11,
6537
+ color: t.textMuted,
6538
+ background: t.statusBg,
6539
+ borderTop: `1px solid ${t.ctrlsBorder}`,
6540
+ display: "flex",
6541
+ gap: 16,
6542
+ flexWrap: "wrap",
6543
+ overflow: "hidden",
6544
+ maxHeight: 28
6545
+ },
6546
+ children: [
6547
+ /* @__PURE__ */ jsxs12("span", { children: [
6548
+ model.nodes.length,
6549
+ " ",
6550
+ variantLabel.toLowerCase(),
6551
+ "s"
6552
+ ] }),
6553
+ /* @__PURE__ */ jsxs12("span", { children: [
6554
+ model.edges.length,
6555
+ " connections"
6556
+ ] }),
6557
+ /* @__PURE__ */ jsxs12("span", { children: [
6558
+ Math.round(transform.scale * 100),
6559
+ "% zoom"
6560
+ ] }),
6561
+ /* @__PURE__ */ jsx12(
6562
+ "span",
6563
+ {
6564
+ style: {
6565
+ marginLeft: "auto",
6566
+ whiteSpace: "nowrap",
6567
+ overflow: "hidden",
6568
+ textOverflow: "ellipsis"
6569
+ },
6570
+ children: "Ctrl+Z undo \xB7 Ctrl+Y redo \xB7 Ctrl+0 fit \xB7 Alt+Arrow traverse"
6571
+ }
6572
+ ),
6573
+ selected && /* @__PURE__ */ jsx12("span", { style: { color: acc.color }, children: model.nodes.find((n) => n.id === selected)?.label })
6574
+ ]
4891
6575
  }
4892
- }
4893
- ),
4894
- selected && /* @__PURE__ */ jsx12(StepEditor, { nodeId: selected, model, onModelChange: (m) => {
4895
- applyAndPush(m);
4896
- }, variant, isDark, t, acc }, selected)
4897
- ] }),
4898
- /* @__PURE__ */ jsxs12("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: [
4899
- /* @__PURE__ */ jsxs12("span", { children: [
4900
- model.nodes.length,
4901
- " ",
4902
- variantLabel.toLowerCase(),
4903
- "s"
4904
- ] }),
4905
- /* @__PURE__ */ jsxs12("span", { children: [
4906
- model.edges.length,
4907
- " connections"
4908
- ] }),
4909
- /* @__PURE__ */ jsxs12("span", { children: [
4910
- Math.round(transform.scale * 100),
4911
- "% zoom"
4912
- ] }),
4913
- /* @__PURE__ */ jsx12("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" }),
4914
- selected && /* @__PURE__ */ jsx12("span", { style: { color: acc.color }, children: model.nodes.find((n) => n.id === selected)?.label })
4915
- ] })
4916
- ] });
6576
+ )
6577
+ ]
6578
+ }
6579
+ );
4917
6580
  }
4918
6581
  function ctrlBtn(accent, isDark) {
4919
6582
  const isTransparent = accent === "transparent";