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.
- package/dist/src/frontend/accuracy-gauge.js +47 -0
- package/dist/src/frontend/boot.js +1 -0
- package/dist/src/frontend/documents.js +5 -0
- package/dist/src/frontend/i18n/en.json +14 -0
- package/dist/src/frontend/i18n/fr.json +14 -0
- package/dist/src/frontend/index.html +149 -0
- package/dist/src/frontend/metadata.js +292 -0
- package/dist/src/frontend/sidebar-resize.js +98 -0
- package/dist/src/frontend/sidebar.js +8 -8
- package/dist/src/lib/hash.d.ts +2 -0
- package/dist/src/lib/hash.d.ts.map +1 -0
- package/dist/src/lib/hash.js +18 -0
- package/dist/src/lib/hash.js.map +1 -0
- package/dist/src/lib/metadata.d.ts +30 -0
- package/dist/src/lib/metadata.d.ts.map +1 -0
- package/dist/src/lib/metadata.js +109 -0
- package/dist/src/lib/metadata.js.map +1 -0
- package/dist/src/mcp/server.d.ts.map +1 -1
- package/dist/src/mcp/server.js +93 -0
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools/metadata.d.ts +34 -0
- package/dist/src/mcp/tools/metadata.d.ts.map +1 -0
- package/dist/src/mcp/tools/metadata.js +76 -0
- package/dist/src/mcp/tools/metadata.js.map +1 -0
- package/dist/src/routes/browse-source.d.ts +3 -0
- package/dist/src/routes/browse-source.d.ts.map +1 -0
- package/dist/src/routes/browse-source.js +79 -0
- package/dist/src/routes/browse-source.js.map +1 -0
- package/dist/src/routes/metadata.d.ts +3 -0
- package/dist/src/routes/metadata.d.ts.map +1 -0
- package/dist/src/routes/metadata.js +107 -0
- package/dist/src/routes/metadata.js.map +1 -0
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +4 -0
- package/dist/src/server.js.map +1 -1
- package/dist/starting-doc/.annotations.json +1 -13
- package/dist/starting-doc/.metadata.json +1 -0
- 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
|
+
})();
|