living-ai-documentation 1.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 (203) hide show
  1. package/LICENSE +661 -0
  2. package/README.fr.md +344 -0
  3. package/README.md +344 -0
  4. package/dist/bin/cli.d.ts +3 -0
  5. package/dist/bin/cli.d.ts.map +1 -0
  6. package/dist/bin/cli.js +262 -0
  7. package/dist/bin/cli.js.map +1 -0
  8. package/dist/src/frontend/accuracy-gauge.js +70 -0
  9. package/dist/src/frontend/admin.html +1532 -0
  10. package/dist/src/frontend/annotations.js +585 -0
  11. package/dist/src/frontend/boot.js +101 -0
  12. package/dist/src/frontend/config.js +29 -0
  13. package/dist/src/frontend/confirm-modal.js +82 -0
  14. package/dist/src/frontend/context.html +1252 -0
  15. package/dist/src/frontend/dark-mode.js +20 -0
  16. package/dist/src/frontend/diagram/alignment.js +161 -0
  17. package/dist/src/frontend/diagram/clipboard.js +187 -0
  18. package/dist/src/frontend/diagram/constants.js +109 -0
  19. package/dist/src/frontend/diagram/custom-shapes.js +104 -0
  20. package/dist/src/frontend/diagram/debug.js +43 -0
  21. package/dist/src/frontend/diagram/drawio-export.js +649 -0
  22. package/dist/src/frontend/diagram/edge-panel.js +293 -0
  23. package/dist/src/frontend/diagram/edge-rendering.js +12 -0
  24. package/dist/src/frontend/diagram/evidence.js +146 -0
  25. package/dist/src/frontend/diagram/grid.js +78 -0
  26. package/dist/src/frontend/diagram/groups.js +102 -0
  27. package/dist/src/frontend/diagram/history.js +157 -0
  28. package/dist/src/frontend/diagram/image-name-modal.js +48 -0
  29. package/dist/src/frontend/diagram/image-upload.js +36 -0
  30. package/dist/src/frontend/diagram/label-editor.js +115 -0
  31. package/dist/src/frontend/diagram/link-panel.js +144 -0
  32. package/dist/src/frontend/diagram/main.js +364 -0
  33. package/dist/src/frontend/diagram/network.js +2214 -0
  34. package/dist/src/frontend/diagram/node-panel.js +389 -0
  35. package/dist/src/frontend/diagram/node-rendering.js +964 -0
  36. package/dist/src/frontend/diagram/persistence.js +168 -0
  37. package/dist/src/frontend/diagram/ports.js +421 -0
  38. package/dist/src/frontend/diagram/selection-overlay.js +387 -0
  39. package/dist/src/frontend/diagram/state.js +43 -0
  40. package/dist/src/frontend/diagram/t.js +3 -0
  41. package/dist/src/frontend/diagram/toast.js +21 -0
  42. package/dist/src/frontend/diagram/unlock-hold.js +206 -0
  43. package/dist/src/frontend/diagram/zoom.js +20 -0
  44. package/dist/src/frontend/diagram-link-modal.js +137 -0
  45. package/dist/src/frontend/diagram.html +1494 -0
  46. package/dist/src/frontend/documents.js +479 -0
  47. package/dist/src/frontend/export.js +338 -0
  48. package/dist/src/frontend/file-attach.js +178 -0
  49. package/dist/src/frontend/files-modal.js +243 -0
  50. package/dist/src/frontend/i18n/en.json +624 -0
  51. package/dist/src/frontend/i18n/fr.json +624 -0
  52. package/dist/src/frontend/i18n.js +32 -0
  53. package/dist/src/frontend/image-paste.js +126 -0
  54. package/dist/src/frontend/index.html +2806 -0
  55. package/dist/src/frontend/local-search.js +476 -0
  56. package/dist/src/frontend/metadata.js +318 -0
  57. package/dist/src/frontend/misc.js +92 -0
  58. package/dist/src/frontend/new-doc-modal.js +285 -0
  59. package/dist/src/frontend/new-folder-modal.js +169 -0
  60. package/dist/src/frontend/search.js +194 -0
  61. package/dist/src/frontend/shape-editor.html +685 -0
  62. package/dist/src/frontend/sidebar-helpers.js +96 -0
  63. package/dist/src/frontend/sidebar-resize.js +98 -0
  64. package/dist/src/frontend/sidebar.js +351 -0
  65. package/dist/src/frontend/snippet-detect.js +25 -0
  66. package/dist/src/frontend/snippet-table.js +85 -0
  67. package/dist/src/frontend/snippet-tree.js +94 -0
  68. package/dist/src/frontend/snippets.js +1146 -0
  69. package/dist/src/frontend/state.js +46 -0
  70. package/dist/src/frontend/utils.js +21 -0
  71. package/dist/src/frontend/validate.js +107 -0
  72. package/dist/src/frontend/vendor/wordcloud2.js +1187 -0
  73. package/dist/src/frontend/wordcloud.js +693 -0
  74. package/dist/src/lib/config.d.ts +26 -0
  75. package/dist/src/lib/config.d.ts.map +1 -0
  76. package/dist/src/lib/config.js +195 -0
  77. package/dist/src/lib/config.js.map +1 -0
  78. package/dist/src/lib/hash.d.ts +2 -0
  79. package/dist/src/lib/hash.d.ts.map +1 -0
  80. package/dist/src/lib/hash.js +18 -0
  81. package/dist/src/lib/hash.js.map +1 -0
  82. package/dist/src/lib/metadata.d.ts +31 -0
  83. package/dist/src/lib/metadata.d.ts.map +1 -0
  84. package/dist/src/lib/metadata.js +128 -0
  85. package/dist/src/lib/metadata.js.map +1 -0
  86. package/dist/src/lib/parser.d.ts +11 -0
  87. package/dist/src/lib/parser.d.ts.map +1 -0
  88. package/dist/src/lib/parser.js +111 -0
  89. package/dist/src/lib/parser.js.map +1 -0
  90. package/dist/src/lib/status.d.ts +9 -0
  91. package/dist/src/lib/status.d.ts.map +1 -0
  92. package/dist/src/lib/status.js +72 -0
  93. package/dist/src/lib/status.js.map +1 -0
  94. package/dist/src/mcp/server.d.ts +3 -0
  95. package/dist/src/mcp/server.d.ts.map +1 -0
  96. package/dist/src/mcp/server.js +2046 -0
  97. package/dist/src/mcp/server.js.map +1 -0
  98. package/dist/src/mcp/tools/diagrams.d.ts +82 -0
  99. package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
  100. package/dist/src/mcp/tools/diagrams.js +594 -0
  101. package/dist/src/mcp/tools/diagrams.js.map +1 -0
  102. package/dist/src/mcp/tools/documents.d.ts +44 -0
  103. package/dist/src/mcp/tools/documents.d.ts.map +1 -0
  104. package/dist/src/mcp/tools/documents.js +186 -0
  105. package/dist/src/mcp/tools/documents.js.map +1 -0
  106. package/dist/src/mcp/tools/git.d.ts +10 -0
  107. package/dist/src/mcp/tools/git.d.ts.map +1 -0
  108. package/dist/src/mcp/tools/git.js +217 -0
  109. package/dist/src/mcp/tools/git.js.map +1 -0
  110. package/dist/src/mcp/tools/metadata.d.ts +57 -0
  111. package/dist/src/mcp/tools/metadata.d.ts.map +1 -0
  112. package/dist/src/mcp/tools/metadata.js +222 -0
  113. package/dist/src/mcp/tools/metadata.js.map +1 -0
  114. package/dist/src/mcp/tools/source.d.ts +29 -0
  115. package/dist/src/mcp/tools/source.d.ts.map +1 -0
  116. package/dist/src/mcp/tools/source.js +196 -0
  117. package/dist/src/mcp/tools/source.js.map +1 -0
  118. package/dist/src/routes/annotations.d.ts +3 -0
  119. package/dist/src/routes/annotations.d.ts.map +1 -0
  120. package/dist/src/routes/annotations.js +83 -0
  121. package/dist/src/routes/annotations.js.map +1 -0
  122. package/dist/src/routes/browse-source.d.ts +3 -0
  123. package/dist/src/routes/browse-source.d.ts.map +1 -0
  124. package/dist/src/routes/browse-source.js +79 -0
  125. package/dist/src/routes/browse-source.js.map +1 -0
  126. package/dist/src/routes/browse.d.ts +3 -0
  127. package/dist/src/routes/browse.d.ts.map +1 -0
  128. package/dist/src/routes/browse.js +91 -0
  129. package/dist/src/routes/browse.js.map +1 -0
  130. package/dist/src/routes/config.d.ts +3 -0
  131. package/dist/src/routes/config.d.ts.map +1 -0
  132. package/dist/src/routes/config.js +145 -0
  133. package/dist/src/routes/config.js.map +1 -0
  134. package/dist/src/routes/context.d.ts +3 -0
  135. package/dist/src/routes/context.d.ts.map +1 -0
  136. package/dist/src/routes/context.js +287 -0
  137. package/dist/src/routes/context.js.map +1 -0
  138. package/dist/src/routes/diagrams.d.ts +3 -0
  139. package/dist/src/routes/diagrams.d.ts.map +1 -0
  140. package/dist/src/routes/diagrams.js +69 -0
  141. package/dist/src/routes/diagrams.js.map +1 -0
  142. package/dist/src/routes/documents.d.ts +11 -0
  143. package/dist/src/routes/documents.d.ts.map +1 -0
  144. package/dist/src/routes/documents.js +450 -0
  145. package/dist/src/routes/documents.js.map +1 -0
  146. package/dist/src/routes/export.d.ts +3 -0
  147. package/dist/src/routes/export.d.ts.map +1 -0
  148. package/dist/src/routes/export.js +280 -0
  149. package/dist/src/routes/export.js.map +1 -0
  150. package/dist/src/routes/files.d.ts +3 -0
  151. package/dist/src/routes/files.d.ts.map +1 -0
  152. package/dist/src/routes/files.js +180 -0
  153. package/dist/src/routes/files.js.map +1 -0
  154. package/dist/src/routes/images.d.ts +3 -0
  155. package/dist/src/routes/images.d.ts.map +1 -0
  156. package/dist/src/routes/images.js +49 -0
  157. package/dist/src/routes/images.js.map +1 -0
  158. package/dist/src/routes/metadata.d.ts +3 -0
  159. package/dist/src/routes/metadata.d.ts.map +1 -0
  160. package/dist/src/routes/metadata.js +131 -0
  161. package/dist/src/routes/metadata.js.map +1 -0
  162. package/dist/src/routes/shape-libraries.d.ts +3 -0
  163. package/dist/src/routes/shape-libraries.d.ts.map +1 -0
  164. package/dist/src/routes/shape-libraries.js +118 -0
  165. package/dist/src/routes/shape-libraries.js.map +1 -0
  166. package/dist/src/routes/wordcloud.d.ts +3 -0
  167. package/dist/src/routes/wordcloud.d.ts.map +1 -0
  168. package/dist/src/routes/wordcloud.js +95 -0
  169. package/dist/src/routes/wordcloud.js.map +1 -0
  170. package/dist/src/server.d.ts +7 -0
  171. package/dist/src/server.d.ts.map +1 -0
  172. package/dist/src/server.js +93 -0
  173. package/dist/src/server.js.map +1 -0
  174. package/dist/starter-doc/.living-doc.json +52 -0
  175. package/dist/starter-doc/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
  176. package/dist/starter-doc/AI/2026_01_01_how_to.md +112 -0
  177. package/dist/starter-doc/AI/PROJECT-INSTRUCTIONS.md +172 -0
  178. package/dist/starter-doc/AI/PROJECT-STACK.md +77 -0
  179. package/dist/starter-doc/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
  180. package/dist/starter-doc/AI/default/AGENTS.md +31 -0
  181. package/dist/starter-doc/AI/default/CLAUDE.md +31 -0
  182. package/dist/starter-doc/AI/default/MEMORY.md +24 -0
  183. package/dist/starter-doc/AI/rules/no-magic-numbers.md +18 -0
  184. package/dist/starter-doc/AI/rules/track-current-work.md +23 -0
  185. package/dist/starter-doc/WORKLOG/current-task.md +57 -0
  186. package/dist/starter-doc-fr/.living-doc.json +52 -0
  187. package/dist/starter-doc-fr/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
  188. package/dist/starter-doc-fr/AI/2026_01_01_how_to.md +100 -0
  189. package/dist/starter-doc-fr/AI/PROJECT-INSTRUCTIONS.md +172 -0
  190. package/dist/starter-doc-fr/AI/PROJECT-STACK.md +77 -0
  191. package/dist/starter-doc-fr/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
  192. package/dist/starter-doc-fr/AI/default/AGENTS.md +31 -0
  193. package/dist/starter-doc-fr/AI/default/CLAUDE.md +31 -0
  194. package/dist/starter-doc-fr/AI/default/MEMORY.md +24 -0
  195. package/dist/starter-doc-fr/AI/rules/no-magic-numbers.md +18 -0
  196. package/dist/starter-doc-fr/AI/rules/track-current-work.md +23 -0
  197. package/dist/starter-doc-fr/WORKLOG/current-task.md +57 -0
  198. package/images/living_documentation.jpg +0 -0
  199. package/images/readme-extra-files.png +0 -0
  200. package/images/readme-filename-pattern.png +0 -0
  201. package/images/readme-intelligent-search-demo.jpg +0 -0
  202. package/images/readme-sidebar.png +0 -0
  203. package/package.json +72 -0
