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.cjs CHANGED
@@ -31,7 +31,7 @@ __export(ui_exports, {
31
31
  module.exports = __toCommonJS(ui_exports);
32
32
 
33
33
  // src/ui/DiagramEditor.tsx
34
- var import_react17 = require("react");
34
+ var import_react20 = require("react");
35
35
 
36
36
  // src/ui/Toolbar.tsx
37
37
  var import_react2 = require("react");
@@ -407,6 +407,12 @@ var ACCENT = {
407
407
  emeraldDarkLight: "rgba(16,185,129,0.12)",
408
408
  emeraldDarkBorder: "rgba(16,185,129,0.3)"
409
409
  };
410
+ function shadowColor(isDark) {
411
+ return isDark ? "rgba(0,0,0,0.55)" : "rgba(15,23,42,0.09)";
412
+ }
413
+ function arrowColor(isDark) {
414
+ return isDark ? "#64748b" : "#94a3b8";
415
+ }
410
416
  function variantAccent(variant, isDark) {
411
417
  if (variant === "question") {
412
418
  return isDark ? { color: ACCENT.amberDark, fill: ACCENT.amberDarkLight, border: ACCENT.amberDarkBorder, glow: ACCENT.amberGlow } : { color: ACCENT.amber, fill: ACCENT.amberLight, border: ACCENT.amberBorder, glow: ACCENT.amberGlow };
@@ -438,10 +444,10 @@ function Toolbar({ onExport, onImport, allowedExports, allowImport = true }) {
438
444
  ] }),
439
445
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: divider }),
440
446
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 4, alignItems: "center" }, children: [
441
- allowImport && onImport && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: () => setImportOpen(true), style: ghostBtn, children: "\u2191 Import" }),
447
+ allowImport && onImport && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: () => setImportOpen(true), "aria-label": "Import diagram", style: ghostBtn, children: "\u2191 Import" }),
442
448
  formats.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
443
449
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: 11, color: darkTheme.inputText, margin: "0 4px" }, children: "Export \u2192" }),
444
- formats.map((f) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: () => onExport(f.key), style: exportBtn, children: f.label }, f.key))
450
+ formats.map((f) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: () => onExport(f.key), "aria-label": `Export as ${f.label}`, style: exportBtn, children: f.label }, f.key))
445
451
  ] })
446
452
  ] }),
447
453
  onImport && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -719,7 +725,7 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
719
725
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: { display: "block", fontSize: 10, fontWeight: 700, color: tt.labelText, marginBottom: 8, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Shape" }),
720
726
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6 }, children: SHAPES.map((s2) => {
721
727
  const active = (node.shape ?? "rectangle") === s2.key;
722
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { onClick: () => setShape(s2.key), style: {
728
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { onClick: () => setShape(s2.key), "aria-pressed": active, style: {
723
729
  display: "flex",
724
730
  flexDirection: "column",
725
731
  alignItems: "center",
@@ -766,7 +772,12 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
766
772
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => removeAnswer(ans), style: { background: "none", border: "none", color: tt.textMuted, cursor: "pointer", fontSize: 12, padding: "8px 10px", flexShrink: 0 }, title: "Remove", children: "\u2715" })
767
773
  ] }, ans + i);
768
774
  }),
769
- addingAnswer ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginTop: 10, background: tt.addFormBg, borderRadius: 10, padding: 12, border: `1.5px solid ${accentBorder}` }, children: [
775
+ addingAnswer ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { role: "group", "aria-label": "Add answer form", onKeyDown: (e) => {
776
+ if (e.key === "Escape") {
777
+ setAddingAnswer(false);
778
+ setNewAnswer("");
779
+ }
780
+ }, style: { marginTop: 10, background: tt.addFormBg, borderRadius: 10, padding: 12, border: `1.5px solid ${accentBorder}` }, children: [
770
781
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { autoFocus: true, value: newAnswer, onChange: (e) => setNewAnswer(e.target.value), onKeyDown: (e) => e.key === "Enter" && addAnswer(), placeholder: "Answer text\u2026", style: { ...inputStyle, marginBottom: 8 } }),
771
782
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: 6 }, children: [
772
783
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: addAnswer, style: addBtnStyle, children: "Add Answer" }),
@@ -804,7 +815,9 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
804
815
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => removeEdge(edge.id), style: { background: "none", border: "none", color: tt.textMuted, cursor: "pointer", fontSize: 12, padding: "8px 10px", flexShrink: 0 }, title: "Remove", children: "\u2715" })
805
816
  ] }, edge.id);
806
817
  }),
807
- addingBranch ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginTop: 10, background: tt.addFormBg, borderRadius: 10, padding: 12, border: `1.5px solid ${accentBorder}` }, children: [
818
+ addingBranch ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { role: "group", "aria-label": "Add branch form", onKeyDown: (e) => {
819
+ if (e.key === "Escape") setAddingBranch(false);
820
+ }, style: { marginTop: 10, background: tt.addFormBg, borderRadius: 10, padding: 12, border: `1.5px solid ${accentBorder}` }, children: [
808
821
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "flex", gap: 6, marginBottom: 10 }, children: ["new", "existing"].map((mode) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => setBranchMode(mode), style: {
809
822
  flex: 1,
810
823
  padding: "5px 0",
@@ -839,18 +852,299 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
839
852
  }
840
853
 
841
854
  // src/ui/SequenceEditor.tsx
842
- var import_react8 = require("react");
855
+ var import_react11 = require("react");
856
+
857
+ // src/ui/SequenceCanvas.tsx
858
+ var import_react4 = require("react");
859
+ var import_jsx_runtime4 = require("react/jsx-runtime");
860
+ var HEADER_H = 64;
861
+ var HEADER_PAD = 24;
862
+ var ROW_H = 64;
863
+ var SIDE_PAD = 40;
864
+ var INDIGO = "#4f46e5";
865
+ var INDIGO_SOFT = "#eef2ff";
866
+ var STYLE_SEQ_GRAB = { cursor: "grab" };
867
+ var STYLE_SEQ_GRABBING = { cursor: "grabbing" };
868
+ var STYLE_SEQ_ACTOR_TEXT = { cursor: "pointer", userSelect: "none" };
869
+ var STYLE_SEQ_REMOVE_BTN = { cursor: "pointer" };
870
+ var STYLE_SEQ_REMOVE_ICON = { pointerEvents: "none", userSelect: "none" };
871
+ var STYLE_SEQ_DRAGGING = { opacity: 0.85 };
872
+ function estimateW(text, pxPerChar = 7) {
873
+ return text.length * pxPerChar;
874
+ }
875
+ function SequenceCanvas(props) {
876
+ const {
877
+ model,
878
+ actors,
879
+ messages,
880
+ t,
881
+ isDark,
882
+ colW,
883
+ totalW,
884
+ totalH,
885
+ actorX,
886
+ msgY,
887
+ selected,
888
+ editingId,
889
+ setEditingId,
890
+ drag,
891
+ onRowMouseDown,
892
+ renameActor,
893
+ removeActor,
894
+ svgRef
895
+ } = props;
896
+ const visualMessages = (0, import_react4.useMemo)(() => {
897
+ if (!drag?.active) return messages;
898
+ const idx = messages.findIndex((m) => m.id === drag.id);
899
+ if (idx < 0) return messages;
900
+ const next = messages.slice();
901
+ const [moved] = next.splice(idx, 1);
902
+ next.splice(drag.targetIdx, 0, moved);
903
+ return next;
904
+ }, [messages, drag]);
905
+ if (actors.length === 0 && messages.length === 0) {
906
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
907
+ position: "absolute",
908
+ inset: 0,
909
+ display: "flex",
910
+ flexDirection: "column",
911
+ alignItems: "center",
912
+ justifyContent: "center",
913
+ gap: 10,
914
+ color: t.textMuted,
915
+ pointerEvents: "none"
916
+ }, children: [
917
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 36, opacity: 0.15, color: t.textPrimary }, children: "\u2194" }),
918
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { fontSize: 13, fontWeight: 500 }, children: [
919
+ "Click ",
920
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { style: { color: INDIGO }, children: "+ Actor" }),
921
+ " then ",
922
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { style: { color: INDIGO }, children: "+ Message" }),
923
+ " to start"
924
+ ] })
925
+ ] });
926
+ }
927
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
928
+ "svg",
929
+ {
930
+ ref: svgRef,
931
+ width: totalW,
932
+ height: totalH,
933
+ style: { display: "block", cursor: drag?.active ? "grabbing" : "default", userSelect: "none" },
934
+ children: [
935
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("defs", { children: [
936
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pattern", { id: "seqdots", x: "0", y: "0", width: "24", height: "24", patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { cx: 12, cy: 12, r: 1.1, fill: t.dot }) }),
937
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("filter", { id: "seqShadow", x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("feDropShadow", { dx: 0, dy: 3, stdDeviation: 5, floodColor: shadowColor(isDark) }) }),
938
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("marker", { id: "seqArrow", markerWidth: 9, markerHeight: 7, refX: 8.5, refY: 3.5, orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: t.arrow }) })
939
+ ] }),
940
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { width: totalW, height: totalH, fill: "url(#seqdots)" }),
941
+ actors.map((name) => {
942
+ const x = actorX(name);
943
+ const top = HEADER_PAD + HEADER_H;
944
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
945
+ "line",
946
+ {
947
+ x1: x,
948
+ x2: x,
949
+ y1: top + 4,
950
+ y2: totalH - 24,
951
+ stroke: t.lifeline,
952
+ strokeWidth: 1.25,
953
+ strokeDasharray: "5 5"
954
+ },
955
+ `life-${name}`
956
+ );
957
+ }),
958
+ visualMessages.map((msg, idx) => {
959
+ const y = msgY(idx);
960
+ const fromX = actorX(msg.from);
961
+ const toX = actorX(msg.to);
962
+ const selectedHere = selected === msg.id;
963
+ const isDragging = drag?.active && drag.id === msg.id;
964
+ const isSelf = msg.from === msg.to;
965
+ const stroke = selectedHere ? INDIGO : t.arrow;
966
+ const dash = msg.style === "dashed" ? "6,4" : void 0;
967
+ const cursorStyle = drag?.active ? STYLE_SEQ_GRABBING : STYLE_SEQ_GRAB;
968
+ const groupStyle = isDragging ? { ...cursorStyle, ...STYLE_SEQ_DRAGGING } : cursorStyle;
969
+ if (isSelf) {
970
+ const startX = fromX;
971
+ const loopW = 36;
972
+ const loopY = y - 6;
973
+ const d = `M ${startX} ${loopY} C ${startX + loopW} ${loopY}, ${startX + loopW} ${loopY + 24}, ${startX} ${loopY + 24}`;
974
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { onMouseDown: (e) => onRowMouseDown(e, msg.id), style: groupStyle, children: [
975
+ (selectedHere || isDragging) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
976
+ "rect",
977
+ {
978
+ x: SIDE_PAD - 8,
979
+ y: y - 22,
980
+ width: totalW - (SIDE_PAD - 8) * 2,
981
+ height: ROW_H - 12,
982
+ rx: 10,
983
+ fill: INDIGO_SOFT,
984
+ opacity: isDark ? 0.18 : 0.6
985
+ }
986
+ ),
987
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d, fill: "none", stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
988
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("text", { x: startX + loopW + 8, y: loopY + 16, fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
989
+ ] }, msg.id);
990
+ }
991
+ const labelX = (fromX + toX) / 2;
992
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { onMouseDown: (e) => onRowMouseDown(e, msg.id), style: groupStyle, children: [
993
+ (selectedHere || isDragging) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
994
+ "rect",
995
+ {
996
+ x: SIDE_PAD - 8,
997
+ y: y - 22,
998
+ width: totalW - (SIDE_PAD - 8) * 2,
999
+ height: ROW_H - 12,
1000
+ rx: 10,
1001
+ fill: INDIGO_SOFT,
1002
+ opacity: isDark ? 0.18 : 0.6
1003
+ }
1004
+ ),
1005
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: fromX, y1: y, x2: toX, y2: y, stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
1006
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1007
+ "rect",
1008
+ {
1009
+ x: labelX - estimateW(msg.label) / 2 - 6,
1010
+ y: y - 18,
1011
+ width: estimateW(msg.label) + 12,
1012
+ height: 18,
1013
+ rx: 6,
1014
+ fill: t.canvas,
1015
+ stroke: selectedHere ? INDIGO : t.cardBorder,
1016
+ strokeWidth: selectedHere ? 1.25 : 1
1017
+ }
1018
+ ),
1019
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("text", { x: labelX, y: y - 5, textAnchor: "middle", fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
1020
+ ] }, msg.id);
1021
+ }),
1022
+ actors.map((name) => {
1023
+ const x = actorX(name);
1024
+ const w = colW - 24;
1025
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { children: [
1026
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1027
+ "rect",
1028
+ {
1029
+ x: x - w / 2,
1030
+ y: HEADER_PAD,
1031
+ width: w,
1032
+ height: HEADER_H,
1033
+ rx: 12,
1034
+ fill: t.actorFill,
1035
+ stroke: t.actorStroke,
1036
+ strokeWidth: 1.25,
1037
+ filter: "url(#seqShadow)"
1038
+ }
1039
+ ),
1040
+ editingId === name ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("foreignObject", { x: x - w / 2 + 8, y: HEADER_PAD + 16, width: w - 16, height: 32, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1041
+ "input",
1042
+ {
1043
+ autoFocus: true,
1044
+ defaultValue: name,
1045
+ onBlur: (e) => {
1046
+ renameActor(name, e.currentTarget.value.trim());
1047
+ setEditingId(null);
1048
+ },
1049
+ onKeyDown: (e) => {
1050
+ if (e.key === "Enter") {
1051
+ renameActor(name, e.target.value.trim());
1052
+ setEditingId(null);
1053
+ }
1054
+ if (e.key === "Escape") setEditingId(null);
1055
+ },
1056
+ style: {
1057
+ width: "100%",
1058
+ height: "100%",
1059
+ border: "none",
1060
+ borderRadius: 6,
1061
+ outline: `2px solid ${INDIGO}`,
1062
+ textAlign: "center",
1063
+ fontSize: 13,
1064
+ fontWeight: 600,
1065
+ background: t.inputBg,
1066
+ color: t.inputText,
1067
+ boxSizing: "border-box",
1068
+ padding: "0 6px",
1069
+ fontFamily: "inherit"
1070
+ }
1071
+ }
1072
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1073
+ "text",
1074
+ {
1075
+ x,
1076
+ y: HEADER_PAD + HEADER_H / 2 + 4,
1077
+ textAnchor: "middle",
1078
+ fontSize: 13,
1079
+ fontWeight: 700,
1080
+ fill: t.actorText,
1081
+ role: "button",
1082
+ tabIndex: 0,
1083
+ "aria-label": `Actor ${name} \u2014 press Enter or F2 to rename`,
1084
+ style: STYLE_SEQ_ACTOR_TEXT,
1085
+ onDoubleClick: () => setEditingId(name),
1086
+ onKeyDown: (e) => {
1087
+ if (e.key === "Enter" || e.key === "F2") {
1088
+ e.preventDefault();
1089
+ setEditingId(name);
1090
+ }
1091
+ },
1092
+ children: name
1093
+ }
1094
+ ),
1095
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1096
+ "circle",
1097
+ {
1098
+ cx: x + w / 2 - 12,
1099
+ cy: HEADER_PAD + 14,
1100
+ r: 9,
1101
+ fill: "transparent",
1102
+ role: "button",
1103
+ tabIndex: 0,
1104
+ "aria-label": `Remove actor ${name}`,
1105
+ style: STYLE_SEQ_REMOVE_BTN,
1106
+ onClick: () => removeActor(name),
1107
+ onKeyDown: (e) => {
1108
+ if (e.key === "Enter" || e.key === " ") {
1109
+ e.preventDefault();
1110
+ removeActor(name);
1111
+ }
1112
+ },
1113
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("title", { children: [
1114
+ "Remove actor ",
1115
+ name
1116
+ ] })
1117
+ }
1118
+ ),
1119
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1120
+ "text",
1121
+ {
1122
+ x: x + w / 2 - 12,
1123
+ y: HEADER_PAD + 18,
1124
+ textAnchor: "middle",
1125
+ fontSize: 12,
1126
+ fill: t.textMuted,
1127
+ style: STYLE_SEQ_REMOVE_ICON,
1128
+ children: "\xD7"
1129
+ }
1130
+ )
1131
+ ] }, `hdr-${name}`);
1132
+ })
1133
+ ]
1134
+ }
1135
+ );
1136
+ }
843
1137
 
844
1138
  // src/ui/hooks/useEditorTheme.ts
845
- var import_react5 = require("react");
1139
+ var import_react6 = require("react");
846
1140
 
847
1141
  // src/ui/hooks/useSystemTheme.ts
