living-documentation 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +329 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +62 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/frontend/admin.html +1073 -0
- package/dist/src/frontend/annotations.js +546 -0
- package/dist/src/frontend/boot.js +90 -0
- package/dist/src/frontend/config.js +19 -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 +172 -0
- package/dist/src/frontend/diagram/constants.js +109 -0
- package/dist/src/frontend/diagram/debug.js +43 -0
- package/dist/src/frontend/diagram/edge-panel.js +260 -0
- package/dist/src/frontend/diagram/edge-rendering.js +12 -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 +153 -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 +299 -0
- package/dist/src/frontend/diagram/network.js +1473 -0
- package/dist/src/frontend/diagram/node-panel.js +267 -0
- package/dist/src/frontend/diagram/node-rendering.js +773 -0
- package/dist/src/frontend/diagram/persistence.js +161 -0
- package/dist/src/frontend/diagram/ports.js +386 -0
- package/dist/src/frontend/diagram/selection-overlay.js +336 -0
- package/dist/src/frontend/diagram/state.js +39 -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 +182 -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 +1279 -0
- package/dist/src/frontend/documents.js +373 -0
- package/dist/src/frontend/export.js +338 -0
- package/dist/src/frontend/i18n/en.json +406 -0
- package/dist/src/frontend/i18n/fr.json +406 -0
- package/dist/src/frontend/i18n.js +32 -0
- package/dist/src/frontend/image-paste.js +101 -0
- package/dist/src/frontend/index.html +2314 -0
- package/dist/src/frontend/misc.js +25 -0
- package/dist/src/frontend/new-doc-modal.js +260 -0
- package/dist/src/frontend/new-folder-modal.js +174 -0
- package/dist/src/frontend/search.js +157 -0
- package/dist/src/frontend/sidebar-helpers.js +58 -0
- package/dist/src/frontend/sidebar.js +182 -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 +534 -0
- package/dist/src/frontend/state.js +28 -0
- package/dist/src/frontend/utils.js +21 -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 +17 -0
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +79 -0
- package/dist/src/lib/config.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/mcp/server.d.ts +3 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +986 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools/diagrams.d.ts +44 -0
- package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
- package/dist/src/mcp/tools/diagrams.js +245 -0
- package/dist/src/mcp/tools/diagrams.js.map +1 -0
- package/dist/src/mcp/tools/documents.d.ts +26 -0
- package/dist/src/mcp/tools/documents.d.ts.map +1 -0
- package/dist/src/mcp/tools/documents.js +127 -0
- package/dist/src/mcp/tools/documents.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 +200 -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.d.ts +3 -0
- package/dist/src/routes/browse.d.ts.map +1 -0
- package/dist/src/routes/browse.js +75 -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 +97 -0
- package/dist/src/routes/config.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 +8 -0
- package/dist/src/routes/documents.d.ts.map +1 -0
- package/dist/src/routes/documents.js +332 -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 +277 -0
- package/dist/src/routes/export.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/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 +76 -0
- package/dist/src/server.js.map +1 -0
- package/dist/starting-doc/.annotations.json +3 -0
- package/dist/starting-doc/.diagrams.json +1884 -0
- package/dist/starting-doc/.living-doc.json +39 -0
- package/dist/starting-doc/1_tutorial/2026_04_11_13_25_[General]_crer_vos_dossiers.md +16 -0
- package/dist/starting-doc/1_tutorial/2026_04_11_18_58_[General]_creer_un_document_dans_un_dossier.md +9 -0
- package/dist/starting-doc/1_tutorial/2026_04_12_09_00_[General]_editer_et_sauvegarder.md +39 -0
- package/dist/starting-doc/1_tutorial/2026_04_12_10_00_[General]_utiliser_les_snippets.md +71 -0
- package/dist/starting-doc/2026_04_08_20_52_[General]_welcome.md +17 -0
- package/dist/starting-doc/2026_04_11_12_55_[General]_premiers_pas.md +271 -0
- package/dist/starting-doc/2_guide/2026_04_08_00_04_[DOCUMENT]_utilisation_des_images_plein_ecran_lien_clickable.md +40 -0
- package/dist/starting-doc/2_guide/2026_04_08_23_38_[Configuration]_demarrage_de_living_documentation.md +32 -0
- package/dist/starting-doc/2_guide/2026_04_09_09_00_[NAVIGATION]_recherche_plein_texte.md +65 -0
- package/dist/starting-doc/2_guide/2026_04_09_10_00_[EXPORT]_exporter_en_pdf.md +43 -0
- package/dist/starting-doc/2_guide/2026_04_09_11_00_[Configuration]_configurer_le_panneau_admin.md +55 -0
- package/dist/starting-doc/2_guide/2026_04_09_12_00_[Configuration]_extra_files.md +68 -0
- package/dist/starting-doc/2_guide/2026_04_09_13_00_[WORDCLOUD]_word_cloud.md +54 -0
- package/dist/starting-doc/2_guide/2026_04_09_14_00_[DIAGRAM]_creer_et_lier_un_diagramme.md +77 -0
- package/dist/starting-doc/3_concept/2026_04_08_20_58_[DOCUMENTING]_ADRS.md +20 -0
- package/dist/starting-doc/3_concept/2026_04_08_22_15_[DOCUMENTING]_living_documentation.md +17 -0
- package/dist/starting-doc/3_concept/2026_04_08_22_46_[METHODOLOGY]_diataxis_architecture_du_contenu.md +16 -0
- package/dist/starting-doc/4_reference/2026_04_08_23_14_[FUNDAMENTALS]_the_living_documentation_tool.md +41 -0
- package/dist/starting-doc/4_reference/2026_04_09_01_00_[REFERENCE]_raccourcis_clavier.md +61 -0
- package/dist/starting-doc/4_reference/2026_04_09_02_00_[REFERENCE]_tokens_pattern_nommage.md +75 -0
- package/dist/starting-doc/4_reference/2026_04_09_03_00_[REFERENCE]_types_de_snippets.md +68 -0
- package/dist/starting-doc/4_reference/2026_04_11_17_31_[FUNDAMENTALS]_architecturer_une_documentation.md +12 -0
- package/dist/starting-doc/4_reference/2026_04_12_14_07_[FUNDAMENTALS]_dossiers_et_catgories.md +89 -0
- package/dist/starting-doc/images/admin_screenshot.png +0 -0
- package/dist/starting-doc/images/ajout-document.png +0 -0
- package/dist/starting-doc/images/ajouter-document-categorie.png +0 -0
- package/dist/starting-doc/images/ajouter_un_document_dans_un_dossier.png +0 -0
- package/dist/starting-doc/images/architecturer_une_documentation_reference.png +0 -0
- package/dist/starting-doc/images/cr_er_un_document.png +0 -0
- package/dist/starting-doc/images/creation-nouveau-dossier.png +0 -0
- package/dist/starting-doc/images/creer-document-context-engineering.png +0 -0
- package/dist/starting-doc/images/creer-dossier-only-tutoriel.png +0 -0
- package/dist/starting-doc/images/creer-dossier-tutoriel.png +0 -0
- package/dist/starting-doc/images/creer-dossiers-done.png +0 -0
- package/dist/starting-doc/images/creer-un-document.png +0 -0
- package/dist/starting-doc/images/creer-vos-dossiers-tutoriel.png +0 -0
- package/dist/starting-doc/images/creer-vos-dossiers.png +0 -0
- package/dist/starting-doc/images/decouverte_adrs.png +0 -0
- package/dist/starting-doc/images/diataxis.png +0 -0
- package/dist/starting-doc/images/diataxis_callout.png +0 -0
- package/dist/starting-doc/images/document-cree.png +0 -0
- package/dist/starting-doc/images/liens_snippets.png +0 -0
- package/dist/starting-doc/images/living_documentation.png +0 -0
- package/dist/starting-doc/images/npm_logo.png +0 -0
- package/dist/starting-doc/images/popup-creer-document.png +0 -0
- package/dist/starting-doc/images/popup-creer-dossier.png +0 -0
- package/dist/starting-doc/images/popup-dossier-cree.png +0 -0
- package/dist/starting-doc/images/quatre-dossiers-crees.png +0 -0
- package/dist/starting-doc/images/screenshot-living-doc.png +0 -0
- package/dist/starting-doc/images/the_living_documentation_tool.png +0 -0
- package/package.json +49 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
// ── Marker (stabilo) annotations ────────────────────────────────────────────
|
|
2
|
+
// Depends on globals from state.js (currentDocId, annotationCounts) and
|
|
3
|
+
// sidebar state (refreshSidebar from state.js).
|
|
4
|
+
|
|
5
|
+
let stabiloActive = false;
|
|
6
|
+
let stabiloHidden = false; // state 3: highlights hidden
|
|
7
|
+
let stabiloPendingRange = null;
|
|
8
|
+
let stabiloAnnotations = [];
|
|
9
|
+
let stabiloDeleteTargetId = null;
|
|
10
|
+
let stabiloReadPopupAnnotationId = null;
|
|
11
|
+
let stabiloReadHideTimer = null;
|
|
12
|
+
|
|
13
|
+
// Cycles: normal → active → hidden → normal
|
|
14
|
+
function toggleMarker() {
|
|
15
|
+
const btn = document.getElementById("stabilo-btn");
|
|
16
|
+
const fills = btn.querySelectorAll("rect, polygon");
|
|
17
|
+
const crossEl = document.getElementById("stabilo-cross");
|
|
18
|
+
|
|
19
|
+
if (!stabiloActive && !stabiloHidden) {
|
|
20
|
+
// normal → active
|
|
21
|
+
stabiloActive = true;
|
|
22
|
+
btn.classList.remove(
|
|
23
|
+
"border-gray-200",
|
|
24
|
+
"dark:border-gray-700",
|
|
25
|
+
"text-gray-600",
|
|
26
|
+
"dark:text-gray-400",
|
|
27
|
+
);
|
|
28
|
+
btn.classList.add(
|
|
29
|
+
"border-yellow-400",
|
|
30
|
+
"bg-yellow-100",
|
|
31
|
+
"dark:bg-yellow-900/40",
|
|
32
|
+
"text-yellow-700",
|
|
33
|
+
"dark:text-yellow-300",
|
|
34
|
+
);
|
|
35
|
+
fills.forEach((el) =>
|
|
36
|
+
el.setAttribute(
|
|
37
|
+
"fill",
|
|
38
|
+
el.tagName === "polygon" ? "#fde047" : "#fef08a",
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
crossEl.style.display = "none";
|
|
42
|
+
} else if (stabiloActive) {
|
|
43
|
+
// active → hidden
|
|
44
|
+
stabiloActive = false;
|
|
45
|
+
stabiloHidden = true;
|
|
46
|
+
btn.classList.remove(
|
|
47
|
+
"border-yellow-400",
|
|
48
|
+
"bg-yellow-100",
|
|
49
|
+
"dark:bg-yellow-900/40",
|
|
50
|
+
"text-yellow-700",
|
|
51
|
+
"dark:text-yellow-300",
|
|
52
|
+
);
|
|
53
|
+
btn.classList.add(
|
|
54
|
+
"border-gray-200",
|
|
55
|
+
"dark:border-gray-700",
|
|
56
|
+
"text-gray-600",
|
|
57
|
+
"dark:text-gray-400",
|
|
58
|
+
);
|
|
59
|
+
fills.forEach((el) =>
|
|
60
|
+
el.setAttribute(
|
|
61
|
+
"fill",
|
|
62
|
+
el.tagName === "polygon"
|
|
63
|
+
? "#93c5fd"
|
|
64
|
+
: el.previousElementSibling
|
|
65
|
+
? "#93c5fd"
|
|
66
|
+
: "#bfdbfe",
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
crossEl.style.display = "block";
|
|
70
|
+
closeMarkerPopup();
|
|
71
|
+
setHighlightsVisible(false);
|
|
72
|
+
} else {
|
|
73
|
+
// hidden → normal
|
|
74
|
+
stabiloHidden = false;
|
|
75
|
+
crossEl.style.display = "none";
|
|
76
|
+
setHighlightsVisible(true);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function setHighlightsVisible(visible) {
|
|
81
|
+
document.querySelectorAll("mark[data-annotation-id]").forEach((m) => {
|
|
82
|
+
m.style.background = visible ? "rgba(250,204,21,0.5)" : "transparent";
|
|
83
|
+
m.style.cursor = visible ? "pointer" : "default";
|
|
84
|
+
m.style.pointerEvents = visible ? "auto" : "none";
|
|
85
|
+
});
|
|
86
|
+
const elevator = document.getElementById("stabilo-elevator");
|
|
87
|
+
if (elevator)
|
|
88
|
+
elevator.style.visibility = visible ? "visible" : "hidden";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Load and apply annotations for current doc
|
|
92
|
+
async function loadAnnotations(docId) {
|
|
93
|
+
stabiloAnnotations = [];
|
|
94
|
+
try {
|
|
95
|
+
stabiloAnnotations = await fetch(
|
|
96
|
+
"/api/annotations/" + encodeURIComponent(docId),
|
|
97
|
+
).then((r) => r.json());
|
|
98
|
+
} catch {
|
|
99
|
+
stabiloAnnotations = [];
|
|
100
|
+
}
|
|
101
|
+
applyAnnotationHighlights();
|
|
102
|
+
renderElevator();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Re-apply all highlight marks in the rendered content
|
|
106
|
+
function applyAnnotationHighlights() {
|
|
107
|
+
const contentEl = document.getElementById("doc-content");
|
|
108
|
+
if (!contentEl) return;
|
|
109
|
+
|
|
110
|
+
// Remove existing marks (unwrap, keep text content)
|
|
111
|
+
contentEl
|
|
112
|
+
.querySelectorAll("mark[data-annotation-id]")
|
|
113
|
+
.forEach((mark) => {
|
|
114
|
+
const parent = mark.parentNode;
|
|
115
|
+
parent.replaceChild(
|
|
116
|
+
document.createTextNode(mark.textContent),
|
|
117
|
+
mark,
|
|
118
|
+
);
|
|
119
|
+
parent.normalize();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
for (const ann of stabiloAnnotations) {
|
|
123
|
+
highlightAnnotation(contentEl, ann);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Remove empty marks left by block-boundary splitting
|
|
127
|
+
contentEl
|
|
128
|
+
.querySelectorAll("mark[data-annotation-id]")
|
|
129
|
+
.forEach((mark) => {
|
|
130
|
+
if (!mark.textContent.trim()) mark.remove();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function highlightAnnotation(contentEl, ann) {
|
|
135
|
+
const selText = (ann.selectedText || "").replace(/\s+/g, " ").trim();
|
|
136
|
+
const ctxBefore = (ann.contextBefore || "")
|
|
137
|
+
.replace(/\s+/g, " ")
|
|
138
|
+
.trim();
|
|
139
|
+
const ctxAfter = (ann.contextAfter || "").replace(/\s+/g, " ").trim();
|
|
140
|
+
if (!selText) return;
|
|
141
|
+
|
|
142
|
+
const escRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
143
|
+
// Build a pattern that allows any whitespace between words
|
|
144
|
+
const toPat = (s) =>
|
|
145
|
+
s
|
|
146
|
+
.split(/\s+/)
|
|
147
|
+
.filter(Boolean)
|
|
148
|
+
.map(escRe)
|
|
149
|
+
.join("\\s+");
|
|
150
|
+
|
|
151
|
+
const text = contentEl.textContent;
|
|
152
|
+
const selPat = toPat(selText);
|
|
153
|
+
let match;
|
|
154
|
+
|
|
155
|
+
// Try with context first for disambiguation
|
|
156
|
+
if (ctxBefore || ctxAfter) {
|
|
157
|
+
const fullPat =
|
|
158
|
+
(ctxBefore ? toPat(ctxBefore) + "\\s*" : "") +
|
|
159
|
+
"(" + selPat + ")" +
|
|
160
|
+
(ctxAfter ? "\\s*" + toPat(ctxAfter) : "");
|
|
161
|
+
try {
|
|
162
|
+
match = new RegExp(fullPat, "d").exec(text);
|
|
163
|
+
} catch {
|
|
164
|
+
/* fall through */
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!match) {
|
|
168
|
+
try {
|
|
169
|
+
match = new RegExp("(" + selPat + ")", "d").exec(text);
|
|
170
|
+
} catch {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!match || !match.indices || !match.indices[1]) return;
|
|
175
|
+
|
|
176
|
+
const [startOff, endOff] = match.indices[1];
|
|
177
|
+
|
|
178
|
+
// Locate the text-node slices that cover [startOff, endOff)
|
|
179
|
+
const walker = document.createTreeWalker(
|
|
180
|
+
contentEl,
|
|
181
|
+
NodeFilter.SHOW_TEXT,
|
|
182
|
+
);
|
|
183
|
+
const slices = [];
|
|
184
|
+
let pos = 0;
|
|
185
|
+
while (walker.nextNode()) {
|
|
186
|
+
const node = walker.currentNode;
|
|
187
|
+
const len = node.nodeValue.length;
|
|
188
|
+
const nStart = pos;
|
|
189
|
+
const nEnd = pos + len;
|
|
190
|
+
pos = nEnd;
|
|
191
|
+
if (nEnd <= startOff) continue;
|
|
192
|
+
if (nStart >= endOff) break;
|
|
193
|
+
const sStart = Math.max(0, startOff - nStart);
|
|
194
|
+
const sEnd = Math.min(len, endOff - nStart);
|
|
195
|
+
if (sStart < sEnd) slices.push({ node, start: sStart, end: sEnd });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Wrap each slice in its own <mark> within its current parent block.
|
|
199
|
+
// This produces valid DOM regardless of table/code/list/heading boundaries.
|
|
200
|
+
const markStyle =
|
|
201
|
+
"background:rgba(250,204,21,0.5);border-radius:2px;cursor:pointer;padding:0 1px;";
|
|
202
|
+
for (const { node, start, end } of slices) {
|
|
203
|
+
let target = node;
|
|
204
|
+
if (start > 0 && start < target.nodeValue.length) {
|
|
205
|
+
target = target.splitText(start);
|
|
206
|
+
}
|
|
207
|
+
const wantLen = end - start;
|
|
208
|
+
if (wantLen > 0 && wantLen < target.nodeValue.length) {
|
|
209
|
+
target.splitText(wantLen);
|
|
210
|
+
}
|
|
211
|
+
const mark = document.createElement("mark");
|
|
212
|
+
mark.setAttribute("data-annotation-id", ann.id);
|
|
213
|
+
mark.setAttribute("style", markStyle);
|
|
214
|
+
target.parentNode.insertBefore(mark, target);
|
|
215
|
+
mark.appendChild(target);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Selection capture ───────────────────────────────────────────────────────
|
|
220
|
+
document.addEventListener("mouseup", (e) => {
|
|
221
|
+
if (!stabiloActive) return;
|
|
222
|
+
if (
|
|
223
|
+
document.getElementById("stabilo-popup") &&
|
|
224
|
+
!document.getElementById("stabilo-popup").classList.contains("hidden")
|
|
225
|
+
)
|
|
226
|
+
return;
|
|
227
|
+
|
|
228
|
+
const sel = window.getSelection();
|
|
229
|
+
if (!sel || sel.isCollapsed || !sel.toString().trim()) return;
|
|
230
|
+
|
|
231
|
+
const contentEl = document.getElementById("doc-content");
|
|
232
|
+
if (!contentEl || !contentEl.contains(sel.anchorNode)) return;
|
|
233
|
+
|
|
234
|
+
const range = sel.getRangeAt(0);
|
|
235
|
+
stabiloPendingRange = range.cloneRange();
|
|
236
|
+
|
|
237
|
+
// Compute the *actual* offset of the selection in contentEl.textContent,
|
|
238
|
+
// not via indexOf (which would match the first occurrence anywhere).
|
|
239
|
+
const preRange = document.createRange();
|
|
240
|
+
preRange.selectNodeContents(contentEl);
|
|
241
|
+
preRange.setEnd(range.startContainer, range.startOffset);
|
|
242
|
+
const rawStart = preRange.toString().length;
|
|
243
|
+
const rawSelected = range.toString();
|
|
244
|
+
const fullText = contentEl.textContent;
|
|
245
|
+
|
|
246
|
+
// Extract context (30 chars before/after) then normalize whitespace
|
|
247
|
+
const normalize = (s) => s.replace(/\s+/g, " ").trim();
|
|
248
|
+
const selectedText = normalize(rawSelected);
|
|
249
|
+
const ctxBefore = normalize(
|
|
250
|
+
fullText.slice(Math.max(0, rawStart - 30), rawStart),
|
|
251
|
+
);
|
|
252
|
+
const ctxAfter = normalize(
|
|
253
|
+
fullText.slice(
|
|
254
|
+
rawStart + rawSelected.length,
|
|
255
|
+
rawStart + rawSelected.length + 30,
|
|
256
|
+
),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
stabiloPendingRange._selectedText = selectedText;
|
|
260
|
+
stabiloPendingRange._contextBefore = ctxBefore;
|
|
261
|
+
stabiloPendingRange._contextAfter = ctxAfter;
|
|
262
|
+
|
|
263
|
+
// Position popup near selection
|
|
264
|
+
const rect = range.getBoundingClientRect();
|
|
265
|
+
positionPopup(
|
|
266
|
+
"stabilo-popup",
|
|
267
|
+
rect.left + window.scrollX,
|
|
268
|
+
rect.bottom + window.scrollY + 8,
|
|
269
|
+
);
|
|
270
|
+
document.getElementById("stabilo-selected-preview").textContent =
|
|
271
|
+
selectedText.length > 120
|
|
272
|
+
? selectedText.slice(0, 120) + "…"
|
|
273
|
+
: selectedText;
|
|
274
|
+
document.getElementById("stabilo-note-input").value = "";
|
|
275
|
+
document.getElementById("stabilo-popup").classList.remove("hidden");
|
|
276
|
+
setTimeout(
|
|
277
|
+
() => document.getElementById("stabilo-note-input").focus(),
|
|
278
|
+
50,
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
function positionPopup(id, x, y) {
|
|
283
|
+
const el = document.getElementById(id);
|
|
284
|
+
const w = 320;
|
|
285
|
+
const vw = window.innerWidth;
|
|
286
|
+
const vh = window.innerHeight;
|
|
287
|
+
let left = x;
|
|
288
|
+
let top = y;
|
|
289
|
+
if (left + w > vw - 16) left = vw - w - 16;
|
|
290
|
+
if (left < 8) left = 8;
|
|
291
|
+
el.style.left = left + "px";
|
|
292
|
+
el.style.top = Math.min(top, vh - 260) + "px";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function closeMarkerPopup() {
|
|
296
|
+
document.getElementById("stabilo-popup").classList.add("hidden");
|
|
297
|
+
stabiloPendingRange = null;
|
|
298
|
+
window.getSelection()?.removeAllRanges();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function cancelMarkerPopup() {
|
|
302
|
+
closeMarkerPopup();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function saveAnnotation() {
|
|
306
|
+
if (!stabiloPendingRange || !currentDocId) return;
|
|
307
|
+
const note = document.getElementById("stabilo-note-input").value.trim();
|
|
308
|
+
if (!note) {
|
|
309
|
+
document.getElementById("stabilo-note-input").focus();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const ann = await fetch(
|
|
315
|
+
"/api/annotations/" + encodeURIComponent(currentDocId),
|
|
316
|
+
{
|
|
317
|
+
method: "POST",
|
|
318
|
+
headers: { "Content-Type": "application/json" },
|
|
319
|
+
body: JSON.stringify({
|
|
320
|
+
selectedText: stabiloPendingRange._selectedText,
|
|
321
|
+
contextBefore: stabiloPendingRange._contextBefore,
|
|
322
|
+
contextAfter: stabiloPendingRange._contextAfter,
|
|
323
|
+
note,
|
|
324
|
+
}),
|
|
325
|
+
},
|
|
326
|
+
).then((r) => r.json());
|
|
327
|
+
|
|
328
|
+
stabiloAnnotations.push(ann);
|
|
329
|
+
closeMarkerPopup();
|
|
330
|
+
applyAnnotationHighlights();
|
|
331
|
+
renderElevator();
|
|
332
|
+
annotationCounts[currentDocId] = (annotationCounts[currentDocId] || 0) + 1;
|
|
333
|
+
refreshSidebar();
|
|
334
|
+
} catch {
|
|
335
|
+
/* ignore */
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── Read popup ──────────────────────────────────────────────────────────────
|
|
340
|
+
function showReadPopup(ann, markEl) {
|
|
341
|
+
clearTimeout(stabiloReadHideTimer);
|
|
342
|
+
stabiloReadPopupAnnotationId = ann.id;
|
|
343
|
+
const popup = document.getElementById("stabilo-read-popup");
|
|
344
|
+
const isOrphan = !document
|
|
345
|
+
.getElementById("doc-content")
|
|
346
|
+
.querySelector(`mark[data-annotation-id="${ann.id}"]`);
|
|
347
|
+
document
|
|
348
|
+
.getElementById("stabilo-read-orphan")
|
|
349
|
+
.classList.toggle("hidden", !isOrphan);
|
|
350
|
+
document.getElementById("stabilo-read-text").textContent = ann.note;
|
|
351
|
+
document.getElementById("stabilo-read-date").textContent = new Date(
|
|
352
|
+
ann.createdAt,
|
|
353
|
+
).toLocaleDateString("fr-FR", {
|
|
354
|
+
day: "2-digit",
|
|
355
|
+
month: "short",
|
|
356
|
+
year: "numeric",
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
document.getElementById("stabilo-delete-btn").onclick = () =>
|
|
360
|
+
askDeleteAnnotation(ann.id);
|
|
361
|
+
|
|
362
|
+
const rect = markEl.getBoundingClientRect();
|
|
363
|
+
positionPopup(
|
|
364
|
+
"stabilo-read-popup",
|
|
365
|
+
rect.left + window.scrollX,
|
|
366
|
+
rect.bottom + window.scrollY + 8,
|
|
367
|
+
);
|
|
368
|
+
popup.classList.remove("hidden");
|
|
369
|
+
|
|
370
|
+
// Highlight elevator pill (preserve red for orphans)
|
|
371
|
+
document.querySelectorAll(".stabilo-pill").forEach((p) => {
|
|
372
|
+
const isOrphan = p.classList.contains("border-red-600");
|
|
373
|
+
if (p.dataset.id === ann.id) {
|
|
374
|
+
p.style.background = isOrphan ? "#b91c1c" : "#f97316";
|
|
375
|
+
} else {
|
|
376
|
+
p.style.background = isOrphan ? "#ef4444" : "#facc15";
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
// Scroll elevator pill into view
|
|
380
|
+
const pill = document.querySelector(
|
|
381
|
+
`.stabilo-pill[data-id="${ann.id}"]`,
|
|
382
|
+
);
|
|
383
|
+
if (pill) pill.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function scheduleHideReadPopup() {
|
|
387
|
+
stabiloReadHideTimer = setTimeout(() => {
|
|
388
|
+
document.getElementById("stabilo-read-popup").classList.add("hidden");
|
|
389
|
+
document.querySelectorAll(".stabilo-pill").forEach((p) => {
|
|
390
|
+
p.style.background = p.classList.contains("border-red-600")
|
|
391
|
+
? "#ef4444"
|
|
392
|
+
: "#facc15";
|
|
393
|
+
});
|
|
394
|
+
}, 300);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Keep popup visible when hovering it
|
|
398
|
+
document
|
|
399
|
+
.getElementById("stabilo-read-popup")
|
|
400
|
+
.addEventListener("mouseenter", () =>
|
|
401
|
+
clearTimeout(stabiloReadHideTimer),
|
|
402
|
+
);
|
|
403
|
+
document
|
|
404
|
+
.getElementById("stabilo-read-popup")
|
|
405
|
+
.addEventListener("mouseleave", () => scheduleHideReadPopup());
|
|
406
|
+
|
|
407
|
+
// ── Delete ──────────────────────────────────────────────────────────────────
|
|
408
|
+
function askDeleteAnnotation(id) {
|
|
409
|
+
stabiloDeleteTargetId = id;
|
|
410
|
+
document.getElementById("stabilo-read-popup").classList.add("hidden");
|
|
411
|
+
const confirmEl = document.getElementById("stabilo-confirm-delete");
|
|
412
|
+
confirmEl.style.left =
|
|
413
|
+
document.getElementById("stabilo-read-popup").style.left;
|
|
414
|
+
confirmEl.style.top =
|
|
415
|
+
document.getElementById("stabilo-read-popup").style.top;
|
|
416
|
+
confirmEl.classList.remove("hidden");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function cancelDeleteAnnotation() {
|
|
420
|
+
document
|
|
421
|
+
.getElementById("stabilo-confirm-delete")
|
|
422
|
+
.classList.add("hidden");
|
|
423
|
+
stabiloDeleteTargetId = null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function confirmDeleteAnnotation() {
|
|
427
|
+
if (!stabiloDeleteTargetId || !currentDocId) return;
|
|
428
|
+
try {
|
|
429
|
+
await fetch(
|
|
430
|
+
`/api/annotations/${encodeURIComponent(currentDocId)}/${stabiloDeleteTargetId}`,
|
|
431
|
+
{ method: "DELETE" },
|
|
432
|
+
);
|
|
433
|
+
stabiloAnnotations = stabiloAnnotations.filter(
|
|
434
|
+
(a) => a.id !== stabiloDeleteTargetId,
|
|
435
|
+
);
|
|
436
|
+
stabiloDeleteTargetId = null;
|
|
437
|
+
document
|
|
438
|
+
.getElementById("stabilo-confirm-delete")
|
|
439
|
+
.classList.add("hidden");
|
|
440
|
+
applyAnnotationHighlights();
|
|
441
|
+
renderElevator();
|
|
442
|
+
if (currentDocId) {
|
|
443
|
+
const next = (annotationCounts[currentDocId] || 0) - 1;
|
|
444
|
+
if (next <= 0) delete annotationCounts[currentDocId];
|
|
445
|
+
else annotationCounts[currentDocId] = next;
|
|
446
|
+
refreshSidebar();
|
|
447
|
+
}
|
|
448
|
+
} catch {
|
|
449
|
+
/* ignore */
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ── Elevator ────────────────────────────────────────────────────────────────
|
|
454
|
+
function renderElevator() {
|
|
455
|
+
const elevator = document.getElementById("stabilo-elevator");
|
|
456
|
+
const contentEl = document.getElementById("doc-content");
|
|
457
|
+
if (!contentEl) return;
|
|
458
|
+
|
|
459
|
+
if (stabiloAnnotations.length === 0) {
|
|
460
|
+
elevator.classList.add("hidden");
|
|
461
|
+
elevator.innerHTML = "";
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
elevator.classList.remove("hidden");
|
|
466
|
+
const docHeight = contentEl.scrollHeight;
|
|
467
|
+
const elevatorHeight = window.innerHeight;
|
|
468
|
+
|
|
469
|
+
elevator.innerHTML = "";
|
|
470
|
+
for (const ann of stabiloAnnotations) {
|
|
471
|
+
const mark = contentEl.querySelector(
|
|
472
|
+
`mark[data-annotation-id="${ann.id}"]`,
|
|
473
|
+
);
|
|
474
|
+
const relPos = mark
|
|
475
|
+
? (mark.offsetTop / Math.max(docHeight, 1)) * elevatorHeight
|
|
476
|
+
: 0;
|
|
477
|
+
const orphan = !mark;
|
|
478
|
+
|
|
479
|
+
const pill = document.createElement("button");
|
|
480
|
+
pill.className =
|
|
481
|
+
"stabilo-pill w-8 h-8 rounded border-2 shadow text-xs flex items-center justify-center transition-colors shrink-0 " +
|
|
482
|
+
(orphan ? "border-red-600" : "border-yellow-500");
|
|
483
|
+
pill.dataset.id = ann.id;
|
|
484
|
+
pill.style.background = orphan ? "#ef4444" : "#facc15";
|
|
485
|
+
const noteShort =
|
|
486
|
+
ann.note.length > 60 ? ann.note.slice(0, 60) + "…" : ann.note;
|
|
487
|
+
pill.title = orphan
|
|
488
|
+
? `${window.t('annotation.orphan')}\n\n${noteShort}`
|
|
489
|
+
: noteShort;
|
|
490
|
+
pill.textContent = orphan ? "⚠" : "✎";
|
|
491
|
+
if (orphan) pill.style.color = "#fff";
|
|
492
|
+
|
|
493
|
+
pill.addEventListener("click", () => {
|
|
494
|
+
const m = contentEl.querySelector(
|
|
495
|
+
`mark[data-annotation-id="${ann.id}"]`,
|
|
496
|
+
);
|
|
497
|
+
if (m) {
|
|
498
|
+
const docView =
|
|
499
|
+
document.getElementById("doc-view") ||
|
|
500
|
+
document.getElementById("content-area");
|
|
501
|
+
docView.scrollTo({ top: m.offsetTop - 120, behavior: "smooth" });
|
|
502
|
+
} else {
|
|
503
|
+
// Orphan annotation — no mark to anchor to; show popup on the pill itself
|
|
504
|
+
clearTimeout(stabiloReadHideTimer);
|
|
505
|
+
showReadPopup(ann, pill);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
pill.addEventListener("mouseenter", () => {
|
|
509
|
+
const m = contentEl.querySelector(
|
|
510
|
+
`mark[data-annotation-id="${ann.id}"]`,
|
|
511
|
+
);
|
|
512
|
+
if (!m) {
|
|
513
|
+
clearTimeout(stabiloReadHideTimer);
|
|
514
|
+
showReadPopup(ann, pill);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const docView = document.getElementById("content-area");
|
|
518
|
+
const markTop =
|
|
519
|
+
m.getBoundingClientRect().top +
|
|
520
|
+
docView.scrollTop -
|
|
521
|
+
docView.getBoundingClientRect().top;
|
|
522
|
+
docView.scrollTo({
|
|
523
|
+
top: markTop - docView.clientHeight / 2,
|
|
524
|
+
behavior: "smooth",
|
|
525
|
+
});
|
|
526
|
+
// Wait for scroll to fully stop before showing popup
|
|
527
|
+
let scrollEndTimer;
|
|
528
|
+
const onScroll = () => {
|
|
529
|
+
clearTimeout(scrollEndTimer);
|
|
530
|
+
scrollEndTimer = setTimeout(() => {
|
|
531
|
+
docView.removeEventListener("scroll", onScroll);
|
|
532
|
+
showReadPopup(ann, m);
|
|
533
|
+
}, 80);
|
|
534
|
+
};
|
|
535
|
+
docView.addEventListener("scroll", onScroll);
|
|
536
|
+
// Fallback if already at position (no scroll event fires)
|
|
537
|
+
scrollEndTimer = setTimeout(() => {
|
|
538
|
+
docView.removeEventListener("scroll", onScroll);
|
|
539
|
+
showReadPopup(ann, m);
|
|
540
|
+
}, 600);
|
|
541
|
+
});
|
|
542
|
+
pill.addEventListener("mouseleave", () => scheduleHideReadPopup());
|
|
543
|
+
|
|
544
|
+
elevator.appendChild(pill);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// ── Boot & global listeners ─────────────────────────────────────────────────
|
|
2
|
+
// Depends on globals from state.js (allDocs, stabiloAnnotations), dark-mode.js
|
|
3
|
+
// (applyDarkMode, loadDarkPref, setupDarkToggle), search.js (setupSearch),
|
|
4
|
+
// wordcloud.js (wcRestorePrefs), config.js (loadConfig), documents.js
|
|
5
|
+
// (loadDocuments, openDocument), annotations.js (showReadPopup,
|
|
6
|
+
// scheduleHideReadPopup), image-paste.js (openLightbox, closeLightbox,
|
|
7
|
+
// imgPasteConfirm, imgPasteCancel).
|
|
8
|
+
|
|
9
|
+
document.addEventListener("DOMContentLoaded", async () => {
|
|
10
|
+
applyDarkMode(loadDarkPref());
|
|
11
|
+
setupDarkToggle();
|
|
12
|
+
setupSearch();
|
|
13
|
+
wcRestorePrefs();
|
|
14
|
+
await loadConfig();
|
|
15
|
+
await loadDocuments();
|
|
16
|
+
|
|
17
|
+
// Deep-link via ?doc=id, otherwise open first General doc
|
|
18
|
+
const params = new URLSearchParams(location.search);
|
|
19
|
+
const docId = params.get("doc");
|
|
20
|
+
if (docId) {
|
|
21
|
+
openDocument(docId, true);
|
|
22
|
+
} else {
|
|
23
|
+
const first =
|
|
24
|
+
allDocs.find((d) => d.category === "General") ?? allDocs[0];
|
|
25
|
+
if (first) openDocument(first.id, true);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Event delegation for annotation marks — survives innerHTML replacements on doc-content
|
|
29
|
+
const contentEl = document.getElementById("doc-content");
|
|
30
|
+
if (contentEl) {
|
|
31
|
+
contentEl.addEventListener("mouseover", (e) => {
|
|
32
|
+
const mark = e.target.closest("mark[data-annotation-id]");
|
|
33
|
+
if (!mark) return;
|
|
34
|
+
const ann = stabiloAnnotations.find(
|
|
35
|
+
(a) => a.id === mark.dataset.annotationId,
|
|
36
|
+
);
|
|
37
|
+
if (ann) showReadPopup(ann, mark);
|
|
38
|
+
});
|
|
39
|
+
contentEl.addEventListener("mouseout", (e) => {
|
|
40
|
+
if (!e.target.closest("mark[data-annotation-id]")) return;
|
|
41
|
+
if (
|
|
42
|
+
e.relatedTarget &&
|
|
43
|
+
e.relatedTarget.closest("mark[data-annotation-id]")
|
|
44
|
+
)
|
|
45
|
+
return;
|
|
46
|
+
scheduleHideReadPopup();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ── Image lightbox (Shift+Click) / follow link (Click) ──────────────────────
|
|
52
|
+
// Capture phase so we intercept before link handlers on child <a> elements
|
|
53
|
+
document.getElementById("doc-content").addEventListener(
|
|
54
|
+
"click",
|
|
55
|
+
(e) => {
|
|
56
|
+
const img = e.target.closest("img");
|
|
57
|
+
if (!img) return;
|
|
58
|
+
if (e.shiftKey) {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
e.stopPropagation();
|
|
61
|
+
openLightbox(img.src, img.alt);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
true,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Escape closes lightbox
|
|
68
|
+
document.addEventListener("keydown", (e) => {
|
|
69
|
+
if (
|
|
70
|
+
e.key === "Escape" &&
|
|
71
|
+
!document.getElementById("img-lightbox").classList.contains("hidden")
|
|
72
|
+
) {
|
|
73
|
+
closeLightbox();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Image-paste name confirm / cancel
|
|
78
|
+
document
|
|
79
|
+
.getElementById("img-paste-name")
|
|
80
|
+
.addEventListener("keydown", (e) => {
|
|
81
|
+
if (e.key === "Enter") imgPasteConfirm();
|
|
82
|
+
if (e.key === "Escape") imgPasteCancel();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Browser back/forward
|
|
86
|
+
window.addEventListener("popstate", (e) => {
|
|
87
|
+
const id =
|
|
88
|
+
e.state?.docId || new URLSearchParams(location.search).get("doc");
|
|
89
|
+
if (id) openDocument(id, true);
|
|
90
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// ── Configuration loading ────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
async function loadConfig() {
|
|
4
|
+
try {
|
|
5
|
+
const cfg = await fetch("/api/config").then((r) => r.json());
|
|
6
|
+
await window.initI18n(cfg.language || 'en');
|
|
7
|
+
window.applyI18n();
|
|
8
|
+
if (cfg.title) document.title = cfg.title;
|
|
9
|
+
document.getElementById("app-title").textContent =
|
|
10
|
+
cfg.title || "Living Documentation";
|
|
11
|
+
if (cfg.filenamePattern) {
|
|
12
|
+
document.getElementById("welcome-pattern").textContent =
|
|
13
|
+
cfg.filenamePattern + ".md";
|
|
14
|
+
}
|
|
15
|
+
} catch {
|
|
16
|
+
await window.initI18n('en');
|
|
17
|
+
/* non-fatal */
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// ── Dark mode toggle ─────────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
function loadDarkPref() {
|
|
4
|
+
const saved = localStorage.getItem("ld-dark");
|
|
5
|
+
if (saved !== null) return saved === "true";
|
|
6
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function applyDarkMode(dark) {
|
|
10
|
+
document.documentElement.classList.toggle("dark", dark);
|
|
11
|
+
document.getElementById("dark-icon").textContent = dark ? "☀" : "☾";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function setupDarkToggle() {
|
|
15
|
+
document.getElementById("dark-toggle").addEventListener("click", () => {
|
|
16
|
+
const isDark = document.documentElement.classList.toggle("dark");
|
|
17
|
+
localStorage.setItem("ld-dark", isDark);
|
|
18
|
+
document.getElementById("dark-icon").textContent = isDark ? "☀" : "☾";
|
|
19
|
+
});
|
|
20
|
+
}
|