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