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.
- package/LICENSE +661 -0
- package/README.fr.md +344 -0
- package/README.md +344 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +262 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/frontend/accuracy-gauge.js +70 -0
- package/dist/src/frontend/admin.html +1532 -0
- package/dist/src/frontend/annotations.js +585 -0
- package/dist/src/frontend/boot.js +101 -0
- package/dist/src/frontend/config.js +29 -0
- package/dist/src/frontend/confirm-modal.js +82 -0
- package/dist/src/frontend/context.html +1252 -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 +187 -0
- package/dist/src/frontend/diagram/constants.js +109 -0
- package/dist/src/frontend/diagram/custom-shapes.js +104 -0
- package/dist/src/frontend/diagram/debug.js +43 -0
- package/dist/src/frontend/diagram/drawio-export.js +649 -0
- package/dist/src/frontend/diagram/edge-panel.js +293 -0
- package/dist/src/frontend/diagram/edge-rendering.js +12 -0
- package/dist/src/frontend/diagram/evidence.js +146 -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 +157 -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 +364 -0
- package/dist/src/frontend/diagram/network.js +2214 -0
- package/dist/src/frontend/diagram/node-panel.js +389 -0
- package/dist/src/frontend/diagram/node-rendering.js +964 -0
- package/dist/src/frontend/diagram/persistence.js +168 -0
- package/dist/src/frontend/diagram/ports.js +421 -0
- package/dist/src/frontend/diagram/selection-overlay.js +387 -0
- package/dist/src/frontend/diagram/state.js +43 -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 +206 -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 +1494 -0
- package/dist/src/frontend/documents.js +479 -0
- package/dist/src/frontend/export.js +338 -0
- package/dist/src/frontend/file-attach.js +178 -0
- package/dist/src/frontend/files-modal.js +243 -0
- package/dist/src/frontend/i18n/en.json +624 -0
- package/dist/src/frontend/i18n/fr.json +624 -0
- package/dist/src/frontend/i18n.js +32 -0
- package/dist/src/frontend/image-paste.js +126 -0
- package/dist/src/frontend/index.html +2806 -0
- package/dist/src/frontend/local-search.js +476 -0
- package/dist/src/frontend/metadata.js +318 -0
- package/dist/src/frontend/misc.js +92 -0
- package/dist/src/frontend/new-doc-modal.js +285 -0
- package/dist/src/frontend/new-folder-modal.js +169 -0
- package/dist/src/frontend/search.js +194 -0
- package/dist/src/frontend/shape-editor.html +685 -0
- package/dist/src/frontend/sidebar-helpers.js +96 -0
- package/dist/src/frontend/sidebar-resize.js +98 -0
- package/dist/src/frontend/sidebar.js +351 -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 +1146 -0
- package/dist/src/frontend/state.js +46 -0
- package/dist/src/frontend/utils.js +21 -0
- package/dist/src/frontend/validate.js +107 -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 +26 -0
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +195 -0
- package/dist/src/lib/config.js.map +1 -0
- package/dist/src/lib/hash.d.ts +2 -0
- package/dist/src/lib/hash.d.ts.map +1 -0
- package/dist/src/lib/hash.js +18 -0
- package/dist/src/lib/hash.js.map +1 -0
- package/dist/src/lib/metadata.d.ts +31 -0
- package/dist/src/lib/metadata.d.ts.map +1 -0
- package/dist/src/lib/metadata.js +128 -0
- package/dist/src/lib/metadata.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/lib/status.d.ts +9 -0
- package/dist/src/lib/status.d.ts.map +1 -0
- package/dist/src/lib/status.js +72 -0
- package/dist/src/lib/status.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 +2046 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools/diagrams.d.ts +82 -0
- package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
- package/dist/src/mcp/tools/diagrams.js +594 -0
- package/dist/src/mcp/tools/diagrams.js.map +1 -0
- package/dist/src/mcp/tools/documents.d.ts +44 -0
- package/dist/src/mcp/tools/documents.d.ts.map +1 -0
- package/dist/src/mcp/tools/documents.js +186 -0
- package/dist/src/mcp/tools/documents.js.map +1 -0
- package/dist/src/mcp/tools/git.d.ts +10 -0
- package/dist/src/mcp/tools/git.d.ts.map +1 -0
- package/dist/src/mcp/tools/git.js +217 -0
- package/dist/src/mcp/tools/git.js.map +1 -0
- package/dist/src/mcp/tools/metadata.d.ts +57 -0
- package/dist/src/mcp/tools/metadata.d.ts.map +1 -0
- package/dist/src/mcp/tools/metadata.js +222 -0
- package/dist/src/mcp/tools/metadata.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 +196 -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-source.d.ts +3 -0
- package/dist/src/routes/browse-source.d.ts.map +1 -0
- package/dist/src/routes/browse-source.js +79 -0
- package/dist/src/routes/browse-source.js.map +1 -0
- package/dist/src/routes/browse.d.ts +3 -0
- package/dist/src/routes/browse.d.ts.map +1 -0
- package/dist/src/routes/browse.js +91 -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 +145 -0
- package/dist/src/routes/config.js.map +1 -0
- package/dist/src/routes/context.d.ts +3 -0
- package/dist/src/routes/context.d.ts.map +1 -0
- package/dist/src/routes/context.js +287 -0
- package/dist/src/routes/context.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 +11 -0
- package/dist/src/routes/documents.d.ts.map +1 -0
- package/dist/src/routes/documents.js +450 -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 +280 -0
- package/dist/src/routes/export.js.map +1 -0
- package/dist/src/routes/files.d.ts +3 -0
- package/dist/src/routes/files.d.ts.map +1 -0
- package/dist/src/routes/files.js +180 -0
- package/dist/src/routes/files.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/metadata.d.ts +3 -0
- package/dist/src/routes/metadata.d.ts.map +1 -0
- package/dist/src/routes/metadata.js +131 -0
- package/dist/src/routes/metadata.js.map +1 -0
- package/dist/src/routes/shape-libraries.d.ts +3 -0
- package/dist/src/routes/shape-libraries.d.ts.map +1 -0
- package/dist/src/routes/shape-libraries.js +118 -0
- package/dist/src/routes/shape-libraries.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 +93 -0
- package/dist/src/server.js.map +1 -0
- package/dist/starter-doc/.living-doc.json +52 -0
- package/dist/starter-doc/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
- package/dist/starter-doc/AI/2026_01_01_how_to.md +112 -0
- package/dist/starter-doc/AI/PROJECT-INSTRUCTIONS.md +172 -0
- package/dist/starter-doc/AI/PROJECT-STACK.md +77 -0
- package/dist/starter-doc/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
- package/dist/starter-doc/AI/default/AGENTS.md +31 -0
- package/dist/starter-doc/AI/default/CLAUDE.md +31 -0
- package/dist/starter-doc/AI/default/MEMORY.md +24 -0
- package/dist/starter-doc/AI/rules/no-magic-numbers.md +18 -0
- package/dist/starter-doc/AI/rules/track-current-work.md +23 -0
- package/dist/starter-doc/WORKLOG/current-task.md +57 -0
- package/dist/starter-doc-fr/.living-doc.json +52 -0
- package/dist/starter-doc-fr/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
- package/dist/starter-doc-fr/AI/2026_01_01_how_to.md +100 -0
- package/dist/starter-doc-fr/AI/PROJECT-INSTRUCTIONS.md +172 -0
- package/dist/starter-doc-fr/AI/PROJECT-STACK.md +77 -0
- package/dist/starter-doc-fr/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
- package/dist/starter-doc-fr/AI/default/AGENTS.md +31 -0
- package/dist/starter-doc-fr/AI/default/CLAUDE.md +31 -0
- package/dist/starter-doc-fr/AI/default/MEMORY.md +24 -0
- package/dist/starter-doc-fr/AI/rules/no-magic-numbers.md +18 -0
- package/dist/starter-doc-fr/AI/rules/track-current-work.md +23 -0
- package/dist/starter-doc-fr/WORKLOG/current-task.md +57 -0
- package/images/living_documentation.jpg +0 -0
- package/images/readme-extra-files.png +0 -0
- package/images/readme-filename-pattern.png +0 -0
- package/images/readme-intelligent-search-demo.jpg +0 -0
- package/images/readme-sidebar.png +0 -0
- 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">📁</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
|
+
}
|