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.
Files changed (173) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +329 -0
  3. package/dist/bin/cli.d.ts +3 -0
  4. package/dist/bin/cli.d.ts.map +1 -0
  5. package/dist/bin/cli.js +62 -0
  6. package/dist/bin/cli.js.map +1 -0
  7. package/dist/src/frontend/admin.html +1073 -0
  8. package/dist/src/frontend/annotations.js +546 -0
  9. package/dist/src/frontend/boot.js +90 -0
  10. package/dist/src/frontend/config.js +19 -0
  11. package/dist/src/frontend/dark-mode.js +20 -0
  12. package/dist/src/frontend/diagram/alignment.js +161 -0
  13. package/dist/src/frontend/diagram/clipboard.js +172 -0
  14. package/dist/src/frontend/diagram/constants.js +109 -0
  15. package/dist/src/frontend/diagram/debug.js +43 -0
  16. package/dist/src/frontend/diagram/edge-panel.js +260 -0
  17. package/dist/src/frontend/diagram/edge-rendering.js +12 -0
  18. package/dist/src/frontend/diagram/grid.js +78 -0
  19. package/dist/src/frontend/diagram/groups.js +102 -0
  20. package/dist/src/frontend/diagram/history.js +153 -0
  21. package/dist/src/frontend/diagram/image-name-modal.js +48 -0
  22. package/dist/src/frontend/diagram/image-upload.js +36 -0
  23. package/dist/src/frontend/diagram/label-editor.js +115 -0
  24. package/dist/src/frontend/diagram/link-panel.js +144 -0
  25. package/dist/src/frontend/diagram/main.js +299 -0
  26. package/dist/src/frontend/diagram/network.js +1473 -0
  27. package/dist/src/frontend/diagram/node-panel.js +267 -0
  28. package/dist/src/frontend/diagram/node-rendering.js +773 -0
  29. package/dist/src/frontend/diagram/persistence.js +161 -0
  30. package/dist/src/frontend/diagram/ports.js +386 -0
  31. package/dist/src/frontend/diagram/selection-overlay.js +336 -0
  32. package/dist/src/frontend/diagram/state.js +39 -0
  33. package/dist/src/frontend/diagram/t.js +3 -0
  34. package/dist/src/frontend/diagram/toast.js +21 -0
  35. package/dist/src/frontend/diagram/unlock-hold.js +182 -0
  36. package/dist/src/frontend/diagram/zoom.js +20 -0
  37. package/dist/src/frontend/diagram-link-modal.js +137 -0
  38. package/dist/src/frontend/diagram.html +1279 -0
  39. package/dist/src/frontend/documents.js +373 -0
  40. package/dist/src/frontend/export.js +338 -0
  41. package/dist/src/frontend/i18n/en.json +406 -0
  42. package/dist/src/frontend/i18n/fr.json +406 -0
  43. package/dist/src/frontend/i18n.js +32 -0
  44. package/dist/src/frontend/image-paste.js +101 -0
  45. package/dist/src/frontend/index.html +2314 -0
  46. package/dist/src/frontend/misc.js +25 -0
  47. package/dist/src/frontend/new-doc-modal.js +260 -0
  48. package/dist/src/frontend/new-folder-modal.js +174 -0
  49. package/dist/src/frontend/search.js +157 -0
  50. package/dist/src/frontend/sidebar-helpers.js +58 -0
  51. package/dist/src/frontend/sidebar.js +182 -0
  52. package/dist/src/frontend/snippet-detect.js +25 -0
  53. package/dist/src/frontend/snippet-table.js +85 -0
  54. package/dist/src/frontend/snippet-tree.js +94 -0
  55. package/dist/src/frontend/snippets.js +534 -0
  56. package/dist/src/frontend/state.js +28 -0
  57. package/dist/src/frontend/utils.js +21 -0
  58. package/dist/src/frontend/vendor/wordcloud2.js +1187 -0
  59. package/dist/src/frontend/wordcloud.js +693 -0
  60. package/dist/src/lib/config.d.ts +17 -0
  61. package/dist/src/lib/config.d.ts.map +1 -0
  62. package/dist/src/lib/config.js +79 -0
  63. package/dist/src/lib/config.js.map +1 -0
  64. package/dist/src/lib/parser.d.ts +11 -0
  65. package/dist/src/lib/parser.d.ts.map +1 -0
  66. package/dist/src/lib/parser.js +111 -0
  67. package/dist/src/lib/parser.js.map +1 -0
  68. package/dist/src/mcp/server.d.ts +3 -0
  69. package/dist/src/mcp/server.d.ts.map +1 -0
  70. package/dist/src/mcp/server.js +986 -0
  71. package/dist/src/mcp/server.js.map +1 -0
  72. package/dist/src/mcp/tools/diagrams.d.ts +44 -0
  73. package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
  74. package/dist/src/mcp/tools/diagrams.js +245 -0
  75. package/dist/src/mcp/tools/diagrams.js.map +1 -0
  76. package/dist/src/mcp/tools/documents.d.ts +26 -0
  77. package/dist/src/mcp/tools/documents.d.ts.map +1 -0
  78. package/dist/src/mcp/tools/documents.js +127 -0
  79. package/dist/src/mcp/tools/documents.js.map +1 -0
  80. package/dist/src/mcp/tools/source.d.ts +29 -0
  81. package/dist/src/mcp/tools/source.d.ts.map +1 -0
  82. package/dist/src/mcp/tools/source.js +200 -0
  83. package/dist/src/mcp/tools/source.js.map +1 -0
  84. package/dist/src/routes/annotations.d.ts +3 -0
  85. package/dist/src/routes/annotations.d.ts.map +1 -0
  86. package/dist/src/routes/annotations.js +83 -0
  87. package/dist/src/routes/annotations.js.map +1 -0
  88. package/dist/src/routes/browse.d.ts +3 -0
  89. package/dist/src/routes/browse.d.ts.map +1 -0
  90. package/dist/src/routes/browse.js +75 -0
  91. package/dist/src/routes/browse.js.map +1 -0
  92. package/dist/src/routes/config.d.ts +3 -0
  93. package/dist/src/routes/config.d.ts.map +1 -0
  94. package/dist/src/routes/config.js +97 -0
  95. package/dist/src/routes/config.js.map +1 -0
  96. package/dist/src/routes/diagrams.d.ts +3 -0
  97. package/dist/src/routes/diagrams.d.ts.map +1 -0
  98. package/dist/src/routes/diagrams.js +69 -0
  99. package/dist/src/routes/diagrams.js.map +1 -0
  100. package/dist/src/routes/documents.d.ts +8 -0
  101. package/dist/src/routes/documents.d.ts.map +1 -0
  102. package/dist/src/routes/documents.js +332 -0
  103. package/dist/src/routes/documents.js.map +1 -0
  104. package/dist/src/routes/export.d.ts +3 -0
  105. package/dist/src/routes/export.d.ts.map +1 -0
  106. package/dist/src/routes/export.js +277 -0
  107. package/dist/src/routes/export.js.map +1 -0
  108. package/dist/src/routes/images.d.ts +3 -0
  109. package/dist/src/routes/images.d.ts.map +1 -0
  110. package/dist/src/routes/images.js +49 -0
  111. package/dist/src/routes/images.js.map +1 -0
  112. package/dist/src/routes/wordcloud.d.ts +3 -0
  113. package/dist/src/routes/wordcloud.d.ts.map +1 -0
  114. package/dist/src/routes/wordcloud.js +95 -0
  115. package/dist/src/routes/wordcloud.js.map +1 -0
  116. package/dist/src/server.d.ts +7 -0
  117. package/dist/src/server.d.ts.map +1 -0
  118. package/dist/src/server.js +76 -0
  119. package/dist/src/server.js.map +1 -0
  120. package/dist/starting-doc/.annotations.json +3 -0
  121. package/dist/starting-doc/.diagrams.json +1884 -0
  122. package/dist/starting-doc/.living-doc.json +39 -0
  123. package/dist/starting-doc/1_tutorial/2026_04_11_13_25_[General]_crer_vos_dossiers.md +16 -0
  124. package/dist/starting-doc/1_tutorial/2026_04_11_18_58_[General]_creer_un_document_dans_un_dossier.md +9 -0
  125. package/dist/starting-doc/1_tutorial/2026_04_12_09_00_[General]_editer_et_sauvegarder.md +39 -0
  126. package/dist/starting-doc/1_tutorial/2026_04_12_10_00_[General]_utiliser_les_snippets.md +71 -0
  127. package/dist/starting-doc/2026_04_08_20_52_[General]_welcome.md +17 -0
  128. package/dist/starting-doc/2026_04_11_12_55_[General]_premiers_pas.md +271 -0
  129. package/dist/starting-doc/2_guide/2026_04_08_00_04_[DOCUMENT]_utilisation_des_images_plein_ecran_lien_clickable.md +40 -0
  130. package/dist/starting-doc/2_guide/2026_04_08_23_38_[Configuration]_demarrage_de_living_documentation.md +32 -0
  131. package/dist/starting-doc/2_guide/2026_04_09_09_00_[NAVIGATION]_recherche_plein_texte.md +65 -0
  132. package/dist/starting-doc/2_guide/2026_04_09_10_00_[EXPORT]_exporter_en_pdf.md +43 -0
  133. package/dist/starting-doc/2_guide/2026_04_09_11_00_[Configuration]_configurer_le_panneau_admin.md +55 -0
  134. package/dist/starting-doc/2_guide/2026_04_09_12_00_[Configuration]_extra_files.md +68 -0
  135. package/dist/starting-doc/2_guide/2026_04_09_13_00_[WORDCLOUD]_word_cloud.md +54 -0
  136. package/dist/starting-doc/2_guide/2026_04_09_14_00_[DIAGRAM]_creer_et_lier_un_diagramme.md +77 -0
  137. package/dist/starting-doc/3_concept/2026_04_08_20_58_[DOCUMENTING]_ADRS.md +20 -0
  138. package/dist/starting-doc/3_concept/2026_04_08_22_15_[DOCUMENTING]_living_documentation.md +17 -0
  139. package/dist/starting-doc/3_concept/2026_04_08_22_46_[METHODOLOGY]_diataxis_architecture_du_contenu.md +16 -0
  140. package/dist/starting-doc/4_reference/2026_04_08_23_14_[FUNDAMENTALS]_the_living_documentation_tool.md +41 -0
  141. package/dist/starting-doc/4_reference/2026_04_09_01_00_[REFERENCE]_raccourcis_clavier.md +61 -0
  142. package/dist/starting-doc/4_reference/2026_04_09_02_00_[REFERENCE]_tokens_pattern_nommage.md +75 -0
  143. package/dist/starting-doc/4_reference/2026_04_09_03_00_[REFERENCE]_types_de_snippets.md +68 -0
  144. package/dist/starting-doc/4_reference/2026_04_11_17_31_[FUNDAMENTALS]_architecturer_une_documentation.md +12 -0
  145. package/dist/starting-doc/4_reference/2026_04_12_14_07_[FUNDAMENTALS]_dossiers_et_catgories.md +89 -0
  146. package/dist/starting-doc/images/admin_screenshot.png +0 -0
  147. package/dist/starting-doc/images/ajout-document.png +0 -0
  148. package/dist/starting-doc/images/ajouter-document-categorie.png +0 -0
  149. package/dist/starting-doc/images/ajouter_un_document_dans_un_dossier.png +0 -0
  150. package/dist/starting-doc/images/architecturer_une_documentation_reference.png +0 -0
  151. package/dist/starting-doc/images/cr_er_un_document.png +0 -0
  152. package/dist/starting-doc/images/creation-nouveau-dossier.png +0 -0
  153. package/dist/starting-doc/images/creer-document-context-engineering.png +0 -0
  154. package/dist/starting-doc/images/creer-dossier-only-tutoriel.png +0 -0
  155. package/dist/starting-doc/images/creer-dossier-tutoriel.png +0 -0
  156. package/dist/starting-doc/images/creer-dossiers-done.png +0 -0
  157. package/dist/starting-doc/images/creer-un-document.png +0 -0
  158. package/dist/starting-doc/images/creer-vos-dossiers-tutoriel.png +0 -0
  159. package/dist/starting-doc/images/creer-vos-dossiers.png +0 -0
  160. package/dist/starting-doc/images/decouverte_adrs.png +0 -0
  161. package/dist/starting-doc/images/diataxis.png +0 -0
  162. package/dist/starting-doc/images/diataxis_callout.png +0 -0
  163. package/dist/starting-doc/images/document-cree.png +0 -0
  164. package/dist/starting-doc/images/liens_snippets.png +0 -0
  165. package/dist/starting-doc/images/living_documentation.png +0 -0
  166. package/dist/starting-doc/images/npm_logo.png +0 -0
  167. package/dist/starting-doc/images/popup-creer-document.png +0 -0
  168. package/dist/starting-doc/images/popup-creer-dossier.png +0 -0
  169. package/dist/starting-doc/images/popup-dossier-cree.png +0 -0
  170. package/dist/starting-doc/images/quatre-dossiers-crees.png +0 -0
  171. package/dist/starting-doc/images/screenshot-living-doc.png +0 -0
  172. package/dist/starting-doc/images/the_living_documentation_tool.png +0 -0
  173. 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">&#128193;</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">&#10003;</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">&#128193;</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
+ }