flowchart-sequence-designer 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/ui/DiagramEditor.tsx
2
- import { useCallback as useCallback7, useEffect as useEffect9, useRef as useRef7, useState as useState11 } from "react";
2
+ import { useCallback as useCallback8, 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";
@@ -375,6 +375,12 @@ var ACCENT = {
375
375
  emeraldDarkLight: "rgba(16,185,129,0.12)",
376
376
  emeraldDarkBorder: "rgba(16,185,129,0.3)"
377
377
  };
378
+ function shadowColor(isDark) {
379
+ return isDark ? "rgba(0,0,0,0.55)" : "rgba(15,23,42,0.09)";
380
+ }
381
+ function arrowColor(isDark) {
382
+ return isDark ? "#64748b" : "#94a3b8";
383
+ }
378
384
  function variantAccent(variant, isDark) {
379
385
  if (variant === "question") {
380
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 };
@@ -406,10 +412,10 @@ function Toolbar({ onExport, onImport, allowedExports, allowImport = true }) {
406
412
  ] }),
407
413
  /* @__PURE__ */ jsx2("div", { style: divider }),
408
414
  /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 4, alignItems: "center" }, children: [
409
- allowImport && onImport && /* @__PURE__ */ jsx2("button", { onClick: () => setImportOpen(true), style: ghostBtn, children: "\u2191 Import" }),
415
+ allowImport && onImport && /* @__PURE__ */ jsx2("button", { onClick: () => setImportOpen(true), "aria-label": "Import diagram", style: ghostBtn, children: "\u2191 Import" }),
410
416
  formats.length > 0 && /* @__PURE__ */ jsxs2(Fragment, { children: [
411
417
  /* @__PURE__ */ jsx2("span", { style: { fontSize: 11, color: darkTheme.inputText, margin: "0 4px" }, children: "Export \u2192" }),
412
- formats.map((f) => /* @__PURE__ */ jsx2("button", { onClick: () => onExport(f.key), style: exportBtn, children: f.label }, f.key))
418
+ formats.map((f) => /* @__PURE__ */ jsx2("button", { onClick: () => onExport(f.key), "aria-label": `Export as ${f.label}`, style: exportBtn, children: f.label }, f.key))
413
419
  ] })
414
420
  ] }),
