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,318 @@
|
|
|
1
|
+
// ── Metadata (source-file dependencies) ─────────────────────────────────────
|
|
2
|
+
// Exposes: openMetadataModal(), closeMetadataModal(),
|
|
3
|
+
// metadataRefresh(), metadataAddPath(), metadataRemovePath(),
|
|
4
|
+
// loadMetadataReport(docId) → used by accuracy-gauge.js
|
|
5
|
+
|
|
6
|
+
let metadataReport = null;
|
|
7
|
+
let metadataBrowseCurrent = ""; // relative to sourceRoot
|
|
8
|
+
let metadataBrowseCache = null;
|
|
9
|
+
|
|
10
|
+
function metadataCurrentDocId() {
|
|
11
|
+
return typeof currentDocId !== "undefined" ? currentDocId : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function loadMetadataReport(docId) {
|
|
15
|
+
if (!docId) {
|
|
16
|
+
metadataReport = null;
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const r = await fetch(
|
|
21
|
+
"/api/metadata/" + encodeURIComponent(docId),
|
|
22
|
+
);
|
|
23
|
+
if (!r.ok) throw new Error(r.statusText);
|
|
24
|
+
metadataReport = await r.json();
|
|
25
|
+
} catch {
|
|
26
|
+
metadataReport = null;
|
|
27
|
+
}
|
|
28
|
+
if (typeof renderAccuracyGauge === "function") {
|
|
29
|
+
renderAccuracyGauge(metadataReport);
|
|
30
|
+
}
|
|
31
|
+
return metadataReport;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function statusBadge(status) {
|
|
35
|
+
if (status === "unchanged") {
|
|
36
|
+
return `<span class="inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300">
|
|
37
|
+
<i class="fa-solid fa-check"></i>
|
|
38
|
+
<span data-i18n="metadata.status.unchanged">Unchanged</span>
|
|
39
|
+
</span>`;
|
|
40
|
+
}
|
|
41
|
+
if (status === "modified") {
|
|
42
|
+
return `<span class="inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300">
|
|
43
|
+
<i class="fa-solid fa-triangle-exclamation"></i>
|
|
44
|
+
<span data-i18n="metadata.status.modified">Modified</span>
|
|
45
|
+
</span>`;
|
|
46
|
+
}
|
|
47
|
+
return `<span class="inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300">
|
|
48
|
+
<i class="fa-solid fa-circle-xmark"></i>
|
|
49
|
+
<span data-i18n="metadata.status.missing">Missing</span>
|
|
50
|
+
</span>`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function renderMetadataList() {
|
|
54
|
+
const listEl = document.getElementById("metadata-list");
|
|
55
|
+
const emptyEl = document.getElementById("metadata-empty");
|
|
56
|
+
if (!listEl || !emptyEl) return;
|
|
57
|
+
|
|
58
|
+
const items = (metadataReport && metadataReport.items) || [];
|
|
59
|
+
if (items.length === 0) {
|
|
60
|
+
listEl.innerHTML = "";
|
|
61
|
+
emptyEl.classList.remove("hidden");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
emptyEl.classList.add("hidden");
|
|
65
|
+
|
|
66
|
+
listEl.innerHTML = items
|
|
67
|
+
.map((it) => {
|
|
68
|
+
const safePath = esc(it.path);
|
|
69
|
+
return `<div class="flex items-center gap-2 px-3 py-2 border-b border-gray-100 dark:border-gray-800">
|
|
70
|
+
<div class="flex-1 min-w-0">
|
|
71
|
+
<div class="text-sm font-mono truncate text-gray-800 dark:text-gray-200" title="${safePath}">${safePath}</div>
|
|
72
|
+
</div>
|
|
73
|
+
${statusBadge(it.status)}
|
|
74
|
+
<button
|
|
75
|
+
onclick="metadataRemovePath('${safePath.replace(/'/g, "\\'")}')"
|
|
76
|
+
data-i18n-title="metadata.remove"
|
|
77
|
+
title="Remove"
|
|
78
|
+
class="metadata-row-remove text-xs px-2 py-1 rounded-lg border border-red-200 dark:border-red-700 text-red-500 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/40 transition-colors"
|
|
79
|
+
>
|
|
80
|
+
<i class="fa-solid fa-trash"></i>
|
|
81
|
+
</button>
|
|
82
|
+
</div>`;
|
|
83
|
+
})
|
|
84
|
+
.join("");
|
|
85
|
+
|
|
86
|
+
if (typeof window.applyI18n === "function") window.applyI18n();
|
|
87
|
+
applyMetadataReadOnlyMode();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Hide the three metadata-mutation controls (and the row-level trash icons)
|
|
91
|
+
// when the current document's frontmatter status is `SuperSeeded`. The server
|
|
92
|
+
// also rejects these mutations independently — this is purely UX. Re-applied
|
|
93
|
+
// after every list re-render so dynamically generated rows stay consistent.
|
|
94
|
+
function applyMetadataReadOnlyMode() {
|
|
95
|
+
const readOnly =
|
|
96
|
+
typeof window.getDocStatus === "function" &&
|
|
97
|
+
window.getDocStatus(
|
|
98
|
+
typeof currentDocContent !== "undefined" ? currentDocContent : "",
|
|
99
|
+
) === "SuperSeeded";
|
|
100
|
+
|
|
101
|
+
const refreshBtn = document.getElementById("metadata-refresh-btn");
|
|
102
|
+
const addBtn = document.getElementById("metadata-add-btn");
|
|
103
|
+
const banner = document.getElementById("metadata-readonly-banner");
|
|
104
|
+
if (refreshBtn) refreshBtn.classList.toggle("hidden", readOnly);
|
|
105
|
+
if (addBtn) addBtn.classList.toggle("hidden", readOnly);
|
|
106
|
+
if (banner) banner.classList.toggle("hidden", !readOnly);
|
|
107
|
+
document
|
|
108
|
+
.querySelectorAll("#metadata-list .metadata-row-remove")
|
|
109
|
+
.forEach((el) => el.classList.toggle("hidden", readOnly));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
window.applyMetadataReadOnlyMode = applyMetadataReadOnlyMode;
|
|
113
|
+
|
|
114
|
+
function renderMetadataSummary() {
|
|
115
|
+
const el = document.getElementById("metadata-summary");
|
|
116
|
+
if (!el) return;
|
|
117
|
+
if (!metadataReport || metadataReport.total === 0) {
|
|
118
|
+
el.textContent = "";
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const pct = Math.round(metadataReport.accuracy * 100);
|
|
122
|
+
const { total, unchanged, modified, missing } = metadataReport;
|
|
123
|
+
el.innerHTML = `<span class="font-semibold">${pct}%</span> · ${unchanged}/${total} ${window.t("metadata.status.unchanged")} · ${modified} ${window.t("metadata.status.modified")} · ${missing} ${window.t("metadata.status.missing")}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function openMetadataModal() {
|
|
127
|
+
const docId = metadataCurrentDocId();
|
|
128
|
+
if (!docId) return;
|
|
129
|
+
await loadMetadataReport(docId);
|
|
130
|
+
renderMetadataList();
|
|
131
|
+
renderMetadataSummary();
|
|
132
|
+
document.getElementById("metadata-modal").classList.remove("hidden");
|
|
133
|
+
document.getElementById("metadata-error").classList.add("hidden");
|
|
134
|
+
// Reset browser
|
|
135
|
+
metadataBrowseCurrent = "";
|
|
136
|
+
metadataBrowseCache = null;
|
|
137
|
+
document.getElementById("metadata-browser").classList.add("hidden");
|
|
138
|
+
applyMetadataReadOnlyMode();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function closeMetadataModal() {
|
|
142
|
+
document.getElementById("metadata-modal").classList.add("hidden");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function metadataRefresh() {
|
|
146
|
+
const docId = metadataCurrentDocId();
|
|
147
|
+
if (!docId) return;
|
|
148
|
+
const btn = document.getElementById("metadata-refresh-btn");
|
|
149
|
+
if (btn) btn.disabled = true;
|
|
150
|
+
try {
|
|
151
|
+
const r = await fetch(
|
|
152
|
+
"/api/metadata/" + encodeURIComponent(docId) + "/refresh",
|
|
153
|
+
{ method: "POST" },
|
|
154
|
+
);
|
|
155
|
+
if (!r.ok) throw new Error(r.statusText);
|
|
156
|
+
metadataReport = await r.json();
|
|
157
|
+
renderMetadataList();
|
|
158
|
+
renderMetadataSummary();
|
|
159
|
+
if (typeof renderAccuracyGauge === "function") {
|
|
160
|
+
renderAccuracyGauge(metadataReport);
|
|
161
|
+
}
|
|
162
|
+
} catch (err) {
|
|
163
|
+
showMetadataError(err.message);
|
|
164
|
+
} finally {
|
|
165
|
+
if (btn) btn.disabled = false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function metadataRemovePath(path) {
|
|
170
|
+
const docId = metadataCurrentDocId();
|
|
171
|
+
if (!docId) return;
|
|
172
|
+
try {
|
|
173
|
+
const r = await fetch(
|
|
174
|
+
"/api/metadata/" + encodeURIComponent(docId),
|
|
175
|
+
{
|
|
176
|
+
method: "DELETE",
|
|
177
|
+
headers: { "Content-Type": "application/json" },
|
|
178
|
+
body: JSON.stringify({ path }),
|
|
179
|
+
},
|
|
180
|
+
);
|
|
181
|
+
if (!r.ok) throw new Error(r.statusText);
|
|
182
|
+
metadataReport = await r.json();
|
|
183
|
+
renderMetadataList();
|
|
184
|
+
renderMetadataSummary();
|
|
185
|
+
if (typeof renderAccuracyGauge === "function") {
|
|
186
|
+
renderAccuracyGauge(metadataReport);
|
|
187
|
+
}
|
|
188
|
+
refreshBrowserIfOpen();
|
|
189
|
+
} catch (err) {
|
|
190
|
+
showMetadataError(err.message);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function metadataAddPath(relPath) {
|
|
195
|
+
const docId = metadataCurrentDocId();
|
|
196
|
+
if (!docId) return;
|
|
197
|
+
try {
|
|
198
|
+
const r = await fetch(
|
|
199
|
+
"/api/metadata/" + encodeURIComponent(docId),
|
|
200
|
+
{
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: { "Content-Type": "application/json" },
|
|
203
|
+
body: JSON.stringify({ path: relPath }),
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
if (!r.ok) {
|
|
207
|
+
const body = await r.json().catch(() => ({}));
|
|
208
|
+
throw new Error(body.error || r.statusText);
|
|
209
|
+
}
|
|
210
|
+
metadataReport = await r.json();
|
|
211
|
+
renderMetadataList();
|
|
212
|
+
renderMetadataSummary();
|
|
213
|
+
if (typeof renderAccuracyGauge === "function") {
|
|
214
|
+
renderAccuracyGauge(metadataReport);
|
|
215
|
+
}
|
|
216
|
+
refreshBrowserIfOpen();
|
|
217
|
+
} catch (err) {
|
|
218
|
+
showMetadataError(err.message);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function refreshBrowserIfOpen() {
|
|
223
|
+
const b = document.getElementById("metadata-browser");
|
|
224
|
+
if (b && !b.classList.contains("hidden")) {
|
|
225
|
+
metadataBrowseLoad(metadataBrowseCurrent);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function showMetadataError(msg) {
|
|
230
|
+
const el = document.getElementById("metadata-error");
|
|
231
|
+
if (!el) return;
|
|
232
|
+
el.textContent = window.t("common.error_prefix") + msg;
|
|
233
|
+
el.classList.remove("hidden");
|
|
234
|
+
setTimeout(() => el.classList.add("hidden"), 5000);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Source browser ─────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
function metadataToggleBrowser() {
|
|
240
|
+
const b = document.getElementById("metadata-browser");
|
|
241
|
+
if (b.classList.contains("hidden")) {
|
|
242
|
+
b.classList.remove("hidden");
|
|
243
|
+
metadataBrowseLoad("");
|
|
244
|
+
} else {
|
|
245
|
+
b.classList.add("hidden");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function metadataBrowseLoad(relPath) {
|
|
250
|
+
const listEl = document.getElementById("metadata-browse-list");
|
|
251
|
+
const pathEl = document.getElementById("metadata-browse-path");
|
|
252
|
+
const upBtn = document.getElementById("metadata-browse-up");
|
|
253
|
+
if (!listEl) return;
|
|
254
|
+
listEl.innerHTML = `<div class="px-3 py-2 text-xs text-gray-400">${esc(window.t("common.loading"))}</div>`;
|
|
255
|
+
try {
|
|
256
|
+
const r = await fetch(
|
|
257
|
+
"/api/browse-source?path=" + encodeURIComponent(relPath || ""),
|
|
258
|
+
);
|
|
259
|
+
if (!r.ok) {
|
|
260
|
+
const body = await r.json().catch(() => ({}));
|
|
261
|
+
throw new Error(body.error || r.statusText);
|
|
262
|
+
}
|
|
263
|
+
const data = await r.json();
|
|
264
|
+
metadataBrowseCache = data;
|
|
265
|
+
metadataBrowseCurrent = data.current || "";
|
|
266
|
+
pathEl.textContent = data.current ? "/" + data.current : "/ (sourceRoot)";
|
|
267
|
+
upBtn.disabled = data.parent === null;
|
|
268
|
+
upBtn.classList.toggle("opacity-30", data.parent === null);
|
|
269
|
+
upBtn.classList.toggle("pointer-events-none", data.parent === null);
|
|
270
|
+
|
|
271
|
+
const dirRows = data.dirs.map(
|
|
272
|
+
(d) => `<button
|
|
273
|
+
onclick="metadataBrowseLoad('${d.path.replace(/'/g, "\\'")}')"
|
|
274
|
+
class="w-full text-left px-3 py-1.5 text-sm hover:bg-gray-50 dark:hover:bg-gray-800 flex items-center gap-2"
|
|
275
|
+
>
|
|
276
|
+
<i class="fa-solid fa-folder text-yellow-500"></i>
|
|
277
|
+
<span class="truncate">${esc(d.name)}</span>
|
|
278
|
+
</button>`,
|
|
279
|
+
);
|
|
280
|
+
const attached = new Set(
|
|
281
|
+
((metadataReport && metadataReport.items) || []).map((it) => it.path),
|
|
282
|
+
);
|
|
283
|
+
const fileRows = data.files
|
|
284
|
+
.filter((f) => !attached.has(f.path))
|
|
285
|
+
.map(
|
|
286
|
+
(f) => `<button
|
|
287
|
+
onclick="metadataAddPath('${f.path.replace(/'/g, "\\'")}')"
|
|
288
|
+
class="w-full text-left px-3 py-1.5 text-sm hover:bg-blue-50 dark:hover:bg-blue-900/30 flex items-center gap-2"
|
|
289
|
+
>
|
|
290
|
+
<i class="fa-solid fa-file text-gray-400"></i>
|
|
291
|
+
<span class="truncate">${esc(f.name)}</span>
|
|
292
|
+
<i class="fa-solid fa-plus ml-auto text-blue-500 text-xs"></i>
|
|
293
|
+
</button>`,
|
|
294
|
+
);
|
|
295
|
+
const rows = [...dirRows, ...fileRows];
|
|
296
|
+
listEl.innerHTML = rows.length
|
|
297
|
+
? rows.join("")
|
|
298
|
+
: `<div class="px-3 py-2 text-xs text-gray-400" data-i18n="common.empty_dir">${esc(window.t("common.empty_dir"))}</div>`;
|
|
299
|
+
} catch (err) {
|
|
300
|
+
listEl.innerHTML = `<div class="px-3 py-2 text-xs text-red-500">${esc(err.message)}</div>`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function metadataBrowseUp() {
|
|
305
|
+
if (!metadataBrowseCache || metadataBrowseCache.parent === null) return;
|
|
306
|
+
metadataBrowseLoad(metadataBrowseCache.parent);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Expose
|
|
310
|
+
window.openMetadataModal = openMetadataModal;
|
|
311
|
+
window.closeMetadataModal = closeMetadataModal;
|
|
312
|
+
window.metadataRefresh = metadataRefresh;
|
|
313
|
+
window.metadataRemovePath = metadataRemovePath;
|
|
314
|
+
window.metadataAddPath = metadataAddPath;
|
|
315
|
+
window.metadataToggleBrowser = metadataToggleBrowser;
|
|
316
|
+
window.metadataBrowseLoad = metadataBrowseLoad;
|
|
317
|
+
window.metadataBrowseUp = metadataBrowseUp;
|
|
318
|
+
window.loadMetadataReport = loadMetadataReport;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// ── Misc viewer helpers ─────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
const DOC_ID_COPY_FEEDBACK_MS = 1800;
|
|
4
|
+
|
|
5
|
+
async function writeClipboardText(text) {
|
|
6
|
+
try {
|
|
7
|
+
await navigator.clipboard.writeText(text);
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
const ta = document.createElement("textarea");
|
|
11
|
+
ta.value = text;
|
|
12
|
+
ta.style.position = "fixed";
|
|
13
|
+
ta.style.opacity = "0";
|
|
14
|
+
document.body.appendChild(ta);
|
|
15
|
+
ta.select();
|
|
16
|
+
let copied = false;
|
|
17
|
+
try {
|
|
18
|
+
copied = document.execCommand("copy");
|
|
19
|
+
} catch {
|
|
20
|
+
copied = false;
|
|
21
|
+
}
|
|
22
|
+
document.body.removeChild(ta);
|
|
23
|
+
return copied;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function applyFullWidthState(isWide) {
|
|
28
|
+
const article = document.getElementById("doc-view");
|
|
29
|
+
const btn = document.getElementById("full-width-btn");
|
|
30
|
+
if (!article || !btn) return;
|
|
31
|
+
article.classList.toggle("max-w-none", isWide);
|
|
32
|
+
article.classList.toggle("max-w-4xl", !isWide);
|
|
33
|
+
article.classList.toggle("mx-auto", !isWide);
|
|
34
|
+
btn.textContent = isWide ? window.t('doc.full_width_narrow_btn') : window.t('doc.full_width_btn');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toggleFullWidth() {
|
|
38
|
+
const article = document.getElementById("doc-view");
|
|
39
|
+
const isWide = !article.classList.contains("max-w-none");
|
|
40
|
+
applyFullWidthState(isWide);
|
|
41
|
+
try {
|
|
42
|
+
localStorage.setItem("ld-full-width", isWide ? "1" : "0");
|
|
43
|
+
} catch {
|
|
44
|
+
/* ignore */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function initFullWidthState() {
|
|
49
|
+
let isWide = false;
|
|
50
|
+
try {
|
|
51
|
+
isWide = localStorage.getItem("ld-full-width") === "1";
|
|
52
|
+
} catch {
|
|
53
|
+
/* ignore */
|
|
54
|
+
}
|
|
55
|
+
applyFullWidthState(isWide);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function copyLink() {
|
|
59
|
+
const copied = await writeClipboardText(location.href);
|
|
60
|
+
if (!copied) return;
|
|
61
|
+
const btn = document.getElementById("copy-link-btn");
|
|
62
|
+
const orig = btn.innerHTML;
|
|
63
|
+
btn.textContent = window.t('doc.copied');
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
btn.innerHTML = orig;
|
|
66
|
+
}, DOC_ID_COPY_FEEDBACK_MS);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function copyCurrentDocMcpId() {
|
|
70
|
+
if (!currentDocId) return;
|
|
71
|
+
const btn = document.getElementById("copy-doc-id-btn");
|
|
72
|
+
if (!btn) return;
|
|
73
|
+
const copyLabel = window.t("doc.copy_mcp_id");
|
|
74
|
+
const copiedLabel = window.t("doc.copy_mcp_id_copied");
|
|
75
|
+
const originalHtml = btn.innerHTML;
|
|
76
|
+
const docId = decodeURIComponent(currentDocId);
|
|
77
|
+
const copied = await writeClipboardText(docId);
|
|
78
|
+
if (!copied) return;
|
|
79
|
+
|
|
80
|
+
btn.title = copiedLabel;
|
|
81
|
+
btn.classList.add("text-green-600", "dark:text-green-400");
|
|
82
|
+
btn.innerHTML = '<i class="fa-solid fa-check" aria-hidden="true"></i>';
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
btn.title = copyLabel;
|
|
85
|
+
btn.classList.remove("text-green-600", "dark:text-green-400");
|
|
86
|
+
btn.innerHTML = originalHtml;
|
|
87
|
+
}, DOC_ID_COPY_FEEDBACK_MS);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function exportPDF() {
|
|
91
|
+
window.print();
|
|
92
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
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
|
+
function newDocNormalizeCategory(raw) {
|
|
12
|
+
return (raw || "")
|
|
13
|
+
.normalize("NFD")
|
|
14
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
15
|
+
.toUpperCase()
|
|
16
|
+
.replace(/[^A-Z0-9_-]/g, "");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function newDocSanitizeCategoryInput() {
|
|
20
|
+
const input = document.getElementById("new-doc-category");
|
|
21
|
+
const normalized = newDocNormalizeCategory(input.value);
|
|
22
|
+
if (input.value !== normalized) input.value = normalized;
|
|
23
|
+
newDocUpdatePreview();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function newDocPopulateCategoryOptions() {
|
|
27
|
+
const list = document.getElementById("new-doc-category-options");
|
|
28
|
+
if (!list) return;
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
(allDocs || []).forEach((d) => {
|
|
31
|
+
const cat = newDocNormalizeCategory(d.category || "");
|
|
32
|
+
if (cat) seen.add(cat);
|
|
33
|
+
});
|
|
34
|
+
const sorted = Array.from(seen).sort((a, b) => a.localeCompare(b));
|
|
35
|
+
list.innerHTML = sorted
|
|
36
|
+
.map((cat) => `<option value="${esc(cat)}"></option>`)
|
|
37
|
+
.join("");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function openNewDocModal() {
|
|
41
|
+
try {
|
|
42
|
+
const cfg = await fetch("/api/config").then((r) => r.json());
|
|
43
|
+
_newDocDocsFolder = cfg.docsFolder || "";
|
|
44
|
+
_newDocPattern =
|
|
45
|
+
cfg.filenamePattern || "YYYY_MM_DD_HH_mm_[Category]_title";
|
|
46
|
+
} catch {
|
|
47
|
+
_newDocDocsFolder = "";
|
|
48
|
+
_newDocPattern = "YYYY_MM_DD_HH_mm_[Category]_title";
|
|
49
|
+
}
|
|
50
|
+
// Pre-fill from currently open document if any
|
|
51
|
+
const currentDoc =
|
|
52
|
+
currentDocId && allDocs.find((d) => d.id === currentDocId);
|
|
53
|
+
const prefillCategory =
|
|
54
|
+
newDocNormalizeCategory(
|
|
55
|
+
(currentDoc && currentDoc.category) || "General",
|
|
56
|
+
) || "GENERAL";
|
|
57
|
+
// Derive folder from currentDocId (encoded relative path, e.g. "1_tutorial%2Fsome_file")
|
|
58
|
+
// For extra files (absolute paths), skip.
|
|
59
|
+
let prefillFolder = "";
|
|
60
|
+
if (currentDocId) {
|
|
61
|
+
const decodedId = decodeURIComponent(currentDocId);
|
|
62
|
+
if (!decodedId.startsWith("/")) {
|
|
63
|
+
const segments = decodedId.split("/");
|
|
64
|
+
if (segments.length > 1) {
|
|
65
|
+
prefillFolder = segments.slice(0, -1).join("/");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const prefillFolderAbs = prefillFolder
|
|
70
|
+
? _newDocDocsFolder + "/" + prefillFolder
|
|
71
|
+
: "";
|
|
72
|
+
|
|
73
|
+
_newDocSelectedFolder = prefillFolder;
|
|
74
|
+
_newDocBrowseCurrent = prefillFolderAbs || null;
|
|
75
|
+
_newDocBrowseParent = null;
|
|
76
|
+
newDocPopulateCategoryOptions();
|
|
77
|
+
document.getElementById("new-doc-title").value = "";
|
|
78
|
+
document.getElementById("new-doc-category").value = prefillCategory;
|
|
79
|
+
document.getElementById("new-doc-folder-display").textContent =
|
|
80
|
+
prefillFolder ? "/" + prefillFolder : "/ (root)";
|
|
81
|
+
document.getElementById("new-doc-browser").classList.add("hidden");
|
|
82
|
+
document.getElementById("new-doc-new-folder-name").value = "";
|
|
83
|
+
document.getElementById("new-doc-error").classList.add("hidden");
|
|
84
|
+
const createBtn = document.getElementById("new-doc-create-btn");
|
|
85
|
+
createBtn.disabled = false;
|
|
86
|
+
createBtn.textContent = window.t('common.create');
|
|
87
|
+
newDocUpdatePreview();
|
|
88
|
+
document.getElementById("new-doc-modal").classList.remove("hidden");
|
|
89
|
+
setTimeout(() => document.getElementById("new-doc-title").focus(), 50);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function closeNewDocModal() {
|
|
93
|
+
document.getElementById("new-doc-modal").classList.add("hidden");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function newDocToggleBrowser() {
|
|
97
|
+
const browser = document.getElementById("new-doc-browser");
|
|
98
|
+
const isHidden = browser.classList.toggle("hidden");
|
|
99
|
+
if (!isHidden)
|
|
100
|
+
newDocLoadBrowse(_newDocBrowseCurrent || _newDocDocsFolder);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function newDocLoadBrowse(dirPath) {
|
|
104
|
+
const list = document.getElementById("new-doc-browse-list");
|
|
105
|
+
list.innerHTML =
|
|
106
|
+
`<p class="px-3 py-4 text-xs text-gray-400 text-center">${window.t('common.loading')}</p>`;
|
|
107
|
+
try {
|
|
108
|
+
const data = await fetch(
|
|
109
|
+
"/api/browse?path=" + encodeURIComponent(dirPath),
|
|
110
|
+
).then((r) => r.json());
|
|
111
|
+
_newDocBrowseCurrent = data.current;
|
|
112
|
+
_newDocBrowseParent = data.parent;
|
|
113
|
+
_newDocSelectedFolder = _newDocAbsToRel(data.current);
|
|
114
|
+
|
|
115
|
+
document.getElementById("new-doc-browse-path").textContent =
|
|
116
|
+
data.current;
|
|
117
|
+
const atRoot = data.current === _newDocDocsFolder;
|
|
118
|
+
document.getElementById("new-doc-browse-up").disabled = atRoot;
|
|
119
|
+
document.getElementById("new-doc-folder-display").textContent =
|
|
120
|
+
_newDocSelectedFolder ? "/" + _newDocSelectedFolder : "/ (root)";
|
|
121
|
+
newDocUpdatePreview();
|
|
122
|
+
|
|
123
|
+
list.innerHTML = data.dirs.length
|
|
124
|
+
? data.dirs
|
|
125
|
+
.map(
|
|
126
|
+
(dir) => `
|
|
127
|
+
<button data-path="${esc(dir.path)}" onclick="newDocLoadBrowse(this.dataset.path)"
|
|
128
|
+
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">
|
|
129
|
+
<span class="text-gray-400 shrink-0">📁</span>
|
|
130
|
+
<span class="text-gray-700 dark:text-gray-300 truncate">${esc(dir.name)}</span>
|
|
131
|
+
</button>`,
|
|
132
|
+
)
|
|
133
|
+
.join("")
|
|
134
|
+
: `<p class="px-3 py-3 text-xs text-gray-400 text-center">${window.t('modal.new_doc.no_subfolders')}</p>`;
|
|
135
|
+
} catch {
|
|
136
|
+
list.innerHTML =
|
|
137
|
+
`<p class="px-3 py-4 text-xs text-red-400 text-center">${window.t('common.cannot_read_dir')}</p>`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function newDocBrowseUp() {
|
|
142
|
+
if (_newDocBrowseCurrent !== _newDocDocsFolder && _newDocBrowseParent) {
|
|
143
|
+
newDocLoadBrowse(_newDocBrowseParent);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function _newDocAbsToRel(absPath) {
|
|
148
|
+
const base = _newDocDocsFolder;
|
|
149
|
+
if (absPath === base) return "";
|
|
150
|
+
if (absPath.startsWith(base + "/"))
|
|
151
|
+
return absPath.slice(base.length + 1);
|
|
152
|
+
return absPath;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function newDocCreateFolder() {
|
|
156
|
+
const name = document
|
|
157
|
+
.getElementById("new-doc-new-folder-name")
|
|
158
|
+
.value.trim();
|
|
159
|
+
if (!name) return;
|
|
160
|
+
const parent = _newDocBrowseCurrent || _newDocDocsFolder;
|
|
161
|
+
const atDocsRoot = parent === _newDocDocsFolder;
|
|
162
|
+
const errEl = document.getElementById("new-doc-error");
|
|
163
|
+
if (atDocsRoot && (name === "files" || name === "images")) {
|
|
164
|
+
if (errEl) {
|
|
165
|
+
errEl.textContent = window.t("modal.new_folder.error_reserved");
|
|
166
|
+
errEl.classList.remove("hidden");
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (errEl) errEl.classList.add("hidden");
|
|
171
|
+
const newRelPath =
|
|
172
|
+
(_newDocAbsToRel(parent) ? _newDocAbsToRel(parent) + "/" : "") + name;
|
|
173
|
+
_newDocSelectedFolder = newRelPath;
|
|
174
|
+
document.getElementById("new-doc-folder-display").textContent =
|
|
175
|
+
"/" + newRelPath;
|
|
176
|
+
document.getElementById("new-doc-new-folder-name").value = "";
|
|
177
|
+
newDocUpdatePreview();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function newDocUpdatePreview() {
|
|
181
|
+
const title = document.getElementById("new-doc-title").value.trim();
|
|
182
|
+
const category =
|
|
183
|
+
newDocNormalizeCategory(
|
|
184
|
+
document.getElementById("new-doc-category").value,
|
|
185
|
+
) || "GENERAL";
|
|
186
|
+
const previewEl = document.getElementById("new-doc-filename-preview");
|
|
187
|
+
|
|
188
|
+
if (!title) {
|
|
189
|
+
previewEl.textContent = window.t('modal.new_doc.title_placeholder');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const now = new Date();
|
|
194
|
+
const year = now.getFullYear();
|
|
195
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
196
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
197
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
198
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
199
|
+
const titleSlug =
|
|
200
|
+
title
|
|
201
|
+
.toLowerCase()
|
|
202
|
+
.replace(/\s+/g, "_")
|
|
203
|
+
.replace(/[^a-z0-9_]/g, "")
|
|
204
|
+
.replace(/_+/g, "_")
|
|
205
|
+
.replace(/^_|_$/g, "") || "document";
|
|
206
|
+
|
|
207
|
+
const filename =
|
|
208
|
+
_newDocPattern
|
|
209
|
+
.replace("YYYY", year)
|
|
210
|
+
.replace("MM", month)
|
|
211
|
+
.replace("DD", day)
|
|
212
|
+
.replace("HH", hours)
|
|
213
|
+
.replace("mm", minutes)
|
|
214
|
+
.replace(/\[Category\]/i, `[${category}]`)
|
|
215
|
+
.replace(
|
|
216
|
+
/(?<![a-z0-9])(?:title_words|title)(?![a-z0-9])/i,
|
|
217
|
+
titleSlug,
|
|
218
|
+
) + ".md";
|
|
219
|
+
|
|
220
|
+
previewEl.textContent = _newDocSelectedFolder
|
|
221
|
+
? _newDocSelectedFolder + "/" + filename
|
|
222
|
+
: filename;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function createNewDocument() {
|
|
226
|
+
const title = document.getElementById("new-doc-title").value.trim();
|
|
227
|
+
const category =
|
|
228
|
+
newDocNormalizeCategory(
|
|
229
|
+
document.getElementById("new-doc-category").value,
|
|
230
|
+
) || "GENERAL";
|
|
231
|
+
const errorEl = document.getElementById("new-doc-error");
|
|
232
|
+
const btn = document.getElementById("new-doc-create-btn");
|
|
233
|
+
|
|
234
|
+
if (!title) {
|
|
235
|
+
errorEl.textContent = window.t('modal.new_doc.error_empty_title');
|
|
236
|
+
errorEl.classList.remove("hidden");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
errorEl.classList.add("hidden");
|
|
241
|
+
btn.disabled = true;
|
|
242
|
+
btn.textContent = window.t('modal.new_folder.creating_btn');
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const res = await fetch("/api/documents", {
|
|
246
|
+
method: "POST",
|
|
247
|
+
headers: { "Content-Type": "application/json" },
|
|
248
|
+
body: JSON.stringify({
|
|
249
|
+
title,
|
|
250
|
+
category,
|
|
251
|
+
folder: _newDocSelectedFolder,
|
|
252
|
+
}),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (!res.ok) {
|
|
256
|
+
const data = await res.json();
|
|
257
|
+
throw new Error(data.error || "Creation failed");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const doc = await res.json();
|
|
261
|
+
closeNewDocModal();
|
|
262
|
+
await loadDocuments();
|
|
263
|
+
openDocument(doc.id);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
errorEl.textContent = window.t('common.error_prefix') + err.message;
|
|
266
|
+
errorEl.classList.remove("hidden");
|
|
267
|
+
btn.disabled = false;
|
|
268
|
+
btn.textContent = window.t('common.create');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Allow Enter key in title/category to submit
|
|
273
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
274
|
+
["new-doc-title", "new-doc-category"].forEach((id) => {
|
|
275
|
+
document.getElementById(id)?.addEventListener("keydown", (e) => {
|
|
276
|
+
if (e.key === "Enter") createNewDocument();
|
|
277
|
+
if (e.key === "Escape") closeNewDocModal();
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
document
|
|
281
|
+
.getElementById("new-doc-new-folder-name")
|
|
282
|
+
?.addEventListener("keydown", (e) => {
|
|
283
|
+
if (e.key === "Enter") newDocCreateFolder();
|
|
284
|
+
});
|
|
285
|
+
});
|