@glyphlab/diagram-react 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +28 -2
- package/diagram.css +892 -0
- package/index.d.ts +192 -0
- package/index.js +1727 -0
- package/index.js.map +1 -0
- package/package.json +46 -4
- package/theme.css +102 -0
package/index.js
ADDED
|
@@ -0,0 +1,1727 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// packages/diagram-react/src/actions.ts
|
|
4
|
+
var NEW_NODE = { w: 280, h: 150 };
|
|
5
|
+
var NEW_GROUP = { w: 360, h: 240 };
|
|
6
|
+
function addNodeAt(controller, x, y, laneId, onError) {
|
|
7
|
+
const before = new Set(controller.getSnapshot().nodes.map((n) => n.id));
|
|
8
|
+
const result = controller.apply({
|
|
9
|
+
ops: [{ op: "createNode", node: { title: "New node", body: "", x: Math.round(x - NEW_NODE.w / 2), y: Math.round(y - NEW_NODE.h / 2), ...laneId && { laneId } } }],
|
|
10
|
+
summary: "Add node",
|
|
11
|
+
source: "user"
|
|
12
|
+
});
|
|
13
|
+
if (!result.ok) {
|
|
14
|
+
onError?.(result.message);
|
|
15
|
+
return void 0;
|
|
16
|
+
}
|
|
17
|
+
return controller.getSnapshot().nodes.find((n) => !before.has(n.id))?.id;
|
|
18
|
+
}
|
|
19
|
+
function addGroupAt(controller, x, y) {
|
|
20
|
+
const before = new Set(controller.getSnapshot().groups.map((g) => g.id));
|
|
21
|
+
const result = controller.apply({
|
|
22
|
+
ops: [{ op: "createGroup", group: { title: "New group", nodeIds: [], x: Math.round(x - NEW_GROUP.w / 2), y: Math.round(y - NEW_GROUP.h / 2), w: NEW_GROUP.w, h: NEW_GROUP.h } }],
|
|
23
|
+
summary: "Add group",
|
|
24
|
+
source: "user"
|
|
25
|
+
});
|
|
26
|
+
if (!result.ok) return void 0;
|
|
27
|
+
return controller.getSnapshot().groups.find((g) => !before.has(g.id))?.id;
|
|
28
|
+
}
|
|
29
|
+
function deleteNode(controller, id) {
|
|
30
|
+
const title = controller.stores.nodes.getState().byId[id]?.title ?? id;
|
|
31
|
+
controller.apply({ ops: [{ op: "deleteNode", id }], summary: `Delete node \u201C${title}\u201D`, source: "user" });
|
|
32
|
+
}
|
|
33
|
+
function deleteGroup(controller, id) {
|
|
34
|
+
const title = controller.stores.groups.getState().byId[id]?.title ?? id;
|
|
35
|
+
controller.apply({ ops: [{ op: "deleteGroup", id }], summary: `Delete group \u201C${title}\u201D`, source: "user" });
|
|
36
|
+
}
|
|
37
|
+
function deleteEdge(controller, id) {
|
|
38
|
+
const label = controller.stores.edges.getState().byId[id]?.label;
|
|
39
|
+
controller.apply({ ops: [{ op: "deleteEdge", id }], summary: `Delete edge${label ? ` \u201C${label}\u201D` : ""}`, source: "user" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// packages/diagram-react/src/colors.ts
|
|
43
|
+
import { arrowId, colorCss, colorKey, colorLabel, COLOR_PRESETS, isPreset } from "@glyphlab/diagram-core";
|
|
44
|
+
function layerStyle(color) {
|
|
45
|
+
const css = colorCss(color);
|
|
46
|
+
return css ? { "--layer": css } : {};
|
|
47
|
+
}
|
|
48
|
+
var edgeColor = colorCss;
|
|
49
|
+
function edgeStyle(color) {
|
|
50
|
+
const css = colorCss(color);
|
|
51
|
+
return css ? { "--edge-color": css } : void 0;
|
|
52
|
+
}
|
|
53
|
+
var arrowBase = (uid) => `${uid}-arrow`;
|
|
54
|
+
|
|
55
|
+
// packages/diagram-react/src/ArrowHeads.tsx
|
|
56
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
57
|
+
function ArrowHeads({ id, colors }) {
|
|
58
|
+
const used = [...new Set(colors.filter((c) => c !== void 0 && colorCss(c) !== void 0))];
|
|
59
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
60
|
+
/* @__PURE__ */ jsx("marker", { id, viewBox: "0 0 10 10", refX: "9", refY: "5", markerWidth: "7", markerHeight: "7", orient: "auto-start-reverse", children: /* @__PURE__ */ jsx("path", { d: "M 0 1 L 9 5 L 0 9 z", className: "arrow-head" }) }),
|
|
61
|
+
used.map((color) => /* @__PURE__ */ jsx("marker", { id: arrowId(id, color), viewBox: "0 0 10 10", refX: "9", refY: "5", markerWidth: "7", markerHeight: "7", orient: "auto-start-reverse", children: /* @__PURE__ */ jsx("path", { d: "M 0 1 L 9 5 L 0 9 z", className: "arrow-head", style: { fill: colorCss(color) } }) }, color))
|
|
62
|
+
] });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// packages/diagram-react/src/beginConnect.ts
|
|
66
|
+
function sidePoint(node, side) {
|
|
67
|
+
if (side === "n") return { x: node.x + node.w / 2, y: node.y };
|
|
68
|
+
if (side === "s") return { x: node.x + node.w / 2, y: node.y + node.h };
|
|
69
|
+
if (side === "w") return { x: node.x, y: node.y + node.h / 2 };
|
|
70
|
+
return { x: node.x + node.w, y: node.y + node.h / 2 };
|
|
71
|
+
}
|
|
72
|
+
function toDiagram(instance, clientX, clientY) {
|
|
73
|
+
const canvas = instance.surface.el?.getBoundingClientRect();
|
|
74
|
+
const { x, y, scale } = instance.ui.viewport.getState();
|
|
75
|
+
return { x: (clientX - (canvas?.left ?? 0) - x) / scale, y: (clientY - (canvas?.top ?? 0) - y) / scale };
|
|
76
|
+
}
|
|
77
|
+
var nodeUnder = (instance, clientX, clientY, not) => {
|
|
78
|
+
const found = document.elementFromPoint(clientX, clientY)?.closest("[data-node-id]");
|
|
79
|
+
if (!found || instance.surface.el && !instance.surface.el.contains(found)) return null;
|
|
80
|
+
const id = found.dataset.nodeId;
|
|
81
|
+
return id && id !== not ? id : null;
|
|
82
|
+
};
|
|
83
|
+
function beginConnect(instance, e, source, side) {
|
|
84
|
+
if (e.button !== 0) return;
|
|
85
|
+
e.stopPropagation();
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
const from = instance.controller.stores.nodes.getState().byId[source];
|
|
88
|
+
if (!from) return;
|
|
89
|
+
const connect = instance.ui.connect;
|
|
90
|
+
connect.getState().start(source, side, toDiagram(instance, e.clientX, e.clientY));
|
|
91
|
+
const onMove = (ev) => connect.getState().move(toDiagram(instance, ev.clientX, ev.clientY), nodeUnder(instance, ev.clientX, ev.clientY, source));
|
|
92
|
+
const stop = () => {
|
|
93
|
+
window.removeEventListener("pointermove", onMove);
|
|
94
|
+
window.removeEventListener("pointerup", onUp);
|
|
95
|
+
window.removeEventListener("pointercancel", onCancel);
|
|
96
|
+
window.removeEventListener("keydown", onKey, true);
|
|
97
|
+
connect.getState().end();
|
|
98
|
+
};
|
|
99
|
+
const onUp = (ev) => {
|
|
100
|
+
const target = nodeUnder(instance, ev.clientX, ev.clientY, source);
|
|
101
|
+
stop();
|
|
102
|
+
if (target) connectNodes(instance, source, target);
|
|
103
|
+
};
|
|
104
|
+
const onCancel = () => stop();
|
|
105
|
+
const onKey = (ev) => {
|
|
106
|
+
if (ev.key !== "Escape") return;
|
|
107
|
+
ev.stopPropagation();
|
|
108
|
+
stop();
|
|
109
|
+
};
|
|
110
|
+
window.addEventListener("pointermove", onMove);
|
|
111
|
+
window.addEventListener("pointerup", onUp);
|
|
112
|
+
window.addEventListener("pointercancel", onCancel);
|
|
113
|
+
window.addEventListener("keydown", onKey, true);
|
|
114
|
+
}
|
|
115
|
+
function connectNodes(instance, from, to) {
|
|
116
|
+
const { controller, ui } = instance;
|
|
117
|
+
const nodes = controller.stores.nodes.getState().byId;
|
|
118
|
+
const find = () => controller.getSnapshot().edges.find((edge) => edge.from === from && edge.to === to && edge.label === "");
|
|
119
|
+
const found = find();
|
|
120
|
+
if (found) {
|
|
121
|
+
ui.selection.getState().select({ kind: "edge", id: found.id });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const result = controller.apply({
|
|
125
|
+
ops: [{ op: "createEdge", edge: { from, to, label: "" } }],
|
|
126
|
+
summary: `Connect \u201C${nodes[from]?.title ?? from}\u201D \u2192 \u201C${nodes[to]?.title ?? to}\u201D`,
|
|
127
|
+
source: "user"
|
|
128
|
+
});
|
|
129
|
+
if (!result.ok) return;
|
|
130
|
+
const id = find()?.id;
|
|
131
|
+
if (id) ui.selection.getState().select({ kind: "edge", id });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// packages/diagram-react/src/ColorControl.tsx
|
|
135
|
+
import { useEffect, useRef } from "react";
|
|
136
|
+
|
|
137
|
+
// packages/diagram-react/src/context.ts
|
|
138
|
+
import { createContext, useContext } from "react";
|
|
139
|
+
import { useStore } from "zustand";
|
|
140
|
+
var DiagramContext = createContext(null);
|
|
141
|
+
var useContextValue = () => {
|
|
142
|
+
const value = useContext(DiagramContext);
|
|
143
|
+
if (!value) throw new Error("This component must be inside a DiagramProvider.");
|
|
144
|
+
return value;
|
|
145
|
+
};
|
|
146
|
+
var useDiagram = () => useContextValue().instance;
|
|
147
|
+
var useSlots = () => useContextValue().slots;
|
|
148
|
+
function useSelected(api, selector) {
|
|
149
|
+
return useStore(api, selector);
|
|
150
|
+
}
|
|
151
|
+
var useNodes = (selector) => useSelected(useDiagram().controller.stores.nodes, selector);
|
|
152
|
+
var useEdges = (selector) => useSelected(useDiagram().controller.stores.edges, selector);
|
|
153
|
+
var useGroups = (selector) => useSelected(useDiagram().controller.stores.groups, selector);
|
|
154
|
+
var useMeta = (selector) => useSelected(useDiagram().controller.stores.meta, selector);
|
|
155
|
+
var useViewport = (selector) => useSelected(useDiagram().ui.viewport, selector);
|
|
156
|
+
var useSelection = (selector) => useSelected(useDiagram().ui.selection, selector);
|
|
157
|
+
var useEditing = (selector) => useSelected(useDiagram().ui.edit, selector);
|
|
158
|
+
var useConnecting = (selector) => useSelected(useDiagram().ui.connect, selector);
|
|
159
|
+
var useHighlights = (selector) => useSelected(useDiagram().ui.highlight, selector);
|
|
160
|
+
function bindStore(api) {
|
|
161
|
+
function useBound(selector) {
|
|
162
|
+
return useStore(api, selector);
|
|
163
|
+
}
|
|
164
|
+
return Object.assign(useBound, api);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// packages/diagram-react/src/ColorControl.tsx
|
|
168
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
169
|
+
var KIND_LABEL = { node: "node", group: "group", edge: "edge" };
|
|
170
|
+
function ColorControl() {
|
|
171
|
+
const { controller } = useDiagram();
|
|
172
|
+
const selection = useSelection((s) => s.selection);
|
|
173
|
+
const item = useNodes((s) => selection?.kind === "node" ? s.byId[selection.id] : void 0);
|
|
174
|
+
const group = useGroups((s) => selection?.kind === "group" ? s.byId[selection.id] : void 0);
|
|
175
|
+
const edge = useEdges((s) => selection?.kind === "edge" ? s.byId[selection.id] : void 0);
|
|
176
|
+
const custom = useRef(null);
|
|
177
|
+
const current = (item ?? group ?? edge)?.color;
|
|
178
|
+
const name = item?.title ?? group?.title ?? edge?.label ?? "edge";
|
|
179
|
+
const choose = (color) => {
|
|
180
|
+
if (!selection || color === (current ?? null)) return;
|
|
181
|
+
const { id } = selection;
|
|
182
|
+
const op = selection.kind === "node" ? { op: "updateNode", id, patch: { color } } : selection.kind === "group" ? { op: "updateGroup", id, patch: { color } } : { op: "updateEdge", id, patch: { color } };
|
|
183
|
+
controller.apply({ ops: [op], summary: `${color === null ? "Remove the color of" : "Color"} ${KIND_LABEL[selection.kind]} \u201C${name}\u201D`, source: "user" });
|
|
184
|
+
};
|
|
185
|
+
const latest = useRef(choose);
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
latest.current = choose;
|
|
188
|
+
});
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
const input = custom.current;
|
|
191
|
+
if (!input) return;
|
|
192
|
+
const onChange = () => latest.current(input.value.toLowerCase());
|
|
193
|
+
input.addEventListener("change", onChange);
|
|
194
|
+
return () => input.removeEventListener("change", onChange);
|
|
195
|
+
}, [selection]);
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (custom.current && current && !isPreset(current)) custom.current.value = current;
|
|
198
|
+
}, [current, selection]);
|
|
199
|
+
if (!selection || !(item ?? group ?? edge)) return null;
|
|
200
|
+
const isOwn = current !== void 0 && !isPreset(current);
|
|
201
|
+
return /* @__PURE__ */ jsxs2(
|
|
202
|
+
"div",
|
|
203
|
+
{
|
|
204
|
+
className: "color-control",
|
|
205
|
+
role: "group",
|
|
206
|
+
"aria-label": `Color of the selected ${KIND_LABEL[selection.kind]}`,
|
|
207
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
208
|
+
onContextMenu: (e) => e.stopPropagation(),
|
|
209
|
+
children: [
|
|
210
|
+
/* @__PURE__ */ jsx2("button", { type: "button", className: "color-swatch none", "aria-label": "Default color", "aria-pressed": current === void 0, title: "Default color", onClick: () => choose(null), children: /* @__PURE__ */ jsxs2("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", "aria-hidden": "true", children: [
|
|
211
|
+
/* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "9" }),
|
|
212
|
+
/* @__PURE__ */ jsx2("path", { d: "M5.6 5.6l12.8 12.8" })
|
|
213
|
+
] }) }),
|
|
214
|
+
COLOR_PRESETS.map((preset) => /* @__PURE__ */ jsx2(
|
|
215
|
+
"button",
|
|
216
|
+
{
|
|
217
|
+
type: "button",
|
|
218
|
+
className: "color-swatch",
|
|
219
|
+
style: { background: colorCss(preset) },
|
|
220
|
+
"aria-label": colorLabel(preset),
|
|
221
|
+
"aria-pressed": current === preset,
|
|
222
|
+
title: colorLabel(preset),
|
|
223
|
+
onClick: () => choose(preset)
|
|
224
|
+
},
|
|
225
|
+
preset
|
|
226
|
+
)),
|
|
227
|
+
/* @__PURE__ */ jsx2("label", { className: `color-swatch custom${isOwn ? " on" : ""}`, title: "Choose another color", children: /* @__PURE__ */ jsx2("input", { ref: custom, type: "color", "aria-label": "Custom color", defaultValue: "#3b82f6" }) })
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// packages/diagram-react/src/DiagramProvider.tsx
|
|
234
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
235
|
+
function DiagramProvider({ instance, slots = {}, children }) {
|
|
236
|
+
return /* @__PURE__ */ jsx3(DiagramContext.Provider, { value: { instance, slots }, children });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// packages/diagram-react/src/DiagramCanvas.tsx
|
|
240
|
+
import { useCallback, useEffect as useEffect6, useLayoutEffect, useMemo as useMemo2, useRef as useRef6, useState as useState6 } from "react";
|
|
241
|
+
import { diagramBounds } from "@glyphlab/diagram-core";
|
|
242
|
+
|
|
243
|
+
// packages/diagram-react/src/EdgeEditor.tsx
|
|
244
|
+
import { useEffect as useEffect2, useRef as useRef2, useState } from "react";
|
|
245
|
+
import { LIMITS, edgeGeometry } from "@glyphlab/diagram-core";
|
|
246
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
247
|
+
function EdgeEditor() {
|
|
248
|
+
const id = useEditing((s) => s.edge);
|
|
249
|
+
const edge = useEdges((s) => id ? s.byId[id] : void 0);
|
|
250
|
+
const from = useNodes((s) => edge ? s.byId[edge.from] : void 0);
|
|
251
|
+
const to = useNodes((s) => edge ? s.byId[edge.to] : void 0);
|
|
252
|
+
if (!id || !edge || !from || !to) return null;
|
|
253
|
+
const { lx, ly } = edgeGeometry(from, to);
|
|
254
|
+
return /* @__PURE__ */ jsx4(LabelInput, { id, label: edge.label, at: { x: lx, y: ly } }, id);
|
|
255
|
+
}
|
|
256
|
+
function LabelInput({ id, label, at }) {
|
|
257
|
+
const { controller, ui } = useDiagram();
|
|
258
|
+
const [value, setValue] = useState(label);
|
|
259
|
+
const ref = useRef2(null);
|
|
260
|
+
const done = useRef2(false);
|
|
261
|
+
useEffect2(() => {
|
|
262
|
+
ref.current?.focus();
|
|
263
|
+
ref.current?.select();
|
|
264
|
+
}, []);
|
|
265
|
+
const finish = (save) => {
|
|
266
|
+
if (done.current) return;
|
|
267
|
+
done.current = true;
|
|
268
|
+
if (save && value !== label) {
|
|
269
|
+
controller.apply({ ops: [{ op: "updateEdge", id, patch: { label: value.trim() } }], summary: "Edit edge label", source: "user" });
|
|
270
|
+
}
|
|
271
|
+
ui.edit.getState().end();
|
|
272
|
+
};
|
|
273
|
+
return /* @__PURE__ */ jsx4(
|
|
274
|
+
"input",
|
|
275
|
+
{
|
|
276
|
+
ref,
|
|
277
|
+
className: "edge-edit",
|
|
278
|
+
"aria-label": "Edge label",
|
|
279
|
+
value,
|
|
280
|
+
maxLength: LIMITS.label,
|
|
281
|
+
placeholder: "Label",
|
|
282
|
+
style: { left: at.x, top: at.y, width: Math.max(120, value.length * 7 + 28) },
|
|
283
|
+
onChange: (e) => setValue(e.target.value),
|
|
284
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
285
|
+
onDoubleClick: (e) => e.stopPropagation(),
|
|
286
|
+
onBlur: () => finish(true),
|
|
287
|
+
onKeyDown: (e) => {
|
|
288
|
+
if (e.key === "Enter") {
|
|
289
|
+
e.preventDefault();
|
|
290
|
+
finish(true);
|
|
291
|
+
} else if (e.key === "Escape") {
|
|
292
|
+
e.stopPropagation();
|
|
293
|
+
finish(false);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// packages/diagram-react/src/GroupEditor.tsx
|
|
301
|
+
import { useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
|
|
302
|
+
import { LIMITS as LIMITS2 } from "@glyphlab/diagram-core";
|
|
303
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
304
|
+
function GroupEditor() {
|
|
305
|
+
const id = useEditing((s) => s.group);
|
|
306
|
+
const group = useGroups((s) => id ? s.byId[id] : void 0);
|
|
307
|
+
if (!id || !group) return null;
|
|
308
|
+
if (group.lane) return /* @__PURE__ */ jsx5(LaneInput, { id, title: group.title, body: group.lane.body, x: group.x, y: group.y, width: group.w }, id);
|
|
309
|
+
return /* @__PURE__ */ jsx5(TitleInput, { id, title: group.title, at: { x: group.x + 14, y: group.y + 8 }, width: Math.max(140, group.w - 28) }, id);
|
|
310
|
+
}
|
|
311
|
+
function LaneInput({ id, title, body, x, y, width }) {
|
|
312
|
+
const { controller, ui } = useDiagram();
|
|
313
|
+
const [name, setName] = useState2(title);
|
|
314
|
+
const [description, setDescription] = useState2(body);
|
|
315
|
+
const [error, setError] = useState2("");
|
|
316
|
+
const ref = useRef3(null);
|
|
317
|
+
useEffect3(() => {
|
|
318
|
+
ref.current?.focus();
|
|
319
|
+
ref.current?.select();
|
|
320
|
+
}, []);
|
|
321
|
+
const save = () => {
|
|
322
|
+
const result = controller.apply({ source: "user", summary: `Edit swimlane "${title}"`, ops: [{ op: "updateLane", id, patch: { title: name.trim(), body: description } }] });
|
|
323
|
+
if (!result.ok) {
|
|
324
|
+
setError(result.message);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
ui.edit.getState().end();
|
|
328
|
+
};
|
|
329
|
+
return /* @__PURE__ */ jsxs3(
|
|
330
|
+
"form",
|
|
331
|
+
{
|
|
332
|
+
className: "lane-edit",
|
|
333
|
+
"aria-label": "Edit swimlane",
|
|
334
|
+
style: { left: x + 8, top: y + 8, width: Math.max(240, width - 16) },
|
|
335
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
336
|
+
onDoubleClick: (e) => e.stopPropagation(),
|
|
337
|
+
onSubmit: (e) => {
|
|
338
|
+
e.preventDefault();
|
|
339
|
+
save();
|
|
340
|
+
},
|
|
341
|
+
onKeyDown: (e) => {
|
|
342
|
+
if (e.key === "Escape") {
|
|
343
|
+
e.stopPropagation();
|
|
344
|
+
ui.edit.getState().end();
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
children: [
|
|
348
|
+
/* @__PURE__ */ jsxs3("label", { children: [
|
|
349
|
+
"Title",
|
|
350
|
+
/* @__PURE__ */ jsx5("input", { ref, "aria-label": "Lane title", value: name, required: true, maxLength: LIMITS2.title, onChange: (e) => setName(e.target.value) })
|
|
351
|
+
] }),
|
|
352
|
+
/* @__PURE__ */ jsxs3("label", { children: [
|
|
353
|
+
"Description",
|
|
354
|
+
/* @__PURE__ */ jsx5("textarea", { "aria-label": "Lane description", value: description, maxLength: LIMITS2.body, onChange: (e) => setDescription(e.target.value) })
|
|
355
|
+
] }),
|
|
356
|
+
error && /* @__PURE__ */ jsx5("p", { role: "alert", children: error }),
|
|
357
|
+
/* @__PURE__ */ jsxs3("div", { children: [
|
|
358
|
+
/* @__PURE__ */ jsx5("button", { className: "dg-btn", type: "button", onClick: () => ui.edit.getState().end(), children: "Cancel" }),
|
|
359
|
+
" ",
|
|
360
|
+
/* @__PURE__ */ jsx5("button", { className: "dg-btn", type: "submit", children: "Save lane" })
|
|
361
|
+
] })
|
|
362
|
+
]
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
function TitleInput({ id, title, at, width }) {
|
|
367
|
+
const { controller, ui } = useDiagram();
|
|
368
|
+
const [value, setValue] = useState2(title);
|
|
369
|
+
const ref = useRef3(null);
|
|
370
|
+
const done = useRef3(false);
|
|
371
|
+
useEffect3(() => {
|
|
372
|
+
ref.current?.focus();
|
|
373
|
+
ref.current?.select();
|
|
374
|
+
}, []);
|
|
375
|
+
const finish = (save) => {
|
|
376
|
+
if (done.current) return;
|
|
377
|
+
done.current = true;
|
|
378
|
+
const next = value.trim();
|
|
379
|
+
if (save && next !== "" && next !== title) {
|
|
380
|
+
controller.apply({ ops: [{ op: "updateGroup", id, patch: { title: next } }], summary: `Rename group \u201C${title}\u201D`, source: "user" });
|
|
381
|
+
}
|
|
382
|
+
ui.edit.getState().end();
|
|
383
|
+
};
|
|
384
|
+
return /* @__PURE__ */ jsx5(
|
|
385
|
+
"input",
|
|
386
|
+
{
|
|
387
|
+
ref,
|
|
388
|
+
className: "group-edit",
|
|
389
|
+
"aria-label": "Group name",
|
|
390
|
+
value,
|
|
391
|
+
maxLength: LIMITS2.title,
|
|
392
|
+
style: { left: at.x, top: at.y, width },
|
|
393
|
+
onChange: (e) => setValue(e.target.value),
|
|
394
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
395
|
+
onDoubleClick: (e) => e.stopPropagation(),
|
|
396
|
+
onBlur: () => finish(true),
|
|
397
|
+
onKeyDown: (e) => {
|
|
398
|
+
if (e.key === "Enter") {
|
|
399
|
+
e.preventDefault();
|
|
400
|
+
finish(true);
|
|
401
|
+
} else if (e.key === "Escape") {
|
|
402
|
+
e.stopPropagation();
|
|
403
|
+
finish(false);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// packages/diagram-react/src/ConnectHandles.tsx
|
|
411
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
412
|
+
var SIDES = ["n", "e", "s", "w"];
|
|
413
|
+
var LABEL = { n: "top", e: "right", s: "bottom", w: "left" };
|
|
414
|
+
function ConnectHandles() {
|
|
415
|
+
const instance = useDiagram();
|
|
416
|
+
const hovered = useConnecting((s) => s.hovered);
|
|
417
|
+
const dragging = useConnecting((s) => s.source !== null);
|
|
418
|
+
const node = useNodes((s) => hovered ? s.byId[hovered] : void 0);
|
|
419
|
+
const editing = useEditing((s) => s.node?.id === hovered);
|
|
420
|
+
if (!hovered || !node || dragging || editing) return null;
|
|
421
|
+
return /* @__PURE__ */ jsx6("div", { className: "connect-box", style: { left: node.x, top: node.y, width: node.w, height: node.h }, children: SIDES.map((side) => /* @__PURE__ */ jsx6(
|
|
422
|
+
"span",
|
|
423
|
+
{
|
|
424
|
+
className: `connect-handle connect-${side}`,
|
|
425
|
+
role: "presentation",
|
|
426
|
+
"data-connect": side,
|
|
427
|
+
title: `Drag onto another node to connect (${LABEL[side]})`,
|
|
428
|
+
onPointerEnter: () => instance.ui.connect.getState().hover(hovered),
|
|
429
|
+
onPointerLeave: () => instance.ui.connect.getState().leave(),
|
|
430
|
+
onPointerDown: (e) => beginConnect(instance, e, hovered, side)
|
|
431
|
+
},
|
|
432
|
+
side
|
|
433
|
+
)) });
|
|
434
|
+
}
|
|
435
|
+
function ConnectPreview() {
|
|
436
|
+
const { uid } = useDiagram();
|
|
437
|
+
const source = useConnecting((s) => s.source);
|
|
438
|
+
const side = useConnecting((s) => s.side);
|
|
439
|
+
const cursor = useConnecting((s) => s.cursor);
|
|
440
|
+
const target = useConnecting((s) => s.target);
|
|
441
|
+
const from = useNodes((s) => source ? s.byId[source] : void 0);
|
|
442
|
+
if (!source || !from || !cursor) return null;
|
|
443
|
+
const a = sidePoint(from, side);
|
|
444
|
+
return /* @__PURE__ */ jsx6("svg", { className: "connect-preview", width: 1, height: 1, "aria-hidden": "true", children: /* @__PURE__ */ jsx6("path", { d: `M ${a.x} ${a.y} L ${cursor.x} ${cursor.y}`, className: target ? "connect-line ready" : "connect-line", markerEnd: `url(#${arrowBase(uid)})` }) });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// packages/diagram-react/src/CanvasMenu.tsx
|
|
448
|
+
import { useEffect as useEffect4, useRef as useRef4 } from "react";
|
|
449
|
+
import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
450
|
+
var icon = { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true };
|
|
451
|
+
function CanvasMenu({ menu, onClose }) {
|
|
452
|
+
const { controller, ui } = useDiagram();
|
|
453
|
+
const ref = useRef4(null);
|
|
454
|
+
useEffect4(() => {
|
|
455
|
+
ref.current?.querySelector('[role="menuitem"]')?.focus();
|
|
456
|
+
const close = () => onClose();
|
|
457
|
+
const onPointer = (e) => {
|
|
458
|
+
if (!ref.current?.contains(e.target)) onClose();
|
|
459
|
+
};
|
|
460
|
+
window.addEventListener("pointerdown", onPointer, true);
|
|
461
|
+
window.addEventListener("blur", close);
|
|
462
|
+
window.addEventListener("resize", close);
|
|
463
|
+
window.addEventListener("wheel", close, { passive: true });
|
|
464
|
+
return () => {
|
|
465
|
+
window.removeEventListener("pointerdown", onPointer, true);
|
|
466
|
+
window.removeEventListener("blur", close);
|
|
467
|
+
window.removeEventListener("resize", close);
|
|
468
|
+
window.removeEventListener("wheel", close);
|
|
469
|
+
};
|
|
470
|
+
}, [onClose]);
|
|
471
|
+
const add = (kind) => {
|
|
472
|
+
onClose();
|
|
473
|
+
const id = kind === "node" ? addNodeAt(controller, menu.at.x, menu.at.y) : addGroupAt(controller, menu.at.x, menu.at.y);
|
|
474
|
+
if (!id) return;
|
|
475
|
+
if (kind === "node") {
|
|
476
|
+
ui.selection.getState().select({ kind: "node", id });
|
|
477
|
+
ui.edit.getState().editNode(id, "title");
|
|
478
|
+
} else {
|
|
479
|
+
ui.selection.getState().select({ kind: "group", id });
|
|
480
|
+
ui.edit.getState().editGroup(id);
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
const onKeyDown = (e) => {
|
|
484
|
+
const items = Array.from(ref.current?.querySelectorAll('[role="menuitem"]') ?? []);
|
|
485
|
+
const index = items.indexOf(document.activeElement);
|
|
486
|
+
if (e.key === "Escape") {
|
|
487
|
+
e.stopPropagation();
|
|
488
|
+
onClose();
|
|
489
|
+
} else if (e.key === "ArrowDown") items[(index + 1) % items.length]?.focus();
|
|
490
|
+
else if (e.key === "ArrowUp") items[(index - 1 + items.length) % items.length]?.focus();
|
|
491
|
+
else if (e.key === "Home") items[0]?.focus();
|
|
492
|
+
else if (e.key === "End") items[items.length - 1]?.focus();
|
|
493
|
+
else return;
|
|
494
|
+
e.preventDefault();
|
|
495
|
+
};
|
|
496
|
+
const left = Math.min(menu.screen.x, window.innerWidth - 200);
|
|
497
|
+
const top = Math.min(menu.screen.y, window.innerHeight - 100);
|
|
498
|
+
return /* @__PURE__ */ jsxs4(
|
|
499
|
+
"div",
|
|
500
|
+
{
|
|
501
|
+
ref,
|
|
502
|
+
className: "canvas-menu",
|
|
503
|
+
role: "menu",
|
|
504
|
+
"aria-label": "Canvas",
|
|
505
|
+
style: { left, top },
|
|
506
|
+
onKeyDown,
|
|
507
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
508
|
+
onContextMenu: (e) => e.preventDefault(),
|
|
509
|
+
children: [
|
|
510
|
+
/* @__PURE__ */ jsxs4("button", { type: "button", role: "menuitem", onClick: () => add("node"), children: [
|
|
511
|
+
/* @__PURE__ */ jsxs4("svg", { ...icon, children: [
|
|
512
|
+
/* @__PURE__ */ jsx7("path", { d: "M21 8 12 3 3 8v8l9 5 9-5z" }),
|
|
513
|
+
/* @__PURE__ */ jsx7("path", { d: "m3.3 7.5 8.7 5 8.7-5M12 22V12.5" })
|
|
514
|
+
] }),
|
|
515
|
+
" Add node"
|
|
516
|
+
] }),
|
|
517
|
+
/* @__PURE__ */ jsxs4("button", { type: "button", role: "menuitem", onClick: () => add("group"), children: [
|
|
518
|
+
/* @__PURE__ */ jsxs4("svg", { ...icon, children: [
|
|
519
|
+
/* @__PURE__ */ jsx7("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", strokeDasharray: "3 3" }),
|
|
520
|
+
/* @__PURE__ */ jsx7("rect", { x: "7", y: "7", width: "6", height: "5", rx: "1" }),
|
|
521
|
+
/* @__PURE__ */ jsx7("rect", { x: "12", y: "13", width: "5", height: "4", rx: "1" })
|
|
522
|
+
] }),
|
|
523
|
+
" Add group"
|
|
524
|
+
] })
|
|
525
|
+
]
|
|
526
|
+
}
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// packages/diagram-react/src/ResizeHandles.tsx
|
|
531
|
+
import { HANDLES } from "@glyphlab/diagram-core";
|
|
532
|
+
|
|
533
|
+
// packages/diagram-react/src/beginEdit.ts
|
|
534
|
+
import { membersOf } from "@glyphlab/diagram-core";
|
|
535
|
+
import { dragRect, minSize, rectPatch } from "@glyphlab/diagram-core";
|
|
536
|
+
var CLICK_SLOP = 3;
|
|
537
|
+
var read = ({ controller }, kind, id) => kind === "node" ? controller.stores.nodes.getState().byId[id] : controller.stores.groups.getState().byId[id];
|
|
538
|
+
function write({ controller }, kind, id, rect) {
|
|
539
|
+
if (kind === "node") controller.stores.nodes.getState().updateNode(id, rect);
|
|
540
|
+
else controller.stores.groups.getState().updateGroup(id, rect);
|
|
541
|
+
}
|
|
542
|
+
function carriedBy({ controller }, kind, id, mode) {
|
|
543
|
+
if (kind !== "group" || mode !== "move") return { nodes: [], groups: [] };
|
|
544
|
+
const snapshot = controller.getSnapshot();
|
|
545
|
+
const group = snapshot.groups.find((g) => g.id === id);
|
|
546
|
+
return group ? membersOf(group, snapshot.nodes, snapshot.groups) : { nodes: [], groups: [] };
|
|
547
|
+
}
|
|
548
|
+
function beginEdit(instance, e, kind, id, mode) {
|
|
549
|
+
const { controller, ui } = instance;
|
|
550
|
+
if (e.button !== 0) return;
|
|
551
|
+
e.stopPropagation();
|
|
552
|
+
ui.selection.getState().select({ kind, id });
|
|
553
|
+
const found = read(instance, kind, id);
|
|
554
|
+
if (!found) return;
|
|
555
|
+
const { title } = found;
|
|
556
|
+
const start = { x: found.x, y: found.y, w: found.w, h: found.h };
|
|
557
|
+
const scale = ui.viewport.getState().scale;
|
|
558
|
+
const origin = { x: e.clientX, y: e.clientY };
|
|
559
|
+
let moved = false;
|
|
560
|
+
let latest = start;
|
|
561
|
+
const carried = carriedBy(instance, kind, id, mode);
|
|
562
|
+
const carriedCount = carried.nodes.length + carried.groups.length;
|
|
563
|
+
const preview = (dx, dy) => {
|
|
564
|
+
for (const n of carried.nodes) controller.stores.nodes.getState().updateNode(n.id, { x: n.x + dx, y: n.y + dy });
|
|
565
|
+
for (const g of carried.groups) controller.stores.groups.getState().updateGroup(g.id, { x: g.x + dx, y: g.y + dy });
|
|
566
|
+
};
|
|
567
|
+
const onMove = (ev) => {
|
|
568
|
+
const dx = ev.clientX - origin.x;
|
|
569
|
+
const dy = ev.clientY - origin.y;
|
|
570
|
+
if (!moved && Math.hypot(dx, dy) < CLICK_SLOP) return;
|
|
571
|
+
moved = true;
|
|
572
|
+
latest = dragRect(start, mode, dx / scale, dy / scale, minSize(kind));
|
|
573
|
+
write(instance, kind, id, latest);
|
|
574
|
+
preview(latest.x - start.x, latest.y - start.y);
|
|
575
|
+
};
|
|
576
|
+
const stop = () => {
|
|
577
|
+
window.removeEventListener("pointermove", onMove);
|
|
578
|
+
window.removeEventListener("pointerup", onUp);
|
|
579
|
+
window.removeEventListener("pointercancel", onCancel);
|
|
580
|
+
window.removeEventListener("keydown", onKey, true);
|
|
581
|
+
};
|
|
582
|
+
const onUp = () => {
|
|
583
|
+
stop();
|
|
584
|
+
const patch = rectPatch(start, latest);
|
|
585
|
+
write(instance, kind, id, start);
|
|
586
|
+
preview(0, 0);
|
|
587
|
+
if (!moved || Object.keys(patch).length === 0) return;
|
|
588
|
+
const what = mode === "move" ? "Move" : "Resize";
|
|
589
|
+
controller.apply({
|
|
590
|
+
ops: [kind === "node" ? { op: "updateNode", id, patch } : { op: "updateGroup", id, patch }],
|
|
591
|
+
summary: `${what} ${kind} \u201C${title}\u201D${carriedCount > 0 ? ` with what is inside it` : ""}`,
|
|
592
|
+
source: "user"
|
|
593
|
+
});
|
|
594
|
+
};
|
|
595
|
+
const onCancel = () => {
|
|
596
|
+
stop();
|
|
597
|
+
write(instance, kind, id, start);
|
|
598
|
+
preview(0, 0);
|
|
599
|
+
};
|
|
600
|
+
const onKey = (ev) => {
|
|
601
|
+
if (ev.key !== "Escape") return;
|
|
602
|
+
ev.stopPropagation();
|
|
603
|
+
onCancel();
|
|
604
|
+
};
|
|
605
|
+
window.addEventListener("pointermove", onMove);
|
|
606
|
+
window.addEventListener("pointerup", onUp);
|
|
607
|
+
window.addEventListener("pointercancel", onCancel);
|
|
608
|
+
window.addEventListener("keydown", onKey, true);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// packages/diagram-react/src/ResizeHandles.tsx
|
|
612
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
613
|
+
var LABELS = {
|
|
614
|
+
n: "top",
|
|
615
|
+
s: "bottom",
|
|
616
|
+
e: "right",
|
|
617
|
+
w: "left",
|
|
618
|
+
ne: "top right",
|
|
619
|
+
nw: "top left",
|
|
620
|
+
se: "bottom right",
|
|
621
|
+
sw: "bottom left"
|
|
622
|
+
};
|
|
623
|
+
function ResizeHandles() {
|
|
624
|
+
const instance = useDiagram();
|
|
625
|
+
const selection = useSelection((s) => s.selection);
|
|
626
|
+
const rect = useNodes((s) => selection?.kind === "node" ? s.byId[selection.id] : void 0);
|
|
627
|
+
const groupRect = useGroups((s) => selection?.kind === "group" ? s.byId[selection.id] : void 0);
|
|
628
|
+
const scale = useViewport((s) => s.scale);
|
|
629
|
+
const target = rect ?? groupRect;
|
|
630
|
+
if (!selection || !target || selection.kind !== "node" && selection.kind !== "group") return null;
|
|
631
|
+
const inverse = Math.min(3, Math.max(0.75, 1 / scale));
|
|
632
|
+
return /* @__PURE__ */ jsx8(
|
|
633
|
+
"div",
|
|
634
|
+
{
|
|
635
|
+
className: "resize-box",
|
|
636
|
+
style: { left: target.x, top: target.y, width: target.w, height: target.h, ["--inv"]: inverse },
|
|
637
|
+
children: HANDLES.map((handle) => /* @__PURE__ */ jsx8(
|
|
638
|
+
"span",
|
|
639
|
+
{
|
|
640
|
+
className: `resize-handle resize-${handle}`,
|
|
641
|
+
role: "presentation",
|
|
642
|
+
"data-handle": handle,
|
|
643
|
+
title: `Resize ${LABELS[handle]}`,
|
|
644
|
+
onPointerDown: (e) => beginEdit(instance, e, selection.kind, selection.id, handle)
|
|
645
|
+
},
|
|
646
|
+
handle
|
|
647
|
+
))
|
|
648
|
+
}
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// packages/diagram-react/src/DiagramEdge.tsx
|
|
653
|
+
import { memo } from "react";
|
|
654
|
+
|
|
655
|
+
// packages/diagram-react/src/hooks.ts
|
|
656
|
+
import { useMemo } from "react";
|
|
657
|
+
import { edgeGeometry as edgeGeometry2 } from "@glyphlab/diagram-core";
|
|
658
|
+
function useIsSelected(kind, id) {
|
|
659
|
+
return useSelection((s) => s.selection?.kind === kind && s.selection.id === id);
|
|
660
|
+
}
|
|
661
|
+
function useEdgeGeometry(id) {
|
|
662
|
+
const edge = useEdges((s) => s.byId[id]);
|
|
663
|
+
const from = useNodes((s) => edge ? s.byId[edge.from] : void 0);
|
|
664
|
+
const to = useNodes((s) => edge ? s.byId[edge.to] : void 0);
|
|
665
|
+
const geometry = useMemo(() => from && to ? edgeGeometry2(from, to) : null, [from, to]);
|
|
666
|
+
return { edge, geometry };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// packages/diagram-react/src/DiagramEdge.tsx
|
|
670
|
+
import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
671
|
+
var DiagramEdge = memo(function DiagramEdge2({ id }) {
|
|
672
|
+
const instance = useDiagram();
|
|
673
|
+
const { edge, geometry } = useEdgeGeometry(id);
|
|
674
|
+
const selected = useIsSelected("edge", id);
|
|
675
|
+
const highlight = useHighlights((s) => s.edges[id]);
|
|
676
|
+
if (!edge || !geometry) return null;
|
|
677
|
+
const className = [
|
|
678
|
+
"edge",
|
|
679
|
+
edge.dashed ? "dashed" : "",
|
|
680
|
+
edge.optional ? "optional" : "",
|
|
681
|
+
edgeColor(edge.color) ? "colored" : "",
|
|
682
|
+
selected ? "selected" : "",
|
|
683
|
+
highlight ? `hl-${highlight}` : ""
|
|
684
|
+
].filter(Boolean).join(" ");
|
|
685
|
+
return /* @__PURE__ */ jsxs5("g", { children: [
|
|
686
|
+
/* @__PURE__ */ jsx9("path", { d: geometry.d, className, style: edgeStyle(edge.color), markerEnd: `url(#${arrowId(arrowBase(instance.uid), edge.color)})` }),
|
|
687
|
+
/* @__PURE__ */ jsx9(
|
|
688
|
+
"path",
|
|
689
|
+
{
|
|
690
|
+
d: geometry.d,
|
|
691
|
+
className: "edge-hit",
|
|
692
|
+
onPointerDown: (e) => {
|
|
693
|
+
e.stopPropagation();
|
|
694
|
+
instance.ui.selection.getState().select({ kind: "edge", id });
|
|
695
|
+
},
|
|
696
|
+
onDoubleClick: (e) => {
|
|
697
|
+
e.stopPropagation();
|
|
698
|
+
instance.ui.edit.getState().editEdge(id);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
)
|
|
702
|
+
] });
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// packages/diagram-react/src/DiagramGroup.tsx
|
|
706
|
+
import { memo as memo2, useState as useState3 } from "react";
|
|
707
|
+
import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
708
|
+
var DiagramGroup = memo2(function DiagramGroup2({ id }) {
|
|
709
|
+
const instance = useDiagram();
|
|
710
|
+
const group = useGroups((s) => s.byId[id]);
|
|
711
|
+
const selected = useIsSelected("group", id);
|
|
712
|
+
const [error, setError] = useState3("");
|
|
713
|
+
if (!group) return null;
|
|
714
|
+
const grab = (e) => beginEdit(instance, e, "group", id, "move");
|
|
715
|
+
const add = (x, y) => {
|
|
716
|
+
setError("");
|
|
717
|
+
const nodeId = addNodeAt(instance.controller, x, y, id, setError);
|
|
718
|
+
if (nodeId) {
|
|
719
|
+
instance.ui.selection.getState().select({ kind: "node", id: nodeId });
|
|
720
|
+
instance.ui.edit.getState().editNode(nodeId, "title");
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
return /* @__PURE__ */ jsxs6(
|
|
724
|
+
"div",
|
|
725
|
+
{
|
|
726
|
+
className: `group${group.lane ? " swimlane" : ""}${selected ? " selected" : ""}`,
|
|
727
|
+
"data-lane-id": group.lane ? id : void 0,
|
|
728
|
+
style: { left: group.x, top: group.y, width: group.w, height: group.h, ...layerStyle(group.color) },
|
|
729
|
+
children: [
|
|
730
|
+
/* @__PURE__ */ jsxs6(
|
|
731
|
+
"div",
|
|
732
|
+
{
|
|
733
|
+
className: "group-bar",
|
|
734
|
+
onPointerDown: grab,
|
|
735
|
+
onDoubleClick: (e) => {
|
|
736
|
+
e.stopPropagation();
|
|
737
|
+
instance.ui.edit.getState().editGroup(id);
|
|
738
|
+
},
|
|
739
|
+
children: [
|
|
740
|
+
/* @__PURE__ */ jsx10("span", { className: "group-title", children: group.title }),
|
|
741
|
+
group.lane && /* @__PURE__ */ jsx10("span", { className: "lane-description", children: group.lane.body || "Double-click to edit lane title and description" })
|
|
742
|
+
]
|
|
743
|
+
}
|
|
744
|
+
),
|
|
745
|
+
group.lane && /* @__PURE__ */ jsx10(
|
|
746
|
+
"div",
|
|
747
|
+
{
|
|
748
|
+
className: "lane-content",
|
|
749
|
+
role: "button",
|
|
750
|
+
tabIndex: 0,
|
|
751
|
+
"aria-label": `Add node to ${group.title}`,
|
|
752
|
+
title: "Double-click to add a node",
|
|
753
|
+
onPointerDown: (e) => {
|
|
754
|
+
e.stopPropagation();
|
|
755
|
+
instance.ui.selection.getState().select({ kind: "group", id });
|
|
756
|
+
},
|
|
757
|
+
onDoubleClick: (e) => {
|
|
758
|
+
e.stopPropagation();
|
|
759
|
+
const at = toDiagram(instance, e.clientX, e.clientY);
|
|
760
|
+
add(at.x, at.y);
|
|
761
|
+
},
|
|
762
|
+
onKeyDown: (e) => {
|
|
763
|
+
if (e.key === "Enter") {
|
|
764
|
+
e.preventDefault();
|
|
765
|
+
add(group.x + group.w / 2, group.y + 180);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
),
|
|
770
|
+
error && /* @__PURE__ */ jsx10("span", { className: "lane-error", role: "alert", children: error }),
|
|
771
|
+
/* @__PURE__ */ jsx10("span", { className: "group-edge group-edge-s", onPointerDown: grab }),
|
|
772
|
+
/* @__PURE__ */ jsx10("span", { className: "group-edge group-edge-w", onPointerDown: grab }),
|
|
773
|
+
/* @__PURE__ */ jsx10("span", { className: "group-edge group-edge-e", onPointerDown: grab })
|
|
774
|
+
]
|
|
775
|
+
}
|
|
776
|
+
);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// packages/diagram-react/src/DiagramNode.tsx
|
|
780
|
+
import { memo as memo3 } from "react";
|
|
781
|
+
|
|
782
|
+
// packages/diagram-react/src/LinkBadge.tsx
|
|
783
|
+
import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
784
|
+
var LABEL2 = {
|
|
785
|
+
missing: "Linked code is missing",
|
|
786
|
+
changed: "Possibly stale: linked code changed",
|
|
787
|
+
moved: "Linked code moved",
|
|
788
|
+
unchecked: "Links not checked"
|
|
789
|
+
};
|
|
790
|
+
var SHAPE = {
|
|
791
|
+
changed: /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
792
|
+
/* @__PURE__ */ jsx11("circle", { cx: "12", cy: "12", r: "9" }),
|
|
793
|
+
/* @__PURE__ */ jsx11("path", { d: "M12 7v5l3 2" })
|
|
794
|
+
] }),
|
|
795
|
+
missing: /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
796
|
+
/* @__PURE__ */ jsx11("circle", { cx: "12", cy: "12", r: "9" }),
|
|
797
|
+
/* @__PURE__ */ jsx11("path", { d: "M12 7v6M12 16.5v.5" })
|
|
798
|
+
] }),
|
|
799
|
+
moved: /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
800
|
+
/* @__PURE__ */ jsx11("circle", { cx: "12", cy: "12", r: "9" }),
|
|
801
|
+
/* @__PURE__ */ jsx11("path", { d: "M7.5 12h8M12.5 8.5 16 12l-3.5 3.5" })
|
|
802
|
+
] }),
|
|
803
|
+
unchecked: /* @__PURE__ */ jsx11("circle", { cx: "12", cy: "12", r: "6" })
|
|
804
|
+
};
|
|
805
|
+
function LinkBadge({ title, state, onOpen }) {
|
|
806
|
+
if (!state || state.status === "ok") return null;
|
|
807
|
+
const status = state.status;
|
|
808
|
+
const label = `${LABEL2[status]}${state.detail ? `: ${state.detail}` : ""}`;
|
|
809
|
+
return /* @__PURE__ */ jsxs7(
|
|
810
|
+
"button",
|
|
811
|
+
{
|
|
812
|
+
type: "button",
|
|
813
|
+
className: `node-link-badge link-${status}`,
|
|
814
|
+
"aria-label": `${label} (${title}). Open its code links.`,
|
|
815
|
+
title: label,
|
|
816
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
817
|
+
onDoubleClick: (e) => e.stopPropagation(),
|
|
818
|
+
onClick: (e) => {
|
|
819
|
+
e.stopPropagation();
|
|
820
|
+
onOpen?.();
|
|
821
|
+
},
|
|
822
|
+
children: [
|
|
823
|
+
/* @__PURE__ */ jsx11("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: SHAPE[status] }),
|
|
824
|
+
state.count && state.count > 1 ? /* @__PURE__ */ jsx11("span", { className: "node-link-count", children: state.count }) : null
|
|
825
|
+
]
|
|
826
|
+
}
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// packages/diagram-react/src/NodeEditor.tsx
|
|
831
|
+
import { useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "react";
|
|
832
|
+
import { LIMITS as LIMITS3 } from "@glyphlab/diagram-core";
|
|
833
|
+
import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
834
|
+
function NodeEditor({ node, field }) {
|
|
835
|
+
const { controller, ui } = useDiagram();
|
|
836
|
+
const [title, setTitle] = useState4(node.title);
|
|
837
|
+
const [body, setBody] = useState4(node.body);
|
|
838
|
+
const titleRef = useRef5(null);
|
|
839
|
+
const bodyRef = useRef5(null);
|
|
840
|
+
const done = useRef5(false);
|
|
841
|
+
useEffect5(() => {
|
|
842
|
+
const el = field === "title" ? titleRef.current : bodyRef.current;
|
|
843
|
+
el?.focus();
|
|
844
|
+
el?.select();
|
|
845
|
+
}, []);
|
|
846
|
+
const finish = (save) => {
|
|
847
|
+
if (done.current) return;
|
|
848
|
+
done.current = true;
|
|
849
|
+
const trimmed = title.trim();
|
|
850
|
+
if (save && trimmed !== "") {
|
|
851
|
+
const patch = {};
|
|
852
|
+
if (trimmed !== node.title) patch.title = trimmed;
|
|
853
|
+
if (body !== node.body) patch.body = body;
|
|
854
|
+
if (Object.keys(patch).length > 0) {
|
|
855
|
+
controller.apply({ ops: [{ op: "updateNode", id: node.id, patch }], summary: `Edit node \u201C${node.title}\u201D`, source: "user" });
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
ui.edit.getState().end();
|
|
859
|
+
};
|
|
860
|
+
return /* @__PURE__ */ jsxs8(
|
|
861
|
+
"div",
|
|
862
|
+
{
|
|
863
|
+
className: "node-editor",
|
|
864
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
865
|
+
onDoubleClick: (e) => e.stopPropagation(),
|
|
866
|
+
onBlur: (e) => {
|
|
867
|
+
if (!e.currentTarget.contains(e.relatedTarget)) finish(true);
|
|
868
|
+
},
|
|
869
|
+
onKeyDown: (e) => {
|
|
870
|
+
if (e.key === "Escape") {
|
|
871
|
+
e.stopPropagation();
|
|
872
|
+
finish(false);
|
|
873
|
+
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
874
|
+
e.preventDefault();
|
|
875
|
+
finish(true);
|
|
876
|
+
}
|
|
877
|
+
},
|
|
878
|
+
children: [
|
|
879
|
+
/* @__PURE__ */ jsx12(
|
|
880
|
+
"input",
|
|
881
|
+
{
|
|
882
|
+
ref: titleRef,
|
|
883
|
+
className: "node-edit-title",
|
|
884
|
+
"aria-label": "Node title",
|
|
885
|
+
value: title,
|
|
886
|
+
maxLength: LIMITS3.title,
|
|
887
|
+
onChange: (e) => setTitle(e.target.value),
|
|
888
|
+
onKeyDown: (e) => {
|
|
889
|
+
if (e.key === "Enter" && !e.metaKey && !e.ctrlKey) {
|
|
890
|
+
e.preventDefault();
|
|
891
|
+
bodyRef.current?.focus();
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
),
|
|
896
|
+
/* @__PURE__ */ jsx12(
|
|
897
|
+
"textarea",
|
|
898
|
+
{
|
|
899
|
+
ref: bodyRef,
|
|
900
|
+
className: "node-edit-body",
|
|
901
|
+
"aria-label": "Node content (Markdown)",
|
|
902
|
+
value: body,
|
|
903
|
+
maxLength: LIMITS3.body,
|
|
904
|
+
spellCheck: false,
|
|
905
|
+
onChange: (e) => setBody(e.target.value)
|
|
906
|
+
}
|
|
907
|
+
)
|
|
908
|
+
]
|
|
909
|
+
}
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// packages/diagram-react/src/DiagramNode.tsx
|
|
914
|
+
import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
915
|
+
var DiagramNode = memo3(function DiagramNode2({ id }) {
|
|
916
|
+
const instance = useDiagram();
|
|
917
|
+
const { renderMarkdown, openNodeContent, useLinkState } = useSlots();
|
|
918
|
+
const linkState = useLinkState?.(id);
|
|
919
|
+
const node = useNodes((s) => s.byId[id]);
|
|
920
|
+
const selected = useIsSelected("node", id);
|
|
921
|
+
const editing = useEditing((s) => s.node?.id === id ? s.node : null);
|
|
922
|
+
const isTarget = useConnecting((s) => s.target === id);
|
|
923
|
+
const highlight = useHighlights((s) => s.nodes[id]);
|
|
924
|
+
if (!node) return null;
|
|
925
|
+
return /* @__PURE__ */ jsx13(
|
|
926
|
+
"article",
|
|
927
|
+
{
|
|
928
|
+
"data-node-id": id,
|
|
929
|
+
className: `node${selected ? " selected" : ""}${editing ? " editing" : ""}${isTarget ? " connect-target" : ""}${highlight ? ` hl-${highlight}` : ""}`,
|
|
930
|
+
style: { left: node.x, top: node.y, width: node.w, height: node.h, ...layerStyle(node.color) },
|
|
931
|
+
onPointerDown: (e) => {
|
|
932
|
+
if (editing || e.target instanceof Element && e.target.closest("a, button")) e.stopPropagation();
|
|
933
|
+
else beginEdit(instance, e, "node", id, "move");
|
|
934
|
+
},
|
|
935
|
+
onDoubleClick: (e) => {
|
|
936
|
+
e.stopPropagation();
|
|
937
|
+
if (e.target instanceof Element && e.target.closest("a, button")) return;
|
|
938
|
+
const onTitle = e.target.closest("h2") !== null;
|
|
939
|
+
instance.ui.edit.getState().editNode(id, onTitle ? "title" : "body");
|
|
940
|
+
},
|
|
941
|
+
onPointerEnter: () => instance.ui.connect.getState().hover(id),
|
|
942
|
+
onPointerLeave: () => instance.ui.connect.getState().leave(),
|
|
943
|
+
children: editing ? /* @__PURE__ */ jsx13(NodeEditor, { node, field: editing.field }) : /* @__PURE__ */ jsxs9(Fragment3, { children: [
|
|
944
|
+
/* @__PURE__ */ jsx13("h2", { children: node.title }),
|
|
945
|
+
openNodeContent && /* @__PURE__ */ jsx13(
|
|
946
|
+
LinkBadge,
|
|
947
|
+
{
|
|
948
|
+
title: node.title,
|
|
949
|
+
state: linkState,
|
|
950
|
+
onOpen: () => {
|
|
951
|
+
instance.ui.selection.getState().select({ kind: "node", id });
|
|
952
|
+
openNodeContent(id, "code");
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
),
|
|
956
|
+
openNodeContent && /* @__PURE__ */ jsx13(
|
|
957
|
+
"button",
|
|
958
|
+
{
|
|
959
|
+
className: "node-expand",
|
|
960
|
+
type: "button",
|
|
961
|
+
"aria-label": `Open full content for ${node.title}`,
|
|
962
|
+
title: "View and edit full content",
|
|
963
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
964
|
+
onDoubleClick: (e) => e.stopPropagation(),
|
|
965
|
+
onClick: (e) => {
|
|
966
|
+
e.stopPropagation();
|
|
967
|
+
instance.ui.selection.getState().select({ kind: "node", id });
|
|
968
|
+
openNodeContent(id);
|
|
969
|
+
},
|
|
970
|
+
children: /* @__PURE__ */ jsx13("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: /* @__PURE__ */ jsx13("path", { d: "M8 3H3v5m13-5h5v5M3 16v5h5m13-5v5h-5M3 3l6 6m12-6-6 6M3 21l6-6m12 6-6-6" }) })
|
|
971
|
+
}
|
|
972
|
+
),
|
|
973
|
+
openNodeContent && node.decisions?.trim() && /* @__PURE__ */ jsxs9(
|
|
974
|
+
"button",
|
|
975
|
+
{
|
|
976
|
+
className: "node-decisions",
|
|
977
|
+
type: "button",
|
|
978
|
+
"aria-label": `Open the decision log for ${node.title}`,
|
|
979
|
+
title: "Decisions recorded: why this was chosen and what else was considered",
|
|
980
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
981
|
+
onDoubleClick: (e) => e.stopPropagation(),
|
|
982
|
+
onClick: (e) => {
|
|
983
|
+
e.stopPropagation();
|
|
984
|
+
instance.ui.selection.getState().select({ kind: "node", id });
|
|
985
|
+
openNodeContent(id, "decisions");
|
|
986
|
+
},
|
|
987
|
+
children: [
|
|
988
|
+
/* @__PURE__ */ jsx13("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx13("path", { d: "M16 3h5v5M8 3H3v5M12 22v-8.3a4 4 0 0 0-1.17-2.87L3 3M15 9l6-6" }) }),
|
|
989
|
+
"Decisions"
|
|
990
|
+
]
|
|
991
|
+
}
|
|
992
|
+
),
|
|
993
|
+
renderMarkdown ? renderMarkdown(node.body) : /* @__PURE__ */ jsx13("div", { className: "dg-plain", children: node.body })
|
|
994
|
+
] })
|
|
995
|
+
}
|
|
996
|
+
);
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// packages/diagram-react/src/EdgeLabel.tsx
|
|
1000
|
+
import { memo as memo4 } from "react";
|
|
1001
|
+
import { labelWidth } from "@glyphlab/diagram-core";
|
|
1002
|
+
import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1003
|
+
var EdgeLabel = memo4(function EdgeLabel2({ id }) {
|
|
1004
|
+
const { edge, geometry } = useEdgeGeometry(id);
|
|
1005
|
+
const selected = useIsSelected("edge", id);
|
|
1006
|
+
const highlight = useHighlights((s) => s.edges[id]);
|
|
1007
|
+
if (!edge || !geometry || !edge.label) return null;
|
|
1008
|
+
const width = labelWidth(edge);
|
|
1009
|
+
const className = ["edge-label", selected ? "selected" : "", highlight ? `hl-${highlight}` : ""].filter(Boolean).join(" ");
|
|
1010
|
+
return /* @__PURE__ */ jsxs10("g", { className, transform: `translate(${geometry.lx}, ${geometry.ly})`, children: [
|
|
1011
|
+
/* @__PURE__ */ jsx14("rect", { x: -width / 2, y: -9, width, height: 18, rx: 4, className: "label-bg" }),
|
|
1012
|
+
/* @__PURE__ */ jsx14("text", { textAnchor: "middle", dy: "3.5", className: "label-text", children: edge.label })
|
|
1013
|
+
] });
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// packages/diagram-react/src/DiagramLayers.tsx
|
|
1017
|
+
import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1018
|
+
function GroupLayer() {
|
|
1019
|
+
const ids = useGroups((s) => s.ids);
|
|
1020
|
+
return ids.map((id) => /* @__PURE__ */ jsx15(DiagramGroup, { id }, id));
|
|
1021
|
+
}
|
|
1022
|
+
function EdgeLayer({ bounds }) {
|
|
1023
|
+
const { uid } = useDiagram();
|
|
1024
|
+
const ids = useEdges((s) => s.ids);
|
|
1025
|
+
const byId = useEdges((s) => s.byId);
|
|
1026
|
+
return /* @__PURE__ */ jsxs11("svg", { className: "edges", width: bounds.w, height: bounds.h, "aria-hidden": "true", children: [
|
|
1027
|
+
/* @__PURE__ */ jsx15("defs", { children: /* @__PURE__ */ jsx15(ArrowHeads, { id: arrowBase(uid), colors: ids.map((id) => byId[id].color) }) }),
|
|
1028
|
+
ids.map((id) => /* @__PURE__ */ jsx15(DiagramEdge, { id }, id))
|
|
1029
|
+
] });
|
|
1030
|
+
}
|
|
1031
|
+
function NodeLayer() {
|
|
1032
|
+
const ids = useNodes((s) => s.ids);
|
|
1033
|
+
return ids.map((id) => /* @__PURE__ */ jsx15(DiagramNode, { id }, id));
|
|
1034
|
+
}
|
|
1035
|
+
function EdgeLabelLayer({ bounds }) {
|
|
1036
|
+
const ids = useEdges((s) => s.ids);
|
|
1037
|
+
return /* @__PURE__ */ jsx15("svg", { className: "labels", width: bounds.w, height: bounds.h, "aria-hidden": "true", children: ids.map((id) => /* @__PURE__ */ jsx15(EdgeLabel, { id }, id)) });
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// packages/diagram-react/src/EdgeStyleControl.tsx
|
|
1041
|
+
import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1042
|
+
function EdgeStyleControl() {
|
|
1043
|
+
const { controller } = useDiagram();
|
|
1044
|
+
const selection = useSelection((s) => s.selection);
|
|
1045
|
+
const edge = useEdges((s) => selection?.kind === "edge" ? s.byId[selection.id] : void 0);
|
|
1046
|
+
if (!selection || selection.kind !== "edge" || !edge) return null;
|
|
1047
|
+
const dashed = edge.dashed === true;
|
|
1048
|
+
const choose = (next) => {
|
|
1049
|
+
if (next === dashed) return;
|
|
1050
|
+
controller.apply({
|
|
1051
|
+
ops: [{ op: "updateEdge", id: edge.id, patch: { dashed: next } }],
|
|
1052
|
+
summary: `Make the edge ${edge.label ? `\u201C${edge.label}\u201D ` : ""}${next ? "dashed" : "solid"}`,
|
|
1053
|
+
source: "user"
|
|
1054
|
+
});
|
|
1055
|
+
};
|
|
1056
|
+
return /* @__PURE__ */ jsxs12("div", { className: "edge-style-control", role: "group", "aria-label": "Line style of the selected edge", onPointerDown: (e) => e.stopPropagation(), onContextMenu: (e) => e.stopPropagation(), children: [
|
|
1057
|
+
/* @__PURE__ */ jsx16("button", { type: "button", className: "edge-style-btn", "aria-pressed": !dashed, title: "Solid line", "aria-label": "Solid line", onClick: () => choose(false), children: /* @__PURE__ */ jsx16("svg", { width: "28", height: "10", viewBox: "0 0 28 10", "aria-hidden": "true", children: /* @__PURE__ */ jsx16("path", { d: "M2 5h24", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }) }),
|
|
1058
|
+
/* @__PURE__ */ jsx16("button", { type: "button", className: "edge-style-btn", "aria-pressed": dashed, title: "Dashed line", "aria-label": "Dashed line", onClick: () => choose(true), children: /* @__PURE__ */ jsx16("svg", { width: "28", height: "10", viewBox: "0 0 28 10", "aria-hidden": "true", children: /* @__PURE__ */ jsx16("path", { d: "M2 5h24", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeDasharray: "5 4" }) }) })
|
|
1059
|
+
] });
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// packages/diagram-react/src/SwimlaneControls.tsx
|
|
1063
|
+
import { useState as useState5 } from "react";
|
|
1064
|
+
import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1065
|
+
function SwimlaneControls() {
|
|
1066
|
+
const { controller, ui } = useDiagram();
|
|
1067
|
+
const type = useMeta((s) => s.type);
|
|
1068
|
+
const groups = useGroups((s) => s.byId);
|
|
1069
|
+
const selection = useSelection((s) => s.selection);
|
|
1070
|
+
const [error, setError] = useState5("");
|
|
1071
|
+
const lanes = Object.values(groups).filter((g) => g.lane).sort((a, b) => a.lane?.orientation === "vertical" ? a.x - b.x : a.y - b.y);
|
|
1072
|
+
if (type !== "swimlane" && lanes.length === 0) return null;
|
|
1073
|
+
const selected = selection?.kind === "group" ? lanes.find((g) => g.id === selection.id) : void 0;
|
|
1074
|
+
const orientation = lanes[0]?.lane?.orientation ?? "horizontal";
|
|
1075
|
+
const apply = (ops, summary) => {
|
|
1076
|
+
const result = controller.apply({ ops, summary, source: "user" });
|
|
1077
|
+
setError(result.ok ? "" : result.message);
|
|
1078
|
+
return result.ok;
|
|
1079
|
+
};
|
|
1080
|
+
const reorder = (direction) => {
|
|
1081
|
+
if (!selected) return;
|
|
1082
|
+
const ids = lanes.map((g) => g.id);
|
|
1083
|
+
const index = ids.indexOf(selected.id);
|
|
1084
|
+
const other = index + direction;
|
|
1085
|
+
if (other < 0 || other >= ids.length) return;
|
|
1086
|
+
[ids[index], ids[other]] = [ids[other], ids[index]];
|
|
1087
|
+
apply([{ op: "arrangeLanes", orientation, laneIds: ids }], "Reorder swimlanes");
|
|
1088
|
+
};
|
|
1089
|
+
return /* @__PURE__ */ jsxs13("div", { className: "swimlane-controls", onPointerDown: (e) => e.stopPropagation(), children: [
|
|
1090
|
+
/* @__PURE__ */ jsx17("button", { className: "dg-btn", type: "button", onClick: () => {
|
|
1091
|
+
const before = new Set(lanes.map((g) => g.id));
|
|
1092
|
+
if (!apply([{ op: "createLane", lane: { title: `Lane ${lanes.length + 1}`, body: "", orientation } }], "Add swimlane")) return;
|
|
1093
|
+
const lane = controller.getSnapshot().groups.find((g) => g.lane && !before.has(g.id));
|
|
1094
|
+
if (lane) {
|
|
1095
|
+
ui.selection.getState().select({ kind: "group", id: lane.id });
|
|
1096
|
+
ui.edit.getState().editGroup(lane.id);
|
|
1097
|
+
}
|
|
1098
|
+
}, children: "Add lane" }),
|
|
1099
|
+
/* @__PURE__ */ jsxs13("select", { "aria-label": "Lane orientation", value: orientation, disabled: lanes.length === 0, onChange: (e) => {
|
|
1100
|
+
const next = e.target.value === "vertical" ? "vertical" : "horizontal";
|
|
1101
|
+
apply([{ op: "arrangeLanes", orientation: next }], "Change swimlane orientation");
|
|
1102
|
+
}, children: [
|
|
1103
|
+
/* @__PURE__ */ jsx17("option", { value: "horizontal", children: "Horizontal lanes" }),
|
|
1104
|
+
/* @__PURE__ */ jsx17("option", { value: "vertical", children: "Vertical lanes" })
|
|
1105
|
+
] }),
|
|
1106
|
+
selected && /* @__PURE__ */ jsxs13(Fragment4, { children: [
|
|
1107
|
+
/* @__PURE__ */ jsx17("button", { className: "dg-btn", type: "button", onClick: () => ui.edit.getState().editGroup(selected.id), children: "Edit lane" }),
|
|
1108
|
+
/* @__PURE__ */ jsx17("button", { className: "dg-btn", type: "button", "aria-label": "Move lane earlier", disabled: lanes[0].id === selected.id, onClick: () => reorder(-1), children: "Earlier" }),
|
|
1109
|
+
/* @__PURE__ */ jsx17("button", { className: "dg-btn", type: "button", "aria-label": "Move lane later", disabled: lanes.at(-1)?.id === selected.id, onClick: () => reorder(1), children: "Later" }),
|
|
1110
|
+
/* @__PURE__ */ jsx17("button", { className: "dg-btn", type: "button", onClick: () => apply([{ op: "deleteLane", id: selected.id }], "Delete swimlane (keep nodes)"), children: "Delete lane" })
|
|
1111
|
+
] }),
|
|
1112
|
+
error && /* @__PURE__ */ jsx17("span", { role: "alert", children: error })
|
|
1113
|
+
] });
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// packages/diagram-react/src/DiagramToolbar.tsx
|
|
1117
|
+
import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1118
|
+
function DiagramToolbar({ title }) {
|
|
1119
|
+
return /* @__PURE__ */ jsxs14("div", { className: "canvas-head", children: [
|
|
1120
|
+
/* @__PURE__ */ jsx18("h1", { children: title }),
|
|
1121
|
+
/* @__PURE__ */ jsxs14("div", { className: "canvas-tools", children: [
|
|
1122
|
+
/* @__PURE__ */ jsx18(EdgeStyleControl, {}),
|
|
1123
|
+
/* @__PURE__ */ jsx18(ColorControl, {})
|
|
1124
|
+
] }),
|
|
1125
|
+
/* @__PURE__ */ jsx18(SwimlaneControls, {})
|
|
1126
|
+
] });
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// packages/diagram-react/src/ZoomControls.tsx
|
|
1130
|
+
import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
1131
|
+
var STEP = 1.2;
|
|
1132
|
+
function ZoomControls({ config }) {
|
|
1133
|
+
const { onZoom, onFit } = config;
|
|
1134
|
+
const scale = useViewport((s) => s.scale);
|
|
1135
|
+
return /* @__PURE__ */ jsxs15("div", { className: "zoom", onPointerDown: (e) => e.stopPropagation(), children: [
|
|
1136
|
+
/* @__PURE__ */ jsx19("button", { className: "dg-icon-btn", type: "button", onClick: () => onZoom(STEP), "aria-label": "Zoom in", children: "+" }),
|
|
1137
|
+
/* @__PURE__ */ jsxs15("span", { className: "zoom-pct", children: [
|
|
1138
|
+
Math.round(scale * 100),
|
|
1139
|
+
"%"
|
|
1140
|
+
] }),
|
|
1141
|
+
/* @__PURE__ */ jsx19("button", { className: "dg-icon-btn", type: "button", onClick: () => onZoom(1 / STEP), "aria-label": "Zoom out", children: "\u2212" }),
|
|
1142
|
+
/* @__PURE__ */ jsx19("button", { className: "dg-btn", type: "button", onClick: onFit, children: "Fit canvas" })
|
|
1143
|
+
] });
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// packages/diagram-react/src/DiagramCanvas.tsx
|
|
1147
|
+
import { jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
1148
|
+
var GRID = 24;
|
|
1149
|
+
var CLICK_SLOP2 = 3;
|
|
1150
|
+
function DiagramCanvas({ title, designKey, empty: emptyMessage }) {
|
|
1151
|
+
const instance = useDiagram();
|
|
1152
|
+
const { controller, ui } = instance;
|
|
1153
|
+
const ref = useRef6(null);
|
|
1154
|
+
const drag = useRef6(null);
|
|
1155
|
+
const [menu, setMenu] = useState6(null);
|
|
1156
|
+
const x = useViewport((s) => s.x);
|
|
1157
|
+
const y = useViewport((s) => s.y);
|
|
1158
|
+
const scale = useViewport((s) => s.scale);
|
|
1159
|
+
const panning = useViewport((s) => s.panning);
|
|
1160
|
+
const groupsById = useGroups((s) => s.byId);
|
|
1161
|
+
const dimmed = useHighlights((s) => s.dimmed);
|
|
1162
|
+
const noNodes = useNodes((s) => s.ids.length === 0);
|
|
1163
|
+
const noGroups = useGroups((s) => s.ids.length === 0);
|
|
1164
|
+
const empty = noNodes && noGroups;
|
|
1165
|
+
const nodesById = useNodes((s) => s.byId);
|
|
1166
|
+
const bounds = useMemo2(() => diagramBounds(Object.values(groupsById), Object.values(nodesById)), [groupsById, nodesById]);
|
|
1167
|
+
const size = useCallback(() => {
|
|
1168
|
+
const el = ref.current;
|
|
1169
|
+
return { width: el?.clientWidth ?? 0, height: el?.clientHeight ?? 0 };
|
|
1170
|
+
}, []);
|
|
1171
|
+
const fit = useCallback(() => ui.viewport.getState().fit(size(), bounds), [ui, size, bounds]);
|
|
1172
|
+
const zoomCenter = useCallback(
|
|
1173
|
+
(factor) => {
|
|
1174
|
+
const { width, height } = size();
|
|
1175
|
+
ui.viewport.getState().zoomAround(factor, width / 2, height / 2);
|
|
1176
|
+
},
|
|
1177
|
+
[ui, size]
|
|
1178
|
+
);
|
|
1179
|
+
useLayoutEffect(() => {
|
|
1180
|
+
ui.viewport.getState().reset(size(), bounds);
|
|
1181
|
+
}, [designKey]);
|
|
1182
|
+
useEffect6(() => {
|
|
1183
|
+
const el = ref.current;
|
|
1184
|
+
if (!el) return;
|
|
1185
|
+
instance.surface.el = el;
|
|
1186
|
+
const onWheel = (e) => {
|
|
1187
|
+
if (e.target?.closest?.("textarea")) return;
|
|
1188
|
+
e.preventDefault();
|
|
1189
|
+
const viewport = ui.viewport.getState();
|
|
1190
|
+
if (e.ctrlKey || e.metaKey) {
|
|
1191
|
+
const rect = el.getBoundingClientRect();
|
|
1192
|
+
viewport.zoomAround(Math.exp(-e.deltaY * 0.01), e.clientX - rect.left, e.clientY - rect.top);
|
|
1193
|
+
} else {
|
|
1194
|
+
viewport.panBy(-e.deltaX, -e.deltaY);
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
el.addEventListener("wheel", onWheel, { passive: false });
|
|
1198
|
+
return () => {
|
|
1199
|
+
el.removeEventListener("wheel", onWheel);
|
|
1200
|
+
if (instance.surface.el === el) instance.surface.el = null;
|
|
1201
|
+
};
|
|
1202
|
+
}, [instance, ui]);
|
|
1203
|
+
useEffect6(() => {
|
|
1204
|
+
const onKeyDown = (e) => {
|
|
1205
|
+
if (e.key === "Escape") ui.selection.getState().clearSelection();
|
|
1206
|
+
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
|
1207
|
+
if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey || e.defaultPrevented) return;
|
|
1208
|
+
if (e.target?.closest?.("input, textarea, select, [contenteditable]")) return;
|
|
1209
|
+
const target = e.target;
|
|
1210
|
+
if (target && target !== document.body && !ref.current?.contains(target)) return;
|
|
1211
|
+
const { selection } = ui.selection.getState();
|
|
1212
|
+
if (!selection) return;
|
|
1213
|
+
e.preventDefault();
|
|
1214
|
+
if (selection.kind === "node") deleteNode(controller, selection.id);
|
|
1215
|
+
else if (selection.kind === "group") deleteGroup(controller, selection.id);
|
|
1216
|
+
else deleteEdge(controller, selection.id);
|
|
1217
|
+
};
|
|
1218
|
+
window.addEventListener("keydown", onKeyDown);
|
|
1219
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
1220
|
+
}, [controller, ui]);
|
|
1221
|
+
const onContextMenu = (e) => {
|
|
1222
|
+
const target = e.target;
|
|
1223
|
+
if (target.closest("input, textarea, [contenteditable]")) return;
|
|
1224
|
+
e.preventDefault();
|
|
1225
|
+
if (target.closest(".node, .group-bar, .group-edge, .edge-hit, .resize-handle, .connect-handle, .canvas-head, .zoom, .canvas-empty")) {
|
|
1226
|
+
setMenu(null);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
setMenu({ screen: { x: e.clientX, y: e.clientY }, at: toDiagram(instance, e.clientX, e.clientY) });
|
|
1230
|
+
};
|
|
1231
|
+
const onPointerDown = (e) => {
|
|
1232
|
+
if (e.button !== 0) return;
|
|
1233
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
1234
|
+
const { x: vx, y: vy, setPanning } = ui.viewport.getState();
|
|
1235
|
+
drag.current = { px: e.clientX, py: e.clientY, vx, vy, moved: false };
|
|
1236
|
+
setPanning(true);
|
|
1237
|
+
};
|
|
1238
|
+
const onPointerMove = (e) => {
|
|
1239
|
+
const d = drag.current;
|
|
1240
|
+
if (!d) return;
|
|
1241
|
+
const dx = e.clientX - d.px;
|
|
1242
|
+
const dy = e.clientY - d.py;
|
|
1243
|
+
if (Math.abs(dx) > CLICK_SLOP2 || Math.abs(dy) > CLICK_SLOP2) d.moved = true;
|
|
1244
|
+
ui.viewport.getState().setPosition(d.vx + dx, d.vy + dy);
|
|
1245
|
+
};
|
|
1246
|
+
const endDrag = () => {
|
|
1247
|
+
const d = drag.current;
|
|
1248
|
+
drag.current = null;
|
|
1249
|
+
ui.viewport.getState().setPanning(false);
|
|
1250
|
+
if (d && !d.moved) ui.selection.getState().clearSelection();
|
|
1251
|
+
};
|
|
1252
|
+
return /* @__PURE__ */ jsxs16(
|
|
1253
|
+
"section",
|
|
1254
|
+
{
|
|
1255
|
+
ref,
|
|
1256
|
+
className: panning ? "canvas dragging" : "canvas",
|
|
1257
|
+
style: {
|
|
1258
|
+
backgroundSize: `${GRID * scale}px ${GRID * scale}px`,
|
|
1259
|
+
backgroundPosition: `${x}px ${y}px`
|
|
1260
|
+
},
|
|
1261
|
+
onPointerDown,
|
|
1262
|
+
onContextMenu,
|
|
1263
|
+
onPointerMove,
|
|
1264
|
+
onPointerUp: endDrag,
|
|
1265
|
+
onPointerCancel: endDrag,
|
|
1266
|
+
children: [
|
|
1267
|
+
/* @__PURE__ */ jsx20(DiagramToolbar, { title }),
|
|
1268
|
+
menu && /* @__PURE__ */ jsx20(CanvasMenu, { menu, onClose: () => setMenu(null) }),
|
|
1269
|
+
/* @__PURE__ */ jsxs16(
|
|
1270
|
+
"div",
|
|
1271
|
+
{
|
|
1272
|
+
className: dimmed ? "world focus" : "world",
|
|
1273
|
+
style: { transform: `translate(${x}px, ${y}px) scale(${scale})` },
|
|
1274
|
+
children: [
|
|
1275
|
+
/* @__PURE__ */ jsx20(GroupLayer, {}),
|
|
1276
|
+
/* @__PURE__ */ jsx20(EdgeLayer, { bounds }),
|
|
1277
|
+
/* @__PURE__ */ jsx20(NodeLayer, {}),
|
|
1278
|
+
/* @__PURE__ */ jsx20(EdgeLabelLayer, { bounds }),
|
|
1279
|
+
/* @__PURE__ */ jsx20(ResizeHandles, {}),
|
|
1280
|
+
/* @__PURE__ */ jsx20(ConnectHandles, {}),
|
|
1281
|
+
/* @__PURE__ */ jsx20(ConnectPreview, {}),
|
|
1282
|
+
/* @__PURE__ */ jsx20(EdgeEditor, {}),
|
|
1283
|
+
/* @__PURE__ */ jsx20(GroupEditor, {})
|
|
1284
|
+
]
|
|
1285
|
+
}
|
|
1286
|
+
),
|
|
1287
|
+
empty && /* @__PURE__ */ jsx20("div", { className: "canvas-empty", role: "note", children: emptyMessage ?? /* @__PURE__ */ jsx20("strong", { children: "This diagram is empty" }) }),
|
|
1288
|
+
/* @__PURE__ */ jsx20(ZoomControls, { config: { onZoom: zoomCenter, onFit: fit } })
|
|
1289
|
+
]
|
|
1290
|
+
}
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// packages/diagram-react/src/DiagramViewer.tsx
|
|
1295
|
+
import { useCallback as useCallback2, useEffect as useEffect7, useId, useMemo as useMemo3, useRef as useRef7, useState as useState7 } from "react";
|
|
1296
|
+
import { HANDLES as HANDLES2, diagramBounds as diagramBounds2, dragRect as dragRect2, edgeGeometry as edgeGeometry3, labelWidth as labelWidth2, membersOf as membersOf2, minSize as minSize2 } from "@glyphlab/diagram-core";
|
|
1297
|
+
import { Fragment as Fragment5, jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
1298
|
+
var MIN_SCALE = 0.1;
|
|
1299
|
+
var MAX_SCALE = 2.5;
|
|
1300
|
+
var GRID2 = 24;
|
|
1301
|
+
function fitView(width, height, bounds) {
|
|
1302
|
+
if (bounds.w === 0 || bounds.h === 0 || width === 0 || height === 0) return { x: 0, y: 0, scale: 1 };
|
|
1303
|
+
const margin = 32;
|
|
1304
|
+
const scale = Math.min(Math.max(Math.min((width - margin * 2) / bounds.w, (height - margin * 2) / bounds.h), MIN_SCALE), 1);
|
|
1305
|
+
return { scale, x: (width - bounds.w * scale) / 2, y: (height - bounds.h * scale) / 2 };
|
|
1306
|
+
}
|
|
1307
|
+
function DiagramViewer({ config }) {
|
|
1308
|
+
const { diagram, highlights, focus = false, renderMarkdown, openNodeContent, movableNodes = false, linkStates } = config;
|
|
1309
|
+
const arrow = `snap-arrow-${useId().replace(/:/g, "")}`;
|
|
1310
|
+
const ref = useRef7(null);
|
|
1311
|
+
const [view, setView] = useState7({ x: 0, y: 0, scale: 1 });
|
|
1312
|
+
const drag = useRef7(null);
|
|
1313
|
+
const [panning, setPanning] = useState7(false);
|
|
1314
|
+
const gesture = useRef7(null);
|
|
1315
|
+
const [moving, setMoving] = useState7(null);
|
|
1316
|
+
const [picked, setPicked] = useState7(null);
|
|
1317
|
+
const [moved, setMoved] = useState7({ diagram, nodes: {}, groups: {} });
|
|
1318
|
+
const at = moved.diagram === diagram ? moved.nodes : void 0;
|
|
1319
|
+
const groupAt = moved.diagram === diagram ? moved.groups : void 0;
|
|
1320
|
+
const bounds = useMemo3(() => diagramBounds2(diagram.groups, diagram.nodes), [diagram]);
|
|
1321
|
+
const nodes = useMemo3(() => at ? diagram.nodes.map((n) => at[n.id] ? { ...n, ...at[n.id] } : n) : diagram.nodes, [diagram, at]);
|
|
1322
|
+
const groups = useMemo3(() => groupAt ? diagram.groups.map((g) => groupAt[g.id] ? { ...g, ...groupAt[g.id] } : g) : diagram.groups, [diagram, groupAt]);
|
|
1323
|
+
const nodesById = useMemo3(() => new Map(nodes.map((n) => [n.id, n])), [nodes]);
|
|
1324
|
+
const fit = useCallback2(() => {
|
|
1325
|
+
const el = ref.current;
|
|
1326
|
+
if (el) setView(fitView(el.clientWidth, el.clientHeight, bounds));
|
|
1327
|
+
}, [bounds]);
|
|
1328
|
+
const touched = useRef7(false);
|
|
1329
|
+
useEffect7(() => {
|
|
1330
|
+
touched.current = false;
|
|
1331
|
+
fit();
|
|
1332
|
+
const el = ref.current;
|
|
1333
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
1334
|
+
const observer = new ResizeObserver(() => {
|
|
1335
|
+
if (!touched.current) fit();
|
|
1336
|
+
});
|
|
1337
|
+
observer.observe(el);
|
|
1338
|
+
return () => observer.disconnect();
|
|
1339
|
+
}, [fit]);
|
|
1340
|
+
const zoomAround = (factor, px, py) => {
|
|
1341
|
+
touched.current = true;
|
|
1342
|
+
setView((v) => {
|
|
1343
|
+
const scale = Math.min(Math.max(v.scale * factor, MIN_SCALE), MAX_SCALE);
|
|
1344
|
+
const k = scale / v.scale;
|
|
1345
|
+
return { scale, x: px - (px - v.x) * k, y: py - (py - v.y) * k };
|
|
1346
|
+
});
|
|
1347
|
+
};
|
|
1348
|
+
const zoomCenter = (factor) => {
|
|
1349
|
+
const el = ref.current;
|
|
1350
|
+
if (el) zoomAround(factor, el.clientWidth / 2, el.clientHeight / 2);
|
|
1351
|
+
};
|
|
1352
|
+
useEffect7(() => {
|
|
1353
|
+
const el = ref.current;
|
|
1354
|
+
if (!el) return;
|
|
1355
|
+
const onWheel = (e) => {
|
|
1356
|
+
e.preventDefault();
|
|
1357
|
+
const rect = el.getBoundingClientRect();
|
|
1358
|
+
zoomAround(Math.exp(-e.deltaY * 15e-4), e.clientX - rect.left, e.clientY - rect.top);
|
|
1359
|
+
};
|
|
1360
|
+
el.addEventListener("wheel", onWheel, { passive: false });
|
|
1361
|
+
return () => el.removeEventListener("wheel", onWheel);
|
|
1362
|
+
}, []);
|
|
1363
|
+
const edges = diagram.edges.flatMap((edge) => {
|
|
1364
|
+
const a = nodesById.get(edge.from);
|
|
1365
|
+
const b = nodesById.get(edge.to);
|
|
1366
|
+
return a && b ? [{ edge, geometry: edgeGeometry3(a, b) }] : [];
|
|
1367
|
+
});
|
|
1368
|
+
const stop = () => {
|
|
1369
|
+
drag.current = null;
|
|
1370
|
+
setPanning(false);
|
|
1371
|
+
};
|
|
1372
|
+
const stopGesture = () => {
|
|
1373
|
+
gesture.current = null;
|
|
1374
|
+
setMoving(null);
|
|
1375
|
+
};
|
|
1376
|
+
const begin = (e, kind, id, mode) => {
|
|
1377
|
+
if (e.button !== 0) return;
|
|
1378
|
+
e.stopPropagation();
|
|
1379
|
+
e.preventDefault();
|
|
1380
|
+
const target = kind === "node" ? nodesById.get(id) : groups.find((g) => g.id === id);
|
|
1381
|
+
if (!target) return;
|
|
1382
|
+
const carried = kind === "group" && mode === "move" ? membersOf2(groups.find((g) => g.id === id), nodes, groups) : { nodes: [], groups: [] };
|
|
1383
|
+
const rect = ({ x, y, w, h }) => ({ x, y, w, h });
|
|
1384
|
+
gesture.current = {
|
|
1385
|
+
kind,
|
|
1386
|
+
id,
|
|
1387
|
+
mode,
|
|
1388
|
+
x: e.clientX,
|
|
1389
|
+
y: e.clientY,
|
|
1390
|
+
start: rect(target),
|
|
1391
|
+
nodes: Object.fromEntries((kind === "node" ? [target] : carried.nodes).map((n) => [n.id, rect(n)])),
|
|
1392
|
+
groups: Object.fromEntries(carried.groups.map((g) => [g.id, rect(g)]))
|
|
1393
|
+
};
|
|
1394
|
+
setPicked({ kind, id });
|
|
1395
|
+
setMoving(id);
|
|
1396
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
1397
|
+
};
|
|
1398
|
+
const pickedRect = picked ? picked.kind === "node" ? nodesById.get(picked.id) : groups.find((g) => g.id === picked.id) : void 0;
|
|
1399
|
+
const resizeFrom = (e, handle) => {
|
|
1400
|
+
if (picked) begin(e, picked.kind, picked.id, handle);
|
|
1401
|
+
};
|
|
1402
|
+
const gestureMove = (e) => {
|
|
1403
|
+
const g = gesture.current;
|
|
1404
|
+
if (!g) return false;
|
|
1405
|
+
const dx = (e.clientX - g.x) / view.scale;
|
|
1406
|
+
const dy = (e.clientY - g.y) / view.scale;
|
|
1407
|
+
if (g.mode !== "move") {
|
|
1408
|
+
const next = dragRect2(g.start, g.mode, dx, dy, minSize2(g.kind));
|
|
1409
|
+
setMoved((m) => {
|
|
1410
|
+
const base = m.diagram === diagram ? m : { diagram, nodes: {}, groups: {} };
|
|
1411
|
+
return g.kind === "node" ? { ...base, nodes: { ...base.nodes, [g.id]: next } } : { ...base, groups: { ...base.groups, [g.id]: next } };
|
|
1412
|
+
});
|
|
1413
|
+
return true;
|
|
1414
|
+
}
|
|
1415
|
+
const shift = (r) => ({ ...r, x: Math.round(r.x + dx), y: Math.round(r.y + dy) });
|
|
1416
|
+
setMoved((m) => {
|
|
1417
|
+
const base = m.diagram === diagram ? m : { diagram, nodes: {}, groups: {} };
|
|
1418
|
+
const nextNodes = { ...base.nodes };
|
|
1419
|
+
const nextGroups = { ...base.groups };
|
|
1420
|
+
for (const [id, r] of Object.entries(g.nodes)) nextNodes[id] = shift(r);
|
|
1421
|
+
for (const [id, r] of Object.entries(g.groups)) nextGroups[id] = shift(r);
|
|
1422
|
+
if (g.kind === "group") nextGroups[g.id] = shift(g.start);
|
|
1423
|
+
return { diagram, nodes: nextNodes, groups: nextGroups };
|
|
1424
|
+
});
|
|
1425
|
+
return true;
|
|
1426
|
+
};
|
|
1427
|
+
return /* @__PURE__ */ jsxs17(
|
|
1428
|
+
"section",
|
|
1429
|
+
{
|
|
1430
|
+
ref,
|
|
1431
|
+
className: `canvas snapshot-canvas${panning ? " dragging" : ""}`,
|
|
1432
|
+
"aria-label": "Diagram snapshot",
|
|
1433
|
+
style: { backgroundSize: `${GRID2 * view.scale}px ${GRID2 * view.scale}px`, backgroundPosition: `${view.x}px ${view.y}px` },
|
|
1434
|
+
onPointerDown: (e) => {
|
|
1435
|
+
if (e.button !== 0) return;
|
|
1436
|
+
drag.current = { x: e.clientX, y: e.clientY, vx: view.x, vy: view.y };
|
|
1437
|
+
setPicked(null);
|
|
1438
|
+
touched.current = true;
|
|
1439
|
+
setPanning(true);
|
|
1440
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
1441
|
+
},
|
|
1442
|
+
onPointerMove: (e) => {
|
|
1443
|
+
if (gestureMove(e)) return;
|
|
1444
|
+
const d = drag.current;
|
|
1445
|
+
if (d) setView((v) => ({ ...v, x: d.vx + e.clientX - d.x, y: d.vy + e.clientY - d.y }));
|
|
1446
|
+
},
|
|
1447
|
+
onPointerUp: () => {
|
|
1448
|
+
stop();
|
|
1449
|
+
stopGesture();
|
|
1450
|
+
},
|
|
1451
|
+
onPointerCancel: () => {
|
|
1452
|
+
stop();
|
|
1453
|
+
stopGesture();
|
|
1454
|
+
},
|
|
1455
|
+
children: [
|
|
1456
|
+
/* @__PURE__ */ jsxs17("div", { className: focus ? "world focus" : "world", style: { transform: `translate(${view.x}px, ${view.y}px) scale(${view.scale})` }, children: [
|
|
1457
|
+
groups.map((g) => {
|
|
1458
|
+
const grab = movableNodes ? (e) => begin(e, "group", g.id, "move") : void 0;
|
|
1459
|
+
const selected = picked?.kind === "group" && picked.id === g.id;
|
|
1460
|
+
return /* @__PURE__ */ jsxs17("div", { className: `group${g.lane ? " swimlane" : ""}${selected ? " selected" : ""}`, style: { left: g.x, top: g.y, width: g.w, height: g.h, ...layerStyle(g.color) }, children: [
|
|
1461
|
+
/* @__PURE__ */ jsxs17("div", { className: movableNodes ? "group-bar movable" : "group-bar", onPointerDown: grab, children: [
|
|
1462
|
+
/* @__PURE__ */ jsx21("span", { className: "group-title", children: g.title }),
|
|
1463
|
+
g.lane && /* @__PURE__ */ jsx21("span", { className: "lane-description", children: g.lane.body })
|
|
1464
|
+
] }),
|
|
1465
|
+
movableNodes && /* @__PURE__ */ jsxs17(Fragment5, { children: [
|
|
1466
|
+
/* @__PURE__ */ jsx21("span", { className: "group-edge group-edge-s", onPointerDown: grab }),
|
|
1467
|
+
/* @__PURE__ */ jsx21("span", { className: "group-edge group-edge-w", onPointerDown: grab }),
|
|
1468
|
+
/* @__PURE__ */ jsx21("span", { className: "group-edge group-edge-e", onPointerDown: grab })
|
|
1469
|
+
] })
|
|
1470
|
+
] }, g.id);
|
|
1471
|
+
}),
|
|
1472
|
+
/* @__PURE__ */ jsxs17("svg", { className: "edges", width: bounds.w, height: bounds.h, "aria-hidden": "true", children: [
|
|
1473
|
+
/* @__PURE__ */ jsx21("defs", { children: /* @__PURE__ */ jsx21(ArrowHeads, { id: arrow, colors: edges.map(({ edge }) => edge.color) }) }),
|
|
1474
|
+
edges.map(({ edge, geometry }) => {
|
|
1475
|
+
const hl = highlights?.edges[edge.id];
|
|
1476
|
+
const className = ["edge", edge.dashed ? "dashed" : "", edge.optional ? "optional" : "", edgeColor(edge.color) ? "colored" : "", hl ? `hl-${hl}` : ""].filter(Boolean).join(" ");
|
|
1477
|
+
return /* @__PURE__ */ jsx21("path", { d: geometry.d, className, style: edgeStyle(edge.color), markerEnd: `url(#${arrowId(arrow, edge.color)})` }, edge.id);
|
|
1478
|
+
})
|
|
1479
|
+
] }),
|
|
1480
|
+
nodes.map((node) => {
|
|
1481
|
+
const hl = highlights?.nodes[node.id];
|
|
1482
|
+
const className = ["node", movableNodes ? "movable" : "", picked?.kind === "node" && picked.id === node.id ? "selected" : "", moving === node.id ? "moving" : "", hl ? `hl-${hl}` : ""].filter(Boolean).join(" ");
|
|
1483
|
+
return /* @__PURE__ */ jsxs17(
|
|
1484
|
+
"article",
|
|
1485
|
+
{
|
|
1486
|
+
"data-node-id": node.id,
|
|
1487
|
+
className,
|
|
1488
|
+
style: { left: node.x, top: node.y, width: node.w, height: node.h, ...layerStyle(node.color) },
|
|
1489
|
+
onPointerDown: movableNodes ? (e) => begin(e, "node", node.id, "move") : void 0,
|
|
1490
|
+
children: [
|
|
1491
|
+
/* @__PURE__ */ jsx21("h2", { children: node.title }),
|
|
1492
|
+
openNodeContent && /* @__PURE__ */ jsx21(LinkBadge, { title: node.title, state: linkStates?.[node.id], onOpen: () => openNodeContent(node.id, "code") }),
|
|
1493
|
+
openNodeContent && /* @__PURE__ */ jsx21(
|
|
1494
|
+
"button",
|
|
1495
|
+
{
|
|
1496
|
+
className: "node-expand",
|
|
1497
|
+
type: "button",
|
|
1498
|
+
"aria-label": `Open full content for ${node.title}`,
|
|
1499
|
+
title: "View full content",
|
|
1500
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
1501
|
+
onClick: () => openNodeContent(node.id),
|
|
1502
|
+
children: /* @__PURE__ */ jsx21("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: /* @__PURE__ */ jsx21("path", { d: "M8 3H3v5m13-5h5v5M3 16v5h5m13-5v5h-5M3 3l6 6m12-6-6 6M3 21l6-6m12 6-6-6" }) })
|
|
1503
|
+
}
|
|
1504
|
+
),
|
|
1505
|
+
openNodeContent && node.decisions?.trim() && /* @__PURE__ */ jsxs17(
|
|
1506
|
+
"button",
|
|
1507
|
+
{
|
|
1508
|
+
className: "node-decisions",
|
|
1509
|
+
type: "button",
|
|
1510
|
+
"aria-label": `Open the decision log for ${node.title}`,
|
|
1511
|
+
title: "Decisions recorded: why this was chosen and what else was considered",
|
|
1512
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
1513
|
+
onClick: () => openNodeContent(node.id, "decisions"),
|
|
1514
|
+
children: [
|
|
1515
|
+
/* @__PURE__ */ jsx21("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx21("path", { d: "M16 3h5v5M8 3H3v5M12 22v-8.3a4 4 0 0 0-1.17-2.87L3 3M15 9l6-6" }) }),
|
|
1516
|
+
"Decisions"
|
|
1517
|
+
]
|
|
1518
|
+
}
|
|
1519
|
+
),
|
|
1520
|
+
renderMarkdown ? renderMarkdown(node.body) : /* @__PURE__ */ jsx21("div", { className: "dg-plain", children: node.body })
|
|
1521
|
+
]
|
|
1522
|
+
},
|
|
1523
|
+
node.id
|
|
1524
|
+
);
|
|
1525
|
+
}),
|
|
1526
|
+
movableNodes && picked && pickedRect && /* @__PURE__ */ jsx21("div", { className: "resize-box", style: { left: pickedRect.x, top: pickedRect.y, width: pickedRect.w, height: pickedRect.h, ["--inv"]: Math.min(3, Math.max(0.75, 1 / view.scale)) }, children: HANDLES2.map((handle) => /* @__PURE__ */ jsx21("span", { className: `resize-handle resize-${handle}`, role: "presentation", title: "Resize", onPointerDown: (e) => resizeFrom(e, handle) }, handle)) }),
|
|
1527
|
+
/* @__PURE__ */ jsx21("svg", { className: "labels", width: bounds.w, height: bounds.h, "aria-hidden": "true", children: edges.map(({ edge, geometry }) => {
|
|
1528
|
+
if (!edge.label) return null;
|
|
1529
|
+
const hl = highlights?.edges[edge.id];
|
|
1530
|
+
const width = labelWidth2(edge);
|
|
1531
|
+
return /* @__PURE__ */ jsxs17("g", { className: ["edge-label", hl ? `hl-${hl}` : ""].filter(Boolean).join(" "), transform: `translate(${geometry.lx}, ${geometry.ly})`, children: [
|
|
1532
|
+
/* @__PURE__ */ jsx21("rect", { x: -width / 2, y: -9, width, height: 18, rx: 4, className: "label-bg" }),
|
|
1533
|
+
/* @__PURE__ */ jsx21("text", { textAnchor: "middle", dy: "3.5", className: "label-text", children: edge.label })
|
|
1534
|
+
] }, edge.id);
|
|
1535
|
+
}) })
|
|
1536
|
+
] }),
|
|
1537
|
+
/* @__PURE__ */ jsxs17("div", { className: "zoom", onPointerDown: (e) => e.stopPropagation(), children: [
|
|
1538
|
+
/* @__PURE__ */ jsx21("button", { className: "dg-icon-btn", type: "button", onClick: () => zoomCenter(1.2), "aria-label": "Zoom in snapshot", children: "+" }),
|
|
1539
|
+
/* @__PURE__ */ jsxs17("span", { className: "zoom-pct", children: [
|
|
1540
|
+
Math.round(view.scale * 100),
|
|
1541
|
+
"%"
|
|
1542
|
+
] }),
|
|
1543
|
+
/* @__PURE__ */ jsx21("button", { className: "dg-icon-btn", type: "button", onClick: () => zoomCenter(1 / 1.2), "aria-label": "Zoom out snapshot", children: "\u2212" }),
|
|
1544
|
+
/* @__PURE__ */ jsx21(
|
|
1545
|
+
"button",
|
|
1546
|
+
{
|
|
1547
|
+
className: "dg-btn",
|
|
1548
|
+
type: "button",
|
|
1549
|
+
onClick: () => {
|
|
1550
|
+
touched.current = false;
|
|
1551
|
+
fit();
|
|
1552
|
+
},
|
|
1553
|
+
children: "Fit"
|
|
1554
|
+
}
|
|
1555
|
+
)
|
|
1556
|
+
] })
|
|
1557
|
+
]
|
|
1558
|
+
}
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// packages/diagram-react/src/ui.ts
|
|
1563
|
+
import { createStore } from "zustand/vanilla";
|
|
1564
|
+
var MIN_SCALE2 = 0.2;
|
|
1565
|
+
var MAX_SCALE2 = 2;
|
|
1566
|
+
var PADDING = 48;
|
|
1567
|
+
var INITIAL_MIN_SCALE = 0.5;
|
|
1568
|
+
var INITIAL_TOP_OFFSET = 40;
|
|
1569
|
+
function clamp(v, lo, hi) {
|
|
1570
|
+
return Math.min(hi, Math.max(lo, v));
|
|
1571
|
+
}
|
|
1572
|
+
function fitScale(size, bounds, min) {
|
|
1573
|
+
if (bounds.w === 0 || bounds.h === 0) return 1;
|
|
1574
|
+
return clamp(Math.min((size.width - PADDING * 2) / bounds.w, (size.height - PADDING * 2) / bounds.h), min, MAX_SCALE2);
|
|
1575
|
+
}
|
|
1576
|
+
var createViewportStore = () => createStore()((set) => ({
|
|
1577
|
+
x: PADDING,
|
|
1578
|
+
y: PADDING,
|
|
1579
|
+
scale: INITIAL_MIN_SCALE,
|
|
1580
|
+
panning: false,
|
|
1581
|
+
setPanning: (panning) => set({ panning }),
|
|
1582
|
+
setPosition: (x, y) => set({ x, y }),
|
|
1583
|
+
panBy: (dx, dy) => set((s) => ({ x: s.x + dx, y: s.y + dy })),
|
|
1584
|
+
zoomAround: (factor, cx, cy) => set((s) => {
|
|
1585
|
+
const scale = clamp(s.scale * factor, MIN_SCALE2, MAX_SCALE2);
|
|
1586
|
+
const k = scale / s.scale;
|
|
1587
|
+
return { scale, x: cx - (cx - s.x) * k, y: cy - (cy - s.y) * k };
|
|
1588
|
+
}),
|
|
1589
|
+
// Whole diagram visible and centered.
|
|
1590
|
+
fit: (size, bounds) => {
|
|
1591
|
+
const scale = fitScale(size, bounds, MIN_SCALE2);
|
|
1592
|
+
set({ scale, x: (size.width - bounds.w * scale) / 2, y: (size.height - bounds.h * scale) / 2 });
|
|
1593
|
+
},
|
|
1594
|
+
// Readable starting view: never below the initial minimum, anchored top-left.
|
|
1595
|
+
reset: (size, bounds) => set({ scale: fitScale(size, bounds, INITIAL_MIN_SCALE), x: PADDING, y: PADDING + INITIAL_TOP_OFFSET })
|
|
1596
|
+
}));
|
|
1597
|
+
var createSelectionStore = () => createStore()((set) => ({
|
|
1598
|
+
selection: null,
|
|
1599
|
+
select: (selection) => set({ selection }),
|
|
1600
|
+
clearSelection: () => set((s) => s.selection ? { selection: null } : s)
|
|
1601
|
+
}));
|
|
1602
|
+
var createEditStore = () => createStore()((set) => ({
|
|
1603
|
+
node: null,
|
|
1604
|
+
edge: null,
|
|
1605
|
+
group: null,
|
|
1606
|
+
editNode: (id, field) => set({ node: { id, field }, edge: null, group: null }),
|
|
1607
|
+
editEdge: (id) => set({ edge: id, node: null, group: null }),
|
|
1608
|
+
editGroup: (id) => set({ group: id, node: null, edge: null }),
|
|
1609
|
+
end: () => set((s) => s.node || s.edge || s.group ? { node: null, edge: null, group: null } : s)
|
|
1610
|
+
}));
|
|
1611
|
+
var LEAVE_DELAY = 250;
|
|
1612
|
+
var createConnectStore = () => {
|
|
1613
|
+
let timer;
|
|
1614
|
+
const cancel = () => {
|
|
1615
|
+
if (timer) clearTimeout(timer);
|
|
1616
|
+
timer = void 0;
|
|
1617
|
+
};
|
|
1618
|
+
return createStore()((set) => ({
|
|
1619
|
+
hovered: null,
|
|
1620
|
+
source: null,
|
|
1621
|
+
side: "e",
|
|
1622
|
+
cursor: null,
|
|
1623
|
+
target: null,
|
|
1624
|
+
hover: (id) => {
|
|
1625
|
+
cancel();
|
|
1626
|
+
set((s) => s.hovered === id ? s : { hovered: id });
|
|
1627
|
+
},
|
|
1628
|
+
leave: () => {
|
|
1629
|
+
cancel();
|
|
1630
|
+
timer = setTimeout(() => set({ hovered: null }), LEAVE_DELAY);
|
|
1631
|
+
},
|
|
1632
|
+
start: (source, side, cursor) => {
|
|
1633
|
+
cancel();
|
|
1634
|
+
set({ source, side, cursor, target: null });
|
|
1635
|
+
},
|
|
1636
|
+
move: (cursor, target) => set({ cursor, target }),
|
|
1637
|
+
end: () => set({ source: null, cursor: null, target: null })
|
|
1638
|
+
}));
|
|
1639
|
+
};
|
|
1640
|
+
var NO_HIGHLIGHTS = { nodes: {}, edges: {}, dimmed: false };
|
|
1641
|
+
var createHighlightStore = () => createStore()((set) => ({
|
|
1642
|
+
...NO_HIGHLIGHTS,
|
|
1643
|
+
set: ({ nodes, edges, dimmed = false }) => set({ nodes, edges, dimmed }),
|
|
1644
|
+
clear: () => set((s) => s.dimmed || Object.keys(s.nodes).length || Object.keys(s.edges).length ? NO_HIGHLIGHTS : s)
|
|
1645
|
+
}));
|
|
1646
|
+
var createCanvasUi = () => ({
|
|
1647
|
+
viewport: createViewportStore(),
|
|
1648
|
+
selection: createSelectionStore(),
|
|
1649
|
+
edit: createEditStore(),
|
|
1650
|
+
connect: createConnectStore(),
|
|
1651
|
+
highlight: createHighlightStore()
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
// packages/diagram-react/src/instance.ts
|
|
1655
|
+
var counter = 0;
|
|
1656
|
+
function createDiagramInstance(controller) {
|
|
1657
|
+
const ui = createCanvasUi();
|
|
1658
|
+
const dropWhatIsGone = () => {
|
|
1659
|
+
const next = controller.getSnapshot();
|
|
1660
|
+
const editing = ui.edit.getState();
|
|
1661
|
+
if (editing.node && !next.nodes.some((n) => n.id === editing.node.id) || editing.edge && !next.edges.some((e) => e.id === editing.edge) || editing.group && !next.groups.some((g) => g.id === editing.group)) {
|
|
1662
|
+
editing.end();
|
|
1663
|
+
}
|
|
1664
|
+
const { selection, clearSelection } = ui.selection.getState();
|
|
1665
|
+
if (!selection) return;
|
|
1666
|
+
const collection = selection.kind === "node" ? next.nodes : selection.kind === "group" ? next.groups : next.edges;
|
|
1667
|
+
if (!collection.some((item) => item.id === selection.id)) clearSelection();
|
|
1668
|
+
};
|
|
1669
|
+
const offs = [
|
|
1670
|
+
controller.subscribe(dropWhatIsGone),
|
|
1671
|
+
// Another diagram is not the one that was selected, even where the ids are the same.
|
|
1672
|
+
controller.onLoad(() => {
|
|
1673
|
+
ui.edit.getState().end();
|
|
1674
|
+
ui.selection.getState().clearSelection();
|
|
1675
|
+
})
|
|
1676
|
+
];
|
|
1677
|
+
return {
|
|
1678
|
+
uid: `dg${++counter}`,
|
|
1679
|
+
controller,
|
|
1680
|
+
ui,
|
|
1681
|
+
surface: { el: null },
|
|
1682
|
+
dispose: () => offs.forEach((off) => off())
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
export {
|
|
1686
|
+
ArrowHeads,
|
|
1687
|
+
COLOR_PRESETS,
|
|
1688
|
+
ColorControl,
|
|
1689
|
+
DiagramCanvas,
|
|
1690
|
+
DiagramEdge,
|
|
1691
|
+
DiagramGroup,
|
|
1692
|
+
DiagramNode,
|
|
1693
|
+
DiagramProvider,
|
|
1694
|
+
DiagramToolbar,
|
|
1695
|
+
DiagramViewer,
|
|
1696
|
+
EdgeLabel,
|
|
1697
|
+
EdgeStyleControl,
|
|
1698
|
+
LinkBadge,
|
|
1699
|
+
MAX_SCALE2 as MAX_SCALE,
|
|
1700
|
+
MIN_SCALE2 as MIN_SCALE,
|
|
1701
|
+
ZoomControls,
|
|
1702
|
+
addGroupAt,
|
|
1703
|
+
addNodeAt,
|
|
1704
|
+
bindStore,
|
|
1705
|
+
colorCss,
|
|
1706
|
+
colorLabel,
|
|
1707
|
+
connectNodes,
|
|
1708
|
+
createCanvasUi,
|
|
1709
|
+
createDiagramInstance,
|
|
1710
|
+
deleteEdge,
|
|
1711
|
+
deleteGroup,
|
|
1712
|
+
deleteNode,
|
|
1713
|
+
edgeStyle,
|
|
1714
|
+
layerStyle,
|
|
1715
|
+
useConnecting,
|
|
1716
|
+
useDiagram,
|
|
1717
|
+
useEdges,
|
|
1718
|
+
useEditing,
|
|
1719
|
+
useGroups,
|
|
1720
|
+
useHighlights,
|
|
1721
|
+
useMeta,
|
|
1722
|
+
useNodes,
|
|
1723
|
+
useSelection,
|
|
1724
|
+
useSlots,
|
|
1725
|
+
useViewport
|
|
1726
|
+
};
|
|
1727
|
+
//# sourceMappingURL=index.js.map
|