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.
Files changed (173) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +329 -0
  3. package/dist/bin/cli.d.ts +3 -0
  4. package/dist/bin/cli.d.ts.map +1 -0
  5. package/dist/bin/cli.js +62 -0
  6. package/dist/bin/cli.js.map +1 -0
  7. package/dist/src/frontend/admin.html +1073 -0
  8. package/dist/src/frontend/annotations.js +546 -0
  9. package/dist/src/frontend/boot.js +90 -0
  10. package/dist/src/frontend/config.js +19 -0
  11. package/dist/src/frontend/dark-mode.js +20 -0
  12. package/dist/src/frontend/diagram/alignment.js +161 -0
  13. package/dist/src/frontend/diagram/clipboard.js +172 -0
  14. package/dist/src/frontend/diagram/constants.js +109 -0
  15. package/dist/src/frontend/diagram/debug.js +43 -0
  16. package/dist/src/frontend/diagram/edge-panel.js +260 -0
  17. package/dist/src/frontend/diagram/edge-rendering.js +12 -0
  18. package/dist/src/frontend/diagram/grid.js +78 -0
  19. package/dist/src/frontend/diagram/groups.js +102 -0
  20. package/dist/src/frontend/diagram/history.js +153 -0
  21. package/dist/src/frontend/diagram/image-name-modal.js +48 -0
  22. package/dist/src/frontend/diagram/image-upload.js +36 -0
  23. package/dist/src/frontend/diagram/label-editor.js +115 -0
  24. package/dist/src/frontend/diagram/link-panel.js +144 -0
  25. package/dist/src/frontend/diagram/main.js +299 -0
  26. package/dist/src/frontend/diagram/network.js +1473 -0
  27. package/dist/src/frontend/diagram/node-panel.js +267 -0
  28. package/dist/src/frontend/diagram/node-rendering.js +773 -0
  29. package/dist/src/frontend/diagram/persistence.js +161 -0
  30. package/dist/src/frontend/diagram/ports.js +386 -0
  31. package/dist/src/frontend/diagram/selection-overlay.js +336 -0
  32. package/dist/src/frontend/diagram/state.js +39 -0
  33. package/dist/src/frontend/diagram/t.js +3 -0
  34. package/dist/src/frontend/diagram/toast.js +21 -0
  35. package/dist/src/frontend/diagram/unlock-hold.js +182 -0
  36. package/dist/src/frontend/diagram/zoom.js +20 -0
  37. package/dist/src/frontend/diagram-link-modal.js +137 -0
  38. package/dist/src/frontend/diagram.html +1279 -0
  39. package/dist/src/frontend/documents.js +373 -0
  40. package/dist/src/frontend/export.js +338 -0
  41. package/dist/src/frontend/i18n/en.json +406 -0
  42. package/dist/src/frontend/i18n/fr.json +406 -0
  43. package/dist/src/frontend/i18n.js +32 -0
  44. package/dist/src/frontend/image-paste.js +101 -0
  45. package/dist/src/frontend/index.html +2314 -0
  46. package/dist/src/frontend/misc.js +25 -0
  47. package/dist/src/frontend/new-doc-modal.js +260 -0
  48. package/dist/src/frontend/new-folder-modal.js +174 -0
  49. package/dist/src/frontend/search.js +157 -0
  50. package/dist/src/frontend/sidebar-helpers.js +58 -0
  51. package/dist/src/frontend/sidebar.js +182 -0
  52. package/dist/src/frontend/snippet-detect.js +25 -0
  53. package/dist/src/frontend/snippet-table.js +85 -0
  54. package/dist/src/frontend/snippet-tree.js +94 -0
  55. package/dist/src/frontend/snippets.js +534 -0
  56. package/dist/src/frontend/state.js +28 -0
  57. package/dist/src/frontend/utils.js +21 -0
  58. package/dist/src/frontend/vendor/wordcloud2.js +1187 -0
  59. package/dist/src/frontend/wordcloud.js +693 -0
  60. package/dist/src/lib/config.d.ts +17 -0
  61. package/dist/src/lib/config.d.ts.map +1 -0
  62. package/dist/src/lib/config.js +79 -0
  63. package/dist/src/lib/config.js.map +1 -0
  64. package/dist/src/lib/parser.d.ts +11 -0
  65. package/dist/src/lib/parser.d.ts.map +1 -0
  66. package/dist/src/lib/parser.js +111 -0
  67. package/dist/src/lib/parser.js.map +1 -0
  68. package/dist/src/mcp/server.d.ts +3 -0
  69. package/dist/src/mcp/server.d.ts.map +1 -0
  70. package/dist/src/mcp/server.js +986 -0
  71. package/dist/src/mcp/server.js.map +1 -0
  72. package/dist/src/mcp/tools/diagrams.d.ts +44 -0
  73. package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
  74. package/dist/src/mcp/tools/diagrams.js +245 -0
  75. package/dist/src/mcp/tools/diagrams.js.map +1 -0
  76. package/dist/src/mcp/tools/documents.d.ts +26 -0
  77. package/dist/src/mcp/tools/documents.d.ts.map +1 -0
  78. package/dist/src/mcp/tools/documents.js +127 -0
  79. package/dist/src/mcp/tools/documents.js.map +1 -0
  80. package/dist/src/mcp/tools/source.d.ts +29 -0
  81. package/dist/src/mcp/tools/source.d.ts.map +1 -0
  82. package/dist/src/mcp/tools/source.js +200 -0
  83. package/dist/src/mcp/tools/source.js.map +1 -0
  84. package/dist/src/routes/annotations.d.ts +3 -0
  85. package/dist/src/routes/annotations.d.ts.map +1 -0
  86. package/dist/src/routes/annotations.js +83 -0
  87. package/dist/src/routes/annotations.js.map +1 -0
  88. package/dist/src/routes/browse.d.ts +3 -0
  89. package/dist/src/routes/browse.d.ts.map +1 -0
  90. package/dist/src/routes/browse.js +75 -0
  91. package/dist/src/routes/browse.js.map +1 -0
  92. package/dist/src/routes/config.d.ts +3 -0
  93. package/dist/src/routes/config.d.ts.map +1 -0
  94. package/dist/src/routes/config.js +97 -0
  95. package/dist/src/routes/config.js.map +1 -0
  96. package/dist/src/routes/diagrams.d.ts +3 -0
  97. package/dist/src/routes/diagrams.d.ts.map +1 -0
  98. package/dist/src/routes/diagrams.js +69 -0
  99. package/dist/src/routes/diagrams.js.map +1 -0
  100. package/dist/src/routes/documents.d.ts +8 -0
  101. package/dist/src/routes/documents.d.ts.map +1 -0
  102. package/dist/src/routes/documents.js +332 -0
  103. package/dist/src/routes/documents.js.map +1 -0
  104. package/dist/src/routes/export.d.ts +3 -0
  105. package/dist/src/routes/export.d.ts.map +1 -0
  106. package/dist/src/routes/export.js +277 -0
  107. package/dist/src/routes/export.js.map +1 -0
  108. package/dist/src/routes/images.d.ts +3 -0
  109. package/dist/src/routes/images.d.ts.map +1 -0
  110. package/dist/src/routes/images.js +49 -0
  111. package/dist/src/routes/images.js.map +1 -0
  112. package/dist/src/routes/wordcloud.d.ts +3 -0
  113. package/dist/src/routes/wordcloud.d.ts.map +1 -0
  114. package/dist/src/routes/wordcloud.js +95 -0
  115. package/dist/src/routes/wordcloud.js.map +1 -0
  116. package/dist/src/server.d.ts +7 -0
  117. package/dist/src/server.d.ts.map +1 -0
  118. package/dist/src/server.js +76 -0
  119. package/dist/src/server.js.map +1 -0
  120. package/dist/starting-doc/.annotations.json +3 -0
  121. package/dist/starting-doc/.diagrams.json +1884 -0
  122. package/dist/starting-doc/.living-doc.json +39 -0
  123. package/dist/starting-doc/1_tutorial/2026_04_11_13_25_[General]_crer_vos_dossiers.md +16 -0
  124. package/dist/starting-doc/1_tutorial/2026_04_11_18_58_[General]_creer_un_document_dans_un_dossier.md +9 -0
  125. package/dist/starting-doc/1_tutorial/2026_04_12_09_00_[General]_editer_et_sauvegarder.md +39 -0
  126. package/dist/starting-doc/1_tutorial/2026_04_12_10_00_[General]_utiliser_les_snippets.md +71 -0
  127. package/dist/starting-doc/2026_04_08_20_52_[General]_welcome.md +17 -0
  128. package/dist/starting-doc/2026_04_11_12_55_[General]_premiers_pas.md +271 -0
  129. package/dist/starting-doc/2_guide/2026_04_08_00_04_[DOCUMENT]_utilisation_des_images_plein_ecran_lien_clickable.md +40 -0
  130. package/dist/starting-doc/2_guide/2026_04_08_23_38_[Configuration]_demarrage_de_living_documentation.md +32 -0
  131. package/dist/starting-doc/2_guide/2026_04_09_09_00_[NAVIGATION]_recherche_plein_texte.md +65 -0
  132. package/dist/starting-doc/2_guide/2026_04_09_10_00_[EXPORT]_exporter_en_pdf.md +43 -0
  133. package/dist/starting-doc/2_guide/2026_04_09_11_00_[Configuration]_configurer_le_panneau_admin.md +55 -0
  134. package/dist/starting-doc/2_guide/2026_04_09_12_00_[Configuration]_extra_files.md +68 -0
  135. package/dist/starting-doc/2_guide/2026_04_09_13_00_[WORDCLOUD]_word_cloud.md +54 -0
  136. package/dist/starting-doc/2_guide/2026_04_09_14_00_[DIAGRAM]_creer_et_lier_un_diagramme.md +77 -0
  137. package/dist/starting-doc/3_concept/2026_04_08_20_58_[DOCUMENTING]_ADRS.md +20 -0
  138. package/dist/starting-doc/3_concept/2026_04_08_22_15_[DOCUMENTING]_living_documentation.md +17 -0
  139. package/dist/starting-doc/3_concept/2026_04_08_22_46_[METHODOLOGY]_diataxis_architecture_du_contenu.md +16 -0
  140. package/dist/starting-doc/4_reference/2026_04_08_23_14_[FUNDAMENTALS]_the_living_documentation_tool.md +41 -0
  141. package/dist/starting-doc/4_reference/2026_04_09_01_00_[REFERENCE]_raccourcis_clavier.md +61 -0
  142. package/dist/starting-doc/4_reference/2026_04_09_02_00_[REFERENCE]_tokens_pattern_nommage.md +75 -0
  143. package/dist/starting-doc/4_reference/2026_04_09_03_00_[REFERENCE]_types_de_snippets.md +68 -0
  144. package/dist/starting-doc/4_reference/2026_04_11_17_31_[FUNDAMENTALS]_architecturer_une_documentation.md +12 -0
  145. package/dist/starting-doc/4_reference/2026_04_12_14_07_[FUNDAMENTALS]_dossiers_et_catgories.md +89 -0
  146. package/dist/starting-doc/images/admin_screenshot.png +0 -0
  147. package/dist/starting-doc/images/ajout-document.png +0 -0
  148. package/dist/starting-doc/images/ajouter-document-categorie.png +0 -0
  149. package/dist/starting-doc/images/ajouter_un_document_dans_un_dossier.png +0 -0
  150. package/dist/starting-doc/images/architecturer_une_documentation_reference.png +0 -0
  151. package/dist/starting-doc/images/cr_er_un_document.png +0 -0
  152. package/dist/starting-doc/images/creation-nouveau-dossier.png +0 -0
  153. package/dist/starting-doc/images/creer-document-context-engineering.png +0 -0
  154. package/dist/starting-doc/images/creer-dossier-only-tutoriel.png +0 -0
  155. package/dist/starting-doc/images/creer-dossier-tutoriel.png +0 -0
  156. package/dist/starting-doc/images/creer-dossiers-done.png +0 -0
  157. package/dist/starting-doc/images/creer-un-document.png +0 -0
  158. package/dist/starting-doc/images/creer-vos-dossiers-tutoriel.png +0 -0
  159. package/dist/starting-doc/images/creer-vos-dossiers.png +0 -0
  160. package/dist/starting-doc/images/decouverte_adrs.png +0 -0
  161. package/dist/starting-doc/images/diataxis.png +0 -0
  162. package/dist/starting-doc/images/diataxis_callout.png +0 -0
  163. package/dist/starting-doc/images/document-cree.png +0 -0
  164. package/dist/starting-doc/images/liens_snippets.png +0 -0
  165. package/dist/starting-doc/images/living_documentation.png +0 -0
  166. package/dist/starting-doc/images/npm_logo.png +0 -0
  167. package/dist/starting-doc/images/popup-creer-document.png +0 -0
  168. package/dist/starting-doc/images/popup-creer-dossier.png +0 -0
  169. package/dist/starting-doc/images/popup-dossier-cree.png +0 -0
  170. package/dist/starting-doc/images/quatre-dossiers-crees.png +0 -0
  171. package/dist/starting-doc/images/screenshot-living-doc.png +0 -0
  172. package/dist/starting-doc/images/the_living_documentation_tool.png +0 -0
  173. package/package.json +49 -0
