living-documentation 7.5.0 → 7.7.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 (53) hide show
  1. package/dist/src/frontend/accuracy-gauge.js +47 -0
  2. package/dist/src/frontend/annotations.js +66 -27
  3. package/dist/src/frontend/boot.js +3 -0
  4. package/dist/src/frontend/documents.js +28 -1
  5. package/dist/src/frontend/i18n/en.json +18 -0
  6. package/dist/src/frontend/i18n/fr.json +18 -0
  7. package/dist/src/frontend/index.html +157 -0
  8. package/dist/src/frontend/metadata.js +292 -0
  9. package/dist/src/frontend/misc.js +24 -2
  10. package/dist/src/frontend/new-doc-modal.js +10 -0
  11. package/dist/src/frontend/new-folder-modal.js +6 -0
  12. package/dist/src/frontend/sidebar-helpers.js +38 -0
  13. package/dist/src/frontend/sidebar.js +38 -5
  14. package/dist/src/frontend/state.js +8 -0
  15. package/dist/src/lib/hash.d.ts +2 -0
  16. package/dist/src/lib/hash.d.ts.map +1 -0
  17. package/dist/src/lib/hash.js +18 -0
  18. package/dist/src/lib/hash.js.map +1 -0
  19. package/dist/src/lib/metadata.d.ts +30 -0
  20. package/dist/src/lib/metadata.d.ts.map +1 -0
  21. package/dist/src/lib/metadata.js +109 -0
  22. package/dist/src/lib/metadata.js.map +1 -0
  23. package/dist/src/mcp/server.d.ts.map +1 -1
  24. package/dist/src/mcp/server.js +93 -0
  25. package/dist/src/mcp/server.js.map +1 -1
  26. package/dist/src/mcp/tools/metadata.d.ts +34 -0
  27. package/dist/src/mcp/tools/metadata.d.ts.map +1 -0
  28. package/dist/src/mcp/tools/metadata.js +76 -0
  29. package/dist/src/mcp/tools/metadata.js.map +1 -0
  30. package/dist/src/routes/browse-source.d.ts +3 -0
  31. package/dist/src/routes/browse-source.d.ts.map +1 -0
  32. package/dist/src/routes/browse-source.js +79 -0
  33. package/dist/src/routes/browse-source.js.map +1 -0
  34. package/dist/src/routes/browse.d.ts +1 -1
  35. package/dist/src/routes/browse.d.ts.map +1 -1
  36. package/dist/src/routes/browse.js +19 -3
  37. package/dist/src/routes/browse.js.map +1 -1
  38. package/dist/src/routes/documents.d.ts.map +1 -1
  39. package/dist/src/routes/documents.js +32 -0
  40. package/dist/src/routes/documents.js.map +1 -1
  41. package/dist/src/routes/metadata.d.ts +3 -0
  42. package/dist/src/routes/metadata.d.ts.map +1 -0
  43. package/dist/src/routes/metadata.js +107 -0
  44. package/dist/src/routes/metadata.js.map +1 -0
  45. package/dist/src/server.d.ts.map +1 -1
  46. package/dist/src/server.js +5 -1
  47. package/dist/src/server.js.map +1 -1
  48. package/dist/starting-doc/.annotations.json +1 -3
  49. package/dist/starting-doc/.metadata.json +1 -0
  50. package/package.json +1 -1
  51. package/dist/starting-doc/2026_04_21_19_47_[General]_tata.md +0 -6
  52. package/dist/starting-doc/2026_04_21_19_47_[General]_tutu.md +0 -11
  53. package/dist/starting-doc/2026_04_21_19_52_[General]_titi.md +0 -5
