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,479 @@
|
|
|
1
|
+
// ── Documents: load, open, edit, save, delete, navigate ─────────────────────
|
|
2
|
+
// Depends on globals from state.js, utils.js, search.js, sidebar.js,
|
|
3
|
+
// annotations (loadAnnotations, applyAnnotationHighlights, renderElevator),
|
|
4
|
+
// and image-paste.js (handleEditorPaste).
|
|
5
|
+
|
|
6
|
+
// Cache of the last rendered doc HTML so search input changes can re-wire
|
|
7
|
+
// the content without a round-trip to the server.
|
|
8
|
+
let _lastDocHtml = null;
|
|
9
|
+
let _lastDocIdRendered = null;
|
|
10
|
+
|
|
11
|
+
function _wireDocContent(html) {
|
|
12
|
+
const contentEl = document.getElementById("doc-content");
|
|
13
|
+
if (!contentEl) return;
|
|
14
|
+
contentEl.innerHTML = html;
|
|
15
|
+
|
|
16
|
+
contentEl.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((h) => {
|
|
17
|
+
if (!h.id) {
|
|
18
|
+
h.id = h.textContent
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[^\w\s-]/g, "")
|
|
21
|
+
.trim()
|
|
22
|
+
.replace(/\s+/g, "-");
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
contentEl.querySelectorAll("pre code").forEach((block) => {
|
|
27
|
+
hljs.highlightElement(block);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
_decorateCodeBlocksWithCopy(contentEl);
|
|
31
|
+
_decorateCollapsibleCodeBlocks(contentEl);
|
|
32
|
+
|
|
33
|
+
if (typeof initLocalSearch === "function") initLocalSearch(contentEl);
|
|
34
|
+
|
|
35
|
+
contentEl.querySelectorAll("a[href]").forEach((a) => {
|
|
36
|
+
const href = a.getAttribute("href");
|
|
37
|
+
const m = href && href.match(/[?&]doc=([^&#]+)/);
|
|
38
|
+
if (!m) return;
|
|
39
|
+
const hashIdx = href.indexOf("#");
|
|
40
|
+
const anchorTarget = hashIdx !== -1 ? href.slice(hashIdx + 1) : null;
|
|
41
|
+
a.addEventListener("click", (e) => {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
openDocument(decodeURIComponent(m[1]), false, true, anchorTarget);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
contentEl.querySelectorAll('a[href^="#"]').forEach((a) => {
|
|
48
|
+
const href = a.getAttribute("href");
|
|
49
|
+
if (!href || href.length < 2) return;
|
|
50
|
+
a.addEventListener("click", (e) => {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
scrollToAnchor(href.slice(1));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
contentEl.querySelectorAll("table").forEach((t) => {
|
|
57
|
+
const wrapper = document.createElement("div");
|
|
58
|
+
wrapper.className = "overflow-x-auto";
|
|
59
|
+
t.parentNode.insertBefore(wrapper, t);
|
|
60
|
+
wrapper.appendChild(t);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const notice = document.getElementById("search-notice");
|
|
64
|
+
const isMetaQuery =
|
|
65
|
+
typeof searchQuery === "string" &&
|
|
66
|
+
searchQuery.toLowerCase().startsWith("metadata://");
|
|
67
|
+
if (searchQuery && !isMetaQuery) {
|
|
68
|
+
const matches = highlightMatches(contentEl, searchQuery);
|
|
69
|
+
buildSearchNotice(matches, searchQuery);
|
|
70
|
+
notice.classList.remove("hidden");
|
|
71
|
+
} else {
|
|
72
|
+
notice.classList.add("hidden");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function refreshSearchInCurrentDoc() {
|
|
77
|
+
if (
|
|
78
|
+
!currentDocId ||
|
|
79
|
+
_lastDocHtml === null ||
|
|
80
|
+
currentDocId !== _lastDocIdRendered
|
|
81
|
+
) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const contentArea = document.getElementById("content-area");
|
|
85
|
+
const scrollTop = contentArea ? contentArea.scrollTop : 0;
|
|
86
|
+
_wireDocContent(_lastDocHtml);
|
|
87
|
+
if (typeof loadAnnotations === "function") loadAnnotations(currentDocId);
|
|
88
|
+
if (contentArea) contentArea.scrollTop = scrollTop;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
window.refreshSearchInCurrentDoc = refreshSearchInCurrentDoc;
|
|
92
|
+
|
|
93
|
+
function _decorateCodeBlocksWithCopy(contentEl) {
|
|
94
|
+
const copyLabel =
|
|
95
|
+
(typeof window.t === "function" && window.t("doc.code_copy")) || "Copy";
|
|
96
|
+
const copiedLabel =
|
|
97
|
+
(typeof window.t === "function" && window.t("doc.code_copied")) ||
|
|
98
|
+
"Copied!";
|
|
99
|
+
contentEl.querySelectorAll("pre").forEach((pre) => {
|
|
100
|
+
const code = pre.querySelector("code");
|
|
101
|
+
if (!code) return;
|
|
102
|
+
if (pre.querySelector(".ld-code-copy")) return;
|
|
103
|
+
const btn = document.createElement("button");
|
|
104
|
+
btn.type = "button";
|
|
105
|
+
btn.className = "ld-code-copy";
|
|
106
|
+
btn.title = copyLabel;
|
|
107
|
+
btn.setAttribute("aria-label", copyLabel);
|
|
108
|
+
btn.innerHTML =
|
|
109
|
+
'<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="5" width="8" height="9" rx="1.5"/><path d="M3 11V3a1 1 0 0 1 1-1h7"/></svg>';
|
|
110
|
+
btn.addEventListener("click", async (e) => {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
e.stopPropagation();
|
|
113
|
+
const text = code.innerText;
|
|
114
|
+
try {
|
|
115
|
+
await navigator.clipboard.writeText(text);
|
|
116
|
+
} catch {
|
|
117
|
+
const ta = document.createElement("textarea");
|
|
118
|
+
ta.value = text;
|
|
119
|
+
ta.style.position = "fixed";
|
|
120
|
+
ta.style.opacity = "0";
|
|
121
|
+
document.body.appendChild(ta);
|
|
122
|
+
ta.select();
|
|
123
|
+
try {
|
|
124
|
+
document.execCommand("copy");
|
|
125
|
+
} catch {
|
|
126
|
+
/* ignore */
|
|
127
|
+
}
|
|
128
|
+
document.body.removeChild(ta);
|
|
129
|
+
}
|
|
130
|
+
btn.classList.add("ld-copied");
|
|
131
|
+
btn.title = copiedLabel;
|
|
132
|
+
btn.innerHTML =
|
|
133
|
+
'<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5 6.5 12 13 4.5"/></svg>';
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
btn.classList.remove("ld-copied");
|
|
136
|
+
btn.title = copyLabel;
|
|
137
|
+
btn.innerHTML =
|
|
138
|
+
'<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="5" width="8" height="9" rx="1.5"/><path d="M3 11V3a1 1 0 0 1 1-1h7"/></svg>';
|
|
139
|
+
}, 1500);
|
|
140
|
+
});
|
|
141
|
+
pre.appendChild(btn);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function _decorateCollapsibleCodeBlocks(contentEl) {
|
|
146
|
+
if (typeof codeBlockMaxHeight !== "number" || codeBlockMaxHeight <= 0) return;
|
|
147
|
+
const more = (typeof window.t === "function" && window.t("doc.code_show_more")) || "▾ Show more";
|
|
148
|
+
const less = (typeof window.t === "function" && window.t("doc.code_show_less")) || "▴ Show less";
|
|
149
|
+
contentEl.querySelectorAll("pre").forEach((pre) => {
|
|
150
|
+
if (!pre.querySelector("code")) return;
|
|
151
|
+
if (pre.scrollHeight <= codeBlockMaxHeight + 8) return;
|
|
152
|
+
pre.classList.add("ld-collapsible");
|
|
153
|
+
const btn = document.createElement("button");
|
|
154
|
+
btn.type = "button";
|
|
155
|
+
btn.className = "ld-code-toggle";
|
|
156
|
+
btn.textContent = more;
|
|
157
|
+
btn.addEventListener("click", (e) => {
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
const expanded = pre.classList.toggle("ld-expanded");
|
|
160
|
+
btn.textContent = expanded ? less : more;
|
|
161
|
+
});
|
|
162
|
+
pre.appendChild(btn);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function loadDocuments() {
|
|
167
|
+
try {
|
|
168
|
+
[allDocs] = await Promise.all([
|
|
169
|
+
fetch("/api/documents").then((r) => r.json()),
|
|
170
|
+
]);
|
|
171
|
+
// Also fetch all directories so empty folders appear in the sidebar
|
|
172
|
+
try {
|
|
173
|
+
const cfg = await fetch("/api/config").then((r) => r.json());
|
|
174
|
+
if (cfg.docsFolder) {
|
|
175
|
+
allFolderPaths = await fetch(
|
|
176
|
+
"/api/browse/alldirs?path=" +
|
|
177
|
+
encodeURIComponent(cfg.docsFolder),
|
|
178
|
+
).then((r) => r.json());
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
allFolderPaths = [];
|
|
182
|
+
}
|
|
183
|
+
await Promise.all([
|
|
184
|
+
refreshAnnotationCounts(),
|
|
185
|
+
refreshFileAttachmentCounts(),
|
|
186
|
+
]);
|
|
187
|
+
renderSidebar(allDocs);
|
|
188
|
+
} catch {
|
|
189
|
+
document.getElementById("category-tree").innerHTML =
|
|
190
|
+
`<p class="px-4 py-4 text-sm text-red-500">${window.t('sidebar.failed_to_load')}</p>`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function refreshAnnotationCounts() {
|
|
195
|
+
try {
|
|
196
|
+
const raw = await fetch("/api/annotations").then((r) => r.json());
|
|
197
|
+
annotationCounts = {};
|
|
198
|
+
for (const [docId, n] of Object.entries(raw || {})) {
|
|
199
|
+
annotationCounts[docId] = n;
|
|
200
|
+
try {
|
|
201
|
+
annotationCounts[encodeURIComponent(docId)] = n;
|
|
202
|
+
} catch {}
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
annotationCounts = {};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function refreshFileAttachmentCounts() {
|
|
210
|
+
try {
|
|
211
|
+
const raw = await fetch("/api/documents/file-counts").then((r) => r.json());
|
|
212
|
+
fileAttachmentCounts = {};
|
|
213
|
+
for (const [docId, n] of Object.entries(raw || {})) {
|
|
214
|
+
fileAttachmentCounts[docId] = n;
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
fileAttachmentCounts = {};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function openDocument(id, skipHistory = false, fromLink = false, anchor = null) {
|
|
222
|
+
// Track navigation history for breadcrumb trail
|
|
223
|
+
// fromLink===true : forward navigation via in-doc link → push current to stack
|
|
224
|
+
// (unless target is already in the stack → rewind instead of loop)
|
|
225
|
+
// fromLink==="restore" : back navigation via history breadcrumb → stack already trimmed, don't touch
|
|
226
|
+
// fromLink===false : sidebar/direct navigation → reset stack
|
|
227
|
+
if (fromLink === true && currentDocId && currentDocId !== id) {
|
|
228
|
+
const existingIdx = navHistory.findIndex((e) => e.id === id);
|
|
229
|
+
if (existingIdx !== -1) {
|
|
230
|
+
navHistory = navHistory.slice(0, existingIdx);
|
|
231
|
+
} else {
|
|
232
|
+
const prev = allDocs && allDocs.find((d) => d.id === currentDocId);
|
|
233
|
+
navHistory.push({
|
|
234
|
+
id: currentDocId,
|
|
235
|
+
title: prev ? prev.title : currentDocId,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
} else if (!fromLink) {
|
|
239
|
+
navHistory = [];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Update back-link banner
|
|
243
|
+
const backEl = document.getElementById("doc-back");
|
|
244
|
+
if (navHistory.length > 0) {
|
|
245
|
+
backEl.innerHTML = navHistory
|
|
246
|
+
.map(
|
|
247
|
+
(entry, i) =>
|
|
248
|
+
`<button onclick="goBackToIndex(${i})"
|
|
249
|
+
class="no-print text-blue-600 dark:text-blue-400 hover:underline">← ${esc(entry.title)}</button>`,
|
|
250
|
+
)
|
|
251
|
+
.join(
|
|
252
|
+
'<span class="text-gray-300 dark:text-gray-600 mx-1">·</span>',
|
|
253
|
+
);
|
|
254
|
+
backEl.classList.remove("hidden");
|
|
255
|
+
} else {
|
|
256
|
+
backEl.classList.add("hidden");
|
|
257
|
+
backEl.innerHTML = "";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
currentDocId = id;
|
|
261
|
+
|
|
262
|
+
// Expand sidebar path to reveal the document
|
|
263
|
+
const doc = allDocs && allDocs.find((d) => d.id === id);
|
|
264
|
+
if (doc) {
|
|
265
|
+
const folder = doc.folder || [];
|
|
266
|
+
// Expand every ancestor folder
|
|
267
|
+
for (let i = 0; i < folder.length; i++) {
|
|
268
|
+
expandedFolders.add(folder.slice(0, i + 1).join("|"));
|
|
269
|
+
}
|
|
270
|
+
// Expand the category at this folder level
|
|
271
|
+
expandedCategories.add([...folder, doc.category].join("|"));
|
|
272
|
+
refreshSidebar();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Update active state in sidebar
|
|
276
|
+
document
|
|
277
|
+
.querySelectorAll(".doc-item")
|
|
278
|
+
.forEach((el) => el.classList.remove("active"));
|
|
279
|
+
const activeItem = document.getElementById("item-" + id);
|
|
280
|
+
if (activeItem) {
|
|
281
|
+
activeItem.classList.add("active");
|
|
282
|
+
activeItem.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Update URL
|
|
286
|
+
if (!skipHistory) {
|
|
287
|
+
const url = new URL(location.href);
|
|
288
|
+
url.searchParams.set("doc", id);
|
|
289
|
+
url.hash = anchor ? `#${anchor}` : "";
|
|
290
|
+
history.pushState({ docId: id, anchor: anchor || null }, "", url);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
document.getElementById("welcome").classList.add("hidden");
|
|
294
|
+
const docView = document.getElementById("doc-view");
|
|
295
|
+
docView.classList.remove("hidden");
|
|
296
|
+
document.getElementById("doc-content").innerHTML =
|
|
297
|
+
`<p class="animate-pulse text-gray-400">${window.t('common.loading')}</p>`;
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const doc = await fetch("/api/documents/" + id).then((r) => {
|
|
301
|
+
if (!r.ok) throw new Error(r.statusText);
|
|
302
|
+
return r.json();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
currentDocContent = doc.content;
|
|
306
|
+
exitEditMode();
|
|
307
|
+
|
|
308
|
+
document.getElementById("doc-title").textContent = doc.title;
|
|
309
|
+
{
|
|
310
|
+
const crumbs = document.getElementById("doc-breadcrumbs");
|
|
311
|
+
const folderPills = (doc.folder || []).map(
|
|
312
|
+
(seg) =>
|
|
313
|
+
`<span title="${esc(seg)}" class="inline-block text-xs font-semibold px-2.5 py-0.5 rounded-full bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-300">${esc(folderLabel(seg))}</span>`,
|
|
314
|
+
);
|
|
315
|
+
const catPill = `<span class="inline-block text-xs font-semibold px-2.5 py-0.5 rounded-full bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300">${esc(doc.category)}</span>`;
|
|
316
|
+
crumbs.innerHTML = [...folderPills, catPill].join("");
|
|
317
|
+
}
|
|
318
|
+
document.getElementById("doc-date").textContent =
|
|
319
|
+
doc.formattedDate || "";
|
|
320
|
+
|
|
321
|
+
_lastDocHtml = doc.html;
|
|
322
|
+
_lastDocIdRendered = id;
|
|
323
|
+
_wireDocContent(doc.html);
|
|
324
|
+
|
|
325
|
+
if (typeof updateValidateButtonForCurrentDoc === "function") {
|
|
326
|
+
updateValidateButtonForCurrentDoc();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Load annotations for this document
|
|
330
|
+
loadAnnotations(id);
|
|
331
|
+
|
|
332
|
+
// Load source-file metadata report (drives the accuracy gauge)
|
|
333
|
+
if (typeof loadMetadataReport === "function") {
|
|
334
|
+
loadMetadataReport(id);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
document.title = doc.title;
|
|
338
|
+
|
|
339
|
+
// Scroll to anchor if present (explicit param wins over URL hash)
|
|
340
|
+
const targetAnchor =
|
|
341
|
+
anchor || (window.location.hash ? window.location.hash.slice(1) : "");
|
|
342
|
+
if (targetAnchor) {
|
|
343
|
+
scrollToAnchor(targetAnchor);
|
|
344
|
+
} else {
|
|
345
|
+
document.getElementById("content-area").scrollTop = 0;
|
|
346
|
+
}
|
|
347
|
+
} catch (err) {
|
|
348
|
+
document.getElementById("doc-content").innerHTML =
|
|
349
|
+
`<p class="text-red-500">${window.t('doc.failed_to_load')}${err.message}</p>`;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ── Edit mode ────────────────────────────────────────────────────────────────
|
|
354
|
+
let _editScrollTop = 0;
|
|
355
|
+
|
|
356
|
+
function enterEditMode() {
|
|
357
|
+
_editScrollTop = document.getElementById("content-area").scrollTop;
|
|
358
|
+
const editor = document.getElementById("doc-editor");
|
|
359
|
+
editor.value = currentDocContent;
|
|
360
|
+
document.getElementById("doc-content").classList.add("hidden");
|
|
361
|
+
editor.classList.remove("hidden");
|
|
362
|
+
document.getElementById("view-actions").classList.add("hidden");
|
|
363
|
+
document.getElementById("edit-actions").classList.remove("hidden");
|
|
364
|
+
editor.focus();
|
|
365
|
+
editor.addEventListener("paste", handleEditorPaste);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function exitEditMode() {
|
|
369
|
+
const editor = document.getElementById("doc-editor");
|
|
370
|
+
editor.removeEventListener("paste", handleEditorPaste);
|
|
371
|
+
editor.classList.add("hidden");
|
|
372
|
+
document.getElementById("doc-content").classList.remove("hidden");
|
|
373
|
+
document.getElementById("edit-actions").classList.add("hidden");
|
|
374
|
+
document.getElementById("view-actions").classList.remove("hidden");
|
|
375
|
+
document.getElementById("edit-save-msg").textContent = "";
|
|
376
|
+
document.getElementById("content-area").scrollTop = _editScrollTop;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── Delete ───────────────────────────────────────────────────────────────────
|
|
380
|
+
function askDeleteDocument() {
|
|
381
|
+
if (!currentDocId) return;
|
|
382
|
+
const doc = allDocs.find((d) => d.id === currentDocId);
|
|
383
|
+
const titleEl = document.getElementById("doc-confirm-delete-title");
|
|
384
|
+
if (titleEl) titleEl.textContent = doc ? doc.title : "";
|
|
385
|
+
document.getElementById("doc-confirm-delete").classList.remove("hidden");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function cancelDeleteDocument(e) {
|
|
389
|
+
if (e && e.target && e.target.id && e.target.id !== "doc-confirm-delete") {
|
|
390
|
+
// clicked inside the card, not the backdrop
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
document.getElementById("doc-confirm-delete").classList.add("hidden");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function confirmDeleteDocument() {
|
|
397
|
+
if (!currentDocId) return;
|
|
398
|
+
const deletedId = currentDocId;
|
|
399
|
+
try {
|
|
400
|
+
const r = await fetch(
|
|
401
|
+
"/api/documents/" + encodeURIComponent(deletedId),
|
|
402
|
+
{ method: "DELETE" },
|
|
403
|
+
);
|
|
404
|
+
if (!r.ok) throw new Error("delete failed");
|
|
405
|
+
} catch {
|
|
406
|
+
document.getElementById("doc-confirm-delete").classList.add("hidden");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
document.getElementById("doc-confirm-delete").classList.add("hidden");
|
|
410
|
+
|
|
411
|
+
// Drop from local state
|
|
412
|
+
allDocs = allDocs.filter((d) => d.id !== deletedId);
|
|
413
|
+
if (Array.isArray(searchResults)) {
|
|
414
|
+
searchResults = searchResults.filter((d) => d.id !== deletedId);
|
|
415
|
+
}
|
|
416
|
+
delete annotationCounts[deletedId];
|
|
417
|
+
try {
|
|
418
|
+
delete annotationCounts[decodeURIComponent(deletedId)];
|
|
419
|
+
} catch {}
|
|
420
|
+
delete fileAttachmentCounts[deletedId];
|
|
421
|
+
currentDocId = null;
|
|
422
|
+
|
|
423
|
+
// Return to welcome screen
|
|
424
|
+
document.getElementById("doc-view").classList.add("hidden");
|
|
425
|
+
document.getElementById("welcome").classList.remove("hidden");
|
|
426
|
+
history.pushState({}, "", window.location.pathname);
|
|
427
|
+
|
|
428
|
+
refreshSidebar();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── Save (in-place edit) ─────────────────────────────────────────────────────
|
|
432
|
+
async function saveDocument() {
|
|
433
|
+
if (!currentDocId) return;
|
|
434
|
+
const content = document.getElementById("doc-editor").value;
|
|
435
|
+
const msgEl = document.getElementById("edit-save-msg");
|
|
436
|
+
msgEl.textContent = window.t('doc.saving');
|
|
437
|
+
msgEl.className = "text-xs text-gray-400";
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
const res = await fetch("/api/documents/" + currentDocId, {
|
|
441
|
+
method: "PUT",
|
|
442
|
+
headers: { "Content-Type": "application/json" },
|
|
443
|
+
body: JSON.stringify({ content }),
|
|
444
|
+
});
|
|
445
|
+
if (!res.ok) throw new Error(await res.text());
|
|
446
|
+
|
|
447
|
+
currentDocContent = content;
|
|
448
|
+
|
|
449
|
+
// Re-fetch rendered HTML and update view
|
|
450
|
+
const doc = await fetch("/api/documents/" + currentDocId).then((r) =>
|
|
451
|
+
r.json(),
|
|
452
|
+
);
|
|
453
|
+
_lastDocHtml = doc.html;
|
|
454
|
+
_lastDocIdRendered = currentDocId;
|
|
455
|
+
_wireDocContent(doc.html);
|
|
456
|
+
|
|
457
|
+
applyAnnotationHighlights();
|
|
458
|
+
renderElevator();
|
|
459
|
+
|
|
460
|
+
const fileLinkMatches = content.match(/\]\(\s*\.?\/files\/[^)\s]+/g);
|
|
461
|
+
const fileLinkCount = fileLinkMatches ? fileLinkMatches.length : 0;
|
|
462
|
+
if (fileLinkCount > 0) fileAttachmentCounts[currentDocId] = fileLinkCount;
|
|
463
|
+
else delete fileAttachmentCounts[currentDocId];
|
|
464
|
+
refreshSidebar();
|
|
465
|
+
|
|
466
|
+
exitEditMode();
|
|
467
|
+
} catch (err) {
|
|
468
|
+
msgEl.textContent = window.t('error.save') + err.message;
|
|
469
|
+
msgEl.className = "text-xs text-red-500 dark:text-red-400";
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ── Back breadcrumb navigation ──────────────────────────────────────────────
|
|
474
|
+
function goBackToIndex(i) {
|
|
475
|
+
const entry = navHistory[i];
|
|
476
|
+
if (!entry) return;
|
|
477
|
+
navHistory = navHistory.slice(0, i); // drop this entry and everything after
|
|
478
|
+
openDocument(entry.id, false, "restore");
|
|
479
|
+
}
|