848
- var import_react4 = require("react");
1142
+ var import_react5 = require("react");
849
1143
  function useIsDark(theme) {
850
- const [sysDark, setSysDark] = (0, import_react4.useState)(
1144
+ const [sysDark, setSysDark] = (0, import_react5.useState)(
851
1145
  () => typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)").matches : false
852
1146
  );
853
- (0, import_react4.useEffect)(() => {
1147
+ (0, import_react5.useEffect)(() => {
854
1148
  if (theme !== "auto" || typeof window === "undefined") return;
855
1149
  const mq = window.matchMedia("(prefers-color-scheme: dark)");
856
1150
  const handler = (e) => setSysDark(e.matches);
@@ -860,10 +1154,10 @@ function useIsDark(theme) {
860
1154
  return theme === "dark" || theme === "auto" && sysDark;
861
1155
  }
862
1156
  function useIsCoarsePointer() {
863
- const [coarse, setCoarse] = (0, import_react4.useState)(
1157
+ const [coarse, setCoarse] = (0, import_react5.useState)(
864
1158
  () => typeof window !== "undefined" ? window.matchMedia("(pointer: coarse)").matches : false
865
1159
  );
866
- (0, import_react4.useEffect)(() => {
1160
+ (0, import_react5.useEffect)(() => {
867
1161
  if (typeof window === "undefined") return;
868
1162
  const mq = window.matchMedia("(pointer: coarse)");
869
1163
  const handler = (e) => setCoarse(e.matches);
@@ -873,10 +1167,10 @@ function useIsCoarsePointer() {
873
1167
  return coarse;
874
1168
  }
875
1169
  function usePrefersReducedMotion() {
876
- const [reduced, setReduced] = (0, import_react4.useState)(
1170
+ const [reduced, setReduced] = (0, import_react5.useState)(
877
1171
  () => typeof window !== "undefined" ? window.matchMedia("(prefers-reduced-motion: reduce)").matches : false
878
1172
  );
879
- (0, import_react4.useEffect)(() => {
1173
+ (0, import_react5.useEffect)(() => {
880
1174
  if (typeof window === "undefined") return;
881
1175
  const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
882
1176
  const handler = (e) => setReduced(e.matches);
@@ -889,7 +1183,7 @@ function usePrefersReducedMotion() {
889
1183
  // src/ui/hooks/useEditorTheme.ts
890
1184
  function useEditorTheme(theme, overrides, palettes) {
891
1185
  const isDark = useIsDark(theme);
892
- const t = (0, import_react5.useMemo)(
1186
+ const t = (0, import_react6.useMemo)(
893
1187
  () => ({ ...isDark ? palettes.dark : palettes.light, ...overrides ?? {} }),
894
1188
  // palettes is a stable module-level constant in every caller, so it is
895
1189
  // deliberately omitted from the dep array to keep the memo key tight.
@@ -900,7 +1194,7 @@ function useEditorTheme(theme, overrides, palettes) {
900
1194
  }
901
1195
 
902
1196
  // src/ui/hooks/useExporters.ts
903
- var import_react6 = require("react");
1197
+ var import_react7 = require("react");
904
1198
 
905
1199
  // src/exporters/mermaid.ts
906
1200
  var SHAPE_OPEN = {
@@ -1286,8 +1580,8 @@ async function toPNG(model) {
1286
1580
  }
1287
1581
 
1288
1582
  // src/ui/hooks/useExporters.ts
1289
- function useExporters(model, onExport, filename = "diagram") {
1290
- return (0, import_react6.useCallback)(async (format) => {
1583
+ function useExporters(model, onExport, filename = "diagram", onSuccess) {
1584
+ return (0, import_react7.useCallback)(async (format) => {
1291
1585
  let content;
1292
1586
  switch (format) {
1293
1587
  case "mermaid":
@@ -1310,6 +1604,7 @@ function useExporters(model, onExport, filename = "diagram") {
1310
1604
  }
1311
1605
  if (onExport) {
1312
1606
  onExport(format, content);
1607
+ onSuccess?.(`Exported as ${format.toUpperCase()}`);
1313
1608
  return;
1314
1609
  }
1315
1610
  const url = content instanceof Blob ? URL.createObjectURL(content) : URL.createObjectURL(new Blob([content], { type: "text/plain" }));
@@ -1318,11 +1613,12 @@ function useExporters(model, onExport, filename = "diagram") {
1318
1613
  a.download = `${filename}.${format === "plantuml" ? "puml" : format}`;
1319
1614
  a.click();
1320
1615
  URL.revokeObjectURL(url);
1321
- }, [model, onExport, filename]);
1616
+ onSuccess?.(`Downloaded ${a.download}`);
1617
+ }, [model, onExport, filename, onSuccess]);
1322
1618
  }
1323
1619
 
1324
1620
  // src/ui/hooks/useImporter.ts
1325
- var import_react7 = require("react");
1621
+ var import_react8 = require("react");
1326
1622
 
1327
1623
  // src/core/model.ts
1328
1624
  var Model = class _Model {
@@ -1616,19 +1912,91 @@ function fromJSON(json) {
1616
1912
 
1617
1913
  // src/ui/hooks/useImporter.ts
1618
1914
  function useImporter(applyAndPush, options = {}) {
1619
- const { expectedType, transform } = options;
1620
- return (0, import_react7.useCallback)((text) => {
1915
+ const { expectedType, transform, onSuccess, onError } = options;
1916
+ const reportError = onError ?? ((msg) => alert(msg));
1917
+ return (0, import_react8.useCallback)((text) => {
1621
1918
  try {
1622
1919
  const parsed = text.trim().startsWith("{") ? fromJSON(text).toJSON() : fromMermaid(text).toJSON();
1623
1920
  if (expectedType && parsed.type !== expectedType) {
1624
- alert(`Imported diagram is not a ${expectedType} diagram.`);
1921
+ reportError(`Imported diagram is not a ${expectedType} diagram.`);
1625
1922
  return;
1626
1923
  }
1627
1924
  applyAndPush(transform ? transform(parsed) : parsed);
1925
+ onSuccess?.("Diagram imported successfully");
1628
1926
  } catch (err) {
1629
- alert(`Import failed: ${err.message}`);
1927
+ reportError(`Import failed: ${err.message}`);
1630
1928
  }
1631
- }, [applyAndPush, expectedType, transform]);
1929
+ }, [applyAndPush, expectedType, transform, onSuccess, onError]);
1930
+ }
1931
+
1932
+ // src/ui/hooks/useToast.ts
1933
+ var import_react9 = require("react");
1934
+ var _toastSeq = 0;
1935
+ function useToast() {
1936
+ const [toasts, setToasts] = (0, import_react9.useState)([]);
1937
+ const showToast = (0, import_react9.useCallback)((message, type = "info") => {
1938
+ const id = ++_toastSeq;
1939
+ setToasts((prev) => [...prev, { id, message, type }]);
1940
+ setTimeout(() => {
1941
+ setToasts((prev) => prev.filter((t) => t.id !== id));
1942
+ }, 3e3);
1943
+ }, []);
1944
+ const dismissToast = (0, import_react9.useCallback)((id) => {
1945
+ setToasts((prev) => prev.filter((t) => t.id !== id));
1946
+ }, []);
1947
+ return { toasts, showToast, dismissToast };
1948
+ }
1949
+
1950
+ // src/ui/ToastContainer.tsx
1951
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1952
+ var TOAST_COLORS = {
1953
+ success: { bg: "#065f46", border: "#10b981", text: "#ecfdf5" },
1954
+ error: { bg: "#7f1d1d", border: "#ef4444", text: "#fef2f2" },
1955
+ info: { bg: "#1e3a5f", border: "#3b82f6", text: "#eff6ff" }
1956
+ };
1957
+ var containerStyle = {
1958
+ position: "absolute",
1959
+ top: 8,
1960
+ right: 8,
1961
+ display: "flex",
1962
+ flexDirection: "column",
1963
+ gap: 6,
1964
+ zIndex: 9999,
1965
+ pointerEvents: "none"
1966
+ };
1967
+ function ToastContainer({ toasts, onDismiss }) {
1968
+ if (toasts.length === 0) return null;
1969
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: containerStyle, children: toasts.map((t) => {
1970
+ const c = TOAST_COLORS[t.type];
1971
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1972
+ "div",
1973
+ {
1974
+ role: "alert",
1975
+ "aria-live": "polite",
1976
+ style: {
1977
+ background: c.bg,
1978
+ border: `1px solid ${c.border}`,
1979
+ color: c.text,
1980
+ padding: "8px 14px",
1981
+ borderRadius: 8,
1982
+ fontSize: 12,
1983
+ fontWeight: 500,
1984
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
1985
+ boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
1986
+ pointerEvents: "auto",
1987
+ cursor: "pointer",
1988
+ maxWidth: 280
1989
+ },
1990
+ onClick: () => onDismiss(t.id),
1991
+ children: [
1992
+ t.type === "success" && "\u2713 ",
1993
+ t.type === "error" && "\u2717 ",
1994
+ t.message
1995
+ ]
1996
+ },
1997
+ t.id
1998
+ );
1999
+ }) });
1632
2000
  }
1633
2001
 
1634
2002
  // src/ui/presets.ts
@@ -1727,10 +2095,33 @@ function cloneModel(m) {
1727
2095
  };
1728
2096
  }
1729
2097
 
2098
+ // src/ui/hooks/useEditorKeyboard.ts
2099
+ var import_react10 = require("react");
2100
+ var isInput = (e) => {
2101
+ const tgt = e.target;
2102
+ return !!(tgt && (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA" || tgt.isContentEditable));
2103
+ };
2104
+ function useEditorKeyboard(commands, deps) {
2105
+ (0, import_react10.useEffect)(() => {
2106
+ const onKey = (e) => {
2107
+ if (isInput(e)) return;
2108
+ for (const cmd of commands) {
2109
+ if (cmd.match(e)) {
2110
+ const handled = cmd.run(e);
2111
+ if (handled) e.preventDefault();
2112
+ return;
2113
+ }
2114
+ }
2115
+ };
2116
+ window.addEventListener("keydown", onKey);
2117
+ return () => window.removeEventListener("keydown", onKey);
2118
+ }, deps);
2119
+ }
2120
+
1730
2121
  // src/ui/SequenceEditor.tsx
1731
- var import_jsx_runtime4 = require("react/jsx-runtime");
1732
- var INDIGO = "#4f46e5";
1733
- var INDIGO_SOFT = "#eef2ff";
2122
+ var import_jsx_runtime6 = require("react/jsx-runtime");
2123
+ var INDIGO2 = "#4f46e5";
2124
+ var INDIGO_SOFT2 = "#eef2ff";
1734
2125
  var lightTheme2 = {
1735
2126
  canvas: "#fafbfc",
1736
2127
  dot: "#dbe3ee",
@@ -1773,11 +2164,11 @@ var darkTheme2 = {
1773
2164
  actorStroke: "rgba(99,102,241,0.45)",
1774
2165
  actorText: "#a5b4fc"
1775
2166
  };
1776
- var HEADER_H = 64;
1777
- var HEADER_PAD = 24;
2167
+ var HEADER_H2 = 64;
2168
+ var HEADER_PAD2 = 24;
1778
2169
  var COL_MIN = 160;
1779
- var ROW_H = 64;
1780
- var SIDE_PAD = 40;
2170
+ var ROW_H2 = 64;
2171
+ var SIDE_PAD2 = 40;
1781
2172
  var DRAG_THRESHOLD = 5;
1782
2173
  function ensureSequenceModel(m) {
1783
2174
  if (m && m.type === "sequence") {
@@ -1795,49 +2186,50 @@ function SequenceEditor({
1795
2186
  theme = "auto",
1796
2187
  themeOverrides
1797
2188
  }) {
1798
- const [model, setModel] = (0, import_react8.useState)(() => ensureSequenceModel(initialModel));
1799
- const [selected, setSelected] = (0, import_react8.useState)(null);
1800
- const [drag, setDrag] = (0, import_react8.useState)(null);
1801
- const [editingId, setEditingId] = (0, import_react8.useState)(null);
1802
- const [editLabel, setEditLabel] = (0, import_react8.useState)("");
1803
- const historyRef = (0, import_react8.useRef)([ensureSequenceModel(initialModel)]);
1804
- const historyIdxRef = (0, import_react8.useRef)(0);
1805
- const svgRef = (0, import_react8.useRef)(null);
2189
+ const [model, setModel] = (0, import_react11.useState)(() => ensureSequenceModel(initialModel));
2190
+ const { toasts, showToast, dismissToast } = useToast();
2191
+ const [selected, setSelected] = (0, import_react11.useState)(null);
2192
+ const [drag, setDrag] = (0, import_react11.useState)(null);
2193
+ const [editingId, setEditingId] = (0, import_react11.useState)(null);
2194
+ const [editLabel, setEditLabel] = (0, import_react11.useState)("");
2195
+ const historyRef = (0, import_react11.useRef)([ensureSequenceModel(initialModel)]);
2196
+ const historyIdxRef = (0, import_react11.useRef)(0);
2197
+ const svgRef = (0, import_react11.useRef)(null);
1806
2198
  const { t, isDark } = useEditorTheme(theme, themeOverrides, { light: lightTheme2, dark: darkTheme2 });
1807
2199
  const actors = model.actors ?? [];
1808
2200
  const messages = model.messages ?? [];
1809
- const colW = (0, import_react8.useMemo)(() => {
2201
+ const colW = (0, import_react11.useMemo)(() => {
1810
2202
  const longest = actors.reduce((m, a) => Math.max(m, a.length), 6);
1811
2203
  return Math.max(COL_MIN, longest * 9 + 40);
1812
2204
  }, [actors]);
1813
- const totalW = SIDE_PAD * 2 + Math.max(1, actors.length) * colW;
1814
- const totalH = HEADER_PAD + HEADER_H + 32 + messages.length * ROW_H + 48;
2205
+ const totalW = SIDE_PAD2 * 2 + Math.max(1, actors.length) * colW;
2206
+ const totalH = HEADER_PAD2 + HEADER_H2 + 32 + messages.length * ROW_H2 + 48;
1815
2207
  const actorX = (name) => {
1816
2208
  const idx = actors.indexOf(name);
1817
- if (idx < 0) return SIDE_PAD + colW / 2;
1818
- return SIDE_PAD + idx * colW + colW / 2;
2209
+ if (idx < 0) return SIDE_PAD2 + colW / 2;
2210
+ return SIDE_PAD2 + idx * colW + colW / 2;
1819
2211
  };
1820
- const msgY = (idx) => HEADER_PAD + HEADER_H + 40 + idx * ROW_H;
1821
- const pushHistory = (0, import_react8.useCallback)((m) => {
2212
+ const msgY = (idx) => HEADER_PAD2 + HEADER_H2 + 40 + idx * ROW_H2;
2213
+ const pushHistory = (0, import_react11.useCallback)((m) => {
1822
2214
  const stack = historyRef.current.slice(0, historyIdxRef.current + 1);
1823
2215
  stack.push(m);
1824
2216
  if (stack.length > 80) stack.shift();
1825
2217
  historyRef.current = stack;
1826
2218
  historyIdxRef.current = stack.length - 1;
1827
2219
  }, []);
1828
- const applyAndPush = (0, import_react8.useCallback)((m) => {
2220
+ const applyAndPush = (0, import_react11.useCallback)((m) => {
1829
2221
  setModel(m);
1830
2222
  onChange?.(m);
1831
2223
  pushHistory(m);
1832
2224
  }, [onChange, pushHistory]);
1833
- const undo = (0, import_react8.useCallback)(() => {
2225
+ const undo = (0, import_react11.useCallback)(() => {
1834
2226
  if (historyIdxRef.current <= 0) return;
1835
2227
  historyIdxRef.current--;
1836
2228
  const m = historyRef.current[historyIdxRef.current];
1837
2229
  setModel(m);
1838
2230
  onChange?.(m);
1839
2231
  }, [onChange]);
1840
- const redo = (0, import_react8.useCallback)(() => {
2232
+ const redo = (0, import_react11.useCallback)(() => {
1841
2233
  if (historyIdxRef.current >= historyRef.current.length - 1) return;
1842
2234
  historyIdxRef.current++;
1843
2235
  const m = historyRef.current[historyIdxRef.current];
@@ -1895,7 +2287,7 @@ function SequenceEditor({
1895
2287
  applyAndPush({ ...model, messages: messages.filter((m) => m.id !== id) });
1896
2288
  if (selected === id) setSelected(null);
1897
2289
  };
1898
- const reorderMessage = (0, import_react8.useCallback)((id, toIdx) => {
2290
+ const reorderMessage = (0, import_react11.useCallback)((id, toIdx) => {
1899
2291
  const fromIdx = messages.findIndex((m) => m.id === id);
1900
2292
  if (fromIdx < 0 || toIdx === fromIdx) return;
1901
2293
  const next = messages.slice();
@@ -1903,38 +2295,32 @@ function SequenceEditor({
1903
2295
  next.splice(toIdx, 0, moved);
1904
2296
  applyAndPush({ ...model, messages: next });
1905
2297
  }, [messages, model, applyAndPush]);
1906
- (0, import_react8.useEffect)(() => {
1907
- const onKey = (e) => {
1908
- const tgt = e.target;
1909
- if (tgt && (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA" || tgt.isContentEditable)) return;
1910
- const ctrl = e.ctrlKey || e.metaKey;
1911
- if (ctrl && e.key === "z") {
1912
- e.preventDefault();
1913
- undo();
1914
- return;
1915
- }
1916
- if (ctrl && (e.key === "y" || e.shiftKey && e.key === "z")) {
1917
- e.preventDefault();
1918
- redo();
1919
- return;
1920
- }
1921
- if (e.key === "Escape") {
1922
- setSelected(null);
1923
- setEditingId(null);
1924
- return;
1925
- }
1926
- if ((e.key === "Delete" || e.key === "Backspace") && selected) {
1927
- e.preventDefault();
1928
- removeMessage(selected);
1929
- }
1930
- };
1931
- window.addEventListener("keydown", onKey);
1932
- return () => window.removeEventListener("keydown", onKey);
1933
- }, [undo, redo, selected]);
1934
- const handleExport = useExporters(model, onExport, "sequence");
2298
+ const keyCommands = [
2299
+ { match: (e) => (e.ctrlKey || e.metaKey) && e.key === "z", run: () => {
2300
+ undo();
2301
+ return true;
2302
+ } },
2303
+ { match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "y" || e.shiftKey && e.key === "z"), run: () => {
2304
+ redo();
2305
+ return true;
2306
+ } },
2307
+ { match: (e) => e.key === "Escape", run: () => {
2308
+ setSelected(null);
2309
+ setEditingId(null);
2310
+ return true;
2311
+ } },
2312
+ { match: (e) => (e.key === "Delete" || e.key === "Backspace") && !!selected, run: () => {
2313
+ removeMessage(selected);
2314
+ return true;
2315
+ } }
2316
+ ];
2317
+ useEditorKeyboard(keyCommands, [undo, redo, selected]);
2318
+ const handleExport = useExporters(model, onExport, "sequence", (msg) => showToast(msg, "success"));
1935
2319
  const handleImport = useImporter(applyAndPush, {
1936
2320
  expectedType: "sequence",
1937
- transform: ensureSequenceModel
2321
+ transform: ensureSequenceModel,
2322
+ onSuccess: (msg) => showToast(msg, "success"),
2323
+ onError: (msg) => showToast(msg, "error")
1938
2324
  });
1939
2325
  const onRowMouseDown = (e, id) => {
1940
2326
  const tag = e.target.tagName;
@@ -1945,9 +2331,9 @@ function SequenceEditor({
1945
2331
  setSelected(id);
1946
2332
  setDrag({ id, startY: e.clientY, originalIdx: idx, targetIdx: idx, active: false });
1947
2333
  };
1948
- (0, import_react8.useEffect)(() => {
2334
+ (0, import_react11.useEffect)(() => {
1949
2335
  if (!drag) return;
1950
- const baseY = HEADER_PAD + HEADER_H + 40;
2336
+ const baseY = HEADER_PAD2 + HEADER_H2 + 40;
1951
2337
  const onMove = (ev) => {
1952
2338
  const dy = ev.clientY - drag.startY;
1953
2339
  if (!drag.active && Math.abs(dy) < DRAG_THRESHOLD) return;
@@ -1955,7 +2341,7 @@ function SequenceEditor({
1955
2341
  if (!svg) return;
1956
2342
  const rect = svg.getBoundingClientRect();
1957
2343
  const yInSvg = ev.clientY - rect.top;
1958
- const raw = Math.floor((yInSvg - baseY + ROW_H / 2) / ROW_H);
2344
+ const raw = Math.floor((yInSvg - baseY + ROW_H2 / 2) / ROW_H2);
1959
2345
  const next = Math.max(0, Math.min(messages.length - 1, raw));
1960
2346
  if (next === drag.targetIdx && drag.active) return;
1961
2347
  setDrag({ ...drag, active: true, targetIdx: next });
@@ -1973,26 +2359,31 @@ function SequenceEditor({
1973
2359
  window.removeEventListener("mouseup", onUp);
1974
2360
  };
1975
2361
  }, [drag, messages.length, reorderMessage]);
1976
- const visualMessages = (0, import_react8.useMemo)(() => {
1977
- if (!drag?.active) return messages;
1978
- const idx = messages.findIndex((m) => m.id === drag.id);
1979
- if (idx < 0) return messages;
1980
- const next = messages.slice();
1981
- const [moved] = next.splice(idx, 1);
1982
- next.splice(drag.targetIdx, 0, moved);
1983
- return next;
1984
- }, [messages, drag]);
1985
2362
  const selectedMsg = selected ? messages.find((m) => m.id === selected) : null;
1986
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
2363
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "fsd-seq-editor", style: {
1987
2364
  display: "flex",
1988
2365
  flexDirection: "column",
1989
2366
  height,
1990
2367
  width: "100%",
1991
2368
  fontFamily: "ui-sans-serif,system-ui,sans-serif",
1992
- background: t.ctrlsBg
2369
+ background: t.ctrlsBg,
2370
+ position: "relative"
1993
2371
  }, children: [
1994
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
1995
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
2372
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ToastContainer, { toasts, onDismiss: dismissToast }),
2373
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("style", { children: `
2374
+ .fsd-seq-editor [role="button"]:focus-visible {
2375
+ outline: 2px solid ${t.actorText};
2376
+ outline-offset: 2px;
2377
+ }
2378
+ .fsd-seq-editor button:focus-visible,
2379
+ .fsd-seq-editor input:focus-visible {
2380
+ outline: 2px solid ${t.actorText};
2381
+ outline-offset: 2px;
2382
+ border-radius: 4px;
2383
+ }
2384
+ ` }),
2385
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
2386
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: {
1996
2387
  display: "flex",
1997
2388
  gap: 8,
1998
2389
  padding: "7px 14px",
@@ -2001,12 +2392,12 @@ function SequenceEditor({
2001
2392
  alignItems: "center",
2002
2393
  flexWrap: "wrap"
2003
2394
  }, children: [
2004
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: addActor, style: primaryBtn(), children: "+ Actor" }),
2005
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: addMessage, style: primaryBtn(), children: "+ Message" }),
2006
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { width: 1, height: 18, background: t.ctrlsBorder, margin: "0 4px" } }),
2007
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: undo, style: ghostBtn2(t), title: "Undo (Ctrl+Z)", children: "\u21B6" }),
2008
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: redo, style: ghostBtn2(t), title: "Redo (Ctrl+Y)", children: "\u21B7" }),
2009
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
2395
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { onClick: addActor, style: primaryBtn(), children: "+ Actor" }),
2396
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { onClick: addMessage, style: primaryBtn(), children: "+ Message" }),
2397
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { width: 1, height: 18, background: t.ctrlsBorder, margin: "0 4px" } }),
2398
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { onClick: undo, style: ghostBtn2(t), title: "Undo (Ctrl+Z)", children: "\u21B6" }),
2399
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { onClick: redo, style: ghostBtn2(t), title: "Redo (Ctrl+Y)", children: "\u21B7" }),
2400
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
2010
2401
  actors.length,
2011
2402
  " actor",
2012
2403
  actors.length === 1 ? "" : "s",
@@ -2017,225 +2408,42 @@ function SequenceEditor({
2017
2408
  " \xB7 drag a row to reorder"
2018
2409
  ] })
2019
2410
  ] }),
2020
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [
2021
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { flex: 1, overflow: "auto", background: t.canvas, position: "relative" }, children: actors.length === 0 && messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
2022
- position: "absolute",
2023
- inset: 0,
2024
- display: "flex",
2025
- flexDirection: "column",
2026
- alignItems: "center",
2027
- justifyContent: "center",
2028
- gap: 10,
2029
- color: t.textMuted,
2030
- pointerEvents: "none"
2031
- }, children: [
2032
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 36, opacity: 0.15, color: t.textPrimary }, children: "\u2194" }),
2033
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { fontSize: 13, fontWeight: 500 }, children: [
2034
- "Click ",
2035
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { style: { color: INDIGO }, children: "+ Actor" }),
2036
- " then ",
2037
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { style: { color: INDIGO }, children: "+ Message" }),
2038
- " to start"
2039
- ] })
2040
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2041
- "svg",
2411
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [
2412
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { flex: 1, overflow: "auto", background: t.canvas, position: "relative" }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2413
+ SequenceCanvas,
2042
2414
  {
2043
- ref: svgRef,
2044
- width: totalW,
2045
- height: totalH,
2046
- style: { display: "block", cursor: drag?.active ? "grabbing" : "default", userSelect: "none" },
2047
- children: [
2048
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("defs", { children: [
2049
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pattern", { id: "seqdots", x: "0", y: "0", width: "24", height: "24", patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { cx: 12, cy: 12, r: 1.1, fill: t.dot }) }),
2050
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("filter", { id: "seqShadow", x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("feDropShadow", { dx: 0, dy: 3, stdDeviation: 5, floodColor: isDark ? "rgba(0,0,0,0.5)" : "rgba(15,23,42,0.09)" }) }),
2051
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("marker", { id: "seqArrow", markerWidth: 9, markerHeight: 7, refX: 8.5, refY: 3.5, orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: t.arrow }) })
2052
- ] }),
2053
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { width: totalW, height: totalH, fill: "url(#seqdots)" }),
2054
- actors.map((name) => {
2055
- const x = actorX(name);
2056
- const top = HEADER_PAD + HEADER_H;
2057
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2058
- "line",
2059
- {
2060
- x1: x,
2061
- x2: x,
2062
- y1: top + 4,
2063
- y2: totalH - 24,
2064
- stroke: t.lifeline,
2065
- strokeWidth: 1.25,
2066
- strokeDasharray: "5 5"
2067
- },
2068
- `life-${name}`
2069
- );
2070
- }),
2071
- visualMessages.map((msg, idx) => {
2072
- const y = msgY(idx);
2073
- const fromX = actorX(msg.from);
2074
- const toX = actorX(msg.to);
2075
- const selectedHere = selected === msg.id;
2076
- const isDragging = drag?.active && drag.id === msg.id;
2077
- const isSelf = msg.from === msg.to;
2078
- const stroke = selectedHere ? INDIGO : t.arrow;
2079
- const dash = msg.style === "dashed" ? "6,4" : void 0;
2080
- const cursor = drag?.active ? "grabbing" : "grab";
2081
- const groupOpacity = isDragging ? 0.85 : 1;
2082
- if (isSelf) {
2083
- const startX = fromX;
2084
- const loopW = 36;
2085
- const loopY = y - 6;
2086
- const d = `M ${startX} ${loopY} C ${startX + loopW} ${loopY}, ${startX + loopW} ${loopY + 24}, ${startX} ${loopY + 24}`;
2087
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { onMouseDown: (e) => onRowMouseDown(e, msg.id), style: { cursor, opacity: groupOpacity }, children: [
2088
- (selectedHere || isDragging) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2089
- "rect",
2090
- {
2091
- x: SIDE_PAD - 8,
2092
- y: y - 22,
2093
- width: totalW - (SIDE_PAD - 8) * 2,
2094
- height: ROW_H - 12,
2095
- rx: 10,
2096
- fill: INDIGO_SOFT,
2097
- opacity: isDark ? 0.18 : 0.6
2098
- }
2099
- ),
2100
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d, fill: "none", stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
2101
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("text", { x: startX + loopW + 8, y: loopY + 16, fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
2102
- ] }, msg.id);
2103
- }
2104
- const labelX = (fromX + toX) / 2;
2105
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { onMouseDown: (e) => onRowMouseDown(e, msg.id), style: { cursor, opacity: groupOpacity }, children: [
2106
- (selectedHere || isDragging) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2107
- "rect",
2108
- {
2109
- x: SIDE_PAD - 8,
2110
- y: y - 22,
2111
- width: totalW - (SIDE_PAD - 8) * 2,
2112
- height: ROW_H - 12,
2113
- rx: 10,
2114
- fill: INDIGO_SOFT,
2115
- opacity: isDark ? 0.18 : 0.6
2116
- }
2117
- ),
2118
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: fromX, y1: y, x2: toX, y2: y, stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
2119
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2120
- "rect",
2121
- {
2122
- x: labelX - estimateW(msg.label) / 2 - 6,
2123
- y: y - 18,
2124
- width: estimateW(msg.label) + 12,
2125
- height: 18,
2126
- rx: 6,
2127
- fill: t.canvas,
2128
- stroke: selectedHere ? INDIGO : t.cardBorder,
2129
- strokeWidth: selectedHere ? 1.25 : 1
2130
- }
2131
- ),
2132
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("text", { x: labelX, y: y - 5, textAnchor: "middle", fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
2133
- ] }, msg.id);
2134
- }),
2135
- actors.map((name) => {
2136
- const x = actorX(name);
2137
- const w = colW - 24;
2138
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { children: [
2139
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2140
- "rect",
2141
- {
2142
- x: x - w / 2,
2143
- y: HEADER_PAD,
2144
- width: w,
2145
- height: HEADER_H,
2146
- rx: 12,
2147
- fill: t.actorFill,
2148
- stroke: t.actorStroke,
2149
- strokeWidth: 1.25,
2150
- filter: "url(#seqShadow)"
2151
- }
2152
- ),
2153
- editingId === name ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("foreignObject", { x: x - w / 2 + 8, y: HEADER_PAD + 16, width: w - 16, height: 32, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2154
- "input",
2155
- {
2156
- autoFocus: true,
2157
- defaultValue: name,
2158
- onBlur: (e) => {
2159
- renameActor(name, e.currentTarget.value.trim());
2160
- setEditingId(null);
2161
- },
2162
- onKeyDown: (e) => {
2163
- if (e.key === "Enter") {
2164
- renameActor(name, e.target.value.trim());
2165
- setEditingId(null);
2166
- }
2167
- if (e.key === "Escape") setEditingId(null);
2168
- },
2169
- style: {
2170
- width: "100%",
2171
- height: "100%",
2172
- border: "none",
2173
- borderRadius: 6,
2174
- outline: `2px solid ${INDIGO}`,
2175
- textAlign: "center",
2176
- fontSize: 13,
2177
- fontWeight: 600,
2178
- background: t.inputBg,
2179
- color: t.inputText,
2180
- boxSizing: "border-box",
2181
- padding: "0 6px",
2182
- fontFamily: "inherit"
2183
- }
2184
- }
2185
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2186
- "text",
2187
- {
2188
- x,
2189
- y: HEADER_PAD + HEADER_H / 2 + 4,
2190
- textAnchor: "middle",
2191
- fontSize: 13,
2192
- fontWeight: 700,
2193
- fill: t.actorText,
2194
- style: { cursor: "pointer", userSelect: "none" },
2195
- onDoubleClick: () => setEditingId(name),
2196
- children: name
2197
- }
2198
- ),
2199
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2200
- "circle",
2201
- {
2202
- cx: x + w / 2 - 12,
2203
- cy: HEADER_PAD + 14,
2204
- r: 9,
2205
- fill: "transparent",
2206
- style: { cursor: "pointer" },
2207
- onClick: () => removeActor(name),
2208
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("title", { children: "Remove actor" })
2209
- }
2210
- ),
2211
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2212
- "text",
2213
- {
2214
- x: x + w / 2 - 12,
2215
- y: HEADER_PAD + 18,
2216
- textAnchor: "middle",
2217
- fontSize: 12,
2218
- fill: t.textMuted,
2219
- style: { pointerEvents: "none", userSelect: "none" },
2220
- children: "\xD7"
2221
- }
2222
- )
2223
- ] }, `hdr-${name}`);
2224
- })
2225
- ]
2415
+ model,
2416
+ actors,
2417
+ messages,
2418
+ t,
2419
+ isDark,
2420
+ colW,
2421
+ totalW,
2422
+ totalH,
2423
+ actorX,
2424
+ msgY,
2425
+ selected,
2426
+ editingId,
2427
+ setEditingId,
2428
+ drag,
2429
+ onRowMouseDown,
2430
+ renameActor,
2431
+ removeActor,
2432
+ svgRef
2226
2433
  }
2227
2434
  ) }),
2228
- selectedMsg && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
2435
+ selectedMsg && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: {
2229
2436
  width: 280,
2437
+ maxWidth: "40vw",
2230
2438
  flexShrink: 0,
2231
2439
  background: t.panelBg,
2232
2440
  borderLeft: `1px solid ${t.panelBorder}`,
2233
2441
  padding: "14px 16px",
2234
2442
  overflowY: "auto"
2235
2443
  }, children: [
2236
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.7, marginBottom: 10 }, children: "Message" }),
2237
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Label, { t, children: "Label" }),
2238
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2444
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.7, marginBottom: 10 }, children: "Message" }),
2445
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Label, { t, children: "Label" }),
2446
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2239
2447
  "input",
2240
2448
  {
2241
2449
  value: editLabel || selectedMsg.label,
@@ -2251,21 +2459,21 @@ function SequenceEditor({
2251
2459
  style: input(t)
2252
2460
  }
2253
2461
  ),
2254
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Label, { t, children: "From" }),
2255
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { value: selectedMsg.from, onChange: (e) => updateMessage(selectedMsg.id, { from: e.target.value }), style: input(t), children: actors.map((a) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: a, children: a }, a)) }),
2256
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Label, { t, children: "To" }),
2257
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { value: selectedMsg.to, onChange: (e) => updateMessage(selectedMsg.id, { to: e.target.value }), style: input(t), children: actors.map((a) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: a, children: a }, a)) }),
2258
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Label, { t, children: "Style" }),
2259
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { display: "flex", gap: 6 }, children: ["solid", "dashed"].map((s2) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2462
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Label, { t, children: "From" }),
2463
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("select", { value: selectedMsg.from, onChange: (e) => updateMessage(selectedMsg.id, { from: e.target.value }), style: input(t), children: actors.map((a) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: a, children: a }, a)) }),
2464
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Label, { t, children: "To" }),
2465
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("select", { value: selectedMsg.to, onChange: (e) => updateMessage(selectedMsg.id, { to: e.target.value }), style: input(t), children: actors.map((a) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: a, children: a }, a)) }),
2466
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Label, { t, children: "Style" }),
2467
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { display: "flex", gap: 6 }, children: ["solid", "dashed"].map((s2) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2260
2468
  "button",
2261
2469
  {
2262
2470
  onClick: () => updateMessage(selectedMsg.id, { style: s2 }),
2263
2471
  style: {
2264
2472
  flex: 1,
2265
2473
  padding: "6px 10px",
2266
- border: `1.5px solid ${selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO : t.inputBorder}`,
2267
- background: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO_SOFT : t.inputBg,
2268
- color: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO : t.textPrimary,
2474
+ border: `1.5px solid ${selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO2 : t.inputBorder}`,
2475
+ background: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO_SOFT2 : t.inputBg,
2476
+ color: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO2 : t.textPrimary,
2269
2477
  borderRadius: 8,
2270
2478
  fontSize: 12,
2271
2479
  fontWeight: 600,
@@ -2276,8 +2484,8 @@ function SequenceEditor({
2276
2484
  },
2277
2485
  s2
2278
2486
  )) }),
2279
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { height: 14 } }),
2280
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2487
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { height: 14 } }),
2488
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2281
2489
  "button",
