living-ai-documentation 1.0.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/LICENSE +661 -0
- package/README.fr.md +344 -0
- package/README.md +344 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +262 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/frontend/accuracy-gauge.js +70 -0
- package/dist/src/frontend/admin.html +1532 -0
- package/dist/src/frontend/annotations.js +585 -0
- package/dist/src/frontend/boot.js +101 -0
- package/dist/src/frontend/config.js +29 -0
- package/dist/src/frontend/confirm-modal.js +82 -0
- package/dist/src/frontend/context.html +1252 -0
- package/dist/src/frontend/dark-mode.js +20 -0
- package/dist/src/frontend/diagram/alignment.js +161 -0
- package/dist/src/frontend/diagram/clipboard.js +187 -0
- package/dist/src/frontend/diagram/constants.js +109 -0
- package/dist/src/frontend/diagram/custom-shapes.js +104 -0
- package/dist/src/frontend/diagram/debug.js +43 -0
- package/dist/src/frontend/diagram/drawio-export.js +649 -0
- package/dist/src/frontend/diagram/edge-panel.js +293 -0
- package/dist/src/frontend/diagram/edge-rendering.js +12 -0
- package/dist/src/frontend/diagram/evidence.js +146 -0
- package/dist/src/frontend/diagram/grid.js +78 -0
- package/dist/src/frontend/diagram/groups.js +102 -0
- package/dist/src/frontend/diagram/history.js +157 -0
- package/dist/src/frontend/diagram/image-name-modal.js +48 -0
- package/dist/src/frontend/diagram/image-upload.js +36 -0
- package/dist/src/frontend/diagram/label-editor.js +115 -0
- package/dist/src/frontend/diagram/link-panel.js +144 -0
- package/dist/src/frontend/diagram/main.js +364 -0
- package/dist/src/frontend/diagram/network.js +2214 -0
- package/dist/src/frontend/diagram/node-panel.js +389 -0
- package/dist/src/frontend/diagram/node-rendering.js +964 -0
- package/dist/src/frontend/diagram/persistence.js +168 -0
- package/dist/src/frontend/diagram/ports.js +421 -0
- package/dist/src/frontend/diagram/selection-overlay.js +387 -0
- package/dist/src/frontend/diagram/state.js +43 -0
- package/dist/src/frontend/diagram/t.js +3 -0
- package/dist/src/frontend/diagram/toast.js +21 -0
- package/dist/src/frontend/diagram/unlock-hold.js +206 -0
- package/dist/src/frontend/diagram/zoom.js +20 -0
- package/dist/src/frontend/diagram-link-modal.js +137 -0
- package/dist/src/frontend/diagram.html +1494 -0
- package/dist/src/frontend/documents.js +479 -0
- package/dist/src/frontend/export.js +338 -0
- package/dist/src/frontend/file-attach.js +178 -0
- package/dist/src/frontend/files-modal.js +243 -0
- package/dist/src/frontend/i18n/en.json +624 -0
- package/dist/src/frontend/i18n/fr.json +624 -0
- package/dist/src/frontend/i18n.js +32 -0
- package/dist/src/frontend/image-paste.js +126 -0
- package/dist/src/frontend/index.html +2806 -0
- package/dist/src/frontend/local-search.js +476 -0
- package/dist/src/frontend/metadata.js +318 -0
- package/dist/src/frontend/misc.js +92 -0
- package/dist/src/frontend/new-doc-modal.js +285 -0
- package/dist/src/frontend/new-folder-modal.js +169 -0
- package/dist/src/frontend/search.js +194 -0
- package/dist/src/frontend/shape-editor.html +685 -0
- package/dist/src/frontend/sidebar-helpers.js +96 -0
- package/dist/src/frontend/sidebar-resize.js +98 -0
- package/dist/src/frontend/sidebar.js +351 -0
- package/dist/src/frontend/snippet-detect.js +25 -0
- package/dist/src/frontend/snippet-table.js +85 -0
- package/dist/src/frontend/snippet-tree.js +94 -0
- package/dist/src/frontend/snippets.js +1146 -0
- package/dist/src/frontend/state.js +46 -0
- package/dist/src/frontend/utils.js +21 -0
- package/dist/src/frontend/validate.js +107 -0
- package/dist/src/frontend/vendor/wordcloud2.js +1187 -0
- package/dist/src/frontend/wordcloud.js +693 -0
- package/dist/src/lib/config.d.ts +26 -0
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +195 -0
- package/dist/src/lib/config.js.map +1 -0
- package/dist/src/lib/hash.d.ts +2 -0
- package/dist/src/lib/hash.d.ts.map +1 -0
- package/dist/src/lib/hash.js +18 -0
- package/dist/src/lib/hash.js.map +1 -0
- package/dist/src/lib/metadata.d.ts +31 -0
- package/dist/src/lib/metadata.d.ts.map +1 -0
- package/dist/src/lib/metadata.js +128 -0
- package/dist/src/lib/metadata.js.map +1 -0
- package/dist/src/lib/parser.d.ts +11 -0
- package/dist/src/lib/parser.d.ts.map +1 -0
- package/dist/src/lib/parser.js +111 -0
- package/dist/src/lib/parser.js.map +1 -0
- package/dist/src/lib/status.d.ts +9 -0
- package/dist/src/lib/status.d.ts.map +1 -0
- package/dist/src/lib/status.js +72 -0
- package/dist/src/lib/status.js.map +1 -0
- package/dist/src/mcp/server.d.ts +3 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +2046 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools/diagrams.d.ts +82 -0
- package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
- package/dist/src/mcp/tools/diagrams.js +594 -0
- package/dist/src/mcp/tools/diagrams.js.map +1 -0
- package/dist/src/mcp/tools/documents.d.ts +44 -0
- package/dist/src/mcp/tools/documents.d.ts.map +1 -0
- package/dist/src/mcp/tools/documents.js +186 -0
- package/dist/src/mcp/tools/documents.js.map +1 -0
- package/dist/src/mcp/tools/git.d.ts +10 -0
- package/dist/src/mcp/tools/git.d.ts.map +1 -0
- package/dist/src/mcp/tools/git.js +217 -0
- package/dist/src/mcp/tools/git.js.map +1 -0
- package/dist/src/mcp/tools/metadata.d.ts +57 -0
- package/dist/src/mcp/tools/metadata.d.ts.map +1 -0
- package/dist/src/mcp/tools/metadata.js +222 -0
- package/dist/src/mcp/tools/metadata.js.map +1 -0
- package/dist/src/mcp/tools/source.d.ts +29 -0
- package/dist/src/mcp/tools/source.d.ts.map +1 -0
- package/dist/src/mcp/tools/source.js +196 -0
- package/dist/src/mcp/tools/source.js.map +1 -0
- package/dist/src/routes/annotations.d.ts +3 -0
- package/dist/src/routes/annotations.d.ts.map +1 -0
- package/dist/src/routes/annotations.js +83 -0
- package/dist/src/routes/annotations.js.map +1 -0
- package/dist/src/routes/browse-source.d.ts +3 -0
- package/dist/src/routes/browse-source.d.ts.map +1 -0
- package/dist/src/routes/browse-source.js +79 -0
- package/dist/src/routes/browse-source.js.map +1 -0
- package/dist/src/routes/browse.d.ts +3 -0
- package/dist/src/routes/browse.d.ts.map +1 -0
- package/dist/src/routes/browse.js +91 -0
- package/dist/src/routes/browse.js.map +1 -0
- package/dist/src/routes/config.d.ts +3 -0
- package/dist/src/routes/config.d.ts.map +1 -0
- package/dist/src/routes/config.js +145 -0
- package/dist/src/routes/config.js.map +1 -0
- package/dist/src/routes/context.d.ts +3 -0
- package/dist/src/routes/context.d.ts.map +1 -0
- package/dist/src/routes/context.js +287 -0
- package/dist/src/routes/context.js.map +1 -0
- package/dist/src/routes/diagrams.d.ts +3 -0
- package/dist/src/routes/diagrams.d.ts.map +1 -0
- package/dist/src/routes/diagrams.js +69 -0
- package/dist/src/routes/diagrams.js.map +1 -0
- package/dist/src/routes/documents.d.ts +11 -0
- package/dist/src/routes/documents.d.ts.map +1 -0
- package/dist/src/routes/documents.js +450 -0
- package/dist/src/routes/documents.js.map +1 -0
- package/dist/src/routes/export.d.ts +3 -0
- package/dist/src/routes/export.d.ts.map +1 -0
- package/dist/src/routes/export.js +280 -0
- package/dist/src/routes/export.js.map +1 -0
- package/dist/src/routes/files.d.ts +3 -0
- package/dist/src/routes/files.d.ts.map +1 -0
- package/dist/src/routes/files.js +180 -0
- package/dist/src/routes/files.js.map +1 -0
- package/dist/src/routes/images.d.ts +3 -0
- package/dist/src/routes/images.d.ts.map +1 -0
- package/dist/src/routes/images.js +49 -0
- package/dist/src/routes/images.js.map +1 -0
- package/dist/src/routes/metadata.d.ts +3 -0
- package/dist/src/routes/metadata.d.ts.map +1 -0
- package/dist/src/routes/metadata.js +131 -0
- package/dist/src/routes/metadata.js.map +1 -0
- package/dist/src/routes/shape-libraries.d.ts +3 -0
- package/dist/src/routes/shape-libraries.d.ts.map +1 -0
- package/dist/src/routes/shape-libraries.js +118 -0
- package/dist/src/routes/shape-libraries.js.map +1 -0
- package/dist/src/routes/wordcloud.d.ts +3 -0
- package/dist/src/routes/wordcloud.d.ts.map +1 -0
- package/dist/src/routes/wordcloud.js +95 -0
- package/dist/src/routes/wordcloud.js.map +1 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +93 -0
- package/dist/src/server.js.map +1 -0
- package/dist/starter-doc/.living-doc.json +52 -0
- package/dist/starter-doc/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
- package/dist/starter-doc/AI/2026_01_01_how_to.md +112 -0
- package/dist/starter-doc/AI/PROJECT-INSTRUCTIONS.md +172 -0
- package/dist/starter-doc/AI/PROJECT-STACK.md +77 -0
- package/dist/starter-doc/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
- package/dist/starter-doc/AI/default/AGENTS.md +31 -0
- package/dist/starter-doc/AI/default/CLAUDE.md +31 -0
- package/dist/starter-doc/AI/default/MEMORY.md +24 -0
- package/dist/starter-doc/AI/rules/no-magic-numbers.md +18 -0
- package/dist/starter-doc/AI/rules/track-current-work.md +23 -0
- package/dist/starter-doc/WORKLOG/current-task.md +57 -0
- package/dist/starter-doc-fr/.living-doc.json +52 -0
- package/dist/starter-doc-fr/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
- package/dist/starter-doc-fr/AI/2026_01_01_how_to.md +100 -0
- package/dist/starter-doc-fr/AI/PROJECT-INSTRUCTIONS.md +172 -0
- package/dist/starter-doc-fr/AI/PROJECT-STACK.md +77 -0
- package/dist/starter-doc-fr/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
- package/dist/starter-doc-fr/AI/default/AGENTS.md +31 -0
- package/dist/starter-doc-fr/AI/default/CLAUDE.md +31 -0
- package/dist/starter-doc-fr/AI/default/MEMORY.md +24 -0
- package/dist/starter-doc-fr/AI/rules/no-magic-numbers.md +18 -0
- package/dist/starter-doc-fr/AI/rules/track-current-work.md +23 -0
- package/dist/starter-doc-fr/WORKLOG/current-task.md +57 -0
- package/images/living_documentation.jpg +0 -0
- package/images/readme-extra-files.png +0 -0
- package/images/readme-filename-pattern.png +0 -0
- package/images/readme-intelligent-search-demo.jpg +0 -0
- package/images/readme-sidebar.png +0 -0
- package/package.json +72 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// ── Edge panel ────────────────────────────────────────────────────────────────
|
|
2
|
+
// Floating formatting toolbar for selected edges (arrow type, line style, font size).
|
|
3
|
+
|
|
4
|
+
import { st, markDirty } from './state.js';
|
|
5
|
+
import { visEdgeProps } from './edge-rendering.js';
|
|
6
|
+
import { pushSnapshot } from './history.js';
|
|
7
|
+
import { t } from './t.js';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_EDGE_COLOR = '#a8a29e';
|
|
10
|
+
const FREE_ARROW_STYLE_KEY = 'ld-free-arrow-style';
|
|
11
|
+
|
|
12
|
+
// Persist the style of the first free arrow (anchor→anchor) in the current
|
|
13
|
+
// selection so the next double-click creation reuses it.
|
|
14
|
+
function persistFreeArrowStyle() {
|
|
15
|
+
const freeId = st.selectedEdgeIds.find((id) => {
|
|
16
|
+
const e = st.edges.get(id);
|
|
17
|
+
if (!e) return false;
|
|
18
|
+
const fromN = st.nodes && st.nodes.get(e.from);
|
|
19
|
+
const toN = st.nodes && st.nodes.get(e.to);
|
|
20
|
+
return fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
|
|
21
|
+
});
|
|
22
|
+
if (!freeId) return;
|
|
23
|
+
const e = st.edges.get(freeId);
|
|
24
|
+
localStorage.setItem(FREE_ARROW_STYLE_KEY, JSON.stringify({
|
|
25
|
+
arrowDir: e.arrowDir || 'to',
|
|
26
|
+
dashes: e.dashes || false,
|
|
27
|
+
edgeColor: e.edgeColor || null,
|
|
28
|
+
edgeWidth: e.edgeWidth || null,
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getLastFreeArrowStyle() {
|
|
33
|
+
try { return JSON.parse(localStorage.getItem(FREE_ARROW_STYLE_KEY)) || {}; }
|
|
34
|
+
catch { return {}; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isEdgeLocked(edge) {
|
|
38
|
+
if (!edge) return false;
|
|
39
|
+
const fromN = st.nodes && st.nodes.get(edge.from);
|
|
40
|
+
const toN = st.nodes && st.nodes.get(edge.to);
|
|
41
|
+
const isFreeArrow = fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
|
|
42
|
+
return isFreeArrow ? !!(fromN.locked && toN.locked) : !!edge.edgeLocked;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function selectedEdgeLockState() {
|
|
46
|
+
const edgeIds = (st.selectedEdgeIds || []).filter((id) => st.edges && st.edges.get(id));
|
|
47
|
+
if (!edgeIds.length) return { allLocked: false, edgeIds };
|
|
48
|
+
return {
|
|
49
|
+
allLocked: edgeIds.every((id) => isEdgeLocked(st.edges.get(id))),
|
|
50
|
+
edgeIds,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function syncEdgeLockButton() {
|
|
55
|
+
const btn = document.getElementById('btnEdgeLock');
|
|
56
|
+
if (!btn) return;
|
|
57
|
+
const { allLocked } = selectedEdgeLockState();
|
|
58
|
+
btn.textContent = allLocked ? '🔓' : '🔒';
|
|
59
|
+
btn.title = t(allLocked ? 'diagram.edge_panel.unlock' : 'diagram.edge_panel.lock');
|
|
60
|
+
btn.setAttribute('aria-label', btn.title);
|
|
61
|
+
btn.classList.toggle('tool-active', allLocked);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function setEdgeLocked(edge, locked) {
|
|
65
|
+
if (!edge) return;
|
|
66
|
+
const fromN = st.nodes && st.nodes.get(edge.from);
|
|
67
|
+
const toN = st.nodes && st.nodes.get(edge.to);
|
|
68
|
+
const isFreeArrow = fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
|
|
69
|
+
if (isFreeArrow) {
|
|
70
|
+
[edge.from, edge.to].forEach((nodeId) => {
|
|
71
|
+
st.nodes.update({ id: nodeId, locked, fixed: locked ? { x: true, y: true } : false, draggable: !locked });
|
|
72
|
+
const bn = st.network && st.network.body.nodes[nodeId];
|
|
73
|
+
if (bn) bn.refreshNeeded = true;
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
st.edges.update({ id: edge.id, edgeLocked: locked });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function showEdgePanel() {
|
|
81
|
+
if (!st.selectedEdgeIds.length) return;
|
|
82
|
+
const e = st.edges.get(st.selectedEdgeIds[0]);
|
|
83
|
+
if (!e) return;
|
|
84
|
+
|
|
85
|
+
syncEdgeLockButton();
|
|
86
|
+
document.getElementById('edgePanelControls').classList.remove('hidden');
|
|
87
|
+
|
|
88
|
+
const dir = e.arrowDir ?? 'to';
|
|
89
|
+
const dashes = e.dashes ?? false;
|
|
90
|
+
|
|
91
|
+
['edgeBtnNone', 'edgeBtnFrom', 'edgeBtnTo', 'edgeBtnBoth'].forEach((id) =>
|
|
92
|
+
document.getElementById(id).classList.remove('edge-btn-active'));
|
|
93
|
+
document.getElementById({ none: 'edgeBtnNone', from: 'edgeBtnFrom', to: 'edgeBtnTo', both: 'edgeBtnBoth' }[dir] || 'edgeBtnTo')
|
|
94
|
+
.classList.add('edge-btn-active');
|
|
95
|
+
|
|
96
|
+
['edgeBtnSolid', 'edgeBtnDashed'].forEach((id) =>
|
|
97
|
+
document.getElementById(id).classList.remove('edge-btn-active'));
|
|
98
|
+
document.getElementById(dashes ? 'edgeBtnDashed' : 'edgeBtnSolid').classList.add('edge-btn-active');
|
|
99
|
+
|
|
100
|
+
// Highlight active color dot.
|
|
101
|
+
const activeColor = (e.edgeColor || DEFAULT_EDGE_COLOR).toLowerCase();
|
|
102
|
+
document.querySelectorAll('#edgePanel [data-edge-color]').forEach((btn) => {
|
|
103
|
+
const isActive = btn.dataset.edgeColor.toLowerCase() === activeColor;
|
|
104
|
+
btn.style.outline = isActive ? '2px solid #f97316' : '';
|
|
105
|
+
btn.style.outlineOffset = isActive ? '2px' : '';
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Show/hide the clear-ports button based on whether this edge has ports.
|
|
109
|
+
const hasPorts = !!(e.fromPort || e.toPort);
|
|
110
|
+
document.getElementById('btnEdgeClearPorts').classList.toggle('edge-btn-active', hasPorts);
|
|
111
|
+
|
|
112
|
+
document.getElementById('edgePanel').classList.remove('hidden');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function toggleEdgeLock() {
|
|
116
|
+
const { allLocked, edgeIds } = selectedEdgeLockState();
|
|
117
|
+
if (!edgeIds.length) return;
|
|
118
|
+
const nextLocked = !allLocked;
|
|
119
|
+
pushSnapshot();
|
|
120
|
+
edgeIds.forEach((id) => setEdgeLocked(st.edges.get(id), nextLocked));
|
|
121
|
+
if (st.network) {
|
|
122
|
+
st.network.redraw();
|
|
123
|
+
if (nextLocked) st.network.unselectAll();
|
|
124
|
+
}
|
|
125
|
+
if (nextLocked) {
|
|
126
|
+
st.selectedNodeIds = [];
|
|
127
|
+
st.selectedEdgeIds = [];
|
|
128
|
+
hideEdgePanel();
|
|
129
|
+
} else {
|
|
130
|
+
syncEdgeLockButton();
|
|
131
|
+
}
|
|
132
|
+
markDirty();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function resetEdgeLabelWidth() {
|
|
136
|
+
if (!st.selectedEdgeIds.length) return;
|
|
137
|
+
pushSnapshot();
|
|
138
|
+
st.selectedEdgeIds.forEach((id) => {
|
|
139
|
+
st.edges.update({ id, edgeLabelWidth: null });
|
|
140
|
+
});
|
|
141
|
+
markDirty();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function clearEdgePorts() {
|
|
145
|
+
if (!st.selectedEdgeIds.length) return;
|
|
146
|
+
pushSnapshot();
|
|
147
|
+
st.selectedEdgeIds.forEach((id) => {
|
|
148
|
+
const e = st.edges.get(id);
|
|
149
|
+
if (!e) return;
|
|
150
|
+
const color = e.edgeColor || DEFAULT_EDGE_COLOR;
|
|
151
|
+
// Restore vis-network's default edge rendering, preserving custom color/width.
|
|
152
|
+
st.edges.update({
|
|
153
|
+
id,
|
|
154
|
+
fromPort: null,
|
|
155
|
+
toPort: null,
|
|
156
|
+
color: { color, highlight: '#f97316', hover: '#f97316' },
|
|
157
|
+
width: e.edgeWidth || 1.5,
|
|
158
|
+
...visEdgeProps(e.arrowDir ?? 'to', e.dashes ?? false),
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
showEdgePanel();
|
|
162
|
+
markDirty();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function setEdgeColor(hex) {
|
|
166
|
+
if (!st.selectedEdgeIds.length) return;
|
|
167
|
+
pushSnapshot();
|
|
168
|
+
st.selectedEdgeIds.forEach((id) => {
|
|
169
|
+
const e = st.edges.get(id);
|
|
170
|
+
if (!e) return;
|
|
171
|
+
const update = { id, edgeColor: hex };
|
|
172
|
+
// For non-port edges, also update vis-network's native color property.
|
|
173
|
+
if (!(e.fromPort || e.toPort)) {
|
|
174
|
+
update.color = { color: hex, highlight: '#f97316', hover: '#f97316' };
|
|
175
|
+
}
|
|
176
|
+
st.edges.update(update);
|
|
177
|
+
});
|
|
178
|
+
persistFreeArrowStyle();
|
|
179
|
+
showEdgePanel();
|
|
180
|
+
markDirty();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function changeEdgeWidth(delta) {
|
|
184
|
+
if (!st.selectedEdgeIds.length) return;
|
|
185
|
+
pushSnapshot();
|
|
186
|
+
st.selectedEdgeIds.forEach((id) => {
|
|
187
|
+
const e = st.edges.get(id);
|
|
188
|
+
if (!e) return;
|
|
189
|
+
const newWidth = Math.max(1, Math.min(8, (e.edgeWidth || 1.5) + delta));
|
|
190
|
+
const update = { id, edgeWidth: newWidth };
|
|
191
|
+
// For non-port edges, also update vis-network's native width property.
|
|
192
|
+
if (!(e.fromPort || e.toPort)) {
|
|
193
|
+
update.width = newWidth;
|
|
194
|
+
}
|
|
195
|
+
st.edges.update(update);
|
|
196
|
+
});
|
|
197
|
+
persistFreeArrowStyle();
|
|
198
|
+
markDirty();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function hideEdgePanel() {
|
|
202
|
+
document.getElementById('edgePanel').classList.add('hidden');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function setEdgeArrow(dir) {
|
|
206
|
+
if (!st.selectedEdgeIds.length) return;
|
|
207
|
+
pushSnapshot();
|
|
208
|
+
st.selectedEdgeIds.forEach((id) => {
|
|
209
|
+
const e = st.edges.get(id);
|
|
210
|
+
if (!e) return;
|
|
211
|
+
const update = { id, arrowDir: dir };
|
|
212
|
+
// For port edges, vis-network arrows stay disabled; drawPortEdge handles them.
|
|
213
|
+
if (!(e.fromPort || e.toPort)) Object.assign(update, visEdgeProps(dir, e.dashes ?? false));
|
|
214
|
+
st.edges.update(update);
|
|
215
|
+
});
|
|
216
|
+
persistFreeArrowStyle();
|
|
217
|
+
showEdgePanel();
|
|
218
|
+
markDirty();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function setEdgeDashes(dashes) {
|
|
222
|
+
if (!st.selectedEdgeIds.length) return;
|
|
223
|
+
pushSnapshot();
|
|
224
|
+
st.selectedEdgeIds.forEach((id) => {
|
|
225
|
+
const e = st.edges.get(id);
|
|
226
|
+
if (!e) return;
|
|
227
|
+
const update = { id, dashes };
|
|
228
|
+
// For port edges, vis-network arrows stay disabled; drawPortEdge handles them.
|
|
229
|
+
if (!(e.fromPort || e.toPort)) Object.assign(update, visEdgeProps(e.arrowDir ?? 'to', dashes));
|
|
230
|
+
st.edges.update(update);
|
|
231
|
+
});
|
|
232
|
+
persistFreeArrowStyle();
|
|
233
|
+
showEdgePanel();
|
|
234
|
+
markDirty();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function changeEdgeFontSize(delta) {
|
|
238
|
+
if (!st.selectedEdgeIds.length) return;
|
|
239
|
+
pushSnapshot();
|
|
240
|
+
st.selectedEdgeIds.forEach((id) => {
|
|
241
|
+
const e = st.edges.get(id);
|
|
242
|
+
if (!e) return;
|
|
243
|
+
const newSize = Math.max(8, Math.min(48, (e.fontSize || 11) + delta));
|
|
244
|
+
// Keep native label transparent — drawEdgeLabels() is the single render path.
|
|
245
|
+
st.edges.update({ id, fontSize: newSize, font: { size: newSize, align: 'middle', color: 'rgba(0,0,0,0)' } });
|
|
246
|
+
});
|
|
247
|
+
markDirty();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function stepEdgeLabelRotation(delta) {
|
|
251
|
+
if (!st.selectedEdgeIds.length) return;
|
|
252
|
+
pushSnapshot();
|
|
253
|
+
st.selectedEdgeIds.forEach((id) => {
|
|
254
|
+
const e = st.edges.get(id);
|
|
255
|
+
if (!e) return;
|
|
256
|
+
const newRotation = (e.labelRotation || 0) + delta;
|
|
257
|
+
// Keep native label transparent — drawEdgeLabels() is the single render path.
|
|
258
|
+
st.edges.update({
|
|
259
|
+
id,
|
|
260
|
+
labelRotation: newRotation,
|
|
261
|
+
font: { size: e.fontSize || 11, align: 'middle', color: 'rgba(0,0,0,0)' },
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
markDirty();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const LABEL_OFFSET_STEP = 5;
|
|
268
|
+
|
|
269
|
+
export function stepEdgeLabelOffset(dx, dy) {
|
|
270
|
+
if (!st.selectedEdgeIds.length) return;
|
|
271
|
+
pushSnapshot();
|
|
272
|
+
st.selectedEdgeIds.forEach((id) => {
|
|
273
|
+
const e = st.edges.get(id);
|
|
274
|
+
if (!e) return;
|
|
275
|
+
st.edges.update({
|
|
276
|
+
id,
|
|
277
|
+
edgeLabelOffsetX: (e.edgeLabelOffsetX || 0) + dx * LABEL_OFFSET_STEP,
|
|
278
|
+
edgeLabelOffsetY: (e.edgeLabelOffsetY || 0) + dy * LABEL_OFFSET_STEP,
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
if (st.network) st.network.redraw();
|
|
282
|
+
markDirty();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function resetEdgeLabelOffset() {
|
|
286
|
+
if (!st.selectedEdgeIds.length) return;
|
|
287
|
+
pushSnapshot();
|
|
288
|
+
st.selectedEdgeIds.forEach((id) => {
|
|
289
|
+
st.edges.update({ id, edgeLabelOffsetX: 0, edgeLabelOffsetY: 0 });
|
|
290
|
+
});
|
|
291
|
+
if (st.network) st.network.redraw();
|
|
292
|
+
markDirty();
|
|
293
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// ── Edge rendering ────────────────────────────────────────────────────────────
|
|
2
|
+
// Builds vis.js edge property objects (arrows, dashes).
|
|
3
|
+
|
|
4
|
+
export function visEdgeProps(arrowDir, dashes) {
|
|
5
|
+
return {
|
|
6
|
+
arrows: {
|
|
7
|
+
to: { enabled: arrowDir === 'to' || arrowDir === 'both', scaleFactor: 0.7 },
|
|
8
|
+
from: { enabled: arrowDir === 'from' || arrowDir === 'both', scaleFactor: 0.7 },
|
|
9
|
+
},
|
|
10
|
+
dashes: dashes === true,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// ── Evidence / source consultation mode ───────────────────────────────────────
|
|
2
|
+
// Shows document-provenance markers for nodes and edges that carry `evidence`.
|
|
3
|
+
|
|
4
|
+
import { st } from './state.js';
|
|
5
|
+
import { t } from './t.js';
|
|
6
|
+
|
|
7
|
+
function evidenceItems(item) {
|
|
8
|
+
return Array.isArray(item && item.evidence)
|
|
9
|
+
? item.evidence.filter((e) => e && typeof e.documentId === 'string' && e.documentId.trim())
|
|
10
|
+
: [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function documentHref(documentId) {
|
|
14
|
+
return '/?doc=' + encodeURIComponent(documentId);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function markerPositionForNode(node) {
|
|
18
|
+
if (!st.network || !node) return null;
|
|
19
|
+
const bodyNode = st.network.body.nodes[node.id];
|
|
20
|
+
if (!bodyNode) return null;
|
|
21
|
+
const w = (bodyNode.shape && bodyNode.shape.width) || node.nodeWidth || 120;
|
|
22
|
+
const h = (bodyNode.shape && bodyNode.shape.height) || node.nodeHeight || 60;
|
|
23
|
+
return st.network.canvasToDOM({ x: bodyNode.x + w / 2 - 8, y: bodyNode.y - h / 2 + 8 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function markerPositionForEdge(edge) {
|
|
27
|
+
if (!st.network || !edge) return null;
|
|
28
|
+
const bbox = st.edgeLabelBBox && st.edgeLabelBBox[edge.id];
|
|
29
|
+
if (bbox) return st.network.canvasToDOM({ x: bbox.cx, y: bbox.cy });
|
|
30
|
+
|
|
31
|
+
const from = st.network.body.nodes[edge.from];
|
|
32
|
+
const to = edge.to && st.network.body.nodes[edge.to];
|
|
33
|
+
if (!from || !to) return null;
|
|
34
|
+
return st.network.canvasToDOM({ x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeMarker(kind, id, pos) {
|
|
38
|
+
const marker = document.createElement('button');
|
|
39
|
+
marker.type = 'button';
|
|
40
|
+
marker.className = 'evidence-marker';
|
|
41
|
+
marker.textContent = '⌘';
|
|
42
|
+
marker.title = t('diagram.evidence.marker_title');
|
|
43
|
+
marker.setAttribute('aria-label', marker.title);
|
|
44
|
+
marker.style.left = `${Math.round(pos.x)}px`;
|
|
45
|
+
marker.style.top = `${Math.round(pos.y)}px`;
|
|
46
|
+
marker.addEventListener('click', (event) => {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
event.stopPropagation();
|
|
49
|
+
showEvidencePanel(kind, id);
|
|
50
|
+
});
|
|
51
|
+
return marker;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function renderEvidenceMarkers() {
|
|
55
|
+
const layer = document.getElementById('evidenceLayer');
|
|
56
|
+
if (!layer) return;
|
|
57
|
+
layer.replaceChildren();
|
|
58
|
+
layer.classList.toggle('hidden', !st.evidenceMode);
|
|
59
|
+
if (!st.evidenceMode || !st.network || !st.nodes || !st.edges) return;
|
|
60
|
+
|
|
61
|
+
for (const node of st.nodes.get()) {
|
|
62
|
+
if (!evidenceItems(node).length || node.shapeType === 'anchor') continue;
|
|
63
|
+
const pos = markerPositionForNode(node);
|
|
64
|
+
if (pos) layer.appendChild(makeMarker('node', node.id, pos));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const edge of st.edges.get()) {
|
|
68
|
+
if (!evidenceItems(edge).length) continue;
|
|
69
|
+
const pos = markerPositionForEdge(edge);
|
|
70
|
+
if (pos) layer.appendChild(makeMarker('edge', edge.id, pos));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function bindEvidenceModeToNetwork() {
|
|
75
|
+
if (!st.network) return;
|
|
76
|
+
st.network.on('afterDrawing', renderEvidenceMarkers);
|
|
77
|
+
renderEvidenceMarkers();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function initEvidenceMode() {
|
|
81
|
+
window.addEventListener('diagram:network-ready', bindEvidenceModeToNetwork);
|
|
82
|
+
document.getElementById('btnEvidenceClose')?.addEventListener('click', hideEvidencePanel);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function toggleEvidenceMode() {
|
|
86
|
+
st.evidenceMode = !st.evidenceMode;
|
|
87
|
+
const btn = document.getElementById('btnEvidenceMode');
|
|
88
|
+
if (btn) btn.classList.toggle('tool-active', st.evidenceMode);
|
|
89
|
+
if (!st.evidenceMode) hideEvidencePanel();
|
|
90
|
+
renderEvidenceMarkers();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function hideEvidencePanel() {
|
|
94
|
+
const panel = document.getElementById('evidencePanel');
|
|
95
|
+
if (panel) panel.classList.add('hidden');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function showEvidencePanel(kind, id) {
|
|
99
|
+
const panel = document.getElementById('evidencePanel');
|
|
100
|
+
const title = document.getElementById('evidencePanelTitle');
|
|
101
|
+
const body = document.getElementById('evidencePanelBody');
|
|
102
|
+
if (!panel || !title || !body) return;
|
|
103
|
+
|
|
104
|
+
const item = kind === 'node' ? st.nodes.get(id) : st.edges.get(id);
|
|
105
|
+
const entries = evidenceItems(item);
|
|
106
|
+
title.textContent = kind === 'node'
|
|
107
|
+
? t('diagram.evidence.node_title')
|
|
108
|
+
: t('diagram.evidence.edge_title');
|
|
109
|
+
body.replaceChildren();
|
|
110
|
+
|
|
111
|
+
if (!entries.length) {
|
|
112
|
+
const empty = document.createElement('p');
|
|
113
|
+
empty.className = 'text-sm text-gray-500 dark:text-gray-400';
|
|
114
|
+
empty.textContent = t('diagram.evidence.empty');
|
|
115
|
+
body.appendChild(empty);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
const card = document.createElement('article');
|
|
120
|
+
card.className = 'rounded-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-3 space-y-2';
|
|
121
|
+
|
|
122
|
+
const link = document.createElement('a');
|
|
123
|
+
link.className = 'block text-sm font-semibold text-blue-600 dark:text-blue-400 hover:underline break-all';
|
|
124
|
+
link.href = documentHref(entry.documentId);
|
|
125
|
+
link.textContent = entry.documentId;
|
|
126
|
+
card.appendChild(link);
|
|
127
|
+
|
|
128
|
+
if (entry.section) {
|
|
129
|
+
const section = document.createElement('p');
|
|
130
|
+
section.className = 'text-xs font-medium text-gray-500 dark:text-gray-400';
|
|
131
|
+
section.textContent = `${t('diagram.evidence.section_label')}: ${entry.section}`;
|
|
132
|
+
card.appendChild(section);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (entry.summary) {
|
|
136
|
+
const summary = document.createElement('p');
|
|
137
|
+
summary.className = 'text-sm text-gray-700 dark:text-gray-300 leading-snug';
|
|
138
|
+
summary.textContent = entry.summary;
|
|
139
|
+
card.appendChild(summary);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
body.appendChild(card);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
panel.classList.remove('hidden');
|
|
146
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// ── Grid & snap ───────────────────────────────────────────────────────────────
|
|
2
|
+
// Grid rendering (beforeDrawing), snap-to-grid (dragEnd).
|
|
3
|
+
|
|
4
|
+
import { st, markDirty } from './state.js';
|
|
5
|
+
import { GRID_SIZE } from './constants.js';
|
|
6
|
+
import { t } from './t.js';
|
|
7
|
+
import { pushSnapshot } from './history.js';
|
|
8
|
+
import { snapToAlignGuides } from './alignment.js';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export function applyGridState(enabled) {
|
|
13
|
+
st.gridEnabled = enabled;
|
|
14
|
+
const btn = document.getElementById('btnGrid');
|
|
15
|
+
btn.classList.toggle('tool-active', enabled);
|
|
16
|
+
btn.title = t('diagram.toolbar.grid');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function toggleGrid() {
|
|
20
|
+
applyGridState(!st.gridEnabled);
|
|
21
|
+
markDirty();
|
|
22
|
+
if (st.network) st.network.redraw();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Called on network "beforeDrawing" — draws grid lines in physical pixel space.
|
|
26
|
+
export function drawGrid(ctx) {
|
|
27
|
+
if (!st.gridEnabled || !st.network) return;
|
|
28
|
+
const isDark = document.documentElement.classList.contains('dark');
|
|
29
|
+
const color = isDark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.15)';
|
|
30
|
+
const scale = st.network.getScale();
|
|
31
|
+
const center = st.network.getViewPosition();
|
|
32
|
+
const canvas = ctx.canvas;
|
|
33
|
+
const W = canvas.width, H = canvas.height;
|
|
34
|
+
|
|
35
|
+
// vis.js coordinates (center.x, scale) are in CSS pixels; must multiply by DPR
|
|
36
|
+
// so the grid aligns correctly on Retina/HiDPI displays.
|
|
37
|
+
const dpr = window.devicePixelRatio || 1;
|
|
38
|
+
const step = GRID_SIZE * scale * dpr;
|
|
39
|
+
const offsetX = (((W / 2 - center.x * scale * dpr) % step) + step) % step;
|
|
40
|
+
const offsetY = (((H / 2 - center.y * scale * dpr) % step) + step) % step;
|
|
41
|
+
|
|
42
|
+
ctx.save();
|
|
43
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0); // physical pixel space
|
|
44
|
+
ctx.strokeStyle = color;
|
|
45
|
+
ctx.lineWidth = 1;
|
|
46
|
+
ctx.beginPath();
|
|
47
|
+
for (let x = offsetX; x < W; x += step) { ctx.moveTo(x, 0); ctx.lineTo(x, H); }
|
|
48
|
+
for (let y = offsetY; y < H; y += step) { ctx.moveTo(0, y); ctx.lineTo(W, y); }
|
|
49
|
+
ctx.stroke();
|
|
50
|
+
ctx.restore();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Snap a canvas position to the nearest grid intersection.
|
|
54
|
+
export function snapToGrid(x, y) {
|
|
55
|
+
return {
|
|
56
|
+
x: Math.round(x / GRID_SIZE) * GRID_SIZE,
|
|
57
|
+
y: Math.round(y / GRID_SIZE) * GRID_SIZE,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Called on network "dragEnd" — alignment snap, then grid snap.
|
|
62
|
+
export function onDragEnd(params) {
|
|
63
|
+
if (!params.nodes || !params.nodes.length) return;
|
|
64
|
+
pushSnapshot();
|
|
65
|
+
snapToAlignGuides(params);
|
|
66
|
+
if (st.gridEnabled) {
|
|
67
|
+
params.nodes.forEach((id) => {
|
|
68
|
+
const bodyNode = st.network.body.nodes[id];
|
|
69
|
+
if (!bodyNode) return;
|
|
70
|
+
const cx = bodyNode.x, cy = bodyNode.y;
|
|
71
|
+
// Snap the center of the shape to grid intersections
|
|
72
|
+
const snappedX = Math.round(cx / GRID_SIZE) * GRID_SIZE;
|
|
73
|
+
const snappedY = Math.round(cy / GRID_SIZE) * GRID_SIZE;
|
|
74
|
+
st.network.moveNode(id, snappedX, snappedY);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
markDirty();
|
|
78
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// ── Group management ──────────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
import { st, markDirty } from './state.js';
|
|
4
|
+
import { pushSnapshot } from './history.js';
|
|
5
|
+
import { SHAPE_DEFAULTS } from './node-rendering.js';
|
|
6
|
+
|
|
7
|
+
// ── Create / destroy ──────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export function groupNodes() {
|
|
10
|
+
if (st.selectedNodeIds.length < 2) return;
|
|
11
|
+
pushSnapshot();
|
|
12
|
+
const groupId = 'g' + Date.now();
|
|
13
|
+
st.selectedNodeIds.forEach((id) => st.nodes.update({ id, groupId }));
|
|
14
|
+
markDirty();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ungroupNodes() {
|
|
18
|
+
if (!st.selectedNodeIds.length) return;
|
|
19
|
+
pushSnapshot();
|
|
20
|
+
// Collect all members of any group touched by the selection
|
|
21
|
+
const groupIds = new Set(
|
|
22
|
+
st.selectedNodeIds.map((id) => { const n = st.nodes.get(id); return n && n.groupId; }).filter(Boolean)
|
|
23
|
+
);
|
|
24
|
+
st.nodes.get().forEach((n) => {
|
|
25
|
+
if (n.groupId && groupIds.has(n.groupId)) st.nodes.update({ id: n.id, groupId: null });
|
|
26
|
+
});
|
|
27
|
+
markDirty();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Selection expansion ───────────────────────────────────────────────────────
|
|
31
|
+
// Called from onSelectNode — expands selection to all group members.
|
|
32
|
+
|
|
33
|
+
export function expandSelectionToGroup(nodeIds) {
|
|
34
|
+
const groupIds = new Set();
|
|
35
|
+
nodeIds.forEach((id) => {
|
|
36
|
+
const n = st.nodes.get(id);
|
|
37
|
+
if (n && n.groupId) groupIds.add(n.groupId);
|
|
38
|
+
});
|
|
39
|
+
if (!groupIds.size) return nodeIds;
|
|
40
|
+
|
|
41
|
+
const expanded = new Set(nodeIds);
|
|
42
|
+
st.nodes.get().forEach((n) => {
|
|
43
|
+
if (n.groupId && groupIds.has(n.groupId)) expanded.add(n.id);
|
|
44
|
+
});
|
|
45
|
+
return [...expanded];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Group outline (drawn on canvas in afterDrawing) ───────────────────────────
|
|
49
|
+
|
|
50
|
+
function nodeBounds(id) {
|
|
51
|
+
const n = st.nodes.get(id);
|
|
52
|
+
const bodyNode = st.network.body.nodes[id];
|
|
53
|
+
if (!bodyNode) return null;
|
|
54
|
+
const shape = (n && n.shapeType) || 'box';
|
|
55
|
+
const defaults = SHAPE_DEFAULTS[shape] || [100, 40];
|
|
56
|
+
const W = (n && n.nodeWidth) || defaults[0];
|
|
57
|
+
const H = (n && n.nodeHeight) || defaults[1];
|
|
58
|
+
const rot = (n && n.rotation) || 0;
|
|
59
|
+
const cx = bodyNode.x, cy = bodyNode.y;
|
|
60
|
+
if (rot === 0) {
|
|
61
|
+
return { minX: cx - W / 2, minY: cy - H / 2, maxX: cx + W / 2, maxY: cy + H / 2 };
|
|
62
|
+
}
|
|
63
|
+
const cos = Math.abs(Math.cos(rot)); const sin = Math.abs(Math.sin(rot));
|
|
64
|
+
const hw = (W * cos + H * sin) / 2;
|
|
65
|
+
const hh = (W * sin + H * cos) / 2;
|
|
66
|
+
return { minX: cx - hw, minY: cy - hh, maxX: cx + hw, maxY: cy + hh };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function drawGroupOutlines(ctx) {
|
|
70
|
+
if (!st.network || !st.selectedNodeIds.length) return;
|
|
71
|
+
|
|
72
|
+
// Find groupIds that have at least one selected member
|
|
73
|
+
const selectedSet = new Set(st.selectedNodeIds);
|
|
74
|
+
const activeGroups = new Set();
|
|
75
|
+
st.selectedNodeIds.forEach((id) => {
|
|
76
|
+
const n = st.nodes.get(id);
|
|
77
|
+
if (n && n.groupId) activeGroups.add(n.groupId);
|
|
78
|
+
});
|
|
79
|
+
if (!activeGroups.size) return;
|
|
80
|
+
|
|
81
|
+
// For each active group, compute bounding box over ALL members
|
|
82
|
+
activeGroups.forEach((groupId) => {
|
|
83
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
84
|
+
st.nodes.get().forEach((n) => {
|
|
85
|
+
if (n.groupId !== groupId) return;
|
|
86
|
+
const b = nodeBounds(n.id);
|
|
87
|
+
if (!b) return;
|
|
88
|
+
minX = Math.min(minX, b.minX); minY = Math.min(minY, b.minY);
|
|
89
|
+
maxX = Math.max(maxX, b.maxX); maxY = Math.max(maxY, b.maxY);
|
|
90
|
+
});
|
|
91
|
+
if (minX === Infinity) return;
|
|
92
|
+
|
|
93
|
+
const PAD = 14;
|
|
94
|
+
ctx.save();
|
|
95
|
+
ctx.strokeStyle = '#6366f1';
|
|
96
|
+
ctx.lineWidth = 1.5;
|
|
97
|
+
ctx.setLineDash([6, 4]);
|
|
98
|
+
ctx.strokeRect(minX - PAD, minY - PAD, maxX - minX + PAD * 2, maxY - minY + PAD * 2);
|
|
99
|
+
ctx.setLineDash([]);
|
|
100
|
+
ctx.restore();
|
|
101
|
+
});
|
|
102
|
+
}
|