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,161 @@
1
+ // ── Alignment guides ───────────────────────────────────────────────────────────
2
+ // Detects center-to-center horizontal/vertical alignment during drag.
3
+ // Priority: same shapeType. Fallback: any shapeType if no same-type match found on that axis.
4
+
5
+ import { st, markDirty } from './state.js';
6
+ import { t } from './t.js';
7
+
8
+ const THRESHOLD = 6; // world units — snap detection distance
9
+ const EXT = 40; // world units — line extension beyond both centers
10
+
11
+ export let activeGuides = [];
12
+
13
+ export function applyAlignGuidesState(enabled) {
14
+ st.alignGuides = enabled;
15
+ const btn = document.getElementById('btnAlign');
16
+ btn.classList.toggle('tool-active', enabled);
17
+ btn.title = t('diagram.toolbar.align_guides');
18
+ if (!enabled) activeGuides = [];
19
+ }
20
+
21
+ export function toggleAlignGuides() {
22
+ applyAlignGuidesState(!st.alignGuides);
23
+ markDirty();
24
+ if (!st.alignGuides && st.network) st.network.redraw();
25
+ }
26
+
27
+ // Among candidates on the same axis, pick the closest to the dragged center.
28
+ // Tiebreak: for H axis (same Y) prefer rightmost (largest ox);
29
+ // for V axis (same X) prefer lowest (largest oy).
30
+ function pickBest(candidates, dx, dy, axis) {
31
+ if (!candidates.length) return null;
32
+ return candidates.reduce((best, curr) => {
33
+ const dBest = axis === 'h' ? Math.abs(dx - best.ox) : Math.abs(dy - best.oy);
34
+ const dCurr = axis === 'h' ? Math.abs(dx - curr.ox) : Math.abs(dy - curr.oy);
35
+ if (dCurr < dBest) return curr;
36
+ if (dCurr === dBest)
37
+ return axis === 'h' ? (curr.ox > best.ox ? curr : best)
38
+ : (curr.oy > best.oy ? curr : best);
39
+ return best;
40
+ });
41
+ }
42
+
43
+ // Collects all candidates per axis, separated by same-type / any-type.
44
+ function collectCandidates(draggedBody, draggedType, draggedIds) {
45
+ const dx = draggedBody.x, dy = draggedBody.y;
46
+ const sameH = [], sameV = [], anyH = [], anyV = [];
47
+
48
+ for (const otherId of st.nodes.getIds()) {
49
+ if (draggedIds.has(otherId)) continue;
50
+ const other = st.nodes.get(otherId);
51
+ if (!other) continue;
52
+ const otherBody = st.network.body.nodes[otherId];
53
+ if (!otherBody) continue;
54
+ const ox = otherBody.x, oy = otherBody.y;
55
+ const sameType = other.shapeType === draggedType;
56
+ const c = { ox, oy };
57
+
58
+ if (Math.abs(dy - oy) <= THRESHOLD) (sameType ? sameH : anyH).push(c);
59
+ if (Math.abs(dx - ox) <= THRESHOLD) (sameType ? sameV : anyV).push(c);
60
+ }
61
+
62
+ return { sameH, sameV, anyH, anyV };
63
+ }
64
+
65
+ // Returns the best snap target Y (H axis) and X (V axis) for a dragged node.
66
+ // Same-type nodes take priority; any-type is used as fallback if no same-type match found.
67
+ function findSnapAxes(draggedBody, draggedType, draggedIds) {
68
+ const dx = draggedBody.x, dy = draggedBody.y;
69
+ const { sameH, sameV, anyH, anyV } = collectCandidates(draggedBody, draggedType, draggedIds);
70
+
71
+ const hMatch = pickBest(sameH.length ? sameH : anyH, dx, dy, 'h');
72
+ const vMatch = pickBest(sameV.length ? sameV : anyV, dx, dy, 'v');
73
+
74
+ return {
75
+ snapX: vMatch ? vMatch.ox : dx,
76
+ snapY: hMatch ? hMatch.oy : dy,
77
+ };
78
+ }
79
+
80
+ // Called at dragEnd — snaps each dragged node onto the guide axis when active.
81
+ export function snapToAlignGuides(params) {
82
+ if (!st.alignGuides || !params.nodes || !params.nodes.length || !st.network) return;
83
+
84
+ const draggedIds = new Set(params.nodes);
85
+
86
+ for (const draggedId of params.nodes) {
87
+ const draggedNode = st.nodes.get(draggedId);
88
+ if (!draggedNode) continue;
89
+ const draggedBody = st.network.body.nodes[draggedId];
90
+ if (!draggedBody) continue;
91
+
92
+ const { snapX, snapY } = findSnapAxes(draggedBody, draggedNode.shapeType, draggedIds);
93
+ if (snapX !== draggedBody.x || snapY !== draggedBody.y) {
94
+ st.network.moveNode(draggedId, snapX, snapY);
95
+ }
96
+ }
97
+ }
98
+
99
+ export function clearAlignGuides() {
100
+ if (activeGuides.length) {
101
+ activeGuides = [];
102
+ if (st.network) st.network.redraw();
103
+ }
104
+ }
105
+
106
+ export function onDragging(params) {
107
+ if (!st.alignGuides || !params.nodes || !params.nodes.length) {
108
+ clearAlignGuides();
109
+ return;
110
+ }
111
+
112
+ const draggedIds = new Set(params.nodes);
113
+ const newGuides = [];
114
+
115
+ for (const draggedId of params.nodes) {
116
+ const draggedNode = st.nodes.get(draggedId);
117
+ if (!draggedNode) continue;
118
+ const draggedType = draggedNode.shapeType;
119
+ const draggedBody = st.network.body.nodes[draggedId];
120
+ if (!draggedBody) continue;
121
+ const dx = draggedBody.x;
122
+ const dy = draggedBody.y;
123
+
124
+ const { sameH, sameV, anyH, anyV } = collectCandidates(draggedBody, draggedType, draggedIds);
125
+
126
+ const hMatch = pickBest(sameH.length ? sameH : anyH, dx, dy, 'h');
127
+ const vMatch = pickBest(sameV.length ? sameV : anyV, dx, dy, 'v');
128
+
129
+ if (hMatch) newGuides.push({ type: 'h', y: (dy + hMatch.oy) / 2, x1: Math.min(dx, hMatch.ox) - EXT, x2: Math.max(dx, hMatch.ox) + EXT });
130
+ if (vMatch) newGuides.push({ type: 'v', x: (dx + vMatch.ox) / 2, y1: Math.min(dy, vMatch.oy) - EXT, y2: Math.max(dy, vMatch.oy) + EXT });
131
+ }
132
+
133
+ activeGuides = newGuides;
134
+ if (st.network) st.network.redraw();
135
+ }
136
+
137
+ // Called on network "afterDrawing" — draws guides in canvas (world) coordinates.
138
+ export function drawAlignmentGuides(ctx) {
139
+ if (!st.alignGuides || !activeGuides.length) return;
140
+
141
+ const isDark = document.documentElement.classList.contains('dark');
142
+ ctx.save();
143
+ ctx.strokeStyle = isDark ? 'rgba(210, 210, 210, 0.9)' : 'rgba(50, 50, 50, 0.85)';
144
+ ctx.lineWidth = 0.5;
145
+ ctx.setLineDash([4, 4]);
146
+ ctx.beginPath();
147
+
148
+ for (const guide of activeGuides) {
149
+ if (guide.type === 'h') {
150
+ ctx.moveTo(guide.x1, guide.y);
151
+ ctx.lineTo(guide.x2, guide.y);
152
+ } else {
153
+ ctx.moveTo(guide.x, guide.y1);
154
+ ctx.lineTo(guide.x, guide.y2);
155
+ }
156
+ }
157
+
158
+ ctx.stroke();
159
+ ctx.setLineDash([]);
160
+ ctx.restore();
161
+ }
@@ -0,0 +1,172 @@
1
+ // ── Clipboard (copy / paste) ───────────────────────────────────────────────────
2
+
3
+ import { st, markDirty } from './state.js';
4
+ import { pushSnapshot } from './history.js';
5
+ import { visNodeProps, SHAPE_DEFAULTS } from './node-rendering.js';
6
+ import { visEdgeProps } from './edge-rendering.js';
7
+ import { showNodePanel } from './node-panel.js';
8
+ import { showToast } from './toast.js';
9
+ import { t } from './t.js';
10
+ import { uploadImageBlob } from './image-upload.js';
11
+
12
+ // ── Shared: render selection to a PNG blob ────────────────────────────────────
13
+
14
+ async function _selectionToBlob() {
15
+ if (!st.network || !st.selectedNodeIds.length) return null;
16
+
17
+ const PAD = 20;
18
+ const dpr = window.devicePixelRatio || 1;
19
+
20
+ // Compute bounding box in canvas coordinates
21
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
22
+ for (const id of st.selectedNodeIds) {
23
+ const n = st.nodes.get(id);
24
+ const bn = st.network.body.nodes[id];
25
+ if (!bn) continue;
26
+ const defaults = SHAPE_DEFAULTS[(n && n.shapeType) || 'box'] || [100, 40];
27
+ const w = (n && n.nodeWidth) || defaults[0];
28
+ const h = (n && n.nodeHeight) || defaults[1];
29
+ const rot = (n && n.rotation) || 0;
30
+ let hw, hh;
31
+ if (rot === 0) {
32
+ hw = w / 2; hh = h / 2;
33
+ } else {
34
+ const cos = Math.abs(Math.cos(rot)); const sin = Math.abs(Math.sin(rot));
35
+ hw = (w * cos + h * sin) / 2;
36
+ hh = (w * sin + h * cos) / 2;
37
+ }
38
+ minX = Math.min(minX, bn.x - hw); maxX = Math.max(maxX, bn.x + hw);
39
+ minY = Math.min(minY, bn.y - hh); maxY = Math.max(maxY, bn.y + hh);
40
+ }
41
+
42
+ // Convert canvas coords to DOM pixels
43
+ const tl = st.network.canvasToDOM({ x: minX, y: minY });
44
+ const br = st.network.canvasToDOM({ x: maxX, y: maxY });
45
+
46
+ const cropX = Math.max(0, Math.floor(tl.x - PAD));
47
+ const cropY = Math.max(0, Math.floor(tl.y - PAD));
48
+ const cropW = Math.ceil(br.x - tl.x + PAD * 2);
49
+ const cropH = Math.ceil(br.y - tl.y + PAD * 2);
50
+
51
+ // Temporarily deselect so highlights don't appear in the PNG
52
+ const savedNodeIds = [...st.selectedNodeIds];
53
+ st.network.unselectAll();
54
+ st.selectedNodeIds = [];
55
+ st.network.redraw();
56
+
57
+ const visCanvas = document.querySelector('#vis-canvas canvas');
58
+ if (!visCanvas) {
59
+ st.network.selectNodes(savedNodeIds);
60
+ return null;
61
+ }
62
+
63
+ // Crop into an offscreen canvas
64
+ const out = document.createElement('canvas');
65
+ out.width = cropW * dpr;
66
+ out.height = cropH * dpr;
67
+ const octx = out.getContext('2d');
68
+
69
+ const isDark = document.documentElement.classList.contains('dark');
70
+ octx.fillStyle = isDark ? '#030712' : '#f9fafb';
71
+ octx.fillRect(0, 0, out.width, out.height);
72
+ octx.drawImage(visCanvas,
73
+ cropX * dpr, cropY * dpr, cropW * dpr, cropH * dpr,
74
+ 0, 0, out.width, out.height
75
+ );
76
+
77
+ const blob = await new Promise((res) => out.toBlob(res, 'image/png'));
78
+ return { blob, savedNodeIds };
79
+ }
80
+
81
+ // ── Copy selection as PNG (to clipboard) ─────────────────────────────────────
82
+
83
+ export async function copySelectionAsPng() {
84
+ const result = await _selectionToBlob();
85
+ if (!result) return;
86
+ const { blob, savedNodeIds } = result;
87
+ try {
88
+ await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
89
+ showToast(t('diagram.toast.png_copied'));
90
+ } catch {
91
+ showToast(t('diagram.toast.png_copy_error'), 'error');
92
+ } finally {
93
+ st.network.selectNodes(savedNodeIds);
94
+ }
95
+ }
96
+
97
+ // ── Save selection as PNG to server ──────────────────────────────────────────
98
+
99
+ export async function saveSelectionAsPng(filename) {
100
+ const result = await _selectionToBlob();
101
+ if (!result) return;
102
+ const { blob, savedNodeIds } = result;
103
+ try {
104
+ const name = filename.replace(/\.[^.]+$/, ''); // strip extension
105
+ await uploadImageBlob(blob, 'png', name);
106
+ showToast(t('diagram.toast.diagram_saved_png'));
107
+ } catch {
108
+ showToast(t('diagram.toast.diagram_save_png_error'), 'error');
109
+ } finally {
110
+ st.network.selectNodes(savedNodeIds);
111
+ }
112
+ }
113
+
114
+ export function copySelected() {
115
+ if (!st.network || !st.selectedNodeIds.length) return;
116
+
117
+ // Use vis-network's full selection — includes anchor endpoints of free arrows,
118
+ // which are excluded from st.selectedNodeIds (no node panel for anchors).
119
+ const allNodeIds = st.network.getSelectedNodes();
120
+ const allEdgeIds = new Set(st.network.getSelectedEdges());
121
+
122
+ const positions = st.network.getPositions(allNodeIds);
123
+ const copiedNodes = allNodeIds.map((id) => {
124
+ const { ctxRenderer, ...rest } = st.nodes.get(id);
125
+ return { ...rest, x: positions[id]?.x ?? rest.x, y: positions[id]?.y ?? rest.y };
126
+ });
127
+
128
+ // Edges where both endpoints are in the selection, plus any explicitly selected edges.
129
+ const selectedSet = new Set(allNodeIds);
130
+ const copiedEdges = st.edges.get().filter((e) =>
131
+ (selectedSet.has(e.from) && selectedSet.has(e.to)) || allEdgeIds.has(e.id)
132
+ );
133
+
134
+ st.clipboard = { nodes: copiedNodes, edges: copiedEdges };
135
+ }
136
+
137
+ export function pasteClipboard() {
138
+ if (!st.clipboard || !st.clipboard.nodes.length || !st.network) return;
139
+ pushSnapshot();
140
+
141
+ const OFFSET = 40;
142
+ const idMap = {};
143
+ st.clipboard.nodes.forEach((n) => { idMap[n.id] = 'n' + Date.now() + Math.random().toString(36).slice(2); });
144
+
145
+ const newNodes = st.clipboard.nodes.map((n) => ({
146
+ ...n, id: idMap[n.id], x: (n.x || 0) + OFFSET, y: (n.y || 0) + OFFSET,
147
+ ...visNodeProps(n.shapeType || 'box', n.colorKey || 'c-gray', n.nodeWidth, n.nodeHeight, n.fontSize, n.textAlign, n.textValign),
148
+ }));
149
+ const newEdges = st.clipboard.edges.map((e) => ({
150
+ ...e,
151
+ id: 'e' + Date.now() + Math.random().toString(36).slice(2),
152
+ from: idMap[e.from], to: idMap[e.to],
153
+ ...visEdgeProps(e.arrowDir ?? 'to', e.dashes ?? false),
154
+ ...(e.fontSize ? { font: { size: e.fontSize, align: 'middle', color: '#6b7280' } } : {}),
155
+ }));
156
+
157
+ st.nodes.add(newNodes);
158
+ st.edges.add(newEdges);
159
+
160
+ const newIds = newNodes.map((n) => n.id);
161
+ st.network.selectNodes(newIds);
162
+ st.selectedNodeIds = newIds;
163
+ st.selectedEdgeIds = [];
164
+ showNodePanel();
165
+ markDirty();
166
+
167
+ // Offset clipboard so each successive paste is staggered
168
+ st.clipboard = {
169
+ nodes: st.clipboard.nodes.map((n) => ({ ...n, x: (n.x || 0) + OFFSET, y: (n.y || 0) + OFFSET })),
170
+ edges: st.clipboard.edges,
171
+ };
172
+ }
@@ -0,0 +1,109 @@
1
+ // ── Constants ─────────────────────────────────────────────────────────────────
2
+ // Pure compile-time values shared across all diagram modules.
3
+
4
+ export const GRID_SIZE = 20;
5
+
6
+ export const TOOL_BTN_MAP = {
7
+ select: 'toolSelect',
8
+ 'addNode:box': 'toolBox',
9
+ 'addNode:ellipse': 'toolEllipse',
10
+ 'addNode:database': 'toolDatabase',
11
+ 'addNode:circle': 'toolCircle',
12
+ 'addNode:actor': 'toolActor',
13
+ 'addNode:post-it': 'toolPostIt',
14
+ 'addNode:text-free': 'toolTextFree',
15
+ 'addNode:image': 'toolImage',
16
+ addEdge: 'toolArrow',
17
+ };
18
+
19
+ export const NODE_COLORS = {
20
+ 'c-gray': { bg: '#f5f5f4', border: '#a8a29e', font: '#292524', hbg: '#e7e5e4', hborder: '#78716c' },
21
+ 'c-blue': { bg: '#dbeafe', border: '#3b82f6', font: '#1e40af', hbg: '#bfdbfe', hborder: '#2563eb' },
22
+ 'c-green': { bg: '#dcfce7', border: '#22c55e', font: '#166534', hbg: '#bbf7d0', hborder: '#16a34a' },
23
+ 'c-amber': { bg: '#fef9c3', border: '#f59e0b', font: '#78350f', hbg: '#fef08a', hborder: '#d97706' },
24
+ 'c-rose': { bg: '#ffe4e6', border: '#f43f5e', font: '#881337', hbg: '#fecdd3', hborder: '#e11d48' },
25
+ 'c-purple': { bg: '#ede9fe', border: '#8b5cf6', font: '#4c1d95', hbg: '#ddd6fe', hborder: '#7c3aed' },
26
+ 'c-teal': { bg: '#ccfbf1', border: '#14b8a6', font: '#134e4a', hbg: '#99f6e4', hborder: '#0d9488' },
27
+ 'c-orange': { bg: '#ffedd5', border: '#f97316', font: '#7c2d12', hbg: '#fed7aa', hborder: '#ea580c' },
28
+ 'c-cyan': { bg: '#cffafe', border: '#06b6d4', font: '#164e63', hbg: '#a5f3fc', hborder: '#0891b2' },
29
+ 'c-indigo': { bg: '#e0e7ff', border: '#6366f1', font: '#312e81', hbg: '#c7d2fe', hborder: '#4f46e5' },
30
+ 'c-pink': { bg: '#fce7f3', border: '#ec4899', font: '#831843', hbg: '#fbcfe8', hborder: '#db2777' },
31
+ 'c-lime': { bg: '#ecfccb', border: '#84cc16', font: '#365314', hbg: '#d9f99d', hborder: '#65a30d' },
32
+ 'c-red': { bg: '#fee2e2', border: '#ef4444', font: '#7f1d1d', hbg: '#fecaca', hborder: '#dc2626' },
33
+ 'c-sky': { bg: '#e0f2fe', border: '#0ea5e9', font: '#0c4a6e', hbg: '#bae6fd', hborder: '#0284c7' },
34
+ 'c-slate': { bg: '#f1f5f9', border: '#64748b', font: '#0f172a', hbg: '#e2e8f0', hborder: '#475569' },
35
+ };
36
+
37
+ // Default palette shown in the diagram editor (can be overridden via admin config).
38
+ export const DEFAULT_NODE_PALETTE = [
39
+ 'c-gray','c-slate','c-blue','c-sky','c-cyan','c-teal','c-green','c-lime',
40
+ 'c-amber','c-orange','c-red','c-rose','c-pink','c-purple','c-indigo',
41
+ ];
42
+
43
+ export const DEFAULT_EDGE_PALETTE = [
44
+ '#a8a29e','#374151','#3b82f6','#14b8a6','#22c55e','#f97316','#ef4444','#a855f7',
45
+ ];
46
+
47
+ // Per-slot lightness ratio (border_L / bg_L) derived from the original NODE_COLORS pairs.
48
+ // Applied when a slot's bg is customised so the border preserves the same contrast as the original.
49
+ export const NODE_L_RATIOS = {
50
+ 'c-gray': 0.667, 'c-slate': 0.488, 'c-blue': 0.645, 'c-sky': 0.518,
51
+ 'c-cyan': 0.473, 'c-teal': 0.448, 'c-green': 0.489, 'c-lime': 0.496,
52
+ 'c-amber': 0.570, 'c-orange': 0.578, 'c-red': 0.640, 'c-rose': 0.636,
53
+ 'c-pink': 0.638, 'c-purple': 0.694, 'c-indigo': 0.710,
54
+ };
55
+
56
+ // Derive border/font/hover colors from a custom bg hex, using HSL so the hue is preserved.
57
+ // lRatio = border_L / bg_L; use NODE_L_RATIOS[key] for per-slot accuracy.
58
+ export function deriveNodeColors(bgHex, lRatio = 0.60) {
59
+ const r = parseInt(bgHex.slice(1, 3), 16) / 255;
60
+ const g = parseInt(bgHex.slice(3, 5), 16) / 255;
61
+ const b = parseInt(bgHex.slice(5, 7), 16) / 255;
62
+
63
+ // RGB → HSL
64
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
65
+ const l = (max + min) / 2;
66
+ const d = max - min;
67
+ let h = 0, s = 0;
68
+ if (d > 0) {
69
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
70
+ if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
71
+ else if (max === g) h = ((b - r) / d + 2) / 6;
72
+ else h = ((r - g) / d + 4) / 6;
73
+ }
74
+
75
+ function hslToHex(hh, ss, ll) {
76
+ let rr, gg, bb;
77
+ if (ss === 0) {
78
+ rr = gg = bb = ll;
79
+ } else {
80
+ const hue2rgb = (p, q, t) => {
81
+ if (t < 0) t += 1; if (t > 1) t -= 1;
82
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
83
+ if (t < 1 / 2) return q;
84
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
85
+ return p;
86
+ };
87
+ const q = ll < 0.5 ? ll * (1 + ss) : ll + ss - ll * ss;
88
+ const p = 2 * ll - q;
89
+ rr = hue2rgb(p, q, hh + 1 / 3);
90
+ gg = hue2rgb(p, q, hh);
91
+ bb = hue2rgb(p, q, hh - 1 / 3);
92
+ }
93
+ return '#' + [rr, gg, bb]
94
+ .map((v) => Math.max(0, Math.min(255, Math.round(v * 255))).toString(16).padStart(2, '0'))
95
+ .join('');
96
+ }
97
+
98
+ const borderL = l * lRatio;
99
+ const hbgL = l * 0.93; // slightly darker than bg for hover
100
+ const hborderL = borderL * 0.89; // slightly darker than border for hover
101
+
102
+ return {
103
+ bg: bgHex,
104
+ border: hslToHex(h, s, borderL),
105
+ font: l > 0.5 ? '#292524' : '#fafaf9',
106
+ hbg: hslToHex(h, s, Math.min(l, hbgL)),
107
+ hborder: hslToHex(h, s, Math.min(borderL, hborderL)),
108
+ };
109
+ }
@@ -0,0 +1,43 @@
1
+ // ── Debug overlay ─────────────────────────────────────────────────────────────
2
+ // DOM overlay showing node IDs and bounding-box coordinates for each node.
3
+ // Toggled by the showDiagramDebug config flag (Admin panel).
4
+
5
+ import { st } from './state.js';
6
+
7
+ export function toggleDebug() {
8
+ st.debugMode = !st.debugMode;
9
+ document.getElementById('btnDebug').classList.toggle('tool-active', st.debugMode);
10
+ if (st.network) st.network.redraw();
11
+ }
12
+
13
+ // Called on network "afterDrawing". Recycles existing .debug-box elements by node id.
14
+ export function drawDebugOverlay() {
15
+ const layer = document.getElementById('debugLayer');
16
+ if (!st.debugMode || !st.network) { layer.innerHTML = ''; return; }
17
+
18
+ const existing = new Map(
19
+ [...layer.querySelectorAll('.debug-box')].map((el) => [el.dataset.nid, el])
20
+ );
21
+ const seen = new Set();
22
+
23
+ for (const id of st.canonicalOrder) {
24
+ const bodyNode = st.network.body.nodes[id];
25
+ if (!bodyNode) continue;
26
+ const w = bodyNode.shape.width || 0;
27
+ const h = bodyNode.shape.height || 0;
28
+ const cx = bodyNode.x, cy = bodyNode.y;
29
+ const L = cx - w / 2, T = cy - h / 2;
30
+
31
+ const dom = st.network.canvasToDOM({ x: cx + w / 2 + 8, y: cy - h / 2 });
32
+ const text = [`id : ${id}`, `cx=${Math.round(cx)} cy=${Math.round(cy)}`, `w =${Math.round(w)} h =${Math.round(h)}`, `L =${Math.round(L)} T =${Math.round(T)}`].join('\n');
33
+
34
+ let box = existing.get(String(id));
35
+ if (!box) { box = document.createElement('div'); box.className = 'debug-box'; box.dataset.nid = String(id); layer.appendChild(box); }
36
+ box.textContent = text;
37
+ box.style.left = Math.round(dom.x) + 'px';
38
+ box.style.top = Math.round(dom.y) + 'px';
39
+ seen.add(String(id));
40
+ }
41
+
42
+ existing.forEach((el, nid) => { if (!seen.has(nid)) el.remove(); });
43
+ }