2282
2490
  {
2283
2491
  onClick: () => removeMessage(selectedMsg.id),
@@ -2287,7 +2495,7 @@ function SequenceEditor({
2287
2495
  )
2288
2496
  ] })
2289
2497
  ] }),
2290
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
2498
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: {
2291
2499
  padding: "4px 14px",
2292
2500
  fontSize: 11,
2293
2501
  color: t.textMuted,
@@ -2296,25 +2504,22 @@ function SequenceEditor({
2296
2504
  display: "flex",
2297
2505
  gap: 16
2298
2506
  }, children: [
2299
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
2507
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
2300
2508
  actors.length,
2301
2509
  " actors"
2302
2510
  ] }),
2303
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
2511
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
2304
2512
  messages.length,
2305
2513
  " messages"
2306
2514
  ] }),
2307
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { marginLeft: "auto" }, children: "double-click actor to rename \xB7 drag a row to reorder" })
2515
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { marginLeft: "auto" }, children: "double-click actor to rename \xB7 drag a row to reorder" })
2308
2516
  ] })
2309
2517
  ] });
2310
2518
  }
2311
- function estimateW(text, pxPerChar = 7) {
2312
- return text.length * pxPerChar;
2313
- }
2314
2519
  function primaryBtn() {
2315
2520
  return {
2316
2521
  padding: "6px 12px",
2317
- background: INDIGO,
2522
+ background: INDIGO2,
2318
2523
  color: "#fff",
2319
2524
  border: "none",
2320
2525
  borderRadius: 8,
@@ -2353,167 +2558,24 @@ function input(t) {
2353
2558
  };
2354
2559
  }
2355
2560
  function Label({ t, children }) {
2356
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 4 }, children });
2561
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 4 }, children });
2357
2562
  }
2358
2563
 
2359
- // src/ui/Minimap.tsx
2360
- var import_react9 = require("react");
2361
- var import_jsx_runtime5 = require("react/jsx-runtime");
2362
- var W = 168;
2363
- var H = 112;
2364
- var PAD = 18;
2365
- function Minimap({
2564
+ // src/ui/NodeNavigator.tsx
2565
+ var import_react12 = require("react");
2566
+ var import_jsx_runtime7 = require("react/jsx-runtime");
2567
+ function NodeNavigator({
2366
2568
  model,
2367
- viewportW,
2368
- viewportH,
2369
- transform,
2370
- measureNode,
2371
- onCenterOn,
2569
+ selected,
2570
+ variant,
2372
2571
  isDark,
2373
- accentColor
2572
+ t,
2573
+ acc,
2574
+ open,
2575
+ onToggle,
2576
+ onSelect
2374
2577
  }) {
2375
- const dragRef = (0, import_react9.useRef)(null);
2376
- const boxes = model.nodes.map((n) => {
2377
- const { w, h } = measureNode(n);
2378
- return { id: n.id, x: n.x ?? 0, y: n.y ?? 0, w, h };
2379
- });
2380
- if (boxes.length === 0) return null;
2381
- const vx = -transform.x / transform.scale;
2382
- const vy = -transform.y / transform.scale;
2383
- const vw = viewportW / transform.scale;
2384
- const vh = viewportH / transform.scale;
2385
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
2386
- for (const b of boxes) {
2387
- minX = Math.min(minX, b.x);
2388
- minY = Math.min(minY, b.y);
2389
- maxX = Math.max(maxX, b.x + b.w);
2390
- maxY = Math.max(maxY, b.y + b.h);
2391
- }
2392
- minX = Math.min(minX, vx);
2393
- minY = Math.min(minY, vy);
2394
- maxX = Math.max(maxX, vx + vw);
2395
- maxY = Math.max(maxY, vy + vh);
2396
- const contentW = Math.max(1, maxX - minX);
2397
- const contentH = Math.max(1, maxY - minY);
2398
- const scale = Math.min((W - PAD * 2) / contentW, (H - PAD * 2) / contentH);
2399
- const offsetX = (W - contentW * scale) / 2 - minX * scale;
2400
- const offsetY = (H - contentH * scale) / 2 - minY * scale;
2401
- const project = (x, y) => ({
2402
- x: offsetX + x * scale,
2403
- y: offsetY + y * scale
2404
- });
2405
- const unproject = (mx, my) => ({
2406
- x: (mx - offsetX) / scale,
2407
- y: (my - offsetY) / scale
2408
- });
2409
- const panTo = (0, import_react9.useCallback)((e) => {
2410
- const rect = e.currentTarget.getBoundingClientRect();
2411
- const mx = e.clientX - rect.left;
2412
- const my = e.clientY - rect.top;
2413
- const { x, y } = unproject(mx, my);
2414
- onCenterOn(x, y);
2415
- }, [onCenterOn, scale, offsetX, offsetY]);
2416
- const onMouseDown = (e) => {
2417
- e.stopPropagation();
2418
- dragRef.current = { active: true };
2419
- panTo(e);
2420
- };
2421
- const onMouseMove = (e) => {
2422
- if (!dragRef.current?.active) return;
2423
- panTo(e);
2424
- };
2425
- const onMouseUp = () => {
2426
- dragRef.current = null;
2427
- };
2428
- const bg = isDark ? "rgba(15,23,42,0.92)" : "rgba(255,255,255,0.94)";
2429
- const border = isDark ? "#334155" : "#e2e8f0";
2430
- const nodeFill = isDark ? "#475569" : "#cbd5e1";
2431
- const viewStroke = accentColor;
2432
- const viewFill = `${accentColor}22`;
2433
- const vp1 = project(vx, vy);
2434
- const vp2 = project(vx + vw, vy + vh);
2435
- const vpRect = {
2436
- x: Math.max(0, Math.min(W, vp1.x)),
2437
- y: Math.max(0, Math.min(H, vp1.y)),
2438
- w: Math.max(2, Math.min(W, vp2.x) - Math.max(0, vp1.x)),
2439
- h: Math.max(2, Math.min(H, vp2.y) - Math.max(0, vp1.y))
2440
- };
2441
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2442
- "div",
2443
- {
2444
- style: {
2445
- position: "absolute",
2446
- bottom: 14,
2447
- right: 14,
2448
- background: bg,
2449
- border: `1px solid ${border}`,
2450
- borderRadius: 10,
2451
- padding: 6,
2452
- boxShadow: isDark ? "0 8px 20px rgba(0,0,0,0.45)" : "0 6px 18px rgba(15,23,42,0.08)",
2453
- backdropFilter: "blur(6px)"
2454
- },
2455
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2456
- "svg",
2457
- {
2458
- width: W,
2459
- height: H,
2460
- style: { display: "block", cursor: "grab", borderRadius: 6 },
2461
- onMouseDown,
2462
- onMouseMove,
2463
- onMouseUp,
2464
- onMouseLeave: onMouseUp,
2465
- children: [
2466
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { width: W, height: H, rx: 6, fill: isDark ? "#0f172a" : "#fafbfc" }),
2467
- boxes.map((b) => {
2468
- const p = project(b.x, b.y);
2469
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2470
- "rect",
2471
- {
2472
- x: p.x,
2473
- y: p.y,
2474
- width: Math.max(2, b.w * scale),
2475
- height: Math.max(2, b.h * scale),
2476
- rx: 2,
2477
- fill: nodeFill
2478
- },
2479
- b.id
2480
- );
2481
- }),
2482
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2483
- "rect",
2484
- {
2485
- x: vpRect.x,
2486
- y: vpRect.y,
2487
- width: vpRect.w,
2488
- height: vpRect.h,
2489
- rx: 3,
2490
- fill: viewFill,
2491
- stroke: viewStroke,
2492
- strokeWidth: 1.25
2493
- }
2494
- )
2495
- ]
2496
- }
2497
- )
2498
- }
2499
- );
2500
- }
2501
-
2502
- // src/ui/NodeNavigator.tsx
2503
- var import_react10 = require("react");
2504
- var import_jsx_runtime6 = require("react/jsx-runtime");
2505
- function NodeNavigator({
2506
- model,
2507
- selected,
2508
- variant,
2509
- isDark,
2510
- t,
2511
- acc,
2512
- open,
2513
- onToggle,
2514
- onSelect
2515
- }) {
2516
- const [search, setSearch] = (0, import_react10.useState)("");
2578
+ const [search, setSearch] = (0, import_react12.useState)("");
2517
2579
  const shapeIcon = (node) => {
2518
2580
  if (variant === "question") return "?";
2519
2581
  if (variant === "journey") return "\u2197";
@@ -2534,7 +2596,7 @@ function NodeNavigator({
2534
2596
  const inEdges = (id) => model.edges.filter((e) => e.to === id).length;
2535
2597
  const outEdges = (id) => model.edges.filter((e) => e.from === id).length;
2536
2598
  if (!open) {
2537
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: {
2599
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: {
2538
2600
  width: 36,
2539
2601
  flexShrink: 0,
2540
2602
  background: t.panelBg,
@@ -2545,19 +2607,21 @@ function NodeNavigator({
2545
2607
  paddingTop: 8,
2546
2608
  gap: 6
2547
2609
  }, children: [
2548
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2610
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2549
2611
  "button",
2550
2612
  {
2551
2613
  onClick: onToggle,
2552
2614
  title: "Open node list",
2615
+ "aria-expanded": false,
2616
+ "aria-label": "Open node list",
2553
2617
  style: { background: "none", border: "none", cursor: "pointer", color: t.textMuted, padding: 6, borderRadius: 6, fontSize: 14, lineHeight: 1 },
2554
2618
  children: "\u2630"
2555
2619
  }
2556
2620
  ),
2557
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 10, color: t.textMuted, fontWeight: 700, writingMode: "vertical-rl", transform: "rotate(180deg)", letterSpacing: 0.5 }, children: model.nodes.length })
2621
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontSize: 10, color: t.textMuted, fontWeight: 700, writingMode: "vertical-rl", transform: "rotate(180deg)", letterSpacing: 0.5 }, children: model.nodes.length })
2558
2622
  ] });