@@ -0,0 +1,182 @@
1
+ // ── Sidebar rendering ────────────────────────────────────────────────────────
2
+ // Depends on globals from state.js, utils.js, sidebar-helpers.js, and
3
+ // `currentDocId` (updated by documents.js when a doc opens).
4
+
5
+ function renderSidebar(docs) {
6
+ const tree = document.getElementById("category-tree");
7
+ const countEl = document.getElementById("doc-count");
8
+
9
+ if (!docs.length && !allFolderPaths.length) {
10
+ tree.innerHTML =
11
+ `<p class="px-4 py-8 text-sm text-gray-400 text-center">${window.t('sidebar.no_docs')}</p>`;
12
+ countEl.textContent = "";
13
+ return;
14
+ }
15
+
16
+ countEl.textContent = `${docs.length} document${docs.length !== 1 ? "s" : ""}`;
17
+
18
+ // Auto-expand only root General initially
19
+ if (expandedCategories.size === 0) expandedCategories.add("General");
20
+
21
+ tree.innerHTML = renderTreeNode(buildFolderTree(docs), []);
22
+ }
23
+
24
+ // Build a recursive tree: each node holds { categories: {cat: docs[]}, children: {label: node} }
25
+ function buildFolderTree(docs) {
26
+ const root = { categories: {}, children: {} };
27
+ for (const doc of docs) {
28
+ let node = root;
29
+ for (const seg of doc.folder || []) {
30
+ if (!node.children[seg])
31
+ node.children[seg] = { categories: {}, children: {} };
32
+ node = node.children[seg];
33
+ }
34
+ if (!node.categories[doc.category])
35
+ node.categories[doc.category] = [];
36
+ node.categories[doc.category].push(doc);
37
+ }
38
+ // Ensure empty folders appear in the tree (only when no active filter)
39
+ if (searchQuery) return root;
40
+ for (const folderPath of allFolderPaths) {
41
+ const segments = folderPath.split("/").filter(Boolean);
42
+ let node = root;
43
+ for (const seg of segments) {
44
+ if (!node.children[seg])
45
+ node.children[seg] = { categories: {}, children: {} };
46
+ node = node.children[seg];
47
+ }
48
+ }
49
+ return root;
50
+ }
51
+
52
+ // Render a tree node at a given folder path (array of segment strings)
53
+ function renderTreeNode(node, folderPath) {
54
+ let html = "";
55
+
56
+ // Helper to render one category group
57
+ const renderCat = (cat) => {
58
+ const catPathKey = [...folderPath, cat].join("|");
59
+ const catNodeId =
60
+ "cat-" +
61
+ [...folderPath, cat].map((s) => s.replace(/\W/g, "-")).join("-");
62
+ const isExpanded = expandedCategories.has(catPathKey);
63
+ const catAnnotatedDocs = node.categories[cat].reduce(
64
+ (s, d) => s + (annotationCounts[d.id] > 0 ? 1 : 0),
65
+ 0,
66
+ );
67
+ return `
68
+ <div class="mb-0.5">
69
+ <button onclick="toggleCategory('${esc(catPathKey)}')"
70
+ class="w-full flex items-center justify-between px-3 py-1.5 text-xs font-semibold
71
+ text-gray-500 dark:text-gray-400 uppercase tracking-wider
72
+ hover:bg-gray-50 dark:hover:bg-gray-800/60 rounded-md transition-colors">
73
+ <span class="flex items-center gap-2">
74
+ <span>${esc(cat)}</span>
75
+ ${annotatedDocsBadge(catAnnotatedDocs)}
76
+ </span>
77
+ <span class="flex items-center gap-1.5">
78
+ <span class="font-normal normal-case text-gray-400">${node.categories[cat].length}</span>
79
+ <span class="transition-transform duration-200 ${isExpanded ? "rotate-90" : ""}" id="arrow-${catNodeId}">&#9656;</span>
80
+ </span>
81
+ </button>
82
+ <div id="${catNodeId}" class="category-docs pl-2 ${isExpanded ? "expanded" : "collapsed"}">
83
+ ${node.categories[cat].map((doc) => renderDocItem(doc)).join("")}
84
+ </div>
85
+ </div>`;
86
+ };
87
+
88
+ // General always first
89
+ if (node.categories["General"]) html += renderCat("General");
90
+
91
+ // Subfolders (sorted alphabetically — numeric prefix like "1_" sorts naturally)
92
+ const childKeys = Object.keys(node.children).sort((a, b) =>
93
+ a.localeCompare(b),
94
+ );
95
+ for (const key of childKeys) {
96
+ const childPath = [...folderPath, key];
97
+ const pathKey = childPath.join("|");
98
+ const nodeId =
99
+ "folder-" + childPath.map((s) => s.replace(/\W/g, "-")).join("-");
100
+ const isExpanded = expandedFolders.has(pathKey);
101
+ const docCount = countTreeDocs(node.children[key]);
102
+ const folderAnnotatedDocs = countTreeAnnotatedDocs(node.children[key]);
103
+ html += `
104
+ <div class="mb-1">
105
+ <button onclick="toggleFolder('${esc(pathKey)}')"
106
+ class="w-full flex items-center justify-between px-3 py-1.5 text-xs font-semibold
107
+ text-violet-600 dark:text-violet-400 uppercase tracking-wider
108
+ hover:bg-gray-50 dark:hover:bg-gray-800/60 rounded-md transition-colors">
109
+ <span class="flex items-center gap-2 min-w-0">
110
+ <span title="${esc(key)}" class="truncate">&#128193; ${esc(folderLabel(key))}</span>
111
+ ${annotatedDocsBadge(folderAnnotatedDocs)}
112
+ </span>
113
+ <span class="flex items-center gap-1.5">
114
+ <span class="font-normal normal-case text-gray-400">${docCount}</span>
115
+ <span class="transition-transform duration-200 ${isExpanded ? "rotate-90" : ""}" id="arrow-${nodeId}">&#9656;</span>
116
+ </span>
117
+ </button>
118
+ <div id="${nodeId}" class="category-docs pl-3 ${isExpanded ? "expanded" : "collapsed"}">
119
+ ${renderTreeNode(node.children[key], childPath)}
120
+ </div>
121
+ </div>`;
122
+ }
123
+
124
+ // Non-General category groups (sorted alphabetically)
125
+ const otherCats = Object.keys(node.categories)
126
+ .filter((c) => c !== "General")
127
+ .sort((a, b) => a.localeCompare(b));
128
+ for (const cat of otherCats) html += renderCat(cat);
129
+
130
+ return html;
131
+ }
132
+
133
+ function renderDocItem(doc) {
134
+ const isActive = doc.id === currentDocId;
135
+ const annCount = annotationCounts[doc.id] || 0;
136
+ return `
137
+ <button onclick="openDocument('${esc(doc.id)}')"
138
+ id="item-${esc(doc.id)}"
139
+ class="doc-item w-full text-left px-3 py-1.5 rounded-md text-sm transition-colors
140
+ text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800/60
141
+ ${isActive ? "active" : ""}">
142
+ <div class="leading-snug flex items-center justify-between gap-2">
143
+ <span class="truncate">${esc(doc.title)}</span>
144
+ ${annotationBadge(annCount)}
145
+ </div>
146
+ ${doc.formattedDate ? `<div class="text-xs text-gray-400 dark:text-gray-500 mt-0.5">${esc(doc.formattedDate)}</div>` : ""}
147
+ </button>`;
148
+ }
149
+
150
+ function toggleCategory(key) {
151
+ const parts = key.split("|");
152
+ const catNodeId =
153
+ "cat-" + parts.map((p) => p.replace(/\W/g, "-")).join("-");
154
+ const el = document.getElementById(catNodeId);
155
+ const arrow = document.getElementById("arrow-" + catNodeId);
156
+ if (!el) return;
157
+ const expanding = el.classList.contains("collapsed");
158
+ el.classList.toggle("collapsed", !expanding);
159
+ el.classList.toggle("expanded", expanding);
160
+ if (arrow) arrow.style.transform = expanding ? "rotate(90deg)" : "";
161
+ if (expanding) expandedCategories.add(key);
162
+ else expandedCategories.delete(key);
163
+ }
164
+
165
+ function toggleFolder(pathKey) {
166
+ const parts = pathKey.split("|");
167
+ const nodeId =
168
+ "folder-" + parts.map((p) => p.replace(/\W/g, "-")).join("-");
169
+ const el = document.getElementById(nodeId);
170
+ const arrow = document.getElementById("arrow-" + nodeId);
171
+ if (!el) return;
172
+ const expanding = el.classList.contains("collapsed");
173
+ el.classList.toggle("collapsed", !expanding);
174
+ el.classList.toggle("expanded", expanding);
175
+ if (arrow) arrow.style.transform = expanding ? "rotate(90deg)" : "";
176
+ if (expanding) expandedFolders.add(pathKey);
177
+ else expandedFolders.delete(pathKey);
178
+ }
179
+
180
+ function toggleSidebar() {
181
+ document.getElementById("sidebar").classList.toggle("hidden");
182
+ }
@@ -0,0 +1,25 @@
1
+ // ── Pure snippet format detection ────────────────────────────────────────────
2
+ // Given a raw piece of Markdown/HTML text, returns the canonical snippet type
3
+ // used by the snippets modal, or null if no format matches.
4
+ // Loaded as a classic script; exposes `detectSnippetType` as a global.
5
+
6
+ function detectSnippetType(text) {
7
+ const t = text.trim();
8
+ // anchor-doc-link before doc-link and anchor-link
9
+ if (/^\[.*?\]\(\?doc=.+#.+\)$/.test(t)) return "anchor-doc-link";
10
+ if (/^\[.*?\]\(\?doc=.+\)$/.test(t)) return "doc-link";
11
+ if (/^\[.*?\]\(#.+\)$/.test(t)) return "anchor-link";
12
+ if (/^!\[.*?\]\(.*?\)$/.test(t)) return "image";
13
+ if (/^\[.*?\]\(.*?\)$/.test(t)) return "link";
14
+ if (/^<details[\s>]/i.test(t)) return "collapsible";
15
+ if (/^<span\s[^>]*color:[^>]*>[\s\S]*<\/span>$/.test(t)) return "colored-text";
16
+ if (/^<div\s[^>]*border-left[^>]*>/.test(t)) return "colored-section";
17
+ if (/^```text\n/.test(t) && /[├└│]/.test(t)) return "tree";
18
+ if (/^```/.test(t)) return "code-block";
19
+ if (/^(\| *.*? *\|)+\n(\| *-+.*\|)+/.test(t)) return "table";
20
+ if (/^> /.test(t)) return "blockquote";
21
+ if (/^(---|\n---\n)$/.test(t)) return "separator";
22
+ if (/^1\. /.test(t)) return "ordered-list";
23
+ if (/^- /.test(t)) return "unordered-list";
24
+ return null;
25
+ }
@@ -0,0 +1,85 @@
1
+ // ── Snippets — Table builder ─────────────────────────────────────────────────
2
+ // Loaded as a classic script; exposes the table-builder helpers and `_tableData`
3
+ // state as globals. Depends on `esc` (utils.js) and `snippetUpdatePreview`
4
+ // (defined inline in index.html).
5
+
6
+ let _tableData = [];
7
+
8
+ function tableInit() {
9
+ _tableData = [
10
+ ["En-tête 1", "En-tête 2", "En-tête 3"],
11
+ ["", "", ""],
12
+ ["", "", ""],
13
+ ];
14
+ tableRenderGrid();
15
+ }
16
+
17
+ function tableRenderGrid() {
18
+ const rows = _tableData.length;
19
+ const cols = _tableData[0]?.length ?? 0;
20
+ document.getElementById("snip-table-rows").textContent = rows;
21
+ document.getElementById("snip-table-cols").textContent = cols;
22
+
23
+ const inputBase =
24
+ "px-2 py-1 text-xs rounded border focus:outline-none focus:ring-1 focus:ring-blue-500 w-full min-w-[80px]";
25
+ const hdrCls = `${inputBase} font-semibold border-blue-300 dark:border-blue-600 bg-blue-50 dark:bg-blue-900/20 text-gray-900 dark:text-gray-100`;
26
+ const cellCls = `${inputBase} border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100`;
27
+
28
+ let html = '<table class="border-collapse w-full"><tbody>';
29
+ for (let r = 0; r < rows; r++) {
30
+ html += "<tr>";
31
+ for (let c = 0; c < cols; c++) {
32
+ const cls = r === 0 ? hdrCls : cellCls;
33
+ html += `<td class="p-0.5"><input type="text" value="${esc(_tableData[r][c])}" oninput="tableUpdate(${r},${c},this.value)" class="${cls}" /></td>`;
34
+ }
35
+ html += "</tr>";
36
+ }
37
+ html += "</tbody></table>";
38
+ document.getElementById("snip-table-grid").innerHTML = html;
39
+ snippetUpdatePreview();
40
+ }
41
+
42
+ function tableUpdate(r, c, val) {
43
+ _tableData[r][c] = val;
44
+ snippetUpdatePreview();
45
+ }
46
+
47
+ function tableChangeRows(delta) {
48
+ const cols = _tableData[0]?.length ?? 3;
49
+ if (delta > 0) {
50
+ _tableData.push(Array(cols).fill(""));
51
+ } else if (_tableData.length > 2) {
52
+ _tableData.pop();
53
+ }
54
+ tableRenderGrid();
55
+ }
56
+
57
+ function tableChangeCols(delta) {
58
+ if (delta > 0) {
59
+ _tableData.forEach((row) => row.push(""));
60
+ } else if ((_tableData[0]?.length ?? 0) > 1) {
61
+ _tableData.forEach((row) => row.pop());
62
+ }
63
+ tableRenderGrid();
64
+ }
65
+
66
+ function buildTableMarkdown() {
67
+ if (!_tableData.length || !_tableData[0]?.length) return "";
68
+ const cols = _tableData[0].length;
69
+ const widths = Array(cols).fill(3);
70
+ _tableData.forEach((row) => {
71
+ row.forEach((cell, c) => {
72
+ widths[c] = Math.max(widths[c], cell.length);
73
+ });
74
+ });
75
+ const fmtRow = (row) =>
76
+ "| " +
77
+ row.map((cell, c) => cell.padEnd(widths[c])).join(" | ") +
78
+ " |";
79
+ const sep = "| " + widths.map((w) => "-".repeat(w)).join(" | ") + " |";
80
+ return [
81
+ fmtRow(_tableData[0]),
82
+ sep,
83
+ ..._tableData.slice(1).map(fmtRow),
84
+ ].join("\n");
85
+ }
@@ -0,0 +1,94 @@
1
+ // ── Snippets — Tree builder ──────────────────────────────────────────────────
2
+ // Loaded as a classic script; exposes the tree-builder helpers and `_treeItems`
3
+ // state as globals. Depends on `esc` (utils.js) and `snippetUpdatePreview`
4
+ // (defined inline in index.html).
5
+
6
+ let _treeItems = [];
7
+
8
+ function treeInit() {
9
+ _treeItems = [
10
+ { name: "dossier/", depth: 0 },
11
+ { name: "fichier.md", depth: 1 },
12
+ ];
13
+ treeRenderList();
14
+ }
15
+
16
+ function treeRenderList() {
17
+ const btnCls =
18
+ "text-xs px-1.5 py-1 rounded border border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-30 disabled:cursor-not-allowed transition-colors";
19
+ document.getElementById("snip-tree-list").innerHTML = _treeItems
20
+ .map(
21
+ (item, i) => `
22
+ <div class="flex items-center gap-1">
23
+ <button onclick="treeIndent(${i},-1)" ${item.depth === 0 ? "disabled" : ""} class="${btnCls}" title="Désindenter">←</button>
24
+ <button onclick="treeIndent(${i},1)" class="${btnCls}" title="Indenter">→</button>
25
+ <span class="text-xs text-gray-300 dark:text-gray-600 font-mono w-4 shrink-0 text-center">${item.depth > 0 ? item.depth : ""}</span>
26
+ <input type="text" value="${esc(item.name)}" oninput="treeUpdate(${i}, this.value)"
27
+ class="flex-1 px-2 py-1 text-xs rounded 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-1 focus:ring-blue-500 font-mono" />
28
+ <button onclick="treeRemoveItem(${i})" class="${btnCls}" title="Supprimer">×</button>
29
+ </div>`,
30
+ )
31
+ .join("");
32
+ snippetUpdatePreview();
33
+ }
34
+
35
+ function treeUpdate(i, val) {
36
+ _treeItems[i].name = val;
37
+ snippetUpdatePreview();
38
+ }
39
+
40
+ function treeIndent(i, delta) {
41
+ _treeItems[i].depth = Math.max(0, _treeItems[i].depth + delta);
42
+ treeRenderList();
43
+ }
44
+
45
+ function treeAddItem() {
46
+ const lastDepth = _treeItems.length
47
+ ? _treeItems[_treeItems.length - 1].depth
48
+ : 0;
49
+ _treeItems.push({ name: "", depth: lastDepth });
50
+ treeRenderList();
51
+ }
52
+
53
+ function treeRemoveItem(i) {
54
+ _treeItems.splice(i, 1);
55
+ treeRenderList();
56
+ }
57
+
58
+ function buildTreeMarkdown() {
59
+ if (!_treeItems.length) return "```text\n(vide)\n```";
60
+ const lines = [];
61
+ for (let i = 0; i < _treeItems.length; i++) {
62
+ const { name, depth } = _treeItems[i];
63
+ if (depth === 0) {
64
+ lines.push(name);
65
+ continue;
66
+ }
67
+
68
+ // Prefix: for each ancestor level a (0..depth-2),
69
+ // show │ if there's a sibling at depth a+1 after this item
70
+ let prefix = "";
71
+ for (let a = 0; a < depth - 1; a++) {
72
+ let continues = false;
73
+ for (let j = i + 1; j < _treeItems.length; j++) {
74
+ if (_treeItems[j].depth <= a + 1) {
75
+ continues = _treeItems[j].depth === a + 1;
76
+ break;
77
+ }
78
+ }
79
+ prefix += continues ? "│ " : " ";
80
+ }
81
+
82
+ // Connector: └── if last sibling at this depth, ├── otherwise
83
+ let isLast = true;
84
+ for (let j = i + 1; j < _treeItems.length; j++) {
85
+ if (_treeItems[j].depth < depth) break;
86
+ if (_treeItems[j].depth === depth) {
87
+ isLast = false;
88
+ break;
89
+ }
90
+ }
91
+ lines.push(prefix + (isLast ? "└── " : "├── ") + name);
92
+ }
93
+ return "```text\n" + lines.join("\n") + "\n```";
94
+ }