living-documentation 7.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 (173) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +329 -0
  3. package/dist/bin/cli.d.ts +3 -0
  4. package/dist/bin/cli.d.ts.map +1 -0
  5. package/dist/bin/cli.js +62 -0
  6. package/dist/bin/cli.js.map +1 -0
  7. package/dist/src/frontend/admin.html +1073 -0
  8. package/dist/src/frontend/annotations.js +546 -0
  9. package/dist/src/frontend/boot.js +90 -0
  10. package/dist/src/frontend/config.js +19 -0
  11. package/dist/src/frontend/dark-mode.js +20 -0
  12. package/dist/src/frontend/diagram/alignment.js +161 -0
  13. package/dist/src/frontend/diagram/clipboard.js +172 -0
  14. package/dist/src/frontend/diagram/constants.js +109 -0
  15. package/dist/src/frontend/diagram/debug.js +43 -0
  16. package/dist/src/frontend/diagram/edge-panel.js +260 -0
  17. package/dist/src/frontend/diagram/edge-rendering.js +12 -0
  18. package/dist/src/frontend/diagram/grid.js +78 -0
  19. package/dist/src/frontend/diagram/groups.js +102 -0
  20. package/dist/src/frontend/diagram/history.js +153 -0
  21. package/dist/src/frontend/diagram/image-name-modal.js +48 -0
  22. package/dist/src/frontend/diagram/image-upload.js +36 -0
  23. package/dist/src/frontend/diagram/label-editor.js +115 -0
  24. package/dist/src/frontend/diagram/link-panel.js +144 -0
  25. package/dist/src/frontend/diagram/main.js +299 -0
  26. package/dist/src/frontend/diagram/network.js +1473 -0
  27. package/dist/src/frontend/diagram/node-panel.js +267 -0
  28. package/dist/src/frontend/diagram/node-rendering.js +773 -0
  29. package/dist/src/frontend/diagram/persistence.js +161 -0
  30. package/dist/src/frontend/diagram/ports.js +386 -0
  31. package/dist/src/frontend/diagram/selection-overlay.js +336 -0
  32. package/dist/src/frontend/diagram/state.js +39 -0
  33. package/dist/src/frontend/diagram/t.js +3 -0
  34. package/dist/src/frontend/diagram/toast.js +21 -0
  35. package/dist/src/frontend/diagram/unlock-hold.js +182 -0
  36. package/dist/src/frontend/diagram/zoom.js +20 -0
  37. package/dist/src/frontend/diagram-link-modal.js +137 -0
  38. package/dist/src/frontend/diagram.html +1279 -0
  39. package/dist/src/frontend/documents.js +373 -0
  40. package/dist/src/frontend/export.js +338 -0
  41. package/dist/src/frontend/i18n/en.json +406 -0
  42. package/dist/src/frontend/i18n/fr.json +406 -0
  43. package/dist/src/frontend/i18n.js +32 -0
  44. package/dist/src/frontend/image-paste.js +101 -0
  45. package/dist/src/frontend/index.html +2314 -0
  46. package/dist/src/frontend/misc.js +25 -0
  47. package/dist/src/frontend/new-doc-modal.js +260 -0
  48. package/dist/src/frontend/new-folder-modal.js +174 -0
  49. package/dist/src/frontend/search.js +157 -0
  50. package/dist/src/frontend/sidebar-helpers.js +58 -0
  51. package/dist/src/frontend/sidebar.js +182 -0
  52. package/dist/src/frontend/snippet-detect.js +25 -0
  53. package/dist/src/frontend/snippet-table.js +85 -0
  54. package/dist/src/frontend/snippet-tree.js +94 -0
  55. package/dist/src/frontend/snippets.js +534 -0
  56. package/dist/src/frontend/state.js +28 -0
  57. package/dist/src/frontend/utils.js +21 -0
  58. package/dist/src/frontend/vendor/wordcloud2.js +1187 -0
  59. package/dist/src/frontend/wordcloud.js +693 -0
  60. package/dist/src/lib/config.d.ts +17 -0
  61. package/dist/src/lib/config.d.ts.map +1 -0
  62. package/dist/src/lib/config.js +79 -0
  63. package/dist/src/lib/config.js.map +1 -0
  64. package/dist/src/lib/parser.d.ts +11 -0
  65. package/dist/src/lib/parser.d.ts.map +1 -0
  66. package/dist/src/lib/parser.js +111 -0
  67. package/dist/src/lib/parser.js.map +1 -0
  68. package/dist/src/mcp/server.d.ts +3 -0
  69. package/dist/src/mcp/server.d.ts.map +1 -0
  70. package/dist/src/mcp/server.js +986 -0
  71. package/dist/src/mcp/server.js.map +1 -0
  72. package/dist/src/mcp/tools/diagrams.d.ts +44 -0
  73. package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
  74. package/dist/src/mcp/tools/diagrams.js +245 -0
  75. package/dist/src/mcp/tools/diagrams.js.map +1 -0
  76. package/dist/src/mcp/tools/documents.d.ts +26 -0
  77. package/dist/src/mcp/tools/documents.d.ts.map +1 -0
  78. package/dist/src/mcp/tools/documents.js +127 -0
  79. package/dist/src/mcp/tools/documents.js.map +1 -0
  80. package/dist/src/mcp/tools/source.d.ts +29 -0
  81. package/dist/src/mcp/tools/source.d.ts.map +1 -0
  82. package/dist/src/mcp/tools/source.js +200 -0
  83. package/dist/src/mcp/tools/source.js.map +1 -0
  84. package/dist/src/routes/annotations.d.ts +3 -0
  85. package/dist/src/routes/annotations.d.ts.map +1 -0
  86. package/dist/src/routes/annotations.js +83 -0
  87. package/dist/src/routes/annotations.js.map +1 -0
  88. package/dist/src/routes/browse.d.ts +3 -0
  89. package/dist/src/routes/browse.d.ts.map +1 -0
  90. package/dist/src/routes/browse.js +75 -0
  91. package/dist/src/routes/browse.js.map +1 -0
  92. package/dist/src/routes/config.d.ts +3 -0
  93. package/dist/src/routes/config.d.ts.map +1 -0
  94. package/dist/src/routes/config.js +97 -0
  95. package/dist/src/routes/config.js.map +1 -0
  96. package/dist/src/routes/diagrams.d.ts +3 -0
  97. package/dist/src/routes/diagrams.d.ts.map +1 -0
  98. package/dist/src/routes/diagrams.js +69 -0
  99. package/dist/src/routes/diagrams.js.map +1 -0
  100. package/dist/src/routes/documents.d.ts +8 -0
  101. package/dist/src/routes/documents.d.ts.map +1 -0
  102. package/dist/src/routes/documents.js +332 -0
  103. package/dist/src/routes/documents.js.map +1 -0
  104. package/dist/src/routes/export.d.ts +3 -0
  105. package/dist/src/routes/export.d.ts.map +1 -0
  106. package/dist/src/routes/export.js +277 -0
  107. package/dist/src/routes/export.js.map +1 -0
  108. package/dist/src/routes/images.d.ts +3 -0
  109. package/dist/src/routes/images.d.ts.map +1 -0
  110. package/dist/src/routes/images.js +49 -0
  111. package/dist/src/routes/images.js.map +1 -0
  112. package/dist/src/routes/wordcloud.d.ts +3 -0
  113. package/dist/src/routes/wordcloud.d.ts.map +1 -0
  114. package/dist/src/routes/wordcloud.js +95 -0
  115. package/dist/src/routes/wordcloud.js.map +1 -0
  116. package/dist/src/server.d.ts +7 -0
  117. package/dist/src/server.d.ts.map +1 -0
  118. package/dist/src/server.js +76 -0
  119. package/dist/src/server.js.map +1 -0
  120. package/dist/starting-doc/.annotations.json +3 -0
  121. package/dist/starting-doc/.diagrams.json +1884 -0
  122. package/dist/starting-doc/.living-doc.json +39 -0
  123. package/dist/starting-doc/1_tutorial/2026_04_11_13_25_[General]_crer_vos_dossiers.md +16 -0
  124. package/dist/starting-doc/1_tutorial/2026_04_11_18_58_[General]_creer_un_document_dans_un_dossier.md +9 -0
  125. package/dist/starting-doc/1_tutorial/2026_04_12_09_00_[General]_editer_et_sauvegarder.md +39 -0
  126. package/dist/starting-doc/1_tutorial/2026_04_12_10_00_[General]_utiliser_les_snippets.md +71 -0
  127. package/dist/starting-doc/2026_04_08_20_52_[General]_welcome.md +17 -0
  128. package/dist/starting-doc/2026_04_11_12_55_[General]_premiers_pas.md +271 -0
  129. package/dist/starting-doc/2_guide/2026_04_08_00_04_[DOCUMENT]_utilisation_des_images_plein_ecran_lien_clickable.md +40 -0
  130. package/dist/starting-doc/2_guide/2026_04_08_23_38_[Configuration]_demarrage_de_living_documentation.md +32 -0
  131. package/dist/starting-doc/2_guide/2026_04_09_09_00_[NAVIGATION]_recherche_plein_texte.md +65 -0
  132. package/dist/starting-doc/2_guide/2026_04_09_10_00_[EXPORT]_exporter_en_pdf.md +43 -0
  133. package/dist/starting-doc/2_guide/2026_04_09_11_00_[Configuration]_configurer_le_panneau_admin.md +55 -0
  134. package/dist/starting-doc/2_guide/2026_04_09_12_00_[Configuration]_extra_files.md +68 -0
  135. package/dist/starting-doc/2_guide/2026_04_09_13_00_[WORDCLOUD]_word_cloud.md +54 -0
  136. package/dist/starting-doc/2_guide/2026_04_09_14_00_[DIAGRAM]_creer_et_lier_un_diagramme.md +77 -0
  137. package/dist/starting-doc/3_concept/2026_04_08_20_58_[DOCUMENTING]_ADRS.md +20 -0
  138. package/dist/starting-doc/3_concept/2026_04_08_22_15_[DOCUMENTING]_living_documentation.md +17 -0
  139. package/dist/starting-doc/3_concept/2026_04_08_22_46_[METHODOLOGY]_diataxis_architecture_du_contenu.md +16 -0
  140. package/dist/starting-doc/4_reference/2026_04_08_23_14_[FUNDAMENTALS]_the_living_documentation_tool.md +41 -0
  141. package/dist/starting-doc/4_reference/2026_04_09_01_00_[REFERENCE]_raccourcis_clavier.md +61 -0
  142. package/dist/starting-doc/4_reference/2026_04_09_02_00_[REFERENCE]_tokens_pattern_nommage.md +75 -0
  143. package/dist/starting-doc/4_reference/2026_04_09_03_00_[REFERENCE]_types_de_snippets.md +68 -0
  144. package/dist/starting-doc/4_reference/2026_04_11_17_31_[FUNDAMENTALS]_architecturer_une_documentation.md +12 -0
  145. package/dist/starting-doc/4_reference/2026_04_12_14_07_[FUNDAMENTALS]_dossiers_et_catgories.md +89 -0
  146. package/dist/starting-doc/images/admin_screenshot.png +0 -0
  147. package/dist/starting-doc/images/ajout-document.png +0 -0
  148. package/dist/starting-doc/images/ajouter-document-categorie.png +0 -0
  149. package/dist/starting-doc/images/ajouter_un_document_dans_un_dossier.png +0 -0
  150. package/dist/starting-doc/images/architecturer_une_documentation_reference.png +0 -0
  151. package/dist/starting-doc/images/cr_er_un_document.png +0 -0
  152. package/dist/starting-doc/images/creation-nouveau-dossier.png +0 -0
  153. package/dist/starting-doc/images/creer-document-context-engineering.png +0 -0
  154. package/dist/starting-doc/images/creer-dossier-only-tutoriel.png +0 -0
  155. package/dist/starting-doc/images/creer-dossier-tutoriel.png +0 -0
  156. package/dist/starting-doc/images/creer-dossiers-done.png +0 -0
  157. package/dist/starting-doc/images/creer-un-document.png +0 -0
  158. package/dist/starting-doc/images/creer-vos-dossiers-tutoriel.png +0 -0
  159. package/dist/starting-doc/images/creer-vos-dossiers.png +0 -0
  160. package/dist/starting-doc/images/decouverte_adrs.png +0 -0
  161. package/dist/starting-doc/images/diataxis.png +0 -0
  162. package/dist/starting-doc/images/diataxis_callout.png +0 -0
  163. package/dist/starting-doc/images/document-cree.png +0 -0
  164. package/dist/starting-doc/images/liens_snippets.png +0 -0
  165. package/dist/starting-doc/images/living_documentation.png +0 -0
  166. package/dist/starting-doc/images/npm_logo.png +0 -0
  167. package/dist/starting-doc/images/popup-creer-document.png +0 -0
  168. package/dist/starting-doc/images/popup-creer-dossier.png +0 -0
  169. package/dist/starting-doc/images/popup-dossier-cree.png +0 -0
  170. package/dist/starting-doc/images/quatre-dossiers-crees.png +0 -0
  171. package/dist/starting-doc/images/screenshot-living-doc.png +0 -0
  172. package/dist/starting-doc/images/the_living_documentation_tool.png +0 -0
  173. package/package.json +49 -0
