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.
Files changed (203) hide show
  1. package/LICENSE +661 -0
  2. package/README.fr.md +344 -0
  3. package/README.md +344 -0
  4. package/dist/bin/cli.d.ts +3 -0
  5. package/dist/bin/cli.d.ts.map +1 -0
  6. package/dist/bin/cli.js +262 -0
  7. package/dist/bin/cli.js.map +1 -0
  8. package/dist/src/frontend/accuracy-gauge.js +70 -0
  9. package/dist/src/frontend/admin.html +1532 -0
  10. package/dist/src/frontend/annotations.js +585 -0
  11. package/dist/src/frontend/boot.js +101 -0
  12. package/dist/src/frontend/config.js +29 -0
  13. package/dist/src/frontend/confirm-modal.js +82 -0
  14. package/dist/src/frontend/context.html +1252 -0
  15. package/dist/src/frontend/dark-mode.js +20 -0
  16. package/dist/src/frontend/diagram/alignment.js +161 -0
  17. package/dist/src/frontend/diagram/clipboard.js +187 -0
  18. package/dist/src/frontend/diagram/constants.js +109 -0
  19. package/dist/src/frontend/diagram/custom-shapes.js +104 -0
  20. package/dist/src/frontend/diagram/debug.js +43 -0
  21. package/dist/src/frontend/diagram/drawio-export.js +649 -0
  22. package/dist/src/frontend/diagram/edge-panel.js +293 -0
  23. package/dist/src/frontend/diagram/edge-rendering.js +12 -0
  24. package/dist/src/frontend/diagram/evidence.js +146 -0
  25. package/dist/src/frontend/diagram/grid.js +78 -0
  26. package/dist/src/frontend/diagram/groups.js +102 -0
  27. package/dist/src/frontend/diagram/history.js +157 -0
  28. package/dist/src/frontend/diagram/image-name-modal.js +48 -0
  29. package/dist/src/frontend/diagram/image-upload.js +36 -0
  30. package/dist/src/frontend/diagram/label-editor.js +115 -0
  31. package/dist/src/frontend/diagram/link-panel.js +144 -0
  32. package/dist/src/frontend/diagram/main.js +364 -0
  33. package/dist/src/frontend/diagram/network.js +2214 -0
  34. package/dist/src/frontend/diagram/node-panel.js +389 -0
  35. package/dist/src/frontend/diagram/node-rendering.js +964 -0
  36. package/dist/src/frontend/diagram/persistence.js +168 -0
  37. package/dist/src/frontend/diagram/ports.js +421 -0
  38. package/dist/src/frontend/diagram/selection-overlay.js +387 -0
  39. package/dist/src/frontend/diagram/state.js +43 -0
  40. package/dist/src/frontend/diagram/t.js +3 -0
  41. package/dist/src/frontend/diagram/toast.js +21 -0
  42. package/dist/src/frontend/diagram/unlock-hold.js +206 -0
  43. package/dist/src/frontend/diagram/zoom.js +20 -0
  44. package/dist/src/frontend/diagram-link-modal.js +137 -0
  45. package/dist/src/frontend/diagram.html +1494 -0
  46. package/dist/src/frontend/documents.js +479 -0
  47. package/dist/src/frontend/export.js +338 -0
  48. package/dist/src/frontend/file-attach.js +178 -0
  49. package/dist/src/frontend/files-modal.js +243 -0
  50. package/dist/src/frontend/i18n/en.json +624 -0
  51. package/dist/src/frontend/i18n/fr.json +624 -0
  52. package/dist/src/frontend/i18n.js +32 -0
  53. package/dist/src/frontend/image-paste.js +126 -0
  54. package/dist/src/frontend/index.html +2806 -0
  55. package/dist/src/frontend/local-search.js +476 -0
  56. package/dist/src/frontend/metadata.js +318 -0
  57. package/dist/src/frontend/misc.js +92 -0
  58. package/dist/src/frontend/new-doc-modal.js +285 -0
  59. package/dist/src/frontend/new-folder-modal.js +169 -0
  60. package/dist/src/frontend/search.js +194 -0
  61. package/dist/src/frontend/shape-editor.html +685 -0
  62. package/dist/src/frontend/sidebar-helpers.js +96 -0
  63. package/dist/src/frontend/sidebar-resize.js +98 -0
  64. package/dist/src/frontend/sidebar.js +351 -0
  65. package/dist/src/frontend/snippet-detect.js +25 -0
  66. package/dist/src/frontend/snippet-table.js +85 -0
  67. package/dist/src/frontend/snippet-tree.js +94 -0
  68. package/dist/src/frontend/snippets.js +1146 -0
  69. package/dist/src/frontend/state.js +46 -0
  70. package/dist/src/frontend/utils.js +21 -0
  71. package/dist/src/frontend/validate.js +107 -0
  72. package/dist/src/frontend/vendor/wordcloud2.js +1187 -0
  73. package/dist/src/frontend/wordcloud.js +693 -0
  74. package/dist/src/lib/config.d.ts +26 -0
  75. package/dist/src/lib/config.d.ts.map +1 -0
  76. package/dist/src/lib/config.js +195 -0
  77. package/dist/src/lib/config.js.map +1 -0
  78. package/dist/src/lib/hash.d.ts +2 -0
  79. package/dist/src/lib/hash.d.ts.map +1 -0
  80. package/dist/src/lib/hash.js +18 -0
  81. package/dist/src/lib/hash.js.map +1 -0
  82. package/dist/src/lib/metadata.d.ts +31 -0
  83. package/dist/src/lib/metadata.d.ts.map +1 -0
  84. package/dist/src/lib/metadata.js +128 -0
  85. package/dist/src/lib/metadata.js.map +1 -0
  86. package/dist/src/lib/parser.d.ts +11 -0
  87. package/dist/src/lib/parser.d.ts.map +1 -0
  88. package/dist/src/lib/parser.js +111 -0
  89. package/dist/src/lib/parser.js.map +1 -0
  90. package/dist/src/lib/status.d.ts +9 -0
  91. package/dist/src/lib/status.d.ts.map +1 -0
  92. package/dist/src/lib/status.js +72 -0
  93. package/dist/src/lib/status.js.map +1 -0
  94. package/dist/src/mcp/server.d.ts +3 -0
  95. package/dist/src/mcp/server.d.ts.map +1 -0
  96. package/dist/src/mcp/server.js +2046 -0
  97. package/dist/src/mcp/server.js.map +1 -0
  98. package/dist/src/mcp/tools/diagrams.d.ts +82 -0
  99. package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
  100. package/dist/src/mcp/tools/diagrams.js +594 -0
  101. package/dist/src/mcp/tools/diagrams.js.map +1 -0
  102. package/dist/src/mcp/tools/documents.d.ts +44 -0
  103. package/dist/src/mcp/tools/documents.d.ts.map +1 -0
  104. package/dist/src/mcp/tools/documents.js +186 -0
  105. package/dist/src/mcp/tools/documents.js.map +1 -0
  106. package/dist/src/mcp/tools/git.d.ts +10 -0
  107. package/dist/src/mcp/tools/git.d.ts.map +1 -0
  108. package/dist/src/mcp/tools/git.js +217 -0
  109. package/dist/src/mcp/tools/git.js.map +1 -0
  110. package/dist/src/mcp/tools/metadata.d.ts +57 -0
  111. package/dist/src/mcp/tools/metadata.d.ts.map +1 -0
  112. package/dist/src/mcp/tools/metadata.js +222 -0
  113. package/dist/src/mcp/tools/metadata.js.map +1 -0
  114. package/dist/src/mcp/tools/source.d.ts +29 -0
  115. package/dist/src/mcp/tools/source.d.ts.map +1 -0
  116. package/dist/src/mcp/tools/source.js +196 -0
  117. package/dist/src/mcp/tools/source.js.map +1 -0
  118. package/dist/src/routes/annotations.d.ts +3 -0
  119. package/dist/src/routes/annotations.d.ts.map +1 -0
  120. package/dist/src/routes/annotations.js +83 -0
  121. package/dist/src/routes/annotations.js.map +1 -0
  122. package/dist/src/routes/browse-source.d.ts +3 -0
  123. package/dist/src/routes/browse-source.d.ts.map +1 -0
  124. package/dist/src/routes/browse-source.js +79 -0
  125. package/dist/src/routes/browse-source.js.map +1 -0
  126. package/dist/src/routes/browse.d.ts +3 -0
  127. package/dist/src/routes/browse.d.ts.map +1 -0
  128. package/dist/src/routes/browse.js +91 -0
  129. package/dist/src/routes/browse.js.map +1 -0
  130. package/dist/src/routes/config.d.ts +3 -0
  131. package/dist/src/routes/config.d.ts.map +1 -0
  132. package/dist/src/routes/config.js +145 -0
  133. package/dist/src/routes/config.js.map +1 -0
  134. package/dist/src/routes/context.d.ts +3 -0
  135. package/dist/src/routes/context.d.ts.map +1 -0
  136. package/dist/src/routes/context.js +287 -0
  137. package/dist/src/routes/context.js.map +1 -0
  138. package/dist/src/routes/diagrams.d.ts +3 -0
  139. package/dist/src/routes/diagrams.d.ts.map +1 -0
  140. package/dist/src/routes/diagrams.js +69 -0
  141. package/dist/src/routes/diagrams.js.map +1 -0
  142. package/dist/src/routes/documents.d.ts +11 -0
  143. package/dist/src/routes/documents.d.ts.map +1 -0
  144. package/dist/src/routes/documents.js +450 -0
  145. package/dist/src/routes/documents.js.map +1 -0
  146. package/dist/src/routes/export.d.ts +3 -0
  147. package/dist/src/routes/export.d.ts.map +1 -0
  148. package/dist/src/routes/export.js +280 -0
  149. package/dist/src/routes/export.js.map +1 -0
  150. package/dist/src/routes/files.d.ts +3 -0
  151. package/dist/src/routes/files.d.ts.map +1 -0
  152. package/dist/src/routes/files.js +180 -0
  153. package/dist/src/routes/files.js.map +1 -0
  154. package/dist/src/routes/images.d.ts +3 -0
  155. package/dist/src/routes/images.d.ts.map +1 -0
  156. package/dist/src/routes/images.js +49 -0
  157. package/dist/src/routes/images.js.map +1 -0
  158. package/dist/src/routes/metadata.d.ts +3 -0
  159. package/dist/src/routes/metadata.d.ts.map +1 -0
  160. package/dist/src/routes/metadata.js +131 -0
  161. package/dist/src/routes/metadata.js.map +1 -0
  162. package/dist/src/routes/shape-libraries.d.ts +3 -0
  163. package/dist/src/routes/shape-libraries.d.ts.map +1 -0
  164. package/dist/src/routes/shape-libraries.js +118 -0
  165. package/dist/src/routes/shape-libraries.js.map +1 -0
  166. package/dist/src/routes/wordcloud.d.ts +3 -0
  167. package/dist/src/routes/wordcloud.d.ts.map +1 -0
  168. package/dist/src/routes/wordcloud.js +95 -0
  169. package/dist/src/routes/wordcloud.js.map +1 -0
  170. package/dist/src/server.d.ts +7 -0
  171. package/dist/src/server.d.ts.map +1 -0
  172. package/dist/src/server.js +93 -0
  173. package/dist/src/server.js.map +1 -0
  174. package/dist/starter-doc/.living-doc.json +52 -0
  175. package/dist/starter-doc/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
  176. package/dist/starter-doc/AI/2026_01_01_how_to.md +112 -0
  177. package/dist/starter-doc/AI/PROJECT-INSTRUCTIONS.md +172 -0
  178. package/dist/starter-doc/AI/PROJECT-STACK.md +77 -0
  179. package/dist/starter-doc/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
  180. package/dist/starter-doc/AI/default/AGENTS.md +31 -0
  181. package/dist/starter-doc/AI/default/CLAUDE.md +31 -0
  182. package/dist/starter-doc/AI/default/MEMORY.md +24 -0
  183. package/dist/starter-doc/AI/rules/no-magic-numbers.md +18 -0
  184. package/dist/starter-doc/AI/rules/track-current-work.md +23 -0
  185. package/dist/starter-doc/WORKLOG/current-task.md +57 -0
  186. package/dist/starter-doc-fr/.living-doc.json +52 -0
  187. package/dist/starter-doc-fr/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
  188. package/dist/starter-doc-fr/AI/2026_01_01_how_to.md +100 -0
  189. package/dist/starter-doc-fr/AI/PROJECT-INSTRUCTIONS.md +172 -0
  190. package/dist/starter-doc-fr/AI/PROJECT-STACK.md +77 -0
  191. package/dist/starter-doc-fr/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
  192. package/dist/starter-doc-fr/AI/default/AGENTS.md +31 -0
  193. package/dist/starter-doc-fr/AI/default/CLAUDE.md +31 -0
  194. package/dist/starter-doc-fr/AI/default/MEMORY.md +24 -0
  195. package/dist/starter-doc-fr/AI/rules/no-magic-numbers.md +18 -0
  196. package/dist/starter-doc-fr/AI/rules/track-current-work.md +23 -0
  197. package/dist/starter-doc-fr/WORKLOG/current-task.md +57 -0
  198. package/images/living_documentation.jpg +0 -0
  199. package/images/readme-extra-files.png +0 -0
  200. package/images/readme-filename-pattern.png +0 -0
  201. package/images/readme-intelligent-search-demo.jpg +0 -0
  202. package/images/readme-sidebar.png +0 -0
  203. package/package.json +72 -0
