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,479 @@
1
+ // ── Documents: load, open, edit, save, delete, navigate ─────────────────────
2
+ // Depends on globals from state.js, utils.js, search.js, sidebar.js,
3
+ // annotations (loadAnnotations, applyAnnotationHighlights, renderElevator),
4
+ // and image-paste.js (handleEditorPaste).
5
+
6
+ // Cache of the last rendered doc HTML so search input changes can re-wire
7
+ // the content without a round-trip to the server.
8
+ let _lastDocHtml = null;
9
+ let _lastDocIdRendered = null;
10
+
11
+ function _wireDocContent(html) {
12
+ const contentEl = document.getElementById("doc-content");
13
+ if (!contentEl) return;
14
+ contentEl.innerHTML = html;
15
+
16
+ contentEl.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((h) => {
17
+ if (!h.id) {
18
+ h.id = h.textContent
19
+ .toLowerCase()
20
+ .replace(/[^\w\s-]/g, "")
21
+ .trim()
22
+ .replace(/\s+/g, "-");
23
+ }
24
+ });
25
+
26
+ contentEl.querySelectorAll("pre code").forEach((block) => {
27
+ hljs.highlightElement(block);
28
+ });
29
+
30
+ _decorateCodeBlocksWithCopy(contentEl);
31
+ _decorateCollapsibleCodeBlocks(contentEl);
32
+
33
+ if (typeof initLocalSearch === "function") initLocalSearch(contentEl);
34
+
35
+ contentEl.querySelectorAll("a[href]").forEach((a) => {
36
+ const href = a.getAttribute("href");
37
+ const m = href && href.match(/[?&]doc=([^&#]+)/);
38
+ if (!m) return;
39
+ const hashIdx = href.indexOf("#");
40
+ const anchorTarget = hashIdx !== -1 ? href.slice(hashIdx + 1) : null;
41
+ a.addEventListener("click", (e) => {
42
+ e.preventDefault();
43
+ openDocument(decodeURIComponent(m[1]), false, true, anchorTarget);
44
+ });
45
+ });
46
+
47
+ contentEl.querySelectorAll('a[href^="#"]').forEach((a) => {
48
+ const href = a.getAttribute("href");
49
+ if (!href || href.length < 2) return;
50
+ a.addEventListener("click", (e) => {
51
+ e.preventDefault();
52
+ scrollToAnchor(href.slice(1));
53
+ });
54
+ });
55
+
56
+ contentEl.querySelectorAll("table").forEach((t) => {
57
+ const wrapper = document.createElement("div");
58
+ wrapper.className = "overflow-x-auto";
59
+ t.parentNode.insertBefore(wrapper, t);
60
+ wrapper.appendChild(t);
61
+ });
62
+
63
+ const notice = document.getElementById("search-notice");
64
+ const isMetaQuery =
65
+ typeof searchQuery === "string" &&
66
+ searchQuery.toLowerCase().startsWith("metadata://");
67
+ if (searchQuery && !isMetaQuery) {
68
+ const matches = highlightMatches(contentEl, searchQuery);
69
+ buildSearchNotice(matches, searchQuery);
70
+ notice.classList.remove("hidden");
71
+ } else {
72
+ notice.classList.add("hidden");
73
+ }
74
+ }
75
+
76
+ function refreshSearchInCurrentDoc() {
77
+ if (
78
+ !currentDocId ||
79
+ _lastDocHtml === null ||
80
+ currentDocId !== _lastDocIdRendered
81
+ ) {
82
+ return;
83
+ }
84
+ const contentArea = document.getElementById("content-area");
85
+ const scrollTop = contentArea ? contentArea.scrollTop : 0;
86
+ _wireDocContent(_lastDocHtml);
87
+ if (typeof loadAnnotations === "function") loadAnnotations(currentDocId);
88
+ if (contentArea) contentArea.scrollTop = scrollTop;
89
+ }
90
+
91
+ window.refreshSearchInCurrentDoc = refreshSearchInCurrentDoc;
92
+
93
+ function _decorateCodeBlocksWithCopy(contentEl) {
94
+ const copyLabel =
95
+ (typeof window.t === "function" && window.t("doc.code_copy")) || "Copy";
96
+ const copiedLabel =
97
+ (typeof window.t === "function" && window.t("doc.code_copied")) ||
98
+ "Copied!";
99
+ contentEl.querySelectorAll("pre").forEach((pre) => {
100
+ const code = pre.querySelector("code");
101
+ if (!code) return;
102
+ if (pre.querySelector(".ld-code-copy")) return;
103
+ const btn = document.createElement("button");
104
+ btn.type = "button";
105
+ btn.className = "ld-code-copy";
106
+ btn.title = copyLabel;
107
+ btn.setAttribute("aria-label", copyLabel);
108
+ btn.innerHTML =
109
+ '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="5" width="8" height="9" rx="1.5"/><path d="M3 11V3a1 1 0 0 1 1-1h7"/></svg>';
110
+ btn.addEventListener("click", async (e) => {
111
+ e.preventDefault();
112
+ e.stopPropagation();
113
+ const text = code.innerText;
114
+ try {
115
+ await navigator.clipboard.writeText(text);
116
+ } catch {
117
+ const ta = document.createElement("textarea");
118
+ ta.value = text;
119
+ ta.style.position = "fixed";
120
+ ta.style.opacity = "0";
121
+ document.body.appendChild(ta);
122
+ ta.select();
123
+ try {
124
+ document.execCommand("copy");
125
+ } catch {
126
+ /* ignore */
127
+ }
128
+ document.body.removeChild(ta);
129
+ }
130
+ btn.classList.add("ld-copied");
131
+ btn.title = copiedLabel;
132
+ btn.innerHTML =
133
+ '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5 6.5 12 13 4.5"/></svg>';
134
+ setTimeout(() => {
135
+ btn.classList.remove("ld-copied");
136
+ btn.title = copyLabel;
137
+ btn.innerHTML =
138
+ '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="5" width="8" height="9" rx="1.5"/><path d="M3 11V3a1 1 0 0 1 1-1h7"/></svg>';
139
+ }, 1500);
140
+ });
141
+ pre.appendChild(btn);
142
+ });
143
+ }
144
+
145
+ function _decorateCollapsibleCodeBlocks(contentEl) {
146
+ if (typeof codeBlockMaxHeight !== "number" || codeBlockMaxHeight <= 0) return;
147
+ const more = (typeof window.t === "function" && window.t("doc.code_show_more")) || "▾ Show more";
148
+ const less = (typeof window.t === "function" && window.t("doc.code_show_less")) || "▴ Show less";
149
+ contentEl.querySelectorAll("pre").forEach((pre) => {
150
+ if (!pre.querySelector("code")) return;
151
+ if (pre.scrollHeight <= codeBlockMaxHeight + 8) return;
152
+ pre.classList.add("ld-collapsible");
153
+ const btn = document.createElement("button");
154
+ btn.type = "button";
155
+ btn.className = "ld-code-toggle";
156
+ btn.textContent = more;
157
+ btn.addEventListener("click", (e) => {
158
+ e.preventDefault();
159
+ const expanded = pre.classList.toggle("ld-expanded");
160
+ btn.textContent = expanded ? less : more;
161
+ });
162
+ pre.appendChild(btn);
163
+ });
164
+ }
165
+
166
+ async function loadDocuments() {
167
+ try {
168
+ [allDocs] = await Promise.all([
169
+ fetch("/api/documents").then((r) => r.json()),
170
+ ]);
171
+ // Also fetch all directories so empty folders appear in the sidebar
172
+ try {
173
+ const cfg = await fetch("/api/config").then((r) => r.json());
174
+ if (cfg.docsFolder) {
175
+ allFolderPaths = await fetch(
176
+ "/api/browse/alldirs?path=" +
177
+ encodeURIComponent(cfg.docsFolder),
178
+ ).then((r) => r.json());
179
+ }
180
+ } catch {
181
+ allFolderPaths = [];
182
+ }
183
+ await Promise.all([
184
+ refreshAnnotationCounts(),
185
+ refreshFileAttachmentCounts(),
186
+ ]);
187
+ renderSidebar(allDocs);
188
+ } catch {
189
+ document.getElementById("category-tree").innerHTML =
190
+ `<p class="px-4 py-4 text-sm text-red-500">${window.t('sidebar.failed_to_load')}</p>`;
191
+ }
192
+ }
193
+
194
+ async function refreshAnnotationCounts() {
195
+ try {
196
+ const raw = await fetch("/api/annotations").then((r) => r.json());
197
+ annotationCounts = {};
198
+ for (const [docId, n] of Object.entries(raw || {})) {
199
+ annotationCounts[docId] = n;
200
+ try {
201
+ annotationCounts[encodeURIComponent(docId)] = n;
202
+ } catch {}
203
+ }
204
+ } catch {
205
+ annotationCounts = {};
206
+ }
207
+ }
208
+
209
+ async function refreshFileAttachmentCounts() {
210
+ try {
211
+ const raw = await fetch("/api/documents/file-counts").then((r) => r.json());
212
+ fileAttachmentCounts = {};
213
+ for (const [docId, n] of Object.entries(raw || {})) {
214
+ fileAttachmentCounts[docId] = n;
215
+ }
216
+ } catch {
217
+ fileAttachmentCounts = {};
218
+ }
219
+ }
220
+
221
+ async function openDocument(id, skipHistory = false, fromLink = false, anchor = null) {
222
+ // Track navigation history for breadcrumb trail
223
+ // fromLink===true : forward navigation via in-doc link → push current to stack
224
+ // (unless target is already in the stack → rewind instead of loop)
225
+ // fromLink==="restore" : back navigation via history breadcrumb → stack already trimmed, don't touch
226
+ // fromLink===false : sidebar/direct navigation → reset stack
227
+ if (fromLink === true && currentDocId && currentDocId !== id) {
228
+ const existingIdx = navHistory.findIndex((e) => e.id === id);
229
+ if (existingIdx !== -1) {
230
+ navHistory = navHistory.slice(0, existingIdx);
231
+ } else {
232
+ const prev = allDocs && allDocs.find((d) => d.id === currentDocId);
233
+ navHistory.push({
234
+ id: currentDocId,
235
+ title: prev ? prev.title : currentDocId,
236
+ });
237
+ }
238
+ } else if (!fromLink) {
239
+ navHistory = [];
240
+ }
241
+
242
+ // Update back-link banner
243
+ const backEl = document.getElementById("doc-back");
244
+ if (navHistory.length > 0) {
245
+ backEl.innerHTML = navHistory
246
+ .map(
247
+ (entry, i) =>
248
+ `<button onclick="goBackToIndex(${i})"
249
+ class="no-print text-blue-600 dark:text-blue-400 hover:underline">&#8592; ${esc(entry.title)}</button>`,
250
+ )
251
+ .join(
252
+ '<span class="text-gray-300 dark:text-gray-600 mx-1">·</span>',
253
+ );
254
+ backEl.classList.remove("hidden");
255
+ } else {
256
+ backEl.classList.add("hidden");
257
+ backEl.innerHTML = "";
258
+ }
259
+
260
+ currentDocId = id;
261
+
262
+ // Expand sidebar path to reveal the document
263
+ const doc = allDocs && allDocs.find((d) => d.id === id);
264
+ if (doc) {
265
+ const folder = doc.folder || [];
266
+ // Expand every ancestor folder
267
+ for (let i = 0; i < folder.length; i++) {
268
+ expandedFolders.add(folder.slice(0, i + 1).join("|"));
269
+ }
270
+ // Expand the category at this folder level
271
+ expandedCategories.add([...folder, doc.category].join("|"));
272
+ refreshSidebar();
273
+ }
274
+
275
+ // Update active state in sidebar
276
+ document
277
+ .querySelectorAll(".doc-item")
278
+ .forEach((el) => el.classList.remove("active"));
279
+ const activeItem = document.getElementById("item-" + id);
280
+ if (activeItem) {
281
+ activeItem.classList.add("active");
282
+ activeItem.scrollIntoView({ block: "nearest", behavior: "smooth" });
283
+ }
284
+
285
+ // Update URL
286
+ if (!skipHistory) {
287
+ const url = new URL(location.href);
288
+ url.searchParams.set("doc", id);
289
+ url.hash = anchor ? `#${anchor}` : "";
290
+ history.pushState({ docId: id, anchor: anchor || null }, "", url);
291
+ }
292
+
293
+ document.getElementById("welcome").classList.add("hidden");
294
+ const docView = document.getElementById("doc-view");
295
+ docView.classList.remove("hidden");
296
+ document.getElementById("doc-content").innerHTML =
297
+ `<p class="animate-pulse text-gray-400">${window.t('common.loading')}</p>`;
298
+
299
+ try {
300
+ const doc = await fetch("/api/documents/" + id).then((r) => {
301
+ if (!r.ok) throw new Error(r.statusText);
302
+ return r.json();
303
+ });
304
+
305
+ currentDocContent = doc.content;
306
+ exitEditMode();
307
+
308
+ document.getElementById("doc-title").textContent = doc.title;
309
+ {
310
+ const crumbs = document.getElementById("doc-breadcrumbs");
311
+ const folderPills = (doc.folder || []).map(
312
+ (seg) =>
313
+ `<span title="${esc(seg)}" class="inline-block text-xs font-semibold px-2.5 py-0.5 rounded-full bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-300">${esc(folderLabel(seg))}</span>`,
314
+ );
315
+ const catPill = `<span class="inline-block text-xs font-semibold px-2.5 py-0.5 rounded-full bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300">${esc(doc.category)}</span>`;
316
+ crumbs.innerHTML = [...folderPills, catPill].join("");
317
+ }
318
+ document.getElementById("doc-date").textContent =
319
+ doc.formattedDate || "";
320
+
321
+ _lastDocHtml = doc.html;
322
+ _lastDocIdRendered = id;
323
+ _wireDocContent(doc.html);
324
+
325
+ if (typeof updateValidateButtonForCurrentDoc === "function") {
326
+ updateValidateButtonForCurrentDoc();
327
+ }
328
+
329
+ // Load annotations for this document
330
+ loadAnnotations(id);
331
+
332
+ // Load source-file metadata report (drives the accuracy gauge)
333
+ if (typeof loadMetadataReport === "function") {
334
+ loadMetadataReport(id);
335
+ }
336
+
337
+ document.title = doc.title;
338
+
339
+ // Scroll to anchor if present (explicit param wins over URL hash)
340
+ const targetAnchor =
341
+ anchor || (window.location.hash ? window.location.hash.slice(1) : "");
342
+ if (targetAnchor) {
343
+ scrollToAnchor(targetAnchor);
344
+ } else {
345
+ document.getElementById("content-area").scrollTop = 0;
346
+ }
347
+ } catch (err) {
348
+ document.getElementById("doc-content").innerHTML =
349
+ `<p class="text-red-500">${window.t('doc.failed_to_load')}${err.message}</p>`;
350
+ }
351
+ }
352
+
353
+ // ── Edit mode ────────────────────────────────────────────────────────────────
354
+ let _editScrollTop = 0;
355
+
356
+ function enterEditMode() {
357
+ _editScrollTop = document.getElementById("content-area").scrollTop;
358
+ const editor = document.getElementById("doc-editor");
359
+ editor.value = currentDocContent;
360
+ document.getElementById("doc-content").classList.add("hidden");
361
+ editor.classList.remove("hidden");
362
+ document.getElementById("view-actions").classList.add("hidden");
363
+ document.getElementById("edit-actions").classList.remove("hidden");
364
+ editor.focus();
365
+ editor.addEventListener("paste", handleEditorPaste);
366
+ }
367
+
368
+ function exitEditMode() {
369
+ const editor = document.getElementById("doc-editor");
370
+ editor.removeEventListener("paste", handleEditorPaste);
371
+ editor.classList.add("hidden");
372
+ document.getElementById("doc-content").classList.remove("hidden");
373
+ document.getElementById("edit-actions").classList.add("hidden");
374
+ document.getElementById("view-actions").classList.remove("hidden");
375
+ document.getElementById("edit-save-msg").textContent = "";
376
+ document.getElementById("content-area").scrollTop = _editScrollTop;
377
+ }
378
+
379
+ // ── Delete ───────────────────────────────────────────────────────────────────
380
+ function askDeleteDocument() {
381
+ if (!currentDocId) return;
382
+ const doc = allDocs.find((d) => d.id === currentDocId);
383
+ const titleEl = document.getElementById("doc-confirm-delete-title");
384
+ if (titleEl) titleEl.textContent = doc ? doc.title : "";
385
+ document.getElementById("doc-confirm-delete").classList.remove("hidden");
386
+ }
387
+
388
+ function cancelDeleteDocument(e) {
389
+ if (e && e.target && e.target.id && e.target.id !== "doc-confirm-delete") {
390
+ // clicked inside the card, not the backdrop
391
+ return;
392
+ }
393
+ document.getElementById("doc-confirm-delete").classList.add("hidden");
394
+ }
395
+
396
+ async function confirmDeleteDocument() {
397
+ if (!currentDocId) return;
398
+ const deletedId = currentDocId;
399
+ try {
400
+ const r = await fetch(
401
+ "/api/documents/" + encodeURIComponent(deletedId),
402
+ { method: "DELETE" },
403
+ );
404
+ if (!r.ok) throw new Error("delete failed");
405
+ } catch {
406
+ document.getElementById("doc-confirm-delete").classList.add("hidden");
407
+ return;
408
+ }
409
+ document.getElementById("doc-confirm-delete").classList.add("hidden");
410
+
411
+ // Drop from local state
412
+ allDocs = allDocs.filter((d) => d.id !== deletedId);
413
+ if (Array.isArray(searchResults)) {
414
+ searchResults = searchResults.filter((d) => d.id !== deletedId);
415
+ }
416
+ delete annotationCounts[deletedId];
417
+ try {
418
+ delete annotationCounts[decodeURIComponent(deletedId)];
419
+ } catch {}
420
+ delete fileAttachmentCounts[deletedId];
421
+ currentDocId = null;
422
+
423
+ // Return to welcome screen
424
+ document.getElementById("doc-view").classList.add("hidden");
425
+ document.getElementById("welcome").classList.remove("hidden");
426
+ history.pushState({}, "", window.location.pathname);
427
+
428
+ refreshSidebar();
429
+ }
430
+
431
+ // ── Save (in-place edit) ─────────────────────────────────────────────────────
432
+ async function saveDocument() {
433
+ if (!currentDocId) return;
434
+ const content = document.getElementById("doc-editor").value;
435
+ const msgEl = document.getElementById("edit-save-msg");
436
+ msgEl.textContent = window.t('doc.saving');
437
+ msgEl.className = "text-xs text-gray-400";
438
+
439
+ try {
440
+ const res = await fetch("/api/documents/" + currentDocId, {
441
+ method: "PUT",
442
+ headers: { "Content-Type": "application/json" },
443
+ body: JSON.stringify({ content }),
444
+ });
445
+ if (!res.ok) throw new Error(await res.text());
446
+
447
+ currentDocContent = content;
448
+
449
+ // Re-fetch rendered HTML and update view
450
+ const doc = await fetch("/api/documents/" + currentDocId).then((r) =>
451
+ r.json(),
452
+ );
453
+ _lastDocHtml = doc.html;
454
+ _lastDocIdRendered = currentDocId;
455
+ _wireDocContent(doc.html);
456
+
457
+ applyAnnotationHighlights();
458
+ renderElevator();
459
+
460
+ const fileLinkMatches = content.match(/\]\(\s*\.?\/files\/[^)\s]+/g);
461
+ const fileLinkCount = fileLinkMatches ? fileLinkMatches.length : 0;
462
+ if (fileLinkCount > 0) fileAttachmentCounts[currentDocId] = fileLinkCount;
463
+ else delete fileAttachmentCounts[currentDocId];
464
+ refreshSidebar();
465
+
466
+ exitEditMode();
467
+ } catch (err) {
468
+ msgEl.textContent = window.t('error.save') + err.message;
469
+ msgEl.className = "text-xs text-red-500 dark:text-red-400";
470
+ }
471
+ }
472
+
473
+ // ── Back breadcrumb navigation ──────────────────────────────────────────────
474
+ function goBackToIndex(i) {
475
+ const entry = navHistory[i];
476
+ if (!entry) return;
477
+ navHistory = navHistory.slice(0, i); // drop this entry and everything after
478
+ openDocument(entry.id, false, "restore");
479
+ }