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,47 @@
1
+ // ── Accuracy gauge ──────────────────────────────────────────────────────────
2
+ // Shown in the sticky document header (right-aligned, own row) when the
3
+ // current document has at least one metadata entry.
4
+
5
+ function accuracyColor(ratio) {
6
+ const pct = Math.max(0, Math.min(1, ratio)) * 100;
7
+ if (pct > 80) return "#16a34a"; // green-600
8
+ if (pct >= 60) return "#eab308"; // yellow-500
9
+ if (pct >= 40) return "#f97316"; // orange-500
10
+ return "#dc2626"; // red-600
11
+ }
12
+
13
+ function renderAccuracyGauge(report) {
14
+ const wrap = document.getElementById("accuracy-gauge");
15
+ if (!wrap) return;
16
+ if (!report || report.total === 0) {
17
+ wrap.classList.add("hidden");
18
+ return;
19
+ }
20
+ wrap.classList.remove("hidden");
21
+
22
+ const pct = Math.round(report.accuracy * 100);
23
+ const color = accuracyColor(report.accuracy);
24
+
25
+ const label = document.getElementById("accuracy-gauge-label");
26
+ const bar = document.getElementById("accuracy-gauge-bar");
27
+ const value = document.getElementById("accuracy-gauge-value");
28
+
29
+ if (label) label.textContent = window.t("accuracy.label");
30
+ if (bar) {
31
+ bar.style.width = pct + "%";
32
+ bar.style.backgroundColor = color;
33
+ }
34
+ if (value) {
35
+ value.textContent = pct + "%";
36
+ value.style.color = color;
37
+ }
38
+
39
+ wrap.title = window
40
+ .t("accuracy.tooltip")
41
+ .replace("{unchanged}", String(report.unchanged))
42
+ .replace("{modified}", String(report.modified))
43
+ .replace("{missing}", String(report.missing))
44
+ .replace("{total}", String(report.total));
45
+ }
46
+
47
+ window.renderAccuracyGauge = renderAccuracyGauge;
@@ -10,21 +10,25 @@ let stabiloDeleteTargetId = null;
10
10
  let stabiloReadPopupAnnotationId = null;
11
11
  let stabiloReadHideTimer = null;
12
12
 