415
421
  onImport && /* @__PURE__ */ jsx2(
@@ -687,7 +693,7 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
687
693
  /* @__PURE__ */ jsx3("label", { style: { display: "block", fontSize: 10, fontWeight: 700, color: tt.labelText, marginBottom: 8, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Shape" }),
688
694
  /* @__PURE__ */ jsx3("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6 }, children: SHAPES.map((s2) => {
689
695
  const active = (node.shape ?? "rectangle") === s2.key;
690
- return /* @__PURE__ */ jsxs3("button", { onClick: () => setShape(s2.key), style: {
696
+ return /* @__PURE__ */ jsxs3("button", { onClick: () => setShape(s2.key), "aria-pressed": active, style: {
691
697
  display: "flex",
692
698
  flexDirection: "column",
693
699
  alignItems: "center",
@@ -734,7 +740,12 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
734
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" })
735
741
  ] }, ans + i);
736
742
  }),
737
- addingAnswer ? /* @__PURE__ */ jsxs3("div", { style: { marginTop: 10, background: tt.addFormBg, borderRadius: 10, padding: 12, border: `1.5px solid ${accentBorder}` }, children: [
743
+ addingAnswer ? /* @__PURE__ */ jsxs3("div", { role: "group", "aria-label": "Add answer form", onKeyDown: (e) => {
744
+ if (e.key === "Escape") {
745
+ setAddingAnswer(false);
746
+ setNewAnswer("");
747
+ }
748
+ }, style: { marginTop: 10, background: tt.addFormBg, borderRadius: 10, padding: 12, border: `1.5px solid ${accentBorder}` }, children: [
738
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 } }),
739
750
  /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 6 }, children: [
740
751
  /* @__PURE__ */ jsx3("button", { onClick: addAnswer, style: addBtnStyle, children: "Add Answer" }),
@@ -772,7 +783,9 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
772
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" })
773
784
  ] }, edge.id);
774
785
  }),
775
- addingBranch ? /* @__PURE__ */ jsxs3("div", { style: { marginTop: 10, background: tt.addFormBg, borderRadius: 10, padding: 12, border: `1.5px solid ${accentBorder}` }, children: [
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: [
776
789
  /* @__PURE__ */ jsx3("div", { style: { display: "flex", gap: 6, marginBottom: 10 }, children: ["new", "existing"].map((mode) => /* @__PURE__ */ jsx3("button", { onClick: () => setBranchMode(mode), style: {
777
790
  flex: 1,
778
791
  padding: "5px 0",
@@ -807,10 +820,291 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
807
820
  }
808
821
 
809
822
  // src/ui/SequenceEditor.tsx
810
- import { useCallback as useCallback4, useEffect as useEffect4, useMemo as useMemo2, useRef as useRef3, useState as useState5 } from "react";
823
+ import { useCallback as useCallback5, useEffect as useEffect5, useMemo as useMemo3, useRef as useRef3, useState as useState6 } from "react";
811
824
 
812
- // src/ui/hooks/useEditorTheme.ts
825
+ // src/ui/SequenceCanvas.tsx
813
826
  import { useMemo } from "react";
827
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
828
+ var HEADER_H = 64;
829
+ var HEADER_PAD = 24;
830
+ var ROW_H = 64;
831
+ var SIDE_PAD = 40;
832
+ var INDIGO = "#4f46e5";
833
+ var INDIGO_SOFT = "#eef2ff";
834
+ var STYLE_SEQ_GRAB = { cursor: "grab" };
835
+ var STYLE_SEQ_GRABBING = { cursor: "grabbing" };
836
+ var STYLE_SEQ_ACTOR_TEXT = { cursor: "pointer", userSelect: "none" };
837
+ var STYLE_SEQ_REMOVE_BTN = { cursor: "pointer" };
838
+ var STYLE_SEQ_REMOVE_ICON = { pointerEvents: "none", userSelect: "none" };
839
+ var STYLE_SEQ_DRAGGING = { opacity: 0.85 };
840
+ function estimateW(text, pxPerChar = 7) {
841
+ return text.length * pxPerChar;
842
+ }
843
+ function SequenceCanvas(props) {
844
+ const {
845
+ model,
846
+ actors,
847
+ messages,
848
+ t,
849
+ isDark,
850
+ colW,
851
+ totalW,
852
+ totalH,
853
+ actorX,
854
+ msgY,
855
+ selected,
856
+ editingId,
857
+ setEditingId,
858
+ drag,
859
+ onRowMouseDown,
860
+ renameActor,
861
+ removeActor,
862
+ svgRef
863
+ } = props;
864
+ const visualMessages = useMemo(() => {
865
+ if (!drag?.active) return messages;
866
+ const idx = messages.findIndex((m) => m.id === drag.id);
867
+ if (idx < 0) return messages;
868
+ const next = messages.slice();
869
+ const [moved] = next.splice(idx, 1);
870
+ next.splice(drag.targetIdx, 0, moved);
871
+ return next;
872
+ }, [messages, drag]);
873
+ 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
+ ] });
894
+ }
895
+ return /* @__PURE__ */ jsxs4(
896
+ "svg",
897
+ {
898
+ ref: svgRef,
899
+ width: totalW,
900
+ height: totalH,
901
+ style: { display: "block", cursor: drag?.active ? "grabbing" : "default", userSelect: "none" },
902
+ children: [
903
+ /* @__PURE__ */ jsxs4("defs", { children: [
904
+ /* @__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
+ /* @__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 }) })
907
+ ] }),
908
+ /* @__PURE__ */ jsx4("rect", { width: totalW, height: totalH, fill: "url(#seqdots)" }),
909
+ actors.map((name) => {
910
+ const x = actorX(name);
911
+ const top = HEADER_PAD + HEADER_H;
912
+ return /* @__PURE__ */ jsx4(
913
+ "line",
914
+ {
915
+ x1: x,
916
+ x2: x,
917
+ y1: top + 4,
918
+ y2: totalH - 24,
919
+ stroke: t.lifeline,
920
+ strokeWidth: 1.25,
921
+ strokeDasharray: "5 5"
922
+ },
923
+ `life-${name}`
924
+ );
925
+ }),
926
+ visualMessages.map((msg, idx) => {
927
+ const y = msgY(idx);
928
+ const fromX = actorX(msg.from);
929
+ const toX = actorX(msg.to);
930
+ const selectedHere = selected === msg.id;
931
+ const isDragging = drag?.active && drag.id === msg.id;
932
+ const isSelf = msg.from === msg.to;
933
+ const stroke = selectedHere ? INDIGO : t.arrow;
934
+ const dash = msg.style === "dashed" ? "6,4" : void 0;
935
+ const cursorStyle = drag?.active ? STYLE_SEQ_GRABBING : STYLE_SEQ_GRAB;
936
+ const groupStyle = isDragging ? { ...cursorStyle, ...STYLE_SEQ_DRAGGING } : cursorStyle;
937
+ if (isSelf) {
938
+ const startX = fromX;
939
+ const loopW = 36;
940
+ const loopY = y - 6;
941
+ const d = `M ${startX} ${loopY} C ${startX + loopW} ${loopY}, ${startX + loopW} ${loopY + 24}, ${startX} ${loopY + 24}`;
942
+ return /* @__PURE__ */ jsxs4("g", { onMouseDown: (e) => onRowMouseDown(e, msg.id), style: groupStyle, children: [
943
+ (selectedHere || isDragging) && /* @__PURE__ */ jsx4(
944
+ "rect",
945
+ {
946
+ x: SIDE_PAD - 8,
947
+ y: y - 22,
948
+ width: totalW - (SIDE_PAD - 8) * 2,
949
+ height: ROW_H - 12,
950
+ rx: 10,
951
+ fill: INDIGO_SOFT,
952
+ opacity: isDark ? 0.18 : 0.6
953
+ }
954
+ ),
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 })
957
+ ] }, msg.id);
958
+ }
959
+ const labelX = (fromX + toX) / 2;
960
+ return /* @__PURE__ */ jsxs4("g", { onMouseDown: (e) => onRowMouseDown(e, msg.id), style: groupStyle, children: [
961
+ (selectedHere || isDragging) && /* @__PURE__ */ jsx4(
962
+ "rect",
963
+ {
964
+ x: SIDE_PAD - 8,
965
+ y: y - 22,
966
+ width: totalW - (SIDE_PAD - 8) * 2,
967
+ height: ROW_H - 12,
968
+ rx: 10,
969
+ fill: INDIGO_SOFT,
970
+ opacity: isDark ? 0.18 : 0.6
971
+ }
972
+ ),
973
+ /* @__PURE__ */ jsx4("line", { x1: fromX, y1: y, x2: toX, y2: y, stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
974
+ /* @__PURE__ */ jsx4(
975
+ "rect",
976
+ {
977
+ x: labelX - estimateW(msg.label) / 2 - 6,
978
+ y: y - 18,
979
+ width: estimateW(msg.label) + 12,
980
+ height: 18,
981
+ rx: 6,
982
+ fill: t.canvas,
983
+ stroke: selectedHere ? INDIGO : t.cardBorder,
984
+ strokeWidth: selectedHere ? 1.25 : 1
985
+ }
986
+ ),
987
+ /* @__PURE__ */ jsx4("text", { x: labelX, y: y - 5, textAnchor: "middle", fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
988
+ ] }, msg.id);
989
+ }),
990
+ actors.map((name) => {
991
+ const x = actorX(name);
992
+ const w = colW - 24;
993
+ return /* @__PURE__ */ jsxs4("g", { children: [
994
+ /* @__PURE__ */ jsx4(
995
+ "rect",
996
+ {
997
+ x: x - w / 2,
998
+ y: HEADER_PAD,
999
+ width: w,
1000
+ height: HEADER_H,
1001
+ rx: 12,
1002
+ fill: t.actorFill,
1003
+ stroke: t.actorStroke,
1004
+ strokeWidth: 1.25,
1005
+ filter: "url(#seqShadow)"
1006
+ }
1007
+ ),
1008
+ editingId === name ? /* @__PURE__ */ jsx4("foreignObject", { x: x - w / 2 + 8, y: HEADER_PAD + 16, width: w - 16, height: 32, children: /* @__PURE__ */ jsx4(
1009
+ "input",
1010
+ {
1011
+ autoFocus: true,
1012
+ defaultValue: name,
1013
+ onBlur: (e) => {
1014
+ renameActor(name, e.currentTarget.value.trim());
1015
+ setEditingId(null);
1016
+ },
1017
+ onKeyDown: (e) => {
1018
+ if (e.key === "Enter") {
1019
+ renameActor(name, e.target.value.trim());
1020
+ setEditingId(null);
1021
+ }
1022
+ if (e.key === "Escape") setEditingId(null);
1023
+ },
1024
+ style: {
1025
+ width: "100%",
1026
+ height: "100%",
1027
+ border: "none",
1028
+ borderRadius: 6,
1029
+ outline: `2px solid ${INDIGO}`,
1030
+ textAlign: "center",
1031
+ fontSize: 13,
1032
+ fontWeight: 600,
1033
+ background: t.inputBg,
1034
+ color: t.inputText,
1035
+ boxSizing: "border-box",
1036
+ padding: "0 6px",
1037
+ fontFamily: "inherit"
1038
+ }
1039
+ }
1040
+ ) }) : /* @__PURE__ */ jsx4(
1041
+ "text",
1042
+ {
1043
+ x,
1044
+ y: HEADER_PAD + HEADER_H / 2 + 4,
1045
+ textAnchor: "middle",
1046
+ fontSize: 13,
1047
+ fontWeight: 700,
1048
+ fill: t.actorText,
1049
+ role: "button",
1050
+ tabIndex: 0,
1051
+ "aria-label": `Actor ${name} \u2014 press Enter or F2 to rename`,
1052
+ style: STYLE_SEQ_ACTOR_TEXT,
1053
+ onDoubleClick: () => setEditingId(name),
1054
+ onKeyDown: (e) => {
1055
+ if (e.key === "Enter" || e.key === "F2") {
1056
+ e.preventDefault();
1057
+ setEditingId(name);
1058
+ }
1059
+ },
1060
+ children: name
1061
+ }
1062
+ ),
1063
+ /* @__PURE__ */ jsx4(
1064
+ "circle",
1065
+ {
1066
+ cx: x + w / 2 - 12,
1067
+ cy: HEADER_PAD + 14,
1068
+ r: 9,
1069
+ fill: "transparent",
1070
+ role: "button",
1071
+ tabIndex: 0,
1072
+ "aria-label": `Remove actor ${name}`,
1073
+ style: STYLE_SEQ_REMOVE_BTN,
1074
+ onClick: () => removeActor(name),
1075
+ onKeyDown: (e) => {
1076
+ if (e.key === "Enter" || e.key === " ") {
1077
+ e.preventDefault();
1078
+ removeActor(name);
1079
+ }
1080
+ },
1081
+ children: /* @__PURE__ */ jsxs4("title", { children: [
1082
+ "Remove actor ",
1083
+ name
1084
+ ] })
1085
+ }
1086
+ ),
1087
+ /* @__PURE__ */ jsx4(
1088
+ "text",
1089
+ {
1090
+ x: x + w / 2 - 12,
1091
+ y: HEADER_PAD + 18,
1092
+ textAnchor: "middle",
1093
+ fontSize: 12,
1094
+ fill: t.textMuted,
1095
+ style: STYLE_SEQ_REMOVE_ICON,
1096
+ children: "\xD7"
1097
+ }
1098
+ )
1099
+ ] }, `hdr-${name}`);
1100
+ })
1101
+ ]
1102
+ }
1103
+ );
1104
+ }
1105
+
1106
+ // src/ui/hooks/useEditorTheme.ts
1107
+ import { useMemo as useMemo2 } from "react";
814
1108
 
815
1109
  // src/ui/hooks/useSystemTheme.ts
816
1110
  import { useEffect as useEffect3, useState as useState4 } from "react";
@@ -857,7 +1151,7 @@ function usePrefersReducedMotion() {
857
1151
  // src/ui/hooks/useEditorTheme.ts
858
1152
  function useEditorTheme(theme, overrides, palettes) {
859
1153
  const isDark = useIsDark(theme);
860
- const t = useMemo(
1154
+ const t = useMemo2(
861
1155
  () => ({ ...isDark ? palettes.dark : palettes.light, ...overrides ?? {} }),
862
1156
  // palettes is a stable module-level constant in every caller, so it is
863
1157
  // deliberately omitted from the dep array to keep the memo key tight.
@@ -1254,7 +1548,7 @@ async function toPNG(model) {
1254
1548
  }
1255
1549
 
1256
1550
  // src/ui/hooks/useExporters.ts
1257
- function useExporters(model, onExport, filename = "diagram") {
1551
+ function useExporters(model, onExport, filename = "diagram", onSuccess) {
1258
1552
  return useCallback2(async (format) => {
1259
1553
  let content;
1260
1554
  switch (format) {
@@ -1278,6 +1572,7 @@ function useExporters(model, onExport, filename = "diagram") {
1278
1572
  }
1279
1573
  if (onExport) {
1280
1574
  onExport(format, content);
1575
+ onSuccess?.(`Exported as ${format.toUpperCase()}`);
1281
1576
  return;
1282
1577
  }
1283
1578
  const url = content instanceof Blob ? URL.createObjectURL(content) : URL.createObjectURL(new Blob([content], { type: "text/plain" }));
@@ -1286,7 +1581,8 @@ function useExporters(model, onExport, filename = "diagram") {
1286
1581
  a.download = `${filename}.${format === "plantuml" ? "puml" : format}`;
1287
1582
  a.click();
1288
1583
  URL.revokeObjectURL(url);
1289
- }, [model, onExport, filename]);
1584
+ onSuccess?.(`Downloaded ${a.download}`);
1585
+ }, [model, onExport, filename, onSuccess]);
1290
1586
  }
1291
1587
 
1292
1588
  // src/ui/hooks/useImporter.ts
@@ -1584,19 +1880,91 @@ function fromJSON(json) {
1584
1880
 
1585
1881
  // src/ui/hooks/useImporter.ts
1586
1882
  function useImporter(applyAndPush, options = {}) {
1587
- const { expectedType, transform } = options;
1883
+ const { expectedType, transform, onSuccess, onError } = options;
1884
+ const reportError = onError ?? ((msg) => alert(msg));
1588
1885
  return useCallback3((text) => {
1589
1886
  try {
1590
1887
  const parsed = text.trim().startsWith("{") ? fromJSON(text).toJSON() : fromMermaid(text).toJSON();
1591
1888
  if (expectedType && parsed.type !== expectedType) {
1592
- alert(`Imported diagram is not a ${expectedType} diagram.`);
1889
+ reportError(`Imported diagram is not a ${expectedType} diagram.`);
1593
1890
  return;
1594
1891
  }
1595
1892
  applyAndPush(transform ? transform(parsed) : parsed);
1893
+ onSuccess?.("Diagram imported successfully");
1596
1894
  } catch (err) {
1597
- alert(`Import failed: ${err.message}`);
1895
+ reportError(`Import failed: ${err.message}`);
1598
1896
  }
1599
- }, [applyAndPush, expectedType, transform]);
1897
+ }, [applyAndPush, expectedType, transform, onSuccess, onError]);
1898
+ }
1899
+
1900
+ // src/ui/hooks/useToast.ts
1901
+ import { useCallback as useCallback4, useState as useState5 } from "react";
1902
+ var _toastSeq = 0;
1903
+ function useToast() {
1904
+ const [toasts, setToasts] = useState5([]);
1905
+ const showToast = useCallback4((message, type = "info") => {
1906
+ const id = ++_toastSeq;
1907
+ setToasts((prev) => [...prev, { id, message, type }]);
1908
+ setTimeout(() => {
1909
+ setToasts((prev) => prev.filter((t) => t.id !== id));
1910
+ }, 3e3);
1911
+ }, []);
1912
+ const dismissToast = useCallback4((id) => {
1913
+ setToasts((prev) => prev.filter((t) => t.id !== id));
1914
+ }, []);
1915
+ return { toasts, showToast, dismissToast };
1916
+ }
1917
+
1918
+ // src/ui/ToastContainer.tsx
1919
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1920
+ var TOAST_COLORS = {
1921
+ success: { bg: "#065f46", border: "#10b981", text: "#ecfdf5" },
1922
+ error: { bg: "#7f1d1d", border: "#ef4444", text: "#fef2f2" },
1923
+ info: { bg: "#1e3a5f", border: "#3b82f6", text: "#eff6ff" }
1924
+ };
1925
+ var containerStyle = {
1926
+ position: "absolute",
1927
+ top: 8,
1928
+ right: 8,
1929
+ display: "flex",
1930
+ flexDirection: "column",
1931
+ gap: 6,
1932
+ zIndex: 9999,
1933
+ pointerEvents: "none"
1934
+ };
1935
+ function ToastContainer({ toasts, onDismiss }) {
1936
+ if (toasts.length === 0) return null;
1937
+ return /* @__PURE__ */ jsx5("div", { style: containerStyle, children: toasts.map((t) => {
1938
+ const c = TOAST_COLORS[t.type];
1939
+ return /* @__PURE__ */ jsxs5(
1940
+ "div",
1941
+ {
1942
+ role: "alert",
1943
+ "aria-live": "polite",
1944
+ style: {
1945
+ background: c.bg,
1946
+ border: `1px solid ${c.border}`,
1947
+ color: c.text,
1948
+ padding: "8px 14px",
1949
+ borderRadius: 8,
1950
+ fontSize: 12,
1951
+ fontWeight: 500,
1952
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
1953
+ boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
1954
+ pointerEvents: "auto",
1955
+ cursor: "pointer",
1956
+ maxWidth: 280
1957
+ },
1958
+ onClick: () => onDismiss(t.id),
1959
+ children: [
1960
+ t.type === "success" && "\u2713 ",
1961
+ t.type === "error" && "\u2717 ",
1962
+ t.message
1963
+ ]
1964
+ },
1965
+ t.id
1966
+ );
1967
+ }) });
1600
1968
  }
1601
1969
 
1602
1970
  // src/ui/presets.ts
@@ -1695,10 +2063,33 @@ function cloneModel(m) {
1695
2063
  };
1696
2064
  }
1697
2065
 
2066
+ // src/ui/hooks/useEditorKeyboard.ts
2067
+ import { useEffect as useEffect4 } from "react";
2068
+ var isInput = (e) => {
2069
+ const tgt = e.target;
2070
+ return !!(tgt && (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA" || tgt.isContentEditable));
2071
+ };
2072
+ function useEditorKeyboard(commands, deps) {
2073
+ useEffect4(() => {
2074
+ const onKey = (e) => {
2075
+ if (isInput(e)) return;
2076
+ for (const cmd of commands) {
2077
+ if (cmd.match(e)) {
2078
+ const handled = cmd.run(e);
2079
+ if (handled) e.preventDefault();
2080
+ return;
2081
+ }
2082
+ }
2083
+ };
2084
+ window.addEventListener("keydown", onKey);
2085
+ return () => window.removeEventListener("keydown", onKey);
2086
+ }, deps);
2087
+ }
2088
+
1698
2089
  // src/ui/SequenceEditor.tsx
1699
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1700
- var INDIGO = "#4f46e5";
1701
- var INDIGO_SOFT = "#eef2ff";
2090
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2091
+ var INDIGO2 = "#4f46e5";
2092
+ var INDIGO_SOFT2 = "#eef2ff";
1702
2093
  var lightTheme2 = {
1703
2094
  canvas: "#fafbfc",
1704
2095
  dot: "#dbe3ee",
@@ -1741,11 +2132,11 @@ var darkTheme2 = {
1741
2132
  actorStroke: "rgba(99,102,241,0.45)",
1742
2133
  actorText: "#a5b4fc"
1743
2134
  };
1744
- var HEADER_H = 64;
1745
- var HEADER_PAD = 24;
2135
+ var HEADER_H2 = 64;
2136
+ var HEADER_PAD2 = 24;
1746
2137
  var COL_MIN = 160;
1747
- var ROW_H = 64;
1748
- var SIDE_PAD = 40;
2138
+ var ROW_H2 = 64;
2139
+ var SIDE_PAD2 = 40;
1749
2140
  var DRAG_THRESHOLD = 5;
1750
2141
  function ensureSequenceModel(m) {
1751
2142
  if (m && m.type === "sequence") {
@@ -1763,49 +2154,50 @@ function SequenceEditor({
1763
2154
  theme = "auto",
1764
2155
  themeOverrides
1765
2156
  }) {
1766
- const [model, setModel] = useState5(() => ensureSequenceModel(initialModel));
1767
- const [selected, setSelected] = useState5(null);
1768
- const [drag, setDrag] = useState5(null);
1769
- const [editingId, setEditingId] = useState5(null);
1770
- const [editLabel, setEditLabel] = useState5("");
2157
+ const [model, setModel] = useState6(() => ensureSequenceModel(initialModel));
2158
+ const { toasts, showToast, dismissToast } = useToast();
2159
+ const [selected, setSelected] = useState6(null);
2160
+ const [drag, setDrag] = useState6(null);
2161
+ const [editingId, setEditingId] = useState6(null);
2162
+ const [editLabel, setEditLabel] = useState6("");
1771
2163
  const historyRef = useRef3([ensureSequenceModel(initialModel)]);
1772
2164
  const historyIdxRef = useRef3(0);
1773
2165
  const svgRef = useRef3(null);
1774
2166
  const { t, isDark } = useEditorTheme(theme, themeOverrides, { light: lightTheme2, dark: darkTheme2 });
1775
2167
  const actors = model.actors ?? [];
1776
2168
  const messages = model.messages ?? [];
1777
- const colW = useMemo2(() => {
2169
+ const colW = useMemo3(() => {
1778
2170
  const longest = actors.reduce((m, a) => Math.max(m, a.length), 6);
1779
2171
  return Math.max(COL_MIN, longest * 9 + 40);
1780
2172
  }, [actors]);
1781
- const totalW = SIDE_PAD * 2 + Math.max(1, actors.length) * colW;
1782
- const totalH = HEADER_PAD + HEADER_H + 32 + messages.length * ROW_H + 48;
2173
+ const totalW = SIDE_PAD2 * 2 + Math.max(1, actors.length) * colW;
2174
+ const totalH = HEADER_PAD2 + HEADER_H2 + 32 + messages.length * ROW_H2 + 48;
1783
2175
  const actorX = (name) => {
1784
2176
  const idx = actors.indexOf(name);
1785
- if (idx < 0) return SIDE_PAD + colW / 2;
1786
- return SIDE_PAD + idx * colW + colW / 2;
2177
+ if (idx < 0) return SIDE_PAD2 + colW / 2;
2178
+ return SIDE_PAD2 + idx * colW + colW / 2;
1787
2179
  };
1788
- const msgY = (idx) => HEADER_PAD + HEADER_H + 40 + idx * ROW_H;
1789
- const pushHistory = useCallback4((m) => {
2180
+ const msgY = (idx) => HEADER_PAD2 + HEADER_H2 + 40 + idx * ROW_H2;
2181
+ const pushHistory = useCallback5((m) => {
1790
2182
  const stack = historyRef.current.slice(0, historyIdxRef.current + 1);
1791
2183
  stack.push(m);
1792
2184
  if (stack.length > 80) stack.shift();
1793
2185
  historyRef.current = stack;
1794
2186
  historyIdxRef.current = stack.length - 1;
1795
2187
  }, []);
1796
- const applyAndPush = useCallback4((m) => {
2188
+ const applyAndPush = useCallback5((m) => {
1797
2189
  setModel(m);
1798
2190
  onChange?.(m);
1799
2191
  pushHistory(m);
1800
2192
  }, [onChange, pushHistory]);
1801
- const undo = useCallback4(() => {
2193
+ const undo = useCallback5(() => {
1802
2194
  if (historyIdxRef.current <= 0) return;
1803
2195
  historyIdxRef.current--;
1804
2196
  const m = historyRef.current[historyIdxRef.current];
1805
2197
  setModel(m);
1806
2198
  onChange?.(m);
1807
2199
  }, [onChange]);
1808
- const redo = useCallback4(() => {
2200
+ const redo = useCallback5(() => {
1809
2201
  if (historyIdxRef.current >= historyRef.current.length - 1) return;
1810
2202
  historyIdxRef.current++;
1811
2203
  const m = historyRef.current[historyIdxRef.current];
@@ -1863,7 +2255,7 @@ function SequenceEditor({
1863
2255
  applyAndPush({ ...model, messages: messages.filter((m) => m.id !== id) });
1864
2256
  if (selected === id) setSelected(null);
1865
2257
  };
1866
- const reorderMessage = useCallback4((id, toIdx) => {
2258
+ const reorderMessage = useCallback5((id, toIdx) => {
1867
2259
  const fromIdx = messages.findIndex((m) => m.id === id);
1868
2260
  if (fromIdx < 0 || toIdx === fromIdx) return;
1869
2261
  const next = messages.slice();
@@ -1871,38 +2263,32 @@ function SequenceEditor({
1871
2263
  next.splice(toIdx, 0, moved);
1872
2264
  applyAndPush({ ...model, messages: next });
1873
2265
  }, [messages, model, applyAndPush]);
1874
- useEffect4(() => {
1875
- const onKey = (e) => {
1876
- const tgt = e.target;
1877
- if (tgt && (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA" || tgt.isContentEditable)) return;
1878
- const ctrl = e.ctrlKey || e.metaKey;
1879
- if (ctrl && e.key === "z") {
1880
- e.preventDefault();
1881
- undo();
1882
- return;
1883
- }
1884
- if (ctrl && (e.key === "y" || e.shiftKey && e.key === "z")) {
1885
- e.preventDefault();
1886
- redo();
1887
- return;
1888
- }
1889
- if (e.key === "Escape") {
1890
- setSelected(null);
1891
- setEditingId(null);
1892
- return;
1893
- }
1894
- if ((e.key === "Delete" || e.key === "Backspace") && selected) {
1895
- e.preventDefault();
1896
- removeMessage(selected);
1897
- }
1898
- };
1899
- window.addEventListener("keydown", onKey);
1900
- return () => window.removeEventListener("keydown", onKey);
1901
- }, [undo, redo, selected]);
1902
- const handleExport = useExporters(model, onExport, "sequence");
2266
+ 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"));
1903
2287
  const handleImport = useImporter(applyAndPush, {
1904
2288
  expectedType: "sequence",
1905
- transform: ensureSequenceModel
2289
+ transform: ensureSequenceModel,
2290
+ onSuccess: (msg) => showToast(msg, "success"),
2291
+ onError: (msg) => showToast(msg, "error")
1906
2292
  });
1907
2293
  const onRowMouseDown = (e, id) => {
1908
2294
  const tag = e.target.tagName;
@@ -1913,9 +2299,9 @@ function SequenceEditor({
1913
2299
  setSelected(id);
1914
2300
  setDrag({ id, startY: e.clientY, originalIdx: idx, targetIdx: idx, active: false });
1915
2301
  };
1916
- useEffect4(() => {
2302
+ useEffect5(() => {
1917
2303
  if (!drag) return;
1918
- const baseY = HEADER_PAD + HEADER_H + 40;
2304
+ const baseY = HEADER_PAD2 + HEADER_H2 + 40;
1919
2305
  const onMove = (ev) => {
1920
2306
  const dy = ev.clientY - drag.startY;
1921
2307
  if (!drag.active && Math.abs(dy) < DRAG_THRESHOLD) return;
@@ -1923,7 +2309,7 @@ function SequenceEditor({
1923
2309
  if (!svg) return;
1924
2310
  const rect = svg.getBoundingClientRect();
1925
2311
  const yInSvg = ev.clientY - rect.top;
1926
- const raw = Math.floor((yInSvg - baseY + ROW_H / 2) / ROW_H);
2312
+ const raw = Math.floor((yInSvg - baseY + ROW_H2 / 2) / ROW_H2);
1927
2313
  const next = Math.max(0, Math.min(messages.length - 1, raw));
1928
2314
  if (next === drag.targetIdx && drag.active) return;
1929
2315
  setDrag({ ...drag, active: true, targetIdx: next });
@@ -1941,26 +2327,31 @@ function SequenceEditor({
1941
2327
  window.removeEventListener("mouseup", onUp);
1942
2328
  };
1943
2329
  }, [drag, messages.length, reorderMessage]);
1944
- const visualMessages = useMemo2(() => {
1945
- if (!drag?.active) return messages;
1946
- const idx = messages.findIndex((m) => m.id === drag.id);
1947
- if (idx < 0) return messages;
1948
- const next = messages.slice();
1949
- const [moved] = next.splice(idx, 1);
1950
- next.splice(drag.targetIdx, 0, moved);
1951
- return next;
1952
- }, [messages, drag]);
1953
2330
  const selectedMsg = selected ? messages.find((m) => m.id === selected) : null;
1954
- return /* @__PURE__ */ jsxs4("div", { style: {
2331
+ return /* @__PURE__ */ jsxs6("div", { className: "fsd-seq-editor", style: {
1955
2332
  display: "flex",
1956
2333
  flexDirection: "column",
1957
2334
  height,
1958
2335
  width: "100%",
1959
2336
  fontFamily: "ui-sans-serif,system-ui,sans-serif",
1960
- background: t.ctrlsBg
2337
+ background: t.ctrlsBg,
2338
+ position: "relative"
1961
2339
  }, children: [
1962
- /* @__PURE__ */ jsx4(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
1963
- /* @__PURE__ */ jsxs4("div", { style: {
2340
+ /* @__PURE__ */ jsx6(ToastContainer, { toasts, onDismiss: dismissToast }),
2341
+ /* @__PURE__ */ jsx6("style", { children: `
2342
+ .fsd-seq-editor [role="button"]:focus-visible {
2343
+ outline: 2px solid ${t.actorText};
2344
+ outline-offset: 2px;
2345
+ }
2346
+ .fsd-seq-editor button:focus-visible,
2347
+ .fsd-seq-editor input:focus-visible {
2348
+ outline: 2px solid ${t.actorText};
2349
+ outline-offset: 2px;
2350
+ border-radius: 4px;
2351
+ }
2352
+ ` }),
2353
+ /* @__PURE__ */ jsx6(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
2354
+ /* @__PURE__ */ jsxs6("div", { style: {
1964
2355
  display: "flex",
1965
2356
  gap: 8,
1966
2357
  padding: "7px 14px",
@@ -1969,12 +2360,12 @@ function SequenceEditor({
1969
2360
  alignItems: "center",
1970
2361
  flexWrap: "wrap"
1971
2362
  }, children: [
1972
- /* @__PURE__ */ jsx4("button", { onClick: addActor, style: primaryBtn(), children: "+ Actor" }),
1973
- /* @__PURE__ */ jsx4("button", { onClick: addMessage, style: primaryBtn(), children: "+ Message" }),
1974
- /* @__PURE__ */ jsx4("div", { style: { width: 1, height: 18, background: t.ctrlsBorder, margin: "0 4px" } }),
1975
- /* @__PURE__ */ jsx4("button", { onClick: undo, style: ghostBtn2(t), title: "Undo (Ctrl+Z)", children: "\u21B6" }),
1976
- /* @__PURE__ */ jsx4("button", { onClick: redo, style: ghostBtn2(t), title: "Redo (Ctrl+Y)", children: "\u21B7" }),
1977
- /* @__PURE__ */ jsxs4("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
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: [
1978
2369
  actors.length,
1979
2370
  " actor",
1980
2371
  actors.length === 1 ? "" : "s",
@@ -1985,225 +2376,42 @@ function SequenceEditor({
1985
2376
  " \xB7 drag a row to reorder"
1986
2377
  ] })
1987
2378
  ] }),
1988
- /* @__PURE__ */ jsxs4("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [
1989
- /* @__PURE__ */ jsx4("div", { style: { flex: 1, overflow: "auto", background: t.canvas, position: "relative" }, children: actors.length === 0 && messages.length === 0 ? /* @__PURE__ */ jsxs4("div", { style: {
1990
- position: "absolute",
1991
- inset: 0,
1992
- display: "flex",
1993
- flexDirection: "column",
1994
- alignItems: "center",
1995
- justifyContent: "center",
1996
- gap: 10,
1997
- color: t.textMuted,
1998
- pointerEvents: "none"
1999
- }, children: [
2000
- /* @__PURE__ */ jsx4("div", { style: { fontSize: 36, opacity: 0.15, color: t.textPrimary }, children: "\u2194" }),
2001
- /* @__PURE__ */ jsxs4("div", { style: { fontSize: 13, fontWeight: 500 }, children: [
2002
- "Click ",
2003
- /* @__PURE__ */ jsx4("strong", { style: { color: INDIGO }, children: "+ Actor" }),
2004
- " then ",
2005
- /* @__PURE__ */ jsx4("strong", { style: { color: INDIGO }, children: "+ Message" }),
2006
- " to start"
2007
- ] })
2008
- ] }) : /* @__PURE__ */ jsxs4(
2009
- "svg",
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,
2010
2382
  {
2011
- ref: svgRef,
2012
- width: totalW,
2013
- height: totalH,
2014
- style: { display: "block", cursor: drag?.active ? "grabbing" : "default", userSelect: "none" },
2015
- children: [
2016
- /* @__PURE__ */ jsxs4("defs", { children: [
2017
- /* @__PURE__ */ jsx4("pattern", { id: "seqdots", x: "0", y: "0", width: "24", height: "24", patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ jsx4("circle", { cx: 12, cy: 12, r: 1.1, fill: t.dot }) }),
2018
- /* @__PURE__ */ jsx4("filter", { id: "seqShadow", x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ jsx4("feDropShadow", { dx: 0, dy: 3, stdDeviation: 5, floodColor: isDark ? "rgba(0,0,0,0.5)" : "rgba(15,23,42,0.09)" }) }),
2019
- /* @__PURE__ */ jsx4("marker", { id: "seqArrow", markerWidth: 9, markerHeight: 7, refX: 8.5, refY: 3.5, orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ jsx4("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: t.arrow }) })
2020
- ] }),
2021
- /* @__PURE__ */ jsx4("rect", { width: totalW, height: totalH, fill: "url(#seqdots)" }),
2022
- actors.map((name) => {
2023
- const x = actorX(name);
2024
- const top = HEADER_PAD + HEADER_H;
2025
- return /* @__PURE__ */ jsx4(
2026
- "line",
2027
- {
2028
- x1: x,
2029
- x2: x,
2030
- y1: top + 4,
2031
- y2: totalH - 24,
2032
- stroke: t.lifeline,
2033
- strokeWidth: 1.25,
2034
- strokeDasharray: "5 5"
2035
- },
2036
- `life-${name}`
2037
- );
2038
- }),
2039
- visualMessages.map((msg, idx) => {
2040
- const y = msgY(idx);
2041
- const fromX = actorX(msg.from);
2042
- const toX = actorX(msg.to);
2043
- const selectedHere = selected === msg.id;
2044
- const isDragging = drag?.active && drag.id === msg.id;
2045
- const isSelf = msg.from === msg.to;
2046
- const stroke = selectedHere ? INDIGO : t.arrow;
2047
- const dash = msg.style === "dashed" ? "6,4" : void 0;
2048
- const cursor = drag?.active ? "grabbing" : "grab";
2049
- const groupOpacity = isDragging ? 0.85 : 1;
2050
- if (isSelf) {
2051
- const startX = fromX;
2052
- const loopW = 36;
2053
- const loopY = y - 6;
2054
- const d = `M ${startX} ${loopY} C ${startX + loopW} ${loopY}, ${startX + loopW} ${loopY + 24}, ${startX} ${loopY + 24}`;
2055
- return /* @__PURE__ */ jsxs4("g", { onMouseDown: (e) => onRowMouseDown(e, msg.id), style: { cursor, opacity: groupOpacity }, children: [
2056
- (selectedHere || isDragging) && /* @__PURE__ */ jsx4(
2057
- "rect",
2058
- {
2059
- x: SIDE_PAD - 8,
2060
- y: y - 22,
2061
- width: totalW - (SIDE_PAD - 8) * 2,
2062
- height: ROW_H - 12,
2063
- rx: 10,
2064
- fill: INDIGO_SOFT,
2065
- opacity: isDark ? 0.18 : 0.6
2066
- }
2067
- ),
2068
- /* @__PURE__ */ jsx4("path", { d, fill: "none", stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
2069
- /* @__PURE__ */ jsx4("text", { x: startX + loopW + 8, y: loopY + 16, fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
2070
- ] }, msg.id);
2071
- }
2072
- const labelX = (fromX + toX) / 2;
2073
- return /* @__PURE__ */ jsxs4("g", { onMouseDown: (e) => onRowMouseDown(e, msg.id), style: { cursor, opacity: groupOpacity }, children: [
2074
- (selectedHere || isDragging) && /* @__PURE__ */ jsx4(
2075
- "rect",
2076
- {
2077
- x: SIDE_PAD - 8,
2078
- y: y - 22,
2079
- width: totalW - (SIDE_PAD - 8) * 2,
2080
- height: ROW_H - 12,
2081
- rx: 10,
2082
- fill: INDIGO_SOFT,
2083
- opacity: isDark ? 0.18 : 0.6
2084
- }
2085
- ),
2086
- /* @__PURE__ */ jsx4("line", { x1: fromX, y1: y, x2: toX, y2: y, stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
2087
- /* @__PURE__ */ jsx4(
2088
- "rect",
2089
- {
2090
- x: labelX - estimateW(msg.label) / 2 - 6,
2091
- y: y - 18,
2092
- width: estimateW(msg.label) + 12,
2093
- height: 18,
2094
- rx: 6,
2095
- fill: t.canvas,
2096
- stroke: selectedHere ? INDIGO : t.cardBorder,
2097
- strokeWidth: selectedHere ? 1.25 : 1
2098
- }
2099
- ),
2100
- /* @__PURE__ */ jsx4("text", { x: labelX, y: y - 5, textAnchor: "middle", fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
2101
- ] }, msg.id);
2102
- }),
2103
- actors.map((name) => {
2104
- const x = actorX(name);
2105
- const w = colW - 24;
2106
- return /* @__PURE__ */ jsxs4("g", { children: [
2107
- /* @__PURE__ */ jsx4(
2108
- "rect",
2109
- {
2110
- x: x - w / 2,
2111
- y: HEADER_PAD,
2112
- width: w,
2113
- height: HEADER_H,
2114
- rx: 12,
2115
- fill: t.actorFill,
2116
- stroke: t.actorStroke,
2117
- strokeWidth: 1.25,
2118
- filter: "url(#seqShadow)"
2119
- }
2120
- ),
2121
- editingId === name ? /* @__PURE__ */ jsx4("foreignObject", { x: x - w / 2 + 8, y: HEADER_PAD + 16, width: w - 16, height: 32, children: /* @__PURE__ */ jsx4(
2122
- "input",
2123
- {
2124
- autoFocus: true,
2125
- defaultValue: name,
2126
- onBlur: (e) => {
2127
- renameActor(name, e.currentTarget.value.trim());
2128
- setEditingId(null);
2129
- },
2130
- onKeyDown: (e) => {
2131
- if (e.key === "Enter") {
2132
- renameActor(name, e.target.value.trim());
2133
- setEditingId(null);
2134
- }
2135
- if (e.key === "Escape") setEditingId(null);
2136
- },
2137
- style: {
2138
- width: "100%",
2139
- height: "100%",
2140
- border: "none",
2141
- borderRadius: 6,
2142
- outline: `2px solid ${INDIGO}`,
2143
- textAlign: "center",
2144
- fontSize: 13,
2145
- fontWeight: 600,
2146
- background: t.inputBg,
2147
- color: t.inputText,
2148
- boxSizing: "border-box",
2149
- padding: "0 6px",
2150
- fontFamily: "inherit"
2151
- }
2152
- }
2153
- ) }) : /* @__PURE__ */ jsx4(
2154
- "text",
2155
- {
2156
- x,
2157
- y: HEADER_PAD + HEADER_H / 2 + 4,
2158
- textAnchor: "middle",
2159
- fontSize: 13,
2160
- fontWeight: 700,
2161
- fill: t.actorText,
2162
- style: { cursor: "pointer", userSelect: "none" },
2163
- onDoubleClick: () => setEditingId(name),
2164
- children: name
2165
- }
2166
- ),
2167
- /* @__PURE__ */ jsx4(
2168
- "circle",
2169
- {
2170
- cx: x + w / 2 - 12,
2171
- cy: HEADER_PAD + 14,
2172
- r: 9,
2173
- fill: "transparent",
2174
- style: { cursor: "pointer" },
2175
- onClick: () => removeActor(name),
2176
- children: /* @__PURE__ */ jsx4("title", { children: "Remove actor" })
2177
- }
2178
- ),
2179
- /* @__PURE__ */ jsx4(
2180
- "text",
2181
- {
2182
- x: x + w / 2 - 12,
2183
- y: HEADER_PAD + 18,
2184
- textAnchor: "middle",
2185
- fontSize: 12,
2186
- fill: t.textMuted,
2187
- style: { pointerEvents: "none", userSelect: "none" },
2188
- children: "\xD7"
2189
- }
2190
- )
2191
- ] }, `hdr-${name}`);
2192
- })
2193
- ]
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
2194
2401
  }
2195
2402
  ) }),
2196
- selectedMsg && /* @__PURE__ */ jsxs4("div", { style: {
2403
+ selectedMsg && /* @__PURE__ */ jsxs6("div", { style: {
2197
2404
  width: 280,
2405
+ maxWidth: "40vw",
2198
2406
  flexShrink: 0,
2199
2407
  background: t.panelBg,
2200
2408
  borderLeft: `1px solid ${t.panelBorder}`,
2201
2409
  padding: "14px 16px",
2202
2410
  overflowY: "auto"
2203
2411
  }, children: [
2204
- /* @__PURE__ */ jsx4("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.7, marginBottom: 10 }, children: "Message" }),
2205
- /* @__PURE__ */ jsx4(Label, { t, children: "Label" }),
2206
- /* @__PURE__ */ jsx4(
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
+ /* @__PURE__ */ jsx6(
2207
2415
  "input",
2208
2416
  {
2209
2417
  value: editLabel || selectedMsg.label,
@@ -2219,21 +2427,21 @@ function SequenceEditor({
2219
2427
  style: input(t)
2220
2428
  }
2221
2429
  ),
2222
- /* @__PURE__ */ jsx4(Label, { t, children: "From" }),
2223
- /* @__PURE__ */ jsx4("select", { value: selectedMsg.from, onChange: (e) => updateMessage(selectedMsg.id, { from: e.target.value }), style: input(t), children: actors.map((a) => /* @__PURE__ */ jsx4("option", { value: a, children: a }, a)) }),
2224
- /* @__PURE__ */ jsx4(Label, { t, children: "To" }),
2225
- /* @__PURE__ */ jsx4("select", { value: selectedMsg.to, onChange: (e) => updateMessage(selectedMsg.id, { to: e.target.value }), style: input(t), children: actors.map((a) => /* @__PURE__ */ jsx4("option", { value: a, children: a }, a)) }),
2226
- /* @__PURE__ */ jsx4(Label, { t, children: "Style" }),
2227
- /* @__PURE__ */ jsx4("div", { style: { display: "flex", gap: 6 }, children: ["solid", "dashed"].map((s2) => /* @__PURE__ */ jsx4(
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(
2228
2436
  "button",
2229
2437
  {
2230
2438
  onClick: () => updateMessage(selectedMsg.id, { style: s2 }),
2231
2439
  style: {
2232
2440
  flex: 1,
2233
2441
  padding: "6px 10px",
2234
- border: `1.5px solid ${selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO : t.inputBorder}`,
2235
- background: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO_SOFT : t.inputBg,
2236
- color: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO : t.textPrimary,
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,
2237
2445
  borderRadius: 8,
2238
2446
  fontSize: 12,
2239
2447
  fontWeight: 600,
@@ -2244,8 +2452,8 @@ function SequenceEditor({
2244
2452
  },
2245
2453
  s2
2246
2454
  )) }),
2247
- /* @__PURE__ */ jsx4("div", { style: { height: 14 } }),
2248
- /* @__PURE__ */ jsx4(
2455
+ /* @__PURE__ */ jsx6("div", { style: { height: 14 } }),
2456
+ /* @__PURE__ */ jsx6(
2249
2457
  "button",
2250
2458
  {
2251
2459
  onClick: () => removeMessage(selectedMsg.id),
@@ -2255,7 +2463,7 @@ function SequenceEditor({
2255
2463
  )
2256
2464
  ] })
2257
2465
  ] }),
2258
- /* @__PURE__ */ jsxs4("div", { style: {
2466
+ /* @__PURE__ */ jsxs6("div", { style: {
2259
2467
  padding: "4px 14px",
2260
2468
  fontSize: 11,
2261
2469
  color: t.textMuted,
@@ -2264,25 +2472,22 @@ function SequenceEditor({
2264
2472
  display: "flex",
2265
2473
  gap: 16
2266
2474
  }, children: [
2267
- /* @__PURE__ */ jsxs4("span", { children: [
2475
+ /* @__PURE__ */ jsxs6("span", { children: [
2268
2476
  actors.length,
2269
2477
  " actors"
2270
2478
  ] }),
2271
- /* @__PURE__ */ jsxs4("span", { children: [
2479
+ /* @__PURE__ */ jsxs6("span", { children: [
2272
2480
  messages.length,
2273
2481
  " messages"
2274
2482
  ] }),
2275
- /* @__PURE__ */ jsx4("span", { style: { marginLeft: "auto" }, children: "double-click actor to rename \xB7 drag a row to reorder" })
2483
+ /* @__PURE__ */ jsx6("span", { style: { marginLeft: "auto" }, children: "double-click actor to rename \xB7 drag a row to reorder" })
2276
2484
  ] })
2277
2485
  ] });
2278
2486
  }
2279
- function estimateW(text, pxPerChar = 7) {
2280
- return text.length * pxPerChar;
2281
- }
2282
2487
  function primaryBtn() {
2283
2488
  return {
2284
2489
  padding: "6px 12px",
2285
- background: INDIGO,
2490
+ background: INDIGO2,
2286
2491
  color: "#fff",
2287
2492
  border: "none",
2288
2493
  borderRadius: 8,
@@ -2321,167 +2526,24 @@ function input(t) {
2321
2526
  };
2322
2527
  }
2323
2528
  function Label({ t, children }) {
2324
- return /* @__PURE__ */ jsx4("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 4 }, children });
2529
+ return /* @__PURE__ */ jsx6("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 4 }, children });
2325
2530
  }
2326
2531
 
2327
- // src/ui/Minimap.tsx
2328
- import { useCallback as useCallback5, useRef as useRef4 } from "react";
2329
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2330
- var W = 168;
2331
- var H = 112;
2332
- var PAD = 18;
2333
- function Minimap({
2532
+ // src/ui/NodeNavigator.tsx
2533
+ import { useState as useState7 } from "react";
2534
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2535
+ function NodeNavigator({
2334
2536
  model,
2335
- viewportW,
2336
- viewportH,
2337
- transform,
2338
- measureNode,
2339
- onCenterOn,
2537
+ selected,
2538
+ variant,
2340
2539
  isDark,
2341
- accentColor
2540
+ t,
2541
+ acc,
2542
+ open,
2543
+ onToggle,
2544
+ onSelect
2342
2545
  }) {
2343
- const dragRef = useRef4(null);
2344
- const boxes = model.nodes.map((n) => {
2345
- const { w, h } = measureNode(n);
2346
- return { id: n.id, x: n.x ?? 0, y: n.y ?? 0, w, h };
2347
- });
2348
- if (boxes.length === 0) return null;
2349
- const vx = -transform.x / transform.scale;
2350
- const vy = -transform.y / transform.scale;
2351
- const vw = viewportW / transform.scale;
2352
- const vh = viewportH / transform.scale;
2353
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
2354
- for (const b of boxes) {
2355
- minX = Math.min(minX, b.x);
2356
- minY = Math.min(minY, b.y);
2357
- maxX = Math.max(maxX, b.x + b.w);
2358
- maxY = Math.max(maxY, b.y + b.h);
2359
- }
2360
- minX = Math.min(minX, vx);
2361
- minY = Math.min(minY, vy);
2362
- maxX = Math.max(maxX, vx + vw);
2363
- maxY = Math.max(maxY, vy + vh);
2364
- const contentW = Math.max(1, maxX - minX);
2365
- const contentH = Math.max(1, maxY - minY);
2366
- const scale = Math.min((W - PAD * 2) / contentW, (H - PAD * 2) / contentH);
2367
- const offsetX = (W - contentW * scale) / 2 - minX * scale;
2368
- const offsetY = (H - contentH * scale) / 2 - minY * scale;
2369
- const project = (x, y) => ({
2370
- x: offsetX + x * scale,
2371
- y: offsetY + y * scale
2372
- });
2373
- const unproject = (mx, my) => ({
2374
- x: (mx - offsetX) / scale,
2375
- y: (my - offsetY) / scale
2376
- });
2377
- const panTo = useCallback5((e) => {
2378
- const rect = e.currentTarget.getBoundingClientRect();
2379
- const mx = e.clientX - rect.left;
2380
- const my = e.clientY - rect.top;
2381
- const { x, y } = unproject(mx, my);
2382
- onCenterOn(x, y);
2383
- }, [onCenterOn, scale, offsetX, offsetY]);
2384
- const onMouseDown = (e) => {
2385
- e.stopPropagation();
2386
- dragRef.current = { active: true };
2387
- panTo(e);
2388
- };
2389
- const onMouseMove = (e) => {
2390
- if (!dragRef.current?.active) return;
2391
- panTo(e);
2392
- };
2393
- const onMouseUp = () => {
2394
- dragRef.current = null;
2395
- };
2396
- const bg = isDark ? "rgba(15,23,42,0.92)" : "rgba(255,255,255,0.94)";
2397
- const border = isDark ? "#334155" : "#e2e8f0";
2398
- const nodeFill = isDark ? "#475569" : "#cbd5e1";
2399
- const viewStroke = accentColor;
2400
- const viewFill = `${accentColor}22`;
2401
- const vp1 = project(vx, vy);
2402
- const vp2 = project(vx + vw, vy + vh);
2403
- const vpRect = {
2404
- x: Math.max(0, Math.min(W, vp1.x)),
2405
- y: Math.max(0, Math.min(H, vp1.y)),
2406
- w: Math.max(2, Math.min(W, vp2.x) - Math.max(0, vp1.x)),
2407
- h: Math.max(2, Math.min(H, vp2.y) - Math.max(0, vp1.y))
2408
- };
2409
- return /* @__PURE__ */ jsx5(
2410
- "div",
2411
- {
2412
- style: {
2413
- position: "absolute",
2414
- bottom: 14,
2415
- right: 14,
2416
- background: bg,
2417
- border: `1px solid ${border}`,
2418
- borderRadius: 10,
2419
- padding: 6,
2420
- boxShadow: isDark ? "0 8px 20px rgba(0,0,0,0.45)" : "0 6px 18px rgba(15,23,42,0.08)",
2421
- backdropFilter: "blur(6px)"
2422
- },
2423
- children: /* @__PURE__ */ jsxs5(
2424
- "svg",
2425
- {
2426
- width: W,
2427
- height: H,
2428
- style: { display: "block", cursor: "grab", borderRadius: 6 },
2429
- onMouseDown,
2430
- onMouseMove,
2431
- onMouseUp,
2432
- onMouseLeave: onMouseUp,
2433
- children: [
2434
- /* @__PURE__ */ jsx5("rect", { width: W, height: H, rx: 6, fill: isDark ? "#0f172a" : "#fafbfc" }),
2435
- boxes.map((b) => {
2436
- const p = project(b.x, b.y);
2437
- return /* @__PURE__ */ jsx5(
2438
- "rect",
2439
- {
2440
- x: p.x,
2441
- y: p.y,
2442
- width: Math.max(2, b.w * scale),
2443
- height: Math.max(2, b.h * scale),
2444
- rx: 2,
2445
- fill: nodeFill
2446
- },
2447
- b.id
2448
- );
2449
- }),
2450
- /* @__PURE__ */ jsx5(
2451
- "rect",
2452
- {
2453
- x: vpRect.x,
2454
- y: vpRect.y,
2455
- width: vpRect.w,
2456
- height: vpRect.h,
2457
- rx: 3,
2458
- fill: viewFill,
2459
- stroke: viewStroke,
2460
- strokeWidth: 1.25
2461
- }
2462
- )
2463
- ]
2464
- }
2465
- )
2466
- }
2467
- );
2468
- }
2469
-
2470
- // src/ui/NodeNavigator.tsx
2471
- import { useState as useState6 } from "react";
2472
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2473
- function NodeNavigator({
2474
- model,
2475
- selected,
2476
- variant,
2477
- isDark,
2478
- t,
2479
- acc,
2480
- open,
2481
- onToggle,
2482
- onSelect
2483
- }) {
2484
- const [search, setSearch] = useState6("");
2546
+ const [search, setSearch] = useState7("");
2485
2547
  const shapeIcon = (node) => {
2486
2548
  if (variant === "question") return "?";
2487
2549
  if (variant === "journey") return "\u2197";
@@ -2502,7 +2564,7 @@ function NodeNavigator({
2502
2564
  const inEdges = (id) => model.edges.filter((e) => e.to === id).length;
2503
2565
  const outEdges = (id) => model.edges.filter((e) => e.from === id).length;
2504
2566
  if (!open) {
2505
- return /* @__PURE__ */ jsxs6("div", { style: {
2567
+ return /* @__PURE__ */ jsxs7("div", { style: {
2506
2568
  width: 36,
2507
2569
  flexShrink: 0,
2508
2570
  background: t.panelBg,
@@ -2513,19 +2575,21 @@ function NodeNavigator({
2513
2575
  paddingTop: 8,
2514
2576
  gap: 6
2515
2577
  }, children: [
2516
- /* @__PURE__ */ jsx6(
2578
+ /* @__PURE__ */ jsx7(
2517
2579
  "button",
2518
2580
  {
2519
2581
  onClick: onToggle,
2520
2582
  title: "Open node list",
2583
+ "aria-expanded": false,
2584
+ "aria-label": "Open node list",
2521
2585
  style: { background: "none", border: "none", cursor: "pointer", color: t.textMuted, padding: 6, borderRadius: 6, fontSize: 14, lineHeight: 1 },
2522
2586
  children: "\u2630"
2523
2587
  }
2524
2588
  ),
2525
- /* @__PURE__ */ jsx6("div", { style: { fontSize: 10, color: t.textMuted, fontWeight: 700, writingMode: "vertical-rl", transform: "rotate(180deg)", letterSpacing: 0.5 }, children: model.nodes.length })
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 })
2526
2590
  ] });
2527
2591
  }
2528
- return /* @__PURE__ */ jsxs6("div", { style: {
2592
+ return /* @__PURE__ */ jsxs7("div", { style: {
2529
2593
  width: 216,
2530
2594
  flexShrink: 0,
2531
2595
  background: t.panelBg,
@@ -2534,7 +2598,7 @@ function NodeNavigator({
2534
2598
  flexDirection: "column",
2535
2599
  overflow: "hidden"
2536
2600
  }, children: [
2537
- /* @__PURE__ */ jsxs6("div", { style: {
2601
+ /* @__PURE__ */ jsxs7("div", { style: {
2538
2602
  display: "flex",
2539
2603
  alignItems: "center",
2540
2604
  justifyContent: "space-between",
@@ -2542,9 +2606,9 @@ function NodeNavigator({
2542
2606
  borderBottom: `1px solid ${t.panelBorder}`,
2543
2607
  flexShrink: 0
2544
2608
  }, children: [
2545
- /* @__PURE__ */ jsxs6("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
2546
- /* @__PURE__ */ jsx6("span", { style: { fontSize: 11, fontWeight: 700, color: t.textSecondary, textTransform: "uppercase", letterSpacing: 0.7 }, children: variant === "question" ? "Questions" : variant === "journey" ? "Steps" : "Nodes" }),
2547
- /* @__PURE__ */ jsx6("span", { style: {
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: {
2548
2612
  fontSize: 10,
2549
2613
  fontWeight: 700,
2550
2614
  color: t.textMuted,
@@ -2553,19 +2617,21 @@ function NodeNavigator({
2553
2617
  borderRadius: 99
2554
2618
  }, children: model.nodes.length })
2555
2619
  ] }),
2556
- /* @__PURE__ */ jsx6(
2620
+ /* @__PURE__ */ jsx7(
2557
2621
  "button",
2558
2622
  {
2559
2623
  onClick: onToggle,
2560
2624
  style: { background: "none", border: "none", cursor: "pointer", color: t.textMuted, padding: "2px 4px", borderRadius: 4, fontSize: 13, lineHeight: 1 },
2561
2625
  title: "Collapse",
2626
+ "aria-expanded": true,
2627
+ "aria-label": "Collapse node list",
2562
2628
  children: "\u2039"
2563
2629
  }
2564
2630
  )
2565
2631
  ] }),
2566
- /* @__PURE__ */ jsx6("div", { style: { padding: "8px 10px", borderBottom: `1px solid ${t.sectionBorder}`, flexShrink: 0 }, children: /* @__PURE__ */ jsxs6("div", { style: { position: "relative" }, children: [
2567
- /* @__PURE__ */ jsx6("span", { style: { position: "absolute", left: 8, top: "50%", transform: "translateY(-50%)", fontSize: 11, color: t.textMuted, pointerEvents: "none" }, children: "\u2315" }),
2568
- /* @__PURE__ */ jsx6(
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(
2569
2635
  "input",
2570
2636
  {
2571
2637
  value: search,
@@ -2586,12 +2652,12 @@ function NodeNavigator({
2586
2652
  }
2587
2653
  )
2588
2654
  ] }) }),
2589
- /* @__PURE__ */ jsxs6("div", { style: { flex: 1, overflowY: "auto", padding: "6px 8px", display: "flex", flexDirection: "column", gap: 2 }, children: [
2590
- filtered.length === 0 && /* @__PURE__ */ jsx6("div", { style: { textAlign: "center", padding: "20px 0", fontSize: 12, color: t.textMuted, fontStyle: "italic" }, children: model.nodes.length === 0 ? "No nodes yet" : "No matches" }),
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" }),
2591
2657
  filtered.map((node, idx) => {
2592
2658
  const isSelected = selected === node.id;
2593
2659
  const answers = node.metadata?.answers ?? [];
2594
- return /* @__PURE__ */ jsxs6(
2660
+ return /* @__PURE__ */ jsxs7(
2595
2661
  "button",
2596
2662
  {
2597
2663
  onClick: () => onSelect(node.id),
@@ -2616,7 +2682,7 @@ function NodeNavigator({
2616
2682
  if (!isSelected) e.currentTarget.style.background = "transparent";
2617
2683
  },
2618
2684
  children: [
2619
- /* @__PURE__ */ jsx6("div", { style: {
2685
+ /* @__PURE__ */ jsx7("div", { style: {
2620
2686
  width: 22,
2621
2687
  height: 22,
2622
2688
  borderRadius: 6,
@@ -2629,8 +2695,8 @@ function NodeNavigator({
2629
2695
  fontSize: variant === "journey" ? 9 : 11,
2630
2696
  fontWeight: 700
2631
2697
  }, children: variant === "journey" ? idx + 1 : shapeIcon(node) }),
2632
- /* @__PURE__ */ jsxs6("div", { style: { flex: 1, minWidth: 0 }, children: [
2633
- /* @__PURE__ */ jsx6("div", { style: {
2698
+ /* @__PURE__ */ jsxs7("div", { style: { flex: 1, minWidth: 0 }, children: [
2699
+ /* @__PURE__ */ jsx7("div", { style: {
2634
2700
  fontSize: 12,
2635
2701
  fontWeight: isSelected ? 600 : 400,
2636
2702
  color: isSelected ? acc.color : t.textPrimary,
@@ -2639,9 +2705,9 @@ function NodeNavigator({
2639
2705
  whiteSpace: "nowrap",
2640
2706
  lineHeight: 1.3
2641
2707
  }, children: node.label }),
2642
- /* @__PURE__ */ jsx6("div", { style: { fontSize: 10, color: t.textMuted, lineHeight: 1.2, marginTop: 1 }, children: variant === "question" ? `${answers.length} answer${answers.length !== 1 ? "s" : ""}` : `${inEdges(node.id)}\u2193 ${outEdges(node.id)}\u2192` })
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` })
2643
2709
  ] }),
2644
- isSelected && /* @__PURE__ */ jsx6("span", { style: { fontSize: 10, color: acc.color, flexShrink: 0 }, children: "\u25C9" })
2710
+ isSelected && /* @__PURE__ */ jsx7("span", { style: { fontSize: 10, color: acc.color, flexShrink: 0 }, children: "\u25C9" })
2645
2711
  ]
2646
2712
  },
2647
2713
  node.id
@@ -2651,137 +2717,6 @@ function NodeNavigator({
2651
2717
  ] });
2652
2718
  }
2653
2719
 
2654
- // src/ui/ContextMenu.tsx
2655
- import { useEffect as useEffect5, useRef as useRef5, useState as useState7 } from "react";
2656
- import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2657
- function ContextMenu({
2658
- x,
2659
- y,
2660
- nodeId,
2661
- edgeId,
2662
- isDark,
2663
- t,
2664
- acc,
2665
- canUndo,
2666
- canRedo,
2667
- onUndo,
2668
- onRedo,
2669
- onReCenter,
2670
- onAddNode,
2671
- onDuplicate,
2672
- onRename,
2673
- onDelete,
2674
- onDisconnect,
2675
- onEdgeRename,
2676
- onEdgeStyle,
2677
- onEdgeArrowhead,
2678
- onEdgeDelete,
2679
- onEdgeResetRouting,
2680
- currentEdgeStyle,
2681
- currentEdgeArrow,
2682
- edgeHasWaypoint,
2683
- containerRef
2684
- }) {
2685
- const menuRef = useRef5(null);
2686
- const [pos, setPos] = useState7({ x, y });
2687
- useEffect5(() => {
2688
- if (!menuRef.current || !containerRef.current) return;
2689
- const m = menuRef.current.getBoundingClientRect();
2690
- const c = containerRef.current.getBoundingClientRect();
2691
- let nx = x, ny = y;
2692
- if (nx + m.width > c.right - 8) nx = x - m.width;
2693
- if (ny + m.height > c.bottom - 8) ny = y - m.height;
2694
- setPos({ x: nx, y: ny });
2695
- }, [x, y, containerRef]);
2696
- const bg = isDark ? "#1e293b" : "#ffffff";
2697
- const border = isDark ? "#334155" : "#e2e8f0";
2698
- const hoverBg = isDark ? "#334155" : "#f1f5f9";
2699
- const dividerColor = isDark ? "#334155" : "#f1f5f9";
2700
- const text = t.textPrimary;
2701
- const muted = t.textMuted;
2702
- const item = (label, onClick, color, disabled) => /* @__PURE__ */ jsx7(
2703
- "button",
2704
- {
2705
- onClick: disabled ? void 0 : onClick,
2706
- style: {
2707
- display: "flex",
2708
- alignItems: "center",
2709
- gap: 10,
2710
- width: "100%",
2711
- padding: "7px 14px",
2712
- background: "none",
2713
- border: "none",
2714
- textAlign: "left",
2715
- cursor: disabled ? "default" : "pointer",
2716
- fontSize: 12,
2717
- fontFamily: "ui-sans-serif,system-ui,sans-serif",
2718
- color: disabled ? muted : color ?? text,
2719
- opacity: disabled ? 0.4 : 1,
2720
- borderRadius: 6
2721
- },
2722
- onMouseEnter: (e) => {
2723
- if (!disabled) e.currentTarget.style.background = hoverBg;
2724
- },
2725
- onMouseLeave: (e) => {
2726
- e.currentTarget.style.background = "none";
2727
- },
2728
- children: label
2729
- },
2730
- label
2731
- );
2732
- const divider2 = /* @__PURE__ */ jsx7("div", { style: { height: 1, background: dividerColor, margin: "4px 0" } });
2733
- return /* @__PURE__ */ jsx7(
2734
- "div",
2735
- {
2736
- ref: menuRef,
2737
- onMouseDown: (e) => e.stopPropagation(),
2738
- style: {
2739
- position: "fixed",
2740
- left: pos.x,
2741
- top: pos.y,
2742
- zIndex: 9999,
2743
- background: bg,
2744
- border: `1px solid ${border}`,
2745
- borderRadius: 10,
2746
- padding: "5px 0",
2747
- minWidth: 180,
2748
- boxShadow: isDark ? "0 8px 32px rgba(0,0,0,0.5)" : "0 8px 32px rgba(0,0,0,0.12)",
2749
- fontFamily: "ui-sans-serif,system-ui,sans-serif"
2750
- },
2751
- children: edgeId ? /* @__PURE__ */ jsxs7(Fragment2, { children: [
2752
- /* @__PURE__ */ jsx7("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Edge" }),
2753
- item("Rename label (dbl-click)", () => onEdgeRename?.()),
2754
- divider2,
2755
- /* @__PURE__ */ jsx7("div", { style: { padding: "4px 14px 2px", fontSize: 9, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Style" }),
2756
- item(`Solid${currentEdgeStyle === "solid" || !currentEdgeStyle ? " \u2713" : ""}`, () => onEdgeStyle?.("solid")),
2757
- item(`Dashed${currentEdgeStyle === "dashed" ? " \u2713" : ""}`, () => onEdgeStyle?.("dashed")),
2758
- item(`Dotted${currentEdgeStyle === "dotted" ? " \u2713" : ""}`, () => onEdgeStyle?.("dotted")),
2759
- divider2,
2760
- /* @__PURE__ */ jsx7("div", { style: { padding: "4px 14px 2px", fontSize: 9, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Arrowhead" }),
2761
- item(`Arrow${currentEdgeArrow !== "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("arrow")),
2762
- item(`None${currentEdgeArrow === "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("none")),
2763
- divider2,
2764
- item("Reset routing", () => onEdgeResetRouting?.(), void 0, !edgeHasWaypoint),
2765
- item("Delete edge", () => onEdgeDelete?.(), "#ef4444")
2766
- ] }) : nodeId ? /* @__PURE__ */ jsxs7(Fragment2, { children: [
2767
- /* @__PURE__ */ jsx7("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Node" }),
2768
- item("Rename (dbl-click)", onRename),
2769
- item("Duplicate", onDuplicate),
2770
- item("Disconnect all edges", onDisconnect),
2771
- divider2,
2772
- item("Delete node", onDelete, "#ef4444")
2773
- ] }) : /* @__PURE__ */ jsxs7(Fragment2, { children: [
2774
- /* @__PURE__ */ jsx7("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Canvas" }),
2775
- item("Add node here", onAddNode, acc.color),
2776
- item("Re-center (Ctrl+0)", onReCenter),
2777
- divider2,
2778
- item("Undo (Ctrl+Z)", onUndo, void 0, !canUndo),
2779
- item("Redo (Ctrl+Y)", onRedo, void 0, !canRedo)
2780
- ] })
2781
- }
2782
- );
2783
- }
2784
-
2785
2720
  // src/ui/render.tsx
2786
2721
  import { useState as useState8 } from "react";
2787
2722
 
@@ -2849,7 +2784,7 @@ function bezierPathVia(x1, y1, wx, wy, x2, y2) {
2849
2784
  }
2850
2785
 
2851
2786
  // src/ui/render.tsx
2852
- import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2787
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2853
2788
  var STYLE_LABEL = { pointerEvents: "none", userSelect: "none" };
2854
2789
  var STYLE_BLUR = { filter: "blur(4px)" };
2855
2790
  var STYLE_EDGE_HIT = { cursor: "pointer" };
@@ -2863,10 +2798,10 @@ function NodeShape({ node, selected, variant, stepNumber, t, isDark, w }) {
2863
2798
  const stroke = selected ? acc.color : t.nodeStroke;
2864
2799
  const fill = selected ? t.nodeSelectedFill : t.nodeFill;
2865
2800
  const sw = selected ? 1.75 : 1.25;
2866
- const glow = selected && /* @__PURE__ */ jsx8(Fragment3, { children: node.shape === "circle" ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
2801
+ const glow = selected && /* @__PURE__ */ jsx8(Fragment2, { children: node.shape === "circle" ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
2867
2802
  /* @__PURE__ */ jsx8("circle", { cx, cy, r: NODE_H2 / 2 + 3, fill: "none", stroke: acc.color, strokeWidth: 6, opacity: 0.18, style: STYLE_BLUR }),
2868
2803
  /* @__PURE__ */ jsx8("circle", { cx, cy, r: NODE_H2 / 2 + 1.5, fill: "none", stroke: acc.color, strokeWidth: 1, opacity: 0.55 })
2869
- ] }) : node.shape === "diamond" ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
2804
+ ] }) : node.shape === "diamond" ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
2870
2805
  /* @__PURE__ */ jsx8(
2871
2806
  "polygon",
2872
2807
  {
@@ -2888,7 +2823,7 @@ function NodeShape({ node, selected, variant, stepNumber, t, isDark, w }) {
2888
2823
  opacity: 0.55
2889
2824
  }
2890
2825
  )
2891
- ] }) : /* @__PURE__ */ jsxs8(Fragment3, { children: [
2826
+ ] }) : /* @__PURE__ */ jsxs8(Fragment2, { children: [
2892
2827
  /* @__PURE__ */ jsx8(
2893
2828
  "rect",
2894
2829
  {
@@ -2920,33 +2855,33 @@ function NodeShape({ node, selected, variant, stepNumber, t, isDark, w }) {
2920
2855
  )
2921
2856
  ] }) });
2922
2857
  const badgeColor = isDark ? ACCENT.emeraldDark : ACCENT.emerald;
2923
- const badge = variant === "journey" && stepNumber !== void 0 && /* @__PURE__ */ jsxs8(Fragment3, { children: [
2858
+ const badge = variant === "journey" && stepNumber !== void 0 && /* @__PURE__ */ jsxs8(Fragment2, { children: [
2924
2859
  /* @__PURE__ */ jsx8("circle", { cx: 14, cy: 14, r: 10, fill: badgeColor }),
2925
2860
  /* @__PURE__ */ jsx8("text", { x: 14, y: 18, textAnchor: "middle", fontSize: 9, fill: "white", fontWeight: "700", style: STYLE_LABEL, children: stepNumber })
2926
2861
  ] });
2927
2862
  switch (node.shape) {
2928
2863
  case "diamond": {
2929
2864
  const pts = `${cx},0 ${w},${cy} ${cx},${NODE_H2} 0,${cy}`;
2930
- return /* @__PURE__ */ jsxs8(Fragment3, { children: [
2865
+ return /* @__PURE__ */ jsxs8(Fragment2, { children: [
2931
2866
  glow,
2932
2867
  /* @__PURE__ */ jsx8("polygon", { points: pts, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
2933
2868
  badge
2934
2869
  ] });
2935
2870
  }
2936
2871
  case "circle":
2937
- return /* @__PURE__ */ jsxs8(Fragment3, { children: [
2872
+ return /* @__PURE__ */ jsxs8(Fragment2, { children: [
2938
2873
  glow,
2939
2874
  /* @__PURE__ */ jsx8("circle", { cx, cy, r: NODE_H2 / 2 - 1, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
2940
2875
  badge
2941
2876
  ] });
2942
2877
  case "parallelogram":
2943
- return /* @__PURE__ */ jsxs8(Fragment3, { children: [
2878
+ return /* @__PURE__ */ jsxs8(Fragment2, { children: [
2944
2879
  glow,
2945
2880
  /* @__PURE__ */ jsx8("polygon", { points: `14,0 ${w},0 ${w - 14},${NODE_H2} 0,${NODE_H2}`, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
2946
2881
  badge
2947
2882
  ] });
2948
2883
  default:
2949
- return /* @__PURE__ */ jsxs8(Fragment3, { children: [
2884
+ return /* @__PURE__ */ jsxs8(Fragment2, { children: [
2950
2885
  glow,
2951
2886
  /* @__PURE__ */ jsx8("rect", { width: w, height: NODE_H2, rx: 14, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
2952
2887
  badge
@@ -2969,7 +2904,7 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
2969
2904
  const textSub = isDark ? "#64748b" : "#94a3b8";
2970
2905
  const textAns = isDark ? "#cbd5e1" : "#374151";
2971
2906
  const portRowY = Q_BASE_H2 + Q_ANS_ROW_H2 - 8;
2972
- const glow = selected && /* @__PURE__ */ jsxs8(Fragment3, { children: [
2907
+ const glow = selected && /* @__PURE__ */ jsxs8(Fragment2, { children: [
2973
2908
  /* @__PURE__ */ jsx8(
2974
2909
  "rect",
2975
2910
  {
@@ -3000,7 +2935,7 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
3000
2935
  }
3001
2936
  )
3002
2937
  ] });
3003
- return /* @__PURE__ */ jsxs8(Fragment3, { children: [
2938
+ return /* @__PURE__ */ jsxs8(Fragment2, { children: [
3004
2939
  glow,
3005
2940
  /* @__PURE__ */ jsx8("rect", { width: qW, height: totalH, rx: 14, fill: nodeBg, stroke: nodeBorder, strokeWidth: selected ? 2 : 1.5, filter: "url(#nodeShadow)" }),
3006
2941
  /* @__PURE__ */ jsx8("clipPath", { id: `qhdr-${node.id}`, children: /* @__PURE__ */ jsx8("rect", { width: qW, height: Q_BASE_H2, rx: 14 }) }),
@@ -3020,7 +2955,7 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
3020
2955
  }
3021
2956
  ),
3022
2957
  /* @__PURE__ */ jsx8("line", { x1: 0, y1: Q_BASE_H2, x2: qW, y2: Q_BASE_H2, stroke: amberLine, strokeWidth: 1 }),
3023
- answers.length === 0 && /* @__PURE__ */ jsxs8(Fragment3, { children: [
2958
+ answers.length === 0 && /* @__PURE__ */ jsxs8(Fragment2, { children: [
3024
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" }),
3025
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" })
3026
2961
  ] }),
@@ -3213,80 +3148,676 @@ function EdgeLine({ edge, nodes, variant, t, isDark, acc, editing, editValue, on
3213
3148
  e.preventDefault();
3214
3149
  onEditCommit?.();
3215
3150
  }
3216
- if (e.key === "Escape") {
3217
- e.preventDefault();
3218
- onEditCancel?.();
3151
+ if (e.key === "Escape") {
3152
+ e.preventDefault();
3153
+ onEditCancel?.();
3154
+ }
3155
+ },
3156
+ onMouseDown: (e) => e.stopPropagation(),
3157
+ style: {
3158
+ width: "100%",
3159
+ height: "100%",
3160
+ border: "none",
3161
+ borderRadius: 6,
3162
+ outline: `2px solid ${acc.color}`,
3163
+ textAlign: "center",
3164
+ fontSize: 10,
3165
+ fontWeight: 500,
3166
+ background: t.inputBg,
3167
+ color: t.inputText,
3168
+ boxSizing: "border-box",
3169
+ padding: "0 6px",
3170
+ fontFamily: "inherit"
3171
+ }
3172
+ }
3173
+ ) }) : edge.label && !isAmber ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
3174
+ /* @__PURE__ */ jsx8(
3175
+ "rect",
3176
+ {
3177
+ x: mx - labelW / 2,
3178
+ y: my - 11,
3179
+ width: labelW,
3180
+ height: 19,
3181
+ rx: 5,
3182
+ fill: t.panelBg,
3183
+ stroke: t.cardBorder,
3184
+ strokeWidth: 1,
3185
+ style: STYLE_EDGE_LABEL_HIT
3186
+ }
3187
+ ),
3188
+ /* @__PURE__ */ jsx8(
3189
+ "text",
3190
+ {
3191
+ x: mx,
3192
+ y: my + 4,
3193
+ textAnchor: "middle",
3194
+ fontSize: 10,
3195
+ fill: t.textSecondary,
3196
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
3197
+ fontWeight: "500",
3198
+ style: STYLE_LABEL,
3199
+ children: edge.label
3200
+ }
3201
+ )
3202
+ ] }) : null
3203
+ ]
3204
+ }
3205
+ );
3206
+ }
3207
+
3208
+ // src/ui/Minimap.tsx
3209
+ import { useCallback as useCallback6, useRef as useRef4 } from "react";
3210
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
3211
+ var W = 168;
3212
+ var H = 112;
3213
+ var PAD = 18;
3214
+ function Minimap({
3215
+ model,
3216
+ viewportW,
3217
+ viewportH,
3218
+ transform,
3219
+ measureNode,
3220
+ onCenterOn,
3221
+ isDark,
3222
+ accentColor
3223
+ }) {
3224
+ const dragRef = useRef4(null);
3225
+ const boxes = model.nodes.map((n) => {
3226
+ const { w, h } = measureNode(n);
3227
+ return { id: n.id, x: n.x ?? 0, y: n.y ?? 0, w, h };
3228
+ });
3229
+ if (boxes.length === 0) return null;
3230
+ const vx = -transform.x / transform.scale;
3231
+ const vy = -transform.y / transform.scale;
3232
+ const vw = viewportW / transform.scale;
3233
+ const vh = viewportH / transform.scale;
3234
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3235
+ for (const b of boxes) {
3236
+ minX = Math.min(minX, b.x);
3237
+ minY = Math.min(minY, b.y);
3238
+ maxX = Math.max(maxX, b.x + b.w);
3239
+ maxY = Math.max(maxY, b.y + b.h);
3240
+ }
3241
+ minX = Math.min(minX, vx);
3242
+ minY = Math.min(minY, vy);
3243
+ maxX = Math.max(maxX, vx + vw);
3244
+ maxY = Math.max(maxY, vy + vh);
3245
+ const contentW = Math.max(1, maxX - minX);
3246
+ const contentH = Math.max(1, maxY - minY);
3247
+ const scale = Math.min((W - PAD * 2) / contentW, (H - PAD * 2) / contentH);
3248
+ const offsetX = (W - contentW * scale) / 2 - minX * scale;
3249
+ const offsetY = (H - contentH * scale) / 2 - minY * scale;
3250
+ const project = (x, y) => ({
3251
+ x: offsetX + x * scale,
3252
+ y: offsetY + y * scale
3253
+ });
3254
+ const unproject = (mx, my) => ({
3255
+ x: (mx - offsetX) / scale,
3256
+ y: (my - offsetY) / scale
3257
+ });
3258
+ const panTo = useCallback6((e) => {
3259
+ const rect = e.currentTarget.getBoundingClientRect();
3260
+ const mx = e.clientX - rect.left;
3261
+ const my = e.clientY - rect.top;
3262
+ const { x, y } = unproject(mx, my);
3263
+ onCenterOn(x, y);
3264
+ }, [onCenterOn, scale, offsetX, offsetY]);
3265
+ const onMouseDown = (e) => {
3266
+ e.stopPropagation();
3267
+ dragRef.current = { active: true };
3268
+ panTo(e);
3269
+ };
3270
+ const onMouseMove = (e) => {
3271
+ if (!dragRef.current?.active) return;
3272
+ panTo(e);
3273
+ };
3274
+ const onMouseUp = () => {
3275
+ dragRef.current = null;
3276
+ };
3277
+ const bg = isDark ? "rgba(15,23,42,0.92)" : "rgba(255,255,255,0.94)";
3278
+ const border = isDark ? "#334155" : "#e2e8f0";
3279
+ const nodeFill = isDark ? "#475569" : "#cbd5e1";
3280
+ const viewStroke = accentColor;
3281
+ const viewFill = `${accentColor}22`;
3282
+ const vp1 = project(vx, vy);
3283
+ const vp2 = project(vx + vw, vy + vh);
3284
+ const vpRect = {
3285
+ x: Math.max(0, Math.min(W, vp1.x)),
3286
+ y: Math.max(0, Math.min(H, vp1.y)),
3287
+ w: Math.max(2, Math.min(W, vp2.x) - Math.max(0, vp1.x)),
3288
+ h: Math.max(2, Math.min(H, vp2.y) - Math.max(0, vp1.y))
3289
+ };
3290
+ return /* @__PURE__ */ jsx9(
3291
+ "div",
3292
+ {
3293
+ "aria-label": "Minimap \u2014 click to re-center the viewport",
3294
+ role: "img",
3295
+ style: {
3296
+ position: "absolute",
3297
+ bottom: 14,
3298
+ right: 14,
3299
+ background: bg,
3300
+ border: `1px solid ${border}`,
3301
+ borderRadius: 10,
3302
+ padding: 6,
3303
+ boxShadow: isDark ? "0 8px 20px rgba(0,0,0,0.45)" : "0 6px 18px rgba(15,23,42,0.08)",
3304
+ backdropFilter: "blur(6px)"
3305
+ },
3306
+ children: /* @__PURE__ */ jsxs9(
3307
+ "svg",
3308
+ {
3309
+ width: W,
3310
+ height: H,
3311
+ style: { display: "block", cursor: "grab", borderRadius: 6 },
3312
+ onMouseDown,
3313
+ onMouseMove,
3314
+ onMouseUp,
3315
+ onMouseLeave: onMouseUp,
3316
+ children: [
3317
+ /* @__PURE__ */ jsx9("rect", { width: W, height: H, rx: 6, fill: isDark ? "#0f172a" : "#fafbfc" }),
3318
+ boxes.map((b) => {
3319
+ const p = project(b.x, b.y);
3320
+ return /* @__PURE__ */ jsx9(
3321
+ "rect",
3322
+ {
3323
+ x: p.x,
3324
+ y: p.y,
3325
+ width: Math.max(2, b.w * scale),
3326
+ height: Math.max(2, b.h * scale),
3327
+ rx: 2,
3328
+ fill: nodeFill
3329
+ },
3330
+ b.id
3331
+ );
3332
+ }),
3333
+ /* @__PURE__ */ jsx9(
3334
+ "rect",
3335
+ {
3336
+ x: vpRect.x,
3337
+ y: vpRect.y,
3338
+ width: vpRect.w,
3339
+ height: vpRect.h,
3340
+ rx: 3,
3341
+ fill: viewFill,
3342
+ stroke: viewStroke,
3343
+ strokeWidth: 1.25
3344
+ }
3345
+ )
3346
+ ]
3347
+ }
3348
+ )
3349
+ }
3350
+ );
3351
+ }
3352
+
3353
+ // src/ui/ContextMenu.tsx
3354
+ import { useEffect as useEffect6, useRef as useRef5, useState as useState9 } from "react";
3355
+ import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
3356
+ function ContextMenu({
3357
+ x,
3358
+ y,
3359
+ nodeId,
3360
+ edgeId,
3361
+ isDark,
3362
+ t,
3363
+ acc,
3364
+ canUndo,
3365
+ canRedo,
3366
+ onUndo,
3367
+ onRedo,
3368
+ onReCenter,
3369
+ onAddNode,
3370
+ onDuplicate,
3371
+ onRename,
3372
+ onDelete,
3373
+ onDisconnect,
3374
+ onEdgeRename,
3375
+ onEdgeStyle,
3376
+ onEdgeArrowhead,
3377
+ onEdgeDelete,
3378
+ onEdgeResetRouting,
3379
+ currentEdgeStyle,
3380
+ currentEdgeArrow,
3381
+ edgeHasWaypoint,
3382
+ containerRef
3383
+ }) {
3384
+ const menuRef = useRef5(null);
3385
+ const [pos, setPos] = useState9({ x, y });
3386
+ useEffect6(() => {
3387
+ if (!menuRef.current || !containerRef.current) return;
3388
+ const m = menuRef.current.getBoundingClientRect();
3389
+ const c = containerRef.current.getBoundingClientRect();
3390
+ let nx = x, ny = y;
3391
+ if (nx + m.width > c.right - 8) nx = x - m.width;
3392
+ if (ny + m.height > c.bottom - 8) ny = y - m.height;
3393
+ setPos({ x: nx, y: ny });
3394
+ }, [x, y, containerRef]);
3395
+ const bg = isDark ? "#1e293b" : "#ffffff";
3396
+ const border = isDark ? "#334155" : "#e2e8f0";
3397
+ const hoverBg = isDark ? "#334155" : "#f1f5f9";
3398
+ const dividerColor = isDark ? "#334155" : "#f1f5f9";
3399
+ const text = t.textPrimary;
3400
+ const muted = t.textMuted;
3401
+ const item = (label, onClick, color, disabled) => /* @__PURE__ */ jsx10(
3402
+ "button",
3403
+ {
3404
+ onClick: disabled ? void 0 : onClick,
3405
+ style: {
3406
+ display: "flex",
3407
+ alignItems: "center",
3408
+ gap: 10,
3409
+ width: "100%",
3410
+ padding: "7px 14px",
3411
+ background: "none",
3412
+ border: "none",
3413
+ textAlign: "left",
3414
+ cursor: disabled ? "default" : "pointer",
3415
+ fontSize: 12,
3416
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
3417
+ color: disabled ? muted : color ?? text,
3418
+ opacity: disabled ? 0.4 : 1,
3419
+ borderRadius: 6
3420
+ },
3421
+ onMouseEnter: (e) => {
3422
+ if (!disabled) e.currentTarget.style.background = hoverBg;
3423
+ },
3424
+ onMouseLeave: (e) => {
3425
+ e.currentTarget.style.background = "none";
3426
+ },
3427
+ children: label
3428
+ },
3429
+ label
3430
+ );
3431
+ const divider2 = /* @__PURE__ */ jsx10("div", { style: { height: 1, background: dividerColor, margin: "4px 0" } });
3432
+ return /* @__PURE__ */ jsx10(
3433
+ "div",
3434
+ {
3435
+ ref: menuRef,
3436
+ onMouseDown: (e) => e.stopPropagation(),
3437
+ style: {
3438
+ position: "fixed",
3439
+ left: pos.x,
3440
+ top: pos.y,
3441
+ zIndex: 9999,
3442
+ background: bg,
3443
+ border: `1px solid ${border}`,
3444
+ borderRadius: 10,
3445
+ padding: "5px 0",
3446
+ minWidth: 180,
3447
+ boxShadow: isDark ? "0 8px 32px rgba(0,0,0,0.5)" : "0 8px 32px rgba(0,0,0,0.12)",
3448
+ fontFamily: "ui-sans-serif,system-ui,sans-serif"
3449
+ },
3450
+ 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" }),
3452
+ item("Rename label (dbl-click)", () => onEdgeRename?.()),
3453
+ 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")),
3458
+ 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")),
3461
+ item(`None${currentEdgeArrow === "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("none")),
3462
+ divider2,
3463
+ item("Reset routing", () => onEdgeResetRouting?.(), void 0, !edgeHasWaypoint),
3464
+ item("Delete edge", () => onEdgeDelete?.(), "#ef4444")
3465
+ ] }) : 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" }),
3467
+ item("Rename (dbl-click)", onRename),
3468
+ item("Duplicate", onDuplicate),
3469
+ item("Disconnect all edges", onDisconnect),
3470
+ divider2,
3471
+ item("Delete node", onDelete, "#ef4444")
3472
+ ] }) : /* @__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" }),
3474
+ item("Add node here", onAddNode, acc.color),
3475
+ item("Re-center (Ctrl+0)", onReCenter),
3476
+ divider2,
3477
+ item("Undo (Ctrl+Z)", onUndo, void 0, !canUndo),
3478
+ item("Redo (Ctrl+Y)", onRedo, void 0, !canRedo)
3479
+ ] })
3480
+ }
3481
+ );
3482
+ }
3483
+
3484
+ // src/ui/DiagramCanvas.tsx
3485
+ import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
3486
+ var STYLE_LABEL2 = { pointerEvents: "none", userSelect: "none" };
3487
+ var STYLE_LIVE_PORT = { opacity: 0.85, pointerEvents: "none" };
3488
+ var STYLE_NODE_GRAB = { cursor: "grab" };
3489
+ 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))" };
3492
+ function DiagramCanvas(props) {
3493
+ const {
3494
+ model,
3495
+ variant,
3496
+ variantLabel,
3497
+ t,
3498
+ isDark,
3499
+ acc,
3500
+ transform,
3501
+ setTransform,
3502
+ selected,
3503
+ selectedSet,
3504
+ hoveredId,
3505
+ setHoveredId,
3506
+ drag,
3507
+ pan,
3508
+ liveEdge,
3509
+ boxSel,
3510
+ alignGuides,
3511
+ editingEdgeId,
3512
+ editEdgeLabel,
3513
+ setEditEdgeLabel,
3514
+ commitEdgeEdit,
3515
+ setEditingEdgeId,
3516
+ beginEditEdge,
3517
+ onEdgeContextMenu,
3518
+ setWaypointDrag,
3519
+ editingId,
3520
+ editLabel,
3521
+ setEditLabel,
3522
+ commitEdit,
3523
+ setEditingId,
3524
+ onNodeMouseDown,
3525
+ onNodeMouseUp,
3526
+ onNodeDblClick,
3527
+ onNodeContextMenu,
3528
+ onPortMouseDown,
3529
+ onAnswerPortDown,
3530
+ onSvgMouseDown,
3531
+ onMouseMove,
3532
+ onMouseUp,
3533
+ onSvgContextMenu,
3534
+ reducedMotion,
3535
+ isCoarse,
3536
+ portR,
3537
+ shadowClr,
3538
+ arrowClr,
3539
+ amberArrow,
3540
+ viewport,
3541
+ svgRef,
3542
+ containerRef,
3543
+ ctxMenu,
3544
+ history,
3545
+ onCtxUndo,
3546
+ onCtxRedo,
3547
+ onCtxReCenter,
3548
+ onCtxAddNode,
3549
+ onCtxDuplicate,
3550
+ onCtxRename,
3551
+ onCtxDelete,
3552
+ onCtxDisconnect,
3553
+ ctxEdgeStyle,
3554
+ ctxEdgeArrow,
3555
+ ctxEdgeHasWaypoint,
3556
+ onCtxEdgeRename,
3557
+ onCtxEdgeStyle,
3558
+ onCtxEdgeArrowhead,
3559
+ onCtxEdgeDelete,
3560
+ onCtxEdgeResetRouting
3561
+ } = 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 ? `
3581
+ .edge-flow { stroke-dasharray: 0; }
3582
+ .edge-flow-amber { stroke-dasharray: 0; }
3583
+ .edge-live { stroke-dasharray: 4 4; }
3584
+ ` : `
3585
+ @keyframes edgeFlow { to { stroke-dashoffset: -13; } }
3586
+ @keyframes edgeFlowFast { to { stroke-dashoffset: -13; } }
3587
+ .edge-flow { stroke-dasharray: 8 5; animation: edgeFlow 0.9s linear infinite; }
3588
+ .edge-flow-amber { stroke-dasharray: 6 4; animation: edgeFlowFast 0.65s linear infinite; }
3589
+ .edge-live { stroke-dasharray: 7 5; animation: edgeFlow 0.55s linear infinite; }
3590
+ ` }),
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"
3219
3649
  }
3220
- },
3221
- onMouseDown: (e) => e.stopPropagation(),
3222
- style: {
3223
- width: "100%",
3224
- height: "100%",
3225
- border: "none",
3226
- borderRadius: 6,
3227
- outline: `2px solid ${acc.color}`,
3228
- textAlign: "center",
3229
- fontSize: 10,
3230
- fontWeight: 500,
3231
- background: t.inputBg,
3232
- color: t.inputText,
3233
- boxSizing: "border-box",
3234
- padding: "0 6px",
3235
- fontFamily: "inherit"
3236
- }
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
3237
3740
  }
3238
- ) }) : edge.label && !isAmber ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
3239
- /* @__PURE__ */ jsx8(
3240
- "rect",
3241
- {
3242
- x: mx - labelW / 2,
3243
- y: my - 11,
3244
- width: labelW,
3245
- height: 19,
3246
- rx: 5,
3247
- fill: t.panelBg,
3248
- stroke: t.cardBorder,
3249
- strokeWidth: 1,
3250
- style: STYLE_EDGE_LABEL_HIT
3251
- }
3252
- ),
3253
- /* @__PURE__ */ jsx8(
3254
- "text",
3255
- {
3256
- x: mx,
3257
- y: my + 4,
3258
- textAnchor: "middle",
3259
- fontSize: 10,
3260
- fill: t.textSecondary,
3261
- fontFamily: "ui-sans-serif,system-ui,sans-serif",
3262
- fontWeight: "500",
3263
- style: STYLE_LABEL,
3264
- children: edge.label
3265
- }
3266
- )
3267
- ] }) : null
3268
- ]
3269
- }
3270
- );
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
+ ] });
3271
3802
  }
3272
3803
 
3273
3804
  // src/ui/hooks/useHistory.ts
3274
- import { useCallback as useCallback6, useRef as useRef6, useState as useState9 } from "react";
3805
+ import { useCallback as useCallback7, useRef as useRef6, useState as useState10 } from "react";
3275
3806
  var MAX_HISTORY = 80;
3276
3807
  function useHistory(initial, onChange) {
3277
- const [state, setState] = useState9(initial);
3808
+ const [state, setState] = useState10(initial);
3278
3809
  const stackRef = useRef6([initial]);
3279
3810
  const idxRef = useRef6(0);
3280
- const [, setTick] = useState9(0);
3811
+ const [, setTick] = useState10(0);
3281
3812
  const bump = () => setTick((n) => n + 1);
3282
- const apply = useCallback6(
3813
+ const apply = useCallback7(
3283
3814
  (next) => {
3284
3815
  setState(next);
3285
3816
  onChange?.(next);
3286
3817
  },
3287
3818
  [onChange]
3288
3819
  );
3289
- const applyAndPush = useCallback6(
3820
+ const applyAndPush = useCallback7(
3290
3821
  (next) => {
3291
3822
  const stack = stackRef.current.slice(0, idxRef.current + 1);
3292
3823
  stack.push(next);
@@ -3299,7 +3830,7 @@ function useHistory(initial, onChange) {
3299
3830
  },
3300
3831
  [onChange]
3301
3832
  );
3302
- const undo = useCallback6(() => {
3833
+ const undo = useCallback7(() => {
3303
3834
  if (idxRef.current <= 0) return;
3304
3835
  idxRef.current--;
3305
3836
  const next = stackRef.current[idxRef.current];
@@ -3307,7 +3838,7 @@ function useHistory(initial, onChange) {
3307
3838
  onChange?.(next);
3308
3839
  bump();
3309
3840
  }, [onChange]);
3310
- const redo = useCallback6(() => {
3841
+ const redo = useCallback7(() => {
3311
3842
  if (idxRef.current >= stackRef.current.length - 1) return;
3312
3843
  idxRef.current++;
3313
3844
  const next = stackRef.current[idxRef.current];
@@ -3327,10 +3858,10 @@ function useHistory(initial, onChange) {
3327
3858
  }
3328
3859
 
3329
3860
  // src/ui/hooks/useCanvasWheel.ts
3330
- import { useEffect as useEffect6 } from "react";
3861
+ import { useEffect as useEffect7 } from "react";
3331
3862
  function useCanvasWheel(ref, setTransform, options = {}) {
3332
3863
  const { min = 0.15, max = 3, factor = 0.1 } = options;
3333
- useEffect6(() => {
3864
+ useEffect7(() => {
3334
3865
  const el = ref.current;
3335
3866
  if (!el) return;
3336
3867
  const onWheel = (e) => {
@@ -3354,7 +3885,7 @@ function useCanvasWheel(ref, setTransform, options = {}) {
3354
3885
  }
3355
3886
 
3356
3887
  // src/ui/hooks/useCanvasTouch.ts
3357
- import { useEffect as useEffect7 } from "react";
3888
+ import { useEffect as useEffect8 } from "react";
3358
3889
  function useCanvasTouch(ref, {
3359
3890
  transform,
3360
3891
  setTransform,
@@ -3364,7 +3895,7 @@ function useCanvasTouch(ref, {
3364
3895
  longPressMs = 550,
3365
3896
  longPressSlop = 8
3366
3897
  }) {
3367
- useEffect7(() => {
3898
+ useEffect8(() => {
3368
3899
  const el = ref.current;
3369
3900
  if (!el) return;
3370
3901
  let touchPan = null;
@@ -3468,10 +3999,10 @@ function useCanvasTouch(ref, {
3468
3999
  }
3469
4000
 
3470
4001
  // src/ui/hooks/useElementSize.ts
3471
- import { useEffect as useEffect8, useState as useState10 } from "react";
4002
+ import { useEffect as useEffect9, useState as useState11 } from "react";
3472
4003
  function useElementSize(ref) {
3473
- const [size, setSize] = useState10({ w: 0, h: 0 });
3474
- useEffect8(() => {
4004
+ const [size, setSize] = useState11({ w: 0, h: 0 });
4005
+ useEffect9(() => {
3475
4006
  const el = ref.current;
3476
4007
  if (!el || typeof ResizeObserver === "undefined") return;
3477
4008
  const measure = () => {
@@ -3577,14 +4108,12 @@ function nearestInDirection(fromX, fromY, dir, candidates) {
3577
4108
  }
3578
4109
 
3579
4110
  // src/ui/DiagramEditor.tsx
3580
- import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
3581
- var STYLE_LABEL2 = { pointerEvents: "none", userSelect: "none" };
3582
- var STYLE_LIVE_PORT = { opacity: 0.85, pointerEvents: "none" };
4111
+ import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
3583
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 };
3584
4113
  var STYLE_FLEX_ROW = { flex: 1, display: "flex", overflow: "hidden" };
3585
4114
  function DiagramEditor(props) {
3586
4115
  if (props.initialModel?.type === "sequence") {
3587
- return /* @__PURE__ */ jsx9(
4116
+ return /* @__PURE__ */ jsx12(
3588
4117
  SequenceEditor,
3589
4118
  {
3590
4119
  initialModel: props.initialModel,
@@ -3598,7 +4127,7 @@ function DiagramEditor(props) {
3598
4127
  }
3599
4128
  );
3600
4129
  }
3601
- return /* @__PURE__ */ jsx9(FlowchartEditor, { ...props });
4130
+ return /* @__PURE__ */ jsx12(FlowchartEditor, { ...props });
3602
4131
  }
3603
4132
  function FlowchartEditor({
3604
4133
  initialModel,
@@ -3612,25 +4141,26 @@ function FlowchartEditor({
3612
4141
  themeOverrides
3613
4142
  }) {
3614
4143
  const base = initialModel ? { ...initialModel, variant: initialModel.variant ?? variant } : presetFlowchartModel(variant);
3615
- const notify = useCallback7((m) => onChange?.(m), [onChange]);
4144
+ const notify = useCallback8((m) => onChange?.(m), [onChange]);
3616
4145
  const history = useHistory(base, notify);
3617
4146
  const { state: model, apply: applyModel, applyAndPush, undo, redo } = history;
3618
- const [transform, setTransform] = useState11({ x: 60, y: 60, scale: 1 });
3619
- const [selected, setSelected] = useState11(null);
3620
- const [selectedSet, setSelectedSet] = useState11(() => /* @__PURE__ */ new Set());
3621
- const [drag, setDrag] = useState11(null);
3622
- const [pan, setPan] = useState11(null);
3623
- const [boxSel, setBoxSel] = useState11(null);
3624
- const [liveEdge, setLiveEdge] = useState11(null);
3625
- const [alignGuides, setAlignGuides] = useState11(null);
3626
- const [waypointDrag, setWaypointDrag] = useState11(null);
4147
+ const { toasts, showToast, dismissToast } = useToast();
4148
+ const [transform, setTransform] = useState12({ x: 60, y: 60, scale: 1 });
4149
+ const [selected, setSelected] = useState12(null);
4150
+ const [selectedSet, setSelectedSet] = useState12(() => /* @__PURE__ */ new Set());
4151
+ const [drag, setDrag] = useState12(null);
4152
+ const [pan, setPan] = useState12(null);
4153
+ const [boxSel, setBoxSel] = useState12(null);
4154
+ const [liveEdge, setLiveEdge] = useState12(null);
4155
+ const [alignGuides, setAlignGuides] = useState12(null);
4156
+ const [waypointDrag, setWaypointDrag] = useState12(null);
3627
4157
  const groupDragOriginsRef = useRef7(null);
3628
4158
  const clipboardRef = useRef7(null);
3629
- const selectOne = useCallback7((id) => {
4159
+ const selectOne = useCallback8((id) => {
3630
4160
  setSelected(id);
3631
4161
  setSelectedSet(id ? /* @__PURE__ */ new Set([id]) : /* @__PURE__ */ new Set());
3632
4162
  }, []);
3633
- const toggleSelect = useCallback7((id) => {
4163
+ const toggleSelect = useCallback8((id) => {
3634
4164
  setSelectedSet((prev) => {
3635
4165
  const next = new Set(prev);
3636
4166
  if (next.has(id)) {
@@ -3644,18 +4174,18 @@ function FlowchartEditor({
3644
4174
  return next;
3645
4175
  });
3646
4176
  }, []);
3647
- const clearSelection = useCallback7(() => {
4177
+ const clearSelection = useCallback8(() => {
3648
4178
  setSelected(null);
3649
4179
  setSelectedSet(/* @__PURE__ */ new Set());
3650
4180
  }, []);
3651
- const [editingId, setEditingId] = useState11(null);
3652
- const [editLabel, setEditLabel] = useState11("");
3653
- const [editingEdgeId, setEditingEdgeId] = useState11(null);
3654
- const [editEdgeLabel, setEditEdgeLabel] = useState11("");
3655
- const [hoveredId, setHoveredId] = useState11(null);
3656
- const [ctxMenu, setCtxMenu] = useState11(null);
3657
- const [navOpen, setNavOpen] = useState11(true);
3658
- const [announcement, setAnnouncement] = useState11("");
4181
+ const [editingId, setEditingId] = useState12(null);
4182
+ const [editLabel, setEditLabel] = useState12("");
4183
+ const [editingEdgeId, setEditingEdgeId] = useState12(null);
4184
+ const [editEdgeLabel, setEditEdgeLabel] = useState12("");
4185
+ const [hoveredId, setHoveredId] = useState12(null);
4186
+ const [ctxMenu, setCtxMenu] = useState12(null);
4187
+ const [navOpen, setNavOpen] = useState12(true);
4188
+ const [announcement, setAnnouncement] = useState12("");
3659
4189
  const svgRef = useRef7(null);
3660
4190
  const containerRef = useRef7(null);
3661
4191
  const reducedMotion = usePrefersReducedMotion();
@@ -3663,7 +4193,7 @@ function FlowchartEditor({
3663
4193
  const isCoarse = useIsCoarsePointer();
3664
4194
  const portR = isCoarse ? 9 : 6;
3665
4195
  const viewport = useElementSize(svgRef);
3666
- const reCenter = useCallback7(() => {
4196
+ const reCenter = useCallback8(() => {
3667
4197
  if (!svgRef.current) return;
3668
4198
  const rect = svgRef.current.getBoundingClientRect();
3669
4199
  const W2 = rect.width, H2 = rect.height;
@@ -3687,7 +4217,7 @@ function FlowchartEditor({
3687
4217
  const cx = (minX + maxX) / 2, cy = (minY + maxY) / 2;
3688
4218
  setTransform({ scale, x: W2 / 2 - cx * scale, y: H2 / 2 - cy * scale });
3689
4219
  }, [model.nodes, variant]);
3690
- const jumpToNode = useCallback7((nodeId) => {
4220
+ const jumpToNode = useCallback8((nodeId) => {
3691
4221
  const node = model.nodes.find((n) => n.id === nodeId);
3692
4222
  if (!node || !svgRef.current) return;
3693
4223
  const rect = svgRef.current.getBoundingClientRect();
@@ -3698,7 +4228,7 @@ function FlowchartEditor({
3698
4228
  setTransform({ scale, x: rect.width / 2 - cx * scale, y: rect.height / 2 - cy * scale });
3699
4229
  selectOne(nodeId);
3700
4230
  }, [model.nodes, variant, transform.scale, selectOne]);
3701
- const duplicateIds = useCallback7((ids) => {
4231
+ const duplicateIds = useCallback8((ids) => {
3702
4232
  if (ids.length === 0) return;
3703
4233
  const idSet = new Set(ids);
3704
4234
  const idMap = /* @__PURE__ */ new Map();
@@ -3730,92 +4260,90 @@ function FlowchartEditor({
3730
4260
  setSelected(newIds[newIds.length - 1] ?? null);
3731
4261
  setSelectedSet(new Set(newIds));
3732
4262
  }, [model, applyAndPush]);
3733
- const duplicateNode = useCallback7((nodeId) => {
4263
+ const duplicateNode = useCallback8((nodeId) => {
3734
4264
  duplicateIds([nodeId]);
3735
4265
  }, [duplicateIds]);
3736
- useEffect9(() => {
4266
+ useEffect10(() => {
3737
4267
  if (!ctxMenu) return;
3738
4268
  const close = () => setCtxMenu(null);
3739
4269
  window.addEventListener("mousedown", close);
3740
4270
  return () => window.removeEventListener("mousedown", close);
3741
4271
  }, [ctxMenu]);
3742
- useEffect9(() => {
3743
- const onKey = (e) => {
3744
- const tgt = e.target;
3745
- if (tgt && (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA" || tgt.isContentEditable)) return;
3746
- const ctrl = e.ctrlKey || e.metaKey;
3747
- if (ctrl && e.key === "z") {
3748
- e.preventDefault();
3749
- undo();
3750
- return;
3751
- }
3752
- if (ctrl && (e.key === "y" || e.shiftKey && e.key === "z")) {
3753
- e.preventDefault();
3754
- redo();
3755
- return;
3756
- }
3757
- if (ctrl && e.key === "0") {
3758
- e.preventDefault();
3759
- reCenter();
3760
- return;
3761
- }
3762
- if (ctrl && (e.key === "d" || e.key === "D")) {
3763
- if (selectedSet.size > 0) {
3764
- e.preventDefault();
3765
- duplicateIds(Array.from(selectedSet));
3766
- }
3767
- return;
4272
+ 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
+ } },
4285
+ {
4286
+ match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "d" || e.key === "D") && selectedSet.size > 0,
4287
+ run: () => {
4288
+ duplicateIds(Array.from(selectedSet));
4289
+ return true;
3768
4290
  }
3769
- if (ctrl && (e.key === "c" || e.key === "C")) {
3770
- if (selectedSet.size > 0) {
3771
- e.preventDefault();
3772
- const ids = new Set(selectedSet);
3773
- const nodes = model.nodes.filter((n) => ids.has(n.id));
3774
- const edges = model.edges.filter((ed) => ids.has(ed.from) && ids.has(ed.to));
3775
- clipboardRef.current = {
3776
- nodes: nodes.map((n) => ({ ...n })),
3777
- edges: edges.map((ed) => ({ ...ed }))
3778
- };
3779
- }
3780
- return;
4291
+ },
4292
+ {
4293
+ match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "c" || e.key === "C") && selectedSet.size > 0,
4294
+ run: () => {
4295
+ const ids = new Set(selectedSet);
4296
+ const nodes = model.nodes.filter((n) => ids.has(n.id));
4297
+ const edges = model.edges.filter((ed) => ids.has(ed.from) && ids.has(ed.to));
4298
+ clipboardRef.current = {
4299
+ nodes: nodes.map((n) => ({ ...n })),
4300
+ edges: edges.map((ed) => ({ ...ed }))
4301
+ };
4302
+ return true;
3781
4303
  }
3782
- if (ctrl && (e.key === "v" || e.key === "V")) {
4304
+ },
4305
+ {
4306
+ match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "v" || e.key === "V"),
4307
+ run: () => {
3783
4308
  const clip = clipboardRef.current;
3784
- if (clip && clip.nodes.length > 0) {
3785
- e.preventDefault();
3786
- const idMap = /* @__PURE__ */ new Map();
3787
- const nextNode = makeIdSource("node", model.nodes);
3788
- const nextEdge = makeIdSource("e", model.edges);
3789
- const newNodes = clip.nodes.map((n) => {
3790
- const newId = nextNode();
3791
- idMap.set(n.id, newId);
3792
- return { ...n, id: newId, x: (n.x ?? 0) + 24, y: (n.y ?? 0) + 24 };
3793
- });
3794
- const newEdges = clip.edges.map((ed) => ({
3795
- ...ed,
3796
- id: nextEdge(),
3797
- from: idMap.get(ed.from) ?? ed.from,
3798
- to: idMap.get(ed.to) ?? ed.to
3799
- }));
3800
- const m = { ...model, nodes: [...model.nodes, ...newNodes], edges: [...model.edges, ...newEdges] };
3801
- applyAndPush(m);
3802
- const newIds = newNodes.map((n) => n.id);
3803
- setSelected(newIds[newIds.length - 1]);
3804
- setSelectedSet(new Set(newIds));
3805
- setAnnouncement(`Pasted ${newIds.length} ${variantLabel.toLowerCase()}${newIds.length === 1 ? "" : "s"}.`);
3806
- }
3807
- return;
4309
+ if (!clip || clip.nodes.length === 0) return false;
4310
+ const idMap = /* @__PURE__ */ new Map();
4311
+ const nextNode = makeIdSource("node", model.nodes);
4312
+ const nextEdge = makeIdSource("e", model.edges);
4313
+ const newNodes = clip.nodes.map((n) => {
4314
+ const newId = nextNode();
4315
+ idMap.set(n.id, newId);
4316
+ return { ...n, id: newId, x: (n.x ?? 0) + 24, y: (n.y ?? 0) + 24 };
4317
+ });
4318
+ const newEdges = clip.edges.map((ed) => ({
4319
+ ...ed,
4320
+ id: nextEdge(),
4321
+ from: idMap.get(ed.from) ?? ed.from,
4322
+ to: idMap.get(ed.to) ?? ed.to
4323
+ }));
4324
+ const m = { ...model, nodes: [...model.nodes, ...newNodes], edges: [...model.edges, ...newEdges] };
4325
+ applyAndPush(m);
4326
+ const newIds = newNodes.map((n) => n.id);
4327
+ setSelected(newIds[newIds.length - 1]);
4328
+ setSelectedSet(new Set(newIds));
4329
+ setAnnouncement(`Pasted ${newIds.length} ${variantLabel.toLowerCase()}${newIds.length === 1 ? "" : "s"}.`);
4330
+ return true;
3808
4331
  }
3809
- if (e.key === "Escape") {
4332
+ },
4333
+ {
4334
+ match: (e) => e.key === "Escape",
4335
+ run: () => {
3810
4336
  if (ctxMenu) setCtxMenu(null);
3811
4337
  if (liveEdge) setLiveEdge(null);
3812
4338
  if (editingId) setEditingId(null);
3813
4339
  if (boxSel) setBoxSel(null);
3814
4340
  if (selectedSet.size > 0) clearSelection();
3815
- return;
4341
+ return true;
3816
4342
  }
3817
- if ((e.key === "Delete" || e.key === "Backspace") && selectedSet.size > 0) {
3818
- e.preventDefault();
4343
+ },
4344
+ {
4345
+ match: (e) => (e.key === "Delete" || e.key === "Backspace") && selectedSet.size > 0,
4346
+ run: () => {
3819
4347
  const ids = new Set(selectedSet);
3820
4348
  const updated = {
3821
4349
  ...model,
@@ -3825,29 +4353,34 @@ function FlowchartEditor({
3825
4353
  applyAndPush(updated);
3826
4354
  clearSelection();
3827
4355
  setAnnouncement(`Deleted ${ids.size} ${variantLabel.toLowerCase()}${ids.size === 1 ? "" : "s"}.`);
3828
- return;
4356
+ return true;
3829
4357
  }
3830
- if (selectedSet.size > 0 && (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight")) {
4358
+ },
4359
+ {
4360
+ match: (e) => selectedSet.size > 0 && e.altKey && !!selected && (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight"),
4361
+ run: (e) => {
3831
4362
  const dirKey = e.key === "ArrowLeft" ? "left" : e.key === "ArrowRight" ? "right" : e.key === "ArrowUp" ? "up" : "down";
3832
- if (e.altKey && selected) {
3833
- e.preventDefault();
3834
- const origin = model.nodes.find((n) => n.id === selected);
3835
- if (!origin) return;
3836
- const od = nodeDims(origin, variant);
3837
- const ox = (origin.x ?? 0) + od.w / 2;
3838
- const oy = (origin.y ?? 0) + od.h / 2;
3839
- const candidates = model.nodes.filter((n) => n.id !== selected).map((n) => {
3840
- const d = nodeDims(n, variant);
3841
- return { id: n.id, x: (n.x ?? 0) + d.w / 2, y: (n.y ?? 0) + d.h / 2 };
3842
- });
3843
- const nextId2 = nearestInDirection(ox, oy, dirKey, candidates);
3844
- if (nextId2) {
3845
- selectOne(nextId2);
3846
- setAnnouncement(`Selected ${model.nodes.find((n) => n.id === nextId2)?.label ?? ""}.`);
3847
- }
3848
- return;
4363
+ const origin = model.nodes.find((n) => n.id === selected);
4364
+ if (!origin) return false;
4365
+ const od = nodeDims(origin, variant);
4366
+ const ox = (origin.x ?? 0) + od.w / 2;
4367
+ const oy = (origin.y ?? 0) + od.h / 2;
4368
+ const candidates = model.nodes.filter((n) => n.id !== selected).map((n) => {
4369
+ const d = nodeDims(n, variant);
4370
+ return { id: n.id, x: (n.x ?? 0) + d.w / 2, y: (n.y ?? 0) + d.h / 2 };
4371
+ });
4372
+ const nextNodeId = nearestInDirection(ox, oy, dirKey, candidates);
4373
+ if (nextNodeId) {
4374
+ selectOne(nextNodeId);
4375
+ setAnnouncement(`Selected ${model.nodes.find((n) => n.id === nextNodeId)?.label ?? ""}.`);
3849
4376
  }
3850
- e.preventDefault();
4377
+ return true;
4378
+ }
4379
+ },
4380
+ {
4381
+ match: (e) => selectedSet.size > 0 && (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight"),
4382
+ run: (e) => {
4383
+ const dirKey = e.key === "ArrowLeft" ? "left" : e.key === "ArrowRight" ? "right" : e.key === "ArrowUp" ? "up" : "down";
3851
4384
  const step = e.shiftKey ? GRID * 4 : GRID;
3852
4385
  const dx = dirKey === "left" ? -step : dirKey === "right" ? step : 0;
3853
4386
  const dy = dirKey === "up" ? -step : dirKey === "down" ? step : 0;
@@ -3857,17 +4390,17 @@ function FlowchartEditor({
3857
4390
  nodes: model.nodes.map((n) => ids.has(n.id) ? { ...n, x: snap((n.x ?? 0) + dx), y: snap((n.y ?? 0) + dy) } : n)
3858
4391
  };
3859
4392
  applyAndPush(updated);
4393
+ return true;
3860
4394
  }
3861
- };
3862
- window.addEventListener("keydown", onKey);
3863
- return () => window.removeEventListener("keydown", onKey);
3864
- }, [undo, redo, reCenter, selected, selectedSet, ctxMenu, liveEdge, editingId, boxSel, model, applyAndPush, duplicateNode, clearSelection]);
3865
- const toCanvas = useCallback7((clientX, clientY) => {
4395
+ }
4396
+ ];
4397
+ useEditorKeyboard(keyCommands, [undo, redo, reCenter, selected, selectedSet, ctxMenu, liveEdge, editingId, boxSel, model, applyAndPush, duplicateNode, clearSelection]);
4398
+ const toCanvas = useCallback8((clientX, clientY) => {
3866
4399
  const rect = svgRef.current.getBoundingClientRect();
3867
4400
  return { x: (clientX - rect.left - transform.x) / transform.scale, y: (clientY - rect.top - transform.y) / transform.scale };
3868
4401
  }, [transform]);
3869
4402
  useCanvasWheel(svgRef, setTransform);
3870
- const onCanvasLongPress = useCallback7((x, y) => {
4403
+ const onCanvasLongPress = useCallback8((x, y) => {
3871
4404
  setCtxMenu({ x, y, nodeId: null });
3872
4405
  }, []);
3873
4406
  useCanvasTouch(svgRef, { transform, setTransform, onLongPress: onCanvasLongPress });
@@ -4138,8 +4671,8 @@ function FlowchartEditor({
4138
4671
  };
4139
4672
  applyAndPush(updated);
4140
4673
  };
4141
- const handleExport = useExporters(model, onExport, "diagram");
4142
- const positionFlowchartNodes = useCallback7((m) => ({
4674
+ const handleExport = useExporters(model, onExport, "diagram", (msg) => showToast(msg, "success"));
4675
+ const positionFlowchartNodes = useCallback8((m) => ({
4143
4676
  ...m,
4144
4677
  nodes: m.nodes.map((n, i) => ({
4145
4678
  ...n,
@@ -4147,14 +4680,19 @@ function FlowchartEditor({
4147
4680
  y: n.y ?? snap(80 + Math.floor(i / 4) * 140)
4148
4681
  }))
4149
4682
  }), []);
4150
- const handleImport = useImporter(applyAndPush, { transform: positionFlowchartNodes });
4683
+ const handleImport = useImporter(applyAndPush, {
4684
+ transform: positionFlowchartNodes,
4685
+ onSuccess: (msg) => showToast(msg, "success"),
4686
+ onError: (msg) => showToast(msg, "error")
4687
+ });
4151
4688
  const acc = variantAccent(variant, isDark);
4152
4689
  const variantLabel = variant === "question" ? "Question" : variant === "journey" ? "Step" : "Node";
4153
- const shadowColor = isDark ? "rgba(0,0,0,0.55)" : "rgba(15,23,42,0.09)";
4154
- const arrowColor = isDark ? "#64748b" : "#94a3b8";
4690
+ const shadowClr = shadowColor(isDark);
4691
+ const arrowClr = arrowColor(isDark);
4155
4692
  const amberArrow = isDark ? ACCENT.amberDark : ACCENT.amber;
4156
- return /* @__PURE__ */ jsxs9("div", { className: "fsd-editor", style: { display: "flex", flexDirection: "column", height, width: "100%", fontFamily: "ui-sans-serif,system-ui,sans-serif", boxSizing: "border-box", background: t.ctrlsBg }, children: [
4157
- /* @__PURE__ */ jsx9("style", { children: `
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: `
4158
4696
  .fsd-editor button:focus-visible,
4159
4697
  .fsd-editor input:focus-visible,
4160
4698
  .fsd-editor textarea:focus-visible,
@@ -4164,12 +4702,16 @@ function FlowchartEditor({
4164
4702
  outline-offset: 2px;
4165
4703
  border-radius: 6px;
4166
4704
  }
4705
+ .fsd-editor svg [role="button"]:focus-visible {
4706
+ outline: 2px solid ${acc.color};
4707
+ outline-offset: 3px;
4708
+ }
4167
4709
  .fsd-editor svg[role="application"]:focus-visible {
4168
4710
  outline: 2px solid ${acc.color};
4169
4711
  outline-offset: -2px;
4170
4712
  }
4171
4713
  ` }),
4172
- /* @__PURE__ */ jsx9(
4714
+ /* @__PURE__ */ jsx12(
4173
4715
  "div",
4174
4716
  {
4175
4717
  role: "status",
@@ -4179,28 +4721,28 @@ function FlowchartEditor({
4179
4721
  children: announcement
4180
4722
  }
4181
4723
  ),
4182
- /* @__PURE__ */ jsx9(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
4183
- /* @__PURE__ */ jsxs9("div", { style: { display: "flex", gap: 6, padding: "7px 14px", background: t.ctrlsBg, borderBottom: `1px solid ${t.ctrlsBorder}`, alignItems: "center", flexWrap: "wrap" }, children: [
4184
- /* @__PURE__ */ jsxs9("button", { onClick: () => addNode(), style: ctrlBtn(acc.color, isDark), children: [
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: [
4185
4727
  "+ ",
4186
4728
  variantLabel
4187
4729
  ] }),
4188
- selectedSet.size > 0 && /* @__PURE__ */ jsxs9(Fragment4, { children: [
4189
- /* @__PURE__ */ jsx9("div", { style: { width: 1, height: 20, background: t.ctrlsBorder, margin: "0 2px" } }),
4190
- /* @__PURE__ */ jsx9("button", { onClick: deleteSelected, style: { ...ctrlBtn("transparent", isDark), color: "#ef4444", border: `1px solid ${isDark ? "#7f1d1d" : "#fca5a5"}` }, children: selectedSet.size > 1 ? `Delete (${selectedSet.size})` : "Delete" })
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" })
4191
4733
  ] }),
4192
- liveEdge && /* @__PURE__ */ jsxs9("span", { style: { fontSize: 11, color: acc.color, fontWeight: 600, marginLeft: 6 }, children: [
4734
+ liveEdge && /* @__PURE__ */ jsxs12("span", { style: { fontSize: 11, color: acc.color, fontWeight: 600, marginLeft: 6 }, children: [
4193
4735
  liveEdge.answerLabel ? `Routing "${liveEdge.answerLabel}" \u2192` : "Drop on a node to connect",
4194
- /* @__PURE__ */ jsx9("span", { style: { fontWeight: 400, color: t.textMuted, marginLeft: 6 }, children: "release to cancel" })
4736
+ /* @__PURE__ */ jsx12("span", { style: { fontWeight: 400, color: t.textMuted, marginLeft: 6 }, children: "release to cancel" })
4195
4737
  ] }),
4196
- /* @__PURE__ */ jsxs9("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
4738
+ /* @__PURE__ */ jsxs12("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
4197
4739
  variant === "question" ? "drag answer port to connect \xB7 " : "drag port dot \xB7 ",
4198
4740
  "scroll to zoom \xB7 drag to pan"
4199
4741
  ] })
4200
4742
  ] }),
4201
- variant !== "flowchart" && /* @__PURE__ */ jsx9("div", { style: { padding: "3px 14px", background: acc.fill, borderBottom: `1px solid ${acc.border}`, fontSize: 11, color: acc.color, fontWeight: 600 }, children: variant === "question" ? "? Question Flow \u2014 add answers in the panel, drag their port to connect" : "\u2197 Journey Map \u2014 numbered steps, drag port to sequence" }),
4202
- /* @__PURE__ */ jsxs9("div", { style: STYLE_FLEX_ROW, children: [
4203
- /* @__PURE__ */ jsx9(
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(
4204
4746
  NodeNavigator,
4205
4747
  {
4206
4748
  model,
@@ -4214,323 +4756,162 @@ function FlowchartEditor({
4214
4756
  onSelect: jumpToNode
4215
4757
  }
4216
4758
  ),
4217
- /* @__PURE__ */ jsxs9("div", { ref: containerRef, style: { flex: 1, overflow: "hidden", position: "relative", background: t.canvas }, children: [
4218
- /* @__PURE__ */ jsxs9(
4219
- "svg",
4220
- {
4221
- ref: svgRef,
4222
- width: "100%",
4223
- height: "100%",
4224
- role: "application",
4225
- "aria-label": `${variantLabel} diagram editor. ${model.nodes.length} ${variantLabel.toLowerCase()}s, ${model.edges.length} connections. Scroll to zoom, drag to pan, click a ${variantLabel.toLowerCase()} to select.`,
4226
- tabIndex: 0,
4227
- style: { display: "block", cursor: pan ? "grabbing" : drag ? "grabbing" : liveEdge ? "crosshair" : "default", userSelect: "none", outline: "none" },
4228
- onMouseDown: onSvgMouseDown,
4229
- onMouseMove,
4230
- onMouseUp,
4231
- onMouseLeave: onMouseUp,
4232
- onContextMenu: onSvgContextMenu,
4233
- children: [
4234
- /* @__PURE__ */ jsxs9("defs", { children: [
4235
- /* @__PURE__ */ jsx9("style", { children: reducedMotion ? `
4236
- .edge-flow { stroke-dasharray: 0; }
4237
- .edge-flow-amber { stroke-dasharray: 0; }
4238
- .edge-live { stroke-dasharray: 4 4; }
4239
- ` : `
4240
- @keyframes edgeFlow { to { stroke-dashoffset: -13; } }
4241
- @keyframes edgeFlowFast { to { stroke-dashoffset: -13; } }
4242
- .edge-flow { stroke-dasharray: 8 5; animation: edgeFlow 0.9s linear infinite; }
4243
- .edge-flow-amber { stroke-dasharray: 6 4; animation: edgeFlowFast 0.65s linear infinite; }
4244
- .edge-live { stroke-dasharray: 7 5; animation: edgeFlow 0.55s linear infinite; }
4245
- ` }),
4246
- /* @__PURE__ */ jsx9("pattern", { id: "dots", width: GRID, height: GRID, patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ jsx9("circle", { cx: GRID / 2, cy: GRID / 2, r: 1.1, fill: t.dot }) }),
4247
- /* @__PURE__ */ jsx9("filter", { id: "nodeShadow", x: "-25%", y: "-25%", width: "150%", height: "160%", children: /* @__PURE__ */ jsx9("feDropShadow", { dx: "0", dy: "3", stdDeviation: "5", floodColor: shadowColor, floodOpacity: "1" }) }),
4248
- /* @__PURE__ */ jsx9("marker", { id: "arrowhead", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ jsx9("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: arrowColor }) }),
4249
- /* @__PURE__ */ jsx9("marker", { id: "arrowAmber", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ jsx9("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: amberArrow }) }),
4250
- /* @__PURE__ */ jsx9("marker", { id: "arrowLive", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ jsx9("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: acc.color }) })
4251
- ] }),
4252
- /* @__PURE__ */ jsx9("rect", { width: "100%", height: "100%", fill: "url(#dots)", "data-bg": "1" }),
4253
- /* @__PURE__ */ jsxs9("g", { transform: `translate(${transform.x},${transform.y}) scale(${transform.scale})`, children: [
4254
- model.edges.map((e) => /* @__PURE__ */ jsx9(
4255
- EdgeLine,
4256
- {
4257
- edge: e,
4258
- nodes: model.nodes,
4259
- variant,
4260
- t,
4261
- isDark,
4262
- acc,
4263
- editing: editingEdgeId === e.id,
4264
- editValue: editEdgeLabel,
4265
- onEditChange: setEditEdgeLabel,
4266
- onEditCommit: commitEdgeEdit,
4267
- onEditCancel: () => setEditingEdgeId(null),
4268
- onDoubleClick: beginEditEdge,
4269
- onContextMenu: onEdgeContextMenu,
4270
- onWaypointDown: (ev, edgeId) => setWaypointDrag(edgeId)
4271
- },
4272
- e.id
4273
- )),
4274
- liveEdge && (() => {
4275
- const d = bezierPath2(liveEdge.fromX, liveEdge.fromY, liveEdge.toX, liveEdge.toY, liveEdge.exitDir);
4276
- return /* @__PURE__ */ jsx9("path", { d, fill: "none", stroke: acc.color, strokeWidth: 2, strokeLinecap: "round", className: "edge-live", opacity: 0.8, markerEnd: "url(#arrowLive)" });
4277
- })(),
4278
- alignGuides?.x && /* @__PURE__ */ jsx9(
4279
- "line",
4280
- {
4281
- x1: alignGuides.x.pos,
4282
- x2: alignGuides.x.pos,
4283
- y1: alignGuides.x.minY,
4284
- y2: alignGuides.x.maxY,
4285
- stroke: acc.color,
4286
- strokeWidth: 1 / transform.scale,
4287
- strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
4288
- opacity: 0.85,
4289
- pointerEvents: "none"
4290
- }
4291
- ),
4292
- alignGuides?.y && /* @__PURE__ */ jsx9(
4293
- "line",
4294
- {
4295
- y1: alignGuides.y.pos,
4296
- y2: alignGuides.y.pos,
4297
- x1: alignGuides.y.minX,
4298
- x2: alignGuides.y.maxX,
4299
- stroke: acc.color,
4300
- strokeWidth: 1 / transform.scale,
4301
- strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
4302
- opacity: 0.85,
4303
- pointerEvents: "none"
4304
- }
4305
- ),
4306
- model.nodes.map((node, idx) => {
4307
- const isHovered = hoveredId === node.id;
4308
- const isQuestion2 = variant === "question";
4309
- const { w: nW, h: nH } = nodeDims(node, variant);
4310
- const isSelected = selectedSet.has(node.id);
4311
- return /* @__PURE__ */ jsxs9(
4312
- "g",
4313
- {
4314
- transform: `translate(${node.x ?? 0},${node.y ?? 0})`,
4315
- role: "button",
4316
- "aria-label": `${variantLabel} ${variant === "journey" ? idx + 1 + ": " : ""}${node.label}${isSelected ? ", selected" : ""}`,
4317
- style: { cursor: drag?.nodeId === node.id ? "grabbing" : "grab" },
4318
- onMouseDown: (e) => onNodeMouseDown(e, node.id),
4319
- onMouseUp: (e) => onNodeMouseUp(e, node.id),
4320
- onDoubleClick: (e) => onNodeDblClick(e, node.id),
4321
- onContextMenu: (e) => onNodeContextMenu(e, node.id),
4322
- onMouseEnter: () => setHoveredId(node.id),
4323
- onMouseLeave: () => setHoveredId(null),
4324
- children: [
4325
- /* @__PURE__ */ jsx9("title", { children: `${variantLabel}: ${node.label}` }),
4326
- isQuestion2 ? /* @__PURE__ */ jsx9(QuestionNode, { node, selected: isSelected, edges: model.edges, isDark, onAnswerPortDown, qW: nW }) : /* @__PURE__ */ jsxs9(Fragment4, { children: [
4327
- /* @__PURE__ */ jsx9(NodeShape, { node, selected: isSelected, variant, stepNumber: variant === "journey" ? idx + 1 : void 0, t, isDark, w: nW }),
4328
- editingId === node.id ? /* @__PURE__ */ jsx9("foreignObject", { x: 6, y: 6, width: nW - 12, height: NODE_H2 - 12, children: /* @__PURE__ */ jsx9(
4329
- "input",
4330
- {
4331
- autoFocus: true,
4332
- value: editLabel,
4333
- onChange: (e) => setEditLabel(e.target.value),
4334
- onBlur: commitEdit,
4335
- onKeyDown: (e) => {
4336
- if (e.key === "Enter") commitEdit();
4337
- if (e.key === "Escape") setEditingId(null);
4338
- },
4339
- style: { width: "100%", height: "100%", border: "none", borderRadius: 6, outline: `2px solid ${acc.color}`, textAlign: "center", fontSize: 13, fontWeight: 500, background: t.inputBg, boxSizing: "border-box", padding: "0 6px", fontFamily: "inherit", color: t.inputText }
4340
- }
4341
- ) }) : /* @__PURE__ */ jsx9("text", { x: nW / 2, y: NODE_H2 / 2 + 5, textAnchor: "middle", fontSize: 13, fontWeight: "500", fontFamily: "ui-sans-serif,system-ui,sans-serif", fill: isSelected ? acc.color : t.textPrimary, style: STYLE_LABEL2, children: node.label }),
4342
- /* @__PURE__ */ jsx9(
4343
- "circle",
4344
- {
4345
- cx: nW / 2,
4346
- cy: NODE_H2 + 1,
4347
- r: portR,
4348
- fill: acc.color,
4349
- stroke: isDark ? "#0f172a" : "white",
4350
- strokeWidth: 2,
4351
- style: { cursor: "crosshair", opacity: isHovered || isCoarse ? 1 : 0, transition: "opacity 0.15s", pointerEvents: isHovered || isCoarse ? "all" : "none", filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.25))" },
4352
- onMouseDown: (e) => onPortMouseDown(e, node.id)
4353
- }
4354
- )
4355
- ] }),
4356
- liveEdge && liveEdge.fromId !== node.id && /* @__PURE__ */ jsx9("circle", { cx: nW / 2, cy: -1, r: portR, fill: acc.color, stroke: isDark ? "#0f172a" : "white", strokeWidth: 2, style: STYLE_LIVE_PORT })
4357
- ]
4358
- },
4359
- node.id
4360
- );
4361
- })
4362
- ] })
4363
- ]
4364
- }
4365
- ),
4366
- boxSel && Math.abs(boxSel.cx - boxSel.sx) + Math.abs(boxSel.cy - boxSel.sy) > 4 && containerRef.current && (() => {
4367
- const rect = containerRef.current.getBoundingClientRect();
4368
- const left = Math.min(boxSel.sx, boxSel.cx) - rect.left;
4369
- const top = Math.min(boxSel.sy, boxSel.cy) - rect.top;
4370
- const w = Math.abs(boxSel.cx - boxSel.sx);
4371
- const h = Math.abs(boxSel.cy - boxSel.sy);
4372
- return /* @__PURE__ */ jsx9(
4373
- "div",
4374
- {
4375
- style: {
4376
- position: "absolute",
4377
- left,
4378
- top,
4379
- width: w,
4380
- height: h,
4381
- border: `1px dashed ${acc.color}`,
4382
- background: isDark ? "rgba(99,102,241,0.10)" : "rgba(99,102,241,0.08)",
4383
- pointerEvents: "none",
4384
- borderRadius: 4
4385
- }
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);
4386
4839
  }
4387
- );
4388
- })(),
4389
- model.nodes.length === 0 && /* @__PURE__ */ jsxs9("div", { style: { position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", pointerEvents: "none", gap: 8 }, children: [
4390
- /* @__PURE__ */ jsx9("div", { style: { fontSize: 36, opacity: 0.1, color: t.textPrimary }, children: variant === "question" ? "?" : variant === "journey" ? "\u2197" : "\u2B21" }),
4391
- /* @__PURE__ */ jsxs9("div", { style: { fontSize: 13, color: t.textMuted, fontWeight: 500 }, children: [
4392
- "Click ",
4393
- /* @__PURE__ */ jsxs9("strong", { style: { color: acc.color }, children: [
4394
- "+ ",
4395
- variantLabel
4396
- ] }),
4397
- " to start"
4398
- ] })
4399
- ] }),
4400
- model.nodes.length > 0 && viewport.w > 0 && /* @__PURE__ */ jsx9(
4401
- Minimap,
4402
- {
4403
- model,
4404
- viewportW: viewport.w,
4405
- viewportH: viewport.h,
4406
- transform,
4407
- isDark,
4408
- accentColor: acc.color,
4409
- measureNode: (n) => nodeDims(n, variant),
4410
- onCenterOn: (cx, cy) => {
4411
- setTransform((tr) => ({ ...tr, x: viewport.w / 2 - cx * tr.scale, y: viewport.h / 2 - cy * tr.scale }));
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);
4412
4847
  }
4413
- }
4414
- ),
4415
- ctxMenu && (() => {
4416
- const ctxEdge = ctxMenu.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0;
4417
- return /* @__PURE__ */ jsx9(
4418
- ContextMenu,
4419
- {
4420
- x: ctxMenu.x,
4421
- y: ctxMenu.y,
4422
- nodeId: ctxMenu.nodeId,
4423
- edgeId: ctxMenu.edgeId,
4424
- isDark,
4425
- t,
4426
- acc,
4427
- canUndo: history.canUndo,
4428
- canRedo: history.canRedo,
4429
- onUndo: () => {
4430
- undo();
4431
- setCtxMenu(null);
4432
- },
4433
- onRedo: () => {
4434
- redo();
4435
- setCtxMenu(null);
4436
- },
4437
- onReCenter: () => {
4438
- reCenter();
4439
- setCtxMenu(null);
4440
- },
4441
- onAddNode: () => {
4442
- const rect = svgRef.current.getBoundingClientRect();
4443
- const cx = (ctxMenu.x - rect.left - transform.x) / transform.scale;
4444
- const cy = (ctxMenu.y - rect.top - transform.y) / transform.scale;
4445
- addNode({ x: cx, y: cy });
4446
- setCtxMenu(null);
4447
- },
4448
- onDuplicate: () => {
4449
- if (ctxMenu.nodeId) {
4450
- duplicateNode(ctxMenu.nodeId);
4451
- setCtxMenu(null);
4452
- }
4453
- },
4454
- onRename: () => {
4455
- if (ctxMenu.nodeId) {
4456
- const node = model.nodes.find((n) => n.id === ctxMenu.nodeId);
4457
- setEditingId(ctxMenu.nodeId);
4458
- setEditLabel(node.label);
4459
- setCtxMenu(null);
4460
- }
4461
- },
4462
- onDelete: () => {
4463
- if (ctxMenu.nodeId) {
4464
- deleteNode(ctxMenu.nodeId);
4465
- setCtxMenu(null);
4466
- }
4467
- },
4468
- onDisconnect: () => {
4469
- if (ctxMenu.nodeId) {
4470
- const m = { ...model, edges: model.edges.filter((e) => e.from !== ctxMenu.nodeId && e.to !== ctxMenu.nodeId) };
4471
- applyAndPush(m);
4472
- setCtxMenu(null);
4473
- }
4474
- },
4475
- currentEdgeStyle: ctxEdge?.style ?? "solid",
4476
- currentEdgeArrow: ctxEdge?.arrowhead ?? "arrow",
4477
- edgeHasWaypoint: !!ctxEdge?.waypoint,
4478
- onEdgeRename: () => {
4479
- if (ctxMenu.edgeId) {
4480
- beginEditEdge(ctxMenu.edgeId);
4481
- setCtxMenu(null);
4482
- }
4483
- },
4484
- onEdgeStyle: (s2) => {
4485
- if (ctxMenu.edgeId) {
4486
- setEdgeStyle(ctxMenu.edgeId, s2);
4487
- setCtxMenu(null);
4488
- }
4489
- },
4490
- onEdgeArrowhead: (a) => {
4491
- if (ctxMenu.edgeId) {
4492
- setEdgeArrowhead(ctxMenu.edgeId, a);
4493
- setCtxMenu(null);
4494
- }
4495
- },
4496
- onEdgeDelete: () => {
4497
- if (ctxMenu.edgeId) {
4498
- deleteEdge(ctxMenu.edgeId);
4499
- setCtxMenu(null);
4500
- }
4501
- },
4502
- onEdgeResetRouting: () => {
4503
- if (ctxMenu.edgeId) {
4504
- resetEdgeRouting(ctxMenu.edgeId);
4505
- setCtxMenu(null);
4506
- }
4507
- },
4508
- containerRef
4848
+ },
4849
+ onCtxDelete: () => {
4850
+ if (ctxMenu?.nodeId) {
4851
+ deleteNode(ctxMenu.nodeId);
4852
+ setCtxMenu(null);
4509
4853
  }
4510
- );
4511
- })()
4512
- ] }),
4513
- selected && /* @__PURE__ */ jsx9(StepEditor, { nodeId: selected, model, onModelChange: (m) => {
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);
4884
+ }
4885
+ },
4886
+ onCtxEdgeResetRouting: () => {
4887
+ if (ctxMenu?.edgeId) {
4888
+ resetEdgeRouting(ctxMenu.edgeId);
4889
+ setCtxMenu(null);
4890
+ }
4891
+ }
4892
+ }
4893
+ ),
4894
+ selected && /* @__PURE__ */ jsx12(StepEditor, { nodeId: selected, model, onModelChange: (m) => {
4514
4895
  applyAndPush(m);
4515
4896
  }, variant, isDark, t, acc }, selected)
4516
4897
  ] }),
4517
- /* @__PURE__ */ jsxs9("div", { style: { padding: "4px 14px", fontSize: 11, color: t.textMuted, background: t.statusBg, borderTop: `1px solid ${t.ctrlsBorder}`, display: "flex", gap: 16 }, children: [
4518
- /* @__PURE__ */ jsxs9("span", { children: [
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: [
4519
4900
  model.nodes.length,
4520
4901
  " ",
4521
4902
  variantLabel.toLowerCase(),
4522
4903
  "s"
4523
4904
  ] }),
4524
- /* @__PURE__ */ jsxs9("span", { children: [
4905
+ /* @__PURE__ */ jsxs12("span", { children: [
4525
4906
  model.edges.length,
4526
4907
  " connections"
4527
4908
  ] }),
4528
- /* @__PURE__ */ jsxs9("span", { children: [
4909
+ /* @__PURE__ */ jsxs12("span", { children: [
4529
4910
  Math.round(transform.scale * 100),
4530
4911
  "% zoom"
4531
4912
  ] }),
4532
- /* @__PURE__ */ jsx9("span", { style: { marginLeft: "auto" }, children: "Ctrl+Z undo \xB7 Ctrl+Y redo \xB7 Ctrl+0 fit \xB7 Alt+Arrow traverse" }),
4533
- selected && /* @__PURE__ */ jsx9("span", { style: { color: acc.color }, children: model.nodes.find((n) => n.id === selected)?.label })
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 })
4534
4915
  ] })
4535
4916
  ] });
4536
4917
  }