living-documentation 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +329 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +62 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/frontend/admin.html +1073 -0
- package/dist/src/frontend/annotations.js +546 -0
- package/dist/src/frontend/boot.js +90 -0
- package/dist/src/frontend/config.js +19 -0
- package/dist/src/frontend/dark-mode.js +20 -0
- package/dist/src/frontend/diagram/alignment.js +161 -0
- package/dist/src/frontend/diagram/clipboard.js +172 -0
- package/dist/src/frontend/diagram/constants.js +109 -0
- package/dist/src/frontend/diagram/debug.js +43 -0
- package/dist/src/frontend/diagram/edge-panel.js +260 -0
- package/dist/src/frontend/diagram/edge-rendering.js +12 -0
- package/dist/src/frontend/diagram/grid.js +78 -0
- package/dist/src/frontend/diagram/groups.js +102 -0
- package/dist/src/frontend/diagram/history.js +153 -0
- package/dist/src/frontend/diagram/image-name-modal.js +48 -0
- package/dist/src/frontend/diagram/image-upload.js +36 -0
- package/dist/src/frontend/diagram/label-editor.js +115 -0
- package/dist/src/frontend/diagram/link-panel.js +144 -0
- package/dist/src/frontend/diagram/main.js +299 -0
- package/dist/src/frontend/diagram/network.js +1473 -0
- package/dist/src/frontend/diagram/node-panel.js +267 -0
- package/dist/src/frontend/diagram/node-rendering.js +773 -0
- package/dist/src/frontend/diagram/persistence.js +161 -0
- package/dist/src/frontend/diagram/ports.js +386 -0
- package/dist/src/frontend/diagram/selection-overlay.js +336 -0
- package/dist/src/frontend/diagram/state.js +39 -0
- package/dist/src/frontend/diagram/t.js +3 -0
- package/dist/src/frontend/diagram/toast.js +21 -0
- package/dist/src/frontend/diagram/unlock-hold.js +182 -0
- package/dist/src/frontend/diagram/zoom.js +20 -0
- package/dist/src/frontend/diagram-link-modal.js +137 -0
- package/dist/src/frontend/diagram.html +1279 -0
- package/dist/src/frontend/documents.js +373 -0
- package/dist/src/frontend/export.js +338 -0
- package/dist/src/frontend/i18n/en.json +406 -0
- package/dist/src/frontend/i18n/fr.json +406 -0
- package/dist/src/frontend/i18n.js +32 -0
- package/dist/src/frontend/image-paste.js +101 -0
- package/dist/src/frontend/index.html +2314 -0
- package/dist/src/frontend/misc.js +25 -0
- package/dist/src/frontend/new-doc-modal.js +260 -0
- package/dist/src/frontend/new-folder-modal.js +174 -0
- package/dist/src/frontend/search.js +157 -0
- package/dist/src/frontend/sidebar-helpers.js +58 -0
- package/dist/src/frontend/sidebar.js +182 -0
- package/dist/src/frontend/snippet-detect.js +25 -0
- package/dist/src/frontend/snippet-table.js +85 -0
- package/dist/src/frontend/snippet-tree.js +94 -0
- package/dist/src/frontend/snippets.js +534 -0
- package/dist/src/frontend/state.js +28 -0
- package/dist/src/frontend/utils.js +21 -0
- package/dist/src/frontend/vendor/wordcloud2.js +1187 -0
- package/dist/src/frontend/wordcloud.js +693 -0
- package/dist/src/lib/config.d.ts +17 -0
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +79 -0
- package/dist/src/lib/config.js.map +1 -0
- package/dist/src/lib/parser.d.ts +11 -0
- package/dist/src/lib/parser.d.ts.map +1 -0
- package/dist/src/lib/parser.js +111 -0
- package/dist/src/lib/parser.js.map +1 -0
- package/dist/src/mcp/server.d.ts +3 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +986 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools/diagrams.d.ts +44 -0
- package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
- package/dist/src/mcp/tools/diagrams.js +245 -0
- package/dist/src/mcp/tools/diagrams.js.map +1 -0
- package/dist/src/mcp/tools/documents.d.ts +26 -0
- package/dist/src/mcp/tools/documents.d.ts.map +1 -0
- package/dist/src/mcp/tools/documents.js +127 -0
- package/dist/src/mcp/tools/documents.js.map +1 -0
- package/dist/src/mcp/tools/source.d.ts +29 -0
- package/dist/src/mcp/tools/source.d.ts.map +1 -0
- package/dist/src/mcp/tools/source.js +200 -0
- package/dist/src/mcp/tools/source.js.map +1 -0
- package/dist/src/routes/annotations.d.ts +3 -0
- package/dist/src/routes/annotations.d.ts.map +1 -0
- package/dist/src/routes/annotations.js +83 -0
- package/dist/src/routes/annotations.js.map +1 -0
- package/dist/src/routes/browse.d.ts +3 -0
- package/dist/src/routes/browse.d.ts.map +1 -0
- package/dist/src/routes/browse.js +75 -0
- package/dist/src/routes/browse.js.map +1 -0
- package/dist/src/routes/config.d.ts +3 -0
- package/dist/src/routes/config.d.ts.map +1 -0
- package/dist/src/routes/config.js +97 -0
- package/dist/src/routes/config.js.map +1 -0
- package/dist/src/routes/diagrams.d.ts +3 -0
- package/dist/src/routes/diagrams.d.ts.map +1 -0
- package/dist/src/routes/diagrams.js +69 -0
- package/dist/src/routes/diagrams.js.map +1 -0
- package/dist/src/routes/documents.d.ts +8 -0
- package/dist/src/routes/documents.d.ts.map +1 -0
- package/dist/src/routes/documents.js +332 -0
- package/dist/src/routes/documents.js.map +1 -0
- package/dist/src/routes/export.d.ts +3 -0
- package/dist/src/routes/export.d.ts.map +1 -0
- package/dist/src/routes/export.js +277 -0
- package/dist/src/routes/export.js.map +1 -0
- package/dist/src/routes/images.d.ts +3 -0
- package/dist/src/routes/images.d.ts.map +1 -0
- package/dist/src/routes/images.js +49 -0
- package/dist/src/routes/images.js.map +1 -0
- package/dist/src/routes/wordcloud.d.ts +3 -0
- package/dist/src/routes/wordcloud.d.ts.map +1 -0
- package/dist/src/routes/wordcloud.js +95 -0
- package/dist/src/routes/wordcloud.js.map +1 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +76 -0
- package/dist/src/server.js.map +1 -0
- package/dist/starting-doc/.annotations.json +3 -0
- package/dist/starting-doc/.diagrams.json +1884 -0
- package/dist/starting-doc/.living-doc.json +39 -0
- package/dist/starting-doc/1_tutorial/2026_04_11_13_25_[General]_crer_vos_dossiers.md +16 -0
- package/dist/starting-doc/1_tutorial/2026_04_11_18_58_[General]_creer_un_document_dans_un_dossier.md +9 -0
- package/dist/starting-doc/1_tutorial/2026_04_12_09_00_[General]_editer_et_sauvegarder.md +39 -0
- package/dist/starting-doc/1_tutorial/2026_04_12_10_00_[General]_utiliser_les_snippets.md +71 -0
- package/dist/starting-doc/2026_04_08_20_52_[General]_welcome.md +17 -0
- package/dist/starting-doc/2026_04_11_12_55_[General]_premiers_pas.md +271 -0
- package/dist/starting-doc/2_guide/2026_04_08_00_04_[DOCUMENT]_utilisation_des_images_plein_ecran_lien_clickable.md +40 -0
- package/dist/starting-doc/2_guide/2026_04_08_23_38_[Configuration]_demarrage_de_living_documentation.md +32 -0
- package/dist/starting-doc/2_guide/2026_04_09_09_00_[NAVIGATION]_recherche_plein_texte.md +65 -0
- package/dist/starting-doc/2_guide/2026_04_09_10_00_[EXPORT]_exporter_en_pdf.md +43 -0
- package/dist/starting-doc/2_guide/2026_04_09_11_00_[Configuration]_configurer_le_panneau_admin.md +55 -0
- package/dist/starting-doc/2_guide/2026_04_09_12_00_[Configuration]_extra_files.md +68 -0
- package/dist/starting-doc/2_guide/2026_04_09_13_00_[WORDCLOUD]_word_cloud.md +54 -0
- package/dist/starting-doc/2_guide/2026_04_09_14_00_[DIAGRAM]_creer_et_lier_un_diagramme.md +77 -0
- package/dist/starting-doc/3_concept/2026_04_08_20_58_[DOCUMENTING]_ADRS.md +20 -0
- package/dist/starting-doc/3_concept/2026_04_08_22_15_[DOCUMENTING]_living_documentation.md +17 -0
- package/dist/starting-doc/3_concept/2026_04_08_22_46_[METHODOLOGY]_diataxis_architecture_du_contenu.md +16 -0
- package/dist/starting-doc/4_reference/2026_04_08_23_14_[FUNDAMENTALS]_the_living_documentation_tool.md +41 -0
- package/dist/starting-doc/4_reference/2026_04_09_01_00_[REFERENCE]_raccourcis_clavier.md +61 -0
- package/dist/starting-doc/4_reference/2026_04_09_02_00_[REFERENCE]_tokens_pattern_nommage.md +75 -0
- package/dist/starting-doc/4_reference/2026_04_09_03_00_[REFERENCE]_types_de_snippets.md +68 -0
- package/dist/starting-doc/4_reference/2026_04_11_17_31_[FUNDAMENTALS]_architecturer_une_documentation.md +12 -0
- package/dist/starting-doc/4_reference/2026_04_12_14_07_[FUNDAMENTALS]_dossiers_et_catgories.md +89 -0
- package/dist/starting-doc/images/admin_screenshot.png +0 -0
- package/dist/starting-doc/images/ajout-document.png +0 -0
- package/dist/starting-doc/images/ajouter-document-categorie.png +0 -0
- package/dist/starting-doc/images/ajouter_un_document_dans_un_dossier.png +0 -0
- package/dist/starting-doc/images/architecturer_une_documentation_reference.png +0 -0
- package/dist/starting-doc/images/cr_er_un_document.png +0 -0
- package/dist/starting-doc/images/creation-nouveau-dossier.png +0 -0
- package/dist/starting-doc/images/creer-document-context-engineering.png +0 -0
- package/dist/starting-doc/images/creer-dossier-only-tutoriel.png +0 -0
- package/dist/starting-doc/images/creer-dossier-tutoriel.png +0 -0
- package/dist/starting-doc/images/creer-dossiers-done.png +0 -0
- package/dist/starting-doc/images/creer-un-document.png +0 -0
- package/dist/starting-doc/images/creer-vos-dossiers-tutoriel.png +0 -0
- package/dist/starting-doc/images/creer-vos-dossiers.png +0 -0
- package/dist/starting-doc/images/decouverte_adrs.png +0 -0
- package/dist/starting-doc/images/diataxis.png +0 -0
- package/dist/starting-doc/images/diataxis_callout.png +0 -0
- package/dist/starting-doc/images/document-cree.png +0 -0
- package/dist/starting-doc/images/liens_snippets.png +0 -0
- package/dist/starting-doc/images/living_documentation.png +0 -0
- package/dist/starting-doc/images/npm_logo.png +0 -0
- package/dist/starting-doc/images/popup-creer-document.png +0 -0
- package/dist/starting-doc/images/popup-creer-dossier.png +0 -0
- package/dist/starting-doc/images/popup-dossier-cree.png +0 -0
- package/dist/starting-doc/images/quatre-dossiers-crees.png +0 -0
- package/dist/starting-doc/images/screenshot-living-doc.png +0 -0
- package/dist/starting-doc/images/the_living_documentation_tool.png +0 -0
- package/package.json +49 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// ── Misc viewer helpers ─────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
function toggleFullWidth() {
|
|
4
|
+
const article = document.getElementById("doc-view");
|
|
5
|
+
const btn = document.getElementById("full-width-btn");
|
|
6
|
+
const isWide = article.classList.toggle("max-w-none");
|
|
7
|
+
article.classList.toggle("max-w-4xl", !isWide);
|
|
8
|
+
article.classList.toggle("mx-auto", !isWide);
|
|
9
|
+
btn.textContent = isWide ? window.t('doc.full_width_narrow_btn') : window.t('doc.full_width_btn');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function copyLink() {
|
|
13
|
+
navigator.clipboard.writeText(location.href).then(() => {
|
|
14
|
+
const btn = document.getElementById("copy-link-btn");
|
|
15
|
+
const orig = btn.innerHTML;
|
|
16
|
+
btn.textContent = window.t('doc.copied');
|
|
17
|
+
setTimeout(() => {
|
|
18
|
+
btn.innerHTML = orig;
|
|
19
|
+
}, 1800);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function exportPDF() {
|
|
24
|
+
window.print();
|
|
25
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// ── New Document modal ──────────────────────────────────────────────────────
|
|
2
|
+
// Depends on globals from state.js (currentDocId, allDocs), documents.js
|
|
3
|
+
// (loadDocuments, openDocument) and utils.js (esc).
|
|
4
|
+
|
|
5
|
+
let _newDocBrowseCurrent = null;
|
|
6
|
+
let _newDocBrowseParent = null;
|
|
7
|
+
let _newDocSelectedFolder = "";
|
|
8
|
+
let _newDocDocsFolder = "";
|
|
9
|
+
let _newDocPattern = "YYYY_MM_DD_HH_mm_[Category]_title";
|
|
10
|
+
|
|
11
|
+
async function openNewDocModal() {
|
|
12
|
+
try {
|
|
13
|
+
const cfg = await fetch("/api/config").then((r) => r.json());
|
|
14
|
+
_newDocDocsFolder = cfg.docsFolder || "";
|
|
15
|
+
_newDocPattern =
|
|
16
|
+
cfg.filenamePattern || "YYYY_MM_DD_HH_mm_[Category]_title";
|
|
17
|
+
} catch {
|
|
18
|
+
_newDocDocsFolder = "";
|
|
19
|
+
_newDocPattern = "YYYY_MM_DD_HH_mm_[Category]_title";
|
|
20
|
+
}
|
|
21
|
+
// Pre-fill from currently open document if any
|
|
22
|
+
const currentDoc =
|
|
23
|
+
currentDocId && allDocs.find((d) => d.id === currentDocId);
|
|
24
|
+
const prefillCategory =
|
|
25
|
+
(currentDoc && currentDoc.category) || "General";
|
|
26
|
+
// Derive folder from currentDocId (encoded relative path, e.g. "1_tutorial%2Fsome_file")
|
|
27
|
+
// For extra files (absolute paths), skip.
|
|
28
|
+
let prefillFolder = "";
|
|
29
|
+
if (currentDocId) {
|
|
30
|
+
const decodedId = decodeURIComponent(currentDocId);
|
|
31
|
+
if (!decodedId.startsWith("/")) {
|
|
32
|
+
const segments = decodedId.split("/");
|
|
33
|
+
if (segments.length > 1) {
|
|
34
|
+
prefillFolder = segments.slice(0, -1).join("/");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const prefillFolderAbs = prefillFolder
|
|
39
|
+
? _newDocDocsFolder + "/" + prefillFolder
|
|
40
|
+
: "";
|
|
41
|
+
|
|
42
|
+
_newDocSelectedFolder = prefillFolder;
|
|
43
|
+
_newDocBrowseCurrent = prefillFolderAbs || null;
|
|
44
|
+
_newDocBrowseParent = null;
|
|
45
|
+
document.getElementById("new-doc-title").value = "";
|
|
46
|
+
document.getElementById("new-doc-category").value = prefillCategory;
|
|
47
|
+
document.getElementById("new-doc-folder-display").textContent =
|
|
48
|
+
prefillFolder ? "/" + prefillFolder : "/ (root)";
|
|
49
|
+
document.getElementById("new-doc-browser").classList.add("hidden");
|
|
50
|
+
document.getElementById("new-doc-new-folder-name").value = "";
|
|
51
|
+
document.getElementById("new-doc-error").classList.add("hidden");
|
|
52
|
+
const createBtn = document.getElementById("new-doc-create-btn");
|
|
53
|
+
createBtn.disabled = false;
|
|
54
|
+
createBtn.textContent = window.t('common.create');
|
|
55
|
+
newDocUpdatePreview();
|
|
56
|
+
document.getElementById("new-doc-modal").classList.remove("hidden");
|
|
57
|
+
setTimeout(() => document.getElementById("new-doc-title").focus(), 50);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function closeNewDocModal() {
|
|
61
|
+
document.getElementById("new-doc-modal").classList.add("hidden");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function newDocToggleBrowser() {
|
|
65
|
+
const browser = document.getElementById("new-doc-browser");
|
|
66
|
+
const isHidden = browser.classList.toggle("hidden");
|
|
67
|
+
if (!isHidden)
|
|
68
|
+
newDocLoadBrowse(_newDocBrowseCurrent || _newDocDocsFolder);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function newDocLoadBrowse(dirPath) {
|
|
72
|
+
const list = document.getElementById("new-doc-browse-list");
|
|
73
|
+
list.innerHTML =
|
|
74
|
+
`<p class="px-3 py-4 text-xs text-gray-400 text-center">${window.t('common.loading')}</p>`;
|
|
75
|
+
try {
|
|
76
|
+
const data = await fetch(
|
|
77
|
+
"/api/browse?path=" + encodeURIComponent(dirPath),
|
|
78
|
+
).then((r) => r.json());
|
|
79
|
+
_newDocBrowseCurrent = data.current;
|
|
80
|
+
_newDocBrowseParent = data.parent;
|
|
81
|
+
document.getElementById("new-doc-browse-path").textContent =
|
|
82
|
+
data.current;
|
|
83
|
+
const atRoot = data.current === _newDocDocsFolder;
|
|
84
|
+
document.getElementById("new-doc-browse-up").disabled = atRoot;
|
|
85
|
+
|
|
86
|
+
const rows = data.dirs.map(
|
|
87
|
+
(dir) =>
|
|
88
|
+
`<div class="flex items-center hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
89
|
+
<button data-path="${esc(dir.path)}" onclick="newDocLoadBrowse(this.dataset.path)"
|
|
90
|
+
class="flex-1 flex items-center gap-2 px-3 py-2 text-sm text-left">
|
|
91
|
+
<span class="text-gray-400 shrink-0">📁</span>
|
|
92
|
+
<span class="text-gray-700 dark:text-gray-300 truncate">${esc(dir.name)}</span>
|
|
93
|
+
</button>
|
|
94
|
+
<button data-path="${esc(dir.path)}" onclick="newDocSelectFolder(this.dataset.path)"
|
|
95
|
+
title="${window.t('modal.new_doc.use_folder_btn')}"
|
|
96
|
+
class="shrink-0 text-blue-400 hover:text-blue-600 px-3 py-2 text-sm transition-colors">✓</button>
|
|
97
|
+
</div>`,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const selectBtn = `<button onclick="newDocSelectCurrentFolder()"
|
|
101
|
+
class="w-full px-3 py-2 text-xs text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-950/30 text-left font-medium border-t border-gray-100 dark:border-gray-800">
|
|
102
|
+
${window.t('modal.new_doc.use_folder_btn')}
|
|
103
|
+
</button>`;
|
|
104
|
+
|
|
105
|
+
list.innerHTML =
|
|
106
|
+
(rows.length
|
|
107
|
+
? rows.join("")
|
|
108
|
+
: `<p class="px-3 py-3 text-xs text-gray-400 text-center">${window.t('modal.new_doc.no_subfolders')}</p>`) +
|
|
109
|
+
selectBtn;
|
|
110
|
+
} catch {
|
|
111
|
+
list.innerHTML =
|
|
112
|
+
`<p class="px-3 py-4 text-xs text-red-400 text-center">${window.t('common.cannot_read_dir')}</p>`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function newDocBrowseUp() {
|
|
117
|
+
if (_newDocBrowseCurrent !== _newDocDocsFolder && _newDocBrowseParent) {
|
|
118
|
+
newDocLoadBrowse(_newDocBrowseParent);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _newDocAbsToRel(absPath) {
|
|
123
|
+
const base = _newDocDocsFolder;
|
|
124
|
+
if (absPath === base) return "";
|
|
125
|
+
if (absPath.startsWith(base + "/"))
|
|
126
|
+
return absPath.slice(base.length + 1);
|
|
127
|
+
return absPath;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function newDocSelectFolder(absPath) {
|
|
131
|
+
_newDocSelectedFolder = _newDocAbsToRel(absPath);
|
|
132
|
+
_newDocBrowseCurrent = absPath;
|
|
133
|
+
document.getElementById("new-doc-folder-display").textContent =
|
|
134
|
+
_newDocSelectedFolder ? "/" + _newDocSelectedFolder : "/ (root)";
|
|
135
|
+
document.getElementById("new-doc-browser").classList.add("hidden");
|
|
136
|
+
newDocUpdatePreview();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function newDocSelectCurrentFolder() {
|
|
140
|
+
newDocSelectFolder(_newDocBrowseCurrent || _newDocDocsFolder);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function newDocCreateFolder() {
|
|
144
|
+
const name = document
|
|
145
|
+
.getElementById("new-doc-new-folder-name")
|
|
146
|
+
.value.trim();
|
|
147
|
+
if (!name) return;
|
|
148
|
+
const parent = _newDocBrowseCurrent || _newDocDocsFolder;
|
|
149
|
+
const newRelPath =
|
|
150
|
+
(_newDocAbsToRel(parent) ? _newDocAbsToRel(parent) + "/" : "") + name;
|
|
151
|
+
_newDocSelectedFolder = newRelPath;
|
|
152
|
+
document.getElementById("new-doc-folder-display").textContent =
|
|
153
|
+
"/" + newRelPath;
|
|
154
|
+
document.getElementById("new-doc-new-folder-name").value = "";
|
|
155
|
+
document.getElementById("new-doc-browser").classList.add("hidden");
|
|
156
|
+
newDocUpdatePreview();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function newDocUpdatePreview() {
|
|
160
|
+
const title = document.getElementById("new-doc-title").value.trim();
|
|
161
|
+
const category =
|
|
162
|
+
document.getElementById("new-doc-category").value.trim() || "General";
|
|
163
|
+
const previewEl = document.getElementById("new-doc-filename-preview");
|
|
164
|
+
|
|
165
|
+
if (!title) {
|
|
166
|
+
previewEl.textContent = window.t('modal.new_doc.title_placeholder');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const now = new Date();
|
|
171
|
+
const year = now.getFullYear();
|
|
172
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
173
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
174
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
175
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
176
|
+
const titleSlug =
|
|
177
|
+
title
|
|
178
|
+
.toLowerCase()
|
|
179
|
+
.replace(/\s+/g, "_")
|
|
180
|
+
.replace(/[^a-z0-9_]/g, "")
|
|
181
|
+
.replace(/_+/g, "_")
|
|
182
|
+
.replace(/^_|_$/g, "") || "document";
|
|
183
|
+
|
|
184
|
+
const filename =
|
|
185
|
+
_newDocPattern
|
|
186
|
+
.replace("YYYY", year)
|
|
187
|
+
.replace("MM", month)
|
|
188
|
+
.replace("DD", day)
|
|
189
|
+
.replace("HH", hours)
|
|
190
|
+
.replace("mm", minutes)
|
|
191
|
+
.replace(/\[Category\]/i, `[${category}]`)
|
|
192
|
+
.replace(
|
|
193
|
+
/(?<![a-z0-9])(?:title_words|title)(?![a-z0-9])/i,
|
|
194
|
+
titleSlug,
|
|
195
|
+
) + ".md";
|
|
196
|
+
|
|
197
|
+
previewEl.textContent = _newDocSelectedFolder
|
|
198
|
+
? _newDocSelectedFolder + "/" + filename
|
|
199
|
+
: filename;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function createNewDocument() {
|
|
203
|
+
const title = document.getElementById("new-doc-title").value.trim();
|
|
204
|
+
const category =
|
|
205
|
+
document.getElementById("new-doc-category").value.trim() || "General";
|
|
206
|
+
const errorEl = document.getElementById("new-doc-error");
|
|
207
|
+
const btn = document.getElementById("new-doc-create-btn");
|
|
208
|
+
|
|
209
|
+
if (!title) {
|
|
210
|
+
errorEl.textContent = window.t('modal.new_doc.error_empty_title');
|
|
211
|
+
errorEl.classList.remove("hidden");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
errorEl.classList.add("hidden");
|
|
216
|
+
btn.disabled = true;
|
|
217
|
+
btn.textContent = window.t('modal.new_folder.creating_btn');
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const res = await fetch("/api/documents", {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: { "Content-Type": "application/json" },
|
|
223
|
+
body: JSON.stringify({
|
|
224
|
+
title,
|
|
225
|
+
category,
|
|
226
|
+
folder: _newDocSelectedFolder,
|
|
227
|
+
}),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (!res.ok) {
|
|
231
|
+
const data = await res.json();
|
|
232
|
+
throw new Error(data.error || "Creation failed");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const doc = await res.json();
|
|
236
|
+
closeNewDocModal();
|
|
237
|
+
await loadDocuments();
|
|
238
|
+
openDocument(doc.id);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
errorEl.textContent = window.t('common.error_prefix') + err.message;
|
|
241
|
+
errorEl.classList.remove("hidden");
|
|
242
|
+
btn.disabled = false;
|
|
243
|
+
btn.textContent = window.t('common.create');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Allow Enter key in title/category to submit
|
|
248
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
249
|
+
["new-doc-title", "new-doc-category"].forEach((id) => {
|
|
250
|
+
document.getElementById(id)?.addEventListener("keydown", (e) => {
|
|
251
|
+
if (e.key === "Enter") createNewDocument();
|
|
252
|
+
if (e.key === "Escape") closeNewDocModal();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
document
|
|
256
|
+
.getElementById("new-doc-new-folder-name")
|
|
257
|
+
?.addEventListener("keydown", (e) => {
|
|
258
|
+
if (e.key === "Enter") newDocCreateFolder();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// ── New Folder modal ────────────────────────────────────────────────────────
|
|
2
|
+
// Depends on globals from state.js (currentDocId) and documents.js
|
|
3
|
+
// (loadDocuments).
|
|
4
|
+
|
|
5
|
+
let _newFolderDocsFolder = "";
|
|
6
|
+
let _newFolderBrowseCurrent = null;
|
|
7
|
+
let _newFolderBrowseParent = null;
|
|
8
|
+
let _newFolderSelectedPath = "";
|
|
9
|
+
|
|
10
|
+
async function openNewFolderModal() {
|
|
11
|
+
try {
|
|
12
|
+
const cfg = await fetch("/api/config").then((r) => r.json());
|
|
13
|
+
_newFolderDocsFolder = cfg.docsFolder || "";
|
|
14
|
+
} catch {
|
|
15
|
+
_newFolderDocsFolder = "";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Pre-fill location from currently open document
|
|
19
|
+
let prefillFolder = "";
|
|
20
|
+
if (currentDocId) {
|
|
21
|
+
const decodedId = decodeURIComponent(currentDocId);
|
|
22
|
+
if (!decodedId.startsWith("/")) {
|
|
23
|
+
const segments = decodedId.split("/");
|
|
24
|
+
if (segments.length > 1)
|
|
25
|
+
prefillFolder = segments.slice(0, -1).join("/");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_newFolderSelectedPath = prefillFolder
|
|
30
|
+
? _newFolderDocsFolder + "/" + prefillFolder
|
|
31
|
+
: _newFolderDocsFolder;
|
|
32
|
+
_newFolderBrowseCurrent = _newFolderSelectedPath;
|
|
33
|
+
_newFolderBrowseParent = null;
|
|
34
|
+
|
|
35
|
+
document.getElementById("new-folder-name").value = "";
|
|
36
|
+
document.getElementById("new-folder-location-display").textContent =
|
|
37
|
+
prefillFolder ? "/" + prefillFolder : "/ (root)";
|
|
38
|
+
document.getElementById("new-folder-browser").classList.add("hidden");
|
|
39
|
+
document.getElementById("new-folder-error").classList.add("hidden");
|
|
40
|
+
const btn = document.getElementById("new-folder-create-btn");
|
|
41
|
+
btn.disabled = false;
|
|
42
|
+
btn.textContent = window.t('modal.new_folder.create_btn');
|
|
43
|
+
newFolderUpdatePreview();
|
|
44
|
+
document.getElementById("new-folder-modal").classList.remove("hidden");
|
|
45
|
+
setTimeout(
|
|
46
|
+
() => document.getElementById("new-folder-name").focus(),
|
|
47
|
+
50,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function closeNewFolderModal() {
|
|
52
|
+
document.getElementById("new-folder-modal").classList.add("hidden");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function newFolderToggleBrowser() {
|
|
56
|
+
const browser = document.getElementById("new-folder-browser");
|
|
57
|
+
const isHidden = browser.classList.toggle("hidden");
|
|
58
|
+
if (!isHidden)
|
|
59
|
+
newFolderLoadBrowse(_newFolderBrowseCurrent || _newFolderDocsFolder);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function newFolderLoadBrowse(dirPath) {
|
|
63
|
+
const list = document.getElementById("new-folder-browse-list");
|
|
64
|
+
list.innerHTML =
|
|
65
|
+
`<p class="px-3 py-4 text-xs text-gray-400 text-center">${window.t('common.loading')}</p>`;
|
|
66
|
+
try {
|
|
67
|
+
const data = await fetch(
|
|
68
|
+
"/api/browse?path=" + encodeURIComponent(dirPath),
|
|
69
|
+
).then((r) => r.json());
|
|
70
|
+
_newFolderBrowseCurrent = data.current;
|
|
71
|
+
_newFolderBrowseParent = data.parent;
|
|
72
|
+
document.getElementById("new-folder-browse-path").textContent =
|
|
73
|
+
data.current;
|
|
74
|
+
const atRoot = data.current === _newFolderDocsFolder;
|
|
75
|
+
document.getElementById("new-folder-browse-up").disabled = atRoot;
|
|
76
|
+
|
|
77
|
+
list.innerHTML = data.dirs.length
|
|
78
|
+
? data.dirs
|
|
79
|
+
.map(
|
|
80
|
+
(dir) => `
|
|
81
|
+
<div class="flex items-center hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
82
|
+
<button data-path="${esc(dir.path)}" onclick="newFolderLoadBrowse(this.dataset.path)"
|
|
83
|
+
class="flex-1 flex items-center gap-2 px-3 py-2 text-sm text-left">
|
|
84
|
+
<span class="text-gray-400 shrink-0">📁</span>
|
|
85
|
+
<span class="truncate text-gray-700 dark:text-gray-300">${esc(dir.name)}</span>
|
|
86
|
+
</button>
|
|
87
|
+
<button data-path="${esc(dir.path)}" onclick="newFolderSelectFolder(this.dataset.path)"
|
|
88
|
+
class="px-3 py-2 text-xs text-blue-600 dark:text-blue-400 hover:underline shrink-0">${window.t('modal.new_folder.browse_select_btn')}</button>
|
|
89
|
+
</div>`,
|
|
90
|
+
)
|
|
91
|
+
.join("")
|
|
92
|
+
: `<p class="px-3 py-3 text-xs text-gray-400 text-center">${window.t('modal.new_folder.no_subfolders')}</p>`;
|
|
93
|
+
} catch {
|
|
94
|
+
list.innerHTML =
|
|
95
|
+
`<p class="px-3 py-3 text-xs text-red-400 text-center">${window.t('modal.new_folder.error_loading')}</p>`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function newFolderBrowseUp() {
|
|
100
|
+
if (_newFolderBrowseParent) newFolderLoadBrowse(_newFolderBrowseParent);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function newFolderSelectCurrentLocation() {
|
|
104
|
+
newFolderSelectFolder(_newFolderBrowseCurrent || _newFolderDocsFolder);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function newFolderSelectFolder(absPath) {
|
|
108
|
+
_newFolderSelectedPath = absPath;
|
|
109
|
+
_newFolderBrowseCurrent = absPath;
|
|
110
|
+
const rel = absPath.startsWith(_newFolderDocsFolder + "/")
|
|
111
|
+
? absPath.slice(_newFolderDocsFolder.length)
|
|
112
|
+
: absPath === _newFolderDocsFolder
|
|
113
|
+
? ""
|
|
114
|
+
: absPath;
|
|
115
|
+
document.getElementById("new-folder-location-display").textContent = rel
|
|
116
|
+
? rel
|
|
117
|
+
: "/ (root)";
|
|
118
|
+
document.getElementById("new-folder-browser").classList.add("hidden");
|
|
119
|
+
newFolderUpdatePreview();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function newFolderUpdatePreview() {
|
|
123
|
+
const name = document.getElementById("new-folder-name").value.trim();
|
|
124
|
+
const previewEl = document.getElementById("new-folder-preview");
|
|
125
|
+
if (!name) {
|
|
126
|
+
previewEl.textContent = window.t('modal.new_folder.enter_name');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const base = _newFolderSelectedPath || _newFolderDocsFolder;
|
|
130
|
+
const rel = base.startsWith(_newFolderDocsFolder)
|
|
131
|
+
? base.slice(_newFolderDocsFolder.length).replace(/^\//, "")
|
|
132
|
+
: "";
|
|
133
|
+
previewEl.textContent = (rel ? rel + "/" : "") + name;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function createNewFolder() {
|
|
137
|
+
const name = document.getElementById("new-folder-name").value.trim();
|
|
138
|
+
const errEl = document.getElementById("new-folder-error");
|
|
139
|
+
errEl.classList.add("hidden");
|
|
140
|
+
|
|
141
|
+
if (!name) {
|
|
142
|
+
errEl.textContent = window.t('modal.new_folder.error_empty');
|
|
143
|
+
errEl.classList.remove("hidden");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (!/^[a-zA-Z0-9_\-. ]+$/.test(name)) {
|
|
147
|
+
errEl.textContent = window.t('modal.new_folder.error_invalid_chars');
|
|
148
|
+
errEl.classList.remove("hidden");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const base = _newFolderSelectedPath || _newFolderDocsFolder;
|
|
153
|
+
const fullPath = base.endsWith("/") ? base + name : base + "/" + name;
|
|
154
|
+
|
|
155
|
+
const btn = document.getElementById("new-folder-create-btn");
|
|
156
|
+
btn.disabled = true;
|
|
157
|
+
btn.textContent = window.t('modal.new_folder.creating_btn');
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const res = await fetch("/api/browse/mkdir", {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: { "Content-Type": "application/json" },
|
|
163
|
+
body: JSON.stringify({ path: fullPath }),
|
|
164
|
+
});
|
|
165
|
+
if (!res.ok) throw new Error(await res.text());
|
|
166
|
+
closeNewFolderModal();
|
|
167
|
+
await loadDocuments();
|
|
168
|
+
} catch (err) {
|
|
169
|
+
btn.disabled = false;
|
|
170
|
+
btn.textContent = window.t('modal.new_folder.create_btn');
|
|
171
|
+
errEl.textContent = window.t('common.error_prefix') + err.message;
|
|
172
|
+
errEl.classList.remove("hidden");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// ── Search + result highlighting ─────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
let searchTimer = null;
|
|
4
|
+
|
|
5
|
+
function setupSearch() {
|
|
6
|
+
["header-search", "sidebar-search"].forEach((id) => {
|
|
7
|
+
const el = document.getElementById(id);
|
|
8
|
+
if (!el) return;
|
|
9
|
+
el.addEventListener("input", (e) => {
|
|
10
|
+
const q = e.target.value.trim();
|
|
11
|
+
// Sync the other input
|
|
12
|
+
["header-search", "sidebar-search"].forEach((oid) => {
|
|
13
|
+
if (oid !== id) {
|
|
14
|
+
const other = document.getElementById(oid);
|
|
15
|
+
if (other) other.value = q;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
clearTimeout(searchTimer);
|
|
19
|
+
if (!q) {
|
|
20
|
+
searchQuery = "";
|
|
21
|
+
searchResults = null;
|
|
22
|
+
renderSidebar(allDocs);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
searchQuery = q;
|
|
26
|
+
// Immediate client-side filter for snappy UX
|
|
27
|
+
const local = allDocs.filter(
|
|
28
|
+
(d) =>
|
|
29
|
+
d.title.toLowerCase().includes(q.toLowerCase()) ||
|
|
30
|
+
d.category.toLowerCase().includes(q.toLowerCase()),
|
|
31
|
+
);
|
|
32
|
+
searchResults = local;
|
|
33
|
+
renderSidebar(local);
|
|
34
|
+
// Then full-text search from server
|
|
35
|
+
searchTimer = setTimeout(() => doSearch(q), 350);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function doSearch(q) {
|
|
41
|
+
try {
|
|
42
|
+
const results = await fetch(
|
|
43
|
+
"/api/documents/search?q=" + encodeURIComponent(q),
|
|
44
|
+
).then((r) => r.json());
|
|
45
|
+
if (q !== searchQuery) return;
|
|
46
|
+
searchResults = results;
|
|
47
|
+
renderSidebar(results);
|
|
48
|
+
} catch {
|
|
49
|
+
/* ignore */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function highlightMatches(el, q) {
|
|
54
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
55
|
+
const nodes = [];
|
|
56
|
+
while (walker.nextNode()) nodes.push(walker.currentNode);
|
|
57
|
+
const re = new RegExp(
|
|
58
|
+
`(${q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`,
|
|
59
|
+
"gi",
|
|
60
|
+
);
|
|
61
|
+
let idx = 0;
|
|
62
|
+
nodes.forEach((node) => {
|
|
63
|
+
if (!node.textContent.toLowerCase().includes(q.toLowerCase())) return;
|
|
64
|
+
const span = document.createElement("span");
|
|
65
|
+
span.innerHTML = node.textContent.replace(
|
|
66
|
+
re,
|
|
67
|
+
(m) => `<mark id="match-${idx++}">${m}</mark>`,
|
|
68
|
+
);
|
|
69
|
+
node.parentNode.replaceChild(span, node);
|
|
70
|
+
});
|
|
71
|
+
// Build snippet list from inserted marks
|
|
72
|
+
const matches = [];
|
|
73
|
+
el.querySelectorAll("mark[id^='match-']").forEach((mark) => {
|
|
74
|
+
const block =
|
|
75
|
+
mark.closest("p, li, td, h1, h2, h3, h4, h5, h6, pre") ||
|
|
76
|
+
mark.parentElement;
|
|
77
|
+
const full = block.textContent.trim();
|
|
78
|
+
const pos = full.toLowerCase().indexOf(q.toLowerCase());
|
|
79
|
+
const start = Math.max(0, pos - 40);
|
|
80
|
+
const end = Math.min(full.length, pos + q.length + 40);
|
|
81
|
+
const snippet =
|
|
82
|
+
(start > 0 ? "…" : "") +
|
|
83
|
+
full.slice(start, end) +
|
|
84
|
+
(end < full.length ? "…" : "");
|
|
85
|
+
matches.push({ id: mark.id, snippet });
|
|
86
|
+
});
|
|
87
|
+
return matches;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildSearchNotice(matches, q) {
|
|
91
|
+
document.getElementById("search-notice-title").textContent =
|
|
92
|
+
window.t(matches.length === 1 ? 'search.notice_singular' : 'search.notice_plural')
|
|
93
|
+
.replace('{count}', matches.length).replace('{query}', q);
|
|
94
|
+
const list = document.getElementById("search-notice-list");
|
|
95
|
+
list.innerHTML = matches
|
|
96
|
+
.map(
|
|
97
|
+
(m, i) =>
|
|
98
|
+
`<li>
|
|
99
|
+
<button onclick="scrollToMatch('${m.id}')" data-match-id="${m.id}"
|
|
100
|
+
class="w-full text-left px-3 py-1.5 hover:bg-yellow-100 dark:hover:bg-yellow-900/30 transition-colors">
|
|
101
|
+
<span class="text-yellow-600 dark:text-yellow-500 font-mono text-xs mr-2">${i + 1}.</span
|
|
102
|
+
><span class="text-xs">${esc(m.snippet)}</span>
|
|
103
|
+
</button>
|
|
104
|
+
</li>`,
|
|
105
|
+
)
|
|
106
|
+
.join("");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function scrollToAnchor(anchorId) {
|
|
110
|
+
const container = document.getElementById("content-area");
|
|
111
|
+
const target = document.getElementById(anchorId);
|
|
112
|
+
if (!container || !target) return;
|
|
113
|
+
const stickyHeader = document.querySelector("#doc-view header");
|
|
114
|
+
const headerHeight = stickyHeader
|
|
115
|
+
? stickyHeader.getBoundingClientRect().height
|
|
116
|
+
: 0;
|
|
117
|
+
const targetTop = target.getBoundingClientRect().top;
|
|
118
|
+
const containerTop = container.getBoundingClientRect().top;
|
|
119
|
+
container.scrollTop += targetTop - containerTop - headerHeight - 8;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function scrollToMatch(id) {
|
|
123
|
+
const mark = document.getElementById(id);
|
|
124
|
+
const container = document.getElementById("content-area");
|
|
125
|
+
if (!mark || !container) return;
|
|
126
|
+
|
|
127
|
+
// Open any collapsed <details> ancestors so the match is visible
|
|
128
|
+
let ancestor = mark.parentElement;
|
|
129
|
+
while (ancestor && ancestor !== container) {
|
|
130
|
+
if (ancestor.tagName === "DETAILS" && !ancestor.open) {
|
|
131
|
+
ancestor.open = true;
|
|
132
|
+
}
|
|
133
|
+
ancestor = ancestor.parentElement;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Orange on the active mark, yellow on the rest
|
|
137
|
+
document
|
|
138
|
+
.querySelectorAll("mark.match-active")
|
|
139
|
+
.forEach((m) => m.classList.remove("match-active"));
|
|
140
|
+
mark.classList.add("match-active");
|
|
141
|
+
|
|
142
|
+
// Highlight active list item
|
|
143
|
+
document.querySelectorAll("#search-notice-list button").forEach((b) => {
|
|
144
|
+
b.classList.toggle("bg-orange-100", b.dataset.matchId === id);
|
|
145
|
+
b.classList.toggle("dark:bg-orange-900/30", b.dataset.matchId === id);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const stickyHeader = document.querySelector("#doc-view header");
|
|
149
|
+
const headerHeight = stickyHeader
|
|
150
|
+
? stickyHeader.getBoundingClientRect().height
|
|
151
|
+
: 0;
|
|
152
|
+
const markTop = mark.getBoundingClientRect().top;
|
|
153
|
+
const containerTop = container.getBoundingClientRect().top;
|
|
154
|
+
const targetOffset =
|
|
155
|
+
headerHeight + (container.clientHeight - headerHeight) / 3;
|
|
156
|
+
container.scrollTop += markTop - containerTop - targetOffset;
|
|
157
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// ── Sidebar helpers — tree traversal + annotation badges ─────────────────────
|
|
2
|
+
// Loaded as a classic script; all symbols are global.
|
|
3
|
+
// Depends on globals defined elsewhere: `annotationCounts` (index.html),
|
|
4
|
+
// `esc` (utils.js), `window.t` (i18n.js).
|
|
5
|
+
|
|
6
|
+
function countTreeDocs(node) {
|
|
7
|
+
let n = Object.values(node.categories).reduce(
|
|
8
|
+
(s, arr) => s + arr.length,
|
|
9
|
+
0,
|
|
10
|
+
);
|
|
11
|
+
for (const child of Object.values(node.children))
|
|
12
|
+
n += countTreeDocs(child);
|
|
13
|
+
return n;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function countTreeAnnotations(node) {
|
|
17
|
+
let n = 0;
|
|
18
|
+
for (const arr of Object.values(node.categories)) {
|
|
19
|
+
for (const doc of arr) n += annotationCounts[doc.id] || 0;
|
|
20
|
+
}
|
|
21
|
+
for (const child of Object.values(node.children))
|
|
22
|
+
n += countTreeAnnotations(child);
|
|
23
|
+
return n;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function countTreeAnnotatedDocs(node) {
|
|
27
|
+
let n = 0;
|
|
28
|
+
for (const arr of Object.values(node.categories)) {
|
|
29
|
+
for (const doc of arr) if (annotationCounts[doc.id] > 0) n += 1;
|
|
30
|
+
}
|
|
31
|
+
for (const child of Object.values(node.children))
|
|
32
|
+
n += countTreeAnnotatedDocs(child);
|
|
33
|
+
return n;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function annotationBadge(count) {
|
|
37
|
+
if (!count) return "";
|
|
38
|
+
const label = window.t
|
|
39
|
+
? window.t("sidebar.annotation_badge")
|
|
40
|
+
: "annotation";
|
|
41
|
+
return `<span title="${count} ${esc(label)}${count > 1 ? "s" : ""}"
|
|
42
|
+
class="inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5
|
|
43
|
+
rounded-full bg-yellow-300 dark:bg-yellow-400
|
|
44
|
+
text-[10px] font-bold text-yellow-900
|
|
45
|
+
border border-yellow-500 shadow-sm">${count}</span>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function annotatedDocsBadge(count) {
|
|
49
|
+
if (!count) return "";
|
|
50
|
+
const label = window.t
|
|
51
|
+
? window.t("sidebar.annotated_docs_badge")
|
|
52
|
+
: "document with annotations";
|
|
53
|
+
return `<span title="${count} ${esc(label)}${count > 1 ? "s" : ""}"
|
|
54
|
+
class="inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5
|
|
55
|
+
rounded-full bg-orange-400 dark:bg-orange-500
|
|
56
|
+
text-[10px] font-bold text-white
|
|
57
|
+
border border-orange-600 shadow-sm">${count}</span>`;
|
|
58
|
+
}
|