13
- // Cycles: normal → active → hidden → normal
14
- function toggleMarker() {
13
+ function applyMarkerVisualState() {
15
14
  const btn = document.getElementById("stabilo-btn");
15
+ if (!btn) return;
16
16
  const fills = btn.querySelectorAll("rect, polygon");
17
17
  const crossEl = document.getElementById("stabilo-cross");
18
18
 
19
- if (!stabiloActive && !stabiloHidden) {
20
- // normal → active
21
- stabiloActive = true;
22
- btn.classList.remove(
23
- "border-gray-200",
24
- "dark:border-gray-700",
25
- "text-gray-600",
26
- "dark:text-gray-400",
27
- );
19
+ btn.classList.remove(
20
+ "border-gray-200",
21
+ "dark:border-gray-700",
22
+ "text-gray-600",
23
+ "dark:text-gray-400",
24
+ "border-yellow-400",
25
+ "bg-yellow-100",
26
+ "dark:bg-yellow-900/40",
27
+ "text-yellow-700",
28
+ "dark:text-yellow-300",
29
+ );
30
+
31
+ if (stabiloActive) {
28
32
  btn.classList.add(
29
33
  "border-yellow-400",
30
34
  "bg-yellow-100",
@@ -39,17 +43,7 @@ function toggleMarker() {
39
43
  ),
40
44
  );
41
45
  crossEl.style.display = "none";
42
- } else if (stabiloActive) {
43
- // active → hidden
44
- stabiloActive = false;
45
- stabiloHidden = true;
46
- btn.classList.remove(
47
- "border-yellow-400",
48
- "bg-yellow-100",
49
- "dark:bg-yellow-900/40",
50
- "text-yellow-700",
51
- "dark:text-yellow-300",
52
- );
46
+ } else {
53
47
  btn.classList.add(
54
48
  "border-gray-200",
55
49
  "dark:border-gray-700",
@@ -66,14 +60,55 @@ function toggleMarker() {
66
60
  : "#bfdbfe",
67
61
  ),
68
62
  );
69
- crossEl.style.display = "block";
70
- closeMarkerPopup();
71
- setHighlightsVisible(false);
63
+ crossEl.style.display = stabiloHidden ? "block" : "none";
64
+ }
65
+ }
66
+
67
+ function persistMarkerState() {
68
+ const state = stabiloActive ? "active" : stabiloHidden ? "hidden" : "normal";
69
+ try {
70
+ localStorage.setItem("ld-marker-state", state);
71
+ } catch {
72
+ /* ignore */
73
+ }
74
+ }
75
+
76
+ function initMarkerState() {
77
+ let saved = "normal";
78
+ try {
79
+ saved = localStorage.getItem("ld-marker-state") || "normal";
80
+ } catch {
81
+ /* ignore */
82
+ }
83
+ stabiloActive = saved === "active";
84
+ stabiloHidden = saved === "hidden";
85
+ applyMarkerVisualState();
86
+ if (stabiloHidden) setHighlightsVisible(false);
87
+ }
88
+
89
+ // Cycles: normal → active → hidden → normal
90
+ function toggleMarker() {
91
+ const wasHidden = stabiloHidden;
92
+
93
+ if (!stabiloActive && !stabiloHidden) {
94
+ stabiloActive = true;
95
+ } else if (stabiloActive) {
96
+ stabiloActive = false;
97
+ stabiloHidden = true;
72
98
  } else {
73
- // hidden → normal
74
99
  stabiloHidden = false;
75
- crossEl.style.display = "none";
100
+ }
101
+
102
+ applyMarkerVisualState();
103
+ persistMarkerState();
104
+
105
+ if (!wasHidden && stabiloHidden) {
106
+ closeMarkerPopup();
107
+ setHighlightsVisible(false);
108
+ refreshSidebar();
109
+ } else if (wasHidden && !stabiloHidden) {
76
110
  setHighlightsVisible(true);
111
+ refreshSidebar();
77
112
  }
78
113
  }
79
114
 
@@ -129,6 +164,8 @@ function applyAnnotationHighlights() {
129
164
  .forEach((mark) => {
130
165
  if (!mark.textContent.trim()) mark.remove();
131
166
  });
167
+
168
+ if (stabiloHidden) setHighlightsVisible(false);
132
169
  }
133
170
 
134
171
  function highlightAnnotation(contentEl, ann) {
@@ -543,4 +580,6 @@ function renderElevator() {
543
580
 
544
581
  elevator.appendChild(pill);
545
582
  }
583
+
584
+ if (stabiloHidden) elevator.style.visibility = "hidden";
546
585
  }
@@ -13,8 +13,11 @@ document.addEventListener("DOMContentLoaded", async () => {
13
13
  wcRestorePrefs();
14
14
  if (typeof initFileAttach === "function") initFileAttach();
15
15
  await loadConfig();
16
+ if (typeof initMarkerState === "function") initMarkerState();
17
+ if (typeof initFullWidthState === "function") initFullWidthState();
16
18
  await loadDocuments();
17
19
  applyHideCategoriesButtonState();
20
+ applyHideAttachmentsButtonState();
18
21
 
19
22
  // Deep-link via ?doc=id, otherwise open first General doc
20
23
  const params = new URLSearchParams(location.search);
@@ -20,7 +20,10 @@ async function loadDocuments() {
20
20
  } catch {
21
21
  allFolderPaths = [];
22
22
  }
23
- await refreshAnnotationCounts();
23
+ await Promise.all([
24
+ refreshAnnotationCounts(),
25
+ refreshFileAttachmentCounts(),
26
+ ]);
24
27
  renderSidebar(allDocs);
25
28
  } catch {
26
29
  document.getElementById("category-tree").innerHTML =
@@ -43,6 +46,18 @@ async function refreshAnnotationCounts() {
43
46
  }
44
47
  }
45
48
 
49
+ async function refreshFileAttachmentCounts() {
50
+ try {
51
+ const raw = await fetch("/api/documents/file-counts").then((r) => r.json());
52
+ fileAttachmentCounts = {};
53
+ for (const [docId, n] of Object.entries(raw || {})) {
54
+ fileAttachmentCounts[docId] = n;
55
+ }
56
+ } catch {
57
+ fileAttachmentCounts = {};
58
+ }
59
+ }
60
+
46
61
  async function openDocument(id, skipHistory = false, fromLink = false, anchor = null) {
47
62
  // Track navigation history for breadcrumb trail
48
63
  // fromLink===true : forward navigation via in-doc link → push current to stack
@@ -149,6 +164,11 @@ async function openDocument(id, skipHistory = false, fromLink = false, anchor =
149
164
  // Load annotations for this document
150
165
  loadAnnotations(id);
151
166
 
167
+ // Load source-file metadata report (drives the accuracy gauge)
168
+ if (typeof loadMetadataReport === "function") {
169
+ loadMetadataReport(id);
170
+ }
171
+
152
172
  // Add IDs to headings for anchor navigation
153
173
  contentEl.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((h) => {
154
174
  if (!h.id) {
@@ -289,6 +309,7 @@ async function confirmDeleteDocument() {
289
309
  try {
290
310
  delete annotationCounts[decodeURIComponent(deletedId)];
291
311
  } catch {}
312
+ delete fileAttachmentCounts[deletedId];
292
313
  currentDocId = null;
293
314
 
294
315
  // Return to welcome screen
@@ -369,6 +390,12 @@ async function saveDocument() {
369
390
  applyAnnotationHighlights();
370
391
  renderElevator();
371
392
 
393
+ const fileLinkMatches = content.match(/\]\(\s*\.?\/files\/[^)\s]+/g);
394
+ const fileLinkCount = fileLinkMatches ? fileLinkMatches.length : 0;
395
+ if (fileLinkCount > 0) fileAttachmentCounts[currentDocId] = fileLinkCount;
396
+ else delete fileAttachmentCounts[currentDocId];
397
+ refreshSidebar();
398
+
372
399
  exitEditMode();
373
400
  } catch (err) {
374
401
  msgEl.textContent = window.t('error.save') + err.message;
@@ -21,6 +21,7 @@
21
21
  "nav.toggle_dark": "Toggle dark mode",
22
22
  "nav.export": "Export",
23
23
  "nav.toggle_categories": "Show/hide category grouping",
24
+ "nav.toggle_attachments": "Show/hide attachments",
24
25
  "nav.export_html": "Export folders as HTML ZIP",
25
26
  "nav.export_all_pdf": "Export all documents as PDF",
26
27
  "nav.new_folder": "New folder",
@@ -37,6 +38,8 @@
37
38
  "sidebar.no_docs": "No documents found.",
38
39
  "sidebar.annotation_badge": "annotation",
39
40
  "sidebar.annotated_docs_badge": "document with annotations",
41
+ "sidebar.file_attachment_badge": "attachment",
42
+ "sidebar.file_attached_docs_badge": "document with attachments",
40
43
 
41
44
  "doc.marker_mode": "Marker mode — highlight to annotate",
42
45
  "doc.marker_btn": "Marker",
@@ -66,6 +69,20 @@
66
69
  "doc.failed_to_load": "Failed to load document: ",
67
70
  "doc.copied": "✓ Copied!",
68
71
 
72
+ "metadata.button": "Metadata",
73
+ "metadata.button_title": "Manage source-file metadata",
74
+ "metadata.title": "Source metadata",
75
+ "metadata.subtitle": "Attach source files to track whether this document is still accurate.",
76
+ "metadata.add": "Add source file",
77
+ "metadata.refresh": "Refresh hashes",
78
+ "metadata.remove": "Remove",
79
+ "metadata.empty": "No metadata yet — add a source file to start tracking accuracy.",
80
+ "metadata.status.unchanged": "Unchanged",
81
+ "metadata.status.modified": "Modified",
82
+ "metadata.status.missing": "Missing",
83
+ "accuracy.label": "Accuracy",
84
+ "accuracy.tooltip": "Unchanged: {unchanged} · Modified: {modified} · Missing: {missing} · Total: {total}",
85
+
69
86
  "modal.new_folder.title": "New folder",
70
87
  "modal.new_folder.name_label": "Folder name",
71
88
  "modal.new_folder.name_placeholder": "my-folder",
@@ -78,6 +95,7 @@
78
95
  "modal.new_folder.creating_btn": "Creating…",
79
96
  "modal.new_folder.error_empty": "Please enter a folder name.",
80
97
  "modal.new_folder.error_invalid_chars": "Invalid characters in folder name.",
98
+ "modal.new_folder.error_reserved": "\"files\" and \"images\" are reserved folder names at the docs root.",
81
99
  "modal.new_folder.no_subfolders": "No subfolders",
82
100
  "modal.new_folder.error_loading": "Error loading",
83
101
 
@@ -21,6 +21,7 @@
21
21
  "nav.toggle_dark": "Basculer le mode sombre",
22
22
  "nav.export": "Exporter",
23
23
  "nav.toggle_categories": "Afficher/masquer le regroupement par catégorie",
24
+ "nav.toggle_attachments": "Afficher/masquer les pièces jointes",
24
25
  "nav.export_html": "Exporter les dossiers en HTML (ZIP)",
25
26
  "nav.export_all_pdf": "Exporter tous les documents en PDF",
26
27
  "nav.new_folder": "Nouveau dossier",
@@ -37,6 +38,8 @@
37
38
  "sidebar.no_docs": "Aucun document trouvé.",
38
39
  "sidebar.annotation_badge": "annotation",
39
40
  "sidebar.annotated_docs_badge": "document contenant des annotations",
41
+ "sidebar.file_attachment_badge": "pièce jointe",
42
+ "sidebar.file_attached_docs_badge": "document contenant des pièces jointes",
40
43
 
41
44
  "doc.marker_mode": "Mode Marqueur — surligner pour annoter",
42
45
  "doc.marker_btn": "Marqueur",
@@ -66,6 +69,20 @@
66
69
  "doc.failed_to_load": "Impossible de charger le document : ",
67
70
  "doc.copied": "✓ Copié !",
68
71
 
72
+ "metadata.button": "Métadonnées",
73
+ "metadata.button_title": "Gérer les métadonnées des fichiers source",
74
+ "metadata.title": "Métadonnées des sources",
75
+ "metadata.subtitle": "Rattachez des fichiers source pour suivre si ce document reste à jour.",
76
+ "metadata.add": "Ajouter un fichier source",
77
+ "metadata.refresh": "Rafraîchir les hashs",
78
+ "metadata.remove": "Retirer",
79
+ "metadata.empty": "Aucune métadonnée — ajoutez un fichier source pour commencer à suivre la justesse.",
80
+ "metadata.status.unchanged": "Inchangé",
81
+ "metadata.status.modified": "Modifié",
82
+ "metadata.status.missing": "Manquant",
83
+ "accuracy.label": "Justesse",
84
+ "accuracy.tooltip": "Inchangés : {unchanged} · Modifiés : {modified} · Manquants : {missing} · Total : {total}",
85
+
69
86
  "modal.new_folder.title": "Nouveau dossier",
70
87
  "modal.new_folder.name_label": "Nom du dossier",
71
88
  "modal.new_folder.name_placeholder": "mon-dossier",
@@ -78,6 +95,7 @@
78
95
  "modal.new_folder.creating_btn": "Création…",
79
96
  "modal.new_folder.error_empty": "Veuillez saisir un nom de dossier.",
80
97
  "modal.new_folder.error_invalid_chars": "Caractères non autorisés dans le nom du dossier.",
98
+ "modal.new_folder.error_reserved": "\"files\" et \"images\" sont des noms de dossiers réservés à la racine des documents.",
81
99
  "modal.new_folder.no_subfolders": "Aucun sous-dossier",
82
100
  "modal.new_folder.error_loading": "Erreur de chargement",
83
101
 
@@ -47,6 +47,8 @@
47
47
  <script defer src="/misc.js"></script>
48
48
  <script defer src="/snippets.js"></script>
49
49
  <script defer src="/annotations.js"></script>
50
+ <script defer src="/metadata.js"></script>
51
+ <script defer src="/accuracy-gauge.js"></script>
50
52
  <script defer src="/export.js"></script>
51
53
  <script defer src="/diagram-link-modal.js"></script>
52
54
  <script defer src="/new-folder-modal.js"></script>
@@ -347,6 +349,15 @@
347
349
  class="text-xs text-gray-400 dark:text-gray-500"
348
350
  ></p>
349
351
  <div class="flex items-center gap-2">
352
+ <button
353
+ id="toggle-attachments-btn"
354
+ onclick="toggleHideAttachments()"
355
+ data-i18n-title="nav.toggle_attachments"
356
+ title="Toggle attachments"
357
+ class="text-gray-400 hover:text-blue-500 dark:text-gray-500 dark:hover:text-blue-400 transition-colors leading-none"
358
+ >
359
+ <i class="fa-solid fa-paperclip"></i>
360
+ </button>
350
361
  <button
351
362
  id="toggle-categories-btn"
352
363
  onclick="toggleHideCategories()"
@@ -564,6 +575,15 @@
564
575
  >
565
576
  <i class="fa-solid fa-link"></i> <span data-i18n="doc.copy_link_btn">Copy link</span>
566
577
  </button>
578
+ <button
579
+ onclick="openMetadataModal()"
580
+ id="metadata-btn"
581
+ data-i18n-title="metadata.button_title"
582
+ title="Manage source-file metadata"
583
+ class="no-print text-sm px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
584
+ >
585
+ <i class="fa-solid fa-code-compare"></i> <span data-i18n="metadata.button">Metadata</span>
586
+ </button>
567
587
  <button
568
588
  onclick="enterEditMode()"
569
589
  data-i18n-title="doc.edit"
@@ -611,6 +631,33 @@
611
631
  </div>
612
632
  </div>
613
633
  </div>
634
+ <!-- Accuracy gauge (shown when the current doc has metadata entries) -->
635
+ <div
636
+ id="accuracy-gauge"
637
+ onclick="openMetadataModal()"
638
+ class="hidden no-print mt-3 flex items-center gap-3 ml-auto w-full sm:w-80 cursor-pointer select-none"
639
+ >
640
+ <span
641
+ id="accuracy-gauge-label"
642
+ data-i18n="accuracy.label"
643
+ class="text-xs text-gray-500 dark:text-gray-400 shrink-0"
644
+ >Accuracy</span>
645
+ <div
646
+ class="flex-1 h-2 rounded-full bg-gray-200 dark:bg-gray-700 overflow-hidden"
647
+ >
648
+ <div
649
+ id="accuracy-gauge-bar"
650
+ class="h-full transition-all"
651
+ style="width: 0%; background-color: #9ca3af"
652
+ ></div>
653
+ </div>
654
+ <span
655
+ id="accuracy-gauge-value"
656
+ class="text-xs font-semibold shrink-0 tabular-nums"
657
+ style="color: #9ca3af"
658
+ >0%</span>
659
+ </div>
660
+
614
661
  <!-- Navigation history back-links -->
615
662
  <div
616
663
  id="doc-back"
@@ -651,6 +698,116 @@
651
698
  </div>
652
699
  <!-- end root -->
653
700
 
701
+ <!-- ── Metadata modal ── -->
702
+ <div
703
+ id="metadata-modal"
704
+ class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/50"
705
+ >
706
+ <div
707
+ class="bg-white dark:bg-gray-900 rounded-xl shadow-xl w-full max-w-2xl mx-4 p-6 space-y-4"
708
+ >
709
+ <div class="flex items-start justify-between gap-4">
710
+ <div>
711
+ <h3 class="text-base font-semibold text-gray-900 dark:text-gray-50">
712
+ <i class="fa-solid fa-code-compare mr-1"></i>
713
+ <span data-i18n="metadata.title">Source metadata</span>
714
+ </h3>
715
+ <p
716
+ data-i18n="metadata.subtitle"
717
+ class="text-xs text-gray-500 dark:text-gray-400 mt-1"
718
+ >
719
+ Attach source files to track whether this document is still accurate.
720
+ </p>
721
+ </div>
722
+ <button
723
+ onclick="closeMetadataModal()"
724
+ data-i18n-title="common.close"
725
+ title="Close"
726
+ class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors"
727
+ >
728
+ <i class="fa-solid fa-xmark text-lg"></i>
729
+ </button>
730
+ </div>
731
+
732
+ <!-- Summary -->
733
+ <div
734
+ id="metadata-summary"
735
+ class="text-xs text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 rounded-lg px-3 py-2"
736
+ ></div>
737
+
738
+ <!-- List -->
739
+ <div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
740
+ <div
741
+ id="metadata-list"
742
+ class="max-h-64 overflow-y-auto"
743
+ ></div>
744
+ <div
745
+ id="metadata-empty"
746
+ data-i18n="metadata.empty"
747
+ class="hidden px-3 py-4 text-center text-xs text-gray-400"
748
+ >
749
+ No metadata yet — add a source file to start tracking accuracy.
750
+ </div>
751
+ </div>
752
+
753
+ <!-- Actions -->
754
+ <div class="flex items-center gap-2 flex-wrap">
755
+ <button
756
+ onclick="metadataToggleBrowser()"
757
+ class="text-sm px-3 py-1.5 rounded-lg border border-blue-200 dark:border-blue-700 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 transition-colors"
758
+ >
759
+ <i class="fa-solid fa-plus"></i> <span data-i18n="metadata.add">Add source file</span>
760
+ </button>
761
+ <button
762
+ onclick="metadataRefresh()"
763
+ id="metadata-refresh-btn"
764
+ class="text-sm px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
765
+ >
766
+ <i class="fa-solid fa-arrows-rotate"></i> <span data-i18n="metadata.refresh">Refresh hashes</span>
767
+ </button>
768
+ </div>
769
+
770
+ <!-- Source browser -->
771
+ <div
772
+ id="metadata-browser"
773
+ class="hidden border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden text-sm"
774
+ >
775
+ <div
776
+ class="flex items-center gap-2 px-3 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"
777
+ >
778
+ <button
779
+ id="metadata-browse-up"
780
+ onclick="metadataBrowseUp()"
781
+ data-i18n="common.up"
782
+ class="text-xs text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-30 disabled:pointer-events-none shrink-0"
783
+ >
784
+ ↑ Up
785
+ </button>
786
+ <span
787
+ id="metadata-browse-path"
788
+ class="text-xs text-gray-500 dark:text-gray-400 font-mono truncate flex-1"
789
+ ></span>
790
+ </div>
791
+ <div
792
+ id="metadata-browse-list"
793
+ class="max-h-56 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800"
794
+ ></div>
795
+ </div>
796
+
797
+ <p id="metadata-error" class="hidden text-xs text-red-500"></p>
798
+
799
+ <div class="flex justify-end">
800
+ <button
801
+ onclick="closeMetadataModal()"
802
+ data-i18n="common.close"
803
+ class="text-sm px-4 py-2 rounded-lg border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
804
+ >
805
+ Close
806
+ </button>
807
+ </div>
808
+ </div>
809
+ </div>
810
+
654
811
  <!-- ── New Folder modal ── -->
655
812
  <div
656
813
  id="new-folder-modal"