living-ai-documentation 1.3.0 → 1.6.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/dist/src/frontend/documents.js +43 -25
- package/dist/src/frontend/i18n/en.json +19 -0
- package/dist/src/frontend/i18n/fr.json +19 -0
- package/dist/src/frontend/index.html +86 -3
- package/dist/src/frontend/inline-snippet-edit.js +219 -0
- package/dist/src/frontend/snippets.js +218 -44
- package/dist/src/frontend/validate.js +23 -8
- package/dist/src/lib/status.js +1 -1
- package/dist/src/lib/status.js.map +1 -1
- package/dist/starter-doc/AI/PROJECT-INSTRUCTIONS.md +48 -2
- package/dist/starter-doc/AI/default/AGENTS.md +1 -1
- package/dist/starter-doc/AI/default/CLAUDE.md +1 -1
- package/dist/starter-doc/WORKLOG/ROADMAP.md +41 -0
- package/dist/starter-doc-fr/AI/PROJECT-INSTRUCTIONS.md +48 -2
- package/dist/starter-doc-fr/AI/default/AGENTS.md +1 -1
- package/dist/starter-doc-fr/AI/default/CLAUDE.md +1 -1
- package/dist/starter-doc-fr/WORKLOG/ROADMAP.md +41 -0
- package/package.json +1 -1
|
@@ -60,6 +60,10 @@ function _wireDocContent(html) {
|
|
|
60
60
|
wrapper.appendChild(t);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
if (typeof initInlineSnippetEditing === "function") {
|
|
64
|
+
initInlineSnippetEditing(contentEl);
|
|
65
|
+
}
|
|
66
|
+
|
|
63
67
|
const notice = document.getElementById("search-notice");
|
|
64
68
|
const isMetaQuery =
|
|
65
69
|
typeof searchQuery === "string" &&
|
|
@@ -429,6 +433,44 @@ async function confirmDeleteDocument() {
|
|
|
429
433
|
}
|
|
430
434
|
|
|
431
435
|
// ── Save (in-place edit) ─────────────────────────────────────────────────────
|
|
436
|
+
async function saveCurrentDocumentContent(content) {
|
|
437
|
+
if (!currentDocId) return;
|
|
438
|
+
const res = await fetch("/api/documents/" + currentDocId, {
|
|
439
|
+
method: "PUT",
|
|
440
|
+
headers: { "Content-Type": "application/json" },
|
|
441
|
+
body: JSON.stringify({ content }),
|
|
442
|
+
});
|
|
443
|
+
if (!res.ok) throw new Error(await res.text());
|
|
444
|
+
|
|
445
|
+
currentDocContent = content;
|
|
446
|
+
|
|
447
|
+
// Re-fetch rendered HTML and update view
|
|
448
|
+
const doc = await fetch("/api/documents/" + currentDocId).then((r) =>
|
|
449
|
+
r.json(),
|
|
450
|
+
);
|
|
451
|
+
_lastDocHtml = doc.html;
|
|
452
|
+
_lastDocIdRendered = currentDocId;
|
|
453
|
+
_wireDocContent(doc.html);
|
|
454
|
+
|
|
455
|
+
if (typeof applyAnnotationHighlights === "function") {
|
|
456
|
+
applyAnnotationHighlights();
|
|
457
|
+
}
|
|
458
|
+
if (typeof renderElevator === "function") 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
|
+
if (typeof updateValidateButtonForCurrentDoc === "function") {
|
|
467
|
+
updateValidateButtonForCurrentDoc();
|
|
468
|
+
}
|
|
469
|
+
if (typeof loadMetadataReport === "function") {
|
|
470
|
+
loadMetadataReport(currentDocId);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
432
474
|
async function saveDocument() {
|
|
433
475
|
if (!currentDocId) return;
|
|
434
476
|
const content = document.getElementById("doc-editor").value;
|
|
@@ -437,31 +479,7 @@ async function saveDocument() {
|
|
|
437
479
|
msgEl.className = "text-xs text-gray-400";
|
|
438
480
|
|
|
439
481
|
try {
|
|
440
|
-
|
|
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();
|
|
482
|
+
await saveCurrentDocumentContent(content);
|
|
465
483
|
|
|
466
484
|
exitEditMode();
|
|
467
485
|
} catch (err) {
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"doc.validate_btn": "Validate",
|
|
71
71
|
"doc.validate_title": "Validate this document?",
|
|
72
72
|
"doc.validate_message": "The frontmatter status will change from \"To be validated\" to \"Accepted\".",
|
|
73
|
+
"doc.validate_worklog_message": "The frontmatter status will change from \"To be validated\" to \"Done\".",
|
|
73
74
|
"doc.validate_detail_low_accuracy": "Reliability is currently {accuracy}. Confirming will also re-baseline source-file hashes, bringing reliability back to 100%.",
|
|
74
75
|
"doc.validate_confirm": "Validate",
|
|
75
76
|
"doc.validate_failed": "Validation failed: ",
|
|
@@ -282,8 +283,16 @@
|
|
|
282
283
|
"snippet.link_anchor_select_hint": "(pick a heading from the document)",
|
|
283
284
|
"snippet.link_anchor_no_headings": "No headings detected in this document",
|
|
284
285
|
"snippet.link_target_doc_label": "Target document",
|
|
286
|
+
"snippet.ordered_list_content_label": "Items",
|
|
287
|
+
"snippet.ordered_list_content_placeholder": "One item per line…",
|
|
288
|
+
"snippet.unordered_list_content_label": "Items",
|
|
289
|
+
"snippet.unordered_list_content_placeholder": "One item per line…",
|
|
285
290
|
"snippet.code_lang_label": "Language",
|
|
286
291
|
"snippet.code_lang_hint": "(e.g. javascript, python, bash…)",
|
|
292
|
+
"snippet.code_content_label": "Code",
|
|
293
|
+
"snippet.code_content_placeholder": "Paste or edit code here…",
|
|
294
|
+
"snippet.blockquote_content_label": "Quote",
|
|
295
|
+
"snippet.blockquote_content_placeholder": "Quote text…",
|
|
287
296
|
"snippet.image_alt_label": "Alt text",
|
|
288
297
|
"snippet.image_alt_placeholder": "Image description",
|
|
289
298
|
"snippet.image_url_label": "Image URL",
|
|
@@ -295,6 +304,16 @@
|
|
|
295
304
|
"snippet.tree_add_btn": "+ Add item",
|
|
296
305
|
"snippet.markdown_preview_label": "Markdown preview",
|
|
297
306
|
"snippet.insert_btn": "Insert",
|
|
307
|
+
"snippet.inline_edit_btn": "Edit inline",
|
|
308
|
+
"snippet.inline_modal_title": "Edit inline snippet",
|
|
309
|
+
"snippet.inline_save_btn": "Save",
|
|
310
|
+
"snippet.inline_save_failed": "Inline edit failed: ",
|
|
311
|
+
"snippet.inline_delete_btn": "Delete block",
|
|
312
|
+
"snippet.inline_delete_title": "Delete block",
|
|
313
|
+
"snippet.inline_delete_message": "Delete this block from the document?",
|
|
314
|
+
"snippet.inline_delete_detail": "This action will remove the selected Markdown block.",
|
|
315
|
+
"snippet.inline_delete_confirm_btn": "Delete",
|
|
316
|
+
"snippet.inline_delete_failed": "Inline block deletion failed: ",
|
|
298
317
|
"snippet.diagram_existing": "Existing diagram",
|
|
299
318
|
"snippet.diagram_new": "New diagram",
|
|
300
319
|
"snippet.diagram_select_label": "Select diagram",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"doc.validate_btn": "Valider",
|
|
71
71
|
"doc.validate_title": "Valider ce document ?",
|
|
72
72
|
"doc.validate_message": "Le statut frontmatter va passer de « To be validated » à « Accepted ».",
|
|
73
|
+
"doc.validate_worklog_message": "Le statut frontmatter va passer de « To be validated » à « Done ».",
|
|
73
74
|
"doc.validate_detail_low_accuracy": "La fiabilité actuelle est de {accuracy}. Confirmer va aussi réinitialiser les hashes des fichiers source, ramenant la fiabilité à 100 %.",
|
|
74
75
|
"doc.validate_confirm": "Valider",
|
|
75
76
|
"doc.validate_failed": "Échec de la validation : ",
|
|
@@ -282,8 +283,16 @@
|
|
|
282
283
|
"snippet.link_anchor_select_hint": "(choisissez un titre du document)",
|
|
283
284
|
"snippet.link_anchor_no_headings": "Aucun titre détecté dans ce document",
|
|
284
285
|
"snippet.link_target_doc_label": "Document cible",
|
|
286
|
+
"snippet.ordered_list_content_label": "Éléments",
|
|
287
|
+
"snippet.ordered_list_content_placeholder": "Un élément par ligne…",
|
|
288
|
+
"snippet.unordered_list_content_label": "Éléments",
|
|
289
|
+
"snippet.unordered_list_content_placeholder": "Un élément par ligne…",
|
|
285
290
|
"snippet.code_lang_label": "Langage",
|
|
286
291
|
"snippet.code_lang_hint": "(ex : javascript, python, bash…)",
|
|
292
|
+
"snippet.code_content_label": "Code",
|
|
293
|
+
"snippet.code_content_placeholder": "Collez ou modifiez le code ici…",
|
|
294
|
+
"snippet.blockquote_content_label": "Citation",
|
|
295
|
+
"snippet.blockquote_content_placeholder": "Texte de la citation…",
|
|
287
296
|
"snippet.image_alt_label": "Texte alternatif",
|
|
288
297
|
"snippet.image_alt_placeholder": "Description de l'image",
|
|
289
298
|
"snippet.image_url_label": "URL de l'image",
|
|
@@ -295,6 +304,16 @@
|
|
|
295
304
|
"snippet.tree_add_btn": "+ Ajouter un élément",
|
|
296
305
|
"snippet.markdown_preview_label": "Aperçu du markdown",
|
|
297
306
|
"snippet.insert_btn": "Insérer",
|
|
307
|
+
"snippet.inline_edit_btn": "Édition inline",
|
|
308
|
+
"snippet.inline_modal_title": "Modifier le snippet inline",
|
|
309
|
+
"snippet.inline_save_btn": "Enregistrer",
|
|
310
|
+
"snippet.inline_save_failed": "Échec de l'édition inline : ",
|
|
311
|
+
"snippet.inline_delete_btn": "Supprimer le bloc",
|
|
312
|
+
"snippet.inline_delete_title": "Supprimer le bloc",
|
|
313
|
+
"snippet.inline_delete_message": "Supprimer ce bloc du document ?",
|
|
314
|
+
"snippet.inline_delete_detail": "Cette action retirera le bloc Markdown sélectionné.",
|
|
315
|
+
"snippet.inline_delete_confirm_btn": "Supprimer",
|
|
316
|
+
"snippet.inline_delete_failed": "Échec de la suppression inline : ",
|
|
298
317
|
"snippet.diagram_existing": "Diagramme existant",
|
|
299
318
|
"snippet.diagram_new": "Nouveau diagramme",
|
|
300
319
|
"snippet.diagram_select_label": "Sélectionner un diagramme",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
<script defer src="/documents.js"></script>
|
|
49
49
|
<script defer src="/misc.js"></script>
|
|
50
50
|
<script defer src="/snippets.js"></script>
|
|
51
|
+
<script defer src="/inline-snippet-edit.js"></script>
|
|
51
52
|
<script defer src="/annotations.js"></script>
|
|
52
53
|
<script defer src="/metadata.js"></script>
|
|
53
54
|
<script defer src="/accuracy-gauge.js"></script>
|
|
@@ -1425,12 +1426,13 @@
|
|
|
1425
1426
|
<div
|
|
1426
1427
|
id="snippets-modal"
|
|
1427
1428
|
class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
|
1428
|
-
|
|
1429
|
+
>
|
|
1429
1430
|
<div
|
|
1431
|
+
id="snippet-modal-card"
|
|
1430
1432
|
class="bg-white dark:bg-gray-900 rounded-xl shadow-xl w-full max-w-lg mx-4 p-6 space-y-5 max-h-[90vh] overflow-y-auto"
|
|
1431
1433
|
>
|
|
1432
1434
|
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-50">
|
|
1433
|
-
<span data-i18n="snippet.modal_title">🧩 Insert a snippet</span>
|
|
1435
|
+
<span id="snippet-modal-title" data-i18n="snippet.modal_title">🧩 Insert a snippet</span>
|
|
1434
1436
|
</h3>
|
|
1435
1437
|
|
|
1436
1438
|
<div
|
|
@@ -1636,6 +1638,44 @@
|
|
|
1636
1638
|
</div>
|
|
1637
1639
|
</div>
|
|
1638
1640
|
|
|
1641
|
+
<!-- Panel: ordered-list -->
|
|
1642
|
+
<div id="snip-panel-ordered-list" class="hidden space-y-3">
|
|
1643
|
+
<div class="space-y-1.5">
|
|
1644
|
+
<label
|
|
1645
|
+
data-i18n="snippet.ordered_list_content_label"
|
|
1646
|
+
class="block text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
1647
|
+
>Items</label
|
|
1648
|
+
>
|
|
1649
|
+
<textarea
|
|
1650
|
+
id="snip-ordered-list-content"
|
|
1651
|
+
rows="10"
|
|
1652
|
+
data-i18n-placeholder="snippet.ordered_list_content_placeholder"
|
|
1653
|
+
placeholder="One item per line…"
|
|
1654
|
+
oninput="snippetUpdatePreview()"
|
|
1655
|
+
class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-y leading-relaxed"
|
|
1656
|
+
></textarea>
|
|
1657
|
+
</div>
|
|
1658
|
+
</div>
|
|
1659
|
+
|
|
1660
|
+
<!-- Panel: unordered-list -->
|
|
1661
|
+
<div id="snip-panel-unordered-list" class="hidden space-y-3">
|
|
1662
|
+
<div class="space-y-1.5">
|
|
1663
|
+
<label
|
|
1664
|
+
data-i18n="snippet.unordered_list_content_label"
|
|
1665
|
+
class="block text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
1666
|
+
>Items</label
|
|
1667
|
+
>
|
|
1668
|
+
<textarea
|
|
1669
|
+
id="snip-unordered-list-content"
|
|
1670
|
+
rows="10"
|
|
1671
|
+
data-i18n-placeholder="snippet.unordered_list_content_placeholder"
|
|
1672
|
+
placeholder="One item per line…"
|
|
1673
|
+
oninput="snippetUpdatePreview()"
|
|
1674
|
+
class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-y leading-relaxed"
|
|
1675
|
+
></textarea>
|
|
1676
|
+
</div>
|
|
1677
|
+
</div>
|
|
1678
|
+
|
|
1639
1679
|
<!-- Panel: code-block -->
|
|
1640
1680
|
<div id="snip-panel-code-block" class="hidden space-y-3">
|
|
1641
1681
|
<div class="space-y-1.5">
|
|
@@ -1654,6 +1694,40 @@
|
|
|
1654
1694
|
class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
1655
1695
|
/>
|
|
1656
1696
|
</div>
|
|
1697
|
+
<div class="space-y-1.5">
|
|
1698
|
+
<label
|
|
1699
|
+
data-i18n="snippet.code_content_label"
|
|
1700
|
+
class="block text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
1701
|
+
>Code</label
|
|
1702
|
+
>
|
|
1703
|
+
<textarea
|
|
1704
|
+
id="snip-code-content"
|
|
1705
|
+
rows="12"
|
|
1706
|
+
data-i18n-placeholder="snippet.code_content_placeholder"
|
|
1707
|
+
placeholder="Paste or edit code here…"
|
|
1708
|
+
oninput="snippetUpdatePreview()"
|
|
1709
|
+
class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-y font-mono leading-relaxed"
|
|
1710
|
+
></textarea>
|
|
1711
|
+
</div>
|
|
1712
|
+
</div>
|
|
1713
|
+
|
|
1714
|
+
<!-- Panel: blockquote -->
|
|
1715
|
+
<div id="snip-panel-blockquote" class="hidden space-y-3">
|
|
1716
|
+
<div class="space-y-1.5">
|
|
1717
|
+
<label
|
|
1718
|
+
data-i18n="snippet.blockquote_content_label"
|
|
1719
|
+
class="block text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
1720
|
+
>Quote</label
|
|
1721
|
+
>
|
|
1722
|
+
<textarea
|
|
1723
|
+
id="snip-blockquote-content"
|
|
1724
|
+
rows="8"
|
|
1725
|
+
data-i18n-placeholder="snippet.blockquote_content_placeholder"
|
|
1726
|
+
placeholder="Quote text…"
|
|
1727
|
+
oninput="snippetUpdatePreview()"
|
|
1728
|
+
class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-y leading-relaxed"
|
|
1729
|
+
></textarea>
|
|
1730
|
+
</div>
|
|
1657
1731
|
</div>
|
|
1658
1732
|
|
|
1659
1733
|
<!-- Panel: image -->
|
|
@@ -2045,7 +2119,7 @@
|
|
|
2045
2119
|
</div>
|
|
2046
2120
|
|
|
2047
2121
|
<!-- Markdown preview -->
|
|
2048
|
-
<div class="space-y-1.5">
|
|
2122
|
+
<div id="snippet-preview-wrap" class="space-y-1.5">
|
|
2049
2123
|
<label
|
|
2050
2124
|
data-i18n="snippet.markdown_preview_label"
|
|
2051
2125
|
class="block text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
@@ -2066,8 +2140,17 @@
|
|
|
2066
2140
|
>
|
|
2067
2141
|
Cancel
|
|
2068
2142
|
</button>
|
|
2143
|
+
<button
|
|
2144
|
+
onclick="deleteInlineSnippetBlock()"
|
|
2145
|
+
id="snippet-delete-btn"
|
|
2146
|
+
data-i18n="snippet.inline_delete_btn"
|
|
2147
|
+
class="hidden text-sm px-4 py-2 rounded-lg border border-red-200 dark:border-red-800 text-red-600 dark:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/30 font-semibold transition-colors"
|
|
2148
|
+
>
|
|
2149
|
+
Delete block
|
|
2150
|
+
</button>
|
|
2069
2151
|
<button
|
|
2070
2152
|
onclick="insertSnippet()"
|
|
2153
|
+
id="snippet-submit-btn"
|
|
2071
2154
|
data-i18n="snippet.insert_btn"
|
|
2072
2155
|
class="text-sm px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold transition-colors"
|
|
2073
2156
|
>
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// ── Inline snippet editing from rendered viewer ─────────────────────────────
|
|
2
|
+
// Right-clicking a rendered fragment that maps to a known snippet offers an
|
|
3
|
+
// inline edit action. The actual form stays the Snippets modal; this file only
|
|
4
|
+
// maps rendered DOM elements back to source Markdown ranges.
|
|
5
|
+
|
|
6
|
+
const _INLINE_SNIPPET_TYPES = new Set([
|
|
7
|
+
"anchor-doc-link",
|
|
8
|
+
"doc-link",
|
|
9
|
+
"anchor-link",
|
|
10
|
+
"image",
|
|
11
|
+
"link",
|
|
12
|
+
"collapsible",
|
|
13
|
+
"colored-text",
|
|
14
|
+
"colored-section",
|
|
15
|
+
"tree",
|
|
16
|
+
"code-block",
|
|
17
|
+
"table",
|
|
18
|
+
"blockquote",
|
|
19
|
+
"separator",
|
|
20
|
+
"ordered-list",
|
|
21
|
+
"unordered-list",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const _INLINE_TYPE_SELECTORS = [
|
|
25
|
+
{ types: ["collapsible"], selector: "details" },
|
|
26
|
+
{ types: ["colored-section"], selector: 'div[style*="border-left"]' },
|
|
27
|
+
{ types: ["colored-text"], selector: 'span[style*="color"]' },
|
|
28
|
+
{ types: ["tree", "code-block"], selector: "pre" },
|
|
29
|
+
{ types: ["table"], selector: "table" },
|
|
30
|
+
{ types: ["blockquote"], selector: "blockquote" },
|
|
31
|
+
{ types: ["separator"], selector: "hr" },
|
|
32
|
+
{ types: ["ordered-list"], selector: "ol" },
|
|
33
|
+
{ types: ["unordered-list"], selector: "ul" },
|
|
34
|
+
{ types: ["image"], selector: "img" },
|
|
35
|
+
{
|
|
36
|
+
types: ["anchor-doc-link", "doc-link", "anchor-link", "link"],
|
|
37
|
+
selector: "a[href]",
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
let _inlineSnippetPopup = null;
|
|
42
|
+
|
|
43
|
+
function _inlineRangesOverlap(a, b) {
|
|
44
|
+
return a.start < b.end && b.start < a.end;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function _inlineAddRange(ranges, start, raw) {
|
|
48
|
+
if (!raw || !raw.trim()) return;
|
|
49
|
+
const type = detectSnippetType(raw);
|
|
50
|
+
if (!type || !_INLINE_SNIPPET_TYPES.has(type)) return;
|
|
51
|
+
const end = start + raw.length;
|
|
52
|
+
const candidate = { start, end, type };
|
|
53
|
+
if (ranges.some((existing) => _inlineRangesOverlap(existing, candidate))) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
ranges.push(candidate);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _inlineAddRegexRanges(ranges, content, regex, groupIndex = 0) {
|
|
60
|
+
let match;
|
|
61
|
+
while ((match = regex.exec(content))) {
|
|
62
|
+
const raw = match[groupIndex];
|
|
63
|
+
const start =
|
|
64
|
+
match.index + (groupIndex > 0 ? match[0].indexOf(raw) : 0);
|
|
65
|
+
_inlineAddRange(ranges, start, raw);
|
|
66
|
+
if (match[0].length === 0) regex.lastIndex += 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _inlineCollectSnippetRanges(content) {
|
|
71
|
+
const ranges = [];
|
|
72
|
+
|
|
73
|
+
_inlineAddRegexRanges(ranges, content, /```[\s\S]*?```/g);
|
|
74
|
+
_inlineAddRegexRanges(ranges, content, /<details[\s\S]*?<\/details>/gi);
|
|
75
|
+
_inlineAddRegexRanges(
|
|
76
|
+
ranges,
|
|
77
|
+
content,
|
|
78
|
+
/<div\b[^>]*border-left[^>]*>[\s\S]*?<\/div>/gi,
|
|
79
|
+
);
|
|
80
|
+
_inlineAddRegexRanges(
|
|
81
|
+
ranges,
|
|
82
|
+
content,
|
|
83
|
+
/<span\b[^>]*color:[^>]*>[\s\S]*?<\/span>/gi,
|
|
84
|
+
);
|
|
85
|
+
_inlineAddRegexRanges(
|
|
86
|
+
ranges,
|
|
87
|
+
content,
|
|
88
|
+
/(?:^|\n)((?:\|[^\n]*\|\n)\|[ \t:|-]*\|(?:\n\|[^\n]*\|)*)/g,
|
|
89
|
+
1,
|
|
90
|
+
);
|
|
91
|
+
_inlineAddRegexRanges(ranges, content, /^> .*(?:\n>.*)*/gm);
|
|
92
|
+
_inlineAddRegexRanges(ranges, content, /^1\. .*(?:\n(?:\d+\.| {3,}\d+\.) .*)*/gm);
|
|
93
|
+
_inlineAddRegexRanges(ranges, content, /^- .*(?:\n(?:- | {2,}- ).*)*/gm);
|
|
94
|
+
_inlineAddRegexRanges(ranges, content, /!\[[^\]\n]*\]\([^)]+\)/g);
|
|
95
|
+
_inlineAddRegexRanges(ranges, content, /\[[^\]\n]+\]\([^)]+\)/g);
|
|
96
|
+
|
|
97
|
+
const frontmatter = content.match(/^---\s*\n[\s\S]*?\n---/);
|
|
98
|
+
_inlineAddRegexRanges(ranges, content, /^---$/gm);
|
|
99
|
+
return ranges
|
|
100
|
+
.filter((range) => {
|
|
101
|
+
if (range.type !== "separator") return true;
|
|
102
|
+
return !frontmatter || range.start > frontmatter[0].length;
|
|
103
|
+
})
|
|
104
|
+
.sort((a, b) => a.start - b.start);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function _inlineTopLevelListElements(contentEl, selector) {
|
|
108
|
+
return Array.from(contentEl.querySelectorAll(selector)).filter(
|
|
109
|
+
(el) => !el.parentElement.closest("ol, ul"),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _inlineAssignElements(contentEl, candidates) {
|
|
114
|
+
contentEl
|
|
115
|
+
.querySelectorAll("[data-inline-snippet-index]")
|
|
116
|
+
.forEach((el) => {
|
|
117
|
+
el.removeAttribute("data-inline-snippet-index");
|
|
118
|
+
el.classList.remove("ld-inline-snippet-target");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
for (const { types, selector } of _INLINE_TYPE_SELECTORS) {
|
|
122
|
+
const matching = candidates.filter((candidate) =>
|
|
123
|
+
types.includes(candidate.type),
|
|
124
|
+
);
|
|
125
|
+
if (!matching.length) continue;
|
|
126
|
+
const elements =
|
|
127
|
+
selector === "ol" || selector === "ul"
|
|
128
|
+
? _inlineTopLevelListElements(contentEl, selector)
|
|
129
|
+
: Array.from(contentEl.querySelectorAll(selector));
|
|
130
|
+
const limit = Math.min(matching.length, elements.length);
|
|
131
|
+
for (let i = 0; i < limit; i += 1) {
|
|
132
|
+
elements[i].dataset.inlineSnippetIndex = String(
|
|
133
|
+
candidates.indexOf(matching[i]),
|
|
134
|
+
);
|
|
135
|
+
elements[i].classList.add("ld-inline-snippet-target");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function _inlineClosePopup() {
|
|
141
|
+
if (_inlineSnippetPopup) {
|
|
142
|
+
_inlineSnippetPopup.remove();
|
|
143
|
+
_inlineSnippetPopup = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function _inlineShowPopup(event, range) {
|
|
148
|
+
_inlineClosePopup();
|
|
149
|
+
const popup = document.createElement("div");
|
|
150
|
+
popup.id = "inline-snippet-popup";
|
|
151
|
+
popup.className =
|
|
152
|
+
"fixed z-50 rounded-lg border border-blue-200 dark:border-blue-800 bg-white dark:bg-gray-900 shadow-lg p-1";
|
|
153
|
+
const btn = document.createElement("button");
|
|
154
|
+
btn.type = "button";
|
|
155
|
+
btn.className =
|
|
156
|
+
"inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-semibold text-blue-700 dark:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/30";
|
|
157
|
+
const icon = document.createElement("i");
|
|
158
|
+
icon.className = "fa-solid fa-pen-to-square";
|
|
159
|
+
icon.setAttribute("aria-hidden", "true");
|
|
160
|
+
const label = document.createElement("span");
|
|
161
|
+
label.textContent = window.t("snippet.inline_edit_btn");
|
|
162
|
+
btn.append(icon, label);
|
|
163
|
+
btn.addEventListener("click", (e) => {
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
e.stopPropagation();
|
|
166
|
+
_inlineClosePopup();
|
|
167
|
+
openSnippetsModalForInlineEdit(range);
|
|
168
|
+
});
|
|
169
|
+
popup.appendChild(btn);
|
|
170
|
+
document.body.appendChild(popup);
|
|
171
|
+
|
|
172
|
+
const margin = 8;
|
|
173
|
+
const rect = popup.getBoundingClientRect();
|
|
174
|
+
const left = Math.min(
|
|
175
|
+
window.innerWidth - rect.width - margin,
|
|
176
|
+
Math.max(margin, event.clientX),
|
|
177
|
+
);
|
|
178
|
+
const top = Math.max(margin, event.clientY - rect.height - margin);
|
|
179
|
+
popup.style.left = `${left}px`;
|
|
180
|
+
popup.style.top = `${top}px`;
|
|
181
|
+
_inlineSnippetPopup = popup;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function initInlineSnippetEditing(contentEl) {
|
|
185
|
+
if (!contentEl || typeof currentDocContent !== "string") return;
|
|
186
|
+
const candidates = _inlineCollectSnippetRanges(currentDocContent);
|
|
187
|
+
_inlineAssignElements(contentEl, candidates);
|
|
188
|
+
|
|
189
|
+
if (contentEl._inlineSnippetContextHandler) {
|
|
190
|
+
contentEl.removeEventListener(
|
|
191
|
+
"contextmenu",
|
|
192
|
+
contentEl._inlineSnippetContextHandler,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
contentEl._inlineSnippetContextHandler = (event) => {
|
|
196
|
+
const target = event.target.closest("[data-inline-snippet-index]");
|
|
197
|
+
if (!target || !contentEl.contains(target)) return;
|
|
198
|
+
const range = candidates[Number(target.dataset.inlineSnippetIndex)];
|
|
199
|
+
if (!range) return;
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
_inlineShowPopup(event, range);
|
|
202
|
+
};
|
|
203
|
+
contentEl.addEventListener(
|
|
204
|
+
"contextmenu",
|
|
205
|
+
contentEl._inlineSnippetContextHandler,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
document.addEventListener("click", (event) => {
|
|
210
|
+
if (_inlineSnippetPopup && !_inlineSnippetPopup.contains(event.target)) {
|
|
211
|
+
_inlineClosePopup();
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
document.addEventListener("keydown", (event) => {
|
|
216
|
+
if (event.key === "Escape") _inlineClosePopup();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
window.initInlineSnippetEditing = initInlineSnippetEditing;
|
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
let _snippetSelStart = 0;
|
|
8
8
|
let _snippetSelEnd = 0;
|
|
9
|
+
let _snippetInlineEdit = false;
|
|
9
10
|
const _SNIPPET_PANELS = [
|
|
10
11
|
"collapsible",
|
|
11
12
|
"link",
|
|
12
13
|
"doc-link",
|
|
13
14
|
"anchor-link",
|
|
14
15
|
"anchor-doc-link",
|
|
16
|
+
"ordered-list",
|
|
17
|
+
"unordered-list",
|
|
15
18
|
"code-block",
|
|
19
|
+
"blockquote",
|
|
16
20
|
"image",
|
|
17
21
|
"table",
|
|
18
22
|
"tree",
|
|
@@ -626,11 +630,38 @@ async function snippetAnchorDocChanged() {
|
|
|
626
630
|
snippetUpdatePreview();
|
|
627
631
|
}
|
|
628
632
|
|
|
629
|
-
function
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
+
function _setSnippetModalMode(isInlineEdit) {
|
|
634
|
+
_snippetInlineEdit = !!isInlineEdit;
|
|
635
|
+
const title = document.getElementById("snippet-modal-title");
|
|
636
|
+
if (title) {
|
|
637
|
+
title.textContent = window.t(
|
|
638
|
+
_snippetInlineEdit ? "snippet.inline_modal_title" : "snippet.modal_title",
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
const submit = document.getElementById("snippet-submit-btn");
|
|
642
|
+
if (submit) {
|
|
643
|
+
submit.textContent = window.t(
|
|
644
|
+
_snippetInlineEdit ? "snippet.inline_save_btn" : "snippet.insert_btn",
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
const typeSelect = document.getElementById("snippet-type");
|
|
648
|
+
if (typeSelect) {
|
|
649
|
+
typeSelect.disabled = _snippetInlineEdit;
|
|
650
|
+
typeSelect.classList.toggle("cursor-not-allowed", _snippetInlineEdit);
|
|
651
|
+
typeSelect.classList.toggle("opacity-70", _snippetInlineEdit);
|
|
652
|
+
}
|
|
653
|
+
const deleteBtn = document.getElementById("snippet-delete-btn");
|
|
654
|
+
if (deleteBtn) {
|
|
655
|
+
deleteBtn.classList.toggle("hidden", !_snippetInlineEdit);
|
|
656
|
+
}
|
|
657
|
+
const card = document.getElementById("snippet-modal-card");
|
|
658
|
+
if (card) {
|
|
659
|
+
card.classList.toggle("max-w-lg", !_snippetInlineEdit);
|
|
660
|
+
card.classList.toggle("max-w-5xl", _snippetInlineEdit);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
633
663
|
|
|
664
|
+
function _openSnippetsModalForText(selectedText, detectedOverride = null) {
|
|
634
665
|
const docOpts = allDocs
|
|
635
666
|
.map((d) => `<option value="${d.id}">${d.title}</option>`)
|
|
636
667
|
.join("");
|
|
@@ -640,13 +671,9 @@ function openSnippetsModal() {
|
|
|
640
671
|
snippetAnchorDocChanged();
|
|
641
672
|
|
|
642
673
|
const msgEl = document.getElementById("snippet-detect-msg");
|
|
643
|
-
const selectedText = editor.value.slice(
|
|
644
|
-
_snippetSelStart,
|
|
645
|
-
_snippetSelEnd,
|
|
646
|
-
);
|
|
647
674
|
|
|
648
675
|
if (selectedText) {
|
|
649
|
-
const detected = detectSnippetType(selectedText);
|
|
676
|
+
const detected = detectedOverride || detectSnippetType(selectedText);
|
|
650
677
|
if (detected) {
|
|
651
678
|
document.getElementById("snippet-type").value = detected;
|
|
652
679
|
snippetTypeChanged();
|
|
@@ -688,8 +715,26 @@ function openSnippetsModal() {
|
|
|
688
715
|
document.getElementById("snippets-modal").classList.remove("hidden");
|
|
689
716
|
}
|
|
690
717
|
|
|
718
|
+
function openSnippetsModal() {
|
|
719
|
+
const editor = document.getElementById("doc-editor");
|
|
720
|
+
_snippetSelStart = editor.selectionStart;
|
|
721
|
+
_snippetSelEnd = editor.selectionEnd;
|
|
722
|
+
_setSnippetModalMode(false);
|
|
723
|
+
_openSnippetsModalForText(editor.value.slice(_snippetSelStart, _snippetSelEnd));
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function openSnippetsModalForInlineEdit(range) {
|
|
727
|
+
if (!range || typeof currentDocContent !== "string") return;
|
|
728
|
+
_snippetSelStart = range.start;
|
|
729
|
+
_snippetSelEnd = range.end;
|
|
730
|
+
_setSnippetModalMode(true);
|
|
731
|
+
const selectedText = currentDocContent.slice(_snippetSelStart, _snippetSelEnd);
|
|
732
|
+
_openSnippetsModalForText(selectedText, range.type || null);
|
|
733
|
+
}
|
|
734
|
+
|
|
691
735
|
function closeSnippetsModal() {
|
|
692
736
|
document.getElementById("snippets-modal").classList.add("hidden");
|
|
737
|
+
_setSnippetModalMode(false);
|
|
693
738
|
}
|
|
694
739
|
|
|
695
740
|
function snippetTypeChanged() {
|
|
@@ -698,8 +743,21 @@ function snippetTypeChanged() {
|
|
|
698
743
|
const panel = document.getElementById("snip-panel-" + p);
|
|
699
744
|
if (panel) panel.classList.toggle("hidden", p !== type);
|
|
700
745
|
});
|
|
701
|
-
const previewWrap = document.getElementById("snippet-preview")
|
|
702
|
-
if (previewWrap)
|
|
746
|
+
const previewWrap = document.getElementById("snippet-preview-wrap");
|
|
747
|
+
if (previewWrap) {
|
|
748
|
+
previewWrap.classList.toggle(
|
|
749
|
+
"hidden",
|
|
750
|
+
type === "attachment" ||
|
|
751
|
+
type === "table" ||
|
|
752
|
+
type === "code-block" ||
|
|
753
|
+
type === "blockquote" ||
|
|
754
|
+
type === "ordered-list" ||
|
|
755
|
+
type === "unordered-list" ||
|
|
756
|
+
type === "colored-section" ||
|
|
757
|
+
type === "colored-text" ||
|
|
758
|
+
type === "tree",
|
|
759
|
+
);
|
|
760
|
+
}
|
|
703
761
|
|
|
704
762
|
if (type === "table") tableInit();
|
|
705
763
|
else if (type === "tree") treeInit();
|
|
@@ -821,32 +879,70 @@ function buildSnippetMarkdown() {
|
|
|
821
879
|
window.t('snippet.link_anchor_placeholder');
|
|
822
880
|
return `[${text}](?doc=${encodeURIComponent(docId)}#${anchor})`;
|
|
823
881
|
}
|
|
824
|
-
case "ordered-list":
|
|
825
|
-
|
|
826
|
-
"
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
882
|
+
case "ordered-list": {
|
|
883
|
+
const content =
|
|
884
|
+
document.getElementById("snip-ordered-list-content").value ||
|
|
885
|
+
[
|
|
886
|
+
"Élément 1",
|
|
887
|
+
"Élément 2",
|
|
888
|
+
" Sous-élément 2.1",
|
|
889
|
+
" Sous-élément 2.2",
|
|
890
|
+
"Élément 3",
|
|
891
|
+
" Sous-élément 3.1",
|
|
892
|
+
" Sous-sous-élément 3.1.1",
|
|
893
|
+
].join("\n");
|
|
894
|
+
const countersByIndent = new Map();
|
|
895
|
+
return content
|
|
896
|
+
.split("\n")
|
|
897
|
+
.filter((line) => line.trim())
|
|
898
|
+
.map((line) => {
|
|
899
|
+
const indent = line.match(/^\s*/)[0];
|
|
900
|
+
const indentLength = indent.length;
|
|
901
|
+
for (const knownIndent of Array.from(countersByIndent.keys())) {
|
|
902
|
+
if (knownIndent > indentLength) countersByIndent.delete(knownIndent);
|
|
903
|
+
}
|
|
904
|
+
const next = (countersByIndent.get(indentLength) || 0) + 1;
|
|
905
|
+
countersByIndent.set(indentLength, next);
|
|
906
|
+
return `${indent}${next}. ${line.trim()}`;
|
|
907
|
+
})
|
|
908
|
+
.join("\n");
|
|
909
|
+
}
|
|
910
|
+
case "unordered-list": {
|
|
911
|
+
const content =
|
|
912
|
+
document.getElementById("snip-unordered-list-content").value ||
|
|
913
|
+
[
|
|
914
|
+
"Élément 1",
|
|
915
|
+
"Élément 2",
|
|
916
|
+
" Sous-élément 2.1",
|
|
917
|
+
" Sous-élément 2.2",
|
|
918
|
+
"Élément 3",
|
|
919
|
+
" Sous-élément 3.1",
|
|
920
|
+
" Sous-sous-élément 3.1.1",
|
|
921
|
+
].join("\n");
|
|
922
|
+
return content
|
|
923
|
+
.split("\n")
|
|
924
|
+
.filter((line) => line.trim())
|
|
925
|
+
.map((line) => {
|
|
926
|
+
const indent = line.match(/^\s*/)[0];
|
|
927
|
+
return `${indent}- ${line.trim()}`;
|
|
928
|
+
})
|
|
929
|
+
.join("\n");
|
|
930
|
+
}
|
|
844
931
|
case "code-block": {
|
|
845
932
|
const lang = document.getElementById("snip-code-lang").value || "";
|
|
846
|
-
|
|
933
|
+
const code =
|
|
934
|
+
document.getElementById("snip-code-content").value || "// code ici";
|
|
935
|
+
return `\`\`\`${lang}\n${code}\n\`\`\``;
|
|
936
|
+
}
|
|
937
|
+
case "blockquote": {
|
|
938
|
+
const content =
|
|
939
|
+
document.getElementById("snip-blockquote-content").value ||
|
|
940
|
+
"Citation ici\n\n— Auteur";
|
|
941
|
+
return content
|
|
942
|
+
.split("\n")
|
|
943
|
+
.map((line) => (line.trim() ? `> ${line}` : ">"))
|
|
944
|
+
.join("\n");
|
|
847
945
|
}
|
|
848
|
-
case "blockquote":
|
|
849
|
-
return `> Citation ici\n>\n> — Auteur`;
|
|
850
946
|
case "separator":
|
|
851
947
|
return `\n---\n`;
|
|
852
948
|
case "image": {
|
|
@@ -903,8 +999,11 @@ function snippetUpdatePreview() {
|
|
|
903
999
|
buildSnippetMarkdown();
|
|
904
1000
|
}
|
|
905
1001
|
|
|
906
|
-
function insertSnippet() {
|
|
1002
|
+
async function insertSnippet() {
|
|
907
1003
|
const type = document.getElementById("snippet-type").value;
|
|
1004
|
+
if (_snippetInlineEdit && (type === "diagram" || type === "attachment")) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
908
1007
|
if (type === "diagram") {
|
|
909
1008
|
insertDiagramSnippet();
|
|
910
1009
|
return;
|
|
@@ -915,7 +1014,21 @@ function insertSnippet() {
|
|
|
915
1014
|
return;
|
|
916
1015
|
}
|
|
917
1016
|
const text = buildSnippetMarkdown();
|
|
1017
|
+
const wasInlineEdit = _snippetInlineEdit;
|
|
918
1018
|
closeSnippetsModal();
|
|
1019
|
+
if (wasInlineEdit) {
|
|
1020
|
+
const before = currentDocContent.slice(0, _snippetSelStart);
|
|
1021
|
+
const after = currentDocContent.slice(_snippetSelEnd);
|
|
1022
|
+
try {
|
|
1023
|
+
await saveCurrentDocumentContent(before + text + after);
|
|
1024
|
+
} catch (err) {
|
|
1025
|
+
alert(
|
|
1026
|
+
window.t("snippet.inline_save_failed") +
|
|
1027
|
+
(err && err.message ? err.message : String(err)),
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
919
1032
|
const editor = document.getElementById("doc-editor");
|
|
920
1033
|
const before = editor.value.slice(0, _snippetSelStart);
|
|
921
1034
|
const after = editor.value.slice(_snippetSelEnd);
|
|
@@ -925,6 +1038,34 @@ function insertSnippet() {
|
|
|
925
1038
|
editor.focus();
|
|
926
1039
|
}
|
|
927
1040
|
|
|
1041
|
+
async function deleteInlineSnippetBlock() {
|
|
1042
|
+
if (!_snippetInlineEdit) return;
|
|
1043
|
+
const ok =
|
|
1044
|
+
typeof showConfirm === "function"
|
|
1045
|
+
? await showConfirm({
|
|
1046
|
+
title: window.t("snippet.inline_delete_title"),
|
|
1047
|
+
message: window.t("snippet.inline_delete_message"),
|
|
1048
|
+
detail: window.t("snippet.inline_delete_detail"),
|
|
1049
|
+
confirmLabel: window.t("snippet.inline_delete_confirm_btn"),
|
|
1050
|
+
danger: true,
|
|
1051
|
+
detailTone: "warning",
|
|
1052
|
+
})
|
|
1053
|
+
: confirm(window.t("snippet.inline_delete_message"));
|
|
1054
|
+
if (!ok) return;
|
|
1055
|
+
|
|
1056
|
+
const before = currentDocContent.slice(0, _snippetSelStart);
|
|
1057
|
+
const after = currentDocContent.slice(_snippetSelEnd);
|
|
1058
|
+
closeSnippetsModal();
|
|
1059
|
+
try {
|
|
1060
|
+
await saveCurrentDocumentContent(before + after);
|
|
1061
|
+
} catch (err) {
|
|
1062
|
+
alert(
|
|
1063
|
+
window.t("snippet.inline_delete_failed") +
|
|
1064
|
+
(err && err.message ? err.message : String(err)),
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
928
1069
|
async function insertDiagramSnippet() {
|
|
929
1070
|
const isNew = document.getElementById("snip-diag-mode-new").checked;
|
|
930
1071
|
const imgName =
|
|
@@ -979,6 +1120,22 @@ async function insertDiagramSnippet() {
|
|
|
979
1120
|
}
|
|
980
1121
|
|
|
981
1122
|
// ── Snippet parsing (detection lives in /snippet-detect.js) ────────────────
|
|
1123
|
+
function _parseMarkdownTableCells(line) {
|
|
1124
|
+
return line
|
|
1125
|
+
.trim()
|
|
1126
|
+
.split("|")
|
|
1127
|
+
.slice(1, -1)
|
|
1128
|
+
.map((cell) => cell.trim());
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function _isMarkdownTableSeparatorLine(line) {
|
|
1132
|
+
const cells = _parseMarkdownTableCells(line);
|
|
1133
|
+
return (
|
|
1134
|
+
cells.length > 0 &&
|
|
1135
|
+
cells.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\s+/g, "")))
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
982
1139
|
function parseAndFillSnippet(text, type) {
|
|
983
1140
|
const t = text.trim();
|
|
984
1141
|
switch (type) {
|
|
@@ -1065,9 +1222,31 @@ function parseAndFillSnippet(text, type) {
|
|
|
1065
1222
|
}
|
|
1066
1223
|
break;
|
|
1067
1224
|
}
|
|
1225
|
+
case "ordered-list": {
|
|
1226
|
+
document.getElementById("snip-ordered-list-content").value = t
|
|
1227
|
+
.split("\n")
|
|
1228
|
+
.map((line) => line.replace(/^(\s*)\d+\.\s?/, "$1"))
|
|
1229
|
+
.join("\n");
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1232
|
+
case "unordered-list": {
|
|
1233
|
+
document.getElementById("snip-unordered-list-content").value = t
|
|
1234
|
+
.split("\n")
|
|
1235
|
+
.map((line) => line.replace(/^(\s*)[-*+]\s?/, "$1"))
|
|
1236
|
+
.join("\n");
|
|
1237
|
+
break;
|
|
1238
|
+
}
|
|
1068
1239
|
case "code-block": {
|
|
1069
|
-
const m = t.match(
|
|
1070
|
-
document.getElementById("snip-code-lang").value = m ? m[1] : "";
|
|
1240
|
+
const m = t.match(/^```\s*([^\n]*)\n([\s\S]*?)\n```$/);
|
|
1241
|
+
document.getElementById("snip-code-lang").value = m ? m[1].trim() : "";
|
|
1242
|
+
document.getElementById("snip-code-content").value = m ? m[2] : "";
|
|
1243
|
+
break;
|
|
1244
|
+
}
|
|
1245
|
+
case "blockquote": {
|
|
1246
|
+
document.getElementById("snip-blockquote-content").value = t
|
|
1247
|
+
.split("\n")
|
|
1248
|
+
.map((line) => line.replace(/^>\s?/, ""))
|
|
1249
|
+
.join("\n");
|
|
1071
1250
|
break;
|
|
1072
1251
|
}
|
|
1073
1252
|
case "image": {
|
|
@@ -1083,14 +1262,9 @@ function parseAndFillSnippet(text, type) {
|
|
|
1083
1262
|
.split("\n")
|
|
1084
1263
|
.filter((l) => /^\|.*\|$/.test(l.trim()));
|
|
1085
1264
|
const dataLines = allLines.filter(
|
|
1086
|
-
(l) =>
|
|
1087
|
-
);
|
|
1088
|
-
_tableData = dataLines.map((line) =>
|
|
1089
|
-
line
|
|
1090
|
-
.split("|")
|
|
1091
|
-
.slice(1, -1)
|
|
1092
|
-
.map((c) => c.trim()),
|
|
1265
|
+
(l) => !_isMarkdownTableSeparatorLine(l),
|
|
1093
1266
|
);
|
|
1267
|
+
_tableData = dataLines.map(_parseMarkdownTableCells);
|
|
1094
1268
|
const maxCols = Math.max(..._tableData.map((r) => r.length));
|
|
1095
1269
|
_tableData.forEach((row) => {
|
|
1096
1270
|
while (row.length < maxCols) row.push("");
|
|
@@ -5,15 +5,21 @@
|
|
|
5
5
|
// below 100%, the confirmation modal also warns that source-file hashes will be
|
|
6
6
|
// re-baselined, and POST /api/metadata/:id/refresh is called after the PUT.
|
|
7
7
|
|
|
8
|
-
// Frontmatter
|
|
9
|
-
//
|
|
10
|
-
|
|
8
|
+
// Frontmatter may use this project's historical `**key:** value` convention or
|
|
9
|
+
// regular YAML-style `key: value` lines. The status line is the only field this
|
|
10
|
+
// module touches.
|
|
11
|
+
const _STATUS_LINE_RE = /^(\s*(?:\*\*status:\*\*|status:)\s*).+?\s*$/im;
|
|
12
|
+
|
|
13
|
+
function isWorklogDocument(docId) {
|
|
14
|
+
if (/%5BWORKLOG%5D/i.test(docId)) return true;
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
11
17
|
|
|
12
18
|
function getDocStatus(content) {
|
|
13
19
|
if (typeof content !== "string") return null;
|
|
14
20
|
const fence = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
15
21
|
if (!fence) return null;
|
|
16
|
-
const m = fence[1].match(
|
|
22
|
+
const m = fence[1].match(/^\s*(?:\*\*status:\*\*|status:)\s*(.+?)\s*$/im);
|
|
17
23
|
return m ? m[1].trim() : null;
|
|
18
24
|
}
|
|
19
25
|
|
|
@@ -27,7 +33,7 @@ function updateValidateButtonForCurrentDoc() {
|
|
|
27
33
|
const status = getDocStatus(
|
|
28
34
|
typeof currentDocContent !== "undefined" ? currentDocContent : "",
|
|
29
35
|
);
|
|
30
|
-
if (status === "
|
|
36
|
+
if (status && status.toUpperCase() === "TO BE VALIDATED") {
|
|
31
37
|
btn.classList.remove("hidden");
|
|
32
38
|
} else {
|
|
33
39
|
btn.classList.add("hidden");
|
|
@@ -53,12 +59,18 @@ async function validateCurrentDoc() {
|
|
|
53
59
|
const pct = Math.round(accuracy * 100);
|
|
54
60
|
const lowAccuracy = pct < 100;
|
|
55
61
|
const detail = lowAccuracy
|
|
56
|
-
? window
|
|
62
|
+
? window
|
|
63
|
+
.t("doc.validate_detail_low_accuracy")
|
|
64
|
+
.replace("{accuracy}", pct + "%")
|
|
57
65
|
: "";
|
|
58
66
|
|
|
59
67
|
const ok = await window.showConfirm({
|
|
60
68
|
title: window.t("doc.validate_title"),
|
|
61
|
-
message: window.t(
|
|
69
|
+
message: window.t(
|
|
70
|
+
isWorklogDocument(currentDocId)
|
|
71
|
+
? "doc.validate_worklog_message"
|
|
72
|
+
: "doc.validate_message",
|
|
73
|
+
),
|
|
62
74
|
detail,
|
|
63
75
|
detailTone: lowAccuracy ? "warning" : undefined,
|
|
64
76
|
confirmLabel: window.t("doc.validate_confirm"),
|
|
@@ -69,7 +81,10 @@ async function validateCurrentDoc() {
|
|
|
69
81
|
if (btn) btn.disabled = true;
|
|
70
82
|
|
|
71
83
|
try {
|
|
72
|
-
const newContent = _replaceStatus(
|
|
84
|
+
const newContent = _replaceStatus(
|
|
85
|
+
currentDocContent,
|
|
86
|
+
isWorklogDocument(currentDocId) ? "Done" : "Accepted",
|
|
87
|
+
);
|
|
73
88
|
if (newContent === currentDocContent) {
|
|
74
89
|
throw new Error("status line not found in frontmatter");
|
|
75
90
|
}
|
package/dist/src/lib/status.js
CHANGED
|
@@ -16,7 +16,7 @@ exports.assertNotSuperSeeded = assertNotSuperSeeded;
|
|
|
16
16
|
const fs_1 = __importDefault(require("fs"));
|
|
17
17
|
const path_1 = __importDefault(require("path"));
|
|
18
18
|
const config_1 = require("./config");
|
|
19
|
-
const STATUS_LINE_RE =
|
|
19
|
+
const STATUS_LINE_RE = /^\s*(?:\*\*status:\*\*|status:)\s*(.+?)\s*$/im;
|
|
20
20
|
const FRONTMATTER_FENCE_RE = /^---\s*\n([\s\S]*?)\n---/;
|
|
21
21
|
function parseDocStatus(content) {
|
|
22
22
|
if (typeof content !== "string")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../../src/lib/status.ts"],"names":[],"mappings":";AAAA,gFAAgF;AAChF,gFAAgF;AAChF,+EAA+E;AAC/E,8EAA8E;AAC9E,+EAA+E;AAC/E,0EAA0E;;;;;;AAS1E,wCAMC;AAmBD,sCAYC;AAiBD,oDAQC;AArED,4CAAoB;AACpB,gDAAwB;AACxB,qCAAsC;AAEtC,MAAM,cAAc,GAAG,+
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../../src/lib/status.ts"],"names":[],"mappings":";AAAA,gFAAgF;AAChF,gFAAgF;AAChF,+EAA+E;AAC/E,8EAA8E;AAC9E,+EAA+E;AAC/E,0EAA0E;;;;;;AAS1E,wCAMC;AAmBD,sCAYC;AAiBD,oDAQC;AArED,4CAAoB;AACpB,gDAAwB;AACxB,qCAAsC;AAEtC,MAAM,cAAc,GAAG,+CAA+C,CAAC;AACvE,MAAM,oBAAoB,GAAG,0BAA0B,CAAC;AAExD,SAAgB,cAAc,CAAC,OAAe;IAC5C,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC7C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;IAClD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IACzC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAChC,CAAC;AAED,8EAA8E;AAC9E,6EAA6E;AAC7E,6EAA6E;AAC7E,SAAS,kBAAkB,CACzB,QAAgB,EAChB,YAAoB,EACpB,UAAoB;IAEpB,IAAI,cAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAClC,MAAM,SAAS,GAAG,YAAY,GAAG,KAAK,CAAC;QACvC,OAAO,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3D,CAAC;IACD,MAAM,QAAQ,GAAG,cAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,YAAY,GAAG,KAAK,CAAC,CAAC;IAC9D,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,cAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,cAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACzE,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAgB,aAAa,CAC3B,QAAgB,EAChB,YAAoB,EACpB,UAAoB;IAEpB,MAAM,QAAQ,GAAG,kBAAkB,CAAC,QAAQ,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;IACxE,IAAI,CAAC,QAAQ,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACvD,IAAI,CAAC;QACH,OAAO,cAAc,CAAC,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IAC5D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAEY,QAAA,kBAAkB,GAAG,aAAa,CAAC;AAEhD,2EAA2E;AAC3E,8EAA8E;AAC9E,gEAAgE;AAChE,MAAa,wBAAyB,SAAQ,KAAK;IAEjD,YACE,OAAO,GAAG,iDAAiD;QAE3D,KAAK,CAAC,OAAO,CAAC,CAAC;QAJR,SAAI,GAAG,sBAAsB,CAAC;QAKrC,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AARD,4DAQC;AAED,SAAgB,oBAAoB,CAClC,QAAgB,EAChB,YAAoB;IAEpB,MAAM,EAAE,UAAU,GAAG,EAAE,EAAE,GAAG,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC;IACjD,IAAI,aAAa,CAAC,QAAQ,EAAE,YAAY,EAAE,UAAU,CAAC,KAAK,0BAAkB,EAAE,CAAC;QAC7E,MAAM,IAAI,wBAAwB,EAAE,CAAC;IACvC,CAAC;AACH,CAAC"}
|
|
@@ -54,8 +54,11 @@ Do not create durable documentation for trivial fixes, mechanical renames, or fo
|
|
|
54
54
|
## Progress Tracking
|
|
55
55
|
|
|
56
56
|
The `DOCS_FOLDER/WORKLOG/` folder holds the operational state of in-progress tasks and the resume points between AI assistants. It does not replace ADRs.
|
|
57
|
+
The `DOCS_FOLDER/WORKLOG/` folder contains three complementary kinds of files; none of them replaces the durable ADRs that live under `DOCS_FOLDER/ADRS/`.
|
|
57
58
|
|
|
58
|
-
`DOCS_FOLDER/WORKLOG/current-task.md`
|
|
59
|
+
### 1. `DOCS_FOLDER/WORKLOG/current-task.md` — shared resume point
|
|
60
|
+
|
|
61
|
+
Every assistant must read it before resuming a task and update it before handing over when it has started, finished, interrupted, or left a known follow-up.
|
|
59
62
|
|
|
60
63
|
The worklog must stay factual and useful for the next agent:
|
|
61
64
|
|
|
@@ -67,7 +70,50 @@ The worklog must stay factual and useful for the next agent:
|
|
|
67
70
|
- remaining verifications;
|
|
68
71
|
- next recommended action.
|
|
69
72
|
|
|
70
|
-
|
|
73
|
+
### 2. `DOCS_FOLDER/WORKLOG/ROADMAP.md` — ticket backlog
|
|
74
|
+
|
|
75
|
+
Ordered list of tickets needed to ship the product (or a major milestone). Format close to an ADR: frontmatter (`date`, `status`, `description`, `tags`) + Markdown body.
|
|
76
|
+
|
|
77
|
+
The starter ships an example roadmap — the user must replace it with the project's real tickets.
|
|
78
|
+
|
|
79
|
+
When a ticket is done, the agent:
|
|
80
|
+
|
|
81
|
+
1. checks and strikes through the ticket's line in `ROADMAP.md`;
|
|
82
|
+
2. creates a dedicated document under `DOCS_FOLDER/WORKLOG/` recording the realization (see section 3).
|
|
83
|
+
|
|
84
|
+
Checking convention inside the "Recommended order" section:
|
|
85
|
+
|
|
86
|
+
- `[ ]` or `[]` — ticket not started;
|
|
87
|
+
- `[x] ~~Ticket XX - ...~~` — ticket done, struck through for quick scanning.
|
|
88
|
+
|
|
89
|
+
### 3. `DOCS_FOLDER/WORKLOG/YYYY_MM_DD_HH_mm_[WORKLOG]_ticket_XX_<slug>.md` — per-ticket realization record
|
|
90
|
+
|
|
91
|
+
Every time a roadmap ticket is completed, create a dedicated document under `DOCS_FOLDER/WORKLOG/`. The format looks like an ADR (same frontmatter) but the **semantics differ**:
|
|
92
|
+
|
|
93
|
+
- ADRs (`DOCS_FOLDER/ADRS/`) document the project's **durable decisions**: architecture, public contracts, structural conventions. They describe the **WHAT** and the **WHY**.
|
|
94
|
+
- per-ticket WORKLOG documents record the **realization** of a roadmap ticket: what was done, what choices were made during execution, what verifications were run. They describe **WHAT THE AGENT DID** for that specific ticket.
|
|
95
|
+
|
|
96
|
+
If a durable choice is made during a ticket (for example, picking a folder-structure convention), create an architectural ADR under `DOCS_FOLDER/ADRS/` and point the WORKLOG document to that ADR — do not duplicate the reasoning.
|
|
97
|
+
|
|
98
|
+
Recommended frontmatter for a per-ticket WORKLOG document:
|
|
99
|
+
|
|
100
|
+
```markdown
|
|
101
|
+
---
|
|
102
|
+
**date:** YYYY-MM-DD
|
|
103
|
+
**status:** To Be Validated
|
|
104
|
+
**description:** One sentence summarizing what was done for this ticket.
|
|
105
|
+
**tags:** worklog, ticket, <domain slugs>
|
|
106
|
+
---
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Recommended sections in the body:
|
|
110
|
+
|
|
111
|
+
- **Context** — short reminder of the ticket and its goal (one paragraph, link to `ROADMAP.md`);
|
|
112
|
+
- **Realization** — factual list of what was done;
|
|
113
|
+
- **Choices made** — technical decisions taken during the ticket. Point to an ADR if the decision is durable;
|
|
114
|
+
- **Verifications** — tests passed, manual validations;
|
|
115
|
+
- **Follow-ups** — points of attention for the next tickets;
|
|
116
|
+
- **Related documents** — link to `ROADMAP.md` and any ADR(s).
|
|
71
117
|
|
|
72
118
|
## ADRs
|
|
73
119
|
|
|
@@ -9,7 +9,7 @@ Before making changes:
|
|
|
9
9
|
3. Read `DOCS_FOLDER/AI/PROJECT-USEFUL-COMMANDS.md` to know the development, build, test, lint, and setup commands.
|
|
10
10
|
4. Read `memory/MEMORY.md` and load only useful memory files.
|
|
11
11
|
5. Read rules in `DOCS_FOLDER/AI/rules/*.md`.
|
|
12
|
-
6. Read `DOCS_FOLDER/WORKLOG/current-task.md` if present to resume the state of the current task.
|
|
12
|
+
6. Read `DOCS_FOLDER/WORKLOG/current-task.md` if present to resume the state of the current task, and `DOCS_FOLDER/WORKLOG/ROADMAP.md` if present to know which ticket to pick up next.
|
|
13
13
|
7. Inspect ADRs in `DOCS_FOLDER/ADRS/` by reading `description` and `tags` first, then open the full ADR only when relevant.
|
|
14
14
|
8. Check whether the `living-ai-documentation` MCP is available and use it to create, update, and keep documentation reliable when the task touches a decision, rule, command, stack, or technical document.
|
|
15
15
|
|
|
@@ -9,7 +9,7 @@ Before making changes:
|
|
|
9
9
|
3. Read `DOCS_FOLDER/AI/PROJECT-USEFUL-COMMANDS.md` to know the development, build, test, lint, and setup commands.
|
|
10
10
|
4. Read `memory/MEMORY.md` and load only useful memory files.
|
|
11
11
|
5. Read rules in `DOCS_FOLDER/AI/rules/*.md`.
|
|
12
|
-
6. Read `DOCS_FOLDER/WORKLOG/current-task.md` if present to resume the state of the current task.
|
|
12
|
+
6. Read `DOCS_FOLDER/WORKLOG/current-task.md` if present to resume the state of the current task, and `DOCS_FOLDER/WORKLOG/ROADMAP.md` if present to know which ticket to pick up next.
|
|
13
13
|
7. Inspect ADRs in `DOCS_FOLDER/ADRS/` by reading `description` and `tags` first, then open the full ADR only when relevant.
|
|
14
14
|
8. Check whether the `living-ai-documentation` MCP is available and use it to create, update, and keep documentation reliable when the task touches a decision, rule, command, stack, or technical document.
|
|
15
15
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
**date:** 2026-01-01
|
|
3
|
+
**description:** Example ticket roadmap — an ordered backlog used to deliver the product incrementally, with a visible acceptance criterion per ticket. This roadmap is written by the user; it is shipped only as an example.
|
|
4
|
+
**tags:** roadmap, tickets, mvp, planning
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Tickets (example)
|
|
8
|
+
|
|
9
|
+
> This is an example roadmap shipped by the Living Documentation starter. Replace the tickets below with the project's real backlog. AI agents must rely on this roadmap to decide which ticket to pick up next.
|
|
10
|
+
|
|
11
|
+
## Checking convention
|
|
12
|
+
|
|
13
|
+
- `[ ]` or `[]` — ticket not started;
|
|
14
|
+
- `[x] ~~Ticket XX - ...~~` — ticket done, struck through for quick scanning.
|
|
15
|
+
|
|
16
|
+
## Recommended order
|
|
17
|
+
|
|
18
|
+
1. [x] ~~Ticket 01 - Initialize the project~~
|
|
19
|
+
2. [ ] Ticket 02 - First screen of the nominal flow
|
|
20
|
+
|
|
21
|
+
> This is an example ticket for the user (remove this line once the real roadmap is written)
|
|
22
|
+
|
|
23
|
+
## Ticket 01 - Initialize the project
|
|
24
|
+
|
|
25
|
+
Goal: lay down the application foundations, ready to evolve.
|
|
26
|
+
|
|
27
|
+
Tasks:
|
|
28
|
+
|
|
29
|
+
- create the project structure (folder layout, baseline dependencies);
|
|
30
|
+
- configure linting and formatting;
|
|
31
|
+
- add a minimal layout and a routing system;
|
|
32
|
+
- verify that the application starts locally.
|
|
33
|
+
|
|
34
|
+
Acceptance criteria:
|
|
35
|
+
|
|
36
|
+
- the application starts locally without errors;
|
|
37
|
+
- the main routes exist;
|
|
38
|
+
- the code is typed if the stack allows it;
|
|
39
|
+
- no backend is required at this stage.
|
|
40
|
+
|
|
41
|
+
## Ticket 02 - Complete me ...
|
|
@@ -54,8 +54,11 @@ Ne pas créer de documentation durable pour les corrections triviales, renommage
|
|
|
54
54
|
## Suivi de progression
|
|
55
55
|
|
|
56
56
|
Le dossier `DOCS_FOLDER/WORKLOG/` contient l'état opérationnel des tâches en cours et les points de reprise entre assistants IA. Il ne remplace pas les ADR.
|
|
57
|
+
Le dossier `DOCS_FOLDER/WORKLOG/` contient trois types de fichiers complémentaires, aucun d'entre eux ne remplace les ADR durables qui vivent dans `DOCS_FOLDER/ADRS/`.
|
|
57
58
|
|
|
58
|
-
`DOCS_FOLDER/WORKLOG/current-task.md`
|
|
59
|
+
### 1. `DOCS_FOLDER/WORKLOG/current-task.md` — point de reprise partagé
|
|
60
|
+
|
|
61
|
+
Tout assistant doit le lire avant de reprendre une tâche et le mettre à jour avant de rendre la main lorsqu'il a commencé, terminé, interrompu ou laissé une suite connue.
|
|
59
62
|
|
|
60
63
|
Le worklog doit rester factuel et utile pour l'agent suivant :
|
|
61
64
|
|
|
@@ -67,7 +70,50 @@ Le worklog doit rester factuel et utile pour l'agent suivant :
|
|
|
67
70
|
- vérifications restantes ;
|
|
68
71
|
- prochaine action recommandée.
|
|
69
72
|
|
|
70
|
-
|
|
73
|
+
### 2. `DOCS_FOLDER/WORKLOG/ROADMAP.md` — backlog de tickets
|
|
74
|
+
|
|
75
|
+
Liste ordonnée des tickets à réaliser pour livrer le produit (ou une grande étape). Format proche d'un ADR : frontmatter (`date`, `status`, `description`, `tags`) + corps Markdown.
|
|
76
|
+
|
|
77
|
+
Le starter livre une roadmap d'exemple — l'utilisateur doit la remplacer par les tickets réels du projet.
|
|
78
|
+
|
|
79
|
+
Quand un ticket est terminé, l'agent :
|
|
80
|
+
|
|
81
|
+
1. coche et barre la ligne du ticket dans `ROADMAP.md` ;
|
|
82
|
+
2. crée un document dédié dans `DOCS_FOLDER/WORKLOG/` qui consigne la réalisation (voir point 3).
|
|
83
|
+
|
|
84
|
+
Convention de cochage dans la section « Ordre recommandé » :
|
|
85
|
+
|
|
86
|
+
- `[ ]` ou `[]` — ticket non démarré ;
|
|
87
|
+
- `[x] ~~Ticket XX - ...~~` — ticket terminé, barré pour visualisation rapide.
|
|
88
|
+
|
|
89
|
+
### 3. `DOCS_FOLDER/WORKLOG/YYYY_MM_DD_HH_mm_[WORKLOG]_ticket_XX_<slug>.md` — trace de réalisation par ticket
|
|
90
|
+
|
|
91
|
+
À chaque fois qu'un ticket de roadmap est terminé, créer un document dédié dans `DOCS_FOLDER/WORKLOG/`. Le format ressemble à un ADR (même frontmatter) mais la **sémantique est différente** :
|
|
92
|
+
|
|
93
|
+
- les ADR (`DOCS_FOLDER/ADRS/`) documentent les **décisions durables** du projet : architecture, contrats publics, conventions structurantes. Ils décrivent le **QUOI** et le **POURQUOI**.
|
|
94
|
+
- les documents WORKLOG par ticket documentent la **réalisation** d'un ticket de roadmap : ce qui a été fait, les choix retenus pendant l'exécution, les vérifications passées. Ils décrivent **CE QUE L'AGENT A FAIT** pour ce ticket précis.
|
|
95
|
+
|
|
96
|
+
Si un choix durable est pris pendant la réalisation d'un ticket (par exemple, le choix d'une convention de structure de dossiers), créer un ADR architectural dans `DOCS_FOLDER/ADRS/` et faire pointer le document WORKLOG vers cet ADR — ne pas dupliquer le raisonnement.
|
|
97
|
+
|
|
98
|
+
Frontmatter recommandé du document WORKLOG par ticket :
|
|
99
|
+
|
|
100
|
+
```markdown
|
|
101
|
+
---
|
|
102
|
+
**date:** YYYY-MM-DD
|
|
103
|
+
**status:** To Be Validated
|
|
104
|
+
**description:** Une phrase qui résume ce qui a été fait pour ce ticket.
|
|
105
|
+
**tags:** worklog, ticket, <slugs métiers>
|
|
106
|
+
---
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Sections recommandées dans le corps :
|
|
110
|
+
|
|
111
|
+
- **Contexte** — rappel du ticket et de son objectif (un paragraphe court, lien vers `ROADMAP.md`) ;
|
|
112
|
+
- **Réalisation** — liste factuelle de ce qui a été fait ;
|
|
113
|
+
- **Choix retenus** — décisions techniques prises pendant le ticket. Pointer vers un ADR si la décision est durable ;
|
|
114
|
+
- **Vérifications** — tests passés, validations manuelles ;
|
|
115
|
+
- **Suites éventuelles** — points de vigilance pour les tickets suivants ;
|
|
116
|
+
- **Documents liés** — lien vers `ROADMAP.md` et ADR(s) éventuel(s).
|
|
71
117
|
|
|
72
118
|
## ADR
|
|
73
119
|
|
|
@@ -9,7 +9,7 @@ Avant toute modification :
|
|
|
9
9
|
3. Lire `DOCS_FOLDER/AI/PROJECT-USEFUL-COMMANDS.md` pour connaître les commandes de développement, build, test, lint et setup.
|
|
10
10
|
4. Lire `memory/MEMORY.md` et charger seulement les fichiers mémoire utiles à la tâche.
|
|
11
11
|
5. Lire toutes les règles dans `DOCS_FOLDER/AI/rules/*.md`.
|
|
12
|
-
6. Lire `DOCS_FOLDER/WORKLOG/current-task.md` si présent pour reprendre l'état de la tâche courante.
|
|
12
|
+
6. Lire `DOCS_FOLDER/WORKLOG/current-task.md` si présent pour reprendre l'état de la tâche courante, et `DOCS_FOLDER/WORKLOG/ROADMAP.md` si présent pour savoir quel ticket attaquer ensuite.
|
|
13
13
|
7. Inspecter les ADR dans `DOCS_FOLDER/ADRS/` en lisant d'abord `description` et `tags`, puis ouvrir l'ADR complet seulement s'il est pertinent.
|
|
14
14
|
8. Vérifier si le MCP `living-ai-documentation` est disponible et l'utiliser pour créer, mettre à jour et fiabiliser la documentation lorsque la tâche touche une décision, une règle, une commande, la stack ou un document technique.
|
|
15
15
|
|
|
@@ -9,7 +9,7 @@ Avant toute modification :
|
|
|
9
9
|
3. Lire `DOCS_FOLDER/AI/PROJECT-USEFUL-COMMANDS.md` pour connaître les commandes de développement, build, test, lint et setup.
|
|
10
10
|
4. Lire `memory/MEMORY.md` et charger seulement les fichiers mémoire utiles à la tâche.
|
|
11
11
|
5. Lire toutes les règles dans `DOCS_FOLDER/AI/rules/*.md`.
|
|
12
|
-
6. Lire `DOCS_FOLDER/WORKLOG/current-task.md` si présent pour reprendre l'état de la tâche courante.
|
|
12
|
+
6. Lire `DOCS_FOLDER/WORKLOG/current-task.md` si présent pour reprendre l'état de la tâche courante, et `DOCS_FOLDER/WORKLOG/ROADMAP.md` si présent pour savoir quel ticket attaquer ensuite.
|
|
13
13
|
7. Inspecter les ADR dans `DOCS_FOLDER/ADRS/` en lisant d'abord `description` et `tags`, puis ouvrir l'ADR complet seulement s'il est pertinent.
|
|
14
14
|
8. Vérifier si le MCP `living-ai-documentation` est disponible et l'utiliser pour créer, mettre à jour et fiabiliser la documentation lorsque la tâche touche une décision, une règle, une commande, la stack ou un document technique.
|
|
15
15
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
**date:** 2026-01-01
|
|
3
|
+
**description:** Exemple de roadmap de tickets — backlog ordonné pour livrer progressivement le produit, avec un critère d'acceptation visible par ticket. Cette roadmap est écrite par l'utilisateur, elle est fournie à titre d'exemple.
|
|
4
|
+
**tags:** roadmap, tickets, mvp, planning
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Tickets (exemple)
|
|
8
|
+
|
|
9
|
+
> Ceci est une roadmap d'exemple fournie par le starter Living Documentation. Remplacer les tickets ci-dessous par le backlog réel du projet. Les agents IA doivent se baser sur cette roadmap pour savoir quel ticket attaquer ensuite.
|
|
10
|
+
|
|
11
|
+
## Convention de cochage
|
|
12
|
+
|
|
13
|
+
- `[ ]` ou `[]` — ticket non démarré ;
|
|
14
|
+
- `[x] ~~Ticket XX - ...~~` — ticket terminé, barré pour la lisibilité.
|
|
15
|
+
|
|
16
|
+
## Ordre recommandé
|
|
17
|
+
|
|
18
|
+
1. [x] ~~Ticket 01 - Initialiser le projet~~
|
|
19
|
+
2. [ ] Ticket 02 - Premier écran du parcours nominal
|
|
20
|
+
|
|
21
|
+
> Ceci est un exemple de ticket pour l'utilisateur (supprimer cette ligne une fois la vraie roadmap écrite)
|
|
22
|
+
|
|
23
|
+
## Ticket 01 - Initialiser le projet
|
|
24
|
+
|
|
25
|
+
Objectif : poser la base applicative, prête à évoluer.
|
|
26
|
+
|
|
27
|
+
Tâches :
|
|
28
|
+
|
|
29
|
+
- créer la structure du projet (arborescence, dépendances de base) ;
|
|
30
|
+
- configurer le linting et le formatage ;
|
|
31
|
+
- ajouter un layout minimal et un système de routes ;
|
|
32
|
+
- valider que l'application démarre localement.
|
|
33
|
+
|
|
34
|
+
Critères d'acceptation :
|
|
35
|
+
|
|
36
|
+
- l'application démarre localement sans erreur ;
|
|
37
|
+
- les routes principales existent ;
|
|
38
|
+
- le code est typé si la stack le permet ;
|
|
39
|
+
- aucun backend n'est requis à ce stade.
|
|
40
|
+
|
|
41
|
+
## Ticket 02 - Complétez ...
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "living-ai-documentation",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Local Markdown documentation hub with a built-in MCP server — coding agents create ADRs, draw diagrams and detect drift while you code.",
|
|
5
5
|
"main": "dist/src/server.js",
|
|
6
6
|
"bin": {
|