living-ai-documentation 1.0.0

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