@@ -0,0 +1,169 @@
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
+ _newFolderSelectedPath = data.current;
73
+
74
+ document.getElementById("new-folder-browse-path").textContent =
75
+ data.current;
76
+ const atRoot = data.current === _newFolderDocsFolder;
77
+ document.getElementById("new-folder-browse-up").disabled = atRoot;
78
+
79
+ const rel = data.current.startsWith(_newFolderDocsFolder + "/")
80
+ ? data.current.slice(_newFolderDocsFolder.length)
81
+ : data.current === _newFolderDocsFolder
82
+ ? ""
83
+ : data.current;
84
+ document.getElementById("new-folder-location-display").textContent = rel
85
+ ? rel
86
+ : "/ (root)";
87
+ newFolderUpdatePreview();
88
+
89
+ list.innerHTML = data.dirs.length
90
+ ? data.dirs
91
+ .map(
92
+ (dir) => `
93
+ <button data-path="${esc(dir.path)}" onclick="newFolderLoadBrowse(this.dataset.path)"
94
+ class="w-full flex items-center gap-2 px-3 py-2 text-sm text-left hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
95
+ <span class="text-gray-400 shrink-0">&#128193;</span>
96
+ <span class="truncate text-gray-700 dark:text-gray-300">${esc(dir.name)}</span>
97
+ </button>`,
98
+ )
99
+ .join("")
100
+ : `<p class="px-3 py-3 text-xs text-gray-400 text-center">${window.t('modal.new_folder.no_subfolders')}</p>`;
101
+ } catch {
102
+ list.innerHTML =
103
+ `<p class="px-3 py-3 text-xs text-red-400 text-center">${window.t('modal.new_folder.error_loading')}</p>`;
104
+ }
105
+ }
106
+
107
+ function newFolderBrowseUp() {
108
+ if (_newFolderBrowseParent) newFolderLoadBrowse(_newFolderBrowseParent);
109
+ }
110
+
111
+ function newFolderUpdatePreview() {
112
+ const name = document.getElementById("new-folder-name").value.trim();
113
+ const previewEl = document.getElementById("new-folder-preview");
114
+ if (!name) {
115
+ previewEl.textContent = window.t('modal.new_folder.enter_name');
116
+ return;
117
+ }
118
+ const base = _newFolderSelectedPath || _newFolderDocsFolder;
119
+ const rel = base.startsWith(_newFolderDocsFolder)
120
+ ? base.slice(_newFolderDocsFolder.length).replace(/^\//, "")
121
+ : "";
122
+ previewEl.textContent = (rel ? rel + "/" : "") + name;
123
+ }
124
+
125
+ async function createNewFolder() {
126
+ const name = document.getElementById("new-folder-name").value.trim();
127
+ const errEl = document.getElementById("new-folder-error");
128
+ errEl.classList.add("hidden");
129
+
130
+ if (!name) {
131
+ errEl.textContent = window.t('modal.new_folder.error_empty');
132
+ errEl.classList.remove("hidden");
133
+ return;
134
+ }
135
+ if (!/^[a-zA-Z0-9_\-. ]+$/.test(name)) {
136
+ errEl.textContent = window.t('modal.new_folder.error_invalid_chars');
137
+ errEl.classList.remove("hidden");
138
+ return;
139
+ }
140
+
141
+ const base = _newFolderSelectedPath || _newFolderDocsFolder;
142
+ const atDocsRoot = base === _newFolderDocsFolder;
143
+ if (atDocsRoot && (name === "files" || name === "images")) {
144
+ errEl.textContent = window.t('modal.new_folder.error_reserved');
145
+ errEl.classList.remove("hidden");
146
+ return;
147
+ }
148
+ const fullPath = base.endsWith("/") ? base + name : base + "/" + name;
149
+
150
+ const btn = document.getElementById("new-folder-create-btn");
151
+ btn.disabled = true;
152
+ btn.textContent = window.t('modal.new_folder.creating_btn');
153
+
154
+ try {
155
+ const res = await fetch("/api/browse/mkdir", {
156
+ method: "POST",
157
+ headers: { "Content-Type": "application/json" },
158
+ body: JSON.stringify({ path: fullPath }),
159
+ });
160
+ if (!res.ok) throw new Error(await res.text());
161
+ closeNewFolderModal();
162
+ await loadDocuments();
163
+ } catch (err) {
164
+ btn.disabled = false;
165
+ btn.textContent = window.t('modal.new_folder.create_btn');
166
+ errEl.textContent = window.t('common.error_prefix') + err.message;
167
+ errEl.classList.remove("hidden");
168
+ }
169
+ }
@@ -0,0 +1,194 @@
1
+ // ── Search + result highlighting ─────────────────────────────────────────────
2
+
3
+ let searchTimer = null;
4
+ const METADATA_SEARCH_PREFIX = "metadata://";
5
+
6
+ function _isMetadataQuery(q) {
7
+ return typeof q === "string" && q.toLowerCase().startsWith(METADATA_SEARCH_PREFIX);
8
+ }
9
+
10
+ function setupSearch() {
11
+ ["header-search", "sidebar-search"].forEach((id) => {
12
+ const el = document.getElementById(id);
13
+ if (!el) return;
14
+ el.addEventListener("input", (e) => {
15
+ const q = e.target.value.trim();
16
+ // Sync the other input
17
+ ["header-search", "sidebar-search"].forEach((oid) => {
18
+ if (oid !== id) {
19
+ const other = document.getElementById(oid);
20
+ if (other) other.value = q;
21
+ }
22
+ });
23
+ clearTimeout(searchTimer);
24
+ if (!q) {
25
+ searchQuery = "";
26
+ searchResults = null;
27
+ renderSidebar(allDocs);
28
+ if (window.refreshSearchInCurrentDoc) window.refreshSearchInCurrentDoc();
29
+ return;
30
+ }
31
+ searchQuery = q;
32
+ if (_isMetadataQuery(q)) {
33
+ // Title/category can't match a metadata:// query — skip the local
34
+ // filter and wait for the server response.
35
+ searchResults = [];
36
+ renderSidebar([]);
37
+ } else {
38
+ // Immediate client-side filter for snappy UX
39
+ const local = allDocs.filter(
40
+ (d) =>
41
+ d.title.toLowerCase().includes(q.toLowerCase()) ||
42
+ d.category.toLowerCase().includes(q.toLowerCase()),
43
+ );
44
+ searchResults = local;
45
+ renderSidebar(local);
46
+ }
47
+ // Refresh highlights inside the currently open doc
48
+ if (window.refreshSearchInCurrentDoc) window.refreshSearchInCurrentDoc();
49
+ // Then full-text search from server
50
+ searchTimer = setTimeout(() => doSearch(q), 350);
51
+ });
52
+ });
53
+ }
54
+
55
+ // Programmatic trigger used by the Files modal after a replace/delete.
56
+ // Fills both search inputs and runs the server search immediately (no debounce).
57
+ function runSearchImmediate(q) {
58
+ const trimmed = (q || "").trim();
59
+ ["header-search", "sidebar-search"].forEach((id) => {
60
+ const el = document.getElementById(id);
61
+ if (el) el.value = trimmed;
62
+ });
63
+ clearTimeout(searchTimer);
64
+ if (!trimmed) {
65
+ searchQuery = "";
66
+ searchResults = null;
67
+ renderSidebar(allDocs);
68
+ return;
69
+ }
70
+ searchQuery = trimmed;
71
+ searchResults = [];
72
+ renderSidebar([]);
73
+ doSearch(trimmed);
74
+ }
75
+ window.runSearchImmediate = runSearchImmediate;
76
+
77
+ async function doSearch(q) {
78
+ try {
79
+ const results = await fetch(
80
+ "/api/documents/search?q=" + encodeURIComponent(q),
81
+ ).then((r) => r.json());
82
+ if (q !== searchQuery) return;
83
+ searchResults = results;
84
+ renderSidebar(results);
85
+ } catch {
86
+ /* ignore */
87
+ }
88
+ }
89
+
90
+ function highlightMatches(el, q) {
91
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
92
+ const nodes = [];
93
+ while (walker.nextNode()) nodes.push(walker.currentNode);
94
+ const re = new RegExp(
95
+ `(${q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`,
96
+ "gi",
97
+ );
98
+ let idx = 0;
99
+ nodes.forEach((node) => {
100
+ if (!node.textContent.toLowerCase().includes(q.toLowerCase())) return;
101
+ const span = document.createElement("span");
102
+ span.innerHTML = node.textContent.replace(
103
+ re,
104
+ (m) => `<mark id="match-${idx++}">${m}</mark>`,
105
+ );
106
+ node.parentNode.replaceChild(span, node);
107
+ });
108
+ // Build snippet list from inserted marks
109
+ const matches = [];
110
+ el.querySelectorAll("mark[id^='match-']").forEach((mark) => {
111
+ const block =
112
+ mark.closest("p, li, td, h1, h2, h3, h4, h5, h6, pre") ||
113
+ mark.parentElement;
114
+ const full = block.textContent.trim();
115
+ const pos = full.toLowerCase().indexOf(q.toLowerCase());
116
+ const start = Math.max(0, pos - 40);
117
+ const end = Math.min(full.length, pos + q.length + 40);
118
+ const snippet =
119
+ (start > 0 ? "…" : "") +
120
+ full.slice(start, end) +
121
+ (end < full.length ? "…" : "");
122
+ matches.push({ id: mark.id, snippet });
123
+ });
124
+ return matches;
125
+ }
126
+
127
+ function buildSearchNotice(matches, q) {
128
+ document.getElementById("search-notice-title").textContent =
129
+ window.t(matches.length === 1 ? 'search.notice_singular' : 'search.notice_plural')
130
+ .replace('{count}', matches.length).replace('{query}', q);
131
+ const list = document.getElementById("search-notice-list");
132
+ list.innerHTML = matches
133
+ .map(
134
+ (m, i) =>
135
+ `<li>
136
+ <button onclick="scrollToMatch('${m.id}')" data-match-id="${m.id}"
137
+ class="w-full text-left px-3 py-1.5 hover:bg-yellow-100 dark:hover:bg-yellow-900/30 transition-colors">
138
+ <span class="text-yellow-600 dark:text-yellow-500 font-mono text-xs mr-2">${i + 1}.</span
139
+ ><span class="text-xs">${esc(m.snippet)}</span>
140
+ </button>
141
+ </li>`,
142
+ )
143
+ .join("");
144
+ }
145
+
146
+ function scrollToAnchor(anchorId) {
147
+ const container = document.getElementById("content-area");
148
+ const target = document.getElementById(anchorId);
149
+ if (!container || !target) return;
150
+ const stickyHeader = document.querySelector("#doc-view header");
151
+ const headerHeight = stickyHeader
152
+ ? stickyHeader.getBoundingClientRect().height
153
+ : 0;
154
+ const targetTop = target.getBoundingClientRect().top;
155
+ const containerTop = container.getBoundingClientRect().top;
156
+ container.scrollTop += targetTop - containerTop - headerHeight - 8;
157
+ }
158
+
159
+ function scrollToMatch(id) {
160
+ const mark = document.getElementById(id);
161
+ const container = document.getElementById("content-area");
162
+ if (!mark || !container) return;
163
+
164
+ // Open any collapsed <details> ancestors so the match is visible
165
+ let ancestor = mark.parentElement;
166
+ while (ancestor && ancestor !== container) {
167
+ if (ancestor.tagName === "DETAILS" && !ancestor.open) {
168
+ ancestor.open = true;
169
+ }
170
+ ancestor = ancestor.parentElement;
171
+ }
172
+
173
+ // Orange on the active mark, yellow on the rest
174
+ document
175
+ .querySelectorAll("mark.match-active")
176
+ .forEach((m) => m.classList.remove("match-active"));
177
+ mark.classList.add("match-active");
178
+
179
+ // Highlight active list item
180
+ document.querySelectorAll("#search-notice-list button").forEach((b) => {
181
+ b.classList.toggle("bg-orange-100", b.dataset.matchId === id);
182
+ b.classList.toggle("dark:bg-orange-900/30", b.dataset.matchId === id);
183
+ });
184
+
185
+ const stickyHeader = document.querySelector("#doc-view header");
186
+ const headerHeight = stickyHeader
187
+ ? stickyHeader.getBoundingClientRect().height
188
+ : 0;
189
+ const markTop = mark.getBoundingClientRect().top;
190
+ const containerTop = container.getBoundingClientRect().top;
191
+ const targetOffset =
192
+ headerHeight + (container.clientHeight - headerHeight) / 3;
193
+ container.scrollTop += markTop - containerTop - targetOffset;
194
+ }