@@ -0,0 +1,336 @@
1
+ // ── Selection / resize / rotate overlay ───────────────────────────────────────
2
+ // Dashed selection box, corner resize handles, and top-centre rotation handle.
3
+
4
+ import { st, markDirty } from './state.js';
5
+ import { visNodeProps, SHAPE_DEFAULTS } from './node-rendering.js';
6
+ import { t } from './t.js';
7
+ import { pushSnapshot } from './history.js';
8
+ import { snapToGrid } from './grid.js';
9
+
10
+ // ── Bounding box helper (works for all shapes including ctxRenderer) ──────────
11
+ function nodeBounds(id) {
12
+ const n = st.nodes.get(id);
13
+ const bodyNode = st.network.body.nodes[id];
14
+ if (!bodyNode) return null;
15
+ const cx = bodyNode.x, cy = bodyNode.y;
16
+ const shape = n && n.shapeType || 'box';
17
+ const defaults = SHAPE_DEFAULTS[shape] || [60, 28];
18
+ const W = (n && n.nodeWidth) || defaults[0];
19
+ const H = (n && n.nodeHeight) || defaults[1];
20
+ // Use the axis-aligned envelope of the (possibly rotated) bounding box.
21
+ const rot = (n && n.rotation) || 0;
22
+ if (rot === 0) {
23
+ return { minX: cx - W / 2, minY: cy - H / 2, maxX: cx + W / 2, maxY: cy + H / 2 };
24
+ }
25
+ const cos = Math.abs(Math.cos(rot));
26
+ const sin = Math.abs(Math.sin(rot));
27
+ const hw = (W * cos + H * sin) / 2;
28
+ const hh = (W * sin + H * cos) / 2;
29
+ // Actor: head extends above cy - H/2 when unrotated
30
+ const headExtra = shape === 'actor' ? (28 * (H / 52) - H / 2) : 0;
31
+ return { minX: cx - hw, minY: cy - hh - headExtra, maxX: cx + hw, maxY: cy + hh };
32
+ }
33
+
34
+ // ── Overlay position ──────────────────────────────────────────────────────────
35
+ export function updateSelectionOverlay() {
36
+ if (!st.network || !st.selectedNodeIds.length) { hideSelectionOverlay(); return; }
37
+ // Anchor-only selections (free arrow drag) have no meaningful bounding box.
38
+ const hasNonAnchor = st.selectedNodeIds.some((id) => { const n = st.nodes && st.nodes.get(id); return !(n && n.shapeType === 'anchor'); });
39
+ if (!hasNonAnchor) { hideSelectionOverlay(); return; }
40
+
41
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
42
+ for (const id of st.selectedNodeIds) {
43
+ try {
44
+ const b = nodeBounds(id);
45
+ if (!b) continue;
46
+ minX = Math.min(minX, b.minX); minY = Math.min(minY, b.minY);
47
+ maxX = Math.max(maxX, b.maxX); maxY = Math.max(maxY, b.maxY);
48
+ } catch (_) { /* node still being created */ }
49
+ }
50
+ if (minX === Infinity) { hideSelectionOverlay(); return; }
51
+
52
+ const PAD = 10;
53
+ const tl = st.network.canvasToDOM({ x: minX, y: minY });
54
+ const br = st.network.canvasToDOM({ x: maxX, y: maxY });
55
+ const ov = document.getElementById('selectionOverlay');
56
+ ov.style.display = 'block';
57
+ ov.style.left = tl.x - PAD + 'px';
58
+ ov.style.top = tl.y - PAD + 'px';
59
+ ov.style.width = br.x - tl.x + PAD * 2 + 'px';
60
+ ov.style.height = br.y - tl.y + PAD * 2 + 'px';
61
+
62
+ // Hide resize/rotate handles when any selected node is locked.
63
+ const anyLocked = st.selectedNodeIds.some((id) => { const n = st.nodes && st.nodes.get(id); return n && n.locked; });
64
+ ['rh-tl','rh-tr','rh-bl','rh-br','rh-rotate','rh-label-rotate'].forEach((id) => {
65
+ document.getElementById(id).style.display = anyLocked ? 'none' : '';
66
+ });
67
+
68
+ // Position rotation handle: top-centre of the overlay, 28px above it.
69
+ const rh = document.getElementById('rh-rotate');
70
+ rh.style.left = (br.x - tl.x) / 2 + PAD - 8 + 'px';
71
+ rh.style.top = '-28px';
72
+
73
+ // Position label rotation handle: top-centre offset left by 24px to avoid overlap.
74
+ const lrh = document.getElementById('rh-label-rotate');
75
+ lrh.style.left = (br.x - tl.x) / 2 + PAD - 8 - 24 + 'px';
76
+ lrh.style.top = '-28px';
77
+ }
78
+
79
+ export function hideSelectionOverlay() {
80
+ document.getElementById('selectionOverlay').style.display = 'none';
81
+ }
82
+
83
+ // ── Resize ────────────────────────────────────────────────────────────────────
84
+ function onResizeStart(e, corner) {
85
+ if (!st.selectedNodeIds.length || !st.network) return;
86
+ // Skip if any selected node is locked.
87
+ if (st.selectedNodeIds.some((id) => { const n = st.nodes && st.nodes.get(id); return n && n.locked; })) return;
88
+ pushSnapshot();
89
+ e.preventDefault();
90
+ e.stopPropagation();
91
+
92
+ const positions = st.network.getPositions(st.selectedNodeIds);
93
+ const startBBs = st.selectedNodeIds.map((id) => {
94
+ const n = st.nodes.get(id);
95
+ const b = nodeBounds(id);
96
+ const shape = (n && n.shapeType) || 'box';
97
+ const defaults = SHAPE_DEFAULTS[shape] || [60, 28];
98
+ const initW = (n && n.nodeWidth) || (b ? Math.round(b.maxX - b.minX) : defaults[0]);
99
+ const initH = (n && n.nodeHeight) || (b ? Math.round(b.maxY - b.minY) : defaults[1]);
100
+ const pos = positions[id] || { x: 0, y: 0 };
101
+ return { id, node: n, initW, initH, initX: pos.x, initY: pos.y };
102
+ });
103
+
104
+ // True bounding box of the whole selection (for scale reference and pivot)
105
+ let bbMinX = Infinity, bbMinY = Infinity, bbMaxX = -Infinity, bbMaxY = -Infinity;
106
+ for (const { id } of startBBs) {
107
+ const b = nodeBounds(id);
108
+ if (!b) continue;
109
+ bbMinX = Math.min(bbMinX, b.minX); bbMinY = Math.min(bbMinY, b.minY);
110
+ bbMaxX = Math.max(bbMaxX, b.maxX); bbMaxY = Math.max(bbMaxY, b.maxY);
111
+ }
112
+ const initBoxW = (bbMaxX - bbMinX) || 1;
113
+ const initBoxH = (bbMaxY - bbMinY) || 1;
114
+
115
+ st.resizeDrag = { corner, startMouse: { x: e.clientX, y: e.clientY }, startBBs, initBoxW, initBoxH, bbMinX, bbMinY, bbMaxX, bbMaxY };
116
+ st.selectedNodeIds.forEach((id) => st.nodes.update({ id, fixed: true }));
117
+ document.getElementById('vis-canvas').style.pointerEvents = 'none';
118
+ document.addEventListener('mousemove', onResizeDrag);
119
+ document.addEventListener('mouseup', onResizeEnd);
120
+ }
121
+
122
+ function onResizeDrag(e) {
123
+ if (!st.resizeDrag || !st.network) return;
124
+ const scale = st.network.getScale();
125
+ const cdx = (e.clientX - st.resizeDrag.startMouse.x) / scale;
126
+ const cdy = (e.clientY - st.resizeDrag.startMouse.y) / scale;
127
+ const MIN = 20;
128
+ const c = st.resizeDrag.corner;
129
+ const updatedIds = [];
130
+
131
+ if (st.resizeDrag.startBBs.length === 1) {
132
+ const { id, node, initW, initH, initX, initY } = st.resizeDrag.startBBs[0];
133
+ let nW = initW, nH = initH;
134
+ if (c === 'br') { nW = initW + cdx; nH = initH + cdy; }
135
+ if (c === 'bl') { nW = initW - cdx; nH = initH + cdy; }
136
+ if (c === 'tr') { nW = initW + cdx; nH = initH - cdy; }
137
+ if (c === 'tl') { nW = initW - cdx; nH = initH - cdy; }
138
+ if ((e.shiftKey || node.shapeType === 'actor') && initW > 0 && initH > 0) {
139
+ const s = Math.max(nW / initW, nH / initH);
140
+ nW = initW * s;
141
+ nH = initH * s;
142
+ }
143
+ nW = Math.max(MIN, Math.round(nW));
144
+ nH = Math.max(MIN, Math.round(nH));
145
+ st.nodes.update({ id, nodeWidth: nW, nodeHeight: nH, ...visNodeProps(node.shapeType || 'box', node.colorKey || 'c-gray', nW, nH, node.fontSize, node.textAlign, node.textValign) });
146
+ if (!st.resizeSymmetric) {
147
+ // Anchor the opposite corner: move center so that the fixed corner stays put.
148
+ let nx = initX, ny = initY;
149
+ if (c === 'br') { nx = initX - initW / 2 + nW / 2; ny = initY - initH / 2 + nH / 2; }
150
+ if (c === 'bl') { nx = initX + initW / 2 - nW / 2; ny = initY - initH / 2 + nH / 2; }
151
+ if (c === 'tr') { nx = initX - initW / 2 + nW / 2; ny = initY + initH / 2 - nH / 2; }
152
+ if (c === 'tl') { nx = initX + initW / 2 - nW / 2; ny = initY + initH / 2 - nH / 2; }
153
+ st.network.moveNode(id, nx, ny);
154
+ }
155
+ updatedIds.push(id);
156
+ } else {
157
+ const { initBoxW, initBoxH, bbMinX, bbMinY, bbMaxX, bbMaxY } = st.resizeDrag;
158
+ let sx = 1, sy = 1;
159
+ if (c === 'br') { sx = (initBoxW + cdx) / initBoxW; sy = (initBoxH + cdy) / initBoxH; }
160
+ if (c === 'bl') { sx = (initBoxW - cdx) / initBoxW; sy = (initBoxH + cdy) / initBoxH; }
161
+ if (c === 'tr') { sx = (initBoxW + cdx) / initBoxW; sy = (initBoxH - cdy) / initBoxH; }
162
+ if (c === 'tl') { sx = (initBoxW - cdx) / initBoxW; sy = (initBoxH - cdy) / initBoxH; }
163
+ sx = Math.max(0.1, sx); sy = Math.max(0.1, sy);
164
+ if (e.shiftKey) { const s = Math.max(sx, sy); sx = s; sy = s; }
165
+
166
+ // Pivot = corner opposite to the drag corner (stays fixed during scale)
167
+ const pivotX = (c === 'br' || c === 'tr') ? bbMinX : bbMaxX;
168
+ const pivotY = (c === 'br' || c === 'bl') ? bbMinY : bbMaxY;
169
+
170
+ for (const { id, node, initW, initH, initX, initY } of st.resizeDrag.startBBs) {
171
+ const actorLock = node.shapeType === 'actor';
172
+ const esx = actorLock ? Math.max(sx, sy) : sx;
173
+ const esy = actorLock ? Math.max(sx, sy) : sy;
174
+ const nW = Math.max(MIN, Math.round(initW * esx));
175
+ const nH = Math.max(MIN, Math.round(initH * esy));
176
+ st.nodes.update({ id, nodeWidth: nW, nodeHeight: nH, ...visNodeProps(node.shapeType || 'box', node.colorKey || 'c-gray', nW, nH, node.fontSize, node.textAlign, node.textValign) });
177
+ st.network.moveNode(id, pivotX + (initX - pivotX) * sx, pivotY + (initY - pivotY) * sy);
178
+ updatedIds.push(id);
179
+ }
180
+ }
181
+
182
+ updatedIds.forEach((id) => { const bn = st.network.body.nodes[id]; if (bn) bn.refreshNeeded = true; });
183
+ st.network.redraw();
184
+ updateSelectionOverlay();
185
+ }
186
+
187
+ function onResizeEnd() {
188
+ if (!st.resizeDrag) return;
189
+ st.selectedNodeIds.forEach((id) => st.nodes.update({ id, fixed: false }));
190
+ document.getElementById('vis-canvas').style.pointerEvents = '';
191
+ document.removeEventListener('mousemove', onResizeDrag);
192
+ document.removeEventListener('mouseup', onResizeEnd);
193
+ if (st.gridEnabled && st.network) {
194
+ st.selectedNodeIds.forEach((id) => {
195
+ const bn = st.network.body.nodes[id];
196
+ if (!bn) return;
197
+ const snapped = snapToGrid(bn.x, bn.y);
198
+ st.network.moveNode(id, snapped.x, snapped.y);
199
+ });
200
+ }
201
+ st.resizeDrag = null;
202
+ markDirty();
203
+ }
204
+
205
+ // ── Rotation ──────────────────────────────────────────────────────────────────
206
+ function onRotateStart(e) {
207
+ if (!st.selectedNodeIds.length || !st.network) return;
208
+ // Skip if any selected node is locked.
209
+ if (st.selectedNodeIds.some((id) => { const n = st.nodes && st.nodes.get(id); return n && n.locked; })) return;
210
+ pushSnapshot();
211
+ e.preventDefault();
212
+ e.stopPropagation();
213
+
214
+ // Barycentre of the selection in canvas coordinates.
215
+ const positions = st.network.getPositions(st.selectedNodeIds);
216
+ const ids = st.selectedNodeIds;
217
+ const cx = ids.reduce((s, id) => s + (positions[id] ? positions[id].x : 0), 0) / ids.length;
218
+ const cy = ids.reduce((s, id) => s + (positions[id] ? positions[id].y : 0), 0) / ids.length;
219
+
220
+ const nodeAngles = ids.map((id) => {
221
+ const n = st.nodes.get(id);
222
+ const pos = positions[id] || { x: 0, y: 0 };
223
+ return {
224
+ id,
225
+ initRotation: (n && n.rotation) || 0,
226
+ // Position relative to barycentre at drag start
227
+ relX: pos.x - cx,
228
+ relY: pos.y - cy,
229
+ };
230
+ });
231
+
232
+ // Horizontal drag → rotation: right = clockwise, left = counter-clockwise.
233
+ // 1 px = 1 degree.
234
+ st.rotateDrag = { startX: e.clientX, nodeAngles, cx, cy };
235
+ document.getElementById('vis-canvas').style.pointerEvents = 'none';
236
+ document.addEventListener('mousemove', onRotateDrag);
237
+ document.addEventListener('mouseup', onRotateEnd);
238
+ }
239
+
240
+ function onRotateDrag(e) {
241
+ if (!st.rotateDrag || !st.network) return;
242
+ const { startX, nodeAngles, cx, cy } = st.rotateDrag;
243
+ const dx = e.clientX - startX;
244
+ const delta = dx * (Math.PI / 180); // 1 px = 1 degree
245
+ const cos = Math.cos(delta);
246
+ const sin = Math.sin(delta);
247
+
248
+ nodeAngles.forEach(({ id, initRotation, relX, relY }) => {
249
+ // Rotate the node's position around the barycentre
250
+ const newX = cx + relX * cos - relY * sin;
251
+ const newY = cy + relX * sin + relY * cos;
252
+ st.network.moveNode(id, newX, newY);
253
+ // Rotate the node's own orientation
254
+ st.nodes.update({ id, rotation: initRotation + delta });
255
+ const bn = st.network.body.nodes[id];
256
+ if (bn) bn.refreshNeeded = true;
257
+ });
258
+ st.network.redraw();
259
+ updateSelectionOverlay();
260
+ }
261
+
262
+ function onRotateEnd() {
263
+ if (!st.rotateDrag) return;
264
+ document.getElementById('vis-canvas').style.pointerEvents = '';
265
+ document.removeEventListener('mousemove', onRotateDrag);
266
+ document.removeEventListener('mouseup', onRotateEnd);
267
+ st.rotateDrag = null;
268
+ markDirty();
269
+ }
270
+
271
+ // ── Label rotation ────────────────────────────────────────────────────────────
272
+ function onLabelRotateStart(e) {
273
+ if (!st.selectedNodeIds.length || !st.network) return;
274
+ pushSnapshot();
275
+ e.preventDefault();
276
+ e.stopPropagation();
277
+
278
+ const nodeAngles = st.selectedNodeIds.map((id) => {
279
+ const n = st.nodes.get(id);
280
+ return { id, initLabelRotation: (n && n.labelRotation) || 0 };
281
+ });
282
+
283
+ st.labelRotateDrag = { startX: e.clientX, nodeAngles };
284
+ document.getElementById('vis-canvas').style.pointerEvents = 'none';
285
+ document.addEventListener('mousemove', onLabelRotateDrag);
286
+ document.addEventListener('mouseup', onLabelRotateEnd);
287
+ }
288
+
289
+ function onLabelRotateDrag(e) {
290
+ if (!st.labelRotateDrag || !st.network) return;
291
+ const { startX, nodeAngles } = st.labelRotateDrag;
292
+ const dx = e.clientX - startX;
293
+ const delta = dx * (Math.PI / 180); // 1 px = 1 degree
294
+
295
+ nodeAngles.forEach(({ id, initLabelRotation }) => {
296
+ st.nodes.update({ id, labelRotation: initLabelRotation + delta });
297
+ const bn = st.network.body.nodes[id];
298
+ if (bn) bn.refreshNeeded = true;
299
+ });
300
+ st.network.redraw();
301
+ }
302
+
303
+ function onLabelRotateEnd() {
304
+ if (!st.labelRotateDrag) return;
305
+ document.getElementById('vis-canvas').style.pointerEvents = '';
306
+ document.removeEventListener('mousemove', onLabelRotateDrag);
307
+ document.removeEventListener('mouseup', onLabelRotateEnd);
308
+ st.labelRotateDrag = null;
309
+ markDirty();
310
+ }
311
+
312
+ // ── Wire handles ──────────────────────────────────────────────────────────────
313
+ ['tl', 'tr', 'bl', 'br'].forEach((corner) => {
314
+ document.getElementById('rh-' + corner).addEventListener('mousedown', (e) => onResizeStart(e, corner));
315
+ });
316
+ document.getElementById('rh-rotate').addEventListener('mousedown', onRotateStart);
317
+ document.getElementById('rh-label-rotate').addEventListener('mousedown', onLabelRotateStart);
318
+
319
+ // ── Resize mode toggle ────────────────────────────────────────────────────────
320
+ export function toggleResizeMode() {
321
+ st.resizeSymmetric = !st.resizeSymmetric;
322
+ const btn = document.getElementById('btnResizeMode');
323
+ const iconCorner = document.getElementById('icon-resize-corner');
324
+ const iconCenter = document.getElementById('icon-resize-center');
325
+ if (st.resizeSymmetric) {
326
+ btn.title = t('diagram.toolbar.resize_center');
327
+ btn.classList.remove('active-tool');
328
+ iconCorner.classList.add('hidden');
329
+ iconCenter.classList.remove('hidden');
330
+ } else {
331
+ btn.title = t('diagram.toolbar.resize_corner');
332
+ btn.classList.add('active-tool');
333
+ iconCorner.classList.remove('hidden');
334
+ iconCenter.classList.add('hidden');
335
+ }
336
+ }
@@ -0,0 +1,39 @@
1
+ // ── Shared mutable state ──────────────────────────────────────────────────────
2
+ // Single object imported by reference so all modules see the same mutations.
3
+
4
+ export const st = {
5
+ network: null,
6
+ nodes: null,
7
+ edges: null,
8
+ diagrams: [],
9
+ currentDiagramId: null,
10
+ currentTool: 'select',
11
+ pendingShape: 'box',
12
+ selectedNodeIds: [],
13
+ selectedEdgeIds: [],
14
+ alignGuides: true,
15
+ gridEnabled: true,
16
+ debugMode: false,
17
+ isDirty: false,
18
+ sidebarOpen: true,
19
+ editingNodeId: null,
20
+ editingEdgeId: null,
21
+ resizeDrag: null,
22
+ rotateDrag: null, // { startX, nodeAngles: [{id, initRotation}] }
23
+ labelRotateDrag: null, // { startX, nodeAngles: [{id, initLabelRotation}] }
24
+ activeStamp: null, // 'color' | 'rotation' | 'fontSize' | null
25
+ stampTargetIds: [], // node IDs waiting to receive the stamped property
26
+ clipboard: null, // { nodes: [], edges: [] }
27
+ canonicalOrder: [], // user-defined z-order, immune to vis.js hover reordering
28
+ edgesStraight: false, // when true, all edges use smooth: disabled (straight lines)
29
+ resizeSymmetric: false, // when true, center is fixed during resize; when false, opposite corner is fixed
30
+ nodeColorOverrides: {}, // colorKey → {bg, border, font, hbg, hborder} — set from config at boot
31
+ edgeLabelCanvasPos: {}, // edgeId → {x, y} canvas coords of last drawn label, used by label editor
32
+ edgeLabelBBox: {}, // edgeId → {cx, cy, w, h, rotation} canvas world coords, used by label resize
33
+ freeArrowFirstPoint: null, // addEdge two-click flow: {x, y} canvas coords of first click, or null
34
+ };
35
+
36
+ export function markDirty() {
37
+ st.isDirty = true;
38
+ document.getElementById('btnSave').disabled = false;
39
+ }
@@ -0,0 +1,3 @@
1
+ // Thin ES-module wrapper around the global window.t set by /i18n.js.
2
+ // Diagram ES modules import { t } from '/diagram/t.js' to stay clean.
3
+ export const t = (key) => (typeof window.t === 'function' ? window.t(key) : key);
@@ -0,0 +1,21 @@
1
+ // ── Toast notifications ───────────────────────────────────────────────────────
2
+ // Minimal sonner-style toasts, no dependencies.
3
+
4
+ export function showToast(message, type = 'success', duration = 3500) {
5
+ const container = document.getElementById('toastContainer');
6
+ const el = document.createElement('div');
7
+ el.className = 'ld-toast ld-toast--' + type;
8
+ el.textContent = message;
9
+ container.appendChild(el);
10
+
11
+ // Animate in on next frame
12
+ requestAnimationFrame(() => el.classList.add('ld-toast--visible'));
13
+
14
+ const hide = () => {
15
+ el.classList.remove('ld-toast--visible');
16
+ el.addEventListener('transitionend', () => el.remove(), { once: true });
17
+ };
18
+
19
+ const timer = setTimeout(hide, duration);
20
+ el.addEventListener('click', () => { clearTimeout(timer); hide(); });
21
+ }
@@ -0,0 +1,182 @@
1
+ // ── Long-press unlock ─────────────────────────────────────────────────────────
2
+ // Locked nodes/edges are fully non-interactive: any click on them is captured
3
+ // here before vis-network sees it. Holding the mouse button on a locked target
4
+ // for UNLOCK_HOLD_MS unlocks it, with a circular progress animation rendered
5
+ // next to the pointer. Releasing early or moving beyond a small tolerance
6
+ // cancels the hold without changing state.
7
+
8
+ import { st, markDirty } from './state.js';
9
+ import { pushSnapshot } from './history.js';
10
+
11
+ const UNLOCK_HOLD_MS = 2000;
12
+ const UNLOCK_MOVE_TOLERANCE = 8; // px
13
+
14
+ let _hold = null;
15
+
16
+ function isEdgeLocked(edge) {
17
+ if (!edge) return false;
18
+ const fromN = st.nodes && st.nodes.get(edge.from);
19
+ const toN = st.nodes && st.nodes.get(edge.to);
20
+ const isFreeArrow = fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
21
+ return isFreeArrow ? !!(fromN.locked && toN.locked) : !!edge.edgeLocked;
22
+ }
23
+
24
+ // Returns a target descriptor ({type, id, ...}) if the DOM point lands on a
25
+ // locked node or locked edge, otherwise null.
26
+ function hitTestLocked(container, clientX, clientY) {
27
+ if (!st.network || !st.nodes || !st.edges) return null;
28
+ const rect = container.getBoundingClientRect();
29
+ const pos = { x: clientX - rect.left, y: clientY - rect.top };
30
+
31
+ const nodeId = st.network.getNodeAt(pos);
32
+ if (nodeId) {
33
+ const n = st.nodes.get(nodeId);
34
+ if (n && n.locked) {
35
+ if (n.shapeType === 'anchor') {
36
+ // Locked anchor → resolve to its parent free-arrow so the whole arrow unlocks.
37
+ const edges = st.edges.get({ filter: (e) => e.from === nodeId || e.to === nodeId });
38
+ for (const e of edges) {
39
+ if (isEdgeLocked(e)) {
40
+ return { type: 'edge', id: e.id, isFreeArrow: true, fromId: e.from, toId: e.to };
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+ return { type: 'node', id: nodeId };
46
+ }
47
+ }
48
+ const edgeId = st.network.getEdgeAt(pos);
49
+ if (edgeId) {
50
+ const e = st.edges.get(edgeId);
51
+ if (isEdgeLocked(e)) {
52
+ const fromN = st.nodes.get(e.from);
53
+ const toN = st.nodes.get(e.to);
54
+ const isFreeArrow = fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
55
+ return { type: 'edge', id: edgeId, isFreeArrow, fromId: e.from, toId: e.to };
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+
61
+ function createOverlay() {
62
+ const C = 2 * Math.PI * 20;
63
+ const el = document.createElement('div');
64
+ el.className = 'unlock-hold-overlay';
65
+ el.style.cssText = [
66
+ 'position:fixed',
67
+ 'pointer-events:none',
68
+ 'width:56px', 'height:56px',
69
+ 'z-index:10000',
70
+ 'transform:translate(-50%,-50%)',
71
+ 'transition:opacity 120ms',
72
+ 'opacity:0',
73
+ ].join(';');
74
+ el.innerHTML = `
75
+ <svg viewBox="0 0 56 56" width="56" height="56">
76
+ <circle cx="28" cy="28" r="24" fill="rgba(17,24,39,0.75)" stroke="rgba(255,255,255,0.25)" stroke-width="2"/>
77
+ <circle class="uh-progress" cx="28" cy="28" r="20" fill="none"
78
+ stroke="#f97316" stroke-width="3"
79
+ stroke-dasharray="${C}" stroke-dashoffset="${C}"
80
+ transform="rotate(-90 28 28)" stroke-linecap="round"/>
81
+ <g transform="translate(20 19)" fill="none" stroke="#f9fafb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
82
+ <rect x="2" y="7" width="12" height="10" rx="2"/>
83
+ <path d="M5 7V5a3 3 0 0 1 6 0"/>
84
+ </g>
85
+ </svg>`;
86
+ document.body.appendChild(el);
87
+ requestAnimationFrame(() => { el.style.opacity = '1'; });
88
+ return el;
89
+ }
90
+
91
+ function positionOverlay(el, clientX, clientY) {
92
+ el.style.left = (clientX + 28) + 'px';
93
+ el.style.top = (clientY + 28) + 'px';
94
+ }
95
+
96
+ function updateProgress(el, progress) {
97
+ const C = 2 * Math.PI * 20;
98
+ const ring = el.querySelector('.uh-progress');
99
+ if (ring) ring.setAttribute('stroke-dashoffset', String(C * (1 - progress)));
100
+ }
101
+
102
+ function performUnlock(target) {
103
+ if (!st.nodes || !st.edges || !st.network) return;
104
+ pushSnapshot();
105
+ if (target.type === 'node') {
106
+ st.nodes.update({ id: target.id, locked: false, fixed: false, draggable: true });
107
+ const bn = st.network.body.nodes[target.id];
108
+ if (bn) bn.refreshNeeded = true;
109
+ } else if (target.isFreeArrow) {
110
+ [target.fromId, target.toId].forEach((nodeId) => {
111
+ st.nodes.update({ id: nodeId, locked: false, fixed: false, draggable: true });
112
+ const bn = st.network.body.nodes[nodeId];
113
+ if (bn) bn.refreshNeeded = true;
114
+ });
115
+ } else {
116
+ st.edges.update({ id: target.id, edgeLocked: false });
117
+ }
118
+ st.network.redraw();
119
+ markDirty();
120
+ }
121
+
122
+ function cancelHold() {
123
+ if (!_hold) return;
124
+ clearTimeout(_hold.timeoutId);
125
+ if (_hold.rafId) cancelAnimationFrame(_hold.rafId);
126
+ if (_hold.overlay && _hold.overlay.parentNode) _hold.overlay.remove();
127
+ document.removeEventListener('mouseup', onUp, true);
128
+ document.removeEventListener('mousemove', onMove, true);
129
+ _hold = null;
130
+ }
131
+
132
+ function tick() {
133
+ if (!_hold) return;
134
+ const elapsed = Date.now() - _hold.startTime;
135
+ const progress = Math.min(elapsed / UNLOCK_HOLD_MS, 1);
136
+ updateProgress(_hold.overlay, progress);
137
+ _hold.rafId = requestAnimationFrame(tick);
138
+ }
139
+
140
+ function onMove(e) {
141
+ if (!_hold) return;
142
+ positionOverlay(_hold.overlay, e.clientX, e.clientY);
143
+ const dx = e.clientX - _hold.startPos.x;
144
+ const dy = e.clientY - _hold.startPos.y;
145
+ if (Math.hypot(dx, dy) > UNLOCK_MOVE_TOLERANCE) cancelHold();
146
+ }
147
+
148
+ function onUp() {
149
+ cancelHold();
150
+ }
151
+
152
+ // Attaches the capture-phase mousedown handler that intercepts clicks on
153
+ // locked targets and starts the long-press unlock hold.
154
+ export function installUnlockHold(container) {
155
+ container.addEventListener('mousedown', (e) => {
156
+ if (e.button !== 0) return;
157
+ const target = hitTestLocked(container, e.clientX, e.clientY);
158
+ if (!target) return;
159
+
160
+ // Block vis-network, panels, and all other handlers.
161
+ e.stopImmediatePropagation();
162
+ e.preventDefault();
163
+
164
+ cancelHold();
165
+ const overlay = createOverlay();
166
+ positionOverlay(overlay, e.clientX, e.clientY);
167
+ _hold = {
168
+ target,
169
+ startTime: Date.now(),
170
+ startPos: { x: e.clientX, y: e.clientY },
171
+ overlay,
172
+ rafId: null,
173
+ timeoutId: setTimeout(() => {
174
+ performUnlock(target);
175
+ cancelHold();
176
+ }, UNLOCK_HOLD_MS),
177
+ };
178
+ tick();
179
+ document.addEventListener('mouseup', onUp, true);
180
+ document.addEventListener('mousemove', onMove, true);
181
+ }, { capture: true });
182
+ }
@@ -0,0 +1,20 @@
1
+ // ── Zoom controls ─────────────────────────────────────────────────────────────
2
+
3
+ import { st } from './state.js';
4
+
5
+ export function adjustZoom(delta) {
6
+ if (!st.network) return;
7
+ st.network.moveTo({ scale: Math.max(0.1, Math.min(3, st.network.getScale() + delta)), animation: false });
8
+ updateZoomDisplay();
9
+ }
10
+
11
+ export function resetZoom() {
12
+ if (!st.network) return;
13
+ st.network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
14
+ setTimeout(updateZoomDisplay, 350);
15
+ }
16
+
17
+ export function updateZoomDisplay() {
18
+ if (!st.network) return;
19
+ document.getElementById('zoomLevel').textContent = Math.round(st.network.getScale() * 100) + '%';
20
+ }