2559
2623
  }
2560
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: {
2624
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: {
2561
2625
  width: 216,
2562
2626
  flexShrink: 0,
2563
2627
  background: t.panelBg,
@@ -2566,7 +2630,7 @@ function NodeNavigator({
2566
2630
  flexDirection: "column",
2567
2631
  overflow: "hidden"
2568
2632
  }, children: [
2569
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: {
2633
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: {
2570
2634
  display: "flex",
2571
2635
  alignItems: "center",
2572
2636
  justifyContent: "space-between",
@@ -2574,9 +2638,9 @@ function NodeNavigator({
2574
2638
  borderBottom: `1px solid ${t.panelBorder}`,
2575
2639
  flexShrink: 0
2576
2640
  }, children: [
2577
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
2578
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { fontSize: 11, fontWeight: 700, color: t.textSecondary, textTransform: "uppercase", letterSpacing: 0.7 }, children: variant === "question" ? "Questions" : variant === "journey" ? "Steps" : "Nodes" }),
2579
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: {
2641
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
2642
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontSize: 11, fontWeight: 700, color: t.textSecondary, textTransform: "uppercase", letterSpacing: 0.7 }, children: variant === "question" ? "Questions" : variant === "journey" ? "Steps" : "Nodes" }),
2643
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: {
2580
2644
  fontSize: 10,
2581
2645
  fontWeight: 700,
2582
2646
  color: t.textMuted,
@@ -2585,19 +2649,21 @@ function NodeNavigator({
2585
2649
  borderRadius: 99
2586
2650
  }, children: model.nodes.length })
2587
2651
  ] }),
2588
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2652
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2589
2653
  "button",
2590
2654
  {
2591
2655
  onClick: onToggle,
2592
2656
  style: { background: "none", border: "none", cursor: "pointer", color: t.textMuted, padding: "2px 4px", borderRadius: 4, fontSize: 13, lineHeight: 1 },
2593
2657
  title: "Collapse",
2658
+ "aria-expanded": true,
2659
+ "aria-label": "Collapse node list",
2594
2660
  children: "\u2039"
2595
2661
  }
2596
2662
  )
2597
2663
  ] }),
2598
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { padding: "8px 10px", borderBottom: `1px solid ${t.sectionBorder}`, flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { position: "relative" }, children: [
2599
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { position: "absolute", left: 8, top: "50%", transform: "translateY(-50%)", fontSize: 11, color: t.textMuted, pointerEvents: "none" }, children: "\u2315" }),
2600
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2664
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { padding: "8px 10px", borderBottom: `1px solid ${t.sectionBorder}`, flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { position: "relative" }, children: [
2665
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { position: "absolute", left: 8, top: "50%", transform: "translateY(-50%)", fontSize: 11, color: t.textMuted, pointerEvents: "none" }, children: "\u2315" }),
2666
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2601
2667
  "input",
2602
2668
  {
2603
2669
  value: search,
@@ -2618,12 +2684,12 @@ function NodeNavigator({
2618
2684
  }
2619
2685
  )
2620
2686
  ] }) }),
2621
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { flex: 1, overflowY: "auto", padding: "6px 8px", display: "flex", flexDirection: "column", gap: 2 }, children: [
2622
- filtered.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { textAlign: "center", padding: "20px 0", fontSize: 12, color: t.textMuted, fontStyle: "italic" }, children: model.nodes.length === 0 ? "No nodes yet" : "No matches" }),
2687
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { flex: 1, overflowY: "auto", padding: "6px 8px", display: "flex", flexDirection: "column", gap: 2 }, children: [
2688
+ filtered.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { textAlign: "center", padding: "20px 0", fontSize: 12, color: t.textMuted, fontStyle: "italic" }, children: model.nodes.length === 0 ? "No nodes yet" : "No matches" }),
2623
2689
  filtered.map((node, idx) => {
2624
2690
  const isSelected = selected === node.id;
2625
2691
  const answers = node.metadata?.answers ?? [];
2626
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2692
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2627
2693
  "button",
2628
2694
  {
2629
2695
  onClick: () => onSelect(node.id),
@@ -2648,7 +2714,7 @@ function NodeNavigator({
2648
2714
  if (!isSelected) e.currentTarget.style.background = "transparent";
2649
2715
  },
2650
2716
  children: [
2651
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: {
2717
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: {
2652
2718
  width: 22,
2653
2719
  height: 22,
2654
2720
  borderRadius: 6,
@@ -2661,8 +2727,8 @@ function NodeNavigator({
2661
2727
  fontSize: variant === "journey" ? 9 : 11,
2662
2728
  fontWeight: 700
2663
2729
  }, children: variant === "journey" ? idx + 1 : shapeIcon(node) }),
2664
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [
2665
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: {
2730
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [
2731
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: {
2666
2732
  fontSize: 12,
2667
2733
  fontWeight: isSelected ? 600 : 400,
2668
2734
  color: isSelected ? acc.color : t.textPrimary,
@@ -2671,9 +2737,9 @@ function NodeNavigator({
2671
2737
  whiteSpace: "nowrap",
2672
2738
  lineHeight: 1.3
2673
2739
  }, children: node.label }),
2674
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 10, color: t.textMuted, lineHeight: 1.2, marginTop: 1 }, children: variant === "question" ? `${answers.length} answer${answers.length !== 1 ? "s" : ""}` : `${inEdges(node.id)}\u2193 ${outEdges(node.id)}\u2192` })
2740
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontSize: 10, color: t.textMuted, lineHeight: 1.2, marginTop: 1 }, children: variant === "question" ? `${answers.length} answer${answers.length !== 1 ? "s" : ""}` : `${inEdges(node.id)}\u2193 ${outEdges(node.id)}\u2192` })
2675
2741
  ] }),
2676
- isSelected && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { fontSize: 10, color: acc.color, flexShrink: 0 }, children: "\u25C9" })
2742
+ isSelected && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontSize: 10, color: acc.color, flexShrink: 0 }, children: "\u25C9" })
2677
2743
  ]
2678
2744
  },
2679
2745
  node.id
@@ -2683,139 +2749,8 @@ function NodeNavigator({
2683
2749
  ] });
2684
2750
  }
2685
2751
 
2686
- // src/ui/ContextMenu.tsx
2687
- var import_react11 = require("react");
2688
- var import_jsx_runtime7 = require("react/jsx-runtime");
2689
- function ContextMenu({
2690
- x,
2691
- y,
2692
- nodeId,
2693
- edgeId,
2694
- isDark,
2695
- t,
2696
- acc,
2697
- canUndo,
2698
- canRedo,
2699
- onUndo,
2700
- onRedo,
2701
- onReCenter,
2702
- onAddNode,
2703
- onDuplicate,
2704
- onRename,
2705
- onDelete,
2706
- onDisconnect,
2707
- onEdgeRename,
2708
- onEdgeStyle,
2709
- onEdgeArrowhead,
2710
- onEdgeDelete,
2711
- onEdgeResetRouting,
2712
- currentEdgeStyle,
2713
- currentEdgeArrow,
2714
- edgeHasWaypoint,
2715
- containerRef
2716
- }) {
2717
- const menuRef = (0, import_react11.useRef)(null);
2718
- const [pos, setPos] = (0, import_react11.useState)({ x, y });
2719
- (0, import_react11.useEffect)(() => {
2720
- if (!menuRef.current || !containerRef.current) return;
2721
- const m = menuRef.current.getBoundingClientRect();
2722
- const c = containerRef.current.getBoundingClientRect();
2723
- let nx = x, ny = y;
2724
- if (nx + m.width > c.right - 8) nx = x - m.width;
2725
- if (ny + m.height > c.bottom - 8) ny = y - m.height;
2726
- setPos({ x: nx, y: ny });
2727
- }, [x, y, containerRef]);
2728
- const bg = isDark ? "#1e293b" : "#ffffff";
2729
- const border = isDark ? "#334155" : "#e2e8f0";
2730
- const hoverBg = isDark ? "#334155" : "#f1f5f9";
2731
- const dividerColor = isDark ? "#334155" : "#f1f5f9";
2732
- const text = t.textPrimary;
2733
- const muted = t.textMuted;
2734
- const item = (label, onClick, color, disabled) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2735
- "button",
2736
- {
2737
- onClick: disabled ? void 0 : onClick,
2738
- style: {
2739
- display: "flex",
2740
- alignItems: "center",
2741
- gap: 10,
2742
- width: "100%",
2743
- padding: "7px 14px",
2744
- background: "none",
2745
- border: "none",
2746
- textAlign: "left",
2747
- cursor: disabled ? "default" : "pointer",
2748
- fontSize: 12,
2749
- fontFamily: "ui-sans-serif,system-ui,sans-serif",
2750
- color: disabled ? muted : color ?? text,
2751
- opacity: disabled ? 0.4 : 1,
2752
- borderRadius: 6
2753
- },
2754
- onMouseEnter: (e) => {
2755
- if (!disabled) e.currentTarget.style.background = hoverBg;
2756
- },
2757
- onMouseLeave: (e) => {
2758
- e.currentTarget.style.background = "none";
2759
- },
2760
- children: label
2761
- },
2762
- label
2763
- );
2764
- const divider2 = /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { height: 1, background: dividerColor, margin: "4px 0" } });
2765
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2766
- "div",
2767
- {
2768
- ref: menuRef,
2769
- onMouseDown: (e) => e.stopPropagation(),
2770
- style: {
2771
- position: "fixed",
2772
- left: pos.x,
2773
- top: pos.y,
2774
- zIndex: 9999,
2775
- background: bg,
2776
- border: `1px solid ${border}`,
2777
- borderRadius: 10,
2778
- padding: "5px 0",
2779
- minWidth: 180,
2780
- boxShadow: isDark ? "0 8px 32px rgba(0,0,0,0.5)" : "0 8px 32px rgba(0,0,0,0.12)",
2781
- fontFamily: "ui-sans-serif,system-ui,sans-serif"
2782
- },
2783
- children: edgeId ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
2784
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Edge" }),
2785
- item("Rename label (dbl-click)", () => onEdgeRename?.()),
2786
- divider2,
2787
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { padding: "4px 14px 2px", fontSize: 9, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Style" }),
2788
- item(`Solid${currentEdgeStyle === "solid" || !currentEdgeStyle ? " \u2713" : ""}`, () => onEdgeStyle?.("solid")),
2789
- item(`Dashed${currentEdgeStyle === "dashed" ? " \u2713" : ""}`, () => onEdgeStyle?.("dashed")),
2790
- item(`Dotted${currentEdgeStyle === "dotted" ? " \u2713" : ""}`, () => onEdgeStyle?.("dotted")),
2791
- divider2,
2792
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { padding: "4px 14px 2px", fontSize: 9, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Arrowhead" }),
2793
- item(`Arrow${currentEdgeArrow !== "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("arrow")),
2794
- item(`None${currentEdgeArrow === "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("none")),
2795
- divider2,
2796
- item("Reset routing", () => onEdgeResetRouting?.(), void 0, !edgeHasWaypoint),
2797
- item("Delete edge", () => onEdgeDelete?.(), "#ef4444")
2798
- ] }) : nodeId ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
2799
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Node" }),
2800
- item("Rename (dbl-click)", onRename),
2801
- item("Duplicate", onDuplicate),
2802
- item("Disconnect all edges", onDisconnect),
2803
- divider2,
2804
- item("Delete node", onDelete, "#ef4444")
2805
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
2806
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Canvas" }),
2807
- item("Add node here", onAddNode, acc.color),
2808
- item("Re-center (Ctrl+0)", onReCenter),
2809
- divider2,
2810
- item("Undo (Ctrl+Z)", onUndo, void 0, !canUndo),
2811
- item("Redo (Ctrl+Y)", onRedo, void 0, !canRedo)
2812
- ] })
2813
- }
2814
- );
2815
- }
2816
-
2817
2752
  // src/ui/render.tsx
2818
- var import_react12 = require("react");
2753
+ var import_react13 = require("react");
2819
2754
 
2820
2755
  // src/ui/layout.ts
2821
2756
  var NODE_H2 = 48;
@@ -3149,7 +3084,7 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
3149
3084
  ] });
3150
3085
  }
