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,546 @@
1
+ // ── Marker (stabilo) annotations ────────────────────────────────────────────
2
+ // Depends on globals from state.js (currentDocId, annotationCounts) and
3
+ // sidebar state (refreshSidebar from state.js).
4
+
5
+ let stabiloActive = false;
6
+ let stabiloHidden = false; // state 3: highlights hidden
7
+ let stabiloPendingRange = null;
8
+ let stabiloAnnotations = [];
9
+ let stabiloDeleteTargetId = null;
10
+ let stabiloReadPopupAnnotationId = null;
11
+ let stabiloReadHideTimer = null;
12
+
13
+ // Cycles: normal → active → hidden → normal
14
+ function toggleMarker() {
15
+ const btn = document.getElementById("stabilo-btn");
16
+ const fills = btn.querySelectorAll("rect, polygon");
17
+ const crossEl = document.getElementById("stabilo-cross");
18
+
19
+ if (!stabiloActive && !stabiloHidden) {
20
+ // normal → active
21
+ stabiloActive = true;
22
+ btn.classList.remove(
23
+ "border-gray-200",
24
+ "dark:border-gray-700",
25
+ "text-gray-600",
26
+ "dark:text-gray-400",
27
+ );
28
+ btn.classList.add(
29
+ "border-yellow-400",
30
+ "bg-yellow-100",
31
+ "dark:bg-yellow-900/40",
32
+ "text-yellow-700",
33
+ "dark:text-yellow-300",
34
+ );
35
+ fills.forEach((el) =>
36
+ el.setAttribute(
37
+ "fill",
38
+ el.tagName === "polygon" ? "#fde047" : "#fef08a",
39
+ ),
40
+ );
41
+ crossEl.style.display = "none";
42
+ } else if (stabiloActive) {
43
+ // active → hidden
44
+ stabiloActive = false;
45
+ stabiloHidden = true;
46
+ btn.classList.remove(
47
+ "border-yellow-400",
48
+ "bg-yellow-100",
49
+ "dark:bg-yellow-900/40",
50
+ "text-yellow-700",
51
+ "dark:text-yellow-300",
52
+ );
53
+ btn.classList.add(
54
+ "border-gray-200",
55
+ "dark:border-gray-700",
56
+ "text-gray-600",
57
+ "dark:text-gray-400",
58
+ );
59
+ fills.forEach((el) =>
60
+ el.setAttribute(
61
+ "fill",
62
+ el.tagName === "polygon"
63
+ ? "#93c5fd"
64
+ : el.previousElementSibling
65
+ ? "#93c5fd"
66
+ : "#bfdbfe",
67
+ ),
68
+ );
69
+ crossEl.style.display = "block";
70
+ closeMarkerPopup();
71
+ setHighlightsVisible(false);
72
+ } else {
73
+ // hidden → normal
74
+ stabiloHidden = false;
75
+ crossEl.style.display = "none";
76
+ setHighlightsVisible(true);
77
+ }
78
+ }
79
+
80
+ function setHighlightsVisible(visible) {
81
+ document.querySelectorAll("mark[data-annotation-id]").forEach((m) => {
82
+ m.style.background = visible ? "rgba(250,204,21,0.5)" : "transparent";
83
+ m.style.cursor = visible ? "pointer" : "default";
84
+ m.style.pointerEvents = visible ? "auto" : "none";
85
+ });
86
+ const elevator = document.getElementById("stabilo-elevator");
87
+ if (elevator)
88
+ elevator.style.visibility = visible ? "visible" : "hidden";
89
+ }
90
+
91
+ // Load and apply annotations for current doc
92
+ async function loadAnnotations(docId) {
93
+ stabiloAnnotations = [];
94
+ try {
95
+ stabiloAnnotations = await fetch(
96
+ "/api/annotations/" + encodeURIComponent(docId),
97
+ ).then((r) => r.json());
98
+ } catch {
99
+ stabiloAnnotations = [];
100
+ }
101
+ applyAnnotationHighlights();
102
+ renderElevator();
103
+ }
104
+
105
+ // Re-apply all highlight marks in the rendered content
106
+ function applyAnnotationHighlights() {
107
+ const contentEl = document.getElementById("doc-content");
108
+ if (!contentEl) return;
109
+
110
+ // Remove existing marks (unwrap, keep text content)
111
+ contentEl
112
+ .querySelectorAll("mark[data-annotation-id]")
113
+ .forEach((mark) => {
114
+ const parent = mark.parentNode;
115
+ parent.replaceChild(
116
+ document.createTextNode(mark.textContent),
117
+ mark,
118
+ );
119
+ parent.normalize();
120
+ });
121
+
122
+ for (const ann of stabiloAnnotations) {
123
+ highlightAnnotation(contentEl, ann);
124
+ }
125
+
126
+ // Remove empty marks left by block-boundary splitting
127
+ contentEl
128
+ .querySelectorAll("mark[data-annotation-id]")
129
+ .forEach((mark) => {
130
+ if (!mark.textContent.trim()) mark.remove();
131
+ });
132
+ }
133
+
134
+ function highlightAnnotation(contentEl, ann) {
135
+ const selText = (ann.selectedText || "").replace(/\s+/g, " ").trim();
136
+ const ctxBefore = (ann.contextBefore || "")
137
+ .replace(/\s+/g, " ")
138
+ .trim();
139
+ const ctxAfter = (ann.contextAfter || "").replace(/\s+/g, " ").trim();
140
+ if (!selText) return;
141
+
142
+ const escRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
143
+ // Build a pattern that allows any whitespace between words
144
+ const toPat = (s) =>
145
+ s
146
+ .split(/\s+/)
147
+ .filter(Boolean)
148
+ .map(escRe)
149
+ .join("\\s+");
150
+
151
+ const text = contentEl.textContent;
152
+ const selPat = toPat(selText);
153
+ let match;
154
+
155
+ // Try with context first for disambiguation
156
+ if (ctxBefore || ctxAfter) {
157
+ const fullPat =
158
+ (ctxBefore ? toPat(ctxBefore) + "\\s*" : "") +
159
+ "(" + selPat + ")" +
160
+ (ctxAfter ? "\\s*" + toPat(ctxAfter) : "");
161
+ try {
162
+ match = new RegExp(fullPat, "d").exec(text);
163
+ } catch {
164
+ /* fall through */
165
+ }
166
+ }
167
+ if (!match) {
168
+ try {
169
+ match = new RegExp("(" + selPat + ")", "d").exec(text);
170
+ } catch {
171
+ return;
172
+ }
173
+ }
174
+ if (!match || !match.indices || !match.indices[1]) return;
175
+
176
+ const [startOff, endOff] = match.indices[1];
177
+
178
+ // Locate the text-node slices that cover [startOff, endOff)
179
+ const walker = document.createTreeWalker(
180
+ contentEl,
181
+ NodeFilter.SHOW_TEXT,
182
+ );
183
+ const slices = [];
184
+ let pos = 0;
185
+ while (walker.nextNode()) {
186
+ const node = walker.currentNode;
187
+ const len = node.nodeValue.length;
188
+ const nStart = pos;
189
+ const nEnd = pos + len;
190
+ pos = nEnd;
191
+ if (nEnd <= startOff) continue;
192
+ if (nStart >= endOff) break;
193
+ const sStart = Math.max(0, startOff - nStart);
194
+ const sEnd = Math.min(len, endOff - nStart);
195
+ if (sStart < sEnd) slices.push({ node, start: sStart, end: sEnd });
196
+ }
197
+
198
+ // Wrap each slice in its own <mark> within its current parent block.
199
+ // This produces valid DOM regardless of table/code/list/heading boundaries.
200
+ const markStyle =
201
+ "background:rgba(250,204,21,0.5);border-radius:2px;cursor:pointer;padding:0 1px;";
202
+ for (const { node, start, end } of slices) {
203
+ let target = node;
204
+ if (start > 0 && start < target.nodeValue.length) {
205
+ target = target.splitText(start);
206
+ }
207
+ const wantLen = end - start;
208
+ if (wantLen > 0 && wantLen < target.nodeValue.length) {
209
+ target.splitText(wantLen);
210
+ }
211
+ const mark = document.createElement("mark");
212
+ mark.setAttribute("data-annotation-id", ann.id);
213
+ mark.setAttribute("style", markStyle);
214
+ target.parentNode.insertBefore(mark, target);
215
+ mark.appendChild(target);
216
+ }
217
+ }
218
+
219
+ // ── Selection capture ───────────────────────────────────────────────────────
220
+ document.addEventListener("mouseup", (e) => {
221
+ if (!stabiloActive) return;
222
+ if (
223
+ document.getElementById("stabilo-popup") &&
224
+ !document.getElementById("stabilo-popup").classList.contains("hidden")
225
+ )
226
+ return;
227
+
228
+ const sel = window.getSelection();
229
+ if (!sel || sel.isCollapsed || !sel.toString().trim()) return;
230
+
231
+ const contentEl = document.getElementById("doc-content");
232
+ if (!contentEl || !contentEl.contains(sel.anchorNode)) return;
233
+
234
+ const range = sel.getRangeAt(0);
235
+ stabiloPendingRange = range.cloneRange();
236
+
237
+ // Compute the *actual* offset of the selection in contentEl.textContent,
238
+ // not via indexOf (which would match the first occurrence anywhere).
239
+ const preRange = document.createRange();
240
+ preRange.selectNodeContents(contentEl);
241
+ preRange.setEnd(range.startContainer, range.startOffset);
242
+ const rawStart = preRange.toString().length;
243
+ const rawSelected = range.toString();
244
+ const fullText = contentEl.textContent;
245
+
246
+ // Extract context (30 chars before/after) then normalize whitespace
247
+ const normalize = (s) => s.replace(/\s+/g, " ").trim();
248
+ const selectedText = normalize(rawSelected);
249
+ const ctxBefore = normalize(
250
+ fullText.slice(Math.max(0, rawStart - 30), rawStart),
251
+ );
252
+ const ctxAfter = normalize(
253
+ fullText.slice(
254
+ rawStart + rawSelected.length,
255
+ rawStart + rawSelected.length + 30,
256
+ ),
257
+ );
258
+
259
+ stabiloPendingRange._selectedText = selectedText;
260
+ stabiloPendingRange._contextBefore = ctxBefore;
261
+ stabiloPendingRange._contextAfter = ctxAfter;
262
+
263
+ // Position popup near selection
264
+ const rect = range.getBoundingClientRect();
265
+ positionPopup(
266
+ "stabilo-popup",
267
+ rect.left + window.scrollX,
268
+ rect.bottom + window.scrollY + 8,
269
+ );
270
+ document.getElementById("stabilo-selected-preview").textContent =
271
+ selectedText.length > 120
272
+ ? selectedText.slice(0, 120) + "…"
273
+ : selectedText;
274
+ document.getElementById("stabilo-note-input").value = "";
275
+ document.getElementById("stabilo-popup").classList.remove("hidden");
276
+ setTimeout(
277
+ () => document.getElementById("stabilo-note-input").focus(),
278
+ 50,
279
+ );
280
+ });
281
+
282
+ function positionPopup(id, x, y) {
283
+ const el = document.getElementById(id);
284
+ const w = 320;
285
+ const vw = window.innerWidth;
286
+ const vh = window.innerHeight;
287
+ let left = x;
288
+ let top = y;
289
+ if (left + w > vw - 16) left = vw - w - 16;
290
+ if (left < 8) left = 8;
291
+ el.style.left = left + "px";
292
+ el.style.top = Math.min(top, vh - 260) + "px";
293
+ }
294
+
295
+ function closeMarkerPopup() {
296
+ document.getElementById("stabilo-popup").classList.add("hidden");
297
+ stabiloPendingRange = null;
298
+ window.getSelection()?.removeAllRanges();
299
+ }
300
+
301
+ function cancelMarkerPopup() {
302
+ closeMarkerPopup();
303
+ }
304
+
305
+ async function saveAnnotation() {
306
+ if (!stabiloPendingRange || !currentDocId) return;
307
+ const note = document.getElementById("stabilo-note-input").value.trim();
308
+ if (!note) {
309
+ document.getElementById("stabilo-note-input").focus();
310
+ return;
311
+ }
312
+
313
+ try {
314
+ const ann = await fetch(
315
+ "/api/annotations/" + encodeURIComponent(currentDocId),
316
+ {
317
+ method: "POST",
318
+ headers: { "Content-Type": "application/json" },
319
+ body: JSON.stringify({
320
+ selectedText: stabiloPendingRange._selectedText,
321
+ contextBefore: stabiloPendingRange._contextBefore,
322
+ contextAfter: stabiloPendingRange._contextAfter,
323
+ note,
324
+ }),
325
+ },
326
+ ).then((r) => r.json());
327
+
328
+ stabiloAnnotations.push(ann);
329
+ closeMarkerPopup();
330
+ applyAnnotationHighlights();
331
+ renderElevator();
332
+ annotationCounts[currentDocId] = (annotationCounts[currentDocId] || 0) + 1;
333
+ refreshSidebar();
334
+ } catch {
335
+ /* ignore */
336
+ }
337
+ }
338
+
339
+ // ── Read popup ──────────────────────────────────────────────────────────────
340
+ function showReadPopup(ann, markEl) {
341
+ clearTimeout(stabiloReadHideTimer);
342
+ stabiloReadPopupAnnotationId = ann.id;
343
+ const popup = document.getElementById("stabilo-read-popup");
344
+ const isOrphan = !document
345
+ .getElementById("doc-content")
346
+ .querySelector(`mark[data-annotation-id="${ann.id}"]`);
347
+ document
348
+ .getElementById("stabilo-read-orphan")
349
+ .classList.toggle("hidden", !isOrphan);
350
+ document.getElementById("stabilo-read-text").textContent = ann.note;
351
+ document.getElementById("stabilo-read-date").textContent = new Date(
352
+ ann.createdAt,
353
+ ).toLocaleDateString("fr-FR", {
354
+ day: "2-digit",
355
+ month: "short",
356
+ year: "numeric",
357
+ });
358
+
359
+ document.getElementById("stabilo-delete-btn").onclick = () =>
360
+ askDeleteAnnotation(ann.id);
361
+
362
+ const rect = markEl.getBoundingClientRect();
363
+ positionPopup(
364
+ "stabilo-read-popup",
365
+ rect.left + window.scrollX,
366
+ rect.bottom + window.scrollY + 8,
367
+ );
368
+ popup.classList.remove("hidden");
369
+
370
+ // Highlight elevator pill (preserve red for orphans)
371
+ document.querySelectorAll(".stabilo-pill").forEach((p) => {
372
+ const isOrphan = p.classList.contains("border-red-600");
373
+ if (p.dataset.id === ann.id) {
374
+ p.style.background = isOrphan ? "#b91c1c" : "#f97316";
375
+ } else {
376
+ p.style.background = isOrphan ? "#ef4444" : "#facc15";
377
+ }
378
+ });
379
+ // Scroll elevator pill into view
380
+ const pill = document.querySelector(
381
+ `.stabilo-pill[data-id="${ann.id}"]`,
382
+ );
383
+ if (pill) pill.scrollIntoView({ block: "nearest", behavior: "smooth" });
384
+ }
385
+
386
+ function scheduleHideReadPopup() {
387
+ stabiloReadHideTimer = setTimeout(() => {
388
+ document.getElementById("stabilo-read-popup").classList.add("hidden");
389
+ document.querySelectorAll(".stabilo-pill").forEach((p) => {
390
+ p.style.background = p.classList.contains("border-red-600")
391
+ ? "#ef4444"
392
+ : "#facc15";
393
+ });
394
+ }, 300);
395
+ }
396
+
397
+ // Keep popup visible when hovering it
398
+ document
399
+ .getElementById("stabilo-read-popup")
400
+ .addEventListener("mouseenter", () =>
401
+ clearTimeout(stabiloReadHideTimer),
402
+ );
403
+ document
404
+ .getElementById("stabilo-read-popup")
405
+ .addEventListener("mouseleave", () => scheduleHideReadPopup());
406
+
407
+ // ── Delete ──────────────────────────────────────────────────────────────────
408
+ function askDeleteAnnotation(id) {
409
+ stabiloDeleteTargetId = id;
410
+ document.getElementById("stabilo-read-popup").classList.add("hidden");
411
+ const confirmEl = document.getElementById("stabilo-confirm-delete");
412
+ confirmEl.style.left =
413
+ document.getElementById("stabilo-read-popup").style.left;
414
+ confirmEl.style.top =
415
+ document.getElementById("stabilo-read-popup").style.top;
416
+ confirmEl.classList.remove("hidden");
417
+ }
418
+
419
+ function cancelDeleteAnnotation() {
420
+ document
421
+ .getElementById("stabilo-confirm-delete")
422
+ .classList.add("hidden");
423
+ stabiloDeleteTargetId = null;
424
+ }
425
+
426
+ async function confirmDeleteAnnotation() {
427
+ if (!stabiloDeleteTargetId || !currentDocId) return;
428
+ try {
429
+ await fetch(
430
+ `/api/annotations/${encodeURIComponent(currentDocId)}/${stabiloDeleteTargetId}`,
431
+ { method: "DELETE" },
432
+ );
433
+ stabiloAnnotations = stabiloAnnotations.filter(
434
+ (a) => a.id !== stabiloDeleteTargetId,
435
+ );
436
+ stabiloDeleteTargetId = null;
437
+ document
438
+ .getElementById("stabilo-confirm-delete")
439
+ .classList.add("hidden");
440
+ applyAnnotationHighlights();
441
+ renderElevator();
442
+ if (currentDocId) {
443
+ const next = (annotationCounts[currentDocId] || 0) - 1;
444
+ if (next <= 0) delete annotationCounts[currentDocId];
445
+ else annotationCounts[currentDocId] = next;
446
+ refreshSidebar();
447
+ }
448
+ } catch {
449
+ /* ignore */
450
+ }
451
+ }
452
+
453
+ // ── Elevator ────────────────────────────────────────────────────────────────
454
+ function renderElevator() {
455
+ const elevator = document.getElementById("stabilo-elevator");
456
+ const contentEl = document.getElementById("doc-content");
457
+ if (!contentEl) return;
458
+
459
+ if (stabiloAnnotations.length === 0) {
460
+ elevator.classList.add("hidden");
461
+ elevator.innerHTML = "";
462
+ return;
463
+ }
464
+
465
+ elevator.classList.remove("hidden");
466
+ const docHeight = contentEl.scrollHeight;
467
+ const elevatorHeight = window.innerHeight;
468
+
469
+ elevator.innerHTML = "";
470
+ for (const ann of stabiloAnnotations) {
471
+ const mark = contentEl.querySelector(
472
+ `mark[data-annotation-id="${ann.id}"]`,
473
+ );
474
+ const relPos = mark
475
+ ? (mark.offsetTop / Math.max(docHeight, 1)) * elevatorHeight
476
+ : 0;
477
+ const orphan = !mark;
478
+
479
+ const pill = document.createElement("button");
480
+ pill.className =
481
+ "stabilo-pill w-8 h-8 rounded border-2 shadow text-xs flex items-center justify-center transition-colors shrink-0 " +
482
+ (orphan ? "border-red-600" : "border-yellow-500");
483
+ pill.dataset.id = ann.id;
484
+ pill.style.background = orphan ? "#ef4444" : "#facc15";
485
+ const noteShort =
486
+ ann.note.length > 60 ? ann.note.slice(0, 60) + "…" : ann.note;
487
+ pill.title = orphan
488
+ ? `${window.t('annotation.orphan')}\n\n${noteShort}`
489
+ : noteShort;
490
+ pill.textContent = orphan ? "⚠" : "✎";
491
+ if (orphan) pill.style.color = "#fff";
492
+
493
+ pill.addEventListener("click", () => {
494
+ const m = contentEl.querySelector(
495
+ `mark[data-annotation-id="${ann.id}"]`,
496
+ );
497
+ if (m) {
498
+ const docView =
499
+ document.getElementById("doc-view") ||
500
+ document.getElementById("content-area");
501
+ docView.scrollTo({ top: m.offsetTop - 120, behavior: "smooth" });
502
+ } else {
503
+ // Orphan annotation — no mark to anchor to; show popup on the pill itself
504
+ clearTimeout(stabiloReadHideTimer);
505
+ showReadPopup(ann, pill);
506
+ }
507
+ });
508
+ pill.addEventListener("mouseenter", () => {
509
+ const m = contentEl.querySelector(
510
+ `mark[data-annotation-id="${ann.id}"]`,
511
+ );
512
+ if (!m) {
513
+ clearTimeout(stabiloReadHideTimer);
514
+ showReadPopup(ann, pill);
515
+ return;
516
+ }
517
+ const docView = document.getElementById("content-area");
518
+ const markTop =
519
+ m.getBoundingClientRect().top +
520
+ docView.scrollTop -
521
+ docView.getBoundingClientRect().top;
522
+ docView.scrollTo({
523
+ top: markTop - docView.clientHeight / 2,
524
+ behavior: "smooth",
525
+ });
526
+ // Wait for scroll to fully stop before showing popup
527
+ let scrollEndTimer;
528
+ const onScroll = () => {
529
+ clearTimeout(scrollEndTimer);
530
+ scrollEndTimer = setTimeout(() => {
531
+ docView.removeEventListener("scroll", onScroll);
532
+ showReadPopup(ann, m);
533
+ }, 80);
534
+ };
535
+ docView.addEventListener("scroll", onScroll);
536
+ // Fallback if already at position (no scroll event fires)
537
+ scrollEndTimer = setTimeout(() => {
538
+ docView.removeEventListener("scroll", onScroll);
539
+ showReadPopup(ann, m);
540
+ }, 600);
541
+ });
542
+ pill.addEventListener("mouseleave", () => scheduleHideReadPopup());
543
+
544
+ elevator.appendChild(pill);
545
+ }
546
+ }
@@ -0,0 +1,90 @@
1
+ // ── Boot & global listeners ─────────────────────────────────────────────────
2
+ // Depends on globals from state.js (allDocs, stabiloAnnotations), dark-mode.js
3
+ // (applyDarkMode, loadDarkPref, setupDarkToggle), search.js (setupSearch),
4
+ // wordcloud.js (wcRestorePrefs), config.js (loadConfig), documents.js
5
+ // (loadDocuments, openDocument), annotations.js (showReadPopup,
6
+ // scheduleHideReadPopup), image-paste.js (openLightbox, closeLightbox,
7
+ // imgPasteConfirm, imgPasteCancel).
8
+
9
+ document.addEventListener("DOMContentLoaded", async () => {
10
+ applyDarkMode(loadDarkPref());
11
+ setupDarkToggle();
12
+ setupSearch();
13
+ wcRestorePrefs();
14
+ await loadConfig();
15
+ await loadDocuments();
16
+
17
+ // Deep-link via ?doc=id, otherwise open first General doc
18
+ const params = new URLSearchParams(location.search);
19
+ const docId = params.get("doc");
20
+ if (docId) {
21
+ openDocument(docId, true);
22
+ } else {
23
+ const first =
24
+ allDocs.find((d) => d.category === "General") ?? allDocs[0];
25
+ if (first) openDocument(first.id, true);
26
+ }
27
+
28
+ // Event delegation for annotation marks — survives innerHTML replacements on doc-content
29
+ const contentEl = document.getElementById("doc-content");
30
+ if (contentEl) {
31
+ contentEl.addEventListener("mouseover", (e) => {
32
+ const mark = e.target.closest("mark[data-annotation-id]");
33
+ if (!mark) return;
34
+ const ann = stabiloAnnotations.find(
35
+ (a) => a.id === mark.dataset.annotationId,
36
+ );
37
+ if (ann) showReadPopup(ann, mark);
38
+ });
39
+ contentEl.addEventListener("mouseout", (e) => {
40
+ if (!e.target.closest("mark[data-annotation-id]")) return;
41
+ if (
42
+ e.relatedTarget &&
43
+ e.relatedTarget.closest("mark[data-annotation-id]")
44
+ )
45
+ return;
46
+ scheduleHideReadPopup();
47
+ });
48
+ }
49
+ });
50
+
51
+ // ── Image lightbox (Shift+Click) / follow link (Click) ──────────────────────
52
+ // Capture phase so we intercept before link handlers on child <a> elements
53
+ document.getElementById("doc-content").addEventListener(
54
+ "click",
55
+ (e) => {
56
+ const img = e.target.closest("img");
57
+ if (!img) return;
58
+ if (e.shiftKey) {
59
+ e.preventDefault();
60
+ e.stopPropagation();
61
+ openLightbox(img.src, img.alt);
62
+ }
63
+ },
64
+ true,
65
+ );
66
+
67
+ // Escape closes lightbox
68
+ document.addEventListener("keydown", (e) => {
69
+ if (
70
+ e.key === "Escape" &&
71
+ !document.getElementById("img-lightbox").classList.contains("hidden")
72
+ ) {
73
+ closeLightbox();
74
+ }
75
+ });
76
+
77
+ // Image-paste name confirm / cancel
78
+ document
79
+ .getElementById("img-paste-name")
80
+ .addEventListener("keydown", (e) => {
81
+ if (e.key === "Enter") imgPasteConfirm();
82
+ if (e.key === "Escape") imgPasteCancel();
83
+ });
84
+
85
+ // Browser back/forward
86
+ window.addEventListener("popstate", (e) => {
87
+ const id =
88
+ e.state?.docId || new URLSearchParams(location.search).get("doc");
89
+ if (id) openDocument(id, true);
90
+ });
@@ -0,0 +1,19 @@
1
+ // ── Configuration loading ────────────────────────────────────────────────────
2
+
3
+ async function loadConfig() {
4
+ try {
5
+ const cfg = await fetch("/api/config").then((r) => r.json());
6
+ await window.initI18n(cfg.language || 'en');
7
+ window.applyI18n();
8
+ if (cfg.title) document.title = cfg.title;
9
+ document.getElementById("app-title").textContent =
10
+ cfg.title || "Living Documentation";
11
+ if (cfg.filenamePattern) {
12
+ document.getElementById("welcome-pattern").textContent =
13
+ cfg.filenamePattern + ".md";
14
+ }
15
+ } catch {
16
+ await window.initI18n('en');
17
+ /* non-fatal */
18
+ }
19
+ }
@@ -0,0 +1,20 @@
1
+ // ── Dark mode toggle ─────────────────────────────────────────────────────────
2
+
3
+ function loadDarkPref() {
4
+ const saved = localStorage.getItem("ld-dark");
5
+ if (saved !== null) return saved === "true";
6
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
7
+ }
8
+
9
+ function applyDarkMode(dark) {
10
+ document.documentElement.classList.toggle("dark", dark);
11
+ document.getElementById("dark-icon").textContent = dark ? "☀" : "☾";
12
+ }
13
+
14
+ function setupDarkToggle() {
15
+ document.getElementById("dark-toggle").addEventListener("click", () => {
16
+ const isDark = document.documentElement.classList.toggle("dark");
17
+ localStorage.setItem("ld-dark", isDark);
18
+ document.getElementById("dark-icon").textContent = isDark ? "☀" : "☾";
19
+ });
20
+ }