flowchart-sequence-designer 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/ui/DiagramEditor.tsx
2
- import { useCallback as useCallback7, useEffect as useEffect9, useRef as useRef7, useState as useState11 } from "react";
2
+ import { useCallback as useCallback7, useEffect as useEffect10, useRef as useRef7, useState as useState11 } from "react";
3
3
 
4
4
  // src/ui/Toolbar.tsx
5
5
  import { useState as useState2 } from "react";
@@ -375,6 +375,12 @@ var ACCENT = {
375
375
  emeraldDarkLight: "rgba(16,185,129,0.12)",
376
376
  emeraldDarkBorder: "rgba(16,185,129,0.3)"
377
377
  };
378
+ function shadowColor(isDark) {
379
+ return isDark ? "rgba(0,0,0,0.55)" : "rgba(15,23,42,0.09)";
380
+ }
381
+ function arrowColor(isDark) {
382
+ return isDark ? "#64748b" : "#94a3b8";
383
+ }
378
384
  function variantAccent(variant, isDark) {
379
385
  if (variant === "question") {
380
386
  return isDark ? { color: ACCENT.amberDark, fill: ACCENT.amberDarkLight, border: ACCENT.amberDarkBorder, glow: ACCENT.amberGlow } : { color: ACCENT.amber, fill: ACCENT.amberLight, border: ACCENT.amberBorder, glow: ACCENT.amberGlow };
@@ -807,10 +813,270 @@ function StepEditor({ nodeId, model, onModelChange, variant = "flowchart", isDar
807
813
  }
808
814
 
809
815
  // src/ui/SequenceEditor.tsx
810
- import { useCallback as useCallback4, useEffect as useEffect4, useMemo as useMemo2, useRef as useRef3, useState as useState5 } from "react";
816
+ import { useCallback as useCallback4, useEffect as useEffect5, useMemo as useMemo3, useRef as useRef3, useState as useState5 } from "react";
811
817
 
812
- // src/ui/hooks/useEditorTheme.ts
818
+ // src/ui/SequenceCanvas.tsx
813
819
  import { useMemo } from "react";
820
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
821
+ var HEADER_H = 64;
822
+ var HEADER_PAD = 24;
823
+ var ROW_H = 64;
824
+ var SIDE_PAD = 40;
825
+ var INDIGO = "#4f46e5";
826
+ var INDIGO_SOFT = "#eef2ff";
827
+ var STYLE_SEQ_GRAB = { cursor: "grab" };
828
+ var STYLE_SEQ_GRABBING = { cursor: "grabbing" };
829
+ var STYLE_SEQ_ACTOR_TEXT = { cursor: "pointer", userSelect: "none" };
830
+ var STYLE_SEQ_REMOVE_BTN = { cursor: "pointer" };
831
+ var STYLE_SEQ_REMOVE_ICON = { pointerEvents: "none", userSelect: "none" };
832
+ var STYLE_SEQ_DRAGGING = { opacity: 0.85 };
833
+ function estimateW(text, pxPerChar = 7) {
834
+ return text.length * pxPerChar;
835
+ }
836
+ function SequenceCanvas(props) {
837
+ const {
838
+ model,
839
+ actors,
840
+ messages,
841
+ t,
842
+ isDark,
843
+ colW,
844
+ totalW,
845
+ totalH,
846
+ actorX,
847
+ msgY,
848
+ selected,
849
+ editingId,
850
+ setEditingId,
851
+ drag,
852
+ onRowMouseDown,
853
+ renameActor,
854
+ removeActor,
855
+ svgRef
856
+ } = props;
857
+ const visualMessages = useMemo(() => {
858
+ if (!drag?.active) return messages;
859
+ const idx = messages.findIndex((m) => m.id === drag.id);
860
+ if (idx < 0) return messages;
861
+ const next = messages.slice();
862
+ const [moved] = next.splice(idx, 1);
863
+ next.splice(drag.targetIdx, 0, moved);
864
+ return next;
865
+ }, [messages, drag]);
866
+ if (actors.length === 0 && messages.length === 0) {
867
+ return /* @__PURE__ */ jsxs4("div", { style: {
868
+ position: "absolute",
869
+ inset: 0,
870
+ display: "flex",
871
+ flexDirection: "column",
872
+ alignItems: "center",
873
+ justifyContent: "center",
874
+ gap: 10,
875
+ color: t.textMuted,
876
+ pointerEvents: "none"
877
+ }, children: [
878
+ /* @__PURE__ */ jsx4("div", { style: { fontSize: 36, opacity: 0.15, color: t.textPrimary }, children: "\u2194" }),
879
+ /* @__PURE__ */ jsxs4("div", { style: { fontSize: 13, fontWeight: 500 }, children: [
880
+ "Click ",
881
+ /* @__PURE__ */ jsx4("strong", { style: { color: INDIGO }, children: "+ Actor" }),
882
+ " then ",
883
+ /* @__PURE__ */ jsx4("strong", { style: { color: INDIGO }, children: "+ Message" }),
884
+ " to start"
885
+ ] })
886
+ ] });
887
+ }
888
+ return /* @__PURE__ */ jsxs4(
889
+ "svg",
890
+ {
891
+ ref: svgRef,
892
+ width: totalW,
893
+ height: totalH,
894
+ style: { display: "block", cursor: drag?.active ? "grabbing" : "default", userSelect: "none" },
895
+ children: [
896
+ /* @__PURE__ */ jsxs4("defs", { children: [
897
+ /* @__PURE__ */ jsx4("pattern", { id: "seqdots", x: "0", y: "0", width: "24", height: "24", patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ jsx4("circle", { cx: 12, cy: 12, r: 1.1, fill: t.dot }) }),
898
+ /* @__PURE__ */ jsx4("filter", { id: "seqShadow", x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ jsx4("feDropShadow", { dx: 0, dy: 3, stdDeviation: 5, floodColor: shadowColor(isDark) }) }),
899
+ /* @__PURE__ */ jsx4("marker", { id: "seqArrow", markerWidth: 9, markerHeight: 7, refX: 8.5, refY: 3.5, orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ jsx4("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: t.arrow }) })
900
+ ] }),
901
+ /* @__PURE__ */ jsx4("rect", { width: totalW, height: totalH, fill: "url(#seqdots)" }),
902
+ actors.map((name) => {
903
+ const x = actorX(name);
904
+ const top = HEADER_PAD + HEADER_H;
905
+ return /* @__PURE__ */ jsx4(
906
+ "line",
907
+ {
908
+ x1: x,
909
+ x2: x,
910
+ y1: top + 4,
911
+ y2: totalH - 24,
912
+ stroke: t.lifeline,
913
+ strokeWidth: 1.25,
914
+ strokeDasharray: "5 5"
915
+ },
916
+ `life-${name}`
917
+ );
918
+ }),
919
+ visualMessages.map((msg, idx) => {
920
+ const y = msgY(idx);
921
+ const fromX = actorX(msg.from);
922
+ const toX = actorX(msg.to);
923
+ const selectedHere = selected === msg.id;
924
+ const isDragging = drag?.active && drag.id === msg.id;
925
+ const isSelf = msg.from === msg.to;
926
+ const stroke = selectedHere ? INDIGO : t.arrow;
927
+ const dash = msg.style === "dashed" ? "6,4" : void 0;
928
+ const cursorStyle = drag?.active ? STYLE_SEQ_GRABBING : STYLE_SEQ_GRAB;
929
+ const groupStyle = isDragging ? { ...cursorStyle, ...STYLE_SEQ_DRAGGING } : cursorStyle;
930
+ if (isSelf) {
931
+ const startX = fromX;
932
+ const loopW = 36;
933
+ const loopY = y - 6;
934
+ const d = `M ${startX} ${loopY} C ${startX + loopW} ${loopY}, ${startX + loopW} ${loopY + 24}, ${startX} ${loopY + 24}`;
935
+ return /* @__PURE__ */ jsxs4("g", { onMouseDown: (e) => onRowMouseDown(e, msg.id), style: groupStyle, children: [
936
+ (selectedHere || isDragging) && /* @__PURE__ */ jsx4(
937
+ "rect",
938
+ {
939
+ x: SIDE_PAD - 8,
940
+ y: y - 22,
941
+ width: totalW - (SIDE_PAD - 8) * 2,
942
+ height: ROW_H - 12,
943
+ rx: 10,
944
+ fill: INDIGO_SOFT,
945
+ opacity: isDark ? 0.18 : 0.6
946
+ }
947
+ ),
948
+ /* @__PURE__ */ jsx4("path", { d, fill: "none", stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
949
+ /* @__PURE__ */ jsx4("text", { x: startX + loopW + 8, y: loopY + 16, fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
950
+ ] }, msg.id);
951
+ }
952
+ const labelX = (fromX + toX) / 2;
953
+ return /* @__PURE__ */ jsxs4("g", { onMouseDown: (e) => onRowMouseDown(e, msg.id), style: groupStyle, children: [
954
+ (selectedHere || isDragging) && /* @__PURE__ */ jsx4(
955
+ "rect",
956
+ {
957
+ x: SIDE_PAD - 8,
958
+ y: y - 22,
959
+ width: totalW - (SIDE_PAD - 8) * 2,
960
+ height: ROW_H - 12,
961
+ rx: 10,
962
+ fill: INDIGO_SOFT,
963
+ opacity: isDark ? 0.18 : 0.6
964
+ }
965
+ ),
966
+ /* @__PURE__ */ jsx4("line", { x1: fromX, y1: y, x2: toX, y2: y, stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
967
+ /* @__PURE__ */ jsx4(
968
+ "rect",
969
+ {
970
+ x: labelX - estimateW(msg.label) / 2 - 6,
971
+ y: y - 18,
972
+ width: estimateW(msg.label) + 12,
973
+ height: 18,
974
+ rx: 6,
975
+ fill: t.canvas,
976
+ stroke: selectedHere ? INDIGO : t.cardBorder,
977
+ strokeWidth: selectedHere ? 1.25 : 1
978
+ }
979
+ ),
980
+ /* @__PURE__ */ jsx4("text", { x: labelX, y: y - 5, textAnchor: "middle", fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
981
+ ] }, msg.id);
982
+ }),
983
+ actors.map((name) => {
984
+ const x = actorX(name);
985
+ const w = colW - 24;
986
+ return /* @__PURE__ */ jsxs4("g", { children: [
987
+ /* @__PURE__ */ jsx4(
988
+ "rect",
989
+ {
990
+ x: x - w / 2,
991
+ y: HEADER_PAD,
992
+ width: w,
993
+ height: HEADER_H,
994
+ rx: 12,
995
+ fill: t.actorFill,
996
+ stroke: t.actorStroke,
997
+ strokeWidth: 1.25,
998
+ filter: "url(#seqShadow)"
999
+ }
1000
+ ),
1001
+ editingId === name ? /* @__PURE__ */ jsx4("foreignObject", { x: x - w / 2 + 8, y: HEADER_PAD + 16, width: w - 16, height: 32, children: /* @__PURE__ */ jsx4(
1002
+ "input",
1003
+ {
1004
+ autoFocus: true,
1005
+ defaultValue: name,
1006
+ onBlur: (e) => {
1007
+ renameActor(name, e.currentTarget.value.trim());
1008
+ setEditingId(null);
1009
+ },
1010
+ onKeyDown: (e) => {
1011
+ if (e.key === "Enter") {
1012
+ renameActor(name, e.target.value.trim());
1013
+ setEditingId(null);
1014
+ }
1015
+ if (e.key === "Escape") setEditingId(null);
1016
+ },
1017
+ style: {
1018
+ width: "100%",
1019
+ height: "100%",
1020
+ border: "none",
1021
+ borderRadius: 6,
1022
+ outline: `2px solid ${INDIGO}`,
1023
+ textAlign: "center",
1024
+ fontSize: 13,
1025
+ fontWeight: 600,
1026
+ background: t.inputBg,
1027
+ color: t.inputText,
1028
+ boxSizing: "border-box",
1029
+ padding: "0 6px",
1030
+ fontFamily: "inherit"
1031
+ }
1032
+ }
1033
+ ) }) : /* @__PURE__ */ jsx4(
1034
+ "text",
1035
+ {
1036
+ x,
1037
+ y: HEADER_PAD + HEADER_H / 2 + 4,
1038
+ textAnchor: "middle",
1039
+ fontSize: 13,
1040
+ fontWeight: 700,
1041
+ fill: t.actorText,
1042
+ style: STYLE_SEQ_ACTOR_TEXT,
1043
+ onDoubleClick: () => setEditingId(name),
1044
+ children: name
1045
+ }
1046
+ ),
1047
+ /* @__PURE__ */ jsx4(
1048
+ "circle",
1049
+ {
1050
+ cx: x + w / 2 - 12,
1051
+ cy: HEADER_PAD + 14,
1052
+ r: 9,
1053
+ fill: "transparent",
1054
+ style: STYLE_SEQ_REMOVE_BTN,
1055
+ onClick: () => removeActor(name),
1056
+ children: /* @__PURE__ */ jsx4("title", { children: "Remove actor" })
1057
+ }
1058
+ ),
1059
+ /* @__PURE__ */ jsx4(
1060
+ "text",
1061
+ {
1062
+ x: x + w / 2 - 12,
1063
+ y: HEADER_PAD + 18,
1064
+ textAnchor: "middle",
1065
+ fontSize: 12,
1066
+ fill: t.textMuted,
1067
+ style: STYLE_SEQ_REMOVE_ICON,
1068
+ children: "\xD7"
1069
+ }
1070
+ )
1071
+ ] }, `hdr-${name}`);
1072
+ })
1073
+ ]
1074
+ }
1075
+ );
1076
+ }
1077
+
1078
+ // src/ui/hooks/useEditorTheme.ts
1079
+ import { useMemo as useMemo2 } from "react";
814
1080
 
815
1081
  // src/ui/hooks/useSystemTheme.ts
816
1082
  import { useEffect as useEffect3, useState as useState4 } from "react";
@@ -857,7 +1123,7 @@ function usePrefersReducedMotion() {
857
1123
  // src/ui/hooks/useEditorTheme.ts
858
1124
  function useEditorTheme(theme, overrides, palettes) {
859
1125
  const isDark = useIsDark(theme);
860
- const t = useMemo(
1126
+ const t = useMemo2(
861
1127
  () => ({ ...isDark ? palettes.dark : palettes.light, ...overrides ?? {} }),
862
1128
  // palettes is a stable module-level constant in every caller, so it is
863
1129
  // deliberately omitted from the dep array to keep the memo key tight.
@@ -1695,10 +1961,33 @@ function cloneModel(m) {
1695
1961
  };
1696
1962
  }
1697
1963
 
1964
+ // src/ui/hooks/useEditorKeyboard.ts
1965
+ import { useEffect as useEffect4 } from "react";
1966
+ var isInput = (e) => {
1967
+ const tgt = e.target;
1968
+ return !!(tgt && (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA" || tgt.isContentEditable));
1969
+ };
1970
+ function useEditorKeyboard(commands, deps) {
1971
+ useEffect4(() => {
1972
+ const onKey = (e) => {
1973
+ if (isInput(e)) return;
1974
+ for (const cmd of commands) {
1975
+ if (cmd.match(e)) {
1976
+ const handled = cmd.run(e);
1977
+ if (handled) e.preventDefault();
1978
+ return;
1979
+ }
1980
+ }
1981
+ };
1982
+ window.addEventListener("keydown", onKey);
1983
+ return () => window.removeEventListener("keydown", onKey);
1984
+ }, deps);
1985
+ }
1986
+
1698
1987
  // src/ui/SequenceEditor.tsx
1699
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1700
- var INDIGO = "#4f46e5";
1701
- var INDIGO_SOFT = "#eef2ff";
1988
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1989
+ var INDIGO2 = "#4f46e5";
1990
+ var INDIGO_SOFT2 = "#eef2ff";
1702
1991
  var lightTheme2 = {
1703
1992
  canvas: "#fafbfc",
1704
1993
  dot: "#dbe3ee",
@@ -1741,11 +2030,11 @@ var darkTheme2 = {
1741
2030
  actorStroke: "rgba(99,102,241,0.45)",
1742
2031
  actorText: "#a5b4fc"
1743
2032
  };
1744
- var HEADER_H = 64;
1745
- var HEADER_PAD = 24;
2033
+ var HEADER_H2 = 64;
2034
+ var HEADER_PAD2 = 24;
1746
2035
  var COL_MIN = 160;
1747
- var ROW_H = 64;
1748
- var SIDE_PAD = 40;
2036
+ var ROW_H2 = 64;
2037
+ var SIDE_PAD2 = 40;
1749
2038
  var DRAG_THRESHOLD = 5;
1750
2039
  function ensureSequenceModel(m) {
1751
2040
  if (m && m.type === "sequence") {
@@ -1774,18 +2063,18 @@ function SequenceEditor({
1774
2063
  const { t, isDark } = useEditorTheme(theme, themeOverrides, { light: lightTheme2, dark: darkTheme2 });
1775
2064
  const actors = model.actors ?? [];
1776
2065
  const messages = model.messages ?? [];
1777
- const colW = useMemo2(() => {
2066
+ const colW = useMemo3(() => {
1778
2067
  const longest = actors.reduce((m, a) => Math.max(m, a.length), 6);
1779
2068
  return Math.max(COL_MIN, longest * 9 + 40);
1780
2069
  }, [actors]);
1781
- const totalW = SIDE_PAD * 2 + Math.max(1, actors.length) * colW;
1782
- const totalH = HEADER_PAD + HEADER_H + 32 + messages.length * ROW_H + 48;
2070
+ const totalW = SIDE_PAD2 * 2 + Math.max(1, actors.length) * colW;
2071
+ const totalH = HEADER_PAD2 + HEADER_H2 + 32 + messages.length * ROW_H2 + 48;
1783
2072
  const actorX = (name) => {
1784
2073
  const idx = actors.indexOf(name);
1785
- if (idx < 0) return SIDE_PAD + colW / 2;
1786
- return SIDE_PAD + idx * colW + colW / 2;
2074
+ if (idx < 0) return SIDE_PAD2 + colW / 2;
2075
+ return SIDE_PAD2 + idx * colW + colW / 2;
1787
2076
  };
1788
- const msgY = (idx) => HEADER_PAD + HEADER_H + 40 + idx * ROW_H;
2077
+ const msgY = (idx) => HEADER_PAD2 + HEADER_H2 + 40 + idx * ROW_H2;
1789
2078
  const pushHistory = useCallback4((m) => {
1790
2079
  const stack = historyRef.current.slice(0, historyIdxRef.current + 1);
1791
2080
  stack.push(m);
@@ -1871,34 +2160,26 @@ function SequenceEditor({
1871
2160
  next.splice(toIdx, 0, moved);
1872
2161
  applyAndPush({ ...model, messages: next });
1873
2162
  }, [messages, model, applyAndPush]);
1874
- useEffect4(() => {
1875
- const onKey = (e) => {
1876
- const tgt = e.target;
1877
- if (tgt && (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA" || tgt.isContentEditable)) return;
1878
- const ctrl = e.ctrlKey || e.metaKey;
1879
- if (ctrl && e.key === "z") {
1880
- e.preventDefault();
1881
- undo();
1882
- return;
1883
- }
1884
- if (ctrl && (e.key === "y" || e.shiftKey && e.key === "z")) {
1885
- e.preventDefault();
1886
- redo();
1887
- return;
1888
- }
1889
- if (e.key === "Escape") {
1890
- setSelected(null);
1891
- setEditingId(null);
1892
- return;
1893
- }
1894
- if ((e.key === "Delete" || e.key === "Backspace") && selected) {
1895
- e.preventDefault();
1896
- removeMessage(selected);
1897
- }
1898
- };
1899
- window.addEventListener("keydown", onKey);
1900
- return () => window.removeEventListener("keydown", onKey);
1901
- }, [undo, redo, selected]);
2163
+ const keyCommands = [
2164
+ { match: (e) => (e.ctrlKey || e.metaKey) && e.key === "z", run: () => {
2165
+ undo();
2166
+ return true;
2167
+ } },
2168
+ { match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "y" || e.shiftKey && e.key === "z"), run: () => {
2169
+ redo();
2170
+ return true;
2171
+ } },
2172
+ { match: (e) => e.key === "Escape", run: () => {
2173
+ setSelected(null);
2174
+ setEditingId(null);
2175
+ return true;
2176
+ } },
2177
+ { match: (e) => (e.key === "Delete" || e.key === "Backspace") && !!selected, run: () => {
2178
+ removeMessage(selected);
2179
+ return true;
2180
+ } }
2181
+ ];
2182
+ useEditorKeyboard(keyCommands, [undo, redo, selected]);
1902
2183
  const handleExport = useExporters(model, onExport, "sequence");
1903
2184
  const handleImport = useImporter(applyAndPush, {
1904
2185
  expectedType: "sequence",
@@ -1913,9 +2194,9 @@ function SequenceEditor({
1913
2194
  setSelected(id);
1914
2195
  setDrag({ id, startY: e.clientY, originalIdx: idx, targetIdx: idx, active: false });
1915
2196
  };
1916
- useEffect4(() => {
2197
+ useEffect5(() => {
1917
2198
  if (!drag) return;
1918
- const baseY = HEADER_PAD + HEADER_H + 40;
2199
+ const baseY = HEADER_PAD2 + HEADER_H2 + 40;
1919
2200
  const onMove = (ev) => {
1920
2201
  const dy = ev.clientY - drag.startY;
1921
2202
  if (!drag.active && Math.abs(dy) < DRAG_THRESHOLD) return;
@@ -1923,7 +2204,7 @@ function SequenceEditor({
1923
2204
  if (!svg) return;
1924
2205
  const rect = svg.getBoundingClientRect();
1925
2206
  const yInSvg = ev.clientY - rect.top;
1926
- const raw = Math.floor((yInSvg - baseY + ROW_H / 2) / ROW_H);
2207
+ const raw = Math.floor((yInSvg - baseY + ROW_H2 / 2) / ROW_H2);
1927
2208
  const next = Math.max(0, Math.min(messages.length - 1, raw));
1928
2209
  if (next === drag.targetIdx && drag.active) return;
1929
2210
  setDrag({ ...drag, active: true, targetIdx: next });
@@ -1941,17 +2222,8 @@ function SequenceEditor({
1941
2222
  window.removeEventListener("mouseup", onUp);
1942
2223
  };
1943
2224
  }, [drag, messages.length, reorderMessage]);
1944
- const visualMessages = useMemo2(() => {
1945
- if (!drag?.active) return messages;
1946
- const idx = messages.findIndex((m) => m.id === drag.id);
1947
- if (idx < 0) return messages;
1948
- const next = messages.slice();
1949
- const [moved] = next.splice(idx, 1);
1950
- next.splice(drag.targetIdx, 0, moved);
1951
- return next;
1952
- }, [messages, drag]);
1953
2225
  const selectedMsg = selected ? messages.find((m) => m.id === selected) : null;
1954
- return /* @__PURE__ */ jsxs4("div", { style: {
2226
+ return /* @__PURE__ */ jsxs5("div", { style: {
1955
2227
  display: "flex",
1956
2228
  flexDirection: "column",
1957
2229
  height,
@@ -1959,8 +2231,8 @@ function SequenceEditor({
1959
2231
  fontFamily: "ui-sans-serif,system-ui,sans-serif",
1960
2232
  background: t.ctrlsBg
1961
2233
  }, children: [
1962
- /* @__PURE__ */ jsx4(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
1963
- /* @__PURE__ */ jsxs4("div", { style: {
2234
+ /* @__PURE__ */ jsx5(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
2235
+ /* @__PURE__ */ jsxs5("div", { style: {
1964
2236
  display: "flex",
1965
2237
  gap: 8,
1966
2238
  padding: "7px 14px",
@@ -1969,12 +2241,12 @@ function SequenceEditor({
1969
2241
  alignItems: "center",
1970
2242
  flexWrap: "wrap"
1971
2243
  }, children: [
1972
- /* @__PURE__ */ jsx4("button", { onClick: addActor, style: primaryBtn(), children: "+ Actor" }),
1973
- /* @__PURE__ */ jsx4("button", { onClick: addMessage, style: primaryBtn(), children: "+ Message" }),
1974
- /* @__PURE__ */ jsx4("div", { style: { width: 1, height: 18, background: t.ctrlsBorder, margin: "0 4px" } }),
1975
- /* @__PURE__ */ jsx4("button", { onClick: undo, style: ghostBtn2(t), title: "Undo (Ctrl+Z)", children: "\u21B6" }),
1976
- /* @__PURE__ */ jsx4("button", { onClick: redo, style: ghostBtn2(t), title: "Redo (Ctrl+Y)", children: "\u21B7" }),
1977
- /* @__PURE__ */ jsxs4("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
2244
+ /* @__PURE__ */ jsx5("button", { onClick: addActor, style: primaryBtn(), children: "+ Actor" }),
2245
+ /* @__PURE__ */ jsx5("button", { onClick: addMessage, style: primaryBtn(), children: "+ Message" }),
2246
+ /* @__PURE__ */ jsx5("div", { style: { width: 1, height: 18, background: t.ctrlsBorder, margin: "0 4px" } }),
2247
+ /* @__PURE__ */ jsx5("button", { onClick: undo, style: ghostBtn2(t), title: "Undo (Ctrl+Z)", children: "\u21B6" }),
2248
+ /* @__PURE__ */ jsx5("button", { onClick: redo, style: ghostBtn2(t), title: "Redo (Ctrl+Y)", children: "\u21B7" }),
2249
+ /* @__PURE__ */ jsxs5("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
1978
2250
  actors.length,
1979
2251
  " actor",
1980
2252
  actors.length === 1 ? "" : "s",
@@ -1985,215 +2257,31 @@ function SequenceEditor({
1985
2257
  " \xB7 drag a row to reorder"
1986
2258
  ] })
1987
2259
  ] }),
1988
- /* @__PURE__ */ jsxs4("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [
1989
- /* @__PURE__ */ jsx4("div", { style: { flex: 1, overflow: "auto", background: t.canvas, position: "relative" }, children: actors.length === 0 && messages.length === 0 ? /* @__PURE__ */ jsxs4("div", { style: {
1990
- position: "absolute",
1991
- inset: 0,
1992
- display: "flex",
1993
- flexDirection: "column",
1994
- alignItems: "center",
1995
- justifyContent: "center",
1996
- gap: 10,
1997
- color: t.textMuted,
1998
- pointerEvents: "none"
1999
- }, children: [
2000
- /* @__PURE__ */ jsx4("div", { style: { fontSize: 36, opacity: 0.15, color: t.textPrimary }, children: "\u2194" }),
2001
- /* @__PURE__ */ jsxs4("div", { style: { fontSize: 13, fontWeight: 500 }, children: [
2002
- "Click ",
2003
- /* @__PURE__ */ jsx4("strong", { style: { color: INDIGO }, children: "+ Actor" }),
2004
- " then ",
2005
- /* @__PURE__ */ jsx4("strong", { style: { color: INDIGO }, children: "+ Message" }),
2006
- " to start"
2007
- ] })
2008
- ] }) : /* @__PURE__ */ jsxs4(
2009
- "svg",
2260
+ /* @__PURE__ */ jsxs5("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [
2261
+ /* @__PURE__ */ jsx5("div", { style: { flex: 1, overflow: "auto", background: t.canvas, position: "relative" }, children: /* @__PURE__ */ jsx5(
2262
+ SequenceCanvas,
2010
2263
  {
2011
- ref: svgRef,
2012
- width: totalW,
2013
- height: totalH,
2014
- style: { display: "block", cursor: drag?.active ? "grabbing" : "default", userSelect: "none" },
2015
- children: [
2016
- /* @__PURE__ */ jsxs4("defs", { children: [
2017
- /* @__PURE__ */ jsx4("pattern", { id: "seqdots", x: "0", y: "0", width: "24", height: "24", patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ jsx4("circle", { cx: 12, cy: 12, r: 1.1, fill: t.dot }) }),
2018
- /* @__PURE__ */ jsx4("filter", { id: "seqShadow", x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ jsx4("feDropShadow", { dx: 0, dy: 3, stdDeviation: 5, floodColor: isDark ? "rgba(0,0,0,0.5)" : "rgba(15,23,42,0.09)" }) }),
2019
- /* @__PURE__ */ jsx4("marker", { id: "seqArrow", markerWidth: 9, markerHeight: 7, refX: 8.5, refY: 3.5, orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ jsx4("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: t.arrow }) })
2020
- ] }),
2021
- /* @__PURE__ */ jsx4("rect", { width: totalW, height: totalH, fill: "url(#seqdots)" }),
2022
- actors.map((name) => {
2023
- const x = actorX(name);
2024
- const top = HEADER_PAD + HEADER_H;
2025
- return /* @__PURE__ */ jsx4(
2026
- "line",
2027
- {
2028
- x1: x,
2029
- x2: x,
2030
- y1: top + 4,
2031
- y2: totalH - 24,
2032
- stroke: t.lifeline,
2033
- strokeWidth: 1.25,
2034
- strokeDasharray: "5 5"
2035
- },
2036
- `life-${name}`
2037
- );
2038
- }),
2039
- visualMessages.map((msg, idx) => {
2040
- const y = msgY(idx);
2041
- const fromX = actorX(msg.from);
2042
- const toX = actorX(msg.to);
2043
- const selectedHere = selected === msg.id;
2044
- const isDragging = drag?.active && drag.id === msg.id;
2045
- const isSelf = msg.from === msg.to;
2046
- const stroke = selectedHere ? INDIGO : t.arrow;
2047
- const dash = msg.style === "dashed" ? "6,4" : void 0;
2048
- const cursor = drag?.active ? "grabbing" : "grab";
2049
- const groupOpacity = isDragging ? 0.85 : 1;
2050
- if (isSelf) {
2051
- const startX = fromX;
2052
- const loopW = 36;
2053
- const loopY = y - 6;
2054
- const d = `M ${startX} ${loopY} C ${startX + loopW} ${loopY}, ${startX + loopW} ${loopY + 24}, ${startX} ${loopY + 24}`;
2055
- return /* @__PURE__ */ jsxs4("g", { onMouseDown: (e) => onRowMouseDown(e, msg.id), style: { cursor, opacity: groupOpacity }, children: [
2056
- (selectedHere || isDragging) && /* @__PURE__ */ jsx4(
2057
- "rect",
2058
- {
2059
- x: SIDE_PAD - 8,
2060
- y: y - 22,
2061
- width: totalW - (SIDE_PAD - 8) * 2,
2062
- height: ROW_H - 12,
2063
- rx: 10,
2064
- fill: INDIGO_SOFT,
2065
- opacity: isDark ? 0.18 : 0.6
2066
- }
2067
- ),
2068
- /* @__PURE__ */ jsx4("path", { d, fill: "none", stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
2069
- /* @__PURE__ */ jsx4("text", { x: startX + loopW + 8, y: loopY + 16, fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
2070
- ] }, msg.id);
2071
- }
2072
- const labelX = (fromX + toX) / 2;
2073
- return /* @__PURE__ */ jsxs4("g", { onMouseDown: (e) => onRowMouseDown(e, msg.id), style: { cursor, opacity: groupOpacity }, children: [
2074
- (selectedHere || isDragging) && /* @__PURE__ */ jsx4(
2075
- "rect",
2076
- {
2077
- x: SIDE_PAD - 8,
2078
- y: y - 22,
2079
- width: totalW - (SIDE_PAD - 8) * 2,
2080
- height: ROW_H - 12,
2081
- rx: 10,
2082
- fill: INDIGO_SOFT,
2083
- opacity: isDark ? 0.18 : 0.6
2084
- }
2085
- ),
2086
- /* @__PURE__ */ jsx4("line", { x1: fromX, y1: y, x2: toX, y2: y, stroke, strokeWidth: 1.5, strokeDasharray: dash, markerEnd: "url(#seqArrow)" }),
2087
- /* @__PURE__ */ jsx4(
2088
- "rect",
2089
- {
2090
- x: labelX - estimateW(msg.label) / 2 - 6,
2091
- y: y - 18,
2092
- width: estimateW(msg.label) + 12,
2093
- height: 18,
2094
- rx: 6,
2095
- fill: t.canvas,
2096
- stroke: selectedHere ? INDIGO : t.cardBorder,
2097
- strokeWidth: selectedHere ? 1.25 : 1
2098
- }
2099
- ),
2100
- /* @__PURE__ */ jsx4("text", { x: labelX, y: y - 5, textAnchor: "middle", fontSize: 11, fill: selectedHere ? INDIGO : t.textPrimary, fontWeight: 500, children: msg.label })
2101
- ] }, msg.id);
2102
- }),
2103
- actors.map((name) => {
2104
- const x = actorX(name);
2105
- const w = colW - 24;
2106
- return /* @__PURE__ */ jsxs4("g", { children: [
2107
- /* @__PURE__ */ jsx4(
2108
- "rect",
2109
- {
2110
- x: x - w / 2,
2111
- y: HEADER_PAD,
2112
- width: w,
2113
- height: HEADER_H,
2114
- rx: 12,
2115
- fill: t.actorFill,
2116
- stroke: t.actorStroke,
2117
- strokeWidth: 1.25,
2118
- filter: "url(#seqShadow)"
2119
- }
2120
- ),
2121
- editingId === name ? /* @__PURE__ */ jsx4("foreignObject", { x: x - w / 2 + 8, y: HEADER_PAD + 16, width: w - 16, height: 32, children: /* @__PURE__ */ jsx4(
2122
- "input",
2123
- {
2124
- autoFocus: true,
2125
- defaultValue: name,
2126
- onBlur: (e) => {
2127
- renameActor(name, e.currentTarget.value.trim());
2128
- setEditingId(null);
2129
- },
2130
- onKeyDown: (e) => {
2131
- if (e.key === "Enter") {
2132
- renameActor(name, e.target.value.trim());
2133
- setEditingId(null);
2134
- }
2135
- if (e.key === "Escape") setEditingId(null);
2136
- },
2137
- style: {
2138
- width: "100%",
2139
- height: "100%",
2140
- border: "none",
2141
- borderRadius: 6,
2142
- outline: `2px solid ${INDIGO}`,
2143
- textAlign: "center",
2144
- fontSize: 13,
2145
- fontWeight: 600,
2146
- background: t.inputBg,
2147
- color: t.inputText,
2148
- boxSizing: "border-box",
2149
- padding: "0 6px",
2150
- fontFamily: "inherit"
2151
- }
2152
- }
2153
- ) }) : /* @__PURE__ */ jsx4(
2154
- "text",
2155
- {
2156
- x,
2157
- y: HEADER_PAD + HEADER_H / 2 + 4,
2158
- textAnchor: "middle",
2159
- fontSize: 13,
2160
- fontWeight: 700,
2161
- fill: t.actorText,
2162
- style: { cursor: "pointer", userSelect: "none" },
2163
- onDoubleClick: () => setEditingId(name),
2164
- children: name
2165
- }
2166
- ),
2167
- /* @__PURE__ */ jsx4(
2168
- "circle",
2169
- {
2170
- cx: x + w / 2 - 12,
2171
- cy: HEADER_PAD + 14,
2172
- r: 9,
2173
- fill: "transparent",
2174
- style: { cursor: "pointer" },
2175
- onClick: () => removeActor(name),
2176
- children: /* @__PURE__ */ jsx4("title", { children: "Remove actor" })
2177
- }
2178
- ),
2179
- /* @__PURE__ */ jsx4(
2180
- "text",
2181
- {
2182
- x: x + w / 2 - 12,
2183
- y: HEADER_PAD + 18,
2184
- textAnchor: "middle",
2185
- fontSize: 12,
2186
- fill: t.textMuted,
2187
- style: { pointerEvents: "none", userSelect: "none" },
2188
- children: "\xD7"
2189
- }
2190
- )
2191
- ] }, `hdr-${name}`);
2192
- })
2193
- ]
2264
+ model,
2265
+ actors,
2266
+ messages,
2267
+ t,
2268
+ isDark,
2269
+ colW,
2270
+ totalW,
2271
+ totalH,
2272
+ actorX,
2273
+ msgY,
2274
+ selected,
2275
+ editingId,
2276
+ setEditingId,
2277
+ drag,
2278
+ onRowMouseDown,
2279
+ renameActor,
2280
+ removeActor,
2281
+ svgRef
2194
2282
  }
2195
2283
  ) }),
2196
- selectedMsg && /* @__PURE__ */ jsxs4("div", { style: {
2284
+ selectedMsg && /* @__PURE__ */ jsxs5("div", { style: {
2197
2285
  width: 280,
2198
2286
  flexShrink: 0,
2199
2287
  background: t.panelBg,
@@ -2201,9 +2289,9 @@ function SequenceEditor({
2201
2289
  padding: "14px 16px",
2202
2290
  overflowY: "auto"
2203
2291
  }, children: [
2204
- /* @__PURE__ */ jsx4("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.7, marginBottom: 10 }, children: "Message" }),
2205
- /* @__PURE__ */ jsx4(Label, { t, children: "Label" }),
2206
- /* @__PURE__ */ jsx4(
2292
+ /* @__PURE__ */ jsx5("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.7, marginBottom: 10 }, children: "Message" }),
2293
+ /* @__PURE__ */ jsx5(Label, { t, children: "Label" }),
2294
+ /* @__PURE__ */ jsx5(
2207
2295
  "input",
2208
2296
  {
2209
2297
  value: editLabel || selectedMsg.label,
@@ -2219,21 +2307,21 @@ function SequenceEditor({
2219
2307
  style: input(t)
2220
2308
  }
2221
2309
  ),
2222
- /* @__PURE__ */ jsx4(Label, { t, children: "From" }),
2223
- /* @__PURE__ */ jsx4("select", { value: selectedMsg.from, onChange: (e) => updateMessage(selectedMsg.id, { from: e.target.value }), style: input(t), children: actors.map((a) => /* @__PURE__ */ jsx4("option", { value: a, children: a }, a)) }),
2224
- /* @__PURE__ */ jsx4(Label, { t, children: "To" }),
2225
- /* @__PURE__ */ jsx4("select", { value: selectedMsg.to, onChange: (e) => updateMessage(selectedMsg.id, { to: e.target.value }), style: input(t), children: actors.map((a) => /* @__PURE__ */ jsx4("option", { value: a, children: a }, a)) }),
2226
- /* @__PURE__ */ jsx4(Label, { t, children: "Style" }),
2227
- /* @__PURE__ */ jsx4("div", { style: { display: "flex", gap: 6 }, children: ["solid", "dashed"].map((s2) => /* @__PURE__ */ jsx4(
2310
+ /* @__PURE__ */ jsx5(Label, { t, children: "From" }),
2311
+ /* @__PURE__ */ jsx5("select", { value: selectedMsg.from, onChange: (e) => updateMessage(selectedMsg.id, { from: e.target.value }), style: input(t), children: actors.map((a) => /* @__PURE__ */ jsx5("option", { value: a, children: a }, a)) }),
2312
+ /* @__PURE__ */ jsx5(Label, { t, children: "To" }),
2313
+ /* @__PURE__ */ jsx5("select", { value: selectedMsg.to, onChange: (e) => updateMessage(selectedMsg.id, { to: e.target.value }), style: input(t), children: actors.map((a) => /* @__PURE__ */ jsx5("option", { value: a, children: a }, a)) }),
2314
+ /* @__PURE__ */ jsx5(Label, { t, children: "Style" }),
2315
+ /* @__PURE__ */ jsx5("div", { style: { display: "flex", gap: 6 }, children: ["solid", "dashed"].map((s2) => /* @__PURE__ */ jsx5(
2228
2316
  "button",
2229
2317
  {
2230
2318
  onClick: () => updateMessage(selectedMsg.id, { style: s2 }),
2231
2319
  style: {
2232
2320
  flex: 1,
2233
2321
  padding: "6px 10px",
2234
- border: `1.5px solid ${selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO : t.inputBorder}`,
2235
- background: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO_SOFT : t.inputBg,
2236
- color: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO : t.textPrimary,
2322
+ border: `1.5px solid ${selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO2 : t.inputBorder}`,
2323
+ background: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO_SOFT2 : t.inputBg,
2324
+ color: selectedMsg.style === s2 || !selectedMsg.style && s2 === "solid" ? INDIGO2 : t.textPrimary,
2237
2325
  borderRadius: 8,
2238
2326
  fontSize: 12,
2239
2327
  fontWeight: 600,
@@ -2244,8 +2332,8 @@ function SequenceEditor({
2244
2332
  },
2245
2333
  s2
2246
2334
  )) }),
2247
- /* @__PURE__ */ jsx4("div", { style: { height: 14 } }),
2248
- /* @__PURE__ */ jsx4(
2335
+ /* @__PURE__ */ jsx5("div", { style: { height: 14 } }),
2336
+ /* @__PURE__ */ jsx5(
2249
2337
  "button",
2250
2338
  {
2251
2339
  onClick: () => removeMessage(selectedMsg.id),
@@ -2255,7 +2343,7 @@ function SequenceEditor({
2255
2343
  )
2256
2344
  ] })
2257
2345
  ] }),
2258
- /* @__PURE__ */ jsxs4("div", { style: {
2346
+ /* @__PURE__ */ jsxs5("div", { style: {
2259
2347
  padding: "4px 14px",
2260
2348
  fontSize: 11,
2261
2349
  color: t.textMuted,
@@ -2264,25 +2352,22 @@ function SequenceEditor({
2264
2352
  display: "flex",
2265
2353
  gap: 16
2266
2354
  }, children: [
2267
- /* @__PURE__ */ jsxs4("span", { children: [
2355
+ /* @__PURE__ */ jsxs5("span", { children: [
2268
2356
  actors.length,
2269
2357
  " actors"
2270
2358
  ] }),
2271
- /* @__PURE__ */ jsxs4("span", { children: [
2359
+ /* @__PURE__ */ jsxs5("span", { children: [
2272
2360
  messages.length,
2273
2361
  " messages"
2274
2362
  ] }),
2275
- /* @__PURE__ */ jsx4("span", { style: { marginLeft: "auto" }, children: "double-click actor to rename \xB7 drag a row to reorder" })
2363
+ /* @__PURE__ */ jsx5("span", { style: { marginLeft: "auto" }, children: "double-click actor to rename \xB7 drag a row to reorder" })
2276
2364
  ] })
2277
2365
  ] });
2278
2366
  }
2279
- function estimateW(text, pxPerChar = 7) {
2280
- return text.length * pxPerChar;
2281
- }
2282
2367
  function primaryBtn() {
2283
2368
  return {
2284
2369
  padding: "6px 12px",
2285
- background: INDIGO,
2370
+ background: INDIGO2,
2286
2371
  color: "#fff",
2287
2372
  border: "none",
2288
2373
  borderRadius: 8,
@@ -2321,180 +2406,37 @@ function input(t) {
2321
2406
  };
2322
2407
  }
2323
2408
  function Label({ t, children }) {
2324
- return /* @__PURE__ */ jsx4("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 4 }, children });
2409
+ return /* @__PURE__ */ jsx5("div", { style: { fontSize: 10, fontWeight: 700, color: t.textMuted, textTransform: "uppercase", letterSpacing: 0.6, marginBottom: 4 }, children });
2325
2410
  }
2326
2411
 
2327
- // src/ui/Minimap.tsx
2328
- import { useCallback as useCallback5, useRef as useRef4 } from "react";
2329
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2330
- var W = 168;
2331
- var H = 112;
2332
- var PAD = 18;
2333
- function Minimap({
2412
+ // src/ui/NodeNavigator.tsx
2413
+ import { useState as useState6 } from "react";
2414
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2415
+ function NodeNavigator({
2334
2416
  model,
2335
- viewportW,
2336
- viewportH,
2337
- transform,
2338
- measureNode,
2339
- onCenterOn,
2417
+ selected,
2418
+ variant,
2340
2419
  isDark,
2341
- accentColor
2420
+ t,
2421
+ acc,
2422
+ open,
2423
+ onToggle,
2424
+ onSelect
2342
2425
  }) {
2343
- const dragRef = useRef4(null);
2344
- const boxes = model.nodes.map((n) => {
2345
- const { w, h } = measureNode(n);
2346
- return { id: n.id, x: n.x ?? 0, y: n.y ?? 0, w, h };
2347
- });
2348
- if (boxes.length === 0) return null;
2349
- const vx = -transform.x / transform.scale;
2350
- const vy = -transform.y / transform.scale;
2351
- const vw = viewportW / transform.scale;
2352
- const vh = viewportH / transform.scale;
2353
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
2354
- for (const b of boxes) {
2355
- minX = Math.min(minX, b.x);
2356
- minY = Math.min(minY, b.y);
2357
- maxX = Math.max(maxX, b.x + b.w);
2358
- maxY = Math.max(maxY, b.y + b.h);
2359
- }
2360
- minX = Math.min(minX, vx);
2361
- minY = Math.min(minY, vy);
2362
- maxX = Math.max(maxX, vx + vw);
2363
- maxY = Math.max(maxY, vy + vh);
2364
- const contentW = Math.max(1, maxX - minX);
2365
- const contentH = Math.max(1, maxY - minY);
2366
- const scale = Math.min((W - PAD * 2) / contentW, (H - PAD * 2) / contentH);
2367
- const offsetX = (W - contentW * scale) / 2 - minX * scale;
2368
- const offsetY = (H - contentH * scale) / 2 - minY * scale;
2369
- const project = (x, y) => ({
2370
- x: offsetX + x * scale,
2371
- y: offsetY + y * scale
2372
- });
2373
- const unproject = (mx, my) => ({
2374
- x: (mx - offsetX) / scale,
2375
- y: (my - offsetY) / scale
2376
- });
2377
- const panTo = useCallback5((e) => {
2378
- const rect = e.currentTarget.getBoundingClientRect();
2379
- const mx = e.clientX - rect.left;
2380
- const my = e.clientY - rect.top;
2381
- const { x, y } = unproject(mx, my);
2382
- onCenterOn(x, y);
2383
- }, [onCenterOn, scale, offsetX, offsetY]);
2384
- const onMouseDown = (e) => {
2385
- e.stopPropagation();
2386
- dragRef.current = { active: true };
2387
- panTo(e);
2388
- };
2389
- const onMouseMove = (e) => {
2390
- if (!dragRef.current?.active) return;
2391
- panTo(e);
2392
- };
2393
- const onMouseUp = () => {
2394
- dragRef.current = null;
2395
- };
2396
- const bg = isDark ? "rgba(15,23,42,0.92)" : "rgba(255,255,255,0.94)";
2397
- const border = isDark ? "#334155" : "#e2e8f0";
2398
- const nodeFill = isDark ? "#475569" : "#cbd5e1";
2399
- const viewStroke = accentColor;
2400
- const viewFill = `${accentColor}22`;
2401
- const vp1 = project(vx, vy);
2402
- const vp2 = project(vx + vw, vy + vh);
2403
- const vpRect = {
2404
- x: Math.max(0, Math.min(W, vp1.x)),
2405
- y: Math.max(0, Math.min(H, vp1.y)),
2406
- w: Math.max(2, Math.min(W, vp2.x) - Math.max(0, vp1.x)),
2407
- h: Math.max(2, Math.min(H, vp2.y) - Math.max(0, vp1.y))
2408
- };
2409
- return /* @__PURE__ */ jsx5(
2410
- "div",
2411
- {
2412
- style: {
2413
- position: "absolute",
2414
- bottom: 14,
2415
- right: 14,
2416
- background: bg,
2417
- border: `1px solid ${border}`,
2418
- borderRadius: 10,
2419
- padding: 6,
2420
- boxShadow: isDark ? "0 8px 20px rgba(0,0,0,0.45)" : "0 6px 18px rgba(15,23,42,0.08)",
2421
- backdropFilter: "blur(6px)"
2422
- },
2423
- children: /* @__PURE__ */ jsxs5(
2424
- "svg",
2425
- {
2426
- width: W,
2427
- height: H,
2428
- style: { display: "block", cursor: "grab", borderRadius: 6 },
2429
- onMouseDown,
2430
- onMouseMove,
2431
- onMouseUp,
2432
- onMouseLeave: onMouseUp,
2433
- children: [
2434
- /* @__PURE__ */ jsx5("rect", { width: W, height: H, rx: 6, fill: isDark ? "#0f172a" : "#fafbfc" }),
2435
- boxes.map((b) => {
2436
- const p = project(b.x, b.y);
2437
- return /* @__PURE__ */ jsx5(
2438
- "rect",
2439
- {
2440
- x: p.x,
2441
- y: p.y,
2442
- width: Math.max(2, b.w * scale),
2443
- height: Math.max(2, b.h * scale),
2444
- rx: 2,
2445
- fill: nodeFill
2446
- },
2447
- b.id
2448
- );
2449
- }),
2450
- /* @__PURE__ */ jsx5(
2451
- "rect",
2452
- {
2453
- x: vpRect.x,
2454
- y: vpRect.y,
2455
- width: vpRect.w,
2456
- height: vpRect.h,
2457
- rx: 3,
2458
- fill: viewFill,
2459
- stroke: viewStroke,
2460
- strokeWidth: 1.25
2461
- }
2462
- )
2463
- ]
2464
- }
2465
- )
2466
- }
2467
- );
2468
- }
2469
-
2470
- // src/ui/NodeNavigator.tsx
2471
- import { useState as useState6 } from "react";
2472
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2473
- function NodeNavigator({
2474
- model,
2475
- selected,
2476
- variant,
2477
- isDark,
2478
- t,
2479
- acc,
2480
- open,
2481
- onToggle,
2482
- onSelect
2483
- }) {
2484
- const [search, setSearch] = useState6("");
2485
- const shapeIcon = (node) => {
2486
- if (variant === "question") return "?";
2487
- if (variant === "journey") return "\u2197";
2488
- switch (node.shape) {
2489
- case "diamond":
2490
- return "\u25C7";
2491
- case "circle":
2492
- return "\u25CB";
2493
- case "parallelogram":
2494
- return "\u25B1";
2495
- default:
2496
- return "\u25AD";
2497
- }
2426
+ const [search, setSearch] = useState6("");
2427
+ const shapeIcon = (node) => {
2428
+ if (variant === "question") return "?";
2429
+ if (variant === "journey") return "\u2197";
2430
+ switch (node.shape) {
2431
+ case "diamond":
2432
+ return "\u25C7";
2433
+ case "circle":
2434
+ return "\u25CB";
2435
+ case "parallelogram":
2436
+ return "\u25B1";
2437
+ default:
2438
+ return "\u25AD";
2439
+ }
2498
2440
  };
2499
2441
  const filtered = model.nodes.filter(
2500
2442
  (n) => n.label.toLowerCase().includes(search.toLowerCase())
@@ -2651,139 +2593,8 @@ function NodeNavigator({
2651
2593
  ] });
2652
2594
  }
2653
2595
 
2654
- // src/ui/ContextMenu.tsx
2655
- import { useEffect as useEffect5, useRef as useRef5, useState as useState7 } from "react";
2656
- import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2657
- function ContextMenu({
2658
- x,
2659
- y,
2660
- nodeId,
2661
- edgeId,
2662
- isDark,
2663
- t,
2664
- acc,
2665
- canUndo,
2666
- canRedo,
2667
- onUndo,
2668
- onRedo,
2669
- onReCenter,
2670
- onAddNode,
2671
- onDuplicate,
2672
- onRename,
2673
- onDelete,
2674
- onDisconnect,
2675
- onEdgeRename,
2676
- onEdgeStyle,
2677
- onEdgeArrowhead,
2678
- onEdgeDelete,
2679
- onEdgeResetRouting,
2680
- currentEdgeStyle,
2681
- currentEdgeArrow,
2682
- edgeHasWaypoint,
2683
- containerRef
2684
- }) {
2685
- const menuRef = useRef5(null);
2686
- const [pos, setPos] = useState7({ x, y });
2687
- useEffect5(() => {
2688
- if (!menuRef.current || !containerRef.current) return;
2689
- const m = menuRef.current.getBoundingClientRect();
2690
- const c = containerRef.current.getBoundingClientRect();
2691
- let nx = x, ny = y;
2692
- if (nx + m.width > c.right - 8) nx = x - m.width;
2693
- if (ny + m.height > c.bottom - 8) ny = y - m.height;
2694
- setPos({ x: nx, y: ny });
2695
- }, [x, y, containerRef]);
2696
- const bg = isDark ? "#1e293b" : "#ffffff";
2697
- const border = isDark ? "#334155" : "#e2e8f0";
2698
- const hoverBg = isDark ? "#334155" : "#f1f5f9";
2699
- const dividerColor = isDark ? "#334155" : "#f1f5f9";
2700
- const text = t.textPrimary;
2701
- const muted = t.textMuted;
2702
- const item = (label, onClick, color, disabled) => /* @__PURE__ */ jsx7(
2703
- "button",
2704
- {
2705
- onClick: disabled ? void 0 : onClick,
2706
- style: {
2707
- display: "flex",
2708
- alignItems: "center",
2709
- gap: 10,
2710
- width: "100%",
2711
- padding: "7px 14px",
2712
- background: "none",
2713
- border: "none",
2714
- textAlign: "left",
2715
- cursor: disabled ? "default" : "pointer",
2716
- fontSize: 12,
2717
- fontFamily: "ui-sans-serif,system-ui,sans-serif",
2718
- color: disabled ? muted : color ?? text,
2719
- opacity: disabled ? 0.4 : 1,
2720
- borderRadius: 6
2721
- },
2722
- onMouseEnter: (e) => {
2723
- if (!disabled) e.currentTarget.style.background = hoverBg;
2724
- },
2725
- onMouseLeave: (e) => {
2726
- e.currentTarget.style.background = "none";
2727
- },
2728
- children: label
2729
- },
2730
- label
2731
- );
2732
- const divider2 = /* @__PURE__ */ jsx7("div", { style: { height: 1, background: dividerColor, margin: "4px 0" } });
2733
- return /* @__PURE__ */ jsx7(
2734
- "div",
2735
- {
2736
- ref: menuRef,
2737
- onMouseDown: (e) => e.stopPropagation(),
2738
- style: {
2739
- position: "fixed",
2740
- left: pos.x,
2741
- top: pos.y,
2742
- zIndex: 9999,
2743
- background: bg,
2744
- border: `1px solid ${border}`,
2745
- borderRadius: 10,
2746
- padding: "5px 0",
2747
- minWidth: 180,
2748
- boxShadow: isDark ? "0 8px 32px rgba(0,0,0,0.5)" : "0 8px 32px rgba(0,0,0,0.12)",
2749
- fontFamily: "ui-sans-serif,system-ui,sans-serif"
2750
- },
2751
- children: edgeId ? /* @__PURE__ */ jsxs7(Fragment2, { children: [
2752
- /* @__PURE__ */ jsx7("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Edge" }),
2753
- item("Rename label (dbl-click)", () => onEdgeRename?.()),
2754
- divider2,
2755
- /* @__PURE__ */ jsx7("div", { style: { padding: "4px 14px 2px", fontSize: 9, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Style" }),
2756
- item(`Solid${currentEdgeStyle === "solid" || !currentEdgeStyle ? " \u2713" : ""}`, () => onEdgeStyle?.("solid")),
2757
- item(`Dashed${currentEdgeStyle === "dashed" ? " \u2713" : ""}`, () => onEdgeStyle?.("dashed")),
2758
- item(`Dotted${currentEdgeStyle === "dotted" ? " \u2713" : ""}`, () => onEdgeStyle?.("dotted")),
2759
- divider2,
2760
- /* @__PURE__ */ jsx7("div", { style: { padding: "4px 14px 2px", fontSize: 9, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Arrowhead" }),
2761
- item(`Arrow${currentEdgeArrow !== "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("arrow")),
2762
- item(`None${currentEdgeArrow === "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("none")),
2763
- divider2,
2764
- item("Reset routing", () => onEdgeResetRouting?.(), void 0, !edgeHasWaypoint),
2765
- item("Delete edge", () => onEdgeDelete?.(), "#ef4444")
2766
- ] }) : nodeId ? /* @__PURE__ */ jsxs7(Fragment2, { children: [
2767
- /* @__PURE__ */ jsx7("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Node" }),
2768
- item("Rename (dbl-click)", onRename),
2769
- item("Duplicate", onDuplicate),
2770
- item("Disconnect all edges", onDisconnect),
2771
- divider2,
2772
- item("Delete node", onDelete, "#ef4444")
2773
- ] }) : /* @__PURE__ */ jsxs7(Fragment2, { children: [
2774
- /* @__PURE__ */ jsx7("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Canvas" }),
2775
- item("Add node here", onAddNode, acc.color),
2776
- item("Re-center (Ctrl+0)", onReCenter),
2777
- divider2,
2778
- item("Undo (Ctrl+Z)", onUndo, void 0, !canUndo),
2779
- item("Redo (Ctrl+Y)", onRedo, void 0, !canRedo)
2780
- ] })
2781
- }
2782
- );
2783
- }
2784
-
2785
2596
  // src/ui/render.tsx
2786
- import { useState as useState8 } from "react";
2597
+ import { useState as useState7 } from "react";
2787
2598
 
2788
2599
  // src/ui/layout.ts
2789
2600
  var NODE_H2 = 48;
@@ -2849,7 +2660,7 @@ function bezierPathVia(x1, y1, wx, wy, x2, y2) {
2849
2660
  }
2850
2661
 
2851
2662
  // src/ui/render.tsx
2852
- import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2663
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2853
2664
  var STYLE_LABEL = { pointerEvents: "none", userSelect: "none" };
2854
2665
  var STYLE_BLUR = { filter: "blur(4px)" };
2855
2666
  var STYLE_EDGE_HIT = { cursor: "pointer" };
@@ -2863,11 +2674,11 @@ function NodeShape({ node, selected, variant, stepNumber, t, isDark, w }) {
2863
2674
  const stroke = selected ? acc.color : t.nodeStroke;
2864
2675
  const fill = selected ? t.nodeSelectedFill : t.nodeFill;
2865
2676
  const sw = selected ? 1.75 : 1.25;
2866
- const glow = selected && /* @__PURE__ */ jsx8(Fragment3, { children: node.shape === "circle" ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
2867
- /* @__PURE__ */ jsx8("circle", { cx, cy, r: NODE_H2 / 2 + 3, fill: "none", stroke: acc.color, strokeWidth: 6, opacity: 0.18, style: STYLE_BLUR }),
2868
- /* @__PURE__ */ jsx8("circle", { cx, cy, r: NODE_H2 / 2 + 1.5, fill: "none", stroke: acc.color, strokeWidth: 1, opacity: 0.55 })
2869
- ] }) : node.shape === "diamond" ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
2870
- /* @__PURE__ */ jsx8(
2677
+ const glow = selected && /* @__PURE__ */ jsx7(Fragment2, { children: node.shape === "circle" ? /* @__PURE__ */ jsxs7(Fragment2, { children: [
2678
+ /* @__PURE__ */ jsx7("circle", { cx, cy, r: NODE_H2 / 2 + 3, fill: "none", stroke: acc.color, strokeWidth: 6, opacity: 0.18, style: STYLE_BLUR }),
2679
+ /* @__PURE__ */ jsx7("circle", { cx, cy, r: NODE_H2 / 2 + 1.5, fill: "none", stroke: acc.color, strokeWidth: 1, opacity: 0.55 })
2680
+ ] }) : node.shape === "diamond" ? /* @__PURE__ */ jsxs7(Fragment2, { children: [
2681
+ /* @__PURE__ */ jsx7(
2871
2682
  "polygon",
2872
2683
  {
2873
2684
  points: `${cx},${-5} ${w + 5},${cy} ${cx},${NODE_H2 + 5} ${-5},${cy}`,
@@ -2878,7 +2689,7 @@ function NodeShape({ node, selected, variant, stepNumber, t, isDark, w }) {
2878
2689
  style: STYLE_BLUR
2879
2690
  }
2880
2691
  ),
2881
- /* @__PURE__ */ jsx8(
2692
+ /* @__PURE__ */ jsx7(
2882
2693
  "polygon",
2883
2694
  {
2884
2695
  points: `${cx},${-2} ${w + 2},${cy} ${cx},${NODE_H2 + 2} ${-2},${cy}`,
@@ -2888,8 +2699,8 @@ function NodeShape({ node, selected, variant, stepNumber, t, isDark, w }) {
2888
2699
  opacity: 0.55
2889
2700
  }
2890
2701
  )
2891
- ] }) : /* @__PURE__ */ jsxs8(Fragment3, { children: [
2892
- /* @__PURE__ */ jsx8(
2702
+ ] }) : /* @__PURE__ */ jsxs7(Fragment2, { children: [
2703
+ /* @__PURE__ */ jsx7(
2893
2704
  "rect",
2894
2705
  {
2895
2706
  x: -4,
@@ -2904,7 +2715,7 @@ function NodeShape({ node, selected, variant, stepNumber, t, isDark, w }) {
2904
2715
  style: STYLE_BLUR
2905
2716
  }
2906
2717
  ),
2907
- /* @__PURE__ */ jsx8(
2718
+ /* @__PURE__ */ jsx7(
2908
2719
  "rect",
2909
2720
  {
2910
2721
  x: -1.5,
@@ -2920,35 +2731,35 @@ function NodeShape({ node, selected, variant, stepNumber, t, isDark, w }) {
2920
2731
  )
2921
2732
  ] }) });
2922
2733
  const badgeColor = isDark ? ACCENT.emeraldDark : ACCENT.emerald;
2923
- const badge = variant === "journey" && stepNumber !== void 0 && /* @__PURE__ */ jsxs8(Fragment3, { children: [
2924
- /* @__PURE__ */ jsx8("circle", { cx: 14, cy: 14, r: 10, fill: badgeColor }),
2925
- /* @__PURE__ */ jsx8("text", { x: 14, y: 18, textAnchor: "middle", fontSize: 9, fill: "white", fontWeight: "700", style: STYLE_LABEL, children: stepNumber })
2734
+ const badge = variant === "journey" && stepNumber !== void 0 && /* @__PURE__ */ jsxs7(Fragment2, { children: [
2735
+ /* @__PURE__ */ jsx7("circle", { cx: 14, cy: 14, r: 10, fill: badgeColor }),
2736
+ /* @__PURE__ */ jsx7("text", { x: 14, y: 18, textAnchor: "middle", fontSize: 9, fill: "white", fontWeight: "700", style: STYLE_LABEL, children: stepNumber })
2926
2737
  ] });
2927
2738
  switch (node.shape) {
2928
2739
  case "diamond": {
2929
2740
  const pts = `${cx},0 ${w},${cy} ${cx},${NODE_H2} 0,${cy}`;
2930
- return /* @__PURE__ */ jsxs8(Fragment3, { children: [
2741
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
2931
2742
  glow,
2932
- /* @__PURE__ */ jsx8("polygon", { points: pts, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
2743
+ /* @__PURE__ */ jsx7("polygon", { points: pts, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
2933
2744
  badge
2934
2745
  ] });
2935
2746
  }
2936
2747
  case "circle":
2937
- return /* @__PURE__ */ jsxs8(Fragment3, { children: [
2748
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
2938
2749
  glow,
2939
- /* @__PURE__ */ jsx8("circle", { cx, cy, r: NODE_H2 / 2 - 1, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
2750
+ /* @__PURE__ */ jsx7("circle", { cx, cy, r: NODE_H2 / 2 - 1, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
2940
2751
  badge
2941
2752
  ] });
2942
2753
  case "parallelogram":
2943
- return /* @__PURE__ */ jsxs8(Fragment3, { children: [
2754
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
2944
2755
  glow,
2945
- /* @__PURE__ */ jsx8("polygon", { points: `14,0 ${w},0 ${w - 14},${NODE_H2} 0,${NODE_H2}`, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
2756
+ /* @__PURE__ */ jsx7("polygon", { points: `14,0 ${w},0 ${w - 14},${NODE_H2} 0,${NODE_H2}`, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
2946
2757
  badge
2947
2758
  ] });
2948
2759
  default:
2949
- return /* @__PURE__ */ jsxs8(Fragment3, { children: [
2760
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
2950
2761
  glow,
2951
- /* @__PURE__ */ jsx8("rect", { width: w, height: NODE_H2, rx: 14, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
2762
+ /* @__PURE__ */ jsx7("rect", { width: w, height: NODE_H2, rx: 14, fill, stroke, strokeWidth: sw, filter: "url(#nodeShadow)" }),
2952
2763
  badge
2953
2764
  ] });
2954
2765
  }
@@ -2969,8 +2780,8 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
2969
2780
  const textSub = isDark ? "#64748b" : "#94a3b8";
2970
2781
  const textAns = isDark ? "#cbd5e1" : "#374151";
2971
2782
  const portRowY = Q_BASE_H2 + Q_ANS_ROW_H2 - 8;
2972
- const glow = selected && /* @__PURE__ */ jsxs8(Fragment3, { children: [
2973
- /* @__PURE__ */ jsx8(
2783
+ const glow = selected && /* @__PURE__ */ jsxs7(Fragment2, { children: [
2784
+ /* @__PURE__ */ jsx7(
2974
2785
  "rect",
2975
2786
  {
2976
2787
  x: -4,
@@ -2985,7 +2796,7 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
2985
2796
  style: STYLE_BLUR
2986
2797
  }
2987
2798
  ),
2988
- /* @__PURE__ */ jsx8(
2799
+ /* @__PURE__ */ jsx7(
2989
2800
  "rect",
2990
2801
  {
2991
2802
  x: -1.5,
@@ -3000,29 +2811,29 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
3000
2811
  }
3001
2812
  )
3002
2813
  ] });
3003
- return /* @__PURE__ */ jsxs8(Fragment3, { children: [
2814
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
3004
2815
  glow,
3005
- /* @__PURE__ */ jsx8("rect", { width: qW, height: totalH, rx: 14, fill: nodeBg, stroke: nodeBorder, strokeWidth: selected ? 2 : 1.5, filter: "url(#nodeShadow)" }),
3006
- /* @__PURE__ */ jsx8("clipPath", { id: `qhdr-${node.id}`, children: /* @__PURE__ */ jsx8("rect", { width: qW, height: Q_BASE_H2, rx: 14 }) }),
3007
- /* @__PURE__ */ jsx8("rect", { width: qW, height: Q_BASE_H2, fill: amberSoft, clipPath: `url(#qhdr-${node.id})` }),
3008
- /* @__PURE__ */ jsx8("rect", { x: 0, y: 0, width: 4, height: Q_BASE_H2, rx: 2, fill: amber }),
3009
- /* @__PURE__ */ jsx8("rect", { x: 12, y: 14, width: 28, height: 28, rx: 8, fill: amber }),
3010
- /* @__PURE__ */ jsx8("text", { x: 26, y: 33, textAnchor: "middle", fontSize: 15, fontWeight: "900", fill: "white", style: STYLE_LABEL, children: "?" }),
3011
- /* @__PURE__ */ jsxs8(
2816
+ /* @__PURE__ */ jsx7("rect", { width: qW, height: totalH, rx: 14, fill: nodeBg, stroke: nodeBorder, strokeWidth: selected ? 2 : 1.5, filter: "url(#nodeShadow)" }),
2817
+ /* @__PURE__ */ jsx7("clipPath", { id: `qhdr-${node.id}`, children: /* @__PURE__ */ jsx7("rect", { width: qW, height: Q_BASE_H2, rx: 14 }) }),
2818
+ /* @__PURE__ */ jsx7("rect", { width: qW, height: Q_BASE_H2, fill: amberSoft, clipPath: `url(#qhdr-${node.id})` }),
2819
+ /* @__PURE__ */ jsx7("rect", { x: 0, y: 0, width: 4, height: Q_BASE_H2, rx: 2, fill: amber }),
2820
+ /* @__PURE__ */ jsx7("rect", { x: 12, y: 14, width: 28, height: 28, rx: 8, fill: amber }),
2821
+ /* @__PURE__ */ jsx7("text", { x: 26, y: 33, textAnchor: "middle", fontSize: 15, fontWeight: "900", fill: "white", style: STYLE_LABEL, children: "?" }),
2822
+ /* @__PURE__ */ jsxs7(
3012
2823
  "text",
3013
2824
  {
3014
2825
  style: STYLE_LABEL,
3015
2826
  fontFamily: "ui-sans-serif,system-ui,sans-serif",
3016
2827
  children: [
3017
- /* @__PURE__ */ jsx8("tspan", { x: 50, y: 27, fontSize: 9, fontWeight: 700, fill: textSub, letterSpacing: 0.6, textAnchor: "start", children: "QUESTION" }),
3018
- /* @__PURE__ */ jsx8("tspan", { x: 50, dy: 15, fontSize: 13, fontWeight: 700, fill: selected ? amber : textMain, textAnchor: "start", children: node.label })
2828
+ /* @__PURE__ */ jsx7("tspan", { x: 50, y: 27, fontSize: 9, fontWeight: 700, fill: textSub, letterSpacing: 0.6, textAnchor: "start", children: "QUESTION" }),
2829
+ /* @__PURE__ */ jsx7("tspan", { x: 50, dy: 15, fontSize: 13, fontWeight: 700, fill: selected ? amber : textMain, textAnchor: "start", children: node.label })
3019
2830
  ]
3020
2831
  }
3021
2832
  ),
3022
- /* @__PURE__ */ jsx8("line", { x1: 0, y1: Q_BASE_H2, x2: qW, y2: Q_BASE_H2, stroke: amberLine, strokeWidth: 1 }),
3023
- answers.length === 0 && /* @__PURE__ */ jsxs8(Fragment3, { children: [
3024
- /* @__PURE__ */ jsx8("text", { x: qW / 2, y: Q_BASE_H2 + 22, textAnchor: "middle", fontSize: 10, fill: amber, opacity: 0.4, fontWeight: 600, style: STYLE_LABEL, children: "No answers yet" }),
3025
- /* @__PURE__ */ jsx8("text", { x: qW / 2, y: Q_BASE_H2 + 36, textAnchor: "middle", fontSize: 9, fill: textSub, opacity: 0.7, style: STYLE_LABEL, children: "Open panel \u2192 Add Answer" })
2833
+ /* @__PURE__ */ jsx7("line", { x1: 0, y1: Q_BASE_H2, x2: qW, y2: Q_BASE_H2, stroke: amberLine, strokeWidth: 1 }),
2834
+ answers.length === 0 && /* @__PURE__ */ jsxs7(Fragment2, { children: [
2835
+ /* @__PURE__ */ jsx7("text", { x: qW / 2, y: Q_BASE_H2 + 22, textAnchor: "middle", fontSize: 10, fill: amber, opacity: 0.4, fontWeight: 600, style: STYLE_LABEL, children: "No answers yet" }),
2836
+ /* @__PURE__ */ jsx7("text", { x: qW / 2, y: Q_BASE_H2 + 36, textAnchor: "middle", fontSize: 9, fill: textSub, opacity: 0.7, style: STYLE_LABEL, children: "Open panel \u2192 Add Answer" })
3026
2837
  ] }),
3027
2838
  answers.map((ans, i) => {
3028
2839
  const prevW = answers.slice(0, i).reduce((s2, a) => s2 + answerCardW2(a) + Q_CARD_PAD2, 0);
@@ -3035,8 +2846,8 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
3035
2846
  const letter = i < 26 ? ANSWER_LETTERS[i] : `${i + 1}`;
3036
2847
  const maxChars = Math.max(2, Math.floor((cW - 20) / 7.5));
3037
2848
  const displayAns = ans.length > maxChars ? ans.slice(0, maxChars - 1) + "\u2026" : ans;
3038
- return /* @__PURE__ */ jsxs8("g", { children: [
3039
- /* @__PURE__ */ jsx8(
2849
+ return /* @__PURE__ */ jsxs7("g", { children: [
2850
+ /* @__PURE__ */ jsx7(
3040
2851
  "rect",
3041
2852
  {
3042
2853
  x: cardX,
@@ -3049,7 +2860,7 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
3049
2860
  strokeWidth: connected ? 1.5 : 1
3050
2861
  }
3051
2862
  ),
3052
- /* @__PURE__ */ jsx8(
2863
+ /* @__PURE__ */ jsx7(
3053
2864
  "rect",
3054
2865
  {
3055
2866
  x: cx - 11,
@@ -3060,7 +2871,7 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
3060
2871
  fill: connected ? amber : isDark ? "#1e293b" : "#fef3c7"
3061
2872
  }
3062
2873
  ),
3063
- /* @__PURE__ */ jsx8(
2874
+ /* @__PURE__ */ jsx7(
3064
2875
  "text",
3065
2876
  {
3066
2877
  x: cx,
@@ -3073,7 +2884,7 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
3073
2884
  children: letter
3074
2885
  }
3075
2886
  ),
3076
- /* @__PURE__ */ jsx8(
2887
+ /* @__PURE__ */ jsx7(
3077
2888
  "text",
3078
2889
  {
3079
2890
  x: cx,
@@ -3087,7 +2898,7 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
3087
2898
  children: displayAns
3088
2899
  }
3089
2900
  ),
3090
- /* @__PURE__ */ jsx8(
2901
+ /* @__PURE__ */ jsx7(
3091
2902
  "circle",
3092
2903
  {
3093
2904
  cx,
@@ -3100,7 +2911,7 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
3100
2911
  onMouseDown: (e) => onAnswerPortDown(e, node.id, ans, cx, portRowY)
3101
2912
  }
3102
2913
  ),
3103
- /* @__PURE__ */ jsx8(
2914
+ /* @__PURE__ */ jsx7(
3104
2915
  "path",
3105
2916
  {
3106
2917
  d: `M ${cx - 3} ${portRowY - 2} L ${cx} ${portRowY + 2} L ${cx + 3} ${portRowY - 2}`,
@@ -3117,7 +2928,7 @@ function QuestionNode({ node, selected, edges, isDark, onAnswerPortDown, qW }) {
3117
2928
  ] });
3118
2929
  }
3119
2930
  function EdgeLine({ edge, nodes, variant, t, isDark, acc, editing, editValue, onEditChange, onEditCommit, onEditCancel, onDoubleClick, onContextMenu, onWaypointDown }) {
3120
- const [hovered, setHovered] = useState8(false);
2931
+ const [hovered, setHovered] = useState7(false);
3121
2932
  const from = nodes.find((n) => n.id === edge.from);
3122
2933
  const to = nodes.find((n) => n.id === edge.to);
3123
2934
  if (!from || !to) return null;
@@ -3156,7 +2967,7 @@ function EdgeLine({ edge, nodes, variant, t, isDark, acc, editing, editValue, on
3156
2967
  const labelW = edge.label ? Math.max(60, Math.ceil(estimateTextW2(edge.label, 7) + 18)) : 60;
3157
2968
  const showHandle = !!onWaypointDown && (hovered || !!wp);
3158
2969
  const flowClass = dash ? void 0 : isAmber ? "edge-flow-amber" : "edge-flow";
3159
- return /* @__PURE__ */ jsxs8(
2970
+ return /* @__PURE__ */ jsxs7(
3160
2971
  "g",
3161
2972
  {
3162
2973
  onDoubleClick: (e) => {
@@ -3169,8 +2980,8 @@ function EdgeLine({ edge, nodes, variant, t, isDark, acc, editing, editValue, on
3169
2980
  onMouseEnter: () => setHovered(true),
3170
2981
  onMouseLeave: () => setHovered(false),
3171
2982
  children: [
3172
- /* @__PURE__ */ jsx8("path", { d, fill: "none", stroke: "transparent", strokeWidth: 14, style: STYLE_EDGE_HIT }),
3173
- /* @__PURE__ */ jsx8(
2983
+ /* @__PURE__ */ jsx7("path", { d, fill: "none", stroke: "transparent", strokeWidth: 14, style: STYLE_EDGE_HIT }),
2984
+ /* @__PURE__ */ jsx7(
3174
2985
  "path",
3175
2986
  {
3176
2987
  d,
@@ -3185,7 +2996,7 @@ function EdgeLine({ edge, nodes, variant, t, isDark, acc, editing, editValue, on
3185
2996
  style: STYLE_NO_EVENTS
3186
2997
  }
3187
2998
  ),
3188
- showHandle && /* @__PURE__ */ jsx8(
2999
+ showHandle && /* @__PURE__ */ jsx7(
3189
3000
  "circle",
3190
3001
  {
3191
3002
  cx: hx,
@@ -3201,7 +3012,7 @@ function EdgeLine({ edge, nodes, variant, t, isDark, acc, editing, editValue, on
3201
3012
  }
3202
3013
  }
3203
3014
  ),
3204
- editing && !isAmber ? /* @__PURE__ */ jsx8("foreignObject", { x: mx - labelW / 2, y: my - 12, width: labelW, height: 22, children: /* @__PURE__ */ jsx8(
3015
+ editing && !isAmber ? /* @__PURE__ */ jsx7("foreignObject", { x: mx - labelW / 2, y: my - 12, width: labelW, height: 22, children: /* @__PURE__ */ jsx7(
3205
3016
  "input",
3206
3017
  {
3207
3018
  autoFocus: true,
@@ -3213,61 +3024,645 @@ function EdgeLine({ edge, nodes, variant, t, isDark, acc, editing, editValue, on
3213
3024
  e.preventDefault();
3214
3025
  onEditCommit?.();
3215
3026
  }
3216
- if (e.key === "Escape") {
3217
- e.preventDefault();
3218
- onEditCancel?.();
3027
+ if (e.key === "Escape") {
3028
+ e.preventDefault();
3029
+ onEditCancel?.();
3030
+ }
3031
+ },
3032
+ onMouseDown: (e) => e.stopPropagation(),
3033
+ style: {
3034
+ width: "100%",
3035
+ height: "100%",
3036
+ border: "none",
3037
+ borderRadius: 6,
3038
+ outline: `2px solid ${acc.color}`,
3039
+ textAlign: "center",
3040
+ fontSize: 10,
3041
+ fontWeight: 500,
3042
+ background: t.inputBg,
3043
+ color: t.inputText,
3044
+ boxSizing: "border-box",
3045
+ padding: "0 6px",
3046
+ fontFamily: "inherit"
3047
+ }
3048
+ }
3049
+ ) }) : edge.label && !isAmber ? /* @__PURE__ */ jsxs7(Fragment2, { children: [
3050
+ /* @__PURE__ */ jsx7(
3051
+ "rect",
3052
+ {
3053
+ x: mx - labelW / 2,
3054
+ y: my - 11,
3055
+ width: labelW,
3056
+ height: 19,
3057
+ rx: 5,
3058
+ fill: t.panelBg,
3059
+ stroke: t.cardBorder,
3060
+ strokeWidth: 1,
3061
+ style: STYLE_EDGE_LABEL_HIT
3062
+ }
3063
+ ),
3064
+ /* @__PURE__ */ jsx7(
3065
+ "text",
3066
+ {
3067
+ x: mx,
3068
+ y: my + 4,
3069
+ textAnchor: "middle",
3070
+ fontSize: 10,
3071
+ fill: t.textSecondary,
3072
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
3073
+ fontWeight: "500",
3074
+ style: STYLE_LABEL,
3075
+ children: edge.label
3076
+ }
3077
+ )
3078
+ ] }) : null
3079
+ ]
3080
+ }
3081
+ );
3082
+ }
3083
+
3084
+ // src/ui/Minimap.tsx
3085
+ import { useCallback as useCallback5, useRef as useRef4 } from "react";
3086
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
3087
+ var W = 168;
3088
+ var H = 112;
3089
+ var PAD = 18;
3090
+ function Minimap({
3091
+ model,
3092
+ viewportW,
3093
+ viewportH,
3094
+ transform,
3095
+ measureNode,
3096
+ onCenterOn,
3097
+ isDark,
3098
+ accentColor
3099
+ }) {
3100
+ const dragRef = useRef4(null);
3101
+ const boxes = model.nodes.map((n) => {
3102
+ const { w, h } = measureNode(n);
3103
+ return { id: n.id, x: n.x ?? 0, y: n.y ?? 0, w, h };
3104
+ });
3105
+ if (boxes.length === 0) return null;
3106
+ const vx = -transform.x / transform.scale;
3107
+ const vy = -transform.y / transform.scale;
3108
+ const vw = viewportW / transform.scale;
3109
+ const vh = viewportH / transform.scale;
3110
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3111
+ for (const b of boxes) {
3112
+ minX = Math.min(minX, b.x);
3113
+ minY = Math.min(minY, b.y);
3114
+ maxX = Math.max(maxX, b.x + b.w);
3115
+ maxY = Math.max(maxY, b.y + b.h);
3116
+ }
3117
+ minX = Math.min(minX, vx);
3118
+ minY = Math.min(minY, vy);
3119
+ maxX = Math.max(maxX, vx + vw);
3120
+ maxY = Math.max(maxY, vy + vh);
3121
+ const contentW = Math.max(1, maxX - minX);
3122
+ const contentH = Math.max(1, maxY - minY);
3123
+ const scale = Math.min((W - PAD * 2) / contentW, (H - PAD * 2) / contentH);
3124
+ const offsetX = (W - contentW * scale) / 2 - minX * scale;
3125
+ const offsetY = (H - contentH * scale) / 2 - minY * scale;
3126
+ const project = (x, y) => ({
3127
+ x: offsetX + x * scale,
3128
+ y: offsetY + y * scale
3129
+ });
3130
+ const unproject = (mx, my) => ({
3131
+ x: (mx - offsetX) / scale,
3132
+ y: (my - offsetY) / scale
3133
+ });
3134
+ const panTo = useCallback5((e) => {
3135
+ const rect = e.currentTarget.getBoundingClientRect();
3136
+ const mx = e.clientX - rect.left;
3137
+ const my = e.clientY - rect.top;
3138
+ const { x, y } = unproject(mx, my);
3139
+ onCenterOn(x, y);
3140
+ }, [onCenterOn, scale, offsetX, offsetY]);
3141
+ const onMouseDown = (e) => {
3142
+ e.stopPropagation();
3143
+ dragRef.current = { active: true };
3144
+ panTo(e);
3145
+ };
3146
+ const onMouseMove = (e) => {
3147
+ if (!dragRef.current?.active) return;
3148
+ panTo(e);
3149
+ };
3150
+ const onMouseUp = () => {
3151
+ dragRef.current = null;
3152
+ };
3153
+ const bg = isDark ? "rgba(15,23,42,0.92)" : "rgba(255,255,255,0.94)";
3154
+ const border = isDark ? "#334155" : "#e2e8f0";
3155
+ const nodeFill = isDark ? "#475569" : "#cbd5e1";
3156
+ const viewStroke = accentColor;
3157
+ const viewFill = `${accentColor}22`;
3158
+ const vp1 = project(vx, vy);
3159
+ const vp2 = project(vx + vw, vy + vh);
3160
+ const vpRect = {
3161
+ x: Math.max(0, Math.min(W, vp1.x)),
3162
+ y: Math.max(0, Math.min(H, vp1.y)),
3163
+ w: Math.max(2, Math.min(W, vp2.x) - Math.max(0, vp1.x)),
3164
+ h: Math.max(2, Math.min(H, vp2.y) - Math.max(0, vp1.y))
3165
+ };
3166
+ return /* @__PURE__ */ jsx8(
3167
+ "div",
3168
+ {
3169
+ style: {
3170
+ position: "absolute",
3171
+ bottom: 14,
3172
+ right: 14,
3173
+ background: bg,
3174
+ border: `1px solid ${border}`,
3175
+ borderRadius: 10,
3176
+ padding: 6,
3177
+ boxShadow: isDark ? "0 8px 20px rgba(0,0,0,0.45)" : "0 6px 18px rgba(15,23,42,0.08)",
3178
+ backdropFilter: "blur(6px)"
3179
+ },
3180
+ children: /* @__PURE__ */ jsxs8(
3181
+ "svg",
3182
+ {
3183
+ width: W,
3184
+ height: H,
3185
+ style: { display: "block", cursor: "grab", borderRadius: 6 },
3186
+ onMouseDown,
3187
+ onMouseMove,
3188
+ onMouseUp,
3189
+ onMouseLeave: onMouseUp,
3190
+ children: [
3191
+ /* @__PURE__ */ jsx8("rect", { width: W, height: H, rx: 6, fill: isDark ? "#0f172a" : "#fafbfc" }),
3192
+ boxes.map((b) => {
3193
+ const p = project(b.x, b.y);
3194
+ return /* @__PURE__ */ jsx8(
3195
+ "rect",
3196
+ {
3197
+ x: p.x,
3198
+ y: p.y,
3199
+ width: Math.max(2, b.w * scale),
3200
+ height: Math.max(2, b.h * scale),
3201
+ rx: 2,
3202
+ fill: nodeFill
3203
+ },
3204
+ b.id
3205
+ );
3206
+ }),
3207
+ /* @__PURE__ */ jsx8(
3208
+ "rect",
3209
+ {
3210
+ x: vpRect.x,
3211
+ y: vpRect.y,
3212
+ width: vpRect.w,
3213
+ height: vpRect.h,
3214
+ rx: 3,
3215
+ fill: viewFill,
3216
+ stroke: viewStroke,
3217
+ strokeWidth: 1.25
3218
+ }
3219
+ )
3220
+ ]
3221
+ }
3222
+ )
3223
+ }
3224
+ );
3225
+ }
3226
+
3227
+ // src/ui/ContextMenu.tsx
3228
+ import { useEffect as useEffect6, useRef as useRef5, useState as useState8 } from "react";
3229
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
3230
+ function ContextMenu({
3231
+ x,
3232
+ y,
3233
+ nodeId,
3234
+ edgeId,
3235
+ isDark,
3236
+ t,
3237
+ acc,
3238
+ canUndo,
3239
+ canRedo,
3240
+ onUndo,
3241
+ onRedo,
3242
+ onReCenter,
3243
+ onAddNode,
3244
+ onDuplicate,
3245
+ onRename,
3246
+ onDelete,
3247
+ onDisconnect,
3248
+ onEdgeRename,
3249
+ onEdgeStyle,
3250
+ onEdgeArrowhead,
3251
+ onEdgeDelete,
3252
+ onEdgeResetRouting,
3253
+ currentEdgeStyle,
3254
+ currentEdgeArrow,
3255
+ edgeHasWaypoint,
3256
+ containerRef
3257
+ }) {
3258
+ const menuRef = useRef5(null);
3259
+ const [pos, setPos] = useState8({ x, y });
3260
+ useEffect6(() => {
3261
+ if (!menuRef.current || !containerRef.current) return;
3262
+ const m = menuRef.current.getBoundingClientRect();
3263
+ const c = containerRef.current.getBoundingClientRect();
3264
+ let nx = x, ny = y;
3265
+ if (nx + m.width > c.right - 8) nx = x - m.width;
3266
+ if (ny + m.height > c.bottom - 8) ny = y - m.height;
3267
+ setPos({ x: nx, y: ny });
3268
+ }, [x, y, containerRef]);
3269
+ const bg = isDark ? "#1e293b" : "#ffffff";
3270
+ const border = isDark ? "#334155" : "#e2e8f0";
3271
+ const hoverBg = isDark ? "#334155" : "#f1f5f9";
3272
+ const dividerColor = isDark ? "#334155" : "#f1f5f9";
3273
+ const text = t.textPrimary;
3274
+ const muted = t.textMuted;
3275
+ const item = (label, onClick, color, disabled) => /* @__PURE__ */ jsx9(
3276
+ "button",
3277
+ {
3278
+ onClick: disabled ? void 0 : onClick,
3279
+ style: {
3280
+ display: "flex",
3281
+ alignItems: "center",
3282
+ gap: 10,
3283
+ width: "100%",
3284
+ padding: "7px 14px",
3285
+ background: "none",
3286
+ border: "none",
3287
+ textAlign: "left",
3288
+ cursor: disabled ? "default" : "pointer",
3289
+ fontSize: 12,
3290
+ fontFamily: "ui-sans-serif,system-ui,sans-serif",
3291
+ color: disabled ? muted : color ?? text,
3292
+ opacity: disabled ? 0.4 : 1,
3293
+ borderRadius: 6
3294
+ },
3295
+ onMouseEnter: (e) => {
3296
+ if (!disabled) e.currentTarget.style.background = hoverBg;
3297
+ },
3298
+ onMouseLeave: (e) => {
3299
+ e.currentTarget.style.background = "none";
3300
+ },
3301
+ children: label
3302
+ },
3303
+ label
3304
+ );
3305
+ const divider2 = /* @__PURE__ */ jsx9("div", { style: { height: 1, background: dividerColor, margin: "4px 0" } });
3306
+ return /* @__PURE__ */ jsx9(
3307
+ "div",
3308
+ {
3309
+ ref: menuRef,
3310
+ onMouseDown: (e) => e.stopPropagation(),
3311
+ style: {
3312
+ position: "fixed",
3313
+ left: pos.x,
3314
+ top: pos.y,
3315
+ zIndex: 9999,
3316
+ background: bg,
3317
+ border: `1px solid ${border}`,
3318
+ borderRadius: 10,
3319
+ padding: "5px 0",
3320
+ minWidth: 180,
3321
+ boxShadow: isDark ? "0 8px 32px rgba(0,0,0,0.5)" : "0 8px 32px rgba(0,0,0,0.12)",
3322
+ fontFamily: "ui-sans-serif,system-ui,sans-serif"
3323
+ },
3324
+ children: edgeId ? /* @__PURE__ */ jsxs9(Fragment3, { children: [
3325
+ /* @__PURE__ */ jsx9("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Edge" }),
3326
+ item("Rename label (dbl-click)", () => onEdgeRename?.()),
3327
+ divider2,
3328
+ /* @__PURE__ */ jsx9("div", { style: { padding: "4px 14px 2px", fontSize: 9, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Style" }),
3329
+ item(`Solid${currentEdgeStyle === "solid" || !currentEdgeStyle ? " \u2713" : ""}`, () => onEdgeStyle?.("solid")),
3330
+ item(`Dashed${currentEdgeStyle === "dashed" ? " \u2713" : ""}`, () => onEdgeStyle?.("dashed")),
3331
+ item(`Dotted${currentEdgeStyle === "dotted" ? " \u2713" : ""}`, () => onEdgeStyle?.("dotted")),
3332
+ divider2,
3333
+ /* @__PURE__ */ jsx9("div", { style: { padding: "4px 14px 2px", fontSize: 9, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Arrowhead" }),
3334
+ item(`Arrow${currentEdgeArrow !== "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("arrow")),
3335
+ item(`None${currentEdgeArrow === "none" ? " \u2713" : ""}`, () => onEdgeArrowhead?.("none")),
3336
+ divider2,
3337
+ item("Reset routing", () => onEdgeResetRouting?.(), void 0, !edgeHasWaypoint),
3338
+ item("Delete edge", () => onEdgeDelete?.(), "#ef4444")
3339
+ ] }) : nodeId ? /* @__PURE__ */ jsxs9(Fragment3, { children: [
3340
+ /* @__PURE__ */ jsx9("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Node" }),
3341
+ item("Rename (dbl-click)", onRename),
3342
+ item("Duplicate", onDuplicate),
3343
+ item("Disconnect all edges", onDisconnect),
3344
+ divider2,
3345
+ item("Delete node", onDelete, "#ef4444")
3346
+ ] }) : /* @__PURE__ */ jsxs9(Fragment3, { children: [
3347
+ /* @__PURE__ */ jsx9("div", { style: { padding: "4px 14px 6px", fontSize: 10, fontWeight: 700, color: muted, textTransform: "uppercase", letterSpacing: 0.8 }, children: "Canvas" }),
3348
+ item("Add node here", onAddNode, acc.color),
3349
+ item("Re-center (Ctrl+0)", onReCenter),
3350
+ divider2,
3351
+ item("Undo (Ctrl+Z)", onUndo, void 0, !canUndo),
3352
+ item("Redo (Ctrl+Y)", onRedo, void 0, !canRedo)
3353
+ ] })
3354
+ }
3355
+ );
3356
+ }
3357
+
3358
+ // src/ui/DiagramCanvas.tsx
3359
+ import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
3360
+ var STYLE_LABEL2 = { pointerEvents: "none", userSelect: "none" };
3361
+ var STYLE_LIVE_PORT = { opacity: 0.85, pointerEvents: "none" };
3362
+ var STYLE_NODE_GRAB = { cursor: "grab" };
3363
+ var STYLE_NODE_GRABBING = { cursor: "grabbing" };
3364
+ 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))" };
3365
+ 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))" };
3366
+ function DiagramCanvas(props) {
3367
+ const {
3368
+ model,
3369
+ variant,
3370
+ variantLabel,
3371
+ t,
3372
+ isDark,
3373
+ acc,
3374
+ transform,
3375
+ setTransform,
3376
+ selected,
3377
+ selectedSet,
3378
+ hoveredId,
3379
+ setHoveredId,
3380
+ drag,
3381
+ pan,
3382
+ liveEdge,
3383
+ boxSel,
3384
+ alignGuides,
3385
+ editingEdgeId,
3386
+ editEdgeLabel,
3387
+ setEditEdgeLabel,
3388
+ commitEdgeEdit,
3389
+ setEditingEdgeId,
3390
+ beginEditEdge,
3391
+ onEdgeContextMenu,
3392
+ setWaypointDrag,
3393
+ editingId,
3394
+ editLabel,
3395
+ setEditLabel,
3396
+ commitEdit,
3397
+ setEditingId,
3398
+ onNodeMouseDown,
3399
+ onNodeMouseUp,
3400
+ onNodeDblClick,
3401
+ onNodeContextMenu,
3402
+ onPortMouseDown,
3403
+ onAnswerPortDown,
3404
+ onSvgMouseDown,
3405
+ onMouseMove,
3406
+ onMouseUp,
3407
+ onSvgContextMenu,
3408
+ reducedMotion,
3409
+ isCoarse,
3410
+ portR,
3411
+ shadowClr,
3412
+ arrowClr,
3413
+ amberArrow,
3414
+ viewport,
3415
+ svgRef,
3416
+ containerRef,
3417
+ ctxMenu,
3418
+ history,
3419
+ onCtxUndo,
3420
+ onCtxRedo,
3421
+ onCtxReCenter,
3422
+ onCtxAddNode,
3423
+ onCtxDuplicate,
3424
+ onCtxRename,
3425
+ onCtxDelete,
3426
+ onCtxDisconnect,
3427
+ ctxEdgeStyle,
3428
+ ctxEdgeArrow,
3429
+ ctxEdgeHasWaypoint,
3430
+ onCtxEdgeRename,
3431
+ onCtxEdgeStyle,
3432
+ onCtxEdgeArrowhead,
3433
+ onCtxEdgeDelete,
3434
+ onCtxEdgeResetRouting
3435
+ } = props;
3436
+ return /* @__PURE__ */ jsxs10("div", { ref: containerRef, style: { flex: 1, overflow: "hidden", position: "relative", background: t.canvas }, children: [
3437
+ /* @__PURE__ */ jsxs10(
3438
+ "svg",
3439
+ {
3440
+ ref: svgRef,
3441
+ width: "100%",
3442
+ height: "100%",
3443
+ role: "application",
3444
+ "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.`,
3445
+ tabIndex: 0,
3446
+ style: { display: "block", cursor: pan ? "grabbing" : drag ? "grabbing" : liveEdge ? "crosshair" : "default", userSelect: "none", outline: "none" },
3447
+ onMouseDown: onSvgMouseDown,
3448
+ onMouseMove,
3449
+ onMouseUp,
3450
+ onMouseLeave: onMouseUp,
3451
+ onContextMenu: onSvgContextMenu,
3452
+ children: [
3453
+ /* @__PURE__ */ jsxs10("defs", { children: [
3454
+ /* @__PURE__ */ jsx10("style", { children: reducedMotion ? `
3455
+ .edge-flow { stroke-dasharray: 0; }
3456
+ .edge-flow-amber { stroke-dasharray: 0; }
3457
+ .edge-live { stroke-dasharray: 4 4; }
3458
+ ` : `
3459
+ @keyframes edgeFlow { to { stroke-dashoffset: -13; } }
3460
+ @keyframes edgeFlowFast { to { stroke-dashoffset: -13; } }
3461
+ .edge-flow { stroke-dasharray: 8 5; animation: edgeFlow 0.9s linear infinite; }
3462
+ .edge-flow-amber { stroke-dasharray: 6 4; animation: edgeFlowFast 0.65s linear infinite; }
3463
+ .edge-live { stroke-dasharray: 7 5; animation: edgeFlow 0.55s linear infinite; }
3464
+ ` }),
3465
+ /* @__PURE__ */ jsx10("pattern", { id: "dots", width: GRID, height: GRID, patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ jsx10("circle", { cx: GRID / 2, cy: GRID / 2, r: 1.1, fill: t.dot }) }),
3466
+ /* @__PURE__ */ jsx10("filter", { id: "nodeShadow", x: "-25%", y: "-25%", width: "150%", height: "160%", children: /* @__PURE__ */ jsx10("feDropShadow", { dx: "0", dy: "3", stdDeviation: "5", floodColor: shadowClr, floodOpacity: "1" }) }),
3467
+ /* @__PURE__ */ jsx10("marker", { id: "arrowhead", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ jsx10("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: arrowClr }) }),
3468
+ /* @__PURE__ */ jsx10("marker", { id: "arrowAmber", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ jsx10("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: amberArrow }) }),
3469
+ /* @__PURE__ */ jsx10("marker", { id: "arrowLive", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ jsx10("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: acc.color }) })
3470
+ ] }),
3471
+ /* @__PURE__ */ jsx10("rect", { width: "100%", height: "100%", fill: "url(#dots)", "data-bg": "1" }),
3472
+ /* @__PURE__ */ jsxs10("g", { transform: `translate(${transform.x},${transform.y}) scale(${transform.scale})`, children: [
3473
+ model.edges.map((e) => /* @__PURE__ */ jsx10(
3474
+ EdgeLine,
3475
+ {
3476
+ edge: e,
3477
+ nodes: model.nodes,
3478
+ variant,
3479
+ t,
3480
+ isDark,
3481
+ acc,
3482
+ editing: editingEdgeId === e.id,
3483
+ editValue: editEdgeLabel,
3484
+ onEditChange: setEditEdgeLabel,
3485
+ onEditCommit: commitEdgeEdit,
3486
+ onEditCancel: () => setEditingEdgeId(null),
3487
+ onDoubleClick: beginEditEdge,
3488
+ onContextMenu: onEdgeContextMenu,
3489
+ onWaypointDown: (ev, edgeId) => setWaypointDrag(edgeId)
3490
+ },
3491
+ e.id
3492
+ )),
3493
+ liveEdge && (() => {
3494
+ const d = bezierPath2(liveEdge.fromX, liveEdge.fromY, liveEdge.toX, liveEdge.toY, liveEdge.exitDir);
3495
+ return /* @__PURE__ */ jsx10("path", { d, fill: "none", stroke: acc.color, strokeWidth: 2, strokeLinecap: "round", className: "edge-live", opacity: 0.8, markerEnd: "url(#arrowLive)" });
3496
+ })(),
3497
+ alignGuides?.x && /* @__PURE__ */ jsx10(
3498
+ "line",
3499
+ {
3500
+ x1: alignGuides.x.pos,
3501
+ x2: alignGuides.x.pos,
3502
+ y1: alignGuides.x.minY,
3503
+ y2: alignGuides.x.maxY,
3504
+ stroke: acc.color,
3505
+ strokeWidth: 1 / transform.scale,
3506
+ strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
3507
+ opacity: 0.85,
3508
+ pointerEvents: "none"
3509
+ }
3510
+ ),
3511
+ alignGuides?.y && /* @__PURE__ */ jsx10(
3512
+ "line",
3513
+ {
3514
+ y1: alignGuides.y.pos,
3515
+ y2: alignGuides.y.pos,
3516
+ x1: alignGuides.y.minX,
3517
+ x2: alignGuides.y.maxX,
3518
+ stroke: acc.color,
3519
+ strokeWidth: 1 / transform.scale,
3520
+ strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
3521
+ opacity: 0.85,
3522
+ pointerEvents: "none"
3219
3523
  }
3220
- },
3221
- onMouseDown: (e) => e.stopPropagation(),
3222
- style: {
3223
- width: "100%",
3224
- height: "100%",
3225
- border: "none",
3226
- borderRadius: 6,
3227
- outline: `2px solid ${acc.color}`,
3228
- textAlign: "center",
3229
- fontSize: 10,
3230
- fontWeight: 500,
3231
- background: t.inputBg,
3232
- color: t.inputText,
3233
- boxSizing: "border-box",
3234
- padding: "0 6px",
3235
- fontFamily: "inherit"
3236
- }
3524
+ ),
3525
+ model.nodes.map((node, idx) => {
3526
+ const isHovered = hoveredId === node.id;
3527
+ const isQuestion2 = variant === "question";
3528
+ const { w: nW } = nodeDims(node, variant);
3529
+ const isSelected = selectedSet.has(node.id);
3530
+ return /* @__PURE__ */ jsxs10(
3531
+ "g",
3532
+ {
3533
+ transform: `translate(${node.x ?? 0},${node.y ?? 0})`,
3534
+ role: "button",
3535
+ "aria-label": `${variantLabel} ${variant === "journey" ? idx + 1 + ": " : ""}${node.label}${isSelected ? ", selected" : ""}`,
3536
+ style: drag?.nodeId === node.id ? STYLE_NODE_GRABBING : STYLE_NODE_GRAB,
3537
+ onMouseDown: (e) => onNodeMouseDown(e, node.id),
3538
+ onMouseUp: (e) => onNodeMouseUp(e, node.id),
3539
+ onDoubleClick: (e) => onNodeDblClick(e, node.id),
3540
+ onContextMenu: (e) => onNodeContextMenu(e, node.id),
3541
+ onMouseEnter: () => setHoveredId(node.id),
3542
+ onMouseLeave: () => setHoveredId(null),
3543
+ children: [
3544
+ /* @__PURE__ */ jsx10("title", { children: `${variantLabel}: ${node.label}` }),
3545
+ isQuestion2 ? /* @__PURE__ */ jsx10(QuestionNode, { node, selected: isSelected, edges: model.edges, isDark, onAnswerPortDown, qW: nW }) : /* @__PURE__ */ jsxs10(Fragment4, { children: [
3546
+ /* @__PURE__ */ jsx10(NodeShape, { node, selected: isSelected, variant, stepNumber: variant === "journey" ? idx + 1 : void 0, t, isDark, w: nW }),
3547
+ editingId === node.id ? /* @__PURE__ */ jsx10("foreignObject", { x: 6, y: 6, width: nW - 12, height: NODE_H2 - 12, children: /* @__PURE__ */ jsx10(
3548
+ "input",
3549
+ {
3550
+ autoFocus: true,
3551
+ value: editLabel,
3552
+ onChange: (e) => setEditLabel(e.target.value),
3553
+ onBlur: commitEdit,
3554
+ onKeyDown: (e) => {
3555
+ if (e.key === "Enter") commitEdit();
3556
+ if (e.key === "Escape") setEditingId(null);
3557
+ },
3558
+ 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 }
3559
+ }
3560
+ ) }) : /* @__PURE__ */ jsx10("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 }),
3561
+ /* @__PURE__ */ jsx10(
3562
+ "circle",
3563
+ {
3564
+ cx: nW / 2,
3565
+ cy: NODE_H2 + 1,
3566
+ r: portR,
3567
+ fill: acc.color,
3568
+ stroke: isDark ? "#0f172a" : "white",
3569
+ strokeWidth: 2,
3570
+ style: isHovered || isCoarse ? STYLE_PORT_VISIBLE : STYLE_PORT_HIDDEN,
3571
+ onMouseDown: (e) => onPortMouseDown(e, node.id)
3572
+ }
3573
+ )
3574
+ ] }),
3575
+ liveEdge && liveEdge.fromId !== node.id && /* @__PURE__ */ jsx10("circle", { cx: nW / 2, cy: -1, r: portR, fill: acc.color, stroke: isDark ? "#0f172a" : "white", strokeWidth: 2, style: STYLE_LIVE_PORT })
3576
+ ]
3577
+ },
3578
+ node.id
3579
+ );
3580
+ })
3581
+ ] })
3582
+ ]
3583
+ }
3584
+ ),
3585
+ boxSel && Math.abs(boxSel.cx - boxSel.sx) + Math.abs(boxSel.cy - boxSel.sy) > 4 && containerRef.current && (() => {
3586
+ const rect = containerRef.current.getBoundingClientRect();
3587
+ const left = Math.min(boxSel.sx, boxSel.cx) - rect.left;
3588
+ const top = Math.min(boxSel.sy, boxSel.cy) - rect.top;
3589
+ const w = Math.abs(boxSel.cx - boxSel.sx);
3590
+ const h = Math.abs(boxSel.cy - boxSel.sy);
3591
+ return /* @__PURE__ */ jsx10(
3592
+ "div",
3593
+ {
3594
+ style: {
3595
+ position: "absolute",
3596
+ left,
3597
+ top,
3598
+ width: w,
3599
+ height: h,
3600
+ border: `1px dashed ${acc.color}`,
3601
+ background: isDark ? "rgba(99,102,241,0.10)" : "rgba(99,102,241,0.08)",
3602
+ pointerEvents: "none",
3603
+ borderRadius: 4
3237
3604
  }
3238
- ) }) : edge.label && !isAmber ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
3239
- /* @__PURE__ */ jsx8(
3240
- "rect",
3241
- {
3242
- x: mx - labelW / 2,
3243
- y: my - 11,
3244
- width: labelW,
3245
- height: 19,
3246
- rx: 5,
3247
- fill: t.panelBg,
3248
- stroke: t.cardBorder,
3249
- strokeWidth: 1,
3250
- style: STYLE_EDGE_LABEL_HIT
3251
- }
3252
- ),
3253
- /* @__PURE__ */ jsx8(
3254
- "text",
3255
- {
3256
- x: mx,
3257
- y: my + 4,
3258
- textAnchor: "middle",
3259
- fontSize: 10,
3260
- fill: t.textSecondary,
3261
- fontFamily: "ui-sans-serif,system-ui,sans-serif",
3262
- fontWeight: "500",
3263
- style: STYLE_LABEL,
3264
- children: edge.label
3265
- }
3266
- )
3267
- ] }) : null
3268
- ]
3269
- }
3270
- );
3605
+ }
3606
+ );
3607
+ })(),
3608
+ model.nodes.length === 0 && /* @__PURE__ */ jsxs10("div", { style: { position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", pointerEvents: "none", gap: 8 }, children: [
3609
+ /* @__PURE__ */ jsx10("div", { style: { fontSize: 36, opacity: 0.1, color: t.textPrimary }, children: variant === "question" ? "?" : variant === "journey" ? "\u2197" : "\u2B21" }),
3610
+ /* @__PURE__ */ jsxs10("div", { style: { fontSize: 13, color: t.textMuted, fontWeight: 500 }, children: [
3611
+ "Click ",
3612
+ /* @__PURE__ */ jsxs10("strong", { style: { color: acc.color }, children: [
3613
+ "+ ",
3614
+ variantLabel
3615
+ ] }),
3616
+ " to start"
3617
+ ] })
3618
+ ] }),
3619
+ model.nodes.length > 0 && viewport.w > 0 && /* @__PURE__ */ jsx10(
3620
+ Minimap,
3621
+ {
3622
+ model,
3623
+ viewportW: viewport.w,
3624
+ viewportH: viewport.h,
3625
+ transform,
3626
+ isDark,
3627
+ accentColor: acc.color,
3628
+ measureNode: (n) => nodeDims(n, variant),
3629
+ onCenterOn: (cx, cy) => {
3630
+ setTransform((tr) => ({ ...tr, x: viewport.w / 2 - cx * tr.scale, y: viewport.h / 2 - cy * tr.scale }));
3631
+ }
3632
+ }
3633
+ ),
3634
+ ctxMenu && /* @__PURE__ */ jsx10(
3635
+ ContextMenu,
3636
+ {
3637
+ x: ctxMenu.x,
3638
+ y: ctxMenu.y,
3639
+ nodeId: ctxMenu.nodeId,
3640
+ edgeId: ctxMenu.edgeId,
3641
+ isDark,
3642
+ t,
3643
+ acc,
3644
+ canUndo: history.canUndo,
3645
+ canRedo: history.canRedo,
3646
+ onUndo: onCtxUndo,
3647
+ onRedo: onCtxRedo,
3648
+ onReCenter: onCtxReCenter,
3649
+ onAddNode: onCtxAddNode,
3650
+ onDuplicate: onCtxDuplicate,
3651
+ onRename: onCtxRename,
3652
+ onDelete: onCtxDelete,
3653
+ onDisconnect: onCtxDisconnect,
3654
+ currentEdgeStyle: ctxEdgeStyle,
3655
+ currentEdgeArrow: ctxEdgeArrow,
3656
+ edgeHasWaypoint: ctxEdgeHasWaypoint,
3657
+ onEdgeRename: onCtxEdgeRename,
3658
+ onEdgeStyle: onCtxEdgeStyle,
3659
+ onEdgeArrowhead: onCtxEdgeArrowhead,
3660
+ onEdgeDelete: onCtxEdgeDelete,
3661
+ onEdgeResetRouting: onCtxEdgeResetRouting,
3662
+ containerRef
3663
+ }
3664
+ )
3665
+ ] });
3271
3666
  }
3272
3667
 
3273
3668
  // src/ui/hooks/useHistory.ts
@@ -3327,10 +3722,10 @@ function useHistory(initial, onChange) {
3327
3722
  }
3328
3723
 
3329
3724
  // src/ui/hooks/useCanvasWheel.ts
3330
- import { useEffect as useEffect6 } from "react";
3725
+ import { useEffect as useEffect7 } from "react";
3331
3726
  function useCanvasWheel(ref, setTransform, options = {}) {
3332
3727
  const { min = 0.15, max = 3, factor = 0.1 } = options;
3333
- useEffect6(() => {
3728
+ useEffect7(() => {
3334
3729
  const el = ref.current;
3335
3730
  if (!el) return;
3336
3731
  const onWheel = (e) => {
@@ -3354,7 +3749,7 @@ function useCanvasWheel(ref, setTransform, options = {}) {
3354
3749
  }
3355
3750
 
3356
3751
  // src/ui/hooks/useCanvasTouch.ts
3357
- import { useEffect as useEffect7 } from "react";
3752
+ import { useEffect as useEffect8 } from "react";
3358
3753
  function useCanvasTouch(ref, {
3359
3754
  transform,
3360
3755
  setTransform,
@@ -3364,7 +3759,7 @@ function useCanvasTouch(ref, {
3364
3759
  longPressMs = 550,
3365
3760
  longPressSlop = 8
3366
3761
  }) {
3367
- useEffect7(() => {
3762
+ useEffect8(() => {
3368
3763
  const el = ref.current;
3369
3764
  if (!el) return;
3370
3765
  let touchPan = null;
@@ -3468,10 +3863,10 @@ function useCanvasTouch(ref, {
3468
3863
  }
3469
3864
 
3470
3865
  // src/ui/hooks/useElementSize.ts
3471
- import { useEffect as useEffect8, useState as useState10 } from "react";
3866
+ import { useEffect as useEffect9, useState as useState10 } from "react";
3472
3867
  function useElementSize(ref) {
3473
3868
  const [size, setSize] = useState10({ w: 0, h: 0 });
3474
- useEffect8(() => {
3869
+ useEffect9(() => {
3475
3870
  const el = ref.current;
3476
3871
  if (!el || typeof ResizeObserver === "undefined") return;
3477
3872
  const measure = () => {
@@ -3577,14 +3972,12 @@ function nearestInDirection(fromX, fromY, dir, candidates) {
3577
3972
  }
3578
3973
 
3579
3974
  // src/ui/DiagramEditor.tsx
3580
- import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
3581
- var STYLE_LABEL2 = { pointerEvents: "none", userSelect: "none" };
3582
- var STYLE_LIVE_PORT = { opacity: 0.85, pointerEvents: "none" };
3975
+ import { Fragment as Fragment5, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
3583
3976
  var STYLE_SR_ONLY = { position: "absolute", width: 1, height: 1, padding: 0, margin: -1, overflow: "hidden", clip: "rect(0 0 0 0)", whiteSpace: "nowrap", border: 0 };
3584
3977
  var STYLE_FLEX_ROW = { flex: 1, display: "flex", overflow: "hidden" };
3585
3978
  function DiagramEditor(props) {
3586
3979
  if (props.initialModel?.type === "sequence") {
3587
- return /* @__PURE__ */ jsx9(
3980
+ return /* @__PURE__ */ jsx11(
3588
3981
  SequenceEditor,
3589
3982
  {
3590
3983
  initialModel: props.initialModel,
@@ -3598,7 +3991,7 @@ function DiagramEditor(props) {
3598
3991
  }
3599
3992
  );
3600
3993
  }
3601
- return /* @__PURE__ */ jsx9(FlowchartEditor, { ...props });
3994
+ return /* @__PURE__ */ jsx11(FlowchartEditor, { ...props });
3602
3995
  }
3603
3996
  function FlowchartEditor({
3604
3997
  initialModel,
@@ -3733,89 +4126,87 @@ function FlowchartEditor({
3733
4126
  const duplicateNode = useCallback7((nodeId) => {
3734
4127
  duplicateIds([nodeId]);
3735
4128
  }, [duplicateIds]);
3736
- useEffect9(() => {
4129
+ useEffect10(() => {
3737
4130
  if (!ctxMenu) return;
3738
4131
  const close = () => setCtxMenu(null);
3739
4132
  window.addEventListener("mousedown", close);
3740
4133
  return () => window.removeEventListener("mousedown", close);
3741
4134
  }, [ctxMenu]);
3742
- useEffect9(() => {
3743
- const onKey = (e) => {
3744
- const tgt = e.target;
3745
- if (tgt && (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA" || tgt.isContentEditable)) return;
3746
- const ctrl = e.ctrlKey || e.metaKey;
3747
- if (ctrl && e.key === "z") {
3748
- e.preventDefault();
3749
- undo();
3750
- return;
3751
- }
3752
- if (ctrl && (e.key === "y" || e.shiftKey && e.key === "z")) {
3753
- e.preventDefault();
3754
- redo();
3755
- return;
3756
- }
3757
- if (ctrl && e.key === "0") {
3758
- e.preventDefault();
3759
- reCenter();
3760
- return;
3761
- }
3762
- if (ctrl && (e.key === "d" || e.key === "D")) {
3763
- if (selectedSet.size > 0) {
3764
- e.preventDefault();
3765
- duplicateIds(Array.from(selectedSet));
3766
- }
3767
- return;
4135
+ const keyCommands = [
4136
+ { match: (e) => (e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey, run: () => {
4137
+ undo();
4138
+ return true;
4139
+ } },
4140
+ { match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "y" || e.shiftKey && e.key === "z"), run: () => {
4141
+ redo();
4142
+ return true;
4143
+ } },
4144
+ { match: (e) => (e.ctrlKey || e.metaKey) && e.key === "0", run: () => {
4145
+ reCenter();
4146
+ return true;
4147
+ } },
4148
+ {
4149
+ match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "d" || e.key === "D") && selectedSet.size > 0,
4150
+ run: () => {
4151
+ duplicateIds(Array.from(selectedSet));
4152
+ return true;
3768
4153
  }
3769
- if (ctrl && (e.key === "c" || e.key === "C")) {
3770
- if (selectedSet.size > 0) {
3771
- e.preventDefault();
3772
- const ids = new Set(selectedSet);
3773
- const nodes = model.nodes.filter((n) => ids.has(n.id));
3774
- const edges = model.edges.filter((ed) => ids.has(ed.from) && ids.has(ed.to));
3775
- clipboardRef.current = {
3776
- nodes: nodes.map((n) => ({ ...n })),
3777
- edges: edges.map((ed) => ({ ...ed }))
3778
- };
3779
- }
3780
- return;
4154
+ },
4155
+ {
4156
+ match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "c" || e.key === "C") && selectedSet.size > 0,
4157
+ run: () => {
4158
+ const ids = new Set(selectedSet);
4159
+ const nodes = model.nodes.filter((n) => ids.has(n.id));
4160
+ const edges = model.edges.filter((ed) => ids.has(ed.from) && ids.has(ed.to));
4161
+ clipboardRef.current = {
4162
+ nodes: nodes.map((n) => ({ ...n })),
4163
+ edges: edges.map((ed) => ({ ...ed }))
4164
+ };
4165
+ return true;
3781
4166
  }
3782
- if (ctrl && (e.key === "v" || e.key === "V")) {
4167
+ },
4168
+ {
4169
+ match: (e) => (e.ctrlKey || e.metaKey) && (e.key === "v" || e.key === "V"),
4170
+ run: () => {
3783
4171
  const clip = clipboardRef.current;
3784
- if (clip && clip.nodes.length > 0) {
3785
- e.preventDefault();
3786
- const idMap = /* @__PURE__ */ new Map();
3787
- const nextNode = makeIdSource("node", model.nodes);
3788
- const nextEdge = makeIdSource("e", model.edges);
3789
- const newNodes = clip.nodes.map((n) => {
3790
- const newId = nextNode();
3791
- idMap.set(n.id, newId);
3792
- return { ...n, id: newId, x: (n.x ?? 0) + 24, y: (n.y ?? 0) + 24 };
3793
- });
3794
- const newEdges = clip.edges.map((ed) => ({
3795
- ...ed,
3796
- id: nextEdge(),
3797
- from: idMap.get(ed.from) ?? ed.from,
3798
- to: idMap.get(ed.to) ?? ed.to
3799
- }));
3800
- const m = { ...model, nodes: [...model.nodes, ...newNodes], edges: [...model.edges, ...newEdges] };
3801
- applyAndPush(m);
3802
- const newIds = newNodes.map((n) => n.id);
3803
- setSelected(newIds[newIds.length - 1]);
3804
- setSelectedSet(new Set(newIds));
3805
- setAnnouncement(`Pasted ${newIds.length} ${variantLabel.toLowerCase()}${newIds.length === 1 ? "" : "s"}.`);
3806
- }
3807
- return;
4172
+ if (!clip || clip.nodes.length === 0) return false;
4173
+ const idMap = /* @__PURE__ */ new Map();
4174
+ const nextNode = makeIdSource("node", model.nodes);
4175
+ const nextEdge = makeIdSource("e", model.edges);
4176
+ const newNodes = clip.nodes.map((n) => {
4177
+ const newId = nextNode();
4178
+ idMap.set(n.id, newId);
4179
+ return { ...n, id: newId, x: (n.x ?? 0) + 24, y: (n.y ?? 0) + 24 };
4180
+ });
4181
+ const newEdges = clip.edges.map((ed) => ({
4182
+ ...ed,
4183
+ id: nextEdge(),
4184
+ from: idMap.get(ed.from) ?? ed.from,
4185
+ to: idMap.get(ed.to) ?? ed.to
4186
+ }));
4187
+ const m = { ...model, nodes: [...model.nodes, ...newNodes], edges: [...model.edges, ...newEdges] };
4188
+ applyAndPush(m);
4189
+ const newIds = newNodes.map((n) => n.id);
4190
+ setSelected(newIds[newIds.length - 1]);
4191
+ setSelectedSet(new Set(newIds));
4192
+ setAnnouncement(`Pasted ${newIds.length} ${variantLabel.toLowerCase()}${newIds.length === 1 ? "" : "s"}.`);
4193
+ return true;
3808
4194
  }
3809
- if (e.key === "Escape") {
4195
+ },
4196
+ {
4197
+ match: (e) => e.key === "Escape",
4198
+ run: () => {
3810
4199
  if (ctxMenu) setCtxMenu(null);
3811
4200
  if (liveEdge) setLiveEdge(null);
3812
4201
  if (editingId) setEditingId(null);
3813
4202
  if (boxSel) setBoxSel(null);
3814
4203
  if (selectedSet.size > 0) clearSelection();
3815
- return;
4204
+ return true;
3816
4205
  }
3817
- if ((e.key === "Delete" || e.key === "Backspace") && selectedSet.size > 0) {
3818
- e.preventDefault();
4206
+ },
4207
+ {
4208
+ match: (e) => (e.key === "Delete" || e.key === "Backspace") && selectedSet.size > 0,
4209
+ run: () => {
3819
4210
  const ids = new Set(selectedSet);
3820
4211
  const updated = {
3821
4212
  ...model,
@@ -3825,29 +4216,34 @@ function FlowchartEditor({
3825
4216
  applyAndPush(updated);
3826
4217
  clearSelection();
3827
4218
  setAnnouncement(`Deleted ${ids.size} ${variantLabel.toLowerCase()}${ids.size === 1 ? "" : "s"}.`);
3828
- return;
4219
+ return true;
3829
4220
  }
3830
- if (selectedSet.size > 0 && (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight")) {
4221
+ },
4222
+ {
4223
+ match: (e) => selectedSet.size > 0 && e.altKey && !!selected && (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight"),
4224
+ run: (e) => {
3831
4225
  const dirKey = e.key === "ArrowLeft" ? "left" : e.key === "ArrowRight" ? "right" : e.key === "ArrowUp" ? "up" : "down";
3832
- if (e.altKey && selected) {
3833
- e.preventDefault();
3834
- const origin = model.nodes.find((n) => n.id === selected);
3835
- if (!origin) return;
3836
- const od = nodeDims(origin, variant);
3837
- const ox = (origin.x ?? 0) + od.w / 2;
3838
- const oy = (origin.y ?? 0) + od.h / 2;
3839
- const candidates = model.nodes.filter((n) => n.id !== selected).map((n) => {
3840
- const d = nodeDims(n, variant);
3841
- return { id: n.id, x: (n.x ?? 0) + d.w / 2, y: (n.y ?? 0) + d.h / 2 };
3842
- });
3843
- const nextId2 = nearestInDirection(ox, oy, dirKey, candidates);
3844
- if (nextId2) {
3845
- selectOne(nextId2);
3846
- setAnnouncement(`Selected ${model.nodes.find((n) => n.id === nextId2)?.label ?? ""}.`);
3847
- }
3848
- return;
4226
+ const origin = model.nodes.find((n) => n.id === selected);
4227
+ if (!origin) return false;
4228
+ const od = nodeDims(origin, variant);
4229
+ const ox = (origin.x ?? 0) + od.w / 2;
4230
+ const oy = (origin.y ?? 0) + od.h / 2;
4231
+ const candidates = model.nodes.filter((n) => n.id !== selected).map((n) => {
4232
+ const d = nodeDims(n, variant);
4233
+ return { id: n.id, x: (n.x ?? 0) + d.w / 2, y: (n.y ?? 0) + d.h / 2 };
4234
+ });
4235
+ const nextNodeId = nearestInDirection(ox, oy, dirKey, candidates);
4236
+ if (nextNodeId) {
4237
+ selectOne(nextNodeId);
4238
+ setAnnouncement(`Selected ${model.nodes.find((n) => n.id === nextNodeId)?.label ?? ""}.`);
3849
4239
  }
3850
- e.preventDefault();
4240
+ return true;
4241
+ }
4242
+ },
4243
+ {
4244
+ match: (e) => selectedSet.size > 0 && (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight"),
4245
+ run: (e) => {
4246
+ const dirKey = e.key === "ArrowLeft" ? "left" : e.key === "ArrowRight" ? "right" : e.key === "ArrowUp" ? "up" : "down";
3851
4247
  const step = e.shiftKey ? GRID * 4 : GRID;
3852
4248
  const dx = dirKey === "left" ? -step : dirKey === "right" ? step : 0;
3853
4249
  const dy = dirKey === "up" ? -step : dirKey === "down" ? step : 0;
@@ -3857,11 +4253,11 @@ function FlowchartEditor({
3857
4253
  nodes: model.nodes.map((n) => ids.has(n.id) ? { ...n, x: snap((n.x ?? 0) + dx), y: snap((n.y ?? 0) + dy) } : n)
3858
4254
  };
3859
4255
  applyAndPush(updated);
4256
+ return true;
3860
4257
  }
3861
- };
3862
- window.addEventListener("keydown", onKey);
3863
- return () => window.removeEventListener("keydown", onKey);
3864
- }, [undo, redo, reCenter, selected, selectedSet, ctxMenu, liveEdge, editingId, boxSel, model, applyAndPush, duplicateNode, clearSelection]);
4258
+ }
4259
+ ];
4260
+ useEditorKeyboard(keyCommands, [undo, redo, reCenter, selected, selectedSet, ctxMenu, liveEdge, editingId, boxSel, model, applyAndPush, duplicateNode, clearSelection]);
3865
4261
  const toCanvas = useCallback7((clientX, clientY) => {
3866
4262
  const rect = svgRef.current.getBoundingClientRect();
3867
4263
  return { x: (clientX - rect.left - transform.x) / transform.scale, y: (clientY - rect.top - transform.y) / transform.scale };
@@ -4150,11 +4546,11 @@ function FlowchartEditor({
4150
4546
  const handleImport = useImporter(applyAndPush, { transform: positionFlowchartNodes });
4151
4547
  const acc = variantAccent(variant, isDark);
4152
4548
  const variantLabel = variant === "question" ? "Question" : variant === "journey" ? "Step" : "Node";
4153
- const shadowColor = isDark ? "rgba(0,0,0,0.55)" : "rgba(15,23,42,0.09)";
4154
- const arrowColor = isDark ? "#64748b" : "#94a3b8";
4549
+ const shadowClr = shadowColor(isDark);
4550
+ const arrowClr = arrowColor(isDark);
4155
4551
  const amberArrow = isDark ? ACCENT.amberDark : ACCENT.amber;
4156
- return /* @__PURE__ */ jsxs9("div", { className: "fsd-editor", style: { display: "flex", flexDirection: "column", height, width: "100%", fontFamily: "ui-sans-serif,system-ui,sans-serif", boxSizing: "border-box", background: t.ctrlsBg }, children: [
4157
- /* @__PURE__ */ jsx9("style", { children: `
4552
+ return /* @__PURE__ */ jsxs11("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: [
4553
+ /* @__PURE__ */ jsx11("style", { children: `
4158
4554
  .fsd-editor button:focus-visible,
4159
4555
  .fsd-editor input:focus-visible,
4160
4556
  .fsd-editor textarea:focus-visible,
@@ -4169,7 +4565,7 @@ function FlowchartEditor({
4169
4565
  outline-offset: -2px;
4170
4566
  }
4171
4567
  ` }),
4172
- /* @__PURE__ */ jsx9(
4568
+ /* @__PURE__ */ jsx11(
4173
4569
  "div",
4174
4570
  {
4175
4571
  role: "status",
@@ -4179,28 +4575,28 @@ function FlowchartEditor({
4179
4575
  children: announcement
4180
4576
  }
4181
4577
  ),
4182
- /* @__PURE__ */ jsx9(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
4183
- /* @__PURE__ */ jsxs9("div", { style: { display: "flex", gap: 6, padding: "7px 14px", background: t.ctrlsBg, borderBottom: `1px solid ${t.ctrlsBorder}`, alignItems: "center", flexWrap: "wrap" }, children: [
4184
- /* @__PURE__ */ jsxs9("button", { onClick: () => addNode(), style: ctrlBtn(acc.color, isDark), children: [
4578
+ /* @__PURE__ */ jsx11(Toolbar, { onExport: handleExport, onImport: allowImport ? handleImport : void 0, allowedExports, allowImport }),
4579
+ /* @__PURE__ */ jsxs11("div", { style: { display: "flex", gap: 6, padding: "7px 14px", background: t.ctrlsBg, borderBottom: `1px solid ${t.ctrlsBorder}`, alignItems: "center", flexWrap: "wrap" }, children: [
4580
+ /* @__PURE__ */ jsxs11("button", { onClick: () => addNode(), style: ctrlBtn(acc.color, isDark), children: [
4185
4581
  "+ ",
4186
4582
  variantLabel
4187
4583
  ] }),
4188
- selectedSet.size > 0 && /* @__PURE__ */ jsxs9(Fragment4, { children: [
4189
- /* @__PURE__ */ jsx9("div", { style: { width: 1, height: 20, background: t.ctrlsBorder, margin: "0 2px" } }),
4190
- /* @__PURE__ */ jsx9("button", { onClick: deleteSelected, style: { ...ctrlBtn("transparent", isDark), color: "#ef4444", border: `1px solid ${isDark ? "#7f1d1d" : "#fca5a5"}` }, children: selectedSet.size > 1 ? `Delete (${selectedSet.size})` : "Delete" })
4584
+ selectedSet.size > 0 && /* @__PURE__ */ jsxs11(Fragment5, { children: [
4585
+ /* @__PURE__ */ jsx11("div", { style: { width: 1, height: 20, background: t.ctrlsBorder, margin: "0 2px" } }),
4586
+ /* @__PURE__ */ jsx11("button", { onClick: deleteSelected, style: { ...ctrlBtn("transparent", isDark), color: "#ef4444", border: `1px solid ${isDark ? "#7f1d1d" : "#fca5a5"}` }, children: selectedSet.size > 1 ? `Delete (${selectedSet.size})` : "Delete" })
4191
4587
  ] }),
4192
- liveEdge && /* @__PURE__ */ jsxs9("span", { style: { fontSize: 11, color: acc.color, fontWeight: 600, marginLeft: 6 }, children: [
4588
+ liveEdge && /* @__PURE__ */ jsxs11("span", { style: { fontSize: 11, color: acc.color, fontWeight: 600, marginLeft: 6 }, children: [
4193
4589
  liveEdge.answerLabel ? `Routing "${liveEdge.answerLabel}" \u2192` : "Drop on a node to connect",
4194
- /* @__PURE__ */ jsx9("span", { style: { fontWeight: 400, color: t.textMuted, marginLeft: 6 }, children: "release to cancel" })
4590
+ /* @__PURE__ */ jsx11("span", { style: { fontWeight: 400, color: t.textMuted, marginLeft: 6 }, children: "release to cancel" })
4195
4591
  ] }),
4196
- /* @__PURE__ */ jsxs9("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
4592
+ /* @__PURE__ */ jsxs11("span", { style: { marginLeft: "auto", fontSize: 11, color: t.textMuted }, children: [
4197
4593
  variant === "question" ? "drag answer port to connect \xB7 " : "drag port dot \xB7 ",
4198
4594
  "scroll to zoom \xB7 drag to pan"
4199
4595
  ] })
4200
4596
  ] }),
4201
- variant !== "flowchart" && /* @__PURE__ */ jsx9("div", { style: { padding: "3px 14px", background: acc.fill, borderBottom: `1px solid ${acc.border}`, fontSize: 11, color: acc.color, fontWeight: 600 }, children: variant === "question" ? "? Question Flow \u2014 add answers in the panel, drag their port to connect" : "\u2197 Journey Map \u2014 numbered steps, drag port to sequence" }),
4202
- /* @__PURE__ */ jsxs9("div", { style: STYLE_FLEX_ROW, children: [
4203
- /* @__PURE__ */ jsx9(
4597
+ variant !== "flowchart" && /* @__PURE__ */ jsx11("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" }),
4598
+ /* @__PURE__ */ jsxs11("div", { style: STYLE_FLEX_ROW, children: [
4599
+ /* @__PURE__ */ jsx11(
4204
4600
  NodeNavigator,
4205
4601
  {
4206
4602
  model,
@@ -4214,323 +4610,162 @@ function FlowchartEditor({
4214
4610
  onSelect: jumpToNode
4215
4611
  }
4216
4612
  ),
4217
- /* @__PURE__ */ jsxs9("div", { ref: containerRef, style: { flex: 1, overflow: "hidden", position: "relative", background: t.canvas }, children: [
4218
- /* @__PURE__ */ jsxs9(
4219
- "svg",
4220
- {
4221
- ref: svgRef,
4222
- width: "100%",
4223
- height: "100%",
4224
- role: "application",
4225
- "aria-label": `${variantLabel} diagram editor. ${model.nodes.length} ${variantLabel.toLowerCase()}s, ${model.edges.length} connections. Scroll to zoom, drag to pan, click a ${variantLabel.toLowerCase()} to select.`,
4226
- tabIndex: 0,
4227
- style: { display: "block", cursor: pan ? "grabbing" : drag ? "grabbing" : liveEdge ? "crosshair" : "default", userSelect: "none", outline: "none" },
4228
- onMouseDown: onSvgMouseDown,
4229
- onMouseMove,
4230
- onMouseUp,
4231
- onMouseLeave: onMouseUp,
4232
- onContextMenu: onSvgContextMenu,
4233
- children: [
4234
- /* @__PURE__ */ jsxs9("defs", { children: [
4235
- /* @__PURE__ */ jsx9("style", { children: reducedMotion ? `
4236
- .edge-flow { stroke-dasharray: 0; }
4237
- .edge-flow-amber { stroke-dasharray: 0; }
4238
- .edge-live { stroke-dasharray: 4 4; }
4239
- ` : `
4240
- @keyframes edgeFlow { to { stroke-dashoffset: -13; } }
4241
- @keyframes edgeFlowFast { to { stroke-dashoffset: -13; } }
4242
- .edge-flow { stroke-dasharray: 8 5; animation: edgeFlow 0.9s linear infinite; }
4243
- .edge-flow-amber { stroke-dasharray: 6 4; animation: edgeFlowFast 0.65s linear infinite; }
4244
- .edge-live { stroke-dasharray: 7 5; animation: edgeFlow 0.55s linear infinite; }
4245
- ` }),
4246
- /* @__PURE__ */ jsx9("pattern", { id: "dots", width: GRID, height: GRID, patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ jsx9("circle", { cx: GRID / 2, cy: GRID / 2, r: 1.1, fill: t.dot }) }),
4247
- /* @__PURE__ */ jsx9("filter", { id: "nodeShadow", x: "-25%", y: "-25%", width: "150%", height: "160%", children: /* @__PURE__ */ jsx9("feDropShadow", { dx: "0", dy: "3", stdDeviation: "5", floodColor: shadowColor, floodOpacity: "1" }) }),
4248
- /* @__PURE__ */ jsx9("marker", { id: "arrowhead", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ jsx9("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: arrowColor }) }),
4249
- /* @__PURE__ */ jsx9("marker", { id: "arrowAmber", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ jsx9("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: amberArrow }) }),
4250
- /* @__PURE__ */ jsx9("marker", { id: "arrowLive", markerWidth: "9", markerHeight: "7", refX: "8", refY: "3.5", orient: "auto", markerUnits: "strokeWidth", children: /* @__PURE__ */ jsx9("path", { d: "M0,0.5 L9,3.5 L0,6.5 L2.2,3.5 Z", fill: acc.color }) })
4251
- ] }),
4252
- /* @__PURE__ */ jsx9("rect", { width: "100%", height: "100%", fill: "url(#dots)", "data-bg": "1" }),
4253
- /* @__PURE__ */ jsxs9("g", { transform: `translate(${transform.x},${transform.y}) scale(${transform.scale})`, children: [
4254
- model.edges.map((e) => /* @__PURE__ */ jsx9(
4255
- EdgeLine,
4256
- {
4257
- edge: e,
4258
- nodes: model.nodes,
4259
- variant,
4260
- t,
4261
- isDark,
4262
- acc,
4263
- editing: editingEdgeId === e.id,
4264
- editValue: editEdgeLabel,
4265
- onEditChange: setEditEdgeLabel,
4266
- onEditCommit: commitEdgeEdit,
4267
- onEditCancel: () => setEditingEdgeId(null),
4268
- onDoubleClick: beginEditEdge,
4269
- onContextMenu: onEdgeContextMenu,
4270
- onWaypointDown: (ev, edgeId) => setWaypointDrag(edgeId)
4271
- },
4272
- e.id
4273
- )),
4274
- liveEdge && (() => {
4275
- const d = bezierPath2(liveEdge.fromX, liveEdge.fromY, liveEdge.toX, liveEdge.toY, liveEdge.exitDir);
4276
- return /* @__PURE__ */ jsx9("path", { d, fill: "none", stroke: acc.color, strokeWidth: 2, strokeLinecap: "round", className: "edge-live", opacity: 0.8, markerEnd: "url(#arrowLive)" });
4277
- })(),
4278
- alignGuides?.x && /* @__PURE__ */ jsx9(
4279
- "line",
4280
- {
4281
- x1: alignGuides.x.pos,
4282
- x2: alignGuides.x.pos,
4283
- y1: alignGuides.x.minY,
4284
- y2: alignGuides.x.maxY,
4285
- stroke: acc.color,
4286
- strokeWidth: 1 / transform.scale,
4287
- strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
4288
- opacity: 0.85,
4289
- pointerEvents: "none"
4290
- }
4291
- ),
4292
- alignGuides?.y && /* @__PURE__ */ jsx9(
4293
- "line",
4294
- {
4295
- y1: alignGuides.y.pos,
4296
- y2: alignGuides.y.pos,
4297
- x1: alignGuides.y.minX,
4298
- x2: alignGuides.y.maxX,
4299
- stroke: acc.color,
4300
- strokeWidth: 1 / transform.scale,
4301
- strokeDasharray: `${4 / transform.scale} ${3 / transform.scale}`,
4302
- opacity: 0.85,
4303
- pointerEvents: "none"
4304
- }
4305
- ),
4306
- model.nodes.map((node, idx) => {
4307
- const isHovered = hoveredId === node.id;
4308
- const isQuestion2 = variant === "question";
4309
- const { w: nW, h: nH } = nodeDims(node, variant);
4310
- const isSelected = selectedSet.has(node.id);
4311
- return /* @__PURE__ */ jsxs9(
4312
- "g",
4313
- {
4314
- transform: `translate(${node.x ?? 0},${node.y ?? 0})`,
4315
- role: "button",
4316
- "aria-label": `${variantLabel} ${variant === "journey" ? idx + 1 + ": " : ""}${node.label}${isSelected ? ", selected" : ""}`,
4317
- style: { cursor: drag?.nodeId === node.id ? "grabbing" : "grab" },
4318
- onMouseDown: (e) => onNodeMouseDown(e, node.id),
4319
- onMouseUp: (e) => onNodeMouseUp(e, node.id),
4320
- onDoubleClick: (e) => onNodeDblClick(e, node.id),
4321
- onContextMenu: (e) => onNodeContextMenu(e, node.id),
4322
- onMouseEnter: () => setHoveredId(node.id),
4323
- onMouseLeave: () => setHoveredId(null),
4324
- children: [
4325
- /* @__PURE__ */ jsx9("title", { children: `${variantLabel}: ${node.label}` }),
4326
- isQuestion2 ? /* @__PURE__ */ jsx9(QuestionNode, { node, selected: isSelected, edges: model.edges, isDark, onAnswerPortDown, qW: nW }) : /* @__PURE__ */ jsxs9(Fragment4, { children: [
4327
- /* @__PURE__ */ jsx9(NodeShape, { node, selected: isSelected, variant, stepNumber: variant === "journey" ? idx + 1 : void 0, t, isDark, w: nW }),
4328
- editingId === node.id ? /* @__PURE__ */ jsx9("foreignObject", { x: 6, y: 6, width: nW - 12, height: NODE_H2 - 12, children: /* @__PURE__ */ jsx9(
4329
- "input",
4330
- {
4331
- autoFocus: true,
4332
- value: editLabel,
4333
- onChange: (e) => setEditLabel(e.target.value),
4334
- onBlur: commitEdit,
4335
- onKeyDown: (e) => {
4336
- if (e.key === "Enter") commitEdit();
4337
- if (e.key === "Escape") setEditingId(null);
4338
- },
4339
- style: { width: "100%", height: "100%", border: "none", borderRadius: 6, outline: `2px solid ${acc.color}`, textAlign: "center", fontSize: 13, fontWeight: 500, background: t.inputBg, boxSizing: "border-box", padding: "0 6px", fontFamily: "inherit", color: t.inputText }
4340
- }
4341
- ) }) : /* @__PURE__ */ jsx9("text", { x: nW / 2, y: NODE_H2 / 2 + 5, textAnchor: "middle", fontSize: 13, fontWeight: "500", fontFamily: "ui-sans-serif,system-ui,sans-serif", fill: isSelected ? acc.color : t.textPrimary, style: STYLE_LABEL2, children: node.label }),
4342
- /* @__PURE__ */ jsx9(
4343
- "circle",
4344
- {
4345
- cx: nW / 2,
4346
- cy: NODE_H2 + 1,
4347
- r: portR,
4348
- fill: acc.color,
4349
- stroke: isDark ? "#0f172a" : "white",
4350
- strokeWidth: 2,
4351
- style: { cursor: "crosshair", opacity: isHovered || isCoarse ? 1 : 0, transition: "opacity 0.15s", pointerEvents: isHovered || isCoarse ? "all" : "none", filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.25))" },
4352
- onMouseDown: (e) => onPortMouseDown(e, node.id)
4353
- }
4354
- )
4355
- ] }),
4356
- liveEdge && liveEdge.fromId !== node.id && /* @__PURE__ */ jsx9("circle", { cx: nW / 2, cy: -1, r: portR, fill: acc.color, stroke: isDark ? "#0f172a" : "white", strokeWidth: 2, style: STYLE_LIVE_PORT })
4357
- ]
4358
- },
4359
- node.id
4360
- );
4361
- })
4362
- ] })
4363
- ]
4364
- }
4365
- ),
4366
- boxSel && Math.abs(boxSel.cx - boxSel.sx) + Math.abs(boxSel.cy - boxSel.sy) > 4 && containerRef.current && (() => {
4367
- const rect = containerRef.current.getBoundingClientRect();
4368
- const left = Math.min(boxSel.sx, boxSel.cx) - rect.left;
4369
- const top = Math.min(boxSel.sy, boxSel.cy) - rect.top;
4370
- const w = Math.abs(boxSel.cx - boxSel.sx);
4371
- const h = Math.abs(boxSel.cy - boxSel.sy);
4372
- return /* @__PURE__ */ jsx9(
4373
- "div",
4374
- {
4375
- style: {
4376
- position: "absolute",
4377
- left,
4378
- top,
4379
- width: w,
4380
- height: h,
4381
- border: `1px dashed ${acc.color}`,
4382
- background: isDark ? "rgba(99,102,241,0.10)" : "rgba(99,102,241,0.08)",
4383
- pointerEvents: "none",
4384
- borderRadius: 4
4385
- }
4613
+ /* @__PURE__ */ jsx11(
4614
+ DiagramCanvas,
4615
+ {
4616
+ model,
4617
+ variant,
4618
+ variantLabel,
4619
+ t,
4620
+ isDark,
4621
+ acc,
4622
+ transform,
4623
+ setTransform,
4624
+ selected,
4625
+ selectedSet,
4626
+ hoveredId,
4627
+ setHoveredId,
4628
+ drag,
4629
+ pan,
4630
+ liveEdge,
4631
+ boxSel,
4632
+ alignGuides,
4633
+ editingEdgeId,
4634
+ editEdgeLabel,
4635
+ setEditEdgeLabel,
4636
+ commitEdgeEdit,
4637
+ setEditingEdgeId,
4638
+ beginEditEdge,
4639
+ onEdgeContextMenu,
4640
+ setWaypointDrag,
4641
+ editingId,
4642
+ editLabel,
4643
+ setEditLabel,
4644
+ commitEdit,
4645
+ setEditingId,
4646
+ onNodeMouseDown,
4647
+ onNodeMouseUp,
4648
+ onNodeDblClick,
4649
+ onNodeContextMenu,
4650
+ onPortMouseDown,
4651
+ onAnswerPortDown,
4652
+ onSvgMouseDown,
4653
+ onMouseMove,
4654
+ onMouseUp,
4655
+ onSvgContextMenu,
4656
+ reducedMotion,
4657
+ isCoarse,
4658
+ portR,
4659
+ shadowClr,
4660
+ arrowClr,
4661
+ amberArrow,
4662
+ viewport,
4663
+ svgRef,
4664
+ containerRef,
4665
+ ctxMenu,
4666
+ history,
4667
+ ctxEdgeStyle: (ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.style ?? "solid",
4668
+ ctxEdgeArrow: (ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.arrowhead ?? "arrow",
4669
+ ctxEdgeHasWaypoint: !!(ctxMenu?.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0)?.waypoint,
4670
+ onCtxUndo: () => {
4671
+ undo();
4672
+ setCtxMenu(null);
4673
+ },
4674
+ onCtxRedo: () => {
4675
+ redo();
4676
+ setCtxMenu(null);
4677
+ },
4678
+ onCtxReCenter: () => {
4679
+ reCenter();
4680
+ setCtxMenu(null);
4681
+ },
4682
+ onCtxAddNode: () => {
4683
+ const rect = svgRef.current.getBoundingClientRect();
4684
+ const cx = (ctxMenu.x - rect.left - transform.x) / transform.scale;
4685
+ const cy = (ctxMenu.y - rect.top - transform.y) / transform.scale;
4686
+ addNode({ x: cx, y: cy });
4687
+ setCtxMenu(null);
4688
+ },
4689
+ onCtxDuplicate: () => {
4690
+ if (ctxMenu?.nodeId) {
4691
+ duplicateNode(ctxMenu.nodeId);
4692
+ setCtxMenu(null);
4386
4693
  }
4387
- );
4388
- })(),
4389
- model.nodes.length === 0 && /* @__PURE__ */ jsxs9("div", { style: { position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", pointerEvents: "none", gap: 8 }, children: [
4390
- /* @__PURE__ */ jsx9("div", { style: { fontSize: 36, opacity: 0.1, color: t.textPrimary }, children: variant === "question" ? "?" : variant === "journey" ? "\u2197" : "\u2B21" }),
4391
- /* @__PURE__ */ jsxs9("div", { style: { fontSize: 13, color: t.textMuted, fontWeight: 500 }, children: [
4392
- "Click ",
4393
- /* @__PURE__ */ jsxs9("strong", { style: { color: acc.color }, children: [
4394
- "+ ",
4395
- variantLabel
4396
- ] }),
4397
- " to start"
4398
- ] })
4399
- ] }),
4400
- model.nodes.length > 0 && viewport.w > 0 && /* @__PURE__ */ jsx9(
4401
- Minimap,
4402
- {
4403
- model,
4404
- viewportW: viewport.w,
4405
- viewportH: viewport.h,
4406
- transform,
4407
- isDark,
4408
- accentColor: acc.color,
4409
- measureNode: (n) => nodeDims(n, variant),
4410
- onCenterOn: (cx, cy) => {
4411
- setTransform((tr) => ({ ...tr, x: viewport.w / 2 - cx * tr.scale, y: viewport.h / 2 - cy * tr.scale }));
4694
+ },
4695
+ onCtxRename: () => {
4696
+ if (ctxMenu?.nodeId) {
4697
+ const node = model.nodes.find((n) => n.id === ctxMenu.nodeId);
4698
+ setEditingId(ctxMenu.nodeId);
4699
+ setEditLabel(node.label);
4700
+ setCtxMenu(null);
4412
4701
  }
4413
- }
4414
- ),
4415
- ctxMenu && (() => {
4416
- const ctxEdge = ctxMenu.edgeId ? model.edges.find((e) => e.id === ctxMenu.edgeId) : void 0;
4417
- return /* @__PURE__ */ jsx9(
4418
- ContextMenu,
4419
- {
4420
- x: ctxMenu.x,
4421
- y: ctxMenu.y,
4422
- nodeId: ctxMenu.nodeId,
4423
- edgeId: ctxMenu.edgeId,
4424
- isDark,
4425
- t,
4426
- acc,
4427
- canUndo: history.canUndo,
4428
- canRedo: history.canRedo,
4429
- onUndo: () => {
4430
- undo();
4431
- setCtxMenu(null);
4432
- },
4433
- onRedo: () => {
4434
- redo();
4435
- setCtxMenu(null);
4436
- },
4437
- onReCenter: () => {
4438
- reCenter();
4439
- setCtxMenu(null);
4440
- },
4441
- onAddNode: () => {
4442
- const rect = svgRef.current.getBoundingClientRect();
4443
- const cx = (ctxMenu.x - rect.left - transform.x) / transform.scale;
4444
- const cy = (ctxMenu.y - rect.top - transform.y) / transform.scale;
4445
- addNode({ x: cx, y: cy });
4446
- setCtxMenu(null);
4447
- },
4448
- onDuplicate: () => {
4449
- if (ctxMenu.nodeId) {
4450
- duplicateNode(ctxMenu.nodeId);
4451
- setCtxMenu(null);
4452
- }
4453
- },
4454
- onRename: () => {
4455
- if (ctxMenu.nodeId) {
4456
- const node = model.nodes.find((n) => n.id === ctxMenu.nodeId);
4457
- setEditingId(ctxMenu.nodeId);
4458
- setEditLabel(node.label);
4459
- setCtxMenu(null);
4460
- }
4461
- },
4462
- onDelete: () => {
4463
- if (ctxMenu.nodeId) {
4464
- deleteNode(ctxMenu.nodeId);
4465
- setCtxMenu(null);
4466
- }
4467
- },
4468
- onDisconnect: () => {
4469
- if (ctxMenu.nodeId) {
4470
- const m = { ...model, edges: model.edges.filter((e) => e.from !== ctxMenu.nodeId && e.to !== ctxMenu.nodeId) };
4471
- applyAndPush(m);
4472
- setCtxMenu(null);
4473
- }
4474
- },
4475
- currentEdgeStyle: ctxEdge?.style ?? "solid",
4476
- currentEdgeArrow: ctxEdge?.arrowhead ?? "arrow",
4477
- edgeHasWaypoint: !!ctxEdge?.waypoint,
4478
- onEdgeRename: () => {
4479
- if (ctxMenu.edgeId) {
4480
- beginEditEdge(ctxMenu.edgeId);
4481
- setCtxMenu(null);
4482
- }
4483
- },
4484
- onEdgeStyle: (s2) => {
4485
- if (ctxMenu.edgeId) {
4486
- setEdgeStyle(ctxMenu.edgeId, s2);
4487
- setCtxMenu(null);
4488
- }
4489
- },
4490
- onEdgeArrowhead: (a) => {
4491
- if (ctxMenu.edgeId) {
4492
- setEdgeArrowhead(ctxMenu.edgeId, a);
4493
- setCtxMenu(null);
4494
- }
4495
- },
4496
- onEdgeDelete: () => {
4497
- if (ctxMenu.edgeId) {
4498
- deleteEdge(ctxMenu.edgeId);
4499
- setCtxMenu(null);
4500
- }
4501
- },
4502
- onEdgeResetRouting: () => {
4503
- if (ctxMenu.edgeId) {
4504
- resetEdgeRouting(ctxMenu.edgeId);
4505
- setCtxMenu(null);
4506
- }
4507
- },
4508
- containerRef
4702
+ },
4703
+ onCtxDelete: () => {
4704
+ if (ctxMenu?.nodeId) {
4705
+ deleteNode(ctxMenu.nodeId);
4706
+ setCtxMenu(null);
4509
4707
  }
4510
- );
4511
- })()
4512
- ] }),
4513
- selected && /* @__PURE__ */ jsx9(StepEditor, { nodeId: selected, model, onModelChange: (m) => {
4708
+ },
4709
+ onCtxDisconnect: () => {
4710
+ if (ctxMenu?.nodeId) {
4711
+ const m = { ...model, edges: model.edges.filter((e) => e.from !== ctxMenu.nodeId && e.to !== ctxMenu.nodeId) };
4712
+ applyAndPush(m);
4713
+ setCtxMenu(null);
4714
+ }
4715
+ },
4716
+ onCtxEdgeRename: () => {
4717
+ if (ctxMenu?.edgeId) {
4718
+ beginEditEdge(ctxMenu.edgeId);
4719
+ setCtxMenu(null);
4720
+ }
4721
+ },
4722
+ onCtxEdgeStyle: (s2) => {
4723
+ if (ctxMenu?.edgeId) {
4724
+ setEdgeStyle(ctxMenu.edgeId, s2);
4725
+ setCtxMenu(null);
4726
+ }
4727
+ },
4728
+ onCtxEdgeArrowhead: (a) => {
4729
+ if (ctxMenu?.edgeId) {
4730
+ setEdgeArrowhead(ctxMenu.edgeId, a);
4731
+ setCtxMenu(null);
4732
+ }
4733
+ },
4734
+ onCtxEdgeDelete: () => {
4735
+ if (ctxMenu?.edgeId) {
4736
+ deleteEdge(ctxMenu.edgeId);
4737
+ setCtxMenu(null);
4738
+ }
4739
+ },
4740
+ onCtxEdgeResetRouting: () => {
4741
+ if (ctxMenu?.edgeId) {
4742
+ resetEdgeRouting(ctxMenu.edgeId);
4743
+ setCtxMenu(null);
4744
+ }
4745
+ }
4746
+ }
4747
+ ),
4748
+ selected && /* @__PURE__ */ jsx11(StepEditor, { nodeId: selected, model, onModelChange: (m) => {
4514
4749
  applyAndPush(m);
4515
4750
  }, variant, isDark, t, acc }, selected)
4516
4751
  ] }),
4517
- /* @__PURE__ */ jsxs9("div", { style: { padding: "4px 14px", fontSize: 11, color: t.textMuted, background: t.statusBg, borderTop: `1px solid ${t.ctrlsBorder}`, display: "flex", gap: 16 }, children: [
4518
- /* @__PURE__ */ jsxs9("span", { children: [
4752
+ /* @__PURE__ */ jsxs11("div", { style: { padding: "4px 14px", fontSize: 11, color: t.textMuted, background: t.statusBg, borderTop: `1px solid ${t.ctrlsBorder}`, display: "flex", gap: 16 }, children: [
4753
+ /* @__PURE__ */ jsxs11("span", { children: [
4519
4754
  model.nodes.length,
4520
4755
  " ",
4521
4756
  variantLabel.toLowerCase(),
4522
4757
  "s"
4523
4758
  ] }),
4524
- /* @__PURE__ */ jsxs9("span", { children: [
4759
+ /* @__PURE__ */ jsxs11("span", { children: [
4525
4760
  model.edges.length,
4526
4761
  " connections"
4527
4762
  ] }),
4528
- /* @__PURE__ */ jsxs9("span", { children: [
4763
+ /* @__PURE__ */ jsxs11("span", { children: [
4529
4764
  Math.round(transform.scale * 100),
4530
4765
  "% zoom"
4531
4766
  ] }),
4532
- /* @__PURE__ */ jsx9("span", { style: { marginLeft: "auto" }, children: "Ctrl+Z undo \xB7 Ctrl+Y redo \xB7 Ctrl+0 fit \xB7 Alt+Arrow traverse" }),
4533
- selected && /* @__PURE__ */ jsx9("span", { style: { color: acc.color }, children: model.nodes.find((n) => n.id === selected)?.label })
4767
+ /* @__PURE__ */ jsx11("span", { style: { marginLeft: "auto" }, children: "Ctrl+Z undo \xB7 Ctrl+Y redo \xB7 Ctrl+0 fit \xB7 Alt+Arrow traverse" }),
4768
+ selected && /* @__PURE__ */ jsx11("span", { style: { color: acc.color }, children: model.nodes.find((n) => n.id === selected)?.label })
4534
4769
  ] })
4535
4770
  ] });
4536
4771
  }