3151
3086
  function EdgeLine({ edge, nodes, variant, t, isDark, acc, editing, editValue, onEditChange, onEditCommit, onEditCancel, onDoubleClick, onContextMenu, onWaypointDown }) {
3152
- const [hovered, setHovered] = (0, import_react12.useState)(false);
3087
+ const [hovered, setHovered] = (0, import_react13.useState)(false);
3153
3088
  const from = nodes.find((n) => n.id === edge.from);
3154
3089
  const to = nodes.find((n) => n.id === edge.to);
3155
3090
  if (!from || !to) return null;
@@ -3245,80 +3180,676 @@ function EdgeLine({ edge, nodes, variant, t, isDark, acc, editing, editValue, on
3245
3180
  e.preventDefault();
3246
3181
  onEditCommit?.();
3247
3182
  }
3248
- if (e.key === "Escape") {
3249
- e.preventDefault();
3250
- onEditCancel?.();
3183
+ if (e.key === "Escape") {
3184
+ e.preventDefault();
3185
+ onEditCancel?.();
3186
+ }
3187
+ },
3188
+ onMouseDown: (e) => e.stopPropagation(),
3189
+ style: {
3190
+ width: "100%",
3191
+ height: "100%",
3192
+ border: "none",
3193
+ borderRadius: 6,
3194
+ outline: `2px solid ${acc.color}`,
3195
+ textAlign: "center",
3196
+ fontSize: 10,
3197
+ fontWeight: 500,
3198
+ background: t.inputBg,
3199
+ color: t.inputText,
3200
+ boxSizing: "border-box",
3201
+ padding: "0 6px",
3202
+ fontFamily: "inherit"
3203
+ }
3204
+ }
3205
+ ) }) : edge.label && !isAmber ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
3206
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3207
+ "rect",
3208
+ {
3209
+ x: mx - labelW / 2,
3210
+ y: my - 11,
3211
+ width: labelW,
3212
+ height: 19,
3213
+ rx: 5,
3214
+ fill: t.panelBg,
3215
+ stroke: t.cardBorder,
3216
+ strokeWidth: 1,
3217
+ style: STYLE_EDGE_LABEL_HIT
3218
+ }
3219
+ ),
3220
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3221
+ "text",
3222
+ {
3223
+ x: mx,
3224
+ y: my + 4,
3225
+ textAnchor: "middle",
3226
+ fontSize: 10,
3227
+ fill: t.textSecondary,
3228
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
3229
+ fontWeight: "500",
3230
+ style: STYLE_LABEL,
3231
+ children: edge.label
3232
+ }
3233
+ )
3234
+ ] }) : null
3235
+ ]
3236
+ }
3237
+ );
3238
+ }
3239
+
3240
+ // src/ui/Minimap.tsx
3241
+ var import_react14 = require("react");
3242
+ var import_jsx_runtime9 = require("react/jsx-runtime");
3243
+ var W = 168;
3244
+ var H = 112;
3245
+ var PAD = 18;
3246
+ function Minimap({
3247
+ model,
3248
+ viewportW,
3249
+ viewportH,
3250
+ transform,
3251
+ measureNode,
3252
+ onCenterOn,
3253
+ isDark,
3254
+ accentColor
3255
+ }) {
3256
+ const dragRef = (0, import_react14.useRef)(null);
3257
+ const boxes = model.nodes.map((n) => {
3258
+ const { w, h } = measureNode(n);
3259
+ return { id: n.id, x: n.x ?? 0, y: n.y ?? 0, w, h };
3260
+ });
3261
+ if (boxes.length === 0) return null;
3262
+ const vx = -transform.x / transform.scale;
3263
+ const vy = -transform.y / transform.scale;
3264
+ const vw = viewportW / transform.scale;
3265
+ const vh = viewportH / transform.scale;
3266
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3267
+ for (const b of boxes) {
3268
+ minX = Math.min(minX, b.x);
3269
+ minY = Math.min(minY, b.y);
3270
+ maxX = Math.max(maxX, b.x + b.w);
3271
+ maxY = Math.max(maxY, b.y + b.h);
3272
+ }
3273
+ minX = Math.min(minX, vx);
3274
+ minY = Math.min(minY, vy);
3275
+ maxX = Math.max(maxX, vx + vw);
3276
+ maxY = Math.max(maxY, vy + vh);
3277
+ const contentW = Math.max(1, maxX - minX);
3278
+ const contentH = Math.max(1, maxY - minY);
3279
+ const scale = Math.min((W - PAD * 2) / contentW, (H - PAD * 2) / contentH);
3280
+ const offsetX = (W - contentW * scale) / 2 - minX * scale;
3281
+ const offsetY = (H - contentH * scale) / 2 - minY * scale;
3282
+ const project = (x, y) => ({
3283
+ x: offsetX + x * scale,
3284
+ y: offsetY + y * scale
3285
+ });
3286
+ const unproject = (mx, my) => ({
3287
+ x: (mx - offsetX) / scale,
3288
+ y: (my - offsetY) / scale
3289
+ });
3290
+ const panTo = (0, import_react14.useCallback)((e) => {
3291
+ const rect = e.currentTarget.getBoundingClientRect();
3292
+ const mx = e.clientX - rect.left;
3293
+ const my = e.clientY - rect.top;
3294
+ const { x, y } = unproject(mx, my);
3295
+ onCenterOn(x, y);
3296
+ }, [onCenterOn, scale, offsetX, offsetY]);
3297
+ const onMouseDown = (e) => {
3298
+ e.stopPropagation();
3299
+ dragRef.current = { active: true };
3300
+ panTo(e);
3301
+ };
3302
+ const onMouseMove = (e) => {
3303
+ if (!dragRef.current?.active) return;
3304
+ panTo(e);
3305
+ };
3306
+ const onMouseUp = () => {
3307
+ dragRef.current = null;
3308
+ };
3309
+ const bg = isDark ? "rgba(15,23,42,0.92)" : "rgba(255,255,255,0.94)";
3310
+ const border = isDark ? "#334155" : "#e2e8f0";
3311
+ const nodeFill = isDark ? "#475569" : "#cbd5e1";
3312
+ const viewStroke = accentColor;
3313
+ const viewFill = `${accentColor}22`;
3314
+ const vp1 = project(vx, vy);
3315
+ const vp2 = project(vx + vw, vy + vh);
3316
+ const vpRect = {
3317
+ x: Math.max(0, Math.min(W, vp1.x)),
3318
+ y: Math.max(0, Math.min(H, vp1.y)),
3319
+ w: Math.max(2, Math.min(W, vp2.x) - Math.max(0, vp1.x)),
3320
+ h: Math.max(2, Math.min(H, vp2.y) - Math.max(0, vp1.y))
3321
+ };
3322
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3323
+ "div",
3324
+ {
3325
+ "aria-label": "Minimap \u2014 click to re-center the viewport",
3326
+ role: "img",
3327
+ style: {
3328
+ position: "absolute",
3329
+ bottom: 14,
3330
+ right: 14,
3331
+ background: bg,
3332
+ border: `1px solid ${border}`,
3333
+ borderRadius: 10,
3334
+ padding: 6,
3335
+ boxShadow: isDark ? "0 8px 20px rgba(0,0,0,0.45)" : "0 6px 18px rgba(15,23,42,0.08)",
3336
+ backdropFilter: "blur(6px)"
3337
+ },
3338
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
3339
+ "svg",
3340
+ {
3341
+ width: W,
3342
+ height: H,
3343
+ style: { display: "block", cursor: "grab", borderRadius: 6 },
3344
+ onMouseDown,
3345
+ onMouseMove,
3346
+ onMouseUp,
3347
+ onMouseLeave: onMouseUp,
3348
+ children: [
3349
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("rect", { width: W, height: H, rx: 6, fill: isDark ? "#0f172a" : "#fafbfc" }),
3350
+ boxes.map((b) => {
3351
+ const p = project(b.x, b.y);
3352
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3353
+ "rect",
3354
+ {
3355
+ x: p.x,
3356
+ y: p.y,
3357
+ width: Math.max(2, b.w * scale),
3358
+ height: Math.max(2, b.h * scale),
3359
+ rx: 2,
3360
+ fill: nodeFill
3361
+ },
3362
+ b.id
3363
+ );
3364
+ }),
3365
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3366
+ "rect",
3367
+ {
3368
+ x: vpRect.x,
3369
+ y: vpRect.y,
3370
+ width: vpRect.w,
3371
+ height: vpRect.h,
3372
+ rx: 3,
3373
+ fill: viewFill,
3374
+ stroke: viewStroke,
3375
+ strokeWidth: 1.25
3376
+ }
3377
+ )
3378
+ ]
3379
+ }
3380
+ )
3381
+ }
3382
+ );
3383
+ }
3384
+
3385
+ // src/ui/ContextMenu.tsx
3386
+ var import_react15 = require("react");
3387
+ var import_jsx_runtime10 = require("react/jsx-runtime");
3388
+ function ContextMenu({
3389
+ x,
3390
+ y,
3391
+ nodeId,
3392
+ edgeId,
3393
+ isDark,
3394
+ t,
3395
+ acc,
3396
+ canUndo,
3397
+ canRedo,
3398
+ onUndo,
3399
+ onRedo,
3400
+ onReCenter,
3401
+ onAddNode,
3402
+ onDuplicate,
3403
+ onRename,
3404
+ onDelete,
3405
+ onDisconnect,
3406
+ onEdgeRename,
3407
+ onEdgeStyle,
3408
+ onEdgeArrowhead,
3409
+ onEdgeDelete,
3410
+ onEdgeResetRouting,
3411
+ currentEdgeStyle,
3412
+ currentEdgeArrow,
3413
+ edgeHasWaypoint,
3414
+ containerRef
3415
+ }) {
3416
+ const menuRef = (0, import_react15.useRef)(null);
3417
+ const [pos, setPos] = (0, import_react15.useState)({ x, y });
3418
+ (0, import_react15.useEffect)(() => {
3419
+ if (!menuRef.current || !containerRef.current) return;
3420
+ const m = menuRef.current.getBoundingClientRect();
3421
+ const c = containerRef.current.getBoundingClientRect();
3422
+ let nx = x, ny = y;
3423
+ if (nx + m.width > c.right - 8) nx = x - m.width;
3424
+ if (ny + m.height > c.bottom - 8) ny = y - m.height;
3425
+ setPos({ x: nx, y: ny });
3426
+ }, [x, y, containerRef]);
3427
+ const bg = isDark ? "#1e293b" : "#ffffff";
3428
+ const border = isDark ? "#334155" : "#e2e8f0";
3429
+ const hoverBg = isDark ? "#334155" : "#f1f5f9";
3430
+ const dividerColor = isDark ? "#334155" : "#f1f5f9";
3431
+ const text = t.textPrimary;
3432
+ const muted = t.textMuted;
3433
+ const item = (label, onClick, color, disabled) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
3434
+ "button",
3435
+ {
3436
+ onClick: disabled ? void 0 : onClick,
3437
+ style: {
3438
+ display: "flex",
3439
+ alignItems: "center",
3440
+ gap: 10,
3441
+ width: "100%",
3442
+ padding: "7px 14px",
3443
+ background: "none",
3444
+ border: "none",
3445
+ textAlign: "left",
3446
+ cursor: disabled ? "default" : "pointer",
3447
+ fontSize: 12,
3448
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
3449
+ color: disabled ? muted : color ?? text,
3450
+ opacity: disabled ? 0.4 : 1,
3451
+ borderRadius: 6
3452
+ },
3453
+ onMouseEnter: (e) => {
3454
+ if (!disabled) e.currentTarget.style.background = hoverBg;
3455
+ },
3456
+ onMouseLeave: (e) => {
3457
+ e.currentTarget.style.background = "none";
3458
+ },
3459
+ children: label
3460
+ },
3461
+ label
3462
+ );
3463
+ const divider2 = /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { height: 1, background: dividerColor, margin: "4px 0" } });
3464
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
3465
+ "div",
3466
+ {
3467
+ ref: menuRef,
3468
+ onMouseDown: (e) => e.stopPropagation(),
3469
+ style: {
3470
+ position: "fixed",
3471
+ left: pos.x,
3472
+ top: pos.y,
3473
+ zIndex: 9999,
3474
+ background: bg,
3475
+ border: `1px solid ${border}`,
3476
+ borderRadius: 10,
3477
+ padding: "5px 0",
3478
+ minWidth: 180,
3479
+ boxShadow: isDark ? "0 8px 32px rgba(0,0,0,0.5)" : "0 8px 32px rgba(0,0,0,0.12)",
3480
+ fontFamily: "ui-sans-serif,system-ui,sans-serif"
3481
+ },
3482
+ children: edgeId ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
3483
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Edge" }),
3484
+ item("Rename label (dbl-click)", () => onEdgeRename?.()),
3485
+ divider2,
3486
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { padding: "4px 14px 2px", fontSize: 9, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Style" }),
3487
+ item(`Solid${currentEdgeStyle === "solid" || !currentEdgeStyle ? " \u2713" : ""}`, () => onEdgeStyle?.("solid")),
3488
+ item(`Dashed${currentEdgeStyle === "dashed" ? " \u2713" : ""}`, () => onEdgeStyle?.("dashed")),
3489
+ item(`Dotted${currentEdgeStyle === "dotted" ? " \u2713" : ""}`, () => onEdgeStyle?.("dotted")),
3490
+ divider2,
3491
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { padding: "4px 14px 2px", fontSize: 9, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Arrowhead" }),
3492
+ item(`Arrow${currentEdgeArrow !== "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("arrow")),
3493
+ item(`None${currentEdgeArrow === "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("none")),
3494
+ divider2,
3495
+ item("Reset routing", () => onEdgeResetRouting?.(), void 0, !edgeHasWaypoint),
3496
+ item("Delete edge", () => onEdgeDelete?.(), "#ef4444")
3497
+ ] }) : nodeId ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
3498
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Node" }),
3499
+ item("Rename (dbl-click)", onRename),
3500
+ item("Duplicate", onDuplicate),
3501
+ item("Disconnect all edges", onDisconnect),
3502
+ divider2,
3503
+ item("Delete node", onDelete, "#ef4444")
3504
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
3505
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Canvas" }),
3506
+ item("Add node here", onAddNode, acc.color),
3507
+ item("Re-center (Ctrl+0)", onReCenter),
3508
+ divider2,
3509
+ item("Undo (Ctrl+Z)", onUndo, void 0, !canUndo),
3510
+ item("Redo (Ctrl+Y)", onRedo, void 0, !canRedo)
3511
+ ] })
3512
+ }
3513
+ );
3514
+ }
3515
+
3516
+ // src/ui/DiagramCanvas.tsx
3517
+ var import_jsx_runtime11 = require("react/jsx-runtime");
3518
+ var STYLE_LABEL2 = { pointerEvents: "none", userSelect: "none" };
3519
+ var STYLE_LIVE_PORT = { opacity: 0.85, pointerEvents: "none" };
3520
+ var STYLE_NODE_GRAB = { cursor: "grab" };
3521
+ var STYLE_NODE_GRABBING = { cursor: "grabbing" };
3522
+ var STYLE_PORT_VISIBLE = { cursor: "crosshair", opacity: 1, transition: "opacity 0.15s", pointerEvents: "all", filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.25))" };
3523
+ var STYLE_PORT_HIDDEN = { cursor: "crosshair", opacity: 0, transition: "opacity 0.15s", pointerEvents: "none", filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.25))" };
3524
+ function DiagramCanvas(props) {
3525
+ const {
3526
+ model,
3527
+ variant,
3528
+ variantLabel,
3529
+ t,
3530
+ isDark,
3531
+ acc,
3532
+ transform,
3533
+ setTransform,
3534
+ selected,
3535
+ selectedSet,
3536
+ hoveredId,
3537
+ setHoveredId,
3538
+ drag,
3539
+ pan,
3540
+ liveEdge,
3541
+ boxSel,
3542
+ alignGuides,
3543
+ editingEdgeId,
3544
+ editEdgeLabel,
3545
+ setEditEdgeLabel,
3546
+ commitEdgeEdit,
3547
+ setEditingEdgeId,
3548
+ beginEditEdge,
3549
+ onEdgeContextMenu,
3550
+ setWaypointDrag,
3551
+ editingId,
3552
+ editLabel,
3553
+ setEditLabel,
3554
+ commitEdit,
3555
+ setEditingId,
3556
+ onNodeMouseDown,
3557
+ onNodeMouseUp,
3558
+ onNodeDblClick,
3559
+ onNodeContextMenu,
3560
+ onPortMouseDown,
3561
+ onAnswerPortDown,
3562
+ onSvgMouseDown,
3563
+ onMouseMove,
3564
+ onMouseUp,
3565
+ onSvgContextMenu,
3566
+ reducedMotion,
3567
+ isCoarse,
3568
+ portR,
3569
+ shadowClr,
3570
+ arrowClr,
3571
+ amberArrow,
3572
+ viewport,
3573
+ svgRef,
3574
+ containerRef,
3575
+ ctxMenu,
3576
+ history,
3577
+ onCtxUndo,
3578
+ onCtxRedo,
3579
+ onCtxReCenter,
3580
+ onCtxAddNode,
3581
+ onCtxDuplicate,
3582
+ onCtxRename,
3583
+ onCtxDelete,
3584
+ onCtxDisconnect,
3585
+ ctxEdgeStyle,
3586
+ ctxEdgeArrow,
3587
+ ctxEdgeHasWaypoint,
3588
+ onCtxEdgeRename,
3589
+ onCtxEdgeStyle,
3590
+ onCtxEdgeArrowhead,
3591
+ onCtxEdgeDelete,
3592
+ onCtxEdgeResetRouting
3593
+ } = props;
3594
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { ref: containerRef, style: { flex: 1, overflow: "hidden", position: "relative", background: t.canvas }, children: [
3595
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
3596
+ "svg",
3597
+ {
3598
+ ref: svgRef,
3599
+ width: "100%",
3600
+ height: "100%",
3601
+ role: "application",
3602
+ "aria-label": `${variantLabel} diagram editor. ${model.nodes.length} ${variantLabel.toLowerCase()}s, ${model.edges.length} connections. Scroll to zoom, drag to pan, click a ${variantLabel.toLowerCase()} to select.`,
3603
+ tabIndex: 0,
3604
+ style: { display: "block", cursor: pan ? "grabbing" : drag ? "grabbing" : liveEdge ? "crosshair" : "default", userSelect: "none", outline: "none" },
3605
+ onMouseDown: onSvgMouseDown,
3606
+ onMouseMove,
3607
+ onMouseUp,
3608
+ onMouseLeave: onMouseUp,
3609
+ onContextMenu: onSvgContextMenu,
3610
+ children: [
3611
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("defs", { children: [
3612
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("style", { children: reducedMotion ? `
3613
+ .edge-flow { stroke-dasharray: 0; }
3614
+ .edge-flow-amber { stroke-dasharray: 0; }
3615
+ .edge-live { stroke-dasharray: 4 4; }
3616
+ ` : `
3617
+ @keyframes edgeFlow { to { stroke-dashoffset: -13; } }
3618
+ @keyframes edgeFlowFast { to { stroke-dashoffset: -13; } }
3619
+ .edge-flow { stroke-dasharray: 8 5; animation: edgeFlow 0.9s linear infinite; }
3620
+ .edge-flow-amber { stroke-dasharray: 6 4; animation: edgeFlowFast 0.65s linear infinite; }
3621
+ .edge-live { stroke-dasharray: 7 5; animation: edgeFlow 0.55s linear infinite; }
3622
+ ` }),
3623
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("pattern", { id: "dots", width: GRID, height: GRID, patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("circle", { cx: GRID / 2, cy: GRID / 2, r: 1.1, fill: t.dot }) }),
3624
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("filter", { id: "nodeShadow", x: "-25%", y: "-25%", width: "150%", height: "160%", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("feDropShadow", { dx: "0", dy: "3", stdDeviation: "5", floodColor: shadowClr, floodOpacity: "1" }) }),
3625
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("marker", { id: "arrowhead", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: arrowClr }) }),
3626
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("marker", { id: "arrowAmber", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: amberArrow }) }),
3627
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("marker", { id: "arrowLive", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: acc.color }) })
3628
+ ] }),
3629
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { width: "100%", height: "100%", fill: "url(#dots)", "data-bg": "1" }),
3630
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("g", { transform: `translate(${transform.x},${transform.y}) scale(${transform.scale})`, children: [
3631
+ model.edges.map((e) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3632
+ EdgeLine,
3633
+ {
3634
+ edge: e,
3635
+ nodes: model.nodes,
3636
+ variant,
3637
+ t,
3638
+ isDark,
3639
+ acc,
3640
+ editing: editingEdgeId === e.id,
3641
+ editValue: editEdgeLabel,
3642
+ onEditChange: setEditEdgeLabel,
3643
+ onEditCommit: commitEdgeEdit,
3644
+ onEditCancel: () => setEditingEdgeId(null),
3645
+ onDoubleClick: beginEditEdge,
3646
+ onContextMenu: onEdgeContextMenu,
3647
+ onWaypointDown: (ev, edgeId) => setWaypointDrag(edgeId)
3648
+ },
3649
+ e.id
3650
+ )),
3651
+ liveEdge && (() => {
3652
+ const d = bezierPath2(liveEdge.fromX, liveEdge.fromY, liveEdge.toX, liveEdge.toY, liveEdge.exitDir);
3653
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d, fill: "none", stroke: acc.color, strokeWidth: 2, strokeLinecap: "round", className: "edge-live", opacity: 0.8, markerEnd: "url(#arrowLive)" });
3654
+ })(),
3655
+ alignGuides?.x && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3656
+ "line",
3657
+ {
3658
+ x1: alignGuides.x.pos,
3659
+ x2: alignGuides.x.pos,
3660
+ y1: alignGuides.x.minY,
3661
+ y2: alignGuides.x.maxY,
3662
+ stroke: acc.color,
3663
+ strokeWidth: 1 / transform.scale,
3664
+ strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
3665
+ opacity: 0.85,
3666
+ pointerEvents: "none"
3667
+ }
3668
+ ),
3669
+ alignGuides?.y && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3670
+ "line",
3671
+ {
3672
+ y1: alignGuides.y.pos,
3673
+ y2: alignGuides.y.pos,
3674
+ x1: alignGuides.y.minX,
3675
+ x2: alignGuides.y.maxX,
3676
+ stroke: acc.color,
3677
+ strokeWidth: 1 / transform.scale,
3678
+ strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
3679
+ opacity: 0.85,
3680
+ pointerEvents: "none"
3251
3681
  }