@@ -0,0 +1,292 @@
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="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
+ }
88
+
89
+ function renderMetadataSummary() {
90
+ const el = document.getElementById("metadata-summary");
91
+ if (!el) return;
92
+ if (!metadataReport || metadataReport.total === 0) {
93
+ el.textContent = "";
94
+ return;
95
+ }
96
+ const pct = Math.round(metadataReport.accuracy * 100);
97
+ const { total, unchanged, modified, missing } = metadataReport;
98
+ 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")}`;
99
+ }
100
+
101
+ async function openMetadataModal() {
102
+ const docId = metadataCurrentDocId();
103
+ if (!docId) return;
104
+ await loadMetadataReport(docId);
105
+ renderMetadataList();
106
+ renderMetadataSummary();
107
+ document.getElementById("metadata-modal").classList.remove("hidden");
108
+ document.getElementById("metadata-error").classList.add("hidden");
109
+ // Reset browser
110
+ metadataBrowseCurrent = "";
111
+ metadataBrowseCache = null;
112
+ document.getElementById("metadata-browser").classList.add("hidden");
113
+ }
114
+
115
+ function closeMetadataModal() {
116
+ document.getElementById("metadata-modal").classList.add("hidden");
117
+ }
118
+
119
+ async function metadataRefresh() {
120
+ const docId = metadataCurrentDocId();
121
+ if (!docId) return;
122
+ const btn = document.getElementById("metadata-refresh-btn");
123
+ if (btn) btn.disabled = true;
124
+ try {
125
+ const r = await fetch(
126
+ "/api/metadata/" + encodeURIComponent(docId) + "/refresh",
127
+ { method: "POST" },
128
+ );
129
+ if (!r.ok) throw new Error(r.statusText);
130
+ metadataReport = await r.json();
131
+ renderMetadataList();
132
+ renderMetadataSummary();
133
+ if (typeof renderAccuracyGauge === "function") {
134
+ renderAccuracyGauge(metadataReport);
135
+ }
136
+ } catch (err) {
137
+ showMetadataError(err.message);
138
+ } finally {
139
+ if (btn) btn.disabled = false;
140
+ }
141
+ }
142
+
143
+ async function metadataRemovePath(path) {
144
+ const docId = metadataCurrentDocId();
145
+ if (!docId) return;
146
+ try {
147
+ const r = await fetch(
148
+ "/api/metadata/" + encodeURIComponent(docId),
149
+ {
150
+ method: "DELETE",
151
+ headers: { "Content-Type": "application/json" },
152
+ body: JSON.stringify({ path }),
153
+ },
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
+ refreshBrowserIfOpen();
163
+ } catch (err) {
164
+ showMetadataError(err.message);
165
+ }
166
+ }
167
+
168
+ async function metadataAddPath(relPath) {
169
+ const docId = metadataCurrentDocId();
170
+ if (!docId) return;
171
+ try {
172
+ const r = await fetch(
173
+ "/api/metadata/" + encodeURIComponent(docId),
174
+ {
175
+ method: "POST",
176
+ headers: { "Content-Type": "application/json" },
177
+ body: JSON.stringify({ path: relPath }),
178
+ },
179
+ );
180
+ if (!r.ok) {
181
+ const body = await r.json().catch(() => ({}));
182
+ throw new Error(body.error || r.statusText);
183
+ }
184
+ metadataReport = await r.json();
185
+ renderMetadataList();
186
+ renderMetadataSummary();
187
+ if (typeof renderAccuracyGauge === "function") {
188
+ renderAccuracyGauge(metadataReport);
189
+ }
190
+ refreshBrowserIfOpen();
191
+ } catch (err) {
192
+ showMetadataError(err.message);
193
+ }
194
+ }
195
+
196
+ function refreshBrowserIfOpen() {
197
+ const b = document.getElementById("metadata-browser");
198
+ if (b && !b.classList.contains("hidden")) {
199
+ metadataBrowseLoad(metadataBrowseCurrent);
200
+ }
201
+ }
202
+
203
+ function showMetadataError(msg) {
204
+ const el = document.getElementById("metadata-error");
205
+ if (!el) return;
206
+ el.textContent = window.t("common.error_prefix") + msg;
207
+ el.classList.remove("hidden");
208
+ setTimeout(() => el.classList.add("hidden"), 5000);
209
+ }
210
+
211
+ // ── Source browser ─────────────────────────────────────────────────────────
212
+
213
+ function metadataToggleBrowser() {
214
+ const b = document.getElementById("metadata-browser");
215
+ if (b.classList.contains("hidden")) {
216
+ b.classList.remove("hidden");
217
+ metadataBrowseLoad("");
218
+ } else {
219
+ b.classList.add("hidden");
220
+ }
221
+ }
222
+
223
+ async function metadataBrowseLoad(relPath) {
224
+ const listEl = document.getElementById("metadata-browse-list");
225
+ const pathEl = document.getElementById("metadata-browse-path");
226
+ const upBtn = document.getElementById("metadata-browse-up");
227
+ if (!listEl) return;
228
+ listEl.innerHTML = `<div class="px-3 py-2 text-xs text-gray-400">${esc(window.t("common.loading"))}</div>`;
229
+ try {
230
+ const r = await fetch(
231
+ "/api/browse-source?path=" + encodeURIComponent(relPath || ""),
232
+ );
233
+ if (!r.ok) {
234
+ const body = await r.json().catch(() => ({}));
235
+ throw new Error(body.error || r.statusText);
236
+ }
237
+ const data = await r.json();
238
+ metadataBrowseCache = data;
239
+ metadataBrowseCurrent = data.current || "";
240
+ pathEl.textContent = data.current ? "/" + data.current : "/ (sourceRoot)";
241
+ upBtn.disabled = data.parent === null;
242
+ upBtn.classList.toggle("opacity-30", data.parent === null);
243
+ upBtn.classList.toggle("pointer-events-none", data.parent === null);
244
+
245
+ const dirRows = data.dirs.map(
246
+ (d) => `<button
247
+ onclick="metadataBrowseLoad('${d.path.replace(/'/g, "\\'")}')"
248
+ 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"
249
+ >
250
+ <i class="fa-solid fa-folder text-yellow-500"></i>
251
+ <span class="truncate">${esc(d.name)}</span>
252
+ </button>`,
253
+ );
254
+ const attached = new Set(
255
+ ((metadataReport && metadataReport.items) || []).map((it) => it.path),
256
+ );
257
+ const fileRows = data.files
258
+ .filter((f) => !attached.has(f.path))
259
+ .map(
260
+ (f) => `<button
261
+ onclick="metadataAddPath('${f.path.replace(/'/g, "\\'")}')"
262
+ 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"
263
+ >
264
+ <i class="fa-solid fa-file text-gray-400"></i>
265
+ <span class="truncate">${esc(f.name)}</span>
266
+ <i class="fa-solid fa-plus ml-auto text-blue-500 text-xs"></i>
267
+ </button>`,
268
+ );
269
+ const rows = [...dirRows, ...fileRows];
270
+ listEl.innerHTML = rows.length
271
+ ? rows.join("")
272
+ : `<div class="px-3 py-2 text-xs text-gray-400" data-i18n="common.empty_dir">${esc(window.t("common.empty_dir"))}</div>`;
273
+ } catch (err) {
274
+ listEl.innerHTML = `<div class="px-3 py-2 text-xs text-red-500">${esc(err.message)}</div>`;
275
+ }
276
+ }
277
+
278
+ function metadataBrowseUp() {
279
+ if (!metadataBrowseCache || metadataBrowseCache.parent === null) return;
280
+ metadataBrowseLoad(metadataBrowseCache.parent);
281
+ }
282
+
283
+ // Expose
284
+ window.openMetadataModal = openMetadataModal;
285
+ window.closeMetadataModal = closeMetadataModal;
286
+ window.metadataRefresh = metadataRefresh;
287
+ window.metadataRemovePath = metadataRemovePath;
288
+ window.metadataAddPath = metadataAddPath;
289
+ window.metadataToggleBrowser = metadataToggleBrowser;
290
+ window.metadataBrowseLoad = metadataBrowseLoad;
291
+ window.metadataBrowseUp = metadataBrowseUp;
292
+ window.loadMetadataReport = loadMetadataReport;
@@ -1,14 +1,36 @@
1
1
  // ── Misc viewer helpers ─────────────────────────────────────────────────────
2
2
 
3
- function toggleFullWidth() {
3
+ function applyFullWidthState(isWide) {
4
4
  const article = document.getElementById("doc-view");
5
5
  const btn = document.getElementById("full-width-btn");
6
- const isWide = article.classList.toggle("max-w-none");
6
+ if (!article || !btn) return;
7
+ article.classList.toggle("max-w-none", isWide);
7
8
  article.classList.toggle("max-w-4xl", !isWide);
8
9
  article.classList.toggle("mx-auto", !isWide);
9
10
  btn.textContent = isWide ? window.t('doc.full_width_narrow_btn') : window.t('doc.full_width_btn');
10
11
  }
11
12
 
13
+ function toggleFullWidth() {
14
+ const article = document.getElementById("doc-view");
15
+ const isWide = !article.classList.contains("max-w-none");
16
+ applyFullWidthState(isWide);
17
+ try {
18
+ localStorage.setItem("ld-full-width", isWide ? "1" : "0");
19
+ } catch {
20
+ /* ignore */
21
+ }
22
+ }
23
+
24
+ function initFullWidthState() {
25
+ let isWide = false;
26
+ try {
27
+ isWide = localStorage.getItem("ld-full-width") === "1";
28
+ } catch {
29
+ /* ignore */
30
+ }
31
+ applyFullWidthState(isWide);
32
+ }
33
+
12
34
  function copyLink() {
13
35
  navigator.clipboard.writeText(location.href).then(() => {
14
36
  const btn = document.getElementById("copy-link-btn");
@@ -158,6 +158,16 @@ function newDocCreateFolder() {
158
158
  .value.trim();
159
159
  if (!name) return;
160
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");
161
171
  const newRelPath =
162
172
  (_newDocAbsToRel(parent) ? _newDocAbsToRel(parent) + "/" : "") + name;
163
173
  _newDocSelectedFolder = newRelPath;
@@ -139,6 +139,12 @@ async function createNewFolder() {
139
139
  }
140
140
 
141
141
  const base = _newFolderSelectedPath || _newFolderDocsFolder;
142
+ const atDocsRoot = base === _newFolderDocsFolder;
143
+ if (atDocsRoot && (name === "files" || name === "images")) {
144
+ errEl.textContent = window.t('modal.new_folder.error_reserved');
145
+ errEl.classList.remove("hidden");
146
+ return;
147
+ }
142
148
  const fullPath = base.endsWith("/") ? base + name : base + "/" + name;
143
149
 
144
150
  const btn = document.getElementById("new-folder-create-btn");
@@ -35,6 +35,7 @@ function countTreeAnnotatedDocs(node) {
35
35
 
36
36
  function annotationBadge(count) {
37
37
  if (!count) return "";
38
+ if (typeof stabiloHidden !== "undefined" && stabiloHidden) return "";
38
39
  const label = window.t
39
40
  ? window.t("sidebar.annotation_badge")
40
41
  : "annotation";
@@ -47,6 +48,7 @@ function annotationBadge(count) {
47
48
 
48
49
  function annotatedDocsBadge(count) {
49
50
  if (!count) return "";
51
+ if (typeof stabiloHidden !== "undefined" && stabiloHidden) return "";
50
52
  const label = window.t
51
53
  ? window.t("sidebar.annotated_docs_badge")
52
54
  : "document with annotations";
@@ -56,3 +58,39 @@ function annotatedDocsBadge(count) {
56
58
  text-[10px] font-bold text-white
57
59
  border border-orange-600 shadow-sm">${count}</span>`;
58
60
  }
61
+
62
+ function countTreeFileAttachedDocs(node) {
63
+ let n = 0;
64
+ for (const arr of Object.values(node.categories)) {
65
+ for (const doc of arr) if (fileAttachmentCounts[doc.id] > 0) n += 1;
66
+ }
67
+ for (const child of Object.values(node.children))
68
+ n += countTreeFileAttachedDocs(child);
69
+ return n;
70
+ }
71
+
72
+ function fileAttachmentBadge(count) {
73
+ if (!count) return "";
74
+ if (typeof hideAttachments !== "undefined" && hideAttachments) return "";
75
+ const label = window.t
76
+ ? window.t("sidebar.file_attachment_badge")
77
+ : "attachment";
78
+ return `<span title="${count} ${esc(label)}${count > 1 ? "s" : ""}"
79
+ class="inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5
80
+ rounded-full bg-sky-200 dark:bg-sky-300
81
+ text-[10px] font-bold text-sky-900
82
+ border border-sky-500 shadow-sm">${count}</span>`;
83
+ }
84
+
85
+ function fileAttachedDocsBadge(count) {
86
+ if (!count) return "";
87
+ if (typeof hideAttachments !== "undefined" && hideAttachments) return "";
88
+ const label = window.t
89
+ ? window.t("sidebar.file_attached_docs_badge")
90
+ : "document with attachments";
91
+ return `<span title="${count} ${esc(label)}${count > 1 ? "s" : ""}"
92
+ class="inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5
93
+ rounded-full bg-slate-400 dark:bg-slate-500
94
+ text-[10px] font-bold text-white
95
+ border border-slate-600 shadow-sm">${count}</span>`;
96
+ }
@@ -72,6 +72,7 @@ function renderTreeNode(node, folderPath) {
72
72
  const isExpanded = expandedFolders.has(pathKey);
73
73
  const docCount = countTreeDocs(node.children[key]);
74
74
  const folderAnnotatedDocs = countTreeAnnotatedDocs(node.children[key]);
75
+ const folderFileDocs = countTreeFileAttachedDocs(node.children[key]);
75
76
  html += `
76
77
  <div class="mb-1">
77
78
  <button onclick="toggleFolder('${esc(pathKey)}')"
@@ -81,6 +82,7 @@ function renderTreeNode(node, folderPath) {
81
82
  <span class="flex items-center gap-2 min-w-0">
82
83
  <span title="${esc(key)}" class="truncate">&#128193; ${esc(folderLabel(key))}</span>
83
84
  ${annotatedDocsBadge(folderAnnotatedDocs)}
85
+ ${fileAttachedDocsBadge(folderFileDocs)}
84
86
  </span>
85
87
  <span class="flex items-center gap-1.5">
86
88
  <span class="font-normal normal-case text-gray-400">${docCount}</span>
@@ -106,6 +108,10 @@ function renderTreeNode(node, folderPath) {
106
108
  (s, d) => s + (annotationCounts[d.id] > 0 ? 1 : 0),
107
109
  0,
108
110
  );
111
+ const catFileDocs = node.categories[cat].reduce(
112
+ (s, d) => s + (fileAttachmentCounts[d.id] > 0 ? 1 : 0),
113
+ 0,
114
+ );
109
115
  return `
110
116
  <div class="mb-0.5">
111
117
  <button onclick="toggleCategory('${esc(catPathKey)}')"
@@ -115,6 +121,7 @@ function renderTreeNode(node, folderPath) {
115
121
  <span class="flex items-center gap-2">
116
122
  <span>${esc(cat)}</span>
117
123
  ${annotatedDocsBadge(catAnnotatedDocs)}
124
+ ${fileAttachedDocsBadge(catFileDocs)}
118
125
  </span>
119
126
  <span class="flex items-center gap-1.5">
120
127
  <span class="font-normal normal-case text-gray-400">${node.categories[cat].length}</span>
@@ -142,6 +149,7 @@ function renderTreeNode(node, folderPath) {
142
149
  const isExpanded = expandedFolders.has(pathKey);
143
150
  const docCount = countTreeDocs(node.children[key]);
144
151
  const folderAnnotatedDocs = countTreeAnnotatedDocs(node.children[key]);
152
+ const folderFileDocs = countTreeFileAttachedDocs(node.children[key]);
145
153
  html += `
146
154
  <div class="mb-1">
147
155
  <button onclick="toggleFolder('${esc(pathKey)}')"
@@ -151,6 +159,7 @@ function renderTreeNode(node, folderPath) {
151
159
  <span class="flex items-center gap-2 min-w-0">
152
160
  <span title="${esc(key)}" class="truncate">&#128193; ${esc(folderLabel(key))}</span>
153
161
  ${annotatedDocsBadge(folderAnnotatedDocs)}
162
+ ${fileAttachedDocsBadge(folderFileDocs)}
154
163
  </span>
155
164
  <span class="flex items-center gap-1.5">
156
165
  <span class="font-normal normal-case text-gray-400">${docCount}</span>
@@ -175,6 +184,7 @@ function renderTreeNode(node, folderPath) {
175
184
  function renderDocItem(doc) {
176
185
  const isActive = doc.id === currentDocId;
177
186
  const annCount = annotationCounts[doc.id] || 0;
187
+ const fileCount = fileAttachmentCounts[doc.id] || 0;
178
188
  return `
179
189
  <button onclick="openDocument('${esc(doc.id)}')"
180
190
  id="item-${esc(doc.id)}"
@@ -183,7 +193,10 @@ function renderDocItem(doc) {
183
193
  ${isActive ? "active" : ""}">
184
194
  <div class="leading-snug flex items-center justify-between gap-2">
185
195
  <span class="truncate">${esc(doc.title)}</span>
186
- ${annotationBadge(annCount)}
196
+ <span class="flex items-center gap-1 shrink-0">
197
+ ${annotationBadge(annCount)}
198
+ ${fileAttachmentBadge(fileCount)}
199
+ </span>
187
200
  </div>
188
201
  ${doc.formattedDate ? `<div class="text-xs text-gray-400 dark:text-gray-500 mt-0.5">${esc(doc.formattedDate)}</div>` : ""}
189
202
  </button>`;
@@ -237,8 +250,28 @@ function toggleHideCategories() {
237
250
  function applyHideCategoriesButtonState() {
238
251
  const btn = document.getElementById("toggle-categories-btn");
239
252
  if (!btn) return;
240
- btn.classList.toggle("text-blue-500", hideCategories);
241
- btn.classList.toggle("dark:text-blue-400", hideCategories);
242
- btn.classList.toggle("text-gray-400", !hideCategories);
243
- btn.classList.toggle("dark:text-gray-500", !hideCategories);
253
+ btn.classList.toggle("text-blue-500", !hideCategories);
254
+ btn.classList.toggle("dark:text-blue-400", !hideCategories);
255
+ btn.classList.toggle("text-gray-400", hideCategories);
256
+ btn.classList.toggle("dark:text-gray-500", hideCategories);
257
+ }
258
+
259
+ function toggleHideAttachments() {
260
+ hideAttachments = !hideAttachments;
261
+ try {
262
+ localStorage.setItem("ld-hide-attachments", hideAttachments ? "1" : "0");
263
+ } catch {
264
+ /* ignore */
265
+ }
266
+ applyHideAttachmentsButtonState();
267
+ refreshSidebar();
268
+ }
269
+
270
+ function applyHideAttachmentsButtonState() {
271
+ const btn = document.getElementById("toggle-attachments-btn");
272
+ if (!btn) return;
273
+ btn.classList.toggle("text-blue-500", !hideAttachments);
274
+ btn.classList.toggle("dark:text-blue-400", !hideAttachments);
275
+ btn.classList.toggle("text-gray-400", hideAttachments);
276
+ btn.classList.toggle("dark:text-gray-500", hideAttachments);
244
277
  }
@@ -4,6 +4,7 @@
4
4
  let allDocs = [];
5
5
  let allFolderPaths = [];
6
6
  let annotationCounts = {};
7
+ let fileAttachmentCounts = {};
7
8
  let currentDocId = null;
8
9
  let currentDocContent = "";
9
10
  let searchQuery = "";
@@ -18,6 +19,13 @@ let hideCategories = (() => {
18
19
  return false;
19
20
  }
20
21
  })();
22
+ let hideAttachments = (() => {
23
+ try {
24
+ return localStorage.getItem("ld-hide-attachments") === "1";
25
+ } catch {
26
+ return false;
27
+ }
28
+ })();
21
29
 
22
30
  function filteredDocs() {
23
31
  if (!searchQuery) return allDocs;
@@ -0,0 +1,2 @@
1
+ export declare function sha256File(absPath: string): string | null;
2
+ //# sourceMappingURL=hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../src/lib/hash.ts"],"names":[],"mappings":"AAGA,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOzD"}
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.sha256File = sha256File;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const crypto_1 = __importDefault(require("crypto"));
9
+ function sha256File(absPath) {
10
+ try {
11
+ const buf = fs_1.default.readFileSync(absPath);
12
+ return crypto_1.default.createHash("sha256").update(buf).digest("hex");
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.js","sourceRoot":"","sources":["../../../src/lib/hash.ts"],"names":[],"mappings":";;;;;AAGA,gCAOC;AAVD,4CAAoB;AACpB,oDAA4B;AAE5B,SAAgB,UAAU,CAAC,OAAe;IACxC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACrC,OAAO,gBAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,30 @@
1
+ export interface MetadataEntry {
2
+ path: string;
3
+ hash: string;
4
+ }
5
+ export type MetadataStore = Record<string, MetadataEntry[]>;
6
+ export type MetadataStatus = "unchanged" | "modified" | "missing";
7
+ export interface MetadataItem {
8
+ path: string;
9
+ storedHash: string;
10
+ currentHash: string | null;
11
+ status: MetadataStatus;
12
+ }
13
+ export interface AccuracyReport {
14
+ items: MetadataItem[];
15
+ total: number;
16
+ unchanged: number;
17
+ modified: number;
18
+ missing: number;
19
+ accuracy: number;
20
+ }
21
+ export declare function metadataPath(docsPath: string): string;
22
+ export declare function readMetadataStore(docsPath: string): MetadataStore;
23
+ export declare function writeMetadataStore(docsPath: string, store: MetadataStore): void;
24
+ export declare function resolveSourceRoot(docsPath: string): string;
25
+ export declare function assertUnderSourceRoot(relOrAbs: string, sourceRoot: string): string;
26
+ export declare function classifyEntry(entry: MetadataEntry, sourceRoot: string): MetadataItem;
27
+ export declare function buildReport(entries: MetadataEntry[], sourceRoot: string): AccuracyReport;
28
+ export declare function getDocEntries(docsPath: string, docId: string): MetadataEntry[];
29
+ export declare function setDocEntries(docsPath: string, docId: string, entries: MetadataEntry[]): void;
30
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../../src/lib/metadata.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;AAE5D,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAElE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAID,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,CAQjE;AAED,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,aAAa,GACnB,IAAI,CAMN;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO1D;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,MAAM,CAUR;AAED,wBAAgB,aAAa,CAC3B,KAAK,EAAE,aAAa,EACpB,UAAU,EAAE,MAAM,GACjB,YAAY,CAmBd;AAGD,wBAAgB,WAAW,CACzB,OAAO,EAAE,aAAa,EAAE,EACxB,UAAU,EAAE,MAAM,GACjB,cAAc,CAmBhB;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,aAAa,EAAE,CAGjB;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,EAAE,GACvB,IAAI,CAKN"}