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