3252
- },
3253
- onMouseDown: (e) => e.stopPropagation(),
3254
- style: {
3255
- width: "100%",
3256
- height: "100%",
3257
- border: "none",
3258
- borderRadius: 6,
3259
- outline: `2px solid ${acc.color}`,
3260
- textAlign: "center",
3261
- fontSize: 10,
3262
- fontWeight: 500,
3263
- background: t.inputBg,
3264
- color: t.inputText,
3265
- boxSizing: "border-box",
3266
- padding: "0 6px",
3267
- fontFamily: "inherit"
3268
- }
3682
+ ),
3683
+ model.nodes.map((node, idx) => {
3684
+ const isHovered = hoveredId === node.id;
3685
+ const isQuestion2 = variant === "question";
3686
+ const { w: nW } = nodeDims(node, variant);
3687
+ const isSelected = selectedSet.has(node.id);
3688
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
3689
+ "g",
3690
+ {
3691
+ transform: `translate(${node.x ?? 0},${node.y ?? 0})`,
3692
+ role: "button",
3693
+ tabIndex: 0,
3694
+ "aria-label": `${variantLabel} ${variant === "journey" ? idx + 1 + ": " : ""}${node.label}${isSelected ? ", selected" : ""}`,
3695
+ style: drag?.nodeId === node.id ? STYLE_NODE_GRABBING : STYLE_NODE_GRAB,
3696
+ onMouseDown: (e) => onNodeMouseDown(e, node.id),
3697
+ onMouseUp: (e) => onNodeMouseUp(e, node.id),
3698
+ onDoubleClick: (e) => onNodeDblClick(e, node.id),
3699
+ onContextMenu: (e) => onNodeContextMenu(e, node.id),
3700
+ onMouseEnter: () => setHoveredId(node.id),
3701
+ onMouseLeave: () => setHoveredId(null),
3702
+ onFocus: () => setHoveredId(node.id),
3703
+ onBlur: () => setHoveredId(null),
3704
+ onKeyDown: (e) => {
3705
+ if (e.key === "F2" || e.key === "Enter" && !e.ctrlKey && !e.metaKey) {
3706
+ e.preventDefault();
3707
+ setEditingId(node.id);
3708
+ setEditLabel(node.label);
3709
+ }
3710
+ },
3711
+ children: [
3712
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("title", { children: `${variantLabel}: ${node.label}` }),
3713
+ isQuestion2 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(QuestionNode, { node, selected: isSelected, edges: model.edges, isDark, onAnswerPortDown, qW: nW }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
3714
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(NodeShape, { node, selected: isSelected, variant, stepNumber: variant === "journey" ? idx + 1 : void 0, t, isDark, w: nW }),
3715
+ editingId === node.id ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("foreignObject", { x: 6, y: 6, width: nW - 12, height: NODE_H2 - 12, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3716
+ "input",
3717
+ {
3718
+ autoFocus: true,
3719
+ value: editLabel,
3720
+ onChange: (e) => setEditLabel(e.target.value),
3721
+ onBlur: commitEdit,
3722
+ onKeyDown: (e) => {
3723
+ if (e.key === "Enter") commitEdit();
3724
+ if (e.key === "Escape") setEditingId(null);
3725
+ },
3726
+ style: { width: "100%", height: "100%", border: "none", borderRadius: 6, outline: `2px solid ${acc.color}`, textAlign: "center", fontSize: 13, fontWeight: 500, background: t.inputBg, boxSizing: "border-box", padding: "0 6px", fontFamily: "inherit", color: t.inputText }
3727
+ }
3728
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("text", { x: nW / 2, y: NODE_H2 / 2 + 5, textAnchor: "middle", fontSize: 13, fontWeight: "500", fontFamily: "ui-sans-serif,system-ui,sans-serif", fill: isSelected ? acc.color : t.textPrimary, style: STYLE_LABEL2, children: node.label }),
3729
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3730
+ "circle",
3731
+ {
3732
+ cx: nW / 2,
3733
+ cy: NODE_H2 + 1,
3734
+ r: portR,
3735
+ fill: acc.color,
3736
+ stroke: isDark ? "#0f172a" : "white",
3737
+ strokeWidth: 2,
3738
+ style: isHovered || isCoarse ? STYLE_PORT_VISIBLE : STYLE_PORT_HIDDEN,
3739
+ onMouseDown: (e) => onPortMouseDown(e, node.id)
3740
+ }
3741
+ )
3742
+ ] }),
3743
+ liveEdge && liveEdge.fromId !== node.id && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("circle", { cx: nW / 2, cy: -1, r: portR, fill: acc.color, stroke: isDark ? "#0f172a" : "white", strokeWidth: 2, style: STYLE_LIVE_PORT })
3744
+ ]
3745
+ },
3746
+ node.id
3747
+ );
3748
+ })
3749
+ ] })
3750
+ ]
3751
+ }
3752
+ ),
3753
+ boxSel && Math.abs(boxSel.cx - boxSel.sx) + Math.abs(boxSel.cy - boxSel.sy) > 4 && containerRef.current && (() => {
3754
+ const rect = containerRef.current.getBoundingClientRect();
3755
+ const left = Math.min(boxSel.sx, boxSel.cx) - rect.left;
3756
+ const top = Math.min(boxSel.sy, boxSel.cy) - rect.top;
3757
+ const w = Math.abs(boxSel.cx - boxSel.sx);
3758
+ const h = Math.abs(boxSel.cy - boxSel.sy);
3759
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3760
+ "div",
3761
+ {
3762
+ style: {
3763
+ position: "absolute",
3764
+ left,
3765
+ top,
3766
+ width: w,
3767
+ height: h,
3768
+ border: `1px dashed ${acc.color}`,
3769
+ background: isDark ? "rgba(99,102,241,0.10)" : "rgba(99,102,241,0.08)",
3770
+ pointerEvents: "none",
3771
+ borderRadius: 4
3269
3772
  }
3270
- ) }) : edge.label && !isAmber ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
3271
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3272
- "rect",
3273
- {
3274
- x: mx - labelW / 2,
3275
- y: my - 11,
3276
- width: labelW,
3277
- height: 19,
3278
- rx: 5,
3279
- fill: t.panelBg,
3280
- stroke: t.cardBorder,
3281
- strokeWidth: 1,
3282
- style: STYLE_EDGE_LABEL_HIT
3283
- }
3284
- ),
3285
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3286
- "text",
3287
- {
3288
- x: mx,
3289
- y: my + 4,
3290
- textAnchor: "middle",
3291
- fontSize: 10,
3292
- fill: t.textSecondary,
3293
- fontFamily: "ui-sans-serif,system-ui,sans-serif",
3294
- fontWeight: "500",
3295
- style: STYLE_LABEL,
3296
- children: edge.label
3297
- }
3298
- )
3299
- ] }) : null
3300
- ]
3301
- }
3302
- );
3773
+ }
3774
+ );
3775
+ })(),
3776
+ model.nodes.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: { position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", pointerEvents: "none", gap: 8 }, children: [
3777
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: { fontSize: 36, opacity: 0.1, color: t.textPrimary }, children: variant === "question" ? "?" : variant === "journey" ? "\u2197" : "\u2B21" }),
3778
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: { fontSize: 13, color: t.textMuted, fontWeight: 500 }, children: [
3779
+ "Click ",
3780
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("strong", { style: { color: acc.color }, children: [
3781
+ "+ ",
3782
+ variantLabel
3783
+ ] }),
3784
+ " to start"
3785
+ ] })
3786
+ ] }),
3787
+ model.nodes.length > 0 && viewport.w > 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3788
+ Minimap,
3789
+ {
3790
+ model,
3791
+ viewportW: viewport.w,
3792
+ viewportH: viewport.h,
3793
+ transform,
3794
+ isDark,
3795
+ accentColor: acc.color,
3796
+ measureNode: (n) => nodeDims(n, variant),
3797
+ onCenterOn: (cx, cy) => {
3798
+ setTransform((tr) => ({ ...tr, x: viewport.w / 2 - cx * tr.scale, y: viewport.h / 2 - cy * tr.scale }));
3799
+ }
3800
+ }
3801
+ ),
3802
+ ctxMenu && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3803
+ ContextMenu,
3804
+ {
3805
+ x: ctxMenu.x,
3806
+ y: ctxMenu.y,
3807
+ nodeId: ctxMenu.nodeId,
3808
+ edgeId: ctxMenu.edgeId,
3809
+ isDark,
3810
+ t,
3811
+ acc,
3812
+ canUndo: history.canUndo,
3813
+ canRedo: history.canRedo,
3814
+ onUndo: onCtxUndo,
3815
+ onRedo: onCtxRedo,
3816
+ onReCenter: onCtxReCenter,
3817
+ onAddNode: onCtxAddNode,
3818
+ onDuplicate: onCtxDuplicate,
3819
+ onRename: onCtxRename,
3820
+ onDelete: onCtxDelete,
3821
+ onDisconnect: onCtxDisconnect,
3822
+ currentEdgeStyle: ctxEdgeStyle,
3823
+ currentEdgeArrow: ctxEdgeArrow,
3824
+ edgeHasWaypoint: ctxEdgeHasWaypoint,
3825
+ onEdgeRename: onCtxEdgeRename,
3826
+ onEdgeStyle: onCtxEdgeStyle,
3827
+ onEdgeArrowhead: onCtxEdgeArrowhead,
3828
+ onEdgeDelete: onCtxEdgeDelete,
3829
+ onEdgeResetRouting: onCtxEdgeResetRouting,
3830
+ containerRef
3831
+ }
3832
+ )
3833
+ ] });
3303
3834
  }
3304
3835
 
3305
3836
  // src/ui/hooks/useHistory.ts
3306
- var import_react13 = require("react");
3837
+ var import_react16 = require("react");
3307
3838
  var MAX_HISTORY = 80;
