living-documentation 7.6.0 → 7.8.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 (38) hide show
  1. package/dist/src/frontend/accuracy-gauge.js +47 -0
  2. package/dist/src/frontend/boot.js +1 -0
  3. package/dist/src/frontend/documents.js +5 -0
  4. package/dist/src/frontend/i18n/en.json +14 -0
  5. package/dist/src/frontend/i18n/fr.json +14 -0
  6. package/dist/src/frontend/index.html +149 -0
  7. package/dist/src/frontend/metadata.js +292 -0
  8. package/dist/src/frontend/sidebar-resize.js +98 -0
  9. package/dist/src/frontend/sidebar.js +8 -8
  10. package/dist/src/lib/hash.d.ts +2 -0
  11. package/dist/src/lib/hash.d.ts.map +1 -0
  12. package/dist/src/lib/hash.js +18 -0
  13. package/dist/src/lib/hash.js.map +1 -0
  14. package/dist/src/lib/metadata.d.ts +30 -0
  15. package/dist/src/lib/metadata.d.ts.map +1 -0
  16. package/dist/src/lib/metadata.js +109 -0
  17. package/dist/src/lib/metadata.js.map +1 -0
  18. package/dist/src/mcp/server.d.ts.map +1 -1
  19. package/dist/src/mcp/server.js +93 -0
  20. package/dist/src/mcp/server.js.map +1 -1
  21. package/dist/src/mcp/tools/metadata.d.ts +34 -0
  22. package/dist/src/mcp/tools/metadata.d.ts.map +1 -0
  23. package/dist/src/mcp/tools/metadata.js +76 -0
  24. package/dist/src/mcp/tools/metadata.js.map +1 -0
  25. package/dist/src/routes/browse-source.d.ts +3 -0
  26. package/dist/src/routes/browse-source.d.ts.map +1 -0
  27. package/dist/src/routes/browse-source.js +79 -0
  28. package/dist/src/routes/browse-source.js.map +1 -0
  29. package/dist/src/routes/metadata.d.ts +3 -0
  30. package/dist/src/routes/metadata.d.ts.map +1 -0
  31. package/dist/src/routes/metadata.js +107 -0
  32. package/dist/src/routes/metadata.js.map +1 -0
  33. package/dist/src/server.d.ts.map +1 -1
  34. package/dist/src/server.js +4 -0
  35. package/dist/src/server.js.map +1 -1
  36. package/dist/starting-doc/.annotations.json +1 -13
  37. package/dist/starting-doc/.metadata.json +1 -0
  38. package/package.json +1 -1