@@ -0,0 +1,389 @@
1
+ // ── Node panel ────────────────────────────────────────────────────────────────
2
+ // Floating formatting toolbar for selected nodes (color, font, alignment, z-order).
3
+
4
+ import { st, markDirty } from './state.js';
5
+ import { SHAPE_DEFAULTS } from './node-rendering.js';
6
+ import { CUSTOM_SHAPE_TYPE, getCustomShapeLabelPlacement } from './custom-shapes.js';
7
+ import { pushSnapshot } from './history.js';
8
+ import { t } from './t.js';
9
+
10
+ const CUSTOM_LABEL_PLACEMENTS = ['below', 'above', 'right', 'left', 'center'];
11
+
12
+ // ── Last-used style persistence (per shape type) ──────────────────────────────
13
+ // Saves colorKey/fontSize/textAlign/textValign per shapeType to localStorage so
14
+ // the next shape of that type is created with the same style.
15
+
16
+ function persistNodeStyle() {
17
+ st.selectedNodeIds.forEach((id) => {
18
+ const n = st.nodes && st.nodes.get(id);
19
+ if (!n || !n.shapeType || n.shapeType === 'anchor') return;
20
+ localStorage.setItem('ld-node-style-' + n.shapeType, JSON.stringify({
21
+ colorKey: n.colorKey || 'c-gray',
22
+ fontSize: n.fontSize || null,
23
+ textAlign: n.textAlign || null,
24
+ textValign: n.textValign || null,
25
+ }));
26
+ });
27
+ }
28
+
29
+ export function getLastNodeStyle(shapeType) {
30
+ try { return JSON.parse(localStorage.getItem('ld-node-style-' + shapeType)) || {}; }
31
+ catch { return {}; }
32
+ }
33
+
34
+ // All shapes are ctxRenderers — vis-network never re-reads the closure after
35
+ // nodes.update(). Force refreshNeeded + redraw so the new colorKey/fontSize/
36
+ // textAlign/textValign values are picked up on the next draw call via st.nodes.get(id).
37
+ function forceRedraw() {
38
+ st.selectedNodeIds.forEach((id) => {
39
+ const bn = st.network && st.network.body.nodes[id];
40
+ if (bn) bn.refreshNeeded = true;
41
+ });
42
+ if (st.network) st.network.redraw();
43
+ }
44
+
45
+ function isEdgeLocked(edge) {
46
+ if (!edge) return false;
47
+ const fromN = st.nodes && st.nodes.get(edge.from);
48
+ const toN = st.nodes && st.nodes.get(edge.to);
49
+ const isFreeArrow = fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
50
+ return isFreeArrow ? !!(fromN.locked && toN.locked) : !!(edge.edgeLocked || (fromN && fromN.locked && toN && toN.locked));
51
+ }
52
+
53
+ function selectedLockState() {
54
+ const nodeIds = (st.selectedNodeIds || []).filter((id) => {
55
+ const n = st.nodes && st.nodes.get(id);
56
+ return n && n.shapeType !== 'anchor';
57
+ });
58
+ const edgeIds = (st.selectedEdgeIds || []).filter((id) => st.edges && st.edges.get(id));
59
+ const total = nodeIds.length + edgeIds.length;
60
+ if (!total) return { allLocked: false, nodeIds, edgeIds };
61
+
62
+ const nodesLocked = nodeIds.every((id) => {
63
+ const n = st.nodes.get(id);
64
+ return !!(n && n.locked);
65
+ });
66
+ const edgesLocked = edgeIds.every((id) => isEdgeLocked(st.edges.get(id)));
67
+ return { allLocked: nodesLocked && edgesLocked, nodeIds, edgeIds };
68
+ }
69
+
70
+ function syncNodeLockButton() {
71
+ const btn = document.getElementById('btnNodeLock');
72
+ if (!btn) return;
73
+ const { allLocked } = selectedLockState();
74
+ btn.textContent = allLocked ? '🔓' : '🔒';
75
+ btn.title = t(allLocked ? 'diagram.node_panel.unlock' : 'diagram.node_panel.lock');
76
+ btn.setAttribute('aria-label', btn.title);
77
+ btn.classList.toggle('tool-active', allLocked);
78
+ }
79
+
80
+ function syncNodeFontSizeValue() {
81
+ const el = document.getElementById('nodeFontSizeValue');
82
+ if (!el) return;
83
+ const sizes = (st.selectedNodeIds || []).map((id) => {
84
+ const n = st.nodes && st.nodes.get(id);
85
+ return n && n.shapeType !== 'anchor' ? (n.fontSize || 13) : null;
86
+ }).filter((size) => size !== null);
87
+
88
+ if (!sizes.length) {
89
+ el.textContent = '–';
90
+ return;
91
+ }
92
+ const first = sizes[0];
93
+ el.textContent = sizes.every((size) => size === first) ? String(first) : '–';
94
+ }
95
+
96
+ function selectedCustomShapeIds() {
97
+ return (st.selectedNodeIds || []).filter((id) => {
98
+ const n = st.nodes && st.nodes.get(id);
99
+ return n && n.shapeType === CUSTOM_SHAPE_TYPE;
100
+ });
101
+ }
102
+
103
+ function effectiveCustomShapeLabelPlacement(node) {
104
+ return CUSTOM_LABEL_PLACEMENTS.includes(node && node.labelPlacement)
105
+ ? node.labelPlacement
106
+ : getCustomShapeLabelPlacement(node && node.customShapeId);
107
+ }
108
+
109
+ function syncCustomShapeLabelPlacementControls() {
110
+ const controls = document.getElementById('customShapeLabelPlacementControls');
111
+ if (!controls) return;
112
+ const customIds = selectedCustomShapeIds();
113
+ controls.classList.toggle('hidden', customIds.length === 0);
114
+ if (!customIds.length) return;
115
+
116
+ const placements = customIds
117
+ .map((id) => effectiveCustomShapeLabelPlacement(st.nodes.get(id)))
118
+ .filter(Boolean);
119
+ const first = placements[0];
120
+ const shared = placements.length && placements.every((placement) => placement === first) ? first : null;
121
+ controls.querySelectorAll('[data-label-placement]').forEach((btn) => {
122
+ btn.classList.toggle('tool-active', !!shared && btn.dataset.labelPlacement === shared);
123
+ });
124
+ }
125
+
126
+ function setEdgeLocked(edge, locked) {
127
+ if (!edge) return;
128
+ const fromN = st.nodes && st.nodes.get(edge.from);
129
+ const toN = st.nodes && st.nodes.get(edge.to);
130
+ const isFreeArrow = fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
131
+ if (isFreeArrow) {
132
+ [edge.from, edge.to].forEach((nodeId) => {
133
+ st.nodes.update({ id: nodeId, locked, fixed: locked ? { x: true, y: true } : false, draggable: !locked });
134
+ const bn = st.network && st.network.body.nodes[nodeId];
135
+ if (bn) bn.refreshNeeded = true;
136
+ });
137
+ } else {
138
+ st.edges.update({ id: edge.id, edgeLocked: locked });
139
+ }
140
+ }
141
+
142
+ export function showNodePanel() {
143
+ document.getElementById('nodePanel').classList.remove('hidden');
144
+ document.getElementById('nodePanelControls').classList.remove('hidden');
145
+ syncNodeLockButton();
146
+ syncNodeFontSizeValue();
147
+ syncCustomShapeLabelPlacementControls();
148
+ // Sync the opacity slider with the first selected node's current value so the
149
+ // slider reflects the live state rather than whatever position it was left at.
150
+ const slider = document.getElementById('nodeBgOpacity');
151
+ if (slider && st.selectedNodeIds.length) {
152
+ const first = st.nodes.get(st.selectedNodeIds[0]);
153
+ const op = first && typeof first.bgOpacity === 'number' ? first.bgOpacity : 1;
154
+ slider.value = String(Math.round(op * 100));
155
+ }
156
+ }
157
+
158
+ export function hideNodePanel() {
159
+ document.getElementById('nodePanel').classList.add('hidden');
160
+ }
161
+
162
+ export function toggleNodeLock() {
163
+ const { allLocked, nodeIds, edgeIds } = selectedLockState();
164
+ if (!nodeIds.length && !edgeIds.length) return;
165
+ const nextLocked = !allLocked;
166
+ pushSnapshot();
167
+ nodeIds.forEach((id) => {
168
+ st.nodes.update({ id, locked: nextLocked, fixed: nextLocked ? { x: true, y: true } : false, draggable: !nextLocked });
169
+ const bn = st.network && st.network.body.nodes[id];
170
+ if (bn) bn.refreshNeeded = true;
171
+ });
172
+ edgeIds.forEach((id) => setEdgeLocked(st.edges.get(id), nextLocked));
173
+ if (st.network) {
174
+ st.network.redraw();
175
+ if (nextLocked) st.network.unselectAll();
176
+ }
177
+ if (nextLocked) {
178
+ st.selectedNodeIds = [];
179
+ st.selectedEdgeIds = [];
180
+ hideNodePanel();
181
+ } else {
182
+ syncNodeLockButton();
183
+ }
184
+ markDirty();
185
+ }
186
+
187
+ export function setNodeColor(colorKey) {
188
+ if (!st.selectedNodeIds.length) return;
189
+ pushSnapshot();
190
+ st.selectedNodeIds.forEach((id) => {
191
+ const n = st.nodes.get(id);
192
+ if (!n) return;
193
+ st.nodes.update({ id, colorKey });
194
+ });
195
+ persistNodeStyle();
196
+ forceRedraw();
197
+ markDirty();
198
+ }
199
+
200
+ // The slider fires `input` on every step during a drag. The caller is expected
201
+ // to push a single snapshot on pointerdown (gesture start) so the whole drag
202
+ // collapses into one undoable action instead of one per step.
203
+ export function setNodeBgOpacity(opacity) {
204
+ if (!st.selectedNodeIds.length) return;
205
+ const clamped = Math.max(0, Math.min(1, opacity));
206
+ st.selectedNodeIds.forEach((id) => {
207
+ const n = st.nodes.get(id);
208
+ if (!n) return;
209
+ st.nodes.update({ id, bgOpacity: clamped });
210
+ });
211
+ forceRedraw();
212
+ markDirty();
213
+ }
214
+
215
+ export function changeNodeFontSize(delta) {
216
+ if (!st.selectedNodeIds.length) return;
217
+ pushSnapshot();
218
+ st.selectedNodeIds.forEach((id) => {
219
+ const n = st.nodes.get(id);
220
+ if (!n) return;
221
+ const newSize = Math.max(8, Math.min(48, (n.fontSize || 13) + delta));
222
+ st.nodes.update({ id, fontSize: newSize });
223
+ });
224
+ persistNodeStyle();
225
+ syncNodeFontSizeValue();
226
+ forceRedraw();
227
+ markDirty();
228
+ }
229
+
230
+ export function setTextAlign(align) {
231
+ if (!st.selectedNodeIds.length) return;
232
+ pushSnapshot();
233
+ st.selectedNodeIds.forEach((id) => {
234
+ const n = st.nodes.get(id);
235
+ if (!n) return;
236
+ st.nodes.update({ id, textAlign: align });
237
+ });
238
+ persistNodeStyle();
239
+ forceRedraw();
240
+ markDirty();
241
+ }
242
+
243
+ export function setTextValign(valign) {
244
+ if (!st.selectedNodeIds.length) return;
245
+ pushSnapshot();
246
+ st.selectedNodeIds.forEach((id) => {
247
+ const n = st.nodes.get(id);
248
+ if (!n) return;
249
+ st.nodes.update({ id, textValign: valign });
250
+ });
251
+ persistNodeStyle();
252
+ forceRedraw();
253
+ markDirty();
254
+ }
255
+
256
+ export function setCustomShapeLabelPlacement(placement) {
257
+ if (!CUSTOM_LABEL_PLACEMENTS.includes(placement)) return;
258
+ const ids = selectedCustomShapeIds();
259
+ if (!ids.length) return;
260
+ pushSnapshot();
261
+ ids.forEach((id) => {
262
+ st.nodes.update({ id, labelPlacement: placement });
263
+ });
264
+ syncCustomShapeLabelPlacementControls();
265
+ forceRedraw();
266
+ markDirty();
267
+ }
268
+
269
+ // ── Stamp (format painter) ────────────────────────────────────────────────────
270
+ // Uses a transparent DOM overlay (#stampOverlay) that intercepts canvas clicks
271
+ // during stamp mode. This bypasses vis-network's event system entirely, avoiding
272
+ // the deselectNode/click ordering problems that make st.activeStamp unreliable.
273
+
274
+ const STAMP_BTNS = { color: 'btnStampColor', fontSize: 'btnStampFontSize', size: 'btnStampSize' };
275
+
276
+ export function activateStamp(type) {
277
+ if (!st.stampTargetIds.length) return; // targets were saved on mousedown
278
+ st.activeStamp = type;
279
+ const overlay = document.getElementById('stampOverlay');
280
+ overlay.style.display = 'block';
281
+ Object.entries(STAMP_BTNS).forEach(([t, id]) =>
282
+ document.getElementById(id).classList.toggle('tool-active', t === type)
283
+ );
284
+ }
285
+
286
+ export function cancelStamp() {
287
+ st.activeStamp = null;
288
+ st.stampTargetIds = [];
289
+ document.getElementById('stampOverlay').style.display = 'none';
290
+ Object.values(STAMP_BTNS).forEach((id) =>
291
+ document.getElementById(id).classList.remove('tool-active')
292
+ );
293
+ }
294
+
295
+ function applyStamp(sourceId) {
296
+ const source = st.nodes.get(sourceId);
297
+ if (!source || !st.activeStamp || !st.stampTargetIds.length) return;
298
+ const type = st.activeStamp;
299
+ const targets = [...st.stampTargetIds]; // snapshot before cancelStamp clears the array
300
+ cancelStamp();
301
+ pushSnapshot();
302
+
303
+ targets.forEach((id) => {
304
+ if (id === sourceId) return;
305
+ const target = st.nodes.get(id);
306
+ if (!target) return;
307
+ if (type === 'color') st.nodes.update({ id, colorKey: source.colorKey || 'c-gray' });
308
+ if (type === 'rotation') st.nodes.update({ id, rotation: source.rotation || 0 });
309
+ if (type === 'fontSize') st.nodes.update({ id, fontSize: source.fontSize || 13 });
310
+ if (type === 'size') st.nodes.update({ id, nodeWidth: source.nodeWidth || null, nodeHeight: source.nodeHeight || null });
311
+ const bn = st.network && st.network.body.nodes[id];
312
+ if (bn) bn.refreshNeeded = true;
313
+ });
314
+ if (st.network) st.network.redraw();
315
+ markDirty();
316
+ }
317
+
318
+ // getNodeAt() is unreliable for shape:'custom' (bounding box near-zero).
319
+ // Manual AABB hit test using DOMtoCanvas + node dimensions, topmost node first.
320
+ function getNodeAtDOMPoint(domX, domY) {
321
+ if (!st.network || !st.nodes) return undefined;
322
+ const cp = st.network.DOMtoCanvas({ x: domX, y: domY });
323
+ for (let i = st.canonicalOrder.length - 1; i >= 0; i--) {
324
+ const id = st.canonicalOrder[i];
325
+ const n = st.nodes.get(id);
326
+ const bn = st.network.body.nodes[id];
327
+ if (!n || !bn) continue;
328
+ const defaults = SHAPE_DEFAULTS[n.shapeType] || [100, 40];
329
+ const w = n.nodeWidth || defaults[0];
330
+ const h = n.nodeHeight || defaults[1];
331
+ const rot = n.rotation || 0;
332
+ let hw, hh;
333
+ if (rot === 0) {
334
+ hw = w / 2; hh = h / 2;
335
+ } else {
336
+ const cos = Math.abs(Math.cos(rot)); const sin = Math.abs(Math.sin(rot));
337
+ hw = (w * cos + h * sin) / 2;
338
+ hh = (w * sin + h * cos) / 2;
339
+ }
340
+ if (Math.abs(cp.x - bn.x) <= hw && Math.abs(cp.y - bn.y) <= hh) return id;
341
+ }
342
+ return undefined;
343
+ }
344
+
345
+ // Wire the stamp overlay click.
346
+ document.getElementById('stampOverlay').addEventListener('click', (e) => {
347
+ if (!st.activeStamp || !st.network) return;
348
+ const rect = document.getElementById('vis-canvas').getBoundingClientRect();
349
+ const nodeId = getNodeAtDOMPoint(e.clientX - rect.left, e.clientY - rect.top);
350
+ if (nodeId !== undefined) {
351
+ applyStamp(nodeId);
352
+ } else {
353
+ cancelStamp();
354
+ }
355
+ });
356
+
357
+ // ── Step rotation ─────────────────────────────────────────────────────────────
358
+
359
+ export function stepRotate(degrees) {
360
+ if (!st.selectedNodeIds.length) return;
361
+ pushSnapshot();
362
+ const delta = degrees * (Math.PI / 180);
363
+ st.selectedNodeIds.forEach((id) => {
364
+ const n = st.nodes.get(id);
365
+ if (!n) return;
366
+ st.nodes.update({ id, rotation: (n.rotation || 0) + delta });
367
+ const bn = st.network && st.network.body.nodes[id];
368
+ if (bn) bn.refreshNeeded = true;
369
+ });
370
+ if (st.network) st.network.redraw();
371
+ markDirty();
372
+ }
373
+
374
+ export function changeZOrder(direction) {
375
+ // direction: +1 = bring to front (last in canonicalOrder = drawn on top)
376
+ // -1 = send to back (first in canonicalOrder = drawn below)
377
+ if (!st.selectedNodeIds.length) return;
378
+ pushSnapshot();
379
+ st.selectedNodeIds.forEach((id) => {
380
+ const idx = st.canonicalOrder.indexOf(id);
381
+ if (idx === -1) return;
382
+ st.canonicalOrder.splice(idx, 1);
383
+ if (direction > 0) st.canonicalOrder.push(id);
384
+ else st.canonicalOrder.unshift(id);
385
+ });
386
+ st.network.redraw();
387
+ st.network.selectNodes(st.selectedNodeIds);
388
+ markDirty();
389
+ }