3308
3839
  function useHistory(initial, onChange) {
3309
- const [state, setState] = (0, import_react13.useState)(initial);
3310
- const stackRef = (0, import_react13.useRef)([initial]);
3311
- const idxRef = (0, import_react13.useRef)(0);
3312
- const [, setTick] = (0, import_react13.useState)(0);
3840
+ const [state, setState] = (0, import_react16.useState)(initial);
3841
+ const stackRef = (0, import_react16.useRef)([initial]);
3842
+ const idxRef = (0, import_react16.useRef)(0);
3843
+ const [, setTick] = (0, import_react16.useState)(0);
3313
3844
  const bump = () => setTick((n) => n + 1);
3314
- const apply = (0, import_react13.useCallback)(
3845
+ const apply = (0, import_react16.useCallback)(
3315
3846
  (next) => {
3316
3847
  setState(next);
3317
3848
  onChange?.(next);
3318
3849
  },
3319
3850
  [onChange]
3320
3851
  );
3321
- const applyAndPush = (0, import_react13.useCallback)(
3852
+ const applyAndPush = (0, import_react16.useCallback)(
3322
3853
  (next) => {
3323
3854
  const stack = stackRef.current.slice(0, idxRef.current + 1);
3324
3855
  stack.push(next);
@@ -3331,7 +3862,7 @@ function useHistory(initial, onChange) {
3331
3862
  },
3332
3863
  [onChange]
3333
3864
  );
3334
- const undo = (0, import_react13.useCallback)(() => {
3865
+ const undo = (0, import_react16.useCallback)(() => {
3335
3866
  if (idxRef.current <= 0) return;
3336
3867
  idxRef.current--;
3337
3868
  const next = stackRef.current[idxRef.current];
@@ -3339,7 +3870,7 @@ function useHistory(initial, onChange) {
3339
3870
  onChange?.(next);
3340
3871
  bump();
3341
3872
  }, [onChange]);
3342
- const redo = (0, import_react13.useCallback)(() => {
3873
+ const redo = (0, import_react16.useCallback)(() => {
3343
3874
  if (idxRef.current >= stackRef.current.length - 1) return;
3344
3875
  idxRef.current++;
3345
3876
  const next = stackRef.current[idxRef.current];
@@ -3359,10 +3890,10 @@ function useHistory(initial, onChange) {
3359
3890
  }
3360
3891
 
3361
3892
  // src/ui/hooks/useCanvasWheel.ts
3362
- var import_react14 = require("react");
3893
+ var import_react17 = require("react");
3363
3894
  function useCanvasWheel(ref, setTransform, options = {}) {
3364
3895
  const { min = 0.15, max = 3, factor = 0.1 } = options;
3365
- (0, import_react14.useEffect)(() => {
3896
+ (0, import_react17.useEffect)(() => {
3366
3897
  const el = ref.current;
3367
3898
  if (!el) return;
3368
3899
  const onWheel = (e) => {
@@ -3386,7 +3917,7 @@ function useCanvasWheel(ref, setTransform, options = {}) {
3386
3917
  }
3387
3918
 
3388
3919
  // src/ui/hooks/useCanvasTouch.ts
3389
- var import_react15 = require("react");
3920
+ var import_react18 = require("react");
3390
3921
  function useCanvasTouch(ref, {
3391
3922
  transform,
3392
3923
  setTransform,
@@ -3396,7 +3927,7 @@ function useCanvasTouch(ref, {
3396
3927
  longPressMs = 550,
3397
3928
  longPressSlop = 8
3398
3929
  }) {
3399
- (0, import_react15.useEffect)(() => {
3930
+ (0, import_react18.useEffect)(() => {
3400
3931
  const el = ref.current;
3401
3932
  if (!el) return;
3402
3933
  let touchPan = null;
@@ -3500,10 +4031,10 @@ function useCanvasTouch(ref, {
3500
4031
  }
3501
4032
 
3502
4033
  // src/ui/hooks/useElementSize.ts
3503
- var import_react16 = require("react");
4034
+ var import_react19 = require("react");
3504
4035
  function useElementSize(ref) {
3505
- const [size, setSize] = (0, import_react16.useState)({ w: 0, h: 0 });
3506
- (0, import_react16.useEffect)(() => {
4036
+ const [size, setSize] = (0, import_react19.useState)({ w: 0, h: 0 });
4037
+ (0, import_react19.useEffect)(() => {
3507
4038
  const el = ref.current;
3508
4039
  if (!el || typeof ResizeObserver === "undefined") return;
3509
4040
  const measure = () => {
@@ -3609,14 +4140,12 @@ function nearestInDirection(fromX, fromY, dir, candidates) {
3609
4140
  }
3610
4141
 
3611
4142
  // src/ui/DiagramEditor.tsx
3612
- var import_jsx_runtime9 = require("react/jsx-runtime");
3613
- var STYLE_LABEL2 = { pointerEvents: "none", userSelect: "none" };
3614
- var STYLE_LIVE_PORT = { opacity: 0.85, pointerEvents: "none" };
4143
+ var import_jsx_runtime12 = require("react/jsx-runtime");
3615
4144
  var STYLE_SR_ONLY = { position: "absolute", width: 1, height: 1, padding: 0, margin: -1, overflow: "hidden", clip: "rect(0 0 0 0)", whiteSpace: "nowrap", border: 0 };
3616
4145
  var STYLE_FLEX_ROW = { flex: 1, display: "flex", overflow: "hidden" };
3617
4146
  function DiagramEditor(props) {
3618
4147
  if (props.initialModel?.type === "sequence") {
3619
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4148
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3620
4149
  SequenceEditor,
3621
4150
  {
3622
4151
  initialModel: props.initialModel,
@@ -3630,7 +4159,7 @@ function DiagramEditor(props) {
3630
4159
  }
3631
4160
  );
3632
4161
  }
3633
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(FlowchartEditor, { ...props });
4162
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(FlowchartEditor, { ...props });
3634
4163
  }
3635
4164
  function FlowchartEditor({
3636
4165
  initialModel,
@@ -3644,25 +4173,26 @@ function FlowchartEditor({
3644
4173
  themeOverrides
3645
4174
  }) {
3646
4175
  const base = initialModel ? { ...initialModel, variant: initialModel.variant ?? variant } : presetFlowchartModel(variant);
3647
- const notify = (0, import_react17.useCallback)((m) => onChange?.(m), [onChange]);
4176
+ const notify = (0, import_react20.useCallback)((m) => onChange?.(m), [onChange]);
3648
4177
  const history = useHistory(base, notify);
3649
4178
  const { state: model, apply: applyModel, applyAndPush, undo, redo } = history;
3650
- const [transform, setTransform] = (0, import_react17.useState)({ x: 60, y: 60, scale: 1 });
3651
- const [selected, setSelected] = (0, import_react17.useState)(null);
3652
- const [selectedSet, setSelectedSet] = (0, import_react17.useState)(() => /* @__PURE__ */ new Set());
3653
- const [drag, setDrag] = (0, import_react17.useState)(null);
3654
- const [pan, setPan] = (0, import_react17.useState)(null);
3655
- const [boxSel, setBoxSel] = (0, import_react17.useState)(null);
3656
- const [liveEdge, setLiveEdge] = (0, import_react17.useState)(null);
3657
- const [alignGuides, setAlignGuides] = (0, import_react17.useState)(null);
3658
- const [waypointDrag, setWaypointDrag] = (0, import_react17.useState)(null);
3659
- const groupDragOriginsRef = (0, import_react17.useRef)(null);
3660
- const clipboardRef = (0, import_react17.useRef)(null);
3661
- const selectOne = (0, import_react17.useCallback)((id) => {
4179
+ const { toasts, showToast, dismissToast } = useToast();
4180
+ const [transform, setTransform] = (0, import_react20.useState)({ x: 60, y: 60, scale: 1 });
4181
+ const [selected, setSelected] = (0, import_react20.useState)(null);
4182
+ const [selectedSet, setSelectedSet] = (0, import_react20.useState)(() => /* @__PURE__ */ new Set());
4183
+ const [drag, setDrag] = (0, import_react20.useState)(null);
4184
+ const [pan, setPan] = (0, import_react20.useState)(null);
4185
+ const [boxSel, setBoxSel] = (0, import_react20.useState)(null);
4186
+ const [liveEdge, setLiveEdge] = (0, import_react20.useState)(null);
4187
+ const [alignGuides, setAlignGuides] = (0, import_react20.useState)(null);
4188
+ const [waypointDrag, setWaypointDrag] = (0, import_react20.useState)(null);
4189
+ const groupDragOriginsRef = (0, import_react20.useRef)(null);
4190
+ const clipboardRef = (0, import_react20.useRef)(null);
4191
+ const selectOne = (0, import_react20.useCallback)((id) => {
3662
4192
  setSelected(id);
3663
4193
  setSelectedSet(id ? /* @__PURE__ */ new Set([id]) : /* @__PURE__ */ new Set());
3664
4194
  }, []);
3665
- const toggleSelect = (0, import_react17.useCallback)((id) => {
4195
+ const toggleSelect = (0, import_react20.useCallback)((id) => {
3666
4196
  setSelectedSet((prev) => {
3667
4197
  const next = new Set(prev);
3668
4198
  if (next.has(id)) {
@@ -3676,26 +4206,26 @@ function FlowchartEditor({
3676
4206
  return next;
3677
4207
  });
3678
4208
  }, []);
3679
- const clearSelection = (0, import_react17.useCallback)(() => {
4209
+ const clearSelection = (0, import_react20.useCallback)(() => {
3680
4210
  setSelected(null);
3681
4211
  setSelectedSet(/* @__PURE__ */ new Set());
3682
4212
  }, []);
3683
- const [editingId, setEditingId] = (0, import_react17.useState)(null);
3684
- const [editLabel, setEditLabel] = (0, import_react17.useState)("");
3685
- const [editingEdgeId, setEditingEdgeId] = (0, import_react17.useState)(null);
3686
- const [editEdgeLabel, setEditEdgeLabel] = (0, import_react17.useState)("");
3687
- const [hoveredId, setHoveredId] = (0, import_react17.useState)(null);
3688
- const [ctxMenu, setCtxMenu] = (0, import_react17.useState)(null);
3689
- const [navOpen, setNavOpen] = (0, import_react17.useState)(true);
3690
- const [announcement, setAnnouncement] = (0, import_react17.useState)("");
3691
- const svgRef = (0, import_react17.useRef)(null);
3692
- const containerRef = (0, import_react17.useRef)(null);
4213
+ const [editingId, setEditingId] = (0, import_react20.useState)(null);
4214
+ const [editLabel, setEditLabel] = (0, import_react20.useState)("");
4215
+ const [editingEdgeId, setEditingEdgeId] = (0, import_react20.useState)(null);
4216
+ const [editEdgeLabel, setEditEdgeLabel] = (0, import_react20.useState)("");
4217
+ const [hoveredId, setHoveredId] = (0, import_react20.useState)(null);
4218
+ const [ctxMenu, setCtxMenu] = (0, import_react20.useState)(null);
4219
+ const [navOpen, setNavOpen] = (0, import_react20.useState)(true);
4220
+ const [announcement, setAnnouncement] = (0, import_react20.useState)("");
4221
+ const svgRef = (0, import_react20.useRef)(null);
4222
+ const containerRef = (0, import_react20.useRef)(null);
3693
4223
  const reducedMotion = usePrefersReducedMotion();
3694
4224
  const { t, isDark } = useEditorTheme(theme, themeOverrides, { light: lightTheme, dark: darkTheme });
3695
4225
  const isCoarse = useIsCoarsePointer();
3696
4226
  const portR = isCoarse ? 9 : 6;
3697
4227
  const viewport = useElementSize(svgRef);
3698
- const reCenter = (0, import_react17.useCallback)(() => {
4228
+ const reCenter = (0, import_react20.useCallback)(() => {
3699
4229
  if (!svgRef.current) return;
3700
4230
  const rect = svgRef.current.getBoundingClientRect();
3701
4231
  const W2 = rect.width, H2 = rect.height;
@@ -3719,7 +4249,7 @@ function FlowchartEditor({
3719
4249
  const cx = (minX + maxX) / 2, cy = (minY + maxY) / 2;
3720
4250
  setTransform({ scale, x: W2 / 2 - cx * scale, y: H2 / 2 - cy * scale });
3721
4251
  }, [model.nodes, variant]);
3722
- const jumpToNode = (0, import_react17.useCallback)((nodeId) => {
4252
+ const jumpToNode = (0, import_react20.useCallback)((nodeId) => {
3723
4253
  const node = model.nodes.find((n) => n.id === nodeId);
3724
4254
  if (!node || !svgRef.current) return;
3725
4255
  const rect = svgRef.current.getBoundingClientRect();
@@ -3730,7 +4260,7 @@ function FlowchartEditor({
3730
4260
  setTransform({ scale, x: rect.width / 2 - cx * scale, y: rect.height / 2 - cy * scale });
3731
4261
  selectOne(nodeId);
3732
4262
  }, [model.nodes, variant, transform.scale, selectOne]);
3733
- const duplicateIds = (0, import_react17.useCallback)((ids) => {
4263
+ const duplicateIds = (0, import_react20.useCallback)((ids) => {
3734
4264
  if (ids.length === 0) return;
3735
4265
  const idSet = new Set(ids);
3736
4266
  const idMap = /* @__PURE__ */ new Map();
@@ -3762,92 +4292,90 @@ function FlowchartEditor({
3762
4292
  setSelected(newIds[newIds.length - 1] ?? null);
3763
4293
  setSelectedSet(new Set(newIds));
3764
4294
  }, [model, applyAndPush]);
3765
- const duplicateNode = (0, import_react17.useCallback)((nodeId) => {
4295
+ const duplicateNode = (0, import_react20.useCallback)((nodeId) => {
3766
4296
  duplicateIds([nodeId]);
3767
4297
  }, [duplicateIds]);
3768
- (0, import_react17.useEffect)(() => {
4298
+ (0, import_react20.useEffect)(() => {
3769
4299
  if (!ctxMenu) return;
3770
4300
  const close = () => setCtxMenu(null);
3771
4301
  window.addEventListener("mousedown", close);
3772
4302
  return () => window.removeEventListener("mousedown", close);
3773
4303
  }, [ctxMenu]);
3774
- (0, import_react17.useEffect)(() => {
3775
- const onKey = (e) => {
3776
- const tgt = e.target;
3777
- if (tgt && (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA" || tgt.isContentEditable)) return;
3778
- const ctrl = e.ctrlKey || e.metaKey;
3779
- if (ctrl && e.key === "z") {
3780
- e.preventDefault();
3781
- undo();
3782
- return;
3783
- }
3784
- if (ctrl && (e.key === "y" || e.shiftKey && e.key === "z")) {
3785
- e.preventDefault();
3786
- redo();
3787
- return;
3788
- }
3789
- if (ctrl && e.key === "0") {
3790
- e.preventDefault();
3791
- reCenter();
3792
- return;
3793
- }
3794
- if (ctrl && (e.key === "d" || e.key === "D")) {
3795
- if (selectedSet.size > 0) {
3796
- e.preventDefault();
3797
- duplicateIds(Array.from(selectedSet));
3798
- }
3799
- return;
4304
+ const keyCommands = [
4305
+ { match: (e) => (e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey, run: () => {
4306
+ undo();
4307
+ return true;
4308
+ } },
4309
+ { match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "y" || e.shiftKey && e.key === "z"), run: () => {
4310
+ redo();
4311
+ return true;
4312
+ } },
4313
+ { match: (e) => (e.ctrlKey || e.metaKey) && e.key === "0", run: () => {
4314
+ reCenter();
4315
+ return true;
4316
+ } },
4317
+ {
4318
+ match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "d" || e.key === "D") && selectedSet.size > 0,
4319
+ run: () => {
4320
+ duplicateIds(Array.from(selectedSet));
4321
+ return true;
3800
4322
  }
3801
- if (ctrl && (e.key === "c" || e.key === "C")) {
3802
- if (selectedSet.size > 0) {
3803
- e.preventDefault();
3804
- const ids = new Set(selectedSet);
3805
- const nodes = model.nodes.filter((n) => ids.has(n.id));
3806
- const edges = model.edges.filter((ed) => ids.has(ed.from) && ids.has(ed.to));
3807
- clipboardRef.current = {
3808
- nodes: nodes.map((n) => ({ ...n })),
3809
- edges: edges.map((ed) => ({ ...ed }))
3810
- };
3811
- }
3812
- return;
4323
+ },
4324
+ {
4325
+ match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "c" || e.key === "C") && selectedSet.size > 0,
4326
+ run: () => {
4327
+ const ids = new Set(selectedSet);
4328
+ const nodes = model.nodes.filter((n) => ids.has(n.id));
4329
+ const edges = model.edges.filter((ed) => ids.has(ed.from) && ids.has(ed.to));
4330
+ clipboardRef.current = {
4331
+ nodes: nodes.map((n) => ({ ...n })),
4332
+ edges: edges.map((ed) => ({ ...ed }))
4333
+ };
4334
+ return true;
3813
4335
  }
3814
- if (ctrl && (e.key === "v" || e.key === "V")) {
4336
+ },
4337
+ {
4338
+ match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "v" || e.key === "V"),
4339
+ run: () => {
3815
4340
  const clip = clipboardRef.current;
3816
- if (clip && clip.nodes.length > 0) {
3817
- e.preventDefault();
3818
- const idMap = /* @__PURE__ */ new Map();
3819
- const nextNode = makeIdSource("node", model.nodes);
3820
- const nextEdge = makeIdSource("e", model.edges);
3821
- const newNodes = clip.nodes.map((n) => {
3822
- const newId = nextNode();
3823
- idMap.set(n.id, newId);
3824
- return { ...n, id: newId, x: (n.x ?? 0) + 24, y: (n.y ?? 0) + 24 };
3825
- });
3826
- const newEdges = clip.edges.map((ed) => ({
3827
- ...ed,
3828
- id: nextEdge(),
3829
- from: idMap.get(ed.from) ?? ed.from,
3830
- to: idMap.get(ed.to) ?? ed.to
3831
- }));
3832
- const m = { ...model, nodes: [...model.nodes, ...newNodes], edges: [...model.edges, ...newEdges] };
3833
- applyAndPush(m);
3834
- const newIds = newNodes.map((n) => n.id);
3835
- setSelected(newIds[newIds.length - 1]);
3836
- setSelectedSet(new Set(newIds));
3837
- setAnnouncement(`Pasted ${newIds.length} ${variantLabel.toLowerCase()}${newIds.length === 1 ? "" : "s"}.`);
3838
- }
3839
- return;
4341
+ if (!clip || clip.nodes.length === 0) return false;
4342
+ const idMap = /* @__PURE__ */ new Map();
4343
+ const nextNode = makeIdSource("node", model.nodes);
4344
+ const nextEdge = makeIdSource("e", model.edges);
4345
+ const newNodes = clip.nodes.map((n) => {
4346
+ const newId = nextNode();
4347
+ idMap.set(n.id, newId);
4348
+ return { ...n, id: newId, x: (n.x ?? 0) + 24, y: (n.y ?? 0) + 24 };
4349
+ });
4350
+ const newEdges = clip.edges.map((ed) => ({
4351
+ ...ed,
4352
+ id: nextEdge(),
4353
+ from: idMap.get(ed.from) ?? ed.from,
4354
+ to: idMap.get(ed.to) ?? ed.to
4355
+ }));
4356
+ const m = { ...model, nodes: [...model.nodes, ...newNodes], edges: [...model.edges, ...newEdges] };
4357
+ applyAndPush(m);
4358
+ const newIds = newNodes.map((n) => n.id);
4359
+ setSelected(newIds[newIds.length - 1]);
4360
+ setSelectedSet(new Set(newIds));
4361
+ setAnnouncement(`Pasted ${newIds.length} ${variantLabel.toLowerCase()}${newIds.length === 1 ? "" : "s"}.`);
4362
+ return true;
3840
4363
  }
3841
- if (e.key === "Escape") {
4364
+ },
4365
+ {
4366
+ match: (e) => e.key === "Escape",
4367
+ run: () => {
3842
4368
  if (ctxMenu) setCtxMenu(null);
3843
4369
  if (liveEdge) setLiveEdge(null);
3844
4370
  if (editingId) setEditingId(null);
3845
4371
  if (boxSel) setBoxSel(null);
3846
4372
  if (selectedSet.size > 0) clearSelection();
3847
- return;
4373
+ return true;
3848
4374
  }
3849
- if ((e.key === "Delete" || e.key === "Backspace") && selectedSet.size > 0) {
3850
- e.preventDefault();
4375
+ },
4376
+ {
4377
+ match: (e) => (e.key === "Delete" || e.key === "Backspace") && selectedSet.size > 0,
4378
+ run: () => {
3851
4379
  const ids = new Set(selectedSet);
3852
4380
  const updated = {
3853
4381
  ...model,
@@ -3857,29 +4385,34 @@ function FlowchartEditor({
3857
4385
  applyAndPush(updated);
3858
4386
  clearSelection();
3859
4387
  setAnnouncement(`Deleted ${ids.size} ${variantLabel.toLowerCase()}${ids.size === 1 ? "" : "s"}.`);
3860
- return;
4388
+ return true;
3861
4389
  }
3862
- if (selectedSet.size > 0 && (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight")) {
4390
+ },
4391
+ {
4392
+ match: (e) => selectedSet.size > 0 && e.altKey && !!selected && (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight"),
4393
+ run: (e) => {
3863
4394
  const dirKey = e.key === "ArrowLeft" ? "left" : e.key === "ArrowRight" ? "right" : e.key === "ArrowUp" ? "up" : "down";
3864
- if (e.altKey && selected) {
3865
- e.preventDefault();
3866
- const origin = model.nodes.find((n) => n.id === selected);
3867
- if (!origin) return;
3868
- const od = nodeDims(origin, variant);
3869
- const ox = (origin.x ?? 0) + od.w / 2;
3870
- const oy = (origin.y ?? 0) + od.h / 2;
3871
- const candidates = model.nodes.filter((n) => n.id !== selected).map((n) => {
3872
- const d = nodeDims(n, variant);
3873
- return { id: n.id, x: (n.x ?? 0) + d.w / 2, y: (n.y ?? 0) + d.h / 2 };
3874
- });
3875
- const nextId2 = nearestInDirection(ox, oy, dirKey, candidates);
3876
- if (nextId2) {
3877
- selectOne(nextId2);
3878
- setAnnouncement(`Selected ${model.nodes.find((n) => n.id === nextId2)?.label ?? ""}.`);
3879
- }
3880
- return;
4395
+ const origin = model.nodes.find((n) => n.id === selected);
4396
+ if (!origin) return false;
4397
+ const od = nodeDims(origin, variant);
4398
+ const ox = (origin.x ?? 0) + od.w / 2;
4399
+ const oy = (origin.y ?? 0) + od.h / 2;
4400
+ const candidates = model.nodes.filter((n) => n.id !== selected).map((n) => {
4401
+ const d = nodeDims(n, variant);
4402
+ return { id: n.id, x: (n.x ?? 0) + d.w / 2, y: (n.y ?? 0) + d.h / 2 };
4403
+ });
4404
+ const nextNodeId = nearestInDirection(ox, oy, dirKey, candidates);
4405
+ if (nextNodeId) {
4406
+ selectOne(nextNodeId);
4407
+ setAnnouncement(`Selected ${model.nodes.find((n) => n.id === nextNodeId)?.label ?? ""}.`);
3881
4408
  }
3882
- e.preventDefault();
4409
+ return true;
4410
+ }
4411
+ },
4412
+ {
4413
+ match: (e) => selectedSet.size > 0 && (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight"),
4414
+ run: (e) => {
4415
+ const dirKey = e.key === "ArrowLeft" ? "left" : e.key === "ArrowRight" ? "right" : e.key === "ArrowUp" ? "up" : "down";
3883
4416
  const step = e.shiftKey ? GRID * 4 : GRID;
3884
4417
  const dx = dirKey === "left" ? -step : dirKey === "right" ? step : 0;
3885
4418
  const dy = dirKey === "up" ? -step : dirKey === "down" ? step : 0;
@@ -3889,17 +4422,17 @@ function FlowchartEditor({
3889
4422
  nodes: model.nodes.map((n) => ids.has(n.id) ? { ...n, x: snap((n.x ?? 0) + dx), y: snap((n.y ?? 0) + dy) } : n)
3890
4423
  };
3891
4424
  applyAndPush(updated);
4425
+ return true;
3892
4426
  }
3893
- };
3894
- window.addEventListener("keydown", onKey);
3895
- return () => window.removeEventListener("keydown", onKey);
3896
- }, [undo, redo, reCenter, selected, selectedSet, ctxMenu, liveEdge, editingId, boxSel, model, applyAndPush, duplicateNode, clearSelection]);
3897
- const toCanvas = (0, import_react17.useCallback)((clientX, clientY) => {
4427
+ }
4428
+ ];
4429
+ useEditorKeyboard(keyCommands, [undo, redo, reCenter, selected, selectedSet, ctxMenu, liveEdge, editingId, boxSel, model, applyAndPush, duplicateNode, clearSelection]);
4430
+ const toCanvas = (0, import_react20.useCallback)((clientX, clientY) => {
3898
4431
  const rect = svgRef.current.getBoundingClientRect();
3899
4432
  return { x: (clientX - rect.left - transform.x) / transform.scale, y: (clientY - rect.top - transform.y) / transform.scale };
3900
4433
  }, [transform]);
3901
4434
  useCanvasWheel(svgRef, setTransform);
3902
- const onCanvasLongPress = (0, import_react17.useCallback)((x, y) => {
4435
+ const onCanvasLongPress = (0, import_react20.useCallback)((x, y) => {
3903
4436
  setCtxMenu({ x, y, nodeId: null });
3904
4437
  }, []);
3905
4438
  useCanvasTouch(svgRef, { transform, setTransform, onLongPress: onCanvasLongPress });
@@ -4170,8 +4703,8 @@ function FlowchartEditor({
4170
4703
  };
4171
4704
  applyAndPush(updated);
4172
4705
  };
4173
- const handleExport = useExporters(model, onExport, "diagram");
4174
- const positionFlowchartNodes = (0, import_react17.useCallback)((m) => ({
4706
+ const handleExport = useExporters(model, onExport, "diagram", (msg) => showToast(msg, "success"));
4707
+ const positionFlowchartNodes = (0, import_react20.useCallback)((m) => ({
4175
4708
  ...m,
4176
4709
  nodes: m.nodes.map((n, i) => ({
4177
4710
  ...n,
@@ -4179,14 +4712,19 @@ function FlowchartEditor({
4179
4712
  y: n.y ?? snap(80 + Math.floor(i / 4) * 140)
4180
4713
  }))
4181
4714
  }), []);
4182
- const handleImport = useImporter(applyAndPush, { transform: positionFlowchartNodes });
4715
+ const handleImport = useImporter(applyAndPush, {
4716
+ transform: positionFlowchartNodes,
4717
+ onSuccess: (msg) => showToast(msg, "success"),
4718
+ onError: (msg) => showToast(msg, "error")
4719
+ });
4183
4720
  const acc = variantAccent(variant, isDark);
4184
4721
  const variantLabel = variant === "question" ? "Question" : variant === "journey" ? "Step" : "Node";
4185
- const shadowColor = isDark ? "rgba(0,0,0,0.55)" : "rgba(15,23,42,0.09)";
4186
- const arrowColor = isDark ? "#64748b" : "#94a3b8";
4722
+ const shadowClr = shadowColor(isDark);
4723
+ const arrowClr = arrowColor(isDark);
4187
4724
  const amberArrow = isDark ? ACCENT.amberDark : ACCENT.amber;
4188
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "fsd-editor", style: { display: "flex", flexDirection: "column", height, width: "100%", fontFamily: "ui-sans-serif,system-ui,sans-serif", boxSizing: "border-box", background: t.ctrlsBg }, children: [
4189
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("style", { children: `
4725
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "fsd-editor", style: { display: "flex", flexDirection: "column", height, width: "100%", fontFamily: "ui-sans-serif,system-ui,sans-serif", boxSizing: "border-box", background: t.ctrlsBg, position: "relative" }, children: [
4726
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ToastContainer, { toasts, onDismiss: dismissToast }),
4727
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("style", { children: `
4190
4728
  .fsd-editor button:focus-visible,
4191
4729
  .fsd-editor input:focus-visible,
4192
4730
  .fsd-editor textarea:focus-visible,
@@ -4196,12 +4734,16 @@ function FlowchartEditor({
4196
4734
  outline-offset: 2px;
4197
4735
  border-radius: 6px;
4198
4736
  }
4737
+ .fsd-editor svg [role="button"]:focus-visible {
4738
+ outline: 2px solid ${acc.color};
4739
+ outline-offset: 3px;
4740
+ }
4199
4741
  .fsd-editor svg[role="application"]:focus-visible {
4200
4742
  outline: 2px solid ${acc.color};
4201
4743
  outline-offset: -2px;
4202
4744
  }
4203
4745
  ` }),
4204
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4746
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
4205
4747
  "div",
4206
4748
  {
4207
4749
  role: "status",
@@ -4211,28 +4753,28 @@ function FlowchartEditor({
4211
4753
  children: announcement
4212
4754
  }
4213
4755
  ),
4214
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
4215
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { display: "flex", gap: 6, padding: "7px 14px", background: t.ctrlsBg, borderBottom: `1px solid ${t.ctrlsBorder}`, alignItems: "center", flexWrap: "wrap" }, children: [
4216
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("button", { onClick: () => addNode(), style: ctrlBtn(acc.color, isDark), children: [
4756
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
4757
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { display: "flex", gap: 6, padding: "7px 14px", background: t.ctrlsBg, borderBottom: `1px solid ${t.ctrlsBorder}`, alignItems: "center", flexWrap: "wrap" }, children: [
4758
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("button", { onClick: () => addNode(), style: ctrlBtn(acc.color, isDark), children: [
4217
4759
  "+ ",
4218
4760
  variantLabel
4219
4761
  ] }),
4220
- selectedSet.size > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
4221
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { width: 1, height: 20, background: t.ctrlsBorder, margin: "0 2px" } }),
4222
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { onClick: deleteSelected, style: { ...ctrlBtn("transparent", isDark), color: "#ef4444", border: `1px solid ${isDark ? "#7f1d1d" : "#fca5a5"}` }, children: selectedSet.size > 1 ? `Delete (${selectedSet.size})` : "Delete" })
4762
+ selectedSet.size > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
4763
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: { width: 1, height: 20, background: t.ctrlsBorder, margin: "0 2px" } }),
4764
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { onClick: deleteSelected, style: { ...ctrlBtn("transparent", isDark), color: "#ef4444", border: `1px solid ${isDark ? "#7f1d1d" : "#fca5a5"}` }, children: selectedSet.size > 1 ? `Delete (${selectedSet.size})` : "Delete" })
4223
4765
  ] }),
4224
- liveEdge && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: { fontSize: 11, color: acc.color, fontWeight: 600, marginLeft: 6 }, children: [
4766
+ liveEdge && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { style: { fontSize: 11, color: acc.color, fontWeight: 600, marginLeft: 6 }, children: [
4225
4767
  liveEdge.answerLabel ? `Routing "${liveEdge.answerLabel}" \u2192` : "Drop on a node to connect",
4226
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: { fontWeight: 400, color: t.textMuted, marginLeft: 6 }, children: "release to cancel" })
4768
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: { fontWeight: 400, color: t.textMuted, marginLeft: 6 }, children: "release to cancel" })
4227
4769
  ] }),
4228
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
4770
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
4229
4771
  variant === "question" ? "drag answer port to connect \xB7 " : "drag port dot \xB7 ",
4230
4772
  "scroll to zoom \xB7 drag to pan"
4231
4773
  ] })
4232
4774
  ] }),
4233
- variant !== "flowchart" && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { padding: "3px 14px", background: acc.fill, borderBottom: `1px solid ${acc.border}`, fontSize: 11, color: acc.color, fontWeight: 600 }, children: variant === "question" ? "? Question Flow \u2014 add answers in the panel, drag their port to connect" : "\u2197 Journey Map \u2014 numbered steps, drag port to sequence" }),
4234
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: STYLE_FLEX_ROW, children: [
4235
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4775
+ variant !== "flowchart" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: { padding: "3px 14px", background: acc.fill, borderBottom: `1px solid ${acc.border}`, fontSize: 11, color: acc.color, fontWeight: 600 }, children: variant === "question" ? "? Question Flow \u2014 add answers in the panel, drag their port to connect" : "\u2197 Journey Map \u2014 numbered steps, drag port to sequence" }),
4776
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: STYLE_FLEX_ROW, children: [
4777
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
4236
4778
  NodeNavigator,
4237
4779
  {
4238
4780
  model,
@@ -4246,323 +4788,162 @@ function FlowchartEditor({
4246
4788
  onSelect: jumpToNode
4247
4789
  }
4248
4790
  ),
4249
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { ref: containerRef, style: { flex: 1, overflow: "hidden", position: "relative", background: t.canvas }, children: [
4250
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
4251
- "svg",
4252
- {
4253
- ref: svgRef,
4254
- width: "100%",
4255
- height: "100%",
4256
- role: "application",
4257
- "aria-label": `${variantLabel} diagram editor. ${model.nodes.length} ${variantLabel.toLowerCase()}s, ${model.edges.length} connections. Scroll to zoom, drag to pan, click a ${variantLabel.toLowerCase()} to select.`,
4258
- tabIndex: 0,
4259
- style: { display: "block", cursor: pan ? "grabbing" : drag ? "grabbing" : liveEdge ? "crosshair" : "default", userSelect: "none", outline: "none" },
4260
- onMouseDown: onSvgMouseDown,
4261
- onMouseMove,
4262
- onMouseUp,
4263
- onMouseLeave: onMouseUp,
4264
- onContextMenu: onSvgContextMenu,
4265
- children: [
4266
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("defs", { children: [
4267
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("style", { children: reducedMotion ? `
4268
- .edge-flow { stroke-dasharray: 0; }
4269
- .edge-flow-amber { stroke-dasharray: 0; }
4270
- .edge-live { stroke-dasharray: 4 4; }
4271
- ` : `
4272
- @keyframes edgeFlow { to { stroke-dashoffset: -13; } }
4273
- @keyframes edgeFlowFast { to { stroke-dashoffset: -13; } }
4274
- .edge-flow { stroke-dasharray: 8 5; animation: edgeFlow 0.9s linear infinite; }
4275
- .edge-flow-amber { stroke-dasharray: 6 4; animation: edgeFlowFast 0.65s linear infinite; }
4276
- .edge-live { stroke-dasharray: 7 5; animation: edgeFlow 0.55s linear infinite; }
4277
- ` }),
4278
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("pattern", { id: "dots", width: GRID, height: GRID, patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("circle", { cx: GRID / 2, cy: GRID / 2, r: 1.1, fill: t.dot }) }),
4279
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("filter", { id: "nodeShadow", x: "-25%", y: "-25%", width: "150%", height: "160%", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("feDropShadow", { dx: "0", dy: "3", stdDeviation: "5", floodColor: shadowColor, floodOpacity: "1" }) }),
4280
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("marker", { id: "arrowhead", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: arrowColor }) }),
4281
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("marker", { id: "arrowAmber", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: amberArrow }) }),
4282
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("marker", { id: "arrowLive", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: acc.color }) })
4283
- ] }),
4284
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("rect", { width: "100%", height: "100%", fill: "url(#dots)", "data-bg": "1" }),
4285
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("g", { transform: `translate(${transform.x},${transform.y}) scale(${transform.scale})`, children: [
4286
- model.edges.map((e) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4287
- EdgeLine,
4288
- {
4289
- edge: e,
4290
- nodes: model.nodes,
4291
- variant,
4292
- t,
4293
- isDark,
4294
- acc,
4295
- editing: editingEdgeId === e.id,
4296
- editValue: editEdgeLabel,
4297
- onEditChange: setEditEdgeLabel,
4298
- onEditCommit: commitEdgeEdit,
4299
- onEditCancel: () => setEditingEdgeId(null),
4300
- onDoubleClick: beginEditEdge,
4301
- onContextMenu: onEdgeContextMenu,
4302
- onWaypointDown: (ev, edgeId) => setWaypointDrag(edgeId)
4303
- },
4304
- e.id
4305
- )),
4306
- liveEdge && (() => {
4307
- const d = bezierPath2(liveEdge.fromX, liveEdge.fromY, liveEdge.toX, liveEdge.toY, liveEdge.exitDir);
4308
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d, fill: "none", stroke: acc.color, strokeWidth: 2, strokeLinecap: "round", className: "edge-live", opacity: 0.8, markerEnd: "url(#arrowLive)" });
4309
- })(),
4310
- alignGuides?.x && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4311
- "line",
4312
- {
4313
- x1: alignGuides.x.pos,
4314
- x2: alignGuides.x.pos,
4315
- y1: alignGuides.x.minY,
4316
- y2: alignGuides.x.maxY,
4317
- stroke: acc.color,
4318
- strokeWidth: 1 / transform.scale,
4319
- strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
4320
- opacity: 0.85,
4321
- pointerEvents: "none"
4322
- }
4323
- ),
4324
- alignGuides?.y && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4325
- "line",
4326
- {
4327
- y1: alignGuides.y.pos,
4328
- y2: alignGuides.y.pos,
4329
- x1: alignGuides.y.minX,
4330
- x2: alignGuides.y.maxX,
4331
- stroke: acc.color,
4332
- strokeWidth: 1 / transform.scale,
4333
- strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
4334
- opacity: 0.85,
4335
- pointerEvents: "none"
4336
- }
4337
- ),
4338
- model.nodes.map((node, idx) => {
4339
- const isHovered = hoveredId === node.id;
4340
- const isQuestion2 = variant === "question";
4341
- const { w: nW, h: nH } = nodeDims(node, variant);
4342
- const isSelected = selectedSet.has(node.id);
4343
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
4344
- "g",
4345
- {
4346
- transform: `translate(${node.x ?? 0},${node.y ?? 0})`,
4347
- role: "button",
4348
- "aria-label": `${variantLabel} ${variant === "journey" ? idx + 1 + ": " : ""}${node.label}${isSelected ? ", selected" : ""}`,
4349
- style: { cursor: drag?.nodeId === node.id ? "grabbing" : "grab" },
4350
- onMouseDown: (e) => onNodeMouseDown(e, node.id),
4351
- onMouseUp: (e) => onNodeMouseUp(e, node.id),
4352
- onDoubleClick: (e) => onNodeDblClick(e, node.id),
4353
- onContextMenu: (e) => onNodeContextMenu(e, node.id),
4354
- onMouseEnter: () => setHoveredId(node.id),
4355
- onMouseLeave: () => setHoveredId(null),
4356
- children: [
4357
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("title", { children: `${variantLabel}: ${node.label}` }),
4358
- isQuestion2 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(QuestionNode, { node, selected: isSelected, edges: model.edges, isDark, onAnswerPortDown, qW: nW }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
4359
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(NodeShape, { node, selected: isSelected, variant, stepNumber: variant === "journey" ? idx + 1 : void 0, t, isDark, w: nW }),
4360
- editingId === node.id ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("foreignObject", { x: 6, y: 6, width: nW - 12, height: NODE_H2 - 12, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4361
- "input",
4362
- {
4363
- autoFocus: true,
4364
- value: editLabel,
4365
- onChange: (e) => setEditLabel(e.target.value),
4366
- onBlur: commitEdit,
4367
- onKeyDown: (e) => {
4368
- if (e.key === "Enter") commitEdit();
4369
- if (e.key === "Escape") setEditingId(null);
4370
- },
4371
- style: { width: "100%", height: "100%", border: "none", borderRadius: 6, outline: `2px solid ${acc.color}`, textAlign: "center", fontSize: 13, fontWeight: 500, background: t.inputBg, boxSizing: "border-box", padding: "0 6px", fontFamily: "inherit", color: t.inputText }
4372
- }
4373
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("text", { x: nW / 2, y: NODE_H2 / 2 + 5, textAnchor: "middle", fontSize: 13, fontWeight: "500", fontFamily: "ui-sans-serif,system-ui,sans-serif", fill: isSelected ? acc.color : t.textPrimary, style: STYLE_LABEL2, children: node.label }),
4374
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4375
- "circle",
4376
- {
4377
- cx: nW / 2,
4378
- cy: NODE_H2 + 1,
4379
- r: portR,
4380
- fill: acc.color,
4381
- stroke: isDark ? "#0f172a" : "white",
4382
- strokeWidth: 2,
4383
- style: { cursor: "crosshair", opacity: isHovered || isCoarse ? 1 : 0, transition: "opacity 0.15s", pointerEvents: isHovered || isCoarse ? "all" : "none", filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.25))" },
4384
- onMouseDown: (e) => onPortMouseDown(e, node.id)
4385
- }
4386
- )
4387
- ] }),
4388
- liveEdge && liveEdge.fromId !== node.id && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("circle", { cx: nW / 2, cy: -1, r: portR, fill: acc.color, stroke: isDark ? "#0f172a" : "white", strokeWidth: 2, style: STYLE_LIVE_PORT })
4389
- ]
4390
- },
4391
- node.id
4392
- );
4393
- })
4394
- ] })
4395
- ]
4396
- }
4397
- ),
4398
- boxSel && Math.abs(boxSel.cx - boxSel.sx) + Math.abs(boxSel.cy - boxSel.sy) > 4 && containerRef.current && (() => {
4399
- const rect = containerRef.current.getBoundingClientRect();
4400
- const left = Math.min(boxSel.sx, boxSel.cx) - rect.left;
4401
- const top = Math.min(boxSel.sy, boxSel.cy) - rect.top;
4402
- const w = Math.abs(boxSel.cx - boxSel.sx);
4403
- const h = Math.abs(boxSel.cy - boxSel.sy);
4404
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4405
- "div",
4406
- {
4407
- style: {
4408
- position: "absolute",
4409
- left,
4410
- top,
4411
- width: w,
4412
- height: h,
4413
- border: `1px dashed ${acc.color}`,
4414
- background: isDark ? "rgba(99,102,241,0.10)" : "rgba(99,102,241,0.08)",
4415
- pointerEvents: "none",
4416
- borderRadius: 4
4417
- }
4791
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
4792
+ DiagramCanvas,
4793
+ {
4794
+ model,
4795
+ variant,
4796
+ variantLabel,
4797
+ t,
4798
+ isDark,
4799
+ acc,
4800
+ transform,
4801
+ setTransform,
4802
+ selected,
4803
+ selectedSet,
4804
+ hoveredId,
4805
+ setHoveredId,
4806
+ drag,
4807
+ pan,
4808
+ liveEdge,
4809
+ boxSel,
4810
+ alignGuides,
4811
+ editingEdgeId,
4812
+ editEdgeLabel,
4813
+ setEditEdgeLabel,
4814
+ commitEdgeEdit,
4815
+ setEditingEdgeId,
4816
+ beginEditEdge,
4817
+ onEdgeContextMenu,
4818
+ setWaypointDrag,
4819
+ editingId,
4820
+ editLabel,
4821
+ setEditLabel,
4822
+ commitEdit,
4823
+ setEditingId,
4824
+ onNodeMouseDown,
4825
+ onNodeMouseUp,
4826
+ onNodeDblClick,
4827
+ onNodeContextMenu,
4828
+ onPortMouseDown,
4829
+ onAnswerPortDown,
4830
+ onSvgMouseDown,
4831
+ onMouseMove,
4832
+ onMouseUp,
4833
+ onSvgContextMenu,
4834
+ reducedMotion,
4835
+ isCoarse,
4836
+ portR,
4837
+ shadowClr,
4838
+ arrowClr,
4839
+ amberArrow,
4840
+ viewport,
4841
+ svgRef,
4842
+ containerRef,
4843
+ ctxMenu,
4844
+ history,
4845
+ ctxEdgeStyle: (ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.style ?? "solid",
4846
+ ctxEdgeArrow: (ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.arrowhead ?? "arrow",
4847
+ ctxEdgeHasWaypoint: !!(ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.waypoint,
4848
+ onCtxUndo: () => {
4849
+ undo();
4850
+ setCtxMenu(null);
4851
+ },
4852
+ onCtxRedo: () => {
4853
+ redo();
4854
+ setCtxMenu(null);
4855
+ },
4856
+ onCtxReCenter: () => {
4857
+ reCenter();
4858
+ setCtxMenu(null);
4859
+ },
4860
+ onCtxAddNode: () => {
4861
+ const rect = svgRef.current.getBoundingClientRect();
4862
+ const cx = (ctxMenu.x - rect.left - transform.x) / transform.scale;
4863
+ const cy = (ctxMenu.y - rect.top - transform.y) / transform.scale;
4864
+ addNode({ x: cx, y: cy });
4865
+ setCtxMenu(null);
4866
+ },
4867
+ onCtxDuplicate: () => {
4868
+ if (ctxMenu?.nodeId) {
4869
+ duplicateNode(ctxMenu.nodeId);
4870
+ setCtxMenu(null);
4418
4871
  }
4419
- );
4420
- })(),
4421
- model.nodes.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", pointerEvents: "none", gap: 8 }, children: [
4422
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { fontSize: 36, opacity: 0.1, color: t.textPrimary }, children: variant === "question" ? "?" : variant === "journey" ? "\u2197" : "\u2B21" }),
4423
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { fontSize: 13, color: t.textMuted, fontWeight: 500 }, children: [
4424
- "Click ",
4425
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("strong", { style: { color: acc.color }, children: [
4426
- "+ ",
4427
- variantLabel
4428
- ] }),
4429
- " to start"
4430
- ] })
4431
- ] }),
4432
- model.nodes.length > 0 && viewport.w > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4433
- Minimap,
4434
- {
4435
- model,
4436
- viewportW: viewport.w,
4437
- viewportH: viewport.h,
4438
- transform,
4439
- isDark,
4440
- accentColor: acc.color,
4441
- measureNode: (n) => nodeDims(n, variant),
4442
- onCenterOn: (cx, cy) => {
4443
- setTransform((tr) => ({ ...tr, x: viewport.w / 2 - cx * tr.scale, y: viewport.h / 2 - cy * tr.scale }));
4872
+ },
4873
+ onCtxRename: () => {
4874
+ if (ctxMenu?.nodeId) {
4875
+ const node = model.nodes.find((n) => n.id === ctxMenu.nodeId);
4876
+ setEditingId(ctxMenu.nodeId);
4877
+ setEditLabel(node.label);
4878
+ setCtxMenu(null);
4444
4879
  }
4445
- }
4446
- ),
4447
- ctxMenu && (() => {
4448
- const ctxEdge = ctxMenu.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0;
4449
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4450
- ContextMenu,
4451
- {
4452
- x: ctxMenu.x,
4453
- y: ctxMenu.y,
4454
- nodeId: ctxMenu.nodeId,
4455
- edgeId: ctxMenu.edgeId,
4456
- isDark,
4457
- t,
4458
- acc,
4459
- canUndo: history.canUndo,
4460
- canRedo: history.canRedo,
4461
- onUndo: () => {
4462
- undo();
4463
- setCtxMenu(null);
4464
- },
4465
- onRedo: () => {
4466
- redo();
4467
- setCtxMenu(null);
4468
- },
4469
- onReCenter: () => {
4470
- reCenter();
4471
- setCtxMenu(null);
4472
- },
4473
- onAddNode: () => {
4474
- const rect = svgRef.current.getBoundingClientRect();
4475
- const cx = (ctxMenu.x - rect.left - transform.x) / transform.scale;
4476
- const cy = (ctxMenu.y - rect.top - transform.y) / transform.scale;
4477
- addNode({ x: cx, y: cy });
4478
- setCtxMenu(null);
4479
- },
4480
- onDuplicate: () => {
4481
- if (ctxMenu.nodeId) {
4482
- duplicateNode(ctxMenu.nodeId);
4483
- setCtxMenu(null);
4484
- }
4485
- },
4486
- onRename: () => {
4487
- if (ctxMenu.nodeId) {
4488
- const node = model.nodes.find((n) => n.id === ctxMenu.nodeId);
4489
- setEditingId(ctxMenu.nodeId);
4490
- setEditLabel(node.label);
4491
- setCtxMenu(null);
4492
- }
4493
- },
4494
- onDelete: () => {
4495
- if (ctxMenu.nodeId) {
4496
- deleteNode(ctxMenu.nodeId);
4497
- setCtxMenu(null);
4498
- }
4499
- },
4500
- onDisconnect: () => {
4501
- if (ctxMenu.nodeId) {
4502
- const m = { ...model, edges: model.edges.filter((e) => e.from !== ctxMenu.nodeId && e.to !== ctxMenu.nodeId) };
4503
- applyAndPush(m);
4504
- setCtxMenu(null);
4505
- }
4506
- },
4507
- currentEdgeStyle: ctxEdge?.style ?? "solid",
4508
- currentEdgeArrow: ctxEdge?.arrowhead ?? "arrow",
4509
- edgeHasWaypoint: !!ctxEdge?.waypoint,
4510
- onEdgeRename: () => {
4511
- if (ctxMenu.edgeId) {
4512
- beginEditEdge(ctxMenu.edgeId);
4513
- setCtxMenu(null);
4514
- }
4515
- },
4516
- onEdgeStyle: (s2) => {
4517
- if (ctxMenu.edgeId) {
4518
- setEdgeStyle(ctxMenu.edgeId, s2);
4519
- setCtxMenu(null);
4520
- }
4521
- },
4522
- onEdgeArrowhead: (a) => {
4523
- if (ctxMenu.edgeId) {
4524
- setEdgeArrowhead(ctxMenu.edgeId, a);
4525
- setCtxMenu(null);
4526
- }
4527
- },
4528
- onEdgeDelete: () => {
4529
- if (ctxMenu.edgeId) {
4530
- deleteEdge(ctxMenu.edgeId);
4531
- setCtxMenu(null);
4532
- }
4533
- },
4534
- onEdgeResetRouting: () => {
4535
- if (ctxMenu.edgeId) {
4536
- resetEdgeRouting(ctxMenu.edgeId);
4537
- setCtxMenu(null);
4538
- }
4539
- },
4540
- containerRef
4880
+ },
4881
+ onCtxDelete: () => {
4882
+ if (ctxMenu?.nodeId) {
4883
+ deleteNode(ctxMenu.nodeId);
4884
+ setCtxMenu(null);
4541
4885
  }
4542
- );
4543
- })()
4544
- ] }),
4545
- selected && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(StepEditor, { nodeId: selected, model, onModelChange: (m) => {
4886
+ },
4887
+ onCtxDisconnect: () => {
4888
+ if (ctxMenu?.nodeId) {
4889
+ const m = { ...model, edges: model.edges.filter((e) => e.from !== ctxMenu.nodeId && e.to !== ctxMenu.nodeId) };
4890
+ applyAndPush(m);
4891
+ setCtxMenu(null);
4892
+ }
4893
+ },
4894
+ onCtxEdgeRename: () => {
4895
+ if (ctxMenu?.edgeId) {
4896
+ beginEditEdge(ctxMenu.edgeId);
4897
+ setCtxMenu(null);
4898
+ }
4899
+ },
4900
+ onCtxEdgeStyle: (s2) => {
4901
+ if (ctxMenu?.edgeId) {
4902
+ setEdgeStyle(ctxMenu.edgeId, s2);
4903
+ setCtxMenu(null);
4904
+ }
4905
+ },
4906
+ onCtxEdgeArrowhead: (a) => {
4907
+ if (ctxMenu?.edgeId) {
4908
+ setEdgeArrowhead(ctxMenu.edgeId, a);
4909
+ setCtxMenu(null);
4910
+ }
4911
+ },
4912
+ onCtxEdgeDelete: () => {
4913
+ if (ctxMenu?.edgeId) {
4914
+ deleteEdge(ctxMenu.edgeId);
4915
+ setCtxMenu(null);
4916
+ }
4917
+ },
4918
+ onCtxEdgeResetRouting: () => {
4919
+ if (ctxMenu?.edgeId) {
4920
+ resetEdgeRouting(ctxMenu.edgeId);
4921
+ setCtxMenu(null);
4922
+ }
4923
+ }
4924
+ }
4925
+ ),
4926
+ selected && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(StepEditor, { nodeId: selected, model, onModelChange: (m) => {
4546
4927
  applyAndPush(m);
4547
4928
  }, variant, isDark, t, acc }, selected)
4548
4929
  ] }),
4549
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { padding: "4px 14px", fontSize: 11, color: t.textMuted, background: t.statusBg, borderTop: `1px solid ${t.ctrlsBorder}`, display: "flex", gap: 16 }, children: [
4550
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { children: [
4930
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { padding: "4px 14px", fontSize: 11, color: t.textMuted, background: t.statusBg, borderTop: `1px solid ${t.ctrlsBorder}`, display: "flex", gap: 16, flexWrap: "wrap", overflow: "hidden", maxHeight: 28 }, children: [
4931
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
4551
4932
  model.nodes.length,
4552
4933
  " ",
4553
4934
  variantLabel.toLowerCase(),
4554
4935
  "s"
4555
4936
  ] }),
4556
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { children: [
4937
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
4557
4938
  model.edges.length,
4558
4939
  " connections"
4559
4940
  ] }),
4560
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { children: [
4941
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
4561
4942
  Math.round(transform.scale * 100),
4562
4943
  "% zoom"
4563
4944
  ] }),
4564
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: { marginLeft: "auto" }, children: "Ctrl+Z undo \xB7 Ctrl+Y redo \xB7 Ctrl+0 fit \xB7 Alt+Arrow traverse" }),
4565
- selected && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: { color: acc.color }, children: model.nodes.find((n) => n.id === selected)?.label })
4945
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: { marginLeft: "auto", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: "Ctrl+Z undo \xB7 Ctrl+Y redo \xB7 Ctrl+0 fit \xB7 Alt+Arrow traverse" }),
4946
+ selected && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: { color: acc.color }, children: model.nodes.find((n) => n.id === selected)?.label })
4566
4947
  ] })
4567
4948
  ] });
4568
4949
  }