@@ -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;
@@ -12,6 +12,7 @@ document.addEventListener("DOMContentLoaded", async () => {
12
12
  setupSearch();
13
13
  wcRestorePrefs();
14
14
  if (typeof initFileAttach === "function") initFileAttach();
15
+ if (typeof initSidebarResize === "function") initSidebarResize();
15
16
  await loadConfig();
16
17
  if (typeof initMarkerState === "function") initMarkerState();
17
18
  if (typeof initFullWidthState === "function") initFullWidthState();
@@ -164,6 +164,11 @@ async function openDocument(id, skipHistory = false, fromLink = false, anchor =
164
164
  // Load annotations for this document
165
165
  loadAnnotations(id);
166
166
 
167
+ // Load source-file metadata report (drives the accuracy gauge)
168
+ if (typeof loadMetadataReport === "function") {
169
+ loadMetadataReport(id);
170
+ }
171
+
167
172
  // Add IDs to headings for anchor navigation
168
173
  contentEl.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((h) => {
169
174
  if (!h.id) {
@@ -69,6 +69,20 @@
69
69
  "doc.failed_to_load": "Failed to load document: ",
70
70
  "doc.copied": "✓ Copied!",
71
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
+
72
86
  "modal.new_folder.title": "New folder",
73
87
  "modal.new_folder.name_label": "Folder name",
74
88
  "modal.new_folder.name_placeholder": "my-folder",
@@ -69,6 +69,20 @@
69
69
  "doc.failed_to_load": "Impossible de charger le document : ",
70
70
  "doc.copied": "✓ Copié !",
71
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
+
72
86
  "modal.new_folder.title": "Nouveau dossier",
73
87
  "modal.new_folder.name_label": "Nom du dossier",
74
88
  "modal.new_folder.name_placeholder": "mon-dossier",
@@ -40,6 +40,7 @@
40
40
  <script defer src="/dark-mode.js"></script>
41
41
  <script defer src="/config.js"></script>
42
42
  <script defer src="/sidebar.js"></script>
43
+ <script defer src="/sidebar-resize.js"></script>
43
44
  <script defer src="/search.js"></script>
44
45
  <script defer src="/image-paste.js"></script>
45
46
  <script defer src="/file-attach.js"></script>
@@ -47,6 +48,8 @@
47
48
  <script defer src="/misc.js"></script>
48
49
  <script defer src="/snippets.js"></script>
49
50
  <script defer src="/annotations.js"></script>
51
+ <script defer src="/metadata.js"></script>
52
+ <script defer src="/accuracy-gauge.js"></script>
50
53
  <script defer src="/export.js"></script>
51
54
  <script defer src="/diagram-link-modal.js"></script>
52
55
  <script defer src="/new-folder-modal.js"></script>
@@ -573,6 +576,15 @@
573
576
  >
574
577
  <i class="fa-solid fa-link"></i> <span data-i18n="doc.copy_link_btn">Copy link</span>
575
578
  </button>
579
+ <button
580
+ onclick="openMetadataModal()"
581
+ id="metadata-btn"
582
+ data-i18n-title="metadata.button_title"
583
+ title="Manage source-file metadata"
584
+ 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"
585
+ >
586
+ <i class="fa-solid fa-code-compare"></i> <span data-i18n="metadata.button">Metadata</span>
587
+ </button>
576
588
  <button
577
589
  onclick="enterEditMode()"
578
590
  data-i18n-title="doc.edit"
@@ -620,6 +632,33 @@
620
632
  </div>
621
633
  </div>
622
634
  </div>
635
+ <!-- Accuracy gauge (shown when the current doc has metadata entries) -->
636
+ <div
637
+ id="accuracy-gauge"
638
+ onclick="openMetadataModal()"
639
+ class="hidden no-print mt-3 flex items-center gap-3 ml-auto w-full sm:w-80 cursor-pointer select-none"
640
+ >
641
+ <span
642
+ id="accuracy-gauge-label"
643
+ data-i18n="accuracy.label"
644
+ class="text-xs text-gray-500 dark:text-gray-400 shrink-0"
645
+ >Accuracy</span>
646
+ <div
647
+ class="flex-1 h-2 rounded-full bg-gray-200 dark:bg-gray-700 overflow-hidden"
648
+ >
649
+ <div
650
+ id="accuracy-gauge-bar"
651
+ class="h-full transition-all"
652
+ style="width: 0%; background-color: #9ca3af"
653
+ ></div>
654
+ </div>
655
+ <span
656
+ id="accuracy-gauge-value"
657
+ class="text-xs font-semibold shrink-0 tabular-nums"
658
+ style="color: #9ca3af"
659
+ >0%</span>
660
+ </div>
661
+
623
662
  <!-- Navigation history back-links -->
624
663
  <div
625
664
  id="doc-back"
@@ -660,6 +699,116 @@
660
699
  </div>
661
700
  <!-- end root -->
662
701
 
702
+ <!-- ── Metadata modal ── -->
703
+ <div
704
+ id="metadata-modal"
705
+ class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/50"
706
+ >
707
+ <div
708
+ class="bg-white dark:bg-gray-900 rounded-xl shadow-xl w-full max-w-2xl mx-4 p-6 space-y-4"
709
+ >
710
+ <div class="flex items-start justify-between gap-4">
711
+ <div>
712
+ <h3 class="text-base font-semibold text-gray-900 dark:text-gray-50">
713
+ <i class="fa-solid fa-code-compare mr-1"></i>
714
+ <span data-i18n="metadata.title">Source metadata</span>
715
+ </h3>
716
+ <p
717
+ data-i18n="metadata.subtitle"
718
+ class="text-xs text-gray-500 dark:text-gray-400 mt-1"
719
+ >
720
+ Attach source files to track whether this document is still accurate.
721
+ </p>
722
+ </div>
723
+ <button
724
+ onclick="closeMetadataModal()"
725
+ data-i18n-title="common.close"
726
+ title="Close"
727
+ class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors"
728
+ >
729
+ <i class="fa-solid fa-xmark text-lg"></i>
730
+ </button>
731
+ </div>
732
+
733
+ <!-- Summary -->
734
+ <div
735
+ id="metadata-summary"
736
+ class="text-xs text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 rounded-lg px-3 py-2"
737
+ ></div>
738
+
739
+ <!-- List -->
740
+ <div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
741
+ <div
742
+ id="metadata-list"
743
+ class="max-h-64 overflow-y-auto"
744
+ ></div>
745
+ <div
746
+ id="metadata-empty"
747
+ data-i18n="metadata.empty"
748
+ class="hidden px-3 py-4 text-center text-xs text-gray-400"
749
+ >
750
+ No metadata yet — add a source file to start tracking accuracy.
751
+ </div>
752
+ </div>
753
+
754
+ <!-- Actions -->
755
+ <div class="flex items-center gap-2 flex-wrap">
756
+ <button
757
+ onclick="metadataToggleBrowser()"
758
+ 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"
759
+ >
760
+ <i class="fa-solid fa-plus"></i> <span data-i18n="metadata.add">Add source file</span>
761
+ </button>
762
+ <button
763
+ onclick="metadataRefresh()"
764
+ id="metadata-refresh-btn"
765
+ 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"
766
+ >
767
+ <i class="fa-solid fa-arrows-rotate"></i> <span data-i18n="metadata.refresh">Refresh hashes</span>
768
+ </button>
769
+ </div>
770
+
771
+ <!-- Source browser -->
772
+ <div
773
+ id="metadata-browser"
774
+ class="hidden border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden text-sm"
775
+ >
776
+ <div
777
+ 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"
778
+ >
779
+ <button
780
+ id="metadata-browse-up"
781
+ onclick="metadataBrowseUp()"
782
+ data-i18n="common.up"
783
+ class="text-xs text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-30 disabled:pointer-events-none shrink-0"
784
+ >
785
+ ↑ Up
786
+ </button>
787
+ <span
788
+ id="metadata-browse-path"
789
+ class="text-xs text-gray-500 dark:text-gray-400 font-mono truncate flex-1"
790
+ ></span>
791
+ </div>
792
+ <div
793
+ id="metadata-browse-list"
794
+ class="max-h-56 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800"
795
+ ></div>
796
+ </div>
797
+
798
+ <p id="metadata-error" class="hidden text-xs text-red-500"></p>
799
+
800
+ <div class="flex justify-end">
801
+ <button
802
+ onclick="closeMetadataModal()"
803
+ data-i18n="common.close"
804
+ 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"
805
+ >
806
+ Close
807
+ </button>
808
+ </div>
809
+ </div>
810
+ </div>
811
+
663
812
  <!-- ── New Folder modal ── -->
664
813
  <div
665
814
  id="new-folder-modal"
@@ -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;
@@ -0,0 +1,98 @@
1
+ // ── Sidebar horizontal resize ───────────────────────────────────────────────
2
+ // Drag a handle on the sidebar's right edge to resize it.
3
+ // Width is persisted in localStorage (`ld-sidebar-w`) across reloads.
4
+ // The toggle (toggleSidebar) keeps working because it only flips the `hidden`
5
+ // class — width stays intact on the element.
6
+
7
+ (function () {
8
+ const STORAGE_KEY = "ld-sidebar-w";
9
+ const MIN_W = 200;
10
+ const MAX_W = 600;
11
+
12
+ function applyStoredWidth() {
13
+ const sidebar = document.getElementById("sidebar");
14
+ if (!sidebar) return;
15
+ let w = parseInt(localStorage.getItem(STORAGE_KEY) || "", 10);
16
+ if (!Number.isFinite(w)) return;
17
+ w = Math.max(MIN_W, Math.min(MAX_W, w));
18
+ sidebar.classList.remove("w-72");
19
+ sidebar.style.width = w + "px";
20
+ }
21
+
22
+ function initSidebarResize() {
23
+ const sidebar = document.getElementById("sidebar");
24
+ if (!sidebar) return;
25
+
26
+ applyStoredWidth();
27
+
28
+ const handle = document.createElement("div");
29
+ handle.id = "sidebar-resize-handle";
30
+ handle.setAttribute("aria-hidden", "true");
31
+ handle.style.cssText = [
32
+ "position:absolute",
33
+ "top:0",
34
+ "right:-2px",
35
+ "width:6px",
36
+ "height:100%",
37
+ "cursor:col-resize",
38
+ "z-index:20",
39
+ "user-select:none",
40
+ "background:transparent",
41
+ "transition:background 0.15s ease",
42
+ ].join(";");
43
+ handle.addEventListener("mouseenter", () => {
44
+ handle.style.background = "rgba(59,130,246,0.35)";
45
+ });
46
+ handle.addEventListener("mouseleave", () => {
47
+ if (!handle.dataset.dragging) handle.style.background = "transparent";
48
+ });
49
+
50
+ // Sidebar needs `relative` so the absolutely-positioned handle anchors to it.
51
+ sidebar.style.position = "relative";
52
+ sidebar.appendChild(handle);
53
+
54
+ let startX = 0;
55
+ let startW = 0;
56
+
57
+ function onMove(e) {
58
+ const dx = e.clientX - startX;
59
+ let w = startW + dx;
60
+ w = Math.max(MIN_W, Math.min(MAX_W, w));
61
+ sidebar.style.width = w + "px";
62
+ }
63
+
64
+ function onUp() {
65
+ document.removeEventListener("mousemove", onMove);
66
+ document.removeEventListener("mouseup", onUp);
67
+ document.body.style.cursor = "";
68
+ document.body.style.userSelect = "";
69
+ delete handle.dataset.dragging;
70
+ handle.style.background = "transparent";
71
+ const finalW = parseInt(sidebar.style.width, 10);
72
+ if (Number.isFinite(finalW)) {
73
+ try {
74
+ localStorage.setItem(STORAGE_KEY, String(finalW));
75
+ } catch {
76
+ /* ignore */
77
+ }
78
+ }
79
+ }
80
+
81
+ handle.addEventListener("mousedown", (e) => {
82
+ e.preventDefault();
83
+ startX = e.clientX;
84
+ startW = sidebar.getBoundingClientRect().width;
85
+ // Drop the Tailwind width class so inline width takes over for good.
86
+ sidebar.classList.remove("w-72");
87
+ sidebar.style.width = startW + "px";
88
+ handle.dataset.dragging = "1";
89
+ handle.style.background = "rgba(59,130,246,0.55)";
90
+ document.body.style.cursor = "col-resize";
91
+ document.body.style.userSelect = "none";
92
+ document.addEventListener("mousemove", onMove);
93
+ document.addEventListener("mouseup", onUp);
94
+ });
95
+ }
96
+
97
+ window.initSidebarResize = initSidebarResize;
98
+ })();