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,318 @@
1
+ // ── Metadata (source-file dependencies) ─────────────────────────────────────
2
+ // Exposes: openMetadataModal(), closeMetadataModal(),
3
+ // metadataRefresh(), metadataAddPath(), metadataRemovePath(),
4
+ // loadMetadataReport(docId) → used by accuracy-gauge.js
5
+
6
+ let metadataReport = null;
7
+ let metadataBrowseCurrent = ""; // relative to sourceRoot
8
+ let metadataBrowseCache = null;
9
+
10
+ function metadataCurrentDocId() {
11
+ return typeof currentDocId !== "undefined" ? currentDocId : null;
12
+ }
13
+
14
+ async function loadMetadataReport(docId) {
15
+ if (!docId) {
16
+ metadataReport = null;
17
+ return null;
18
+ }
19
+ try {
20
+ const r = await fetch(
21
+ "/api/metadata/" + encodeURIComponent(docId),
22
+ );
23
+ if (!r.ok) throw new Error(r.statusText);
24
+ metadataReport = await r.json();
25
+ } catch {
26
+ metadataReport = null;
27
+ }
28
+ if (typeof renderAccuracyGauge === "function") {
29
+ renderAccuracyGauge(metadataReport);
30
+ }
31
+ return metadataReport;
32
+ }
33
+
34
+ function statusBadge(status) {
35
+ if (status === "unchanged") {
36
+ return `<span class="inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300">
37
+ <i class="fa-solid fa-check"></i>
38
+ <span data-i18n="metadata.status.unchanged">Unchanged</span>
39
+ </span>`;
40
+ }
41
+ if (status === "modified") {
42
+ return `<span class="inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300">
43
+ <i class="fa-solid fa-triangle-exclamation"></i>
44
+ <span data-i18n="metadata.status.modified">Modified</span>
45
+ </span>`;
46
+ }
47
+ return `<span class="inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300">
48
+ <i class="fa-solid fa-circle-xmark"></i>
49
+ <span data-i18n="metadata.status.missing">Missing</span>
50
+ </span>`;
51
+ }
52
+
53
+ function renderMetadataList() {
54
+ const listEl = document.getElementById("metadata-list");
55
+ const emptyEl = document.getElementById("metadata-empty");
56
+ if (!listEl || !emptyEl) return;
57
+
58
+ const items = (metadataReport && metadataReport.items) || [];
59
+ if (items.length === 0) {
60
+ listEl.innerHTML = "";
61
+ emptyEl.classList.remove("hidden");
62
+ return;
63
+ }
64
+ emptyEl.classList.add("hidden");
65
+
66
+ listEl.innerHTML = items
67
+ .map((it) => {
68
+ const safePath = esc(it.path);
69
+ return `<div class="flex items-center gap-2 px-3 py-2 border-b border-gray-100 dark:border-gray-800">
70
+ <div class="flex-1 min-w-0">
71
+ <div class="text-sm font-mono truncate text-gray-800 dark:text-gray-200" title="${safePath}">${safePath}</div>
72
+ </div>
73
+ ${statusBadge(it.status)}
74
+ <button
75
+ onclick="metadataRemovePath('${safePath.replace(/'/g, "\\'")}')"
76
+ data-i18n-title="metadata.remove"
77
+ title="Remove"
78
+ class="metadata-row-remove text-xs px-2 py-1 rounded-lg border border-red-200 dark:border-red-700 text-red-500 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/40 transition-colors"
79
+ >
80
+ <i class="fa-solid fa-trash"></i>
81
+ </button>
82
+ </div>`;
83
+ })
84
+ .join("");
85
+
86
+ if (typeof window.applyI18n === "function") window.applyI18n();
87
+ applyMetadataReadOnlyMode();
88
+ }
89
+
90
+ // Hide the three metadata-mutation controls (and the row-level trash icons)
91
+ // when the current document's frontmatter status is `SuperSeeded`. The server
92
+ // also rejects these mutations independently — this is purely UX. Re-applied
93
+ // after every list re-render so dynamically generated rows stay consistent.
94
+ function applyMetadataReadOnlyMode() {
95
+ const readOnly =
96
+ typeof window.getDocStatus === "function" &&
97
+ window.getDocStatus(
98
+ typeof currentDocContent !== "undefined" ? currentDocContent : "",
99
+ ) === "SuperSeeded";
100
+
101
+ const refreshBtn = document.getElementById("metadata-refresh-btn");
102
+ const addBtn = document.getElementById("metadata-add-btn");
103
+ const banner = document.getElementById("metadata-readonly-banner");
104
+ if (refreshBtn) refreshBtn.classList.toggle("hidden", readOnly);
105
+ if (addBtn) addBtn.classList.toggle("hidden", readOnly);
106
+ if (banner) banner.classList.toggle("hidden", !readOnly);
107
+ document
108
+ .querySelectorAll("#metadata-list .metadata-row-remove")
109
+ .forEach((el) => el.classList.toggle("hidden", readOnly));
110
+ }
111
+
112
+ window.applyMetadataReadOnlyMode = applyMetadataReadOnlyMode;
113
+
114
+ function renderMetadataSummary() {
115
+ const el = document.getElementById("metadata-summary");
116
+ if (!el) return;
117
+ if (!metadataReport || metadataReport.total === 0) {
118
+ el.textContent = "";
119
+ return;
120
+ }
121
+ const pct = Math.round(metadataReport.accuracy * 100);
122
+ const { total, unchanged, modified, missing } = metadataReport;
123
+ el.innerHTML = `<span class="font-semibold">${pct}%</span> · ${unchanged}/${total} ${window.t("metadata.status.unchanged")} · ${modified} ${window.t("metadata.status.modified")} · ${missing} ${window.t("metadata.status.missing")}`;
124
+ }
125
+
126
+ async function openMetadataModal() {
127
+ const docId = metadataCurrentDocId();
128
+ if (!docId) return;
129
+ await loadMetadataReport(docId);
130
+ renderMetadataList();
131
+ renderMetadataSummary();
132
+ document.getElementById("metadata-modal").classList.remove("hidden");
133
+ document.getElementById("metadata-error").classList.add("hidden");
134
+ // Reset browser
135
+ metadataBrowseCurrent = "";
136
+ metadataBrowseCache = null;
137
+ document.getElementById("metadata-browser").classList.add("hidden");
138
+ applyMetadataReadOnlyMode();
139
+ }
140
+
141
+ function closeMetadataModal() {
142
+ document.getElementById("metadata-modal").classList.add("hidden");
143
+ }
144
+
145
+ async function metadataRefresh() {
146
+ const docId = metadataCurrentDocId();
147
+ if (!docId) return;
148
+ const btn = document.getElementById("metadata-refresh-btn");
149
+ if (btn) btn.disabled = true;
150
+ try {
151
+ const r = await fetch(
152
+ "/api/metadata/" + encodeURIComponent(docId) + "/refresh",
153
+ { method: "POST" },
154
+ );
155
+ if (!r.ok) throw new Error(r.statusText);
156
+ metadataReport = await r.json();
157
+ renderMetadataList();
158
+ renderMetadataSummary();
159
+ if (typeof renderAccuracyGauge === "function") {
160
+ renderAccuracyGauge(metadataReport);
161
+ }
162
+ } catch (err) {
163
+ showMetadataError(err.message);
164
+ } finally {
165
+ if (btn) btn.disabled = false;
166
+ }
167
+ }
168
+
169
+ async function metadataRemovePath(path) {
170
+ const docId = metadataCurrentDocId();
171
+ if (!docId) return;
172
+ try {
173
+ const r = await fetch(
174
+ "/api/metadata/" + encodeURIComponent(docId),
175
+ {
176
+ method: "DELETE",
177
+ headers: { "Content-Type": "application/json" },
178
+ body: JSON.stringify({ path }),
179
+ },
180
+ );
181
+ if (!r.ok) throw new Error(r.statusText);
182
+ metadataReport = await r.json();
183
+ renderMetadataList();
184
+ renderMetadataSummary();
185
+ if (typeof renderAccuracyGauge === "function") {
186
+ renderAccuracyGauge(metadataReport);
187
+ }
188
+ refreshBrowserIfOpen();
189
+ } catch (err) {
190
+ showMetadataError(err.message);
191
+ }
192
+ }
193
+
194
+ async function metadataAddPath(relPath) {
195
+ const docId = metadataCurrentDocId();
196
+ if (!docId) return;
197
+ try {
198
+ const r = await fetch(
199
+ "/api/metadata/" + encodeURIComponent(docId),
200
+ {
201
+ method: "POST",
202
+ headers: { "Content-Type": "application/json" },
203
+ body: JSON.stringify({ path: relPath }),
204
+ },
205
+ );
206
+ if (!r.ok) {
207
+ const body = await r.json().catch(() => ({}));
208
+ throw new Error(body.error || r.statusText);
209
+ }
210
+ metadataReport = await r.json();
211
+ renderMetadataList();
212
+ renderMetadataSummary();
213
+ if (typeof renderAccuracyGauge === "function") {
214
+ renderAccuracyGauge(metadataReport);
215
+ }
216
+ refreshBrowserIfOpen();
217
+ } catch (err) {
218
+ showMetadataError(err.message);
219
+ }
220
+ }
221
+
222
+ function refreshBrowserIfOpen() {
223
+ const b = document.getElementById("metadata-browser");
224
+ if (b && !b.classList.contains("hidden")) {
225
+ metadataBrowseLoad(metadataBrowseCurrent);
226
+ }
227
+ }
228
+
229
+ function showMetadataError(msg) {
230
+ const el = document.getElementById("metadata-error");
231
+ if (!el) return;
232
+ el.textContent = window.t("common.error_prefix") + msg;
233
+ el.classList.remove("hidden");
234
+ setTimeout(() => el.classList.add("hidden"), 5000);
235
+ }
236
+
237
+ // ── Source browser ─────────────────────────────────────────────────────────
238
+
239
+ function metadataToggleBrowser() {
240
+ const b = document.getElementById("metadata-browser");
241
+ if (b.classList.contains("hidden")) {
242
+ b.classList.remove("hidden");
243
+ metadataBrowseLoad("");
244
+ } else {
245
+ b.classList.add("hidden");
246
+ }
247
+ }
248
+
249
+ async function metadataBrowseLoad(relPath) {
250
+ const listEl = document.getElementById("metadata-browse-list");
251
+ const pathEl = document.getElementById("metadata-browse-path");
252
+ const upBtn = document.getElementById("metadata-browse-up");
253
+ if (!listEl) return;
254
+ listEl.innerHTML = `<div class="px-3 py-2 text-xs text-gray-400">${esc(window.t("common.loading"))}</div>`;
255
+ try {
256
+ const r = await fetch(
257
+ "/api/browse-source?path=" + encodeURIComponent(relPath || ""),
258
+ );
259
+ if (!r.ok) {
260
+ const body = await r.json().catch(() => ({}));
261
+ throw new Error(body.error || r.statusText);
262
+ }
263
+ const data = await r.json();
264
+ metadataBrowseCache = data;
265
+ metadataBrowseCurrent = data.current || "";
266
+ pathEl.textContent = data.current ? "/" + data.current : "/ (sourceRoot)";
267
+ upBtn.disabled = data.parent === null;
268
+ upBtn.classList.toggle("opacity-30", data.parent === null);
269
+ upBtn.classList.toggle("pointer-events-none", data.parent === null);
270
+
271
+ const dirRows = data.dirs.map(
272
+ (d) => `<button
273
+ onclick="metadataBrowseLoad('${d.path.replace(/'/g, "\\'")}')"
274
+ class="w-full text-left px-3 py-1.5 text-sm hover:bg-gray-50 dark:hover:bg-gray-800 flex items-center gap-2"
275
+ >
276
+ <i class="fa-solid fa-folder text-yellow-500"></i>
277
+ <span class="truncate">${esc(d.name)}</span>
278
+ </button>`,
279
+ );
280
+ const attached = new Set(
281
+ ((metadataReport && metadataReport.items) || []).map((it) => it.path),
282
+ );
283
+ const fileRows = data.files
284
+ .filter((f) => !attached.has(f.path))
285
+ .map(
286
+ (f) => `<button
287
+ onclick="metadataAddPath('${f.path.replace(/'/g, "\\'")}')"
288
+ class="w-full text-left px-3 py-1.5 text-sm hover:bg-blue-50 dark:hover:bg-blue-900/30 flex items-center gap-2"
289
+ >
290
+ <i class="fa-solid fa-file text-gray-400"></i>
291
+ <span class="truncate">${esc(f.name)}</span>
292
+ <i class="fa-solid fa-plus ml-auto text-blue-500 text-xs"></i>
293
+ </button>`,
294
+ );
295
+ const rows = [...dirRows, ...fileRows];
296
+ listEl.innerHTML = rows.length
297
+ ? rows.join("")
298
+ : `<div class="px-3 py-2 text-xs text-gray-400" data-i18n="common.empty_dir">${esc(window.t("common.empty_dir"))}</div>`;
299
+ } catch (err) {
300
+ listEl.innerHTML = `<div class="px-3 py-2 text-xs text-red-500">${esc(err.message)}</div>`;
301
+ }
302
+ }
303
+
304
+ function metadataBrowseUp() {
305
+ if (!metadataBrowseCache || metadataBrowseCache.parent === null) return;
306
+ metadataBrowseLoad(metadataBrowseCache.parent);
307
+ }
308
+
309
+ // Expose
310
+ window.openMetadataModal = openMetadataModal;
311
+ window.closeMetadataModal = closeMetadataModal;
312
+ window.metadataRefresh = metadataRefresh;
313
+ window.metadataRemovePath = metadataRemovePath;
314
+ window.metadataAddPath = metadataAddPath;
315
+ window.metadataToggleBrowser = metadataToggleBrowser;
316
+ window.metadataBrowseLoad = metadataBrowseLoad;
317
+ window.metadataBrowseUp = metadataBrowseUp;
318
+ window.loadMetadataReport = loadMetadataReport;
@@ -0,0 +1,92 @@
1
+ // ── Misc viewer helpers ─────────────────────────────────────────────────────
2
+
3
+ const DOC_ID_COPY_FEEDBACK_MS = 1800;
4
+
5
+ async function writeClipboardText(text) {
6
+ try {
7
+ await navigator.clipboard.writeText(text);
8
+ return true;
9
+ } catch {
10
+ const ta = document.createElement("textarea");
11
+ ta.value = text;
12
+ ta.style.position = "fixed";
13
+ ta.style.opacity = "0";
14
+ document.body.appendChild(ta);
15
+ ta.select();
16
+ let copied = false;
17
+ try {
18
+ copied = document.execCommand("copy");
19
+ } catch {
20
+ copied = false;
21
+ }
22
+ document.body.removeChild(ta);
23
+ return copied;
24
+ }
25
+ }
26
+
27
+ function applyFullWidthState(isWide) {
28
+ const article = document.getElementById("doc-view");
29
+ const btn = document.getElementById("full-width-btn");
30
+ if (!article || !btn) return;
31
+ article.classList.toggle("max-w-none", isWide);
32
+ article.classList.toggle("max-w-4xl", !isWide);
33
+ article.classList.toggle("mx-auto", !isWide);
34
+ btn.textContent = isWide ? window.t('doc.full_width_narrow_btn') : window.t('doc.full_width_btn');
35
+ }
36
+
37
+ function toggleFullWidth() {
38
+ const article = document.getElementById("doc-view");
39
+ const isWide = !article.classList.contains("max-w-none");
40
+ applyFullWidthState(isWide);
41
+ try {
42
+ localStorage.setItem("ld-full-width", isWide ? "1" : "0");
43
+ } catch {
44
+ /* ignore */
45
+ }
46
+ }
47
+
48
+ function initFullWidthState() {
49
+ let isWide = false;
50
+ try {
51
+ isWide = localStorage.getItem("ld-full-width") === "1";
52
+ } catch {
53
+ /* ignore */
54
+ }
55
+ applyFullWidthState(isWide);
56
+ }
57
+
58
+ async function copyLink() {
59
+ const copied = await writeClipboardText(location.href);
60
+ if (!copied) return;
61
+ const btn = document.getElementById("copy-link-btn");
62
+ const orig = btn.innerHTML;
63
+ btn.textContent = window.t('doc.copied');
64
+ setTimeout(() => {
65
+ btn.innerHTML = orig;
66
+ }, DOC_ID_COPY_FEEDBACK_MS);
67
+ }
68
+
69
+ async function copyCurrentDocMcpId() {
70
+ if (!currentDocId) return;
71
+ const btn = document.getElementById("copy-doc-id-btn");
72
+ if (!btn) return;
73
+ const copyLabel = window.t("doc.copy_mcp_id");
74
+ const copiedLabel = window.t("doc.copy_mcp_id_copied");
75
+ const originalHtml = btn.innerHTML;
76
+ const docId = decodeURIComponent(currentDocId);
77
+ const copied = await writeClipboardText(docId);
78
+ if (!copied) return;
79
+
80
+ btn.title = copiedLabel;
81
+ btn.classList.add("text-green-600", "dark:text-green-400");
82
+ btn.innerHTML = '<i class="fa-solid fa-check" aria-hidden="true"></i>';
83
+ setTimeout(() => {
84
+ btn.title = copyLabel;
85
+ btn.classList.remove("text-green-600", "dark:text-green-400");
86
+ btn.innerHTML = originalHtml;
87
+ }, DOC_ID_COPY_FEEDBACK_MS);
88
+ }
89
+
90
+ function exportPDF() {
91
+ window.print();
92
+ }
@@ -0,0 +1,285 @@
1
+ // ── New Document modal ──────────────────────────────────────────────────────
2
+ // Depends on globals from state.js (currentDocId, allDocs), documents.js
3
+ // (loadDocuments, openDocument) and utils.js (esc).
4
+
5
+ let _newDocBrowseCurrent = null;
6
+ let _newDocBrowseParent = null;
7
+ let _newDocSelectedFolder = "";
8
+ let _newDocDocsFolder = "";
9
+ let _newDocPattern = "YYYY_MM_DD_HH_mm_[Category]_title";
10
+
11
+ function newDocNormalizeCategory(raw) {
12
+ return (raw || "")
13
+ .normalize("NFD")
14
+ .replace(/[\u0300-\u036f]/g, "")
15
+ .toUpperCase()
16
+ .replace(/[^A-Z0-9_-]/g, "");
17
+ }
18
+
19
+ function newDocSanitizeCategoryInput() {
20
+ const input = document.getElementById("new-doc-category");
21
+ const normalized = newDocNormalizeCategory(input.value);
22
+ if (input.value !== normalized) input.value = normalized;
23
+ newDocUpdatePreview();
24
+ }
25
+
26
+ function newDocPopulateCategoryOptions() {
27
+ const list = document.getElementById("new-doc-category-options");
28
+ if (!list) return;
29
+ const seen = new Set();
30
+ (allDocs || []).forEach((d) => {
31
+ const cat = newDocNormalizeCategory(d.category || "");
32
+ if (cat) seen.add(cat);
33
+ });
34
+ const sorted = Array.from(seen).sort((a, b) => a.localeCompare(b));
35
+ list.innerHTML = sorted
36
+ .map((cat) => `<option value="${esc(cat)}"></option>`)
37
+ .join("");
38
+ }
39
+
40
+ async function openNewDocModal() {
41
+ try {
42
+ const cfg = await fetch("/api/config").then((r) => r.json());
43
+ _newDocDocsFolder = cfg.docsFolder || "";
44
+ _newDocPattern =
45
+ cfg.filenamePattern || "YYYY_MM_DD_HH_mm_[Category]_title";
46
+ } catch {
47
+ _newDocDocsFolder = "";
48
+ _newDocPattern = "YYYY_MM_DD_HH_mm_[Category]_title";
49
+ }
50
+ // Pre-fill from currently open document if any
51
+ const currentDoc =
52
+ currentDocId && allDocs.find((d) => d.id === currentDocId);
53
+ const prefillCategory =
54
+ newDocNormalizeCategory(
55
+ (currentDoc && currentDoc.category) || "General",
56
+ ) || "GENERAL";
57
+ // Derive folder from currentDocId (encoded relative path, e.g. "1_tutorial%2Fsome_file")
58
+ // For extra files (absolute paths), skip.
59
+ let prefillFolder = "";
60
+ if (currentDocId) {
61
+ const decodedId = decodeURIComponent(currentDocId);
62
+ if (!decodedId.startsWith("/")) {
63
+ const segments = decodedId.split("/");
64
+ if (segments.length > 1) {
65
+ prefillFolder = segments.slice(0, -1).join("/");
66
+ }
67
+ }
68
+ }
69
+ const prefillFolderAbs = prefillFolder
70
+ ? _newDocDocsFolder + "/" + prefillFolder
71
+ : "";
72
+
73
+ _newDocSelectedFolder = prefillFolder;
74
+ _newDocBrowseCurrent = prefillFolderAbs || null;
75
+ _newDocBrowseParent = null;
76
+ newDocPopulateCategoryOptions();
77
+ document.getElementById("new-doc-title").value = "";
78
+ document.getElementById("new-doc-category").value = prefillCategory;
79
+ document.getElementById("new-doc-folder-display").textContent =
80
+ prefillFolder ? "/" + prefillFolder : "/ (root)";
81
+ document.getElementById("new-doc-browser").classList.add("hidden");
82
+ document.getElementById("new-doc-new-folder-name").value = "";
83
+ document.getElementById("new-doc-error").classList.add("hidden");
84
+ const createBtn = document.getElementById("new-doc-create-btn");
85
+ createBtn.disabled = false;
86
+ createBtn.textContent = window.t('common.create');
87
+ newDocUpdatePreview();
88
+ document.getElementById("new-doc-modal").classList.remove("hidden");
89
+ setTimeout(() => document.getElementById("new-doc-title").focus(), 50);
90
+ }
91
+
92
+ function closeNewDocModal() {
93
+ document.getElementById("new-doc-modal").classList.add("hidden");
94
+ }
95
+
96
+ function newDocToggleBrowser() {
97
+ const browser = document.getElementById("new-doc-browser");
98
+ const isHidden = browser.classList.toggle("hidden");
99
+ if (!isHidden)
100
+ newDocLoadBrowse(_newDocBrowseCurrent || _newDocDocsFolder);
101
+ }
102
+
103
+ async function newDocLoadBrowse(dirPath) {
104
+ const list = document.getElementById("new-doc-browse-list");
105
+ list.innerHTML =
106
+ `<p class="px-3 py-4 text-xs text-gray-400 text-center">${window.t('common.loading')}</p>`;
107
+ try {
108
+ const data = await fetch(
109
+ "/api/browse?path=" + encodeURIComponent(dirPath),
110
+ ).then((r) => r.json());
111
+ _newDocBrowseCurrent = data.current;
112
+ _newDocBrowseParent = data.parent;
113
+ _newDocSelectedFolder = _newDocAbsToRel(data.current);
114
+
115
+ document.getElementById("new-doc-browse-path").textContent =
116
+ data.current;
117
+ const atRoot = data.current === _newDocDocsFolder;
118
+ document.getElementById("new-doc-browse-up").disabled = atRoot;
119
+ document.getElementById("new-doc-folder-display").textContent =
120
+ _newDocSelectedFolder ? "/" + _newDocSelectedFolder : "/ (root)";
121
+ newDocUpdatePreview();
122
+
123
+ list.innerHTML = data.dirs.length
124
+ ? data.dirs
125
+ .map(
126
+ (dir) => `
127
+ <button data-path="${esc(dir.path)}" onclick="newDocLoadBrowse(this.dataset.path)"
128
+ class="w-full flex items-center gap-2 px-3 py-2 text-sm text-left hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
129
+ <span class="text-gray-400 shrink-0">&#128193;</span>
130
+ <span class="text-gray-700 dark:text-gray-300 truncate">${esc(dir.name)}</span>
131
+ </button>`,
132
+ )
133
+ .join("")
134
+ : `<p class="px-3 py-3 text-xs text-gray-400 text-center">${window.t('modal.new_doc.no_subfolders')}</p>`;
135
+ } catch {
136
+ list.innerHTML =
137
+ `<p class="px-3 py-4 text-xs text-red-400 text-center">${window.t('common.cannot_read_dir')}</p>`;
138
+ }
139
+ }
140
+
141
+ function newDocBrowseUp() {
142
+ if (_newDocBrowseCurrent !== _newDocDocsFolder && _newDocBrowseParent) {
143
+ newDocLoadBrowse(_newDocBrowseParent);
144
+ }
145
+ }
146
+
147
+ function _newDocAbsToRel(absPath) {
148
+ const base = _newDocDocsFolder;
149
+ if (absPath === base) return "";
150
+ if (absPath.startsWith(base + "/"))
151
+ return absPath.slice(base.length + 1);
152
+ return absPath;
153
+ }
154
+
155
+ function newDocCreateFolder() {
156
+ const name = document
157
+ .getElementById("new-doc-new-folder-name")
158
+ .value.trim();
159
+ if (!name) return;
160
+ const parent = _newDocBrowseCurrent || _newDocDocsFolder;
161
+ const atDocsRoot = parent === _newDocDocsFolder;
162
+ const errEl = document.getElementById("new-doc-error");
163
+ if (atDocsRoot && (name === "files" || name === "images")) {
164
+ if (errEl) {
165
+ errEl.textContent = window.t("modal.new_folder.error_reserved");
166
+ errEl.classList.remove("hidden");
167
+ }
168
+ return;
169
+ }
170
+ if (errEl) errEl.classList.add("hidden");
171
+ const newRelPath =
172
+ (_newDocAbsToRel(parent) ? _newDocAbsToRel(parent) + "/" : "") + name;
173
+ _newDocSelectedFolder = newRelPath;
174
+ document.getElementById("new-doc-folder-display").textContent =
175
+ "/" + newRelPath;
176
+ document.getElementById("new-doc-new-folder-name").value = "";
177
+ newDocUpdatePreview();
178
+ }
179
+
180
+ function newDocUpdatePreview() {
181
+ const title = document.getElementById("new-doc-title").value.trim();
182
+ const category =
183
+ newDocNormalizeCategory(
184
+ document.getElementById("new-doc-category").value,
185
+ ) || "GENERAL";
186
+ const previewEl = document.getElementById("new-doc-filename-preview");
187
+
188
+ if (!title) {
189
+ previewEl.textContent = window.t('modal.new_doc.title_placeholder');
190
+ return;
191
+ }
192
+
193
+ const now = new Date();
194
+ const year = now.getFullYear();
195
+ const month = String(now.getMonth() + 1).padStart(2, "0");
196
+ const day = String(now.getDate()).padStart(2, "0");
197
+ const hours = String(now.getHours()).padStart(2, "0");
198
+ const minutes = String(now.getMinutes()).padStart(2, "0");
199
+ const titleSlug =
200
+ title
201
+ .toLowerCase()
202
+ .replace(/\s+/g, "_")
203
+ .replace(/[^a-z0-9_]/g, "")
204
+ .replace(/_+/g, "_")
205
+ .replace(/^_|_$/g, "") || "document";
206
+
207
+ const filename =
208
+ _newDocPattern
209
+ .replace("YYYY", year)
210
+ .replace("MM", month)
211
+ .replace("DD", day)
212
+ .replace("HH", hours)
213
+ .replace("mm", minutes)
214
+ .replace(/\[Category\]/i, `[${category}]`)
215
+ .replace(
216
+ /(?<![a-z0-9])(?:title_words|title)(?![a-z0-9])/i,
217
+ titleSlug,
218
+ ) + ".md";
219
+
220
+ previewEl.textContent = _newDocSelectedFolder
221
+ ? _newDocSelectedFolder + "/" + filename
222
+ : filename;
223
+ }
224
+
225
+ async function createNewDocument() {
226
+ const title = document.getElementById("new-doc-title").value.trim();
227
+ const category =
228
+ newDocNormalizeCategory(
229
+ document.getElementById("new-doc-category").value,
230
+ ) || "GENERAL";
231
+ const errorEl = document.getElementById("new-doc-error");
232
+ const btn = document.getElementById("new-doc-create-btn");
233
+
234
+ if (!title) {
235
+ errorEl.textContent = window.t('modal.new_doc.error_empty_title');
236
+ errorEl.classList.remove("hidden");
237
+ return;
238
+ }
239
+
240
+ errorEl.classList.add("hidden");
241
+ btn.disabled = true;
242
+ btn.textContent = window.t('modal.new_folder.creating_btn');
243
+
244
+ try {
245
+ const res = await fetch("/api/documents", {
246
+ method: "POST",
247
+ headers: { "Content-Type": "application/json" },
248
+ body: JSON.stringify({
249
+ title,
250
+ category,
251
+ folder: _newDocSelectedFolder,
252
+ }),
253
+ });
254
+
255
+ if (!res.ok) {
256
+ const data = await res.json();
257
+ throw new Error(data.error || "Creation failed");
258
+ }
259
+
260
+ const doc = await res.json();
261
+ closeNewDocModal();
262
+ await loadDocuments();
263
+ openDocument(doc.id);
264
+ } catch (err) {
265
+ errorEl.textContent = window.t('common.error_prefix') + err.message;
266
+ errorEl.classList.remove("hidden");
267
+ btn.disabled = false;
268
+ btn.textContent = window.t('common.create');
269
+ }
270
+ }
271
+
272
+ // Allow Enter key in title/category to submit
273
+ document.addEventListener("DOMContentLoaded", () => {
274
+ ["new-doc-title", "new-doc-category"].forEach((id) => {
275
+ document.getElementById(id)?.addEventListener("keydown", (e) => {
276
+ if (e.key === "Enter") createNewDocument();
277
+ if (e.key === "Escape") closeNewDocModal();
278
+ });
279
+ });
280
+ document
281
+ .getElementById("new-doc-new-folder-name")
282
+ ?.addEventListener("keydown", (e) => {
283
+ if (e.key === "Enter") newDocCreateFolder();
284
+ });
285
+ });