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,1073 @@
1
+ <!doctype html>
2
+ <html lang="en" class="h-full">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Admin — Living Documentation</title>
7
+
8
+ <script src="/i18n.js"></script>
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+
11
+ <script>
12
+ tailwind.config = { darkMode: "class", theme: { extend: {} } };
13
+ </script>
14
+ </head>
15
+
16
+ <body
17
+ class="h-full bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100"
18
+ >
19
+ <!-- ── Header ── -->
20
+ <header
21
+ class="flex items-center justify-between px-4 h-14 border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shadow-sm"
22
+ >
23
+ <div class="flex items-center gap-3">
24
+ <span
25
+ class="text-blue-600 dark:text-blue-400 text-xl select-none"
26
+ aria-hidden="true"
27
+ >&#128218;</span
28
+ >
29
+ <h1 class="text-base font-semibold tracking-tight" data-i18n="admin.title">Admin Panel</h1>
30
+ </div>
31
+
32
+ <div class="flex items-center gap-3">
33
+ <button
34
+ id="dark-toggle"
35
+ data-i18n-title="admin.toggle_dark"
36
+ class="p-2 rounded-lg text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
37
+ >
38
+ <span id="dark-icon" class="text-lg leading-none">☾</span>
39
+ </button>
40
+ <span class="text-gray-300 dark:text-gray-700">|</span>
41
+ <a
42
+ href="/"
43
+ class="text-blue-600 dark:text-blue-400 hover:underline text-sm"
44
+ >
45
+ <span data-i18n="admin.back_to_docs">Back to docs →</span>
46
+ </a>
47
+ </div>
48
+ </header>
49
+
50
+ <!-- ── Page ── -->
51
+ <main class="max-w-6xl mx-auto px-6 py-10">
52
+ <!-- ── Two-column layout ── -->
53
+ <form id="config-form" novalidate>
54
+ <!-- Title + Save on same row -->
55
+ <div class="flex items-center justify-between mb-8">
56
+ <div>
57
+ <h2 class="text-xl font-bold text-gray-900 dark:text-gray-50" data-i18n="admin.config.title">
58
+ Configuration
59
+ </h2>
60
+ <p class="mt-1 text-sm text-gray-500 dark:text-gray-400" data-i18n="admin.config.description">
61
+ Settings are saved to .living-doc.json in your docs folder.
62
+ </p>
63
+ </div>
64
+ <div class="flex items-center gap-4 shrink-0">
65
+ <div id="save-msg" class="text-sm"></div>
66
+ <button
67
+ type="submit"
68
+ class="px-5 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-950"
69
+ >
70
+ <span data-i18n="admin.config.save_btn">Save changes</span>
71
+ </button>
72
+ </div>
73
+ </div>
74
+
75
+ <div class="grid grid-cols-2 gap-8 items-start">
76
+ <!-- ── Left column ── -->
77
+ <div class="space-y-6">
78
+ <!-- Server Info -->
79
+ <div
80
+ class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5"
81
+ >
82
+ <h3
83
+ class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-4"
84
+ >
85
+ <span data-i18n="admin.server_info.title">Server Info</span>
86
+ </h3>
87
+ <dl class="grid grid-cols-[1fr_auto] gap-x-6 gap-y-3 text-sm">
88
+ <div>
89
+ <dt
90
+ class="text-xs text-gray-400 uppercase tracking-wide mb-0.5"
91
+ >
92
+ <span data-i18n="admin.server_info.docs_folder">Docs Folder</span>
93
+ </dt>
94
+ <dd
95
+ id="info-folder"
96
+ class="font-mono text-xs text-gray-700 dark:text-gray-300 break-all"
97
+ >
98
+
99
+ </dd>
100
+ </div>
101
+ <div>
102
+ <dt
103
+ class="text-xs text-gray-400 uppercase tracking-wide mb-0.5"
104
+ >
105
+ <span data-i18n="admin.server_info.port">Port</span>
106
+ </dt>
107
+ <dd
108
+ id="info-port"
109
+ class="font-mono text-xs text-gray-700 dark:text-gray-300"
110
+ >
111
+
112
+ </dd>
113
+ </div>
114
+ <div class="col-span-2 space-y-1.5">
115
+ <label
116
+ class="block text-xs text-gray-400 uppercase tracking-wide mb-0.5"
117
+ for="field-source-root"
118
+ data-i18n="admin.server_info.source_root"
119
+ >Source Root</label
120
+ >
121
+ <input
122
+ id="field-source-root"
123
+ name="sourceRoot"
124
+ type="text"
125
+ class="w-full px-3 py-2 font-mono text-xs rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 break-all"
126
+ placeholder="/absolute/path"
127
+ />
128
+ <p
129
+ class="text-xs text-gray-400 dark:text-gray-500"
130
+ data-i18n="admin.server_info.source_root_hint"
131
+ >
132
+ Absolute path used by MCP source tools. Defaults to the parent of Docs Folder when empty.
133
+ </p>
134
+ </div>
135
+ </dl>
136
+ </div>
137
+
138
+ <!-- Extra Files -->
139
+ <div
140
+ class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5 space-y-4"
141
+ >
142
+ <div>
143
+ <h3
144
+ class="text-sm font-semibold text-gray-700 dark:text-gray-300"
145
+ >
146
+ <span data-i18n="admin.extra_files.title">General — Extra Files</span>
147
+ </h3>
148
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400" data-i18n="admin.extra_files.description">
149
+ Add Markdown files from outside the docs folder to the General section.
150
+ </p>
151
+ </div>
152
+
153
+ <!-- File browser -->
154
+ <div
155
+ class="rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"
156
+ >
157
+ <div
158
+ class="flex items-center gap-2 px-3 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"
159
+ >
160
+ <button
161
+ id="browse-up"
162
+ onclick="browseUp()"
163
+ class="text-xs text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-30 disabled:pointer-events-none shrink-0"
164
+ >
165
+ <span data-i18n="common.up">↑ Up</span>
166
+ </button>
167
+ <span
168
+ id="browse-path"
169
+ class="font-mono text-xs text-gray-400 dark:text-gray-500 truncate flex-1 text-right"
170
+ ></span>
171
+ </div>
172
+ <div
173
+ id="browse-list"
174
+ class="divide-y divide-gray-100 dark:divide-gray-800 max-h-52 overflow-y-auto"
175
+ ></div>
176
+ </div>
177
+
178
+ <!-- Added files -->
179
+ <div>
180
+ <p
181
+ class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2"
182
+ >
183
+ Added files
184
+ </p>
185
+ <div id="extra-files-list" class="space-y-1"></div>
186
+ <p id="extra-files-empty" class="text-xs text-gray-400 italic">
187
+ <span data-i18n="admin.extra_files.empty">No extra files added yet.</span>
188
+ </p>
189
+ </div>
190
+ </div>
191
+ </div>
192
+
193
+ <!-- ── Right column ── -->
194
+ <div class="space-y-6">
195
+ <!-- Appearance & Metadata -->
196
+ <div
197
+ class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 divide-y divide-gray-100 dark:divide-gray-800"
198
+ >
199
+ <div class="px-5 py-4">
200
+ <h3
201
+ class="text-sm font-semibold text-gray-700 dark:text-gray-300"
202
+ >
203
+ Appearance & Metadata
204
+ </h3>
205
+ </div>
206
+ <div class="px-5 py-4 space-y-1.5">
207
+ <label
208
+ class="block text-sm font-medium text-gray-700 dark:text-gray-300"
209
+ for="field-title"
210
+ data-i18n="admin.appearance.site_title_label">Site Title</label
211
+ >
212
+ <input
213
+ id="field-title"
214
+ name="title"
215
+ type="text"
216
+ 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 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
217
+ placeholder="Living Documentation"
218
+ />
219
+ <p class="text-xs text-gray-400 dark:text-gray-500" data-i18n="admin.appearance.site_title_hint">
220
+ Displayed in the browser tab and sidebar header.
221
+ </p>
222
+ </div>
223
+ <div class="px-5 py-4 space-y-1.5">
224
+ <label
225
+ class="block text-sm font-medium text-gray-700 dark:text-gray-300"
226
+ for="field-theme"
227
+ data-i18n="admin.appearance.theme_label">Default Theme</label
228
+ >
229
+ <select
230
+ id="field-theme"
231
+ name="theme"
232
+ 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"
233
+ >
234
+ <option value="system" data-i18n="admin.appearance.theme_system">System (follow OS preference)</option>
235
+ <option value="light" data-i18n="admin.appearance.theme_light">Light</option>
236
+ <option value="dark" data-i18n="admin.appearance.theme_dark">Dark</option>
237
+ </select>
238
+ <p class="text-xs text-gray-400 dark:text-gray-500" data-i18n="admin.appearance.theme_hint">
239
+ Users can always override with the toggle button.
240
+ </p>
241
+ </div>
242
+ <div class="px-5 py-4 space-y-1.5">
243
+ <label
244
+ class="block text-sm font-medium text-gray-700 dark:text-gray-300"
245
+ for="field-language"
246
+ data-i18n="admin.appearance.language_label">Language</label>
247
+ <select
248
+ id="field-language"
249
+ name="language"
250
+ 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"
251
+ >
252
+ <option value="en" data-i18n="admin.appearance.language_en">English</option>
253
+ <option value="fr" data-i18n="admin.appearance.language_fr">Français</option>
254
+ </select>
255
+ <p class="text-xs text-gray-400 dark:text-gray-500" data-i18n="admin.appearance.language_hint">
256
+ Interface language for all pages.
257
+ </p>
258
+ </div>
259
+ <div class="px-5 py-4">
260
+ <label class="flex items-center gap-3 cursor-pointer">
261
+ <input
262
+ id="field-debug"
263
+ name="showDiagramDebug"
264
+ type="checkbox"
265
+ class="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
266
+ />
267
+ <span
268
+ class="text-sm font-medium text-gray-700 dark:text-gray-300"
269
+ data-i18n="admin.appearance.debug_label">Show debug button in diagram editor</span
270
+ >
271
+ </label>
272
+ <p class="mt-1 text-xs text-gray-400 dark:text-gray-500 ml-7">
273
+ Displays a Debug button that overlays raw position and size
274
+ info on each node — useful for diagnosing snap-to-grid issues.
275
+ </p>
276
+ </div>
277
+ <div class="px-5 py-4 space-y-1.5">
278
+ <label
279
+ class="block text-sm font-medium text-gray-700 dark:text-gray-300"
280
+ for="field-pattern"
281
+ data-i18n="admin.pattern.label">Filename Pattern</label
282
+ >
283
+ <input
284
+ id="field-pattern"
285
+ name="filenamePattern"
286
+ type="text"
287
+ 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 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
288
+ placeholder="YYYY_MM_DD_HH_mm_[Category]_title"
289
+ />
290
+ <p class="text-xs text-gray-400 dark:text-gray-500">
291
+ Parsed for date, category, and title.
292
+ <span
293
+ id="pattern-desc-example"
294
+ class="font-mono bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 px-1 rounded"
295
+ >YYYY_MM_DD_HH_mm_[Category]_title.md</span
296
+ >
297
+ </p>
298
+ </div>
299
+ </div>
300
+
301
+ <!-- Pattern Preview -->
302
+ <div
303
+ id="pattern-preview"
304
+ class="rounded-xl border border-blue-100 dark:border-blue-900 bg-blue-50 dark:bg-blue-950/30 p-5"
305
+ >
306
+ <h3
307
+ class="text-sm font-semibold text-blue-700 dark:text-blue-400 mb-3"
308
+ >
309
+ Pattern Preview
310
+ </h3>
311
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
312
+ How your pattern parses example filenames:
313
+ </p>
314
+ <div id="preview-rows" class="space-y-2 text-sm"></div>
315
+ </div>
316
+ </div>
317
+ </div>
318
+
319
+ <!-- ── Diagram Palettes (full width) ── -->
320
+ <div
321
+ class="mt-8 rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 divide-y divide-gray-100 dark:divide-gray-800"
322
+ >
323
+ <div class="px-5 py-4">
324
+ <h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">
325
+ Diagram Color Palettes
326
+ </h3>
327
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
328
+ Customize the colors available in the diagram editor. Changes take
329
+ effect immediately.
330
+ </p>
331
+ </div>
332
+ <div
333
+ class="grid grid-cols-2 divide-x divide-gray-100 dark:divide-gray-800"
334
+ >
335
+ <!-- Node palette -->
336
+ <div class="px-5 py-4 space-y-3">
337
+ <div class="flex items-center justify-between">
338
+ <h4
339
+ class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide"
340
+ >
341
+ Shape colors
342
+ </h4>
343
+ <button
344
+ type="button"
345
+ onclick="resetNodePalette()"
346
+ class="text-xs text-blue-600 dark:text-blue-400 hover:underline"
347
+ >
348
+ Reset
349
+ </button>
350
+ </div>
351
+ <p class="text-xs text-gray-400 dark:text-gray-500">
352
+ Click a color to show or hide it in the node panel. Dimmed =
353
+ hidden.
354
+ </p>
355
+ <div id="nodeSwatches" class="flex flex-wrap gap-2"></div>
356
+ </div>
357
+
358
+ <!-- Edge palette -->
359
+ <div class="px-5 py-4 space-y-3">
360
+ <div class="flex items-center justify-between">
361
+ <h4
362
+ class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide"
363
+ >
364
+ Arrow colors
365
+ </h4>
366
+ <button
367
+ type="button"
368
+ onclick="resetEdgePalette()"
369
+ class="text-xs text-blue-600 dark:text-blue-400 hover:underline"
370
+ >
371
+ Reset
372
+ </button>
373
+ </div>
374
+ <p class="text-xs text-gray-400 dark:text-gray-500">
375
+ Add or remove colors available for arrows. Click ✕ to remove.
376
+ </p>
377
+ <div
378
+ id="edgeSwatches"
379
+ class="flex flex-wrap gap-3 items-end"
380
+ ></div>
381
+ <div class="flex items-center gap-2 pt-1">
382
+ <input
383
+ type="color"
384
+ id="newEdgeColor"
385
+ value="#3b82f6"
386
+ class="w-8 h-8 rounded cursor-pointer border border-gray-300 dark:border-gray-600 bg-transparent"
387
+ />
388
+ <button
389
+ type="button"
390
+ onclick="addEdgeColor()"
391
+ class="px-3 py-1.5 text-xs rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-medium transition-colors"
392
+ >
393
+ Add color
394
+ </button>
395
+ </div>
396
+ </div>
397
+ </div>
398
+ </div>
399
+ </form>
400
+
401
+ <!-- ── AGPL-3.0 License ── -->
402
+ <div class="mt-12 rounded-2xl overflow-hidden border border-blue-100 dark:border-blue-900/40 shadow-sm">
403
+ <!-- Gradient banner -->
404
+ <div class="bg-gradient-to-r from-blue-600 via-blue-500 to-indigo-500 px-6 py-5 flex items-center gap-4">
405
+ <div class="text-white text-3xl select-none" aria-hidden="true">📖</div>
406
+ <div>
407
+ <h3 class="text-white font-bold text-base tracking-tight" data-i18n="admin.license.title">
408
+ AGPL-3.0 License — Open Source
409
+ </h3>
410
+ <p class="text-blue-100 text-xs mt-0.5" data-i18n="admin.license.subtitle">
411
+ Free to use, fork, and build upon — derivatives and network-hosted services must remain under AGPL (copyleft + SaaS clause).
412
+ </p>
413
+ </div>
414
+ </div>
415
+
416
+ <!-- Body -->
417
+ <div class="bg-white dark:bg-gray-900 px-6 py-5 space-y-4">
418
+ <p class="text-sm text-gray-700 dark:text-gray-300 leading-relaxed" data-i18n-html="admin.license.body">
419
+ This open source project is gracefully provided to you by its conceptors
420
+ <strong>Youssef MEDAGHRI-ALAOUI</strong> &amp; <strong>Claude Code</strong>.
421
+ </p>
422
+
423
+ <div class="flex flex-wrap gap-3 pt-1">
424
+ <a
425
+ href="https://www.linkedin.com/in/youssef-medaghri-alaoui-93b2922/"
426
+ target="_blank"
427
+ rel="noopener noreferrer"
428
+ class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-[#0A66C2] hover:bg-[#0958a8] text-white text-sm font-semibold transition-colors shadow-sm"
429
+ >
430
+ <svg class="w-4 h-4 shrink-0" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
431
+ <path d="M20.45 20.45h-3.55v-5.57c0-1.33-.03-3.04-1.85-3.04-1.85 0-2.13 1.44-2.13 2.93v5.68H9.37V9h3.41v1.56h.05c.47-.9 1.63-1.85 3.35-1.85 3.58 0 4.24 2.36 4.24 5.43v6.31ZM5.34 7.43a2.06 2.06 0 1 1 0-4.12 2.06 2.06 0 0 1 0 4.12ZM7.12 20.45H3.56V9h3.56v11.45ZM22.22 0H1.77C.79 0 0 .77 0 1.72v20.56C0 23.23.79 24 1.77 24h20.45C23.2 24 24 23.23 24 22.28V1.72C24 .77 23.2 0 22.22 0Z"/>
432
+ </svg>
433
+ <span data-i18n="admin.license.linkedin_btn">Youssef on LinkedIn</span>
434
+ </a>
435
+
436
+ <a
437
+ href="https://www.npmjs.com/package/living-documentation"
438
+ target="_blank"
439
+ rel="noopener noreferrer"
440
+ class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-[#CB3837] hover:bg-[#a82d2d] text-white text-sm font-semibold transition-colors shadow-sm"
441
+ >
442
+ <svg class="w-4 h-4 shrink-0" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
443
+ <path d="M0 0v24h24V0H0Zm19.2 19.2H13.6V8h-3.2v11.2H4.8V4.8h14.4v14.4Z"/>
444
+ </svg>
445
+ <span data-i18n="admin.license.npm_btn">living-documentation on npm</span>
446
+ </a>
447
+ </div>
448
+ </div>
449
+ </div>
450
+ </main>
451
+
452
+ <script>
453
+ // ── Boot ───────────────────────────────────────────────────
454
+ document.addEventListener("DOMContentLoaded", async () => {
455
+ applyDarkMode(loadDarkPref());
456
+ setupDarkToggle();
457
+ await loadConfig();
458
+ setupPatternPreview();
459
+ document
460
+ .getElementById("config-form")
461
+ .addEventListener("submit", saveConfig);
462
+ });
463
+
464
+ // ── Dark mode ──────────────────────────────────────────────
465
+ function loadDarkPref() {
466
+ const saved = localStorage.getItem("ld-dark");
467
+ if (saved !== null) return saved === "true";
468
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
469
+ }
470
+ function applyDarkMode(dark) {
471
+ document.documentElement.classList.toggle("dark", dark);
472
+ document.getElementById("dark-icon").textContent = dark ? "☀" : "☾";
473
+ }
474
+ function setupDarkToggle() {
475
+ document.getElementById("dark-toggle").addEventListener("click", () => {
476
+ const isDark = document.documentElement.classList.toggle("dark");
477
+ localStorage.setItem("ld-dark", isDark);
478
+ document.getElementById("dark-icon").textContent = isDark ? "☀" : "☾";
479
+ });
480
+ }
481
+
482
+ // ── Config ─────────────────────────────────────────────────
483
+ async function loadConfig() {
484
+ try {
485
+ const cfg = await fetch("/api/config").then((r) => r.json());
486
+ await window.initI18n(cfg.language || 'en');
487
+ window.applyI18n();
488
+ document.getElementById("info-folder").textContent =
489
+ cfg.docsFolder || "—";
490
+ document.getElementById("info-port").textContent = cfg.port || "—";
491
+ document.getElementById("field-source-root").value =
492
+ cfg.sourceRoot || "";
493
+ document.getElementById("field-title").value = cfg.title || "";
494
+ document.getElementById("field-theme").value = cfg.theme || "system";
495
+ document.getElementById("field-language").value = cfg.language || "en";
496
+ document.getElementById("field-pattern").value =
497
+ cfg.filenamePattern || "";
498
+ document.getElementById("field-debug").checked =
499
+ !!cfg.showDiagramDebug;
500
+ updatePreview(cfg.filenamePattern);
501
+ initExtraFiles(cfg);
502
+ initPalettes(cfg);
503
+ } catch {
504
+ showMsg(window.t('admin.msg.load_failed'), "error");
505
+ }
506
+ }
507
+
508
+ async function saveConfig(e) {
509
+ e.preventDefault();
510
+ const filenamePattern = document
511
+ .getElementById("field-pattern")
512
+ .value.trim();
513
+ if (filenamePattern) {
514
+ const catCount = (filenamePattern.match(/\[Category\]/gi) || [])
515
+ .length;
516
+ if (catCount === 0) {
517
+ showMsg(window.t('admin.msg.pattern_no_category'), "error");
518
+ return;
519
+ }
520
+ if (catCount > 1) {
521
+ showMsg(window.t('admin.msg.pattern_one_category'), "error");
522
+ return;
523
+ }
524
+ }
525
+ const sourceRootRaw = document
526
+ .getElementById("field-source-root")
527
+ .value.trim();
528
+ const payload = {
529
+ title: document.getElementById("field-title").value.trim(),
530
+ theme: document.getElementById("field-theme").value,
531
+ language: document.getElementById("field-language").value,
532
+ filenamePattern,
533
+ showDiagramDebug: document.getElementById("field-debug").checked,
534
+ diagramNodePalette: [...nodePalette],
535
+ diagramEdgePalette: [...edgePalette],
536
+ sourceRoot: sourceRootRaw === "" ? null : sourceRootRaw,
537
+ };
538
+
539
+ try {
540
+ const res = await fetch("/api/config", {
541
+ method: "PUT",
542
+ headers: { "Content-Type": "application/json" },
543
+ body: JSON.stringify(payload),
544
+ });
545
+ if (!res.ok) throw new Error(await res.text());
546
+ showMsg(window.t('admin.msg.saved'), "ok");
547
+ } catch (err) {
548
+ showMsg(window.t('admin.msg.save_failed') + err.message, "error");
549
+ }
550
+ }
551
+
552
+ function showMsg(text, type) {
553
+ const el = document.getElementById("save-msg");
554
+ el.textContent = text;
555
+ el.className =
556
+ "text-sm " +
557
+ (type === "ok"
558
+ ? "text-green-600 dark:text-green-400"
559
+ : "text-red-600 dark:text-red-400");
560
+ if (type === "ok")
561
+ setTimeout(() => {
562
+ el.textContent = "";
563
+ }, 3000);
564
+ }
565
+
566
+ // ── Pattern preview ────────────────────────────────────────
567
+ const EXAMPLES = [
568
+ "2024_01_15_09_30_[DevOps]_deploy_pipeline.md",
569
+ "2023_11_03_14_45_[Frontend]_react_hooks_guide.md",
570
+ "2025_06_20_08_00_meeting_notes.md",
571
+ "readme.md",
572
+ ];
573
+
574
+ function dateStrToISO(s) {
575
+ const p = s.split("_");
576
+ return p.length === 5
577
+ ? p[0] + "-" + p[1] + "-" + p[2] + "T" + p[3] + ":" + p[4]
578
+ : p[0] + "-" + p[1] + "-" + p[2];
579
+ }
580
+
581
+ function buildPatternsFromFormat(patternStr) {
582
+ if (!patternStr) patternStr = "YYYY_MM_DD_HH_mm_[Category]_title";
583
+ const hasDate = /YYYY.*MM.*DD/.test(patternStr);
584
+ const hasTime = hasDate && /HH.*mm/.test(patternStr);
585
+ const hasCategory = /\[Category\]/i.test(patternStr);
586
+ const dateGroup = hasTime
587
+ ? "(\\d{4}_\\d{2}_\\d{2}(?:_\\d{2}_\\d{2})?)"
588
+ : "(\\d{4}_\\d{2}_\\d{2})";
589
+ const catGroup = "\\[([^\\]]+)\\]";
590
+ const catBeforeDate =
591
+ hasDate &&
592
+ hasCategory &&
593
+ patternStr.search(/\[Category\]/i) < patternStr.search(/YYYY/i);
594
+ let full = null,
595
+ dateOnly = null;
596
+ if (hasDate && hasCategory) {
597
+ const ordered = catBeforeDate
598
+ ? catGroup + "_" + dateGroup
599
+ : dateGroup + "_" + catGroup;
600
+ full = new RegExp("^" + ordered + "_(.+)\\.md$", "i");
601
+ dateOnly = new RegExp("^" + dateGroup + "_(.+)\\.md$", "i");
602
+ } else if (hasDate) {
603
+ dateOnly = new RegExp("^" + dateGroup + "_(.+)\\.md$", "i");
604
+ } else if (hasCategory) {
605
+ full = new RegExp("^" + catGroup + "_(.+)\\.md$", "i");
606
+ }
607
+ return { full, dateOnly, hasDate, hasCategory, catBeforeDate };
608
+ }
609
+
610
+ function parsePreview(filename, patterns) {
611
+ if (patterns.full) {
612
+ const m = filename.match(patterns.full);
613
+ if (m) {
614
+ if (patterns.hasDate && patterns.hasCategory) {
615
+ const dateStr = patterns.catBeforeDate ? m[2] : m[1];
616
+ const category = patterns.catBeforeDate ? m[1] : m[2];
617
+ return {
618
+ date: dateStrToISO(dateStr),
619
+ category,
620
+ title: titleCase(m[3]),
621
+ match: true,
622
+ };
623
+ } else if (patterns.hasCategory) {
624
+ return {
625
+ date: null,
626
+ category: m[1],
627
+ title: titleCase(m[2]),
628
+ match: true,
629
+ };
630
+ }
631
+ }
632
+ }
633
+ if (patterns.dateOnly) {
634
+ const m = filename.match(patterns.dateOnly);
635
+ if (m) {
636
+ return {
637
+ date: dateStrToISO(m[1]),
638
+ category: "General",
639
+ title: titleCase(m[2]),
640
+ match: true,
641
+ };
642
+ }
643
+ }
644
+ return {
645
+ date: null,
646
+ category: "General",
647
+ title: filename.replace(".md", ""),
648
+ match: false,
649
+ };
650
+ }
651
+
652
+ function titleCase(s) {
653
+ return s
654
+ .split("_")
655
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
656
+ .join(" ");
657
+ }
658
+
659
+ function updatePreview() {
660
+ const patternVal = document
661
+ .getElementById("field-pattern")
662
+ .value.trim();
663
+ const descSpan = document.getElementById("pattern-desc-example");
664
+ descSpan.textContent =
665
+ (patternVal || "YYYY_MM_DD_HH_mm_[Category]_title") + ".md";
666
+ const patterns = buildPatternsFromFormat(patternVal);
667
+ const rows = document.getElementById("preview-rows");
668
+ rows.innerHTML = EXAMPLES.map((f) => {
669
+ const p = parsePreview(f, patterns);
670
+ return `
671
+ <div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-3">
672
+ <p class="font-mono text-xs text-gray-500 dark:text-gray-400 mb-2 break-all">${esc(f)}</p>
673
+ <div class="grid grid-cols-3 gap-2 text-xs">
674
+ <div>
675
+ <span class="text-gray-400 block mb-0.5">Date</span>
676
+ <span class="${p.date ? "text-green-600 dark:text-green-400" : "text-gray-400"}">${p.date || "none"}</span>
677
+ </div>
678
+ <div>
679
+ <span class="text-gray-400 block mb-0.5">Category</span>
680
+ <span class="text-blue-600 dark:text-blue-400">${esc(p.category)}</span>
681
+ </div>
682
+ <div>
683
+ <span class="text-gray-400 block mb-0.5">Title</span>
684
+ <span class="text-gray-700 dark:text-gray-300 truncate block">${esc(p.title)}</span>
685
+ </div>
686
+ </div>
687
+ </div>`;
688
+ }).join("");
689
+ }
690
+
691
+ function setupPatternPreview() {
692
+ document
693
+ .getElementById("field-pattern")
694
+ .addEventListener("input", updatePreview);
695
+ }
696
+
697
+ function esc(s) {
698
+ return String(s)
699
+ .replace(/&/g, "&amp;")
700
+ .replace(/</g, "&lt;")
701
+ .replace(/>/g, "&gt;")
702
+ .replace(/"/g, "&quot;")
703
+ .replace(/'/g, "&#39;");
704
+ }
705
+
706
+ // ── Extra Files / Browser ──────────────────────────────────
707
+ let extraFiles = [];
708
+ let browseParent = null;
709
+ let browseCurrent = "";
710
+ let docsFolder = "";
711
+
712
+ function initExtraFiles(cfg) {
713
+ extraFiles = cfg.extraFiles || [];
714
+ docsFolder = cfg.docsFolder || "";
715
+ renderExtraFiles();
716
+ loadBrowse(cfg.docsFolder || "/");
717
+ }
718
+
719
+ function relativeDisplay(filePath) {
720
+ if (!docsFolder) return filePath;
721
+ const fromParts = docsFolder.split("/").filter(Boolean);
722
+ const toParts = filePath.split("/").filter(Boolean);
723
+ let common = 0;
724
+ while (
725
+ common < fromParts.length &&
726
+ common < toParts.length &&
727
+ fromParts[common] === toParts[common]
728
+ )
729
+ common++;
730
+ const ups = fromParts.length - common;
731
+ const downs = toParts.slice(common).join("/");
732
+ const rel = "../".repeat(ups) + downs;
733
+ return "$DOCS_FOLDER/" + rel;
734
+ }
735
+
736
+ async function loadBrowse(dirPath) {
737
+ const list = document.getElementById("browse-list");
738
+ list.innerHTML =
739
+ '<p class="px-3 py-4 text-xs text-gray-400 text-center">Loading…</p>';
740
+ try {
741
+ const data = await fetch(
742
+ "/api/browse?path=" + encodeURIComponent(dirPath),
743
+ ).then((r) => r.json());
744
+ browseCurrent = data.current;
745
+ browseParent = data.parent;
746
+ document.getElementById("browse-path").textContent = relativeDisplay(
747
+ data.current,
748
+ );
749
+ document.getElementById("browse-up").disabled = !data.parent;
750
+
751
+ const rows = [];
752
+
753
+ for (const dir of data.dirs) {
754
+ rows.push(`
755
+ <button data-path="${esc(dir.path)}" onclick="loadBrowse(this.dataset.path)"
756
+ class="w-full flex items-center gap-2 px-3 py-2 text-sm text-left
757
+ hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
758
+ <span class="text-gray-400 shrink-0">📁</span>
759
+ <span class="text-gray-700 dark:text-gray-300 truncate">${esc(dir.name)}</span>
760
+ </button>`);
761
+ }
762
+
763
+ for (const file of data.files) {
764
+ const added = extraFiles.includes(file.path);
765
+ rows.push(`
766
+ <div class="flex items-center justify-between gap-2 px-3 py-2 text-sm
767
+ hover:bg-gray-50 dark:hover:bg-gray-800">
768
+ <span class="flex items-center gap-2 text-gray-700 dark:text-gray-300 min-w-0">
769
+ <span class="text-gray-400 shrink-0">📄</span>
770
+ <span class="truncate">${esc(file.name)}</span>
771
+ </span>
772
+ ${
773
+ added
774
+ ? '<span class="text-xs text-green-600 dark:text-green-400 shrink-0">Added</span>'
775
+ : `<button data-path="${esc(file.path)}" onclick="addExtraFile(this.dataset.path)"
776
+ class="text-xs text-blue-600 dark:text-blue-400 hover:underline shrink-0">+ Add</button>`
777
+ }
778
+ </div>`);
779
+ }
780
+
781
+ if (rows.length === 0) {
782
+ list.innerHTML =
783
+ '<p class="px-3 py-4 text-xs text-gray-400 text-center">Empty directory</p>';
784
+ } else {
785
+ list.innerHTML = rows.join("");
786
+ }
787
+ } catch {
788
+ list.innerHTML =
789
+ '<p class="px-3 py-4 text-xs text-red-400 text-center">Cannot read directory</p>';
790
+ }
791
+ }
792
+
793
+ function browseUp() {
794
+ if (browseParent) loadBrowse(browseParent);
795
+ }
796
+
797
+ async function addExtraFile(filePath) {
798
+ if (extraFiles.includes(filePath)) return;
799
+ extraFiles = [...extraFiles, filePath];
800
+ await saveExtraFiles();
801
+ renderExtraFiles();
802
+ loadBrowse(browseCurrent);
803
+ }
804
+
805
+ async function removeExtraFile(filePath) {
806
+ extraFiles = extraFiles.filter((f) => f !== filePath);
807
+ await saveExtraFiles();
808
+ renderExtraFiles();
809
+ loadBrowse(browseCurrent);
810
+ }
811
+
812
+ async function saveExtraFiles() {
813
+ try {
814
+ const res = await fetch("/api/config", {
815
+ method: "PUT",
816
+ headers: { "Content-Type": "application/json" },
817
+ body: JSON.stringify({ extraFiles }),
818
+ });
819
+ if (!res.ok) throw new Error(await res.text());
820
+ } catch (err) {
821
+ showMsg("Failed to save: " + err.message, "error");
822
+ }
823
+ }
824
+
825
+ function moveExtraFile(index, direction) {
826
+ const target = index + direction;
827
+ if (target < 0 || target >= extraFiles.length) return;
828
+ const next = [...extraFiles];
829
+ [next[index], next[target]] = [next[target], next[index]];
830
+ extraFiles = next;
831
+ saveExtraFiles();
832
+ renderExtraFiles();
833
+ }
834
+
835
+ // ── Diagram Palettes ───────────────────────────────────────
836
+ // NODE_COLORS_ADMIN mirrors constants.js NODE_COLORS (default bg values).
837
+ // Mirrors NODE_COLORS from constants.js (bg + original border).
838
+ const NODE_COLORS_ADMIN = {
839
+ "c-white": { bg: "#ffffff", border: "#dbd5d1", label: "White" },
840
+ "c-gray": { bg: "#f5f5f4", border: "#a8a29e", label: "Gris" },
841
+ "c-slate": { bg: "#f1f5f9", border: "#64748b", label: "Ardoise" },
842
+ "c-blue": { bg: "#dbeafe", border: "#3b82f6", label: "Bleu" },
843
+ "c-sky": { bg: "#e0f2fe", border: "#0ea5e9", label: "Ciel" },
844
+ "c-cyan": { bg: "#cffafe", border: "#06b6d4", label: "Cyan" },
845
+ "c-teal": { bg: "#ccfbf1", border: "#14b8a6", label: "Teal" },
846
+ "c-green": { bg: "#dcfce7", border: "#22c55e", label: "Vert" },
847
+ "c-lime": { bg: "#ecfccb", border: "#84cc16", label: "Lime" },
848
+ "c-amber": { bg: "#fef9c3", border: "#f59e0b", label: "Ambre" },
849
+ "c-orange": { bg: "#ffedd5", border: "#f97316", label: "Orange" },
850
+ "c-red": { bg: "#fee2e2", border: "#ef4444", label: "Rouge" },
851
+ "c-rose": { bg: "#ffe4e6", border: "#f43f5e", label: "Rose" },
852
+ "c-pink": { bg: "#fce7f3", border: "#ec4899", label: "Fuschia" },
853
+ "c-purple": { bg: "#ede9fe", border: "#8b5cf6", label: "Violet" },
854
+ };
855
+ const NODE_PALETTE_KEYS = Object.keys(NODE_COLORS_ADMIN); // fixed order of 15 slots
856
+ const DEFAULT_EDGE_PALETTE_ADMIN = [
857
+ "#ffffff",
858
+ "#a8a29e",
859
+ "#374151",
860
+ "#3b82f6",
861
+ "#14b8a6",
862
+ "#22c55e",
863
+ "#f97316",
864
+ "#ef4444",
865
+ "#a855f7",
866
+ ];
867
+
868
+ // nodePalette: array of 15 bg hex strings, one per slot (positional).
869
+ let nodePalette = NODE_PALETTE_KEYS.map((k) => NODE_COLORS_ADMIN[k].bg);
870
+ let edgePalette = [...DEFAULT_EDGE_PALETTE_ADMIN];
871
+
872
+ // Per-slot lightness ratio (border_L / bg_L) — mirrors NODE_L_RATIOS from constants.js.
873
+ const NODE_L_RATIOS_ADMIN = {
874
+ "c-gray": 0.667,
875
+ "c-slate": 0.488,
876
+ "c-blue": 0.645,
877
+ "c-sky": 0.518,
878
+ "c-cyan": 0.473,
879
+ "c-teal": 0.448,
880
+ "c-green": 0.489,
881
+ "c-lime": 0.496,
882
+ "c-amber": 0.57,
883
+ "c-orange": 0.578,
884
+ "c-red": 0.64,
885
+ "c-rose": 0.636,
886
+ "c-pink": 0.638,
887
+ "c-purple": 0.694,
888
+ "c-indigo": 0.71,
889
+ };
890
+
891
+ // Mirrors deriveNodeColors from constants.js (no ES module in admin.html).
892
+ function deriveNodeColors(bgHex, lRatio = 0.6) {
893
+ const r = parseInt(bgHex.slice(1, 3), 16) / 255;
894
+ const g = parseInt(bgHex.slice(3, 5), 16) / 255;
895
+ const b = parseInt(bgHex.slice(5, 7), 16) / 255;
896
+ const max = Math.max(r, g, b),
897
+ min = Math.min(r, g, b);
898
+ const l = (max + min) / 2;
899
+ const d = max - min;
900
+ let h = 0,
901
+ s = 0;
902
+ if (d > 0) {
903
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
904
+ if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
905
+ else if (max === g) h = ((b - r) / d + 2) / 6;
906
+ else h = ((r - g) / d + 4) / 6;
907
+ }
908
+ function hslToHex(hh, ss, ll) {
909
+ let rr, gg, bb;
910
+ if (ss === 0) {
911
+ rr = gg = bb = ll;
912
+ } else {
913
+ const hue2rgb = (p, q, t) => {
914
+ if (t < 0) t += 1;
915
+ if (t > 1) t -= 1;
916
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
917
+ if (t < 1 / 2) return q;
918
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
919
+ return p;
920
+ };
921
+ const q = ll < 0.5 ? ll * (1 + ss) : ll + ss - ll * ss;
922
+ const p = 2 * ll - q;
923
+ rr = hue2rgb(p, q, hh + 1 / 3);
924
+ gg = hue2rgb(p, q, hh);
925
+ bb = hue2rgb(p, q, hh - 1 / 3);
926
+ }
927
+ return (
928
+ "#" +
929
+ [rr, gg, bb]
930
+ .map((v) =>
931
+ Math.max(0, Math.min(255, Math.round(v * 255)))
932
+ .toString(16)
933
+ .padStart(2, "0"),
934
+ )
935
+ .join("")
936
+ );
937
+ }
938
+ const border = hslToHex(h, s, l * lRatio);
939
+ return { bg: bgHex, border };
940
+ }
941
+
942
+ function initPalettes(cfg) {
943
+ // nodePalette: positional array of 15 bg hex strings
944
+ if (
945
+ Array.isArray(cfg.diagramNodePalette) &&
946
+ cfg.diagramNodePalette.length
947
+ ) {
948
+ nodePalette = NODE_PALETTE_KEYS.map(
949
+ (k, i) => cfg.diagramNodePalette[i] || NODE_COLORS_ADMIN[k].bg,
950
+ );
951
+ } else {
952
+ nodePalette = NODE_PALETTE_KEYS.map((k) => NODE_COLORS_ADMIN[k].bg);
953
+ }
954
+ edgePalette =
955
+ Array.isArray(cfg.diagramEdgePalette) && cfg.diagramEdgePalette.length
956
+ ? [...cfg.diagramEdgePalette]
957
+ : [...DEFAULT_EDGE_PALETTE_ADMIN];
958
+ renderNodePalette();
959
+ renderEdgePalette();
960
+ }
961
+
962
+ function swatchBorder(key, bg) {
963
+ // Use original hand-crafted border if bg is unchanged; derive for custom colours.
964
+ const def = NODE_COLORS_ADMIN[key];
965
+ return bg === def.bg
966
+ ? def.border
967
+ : deriveNodeColors(bg, NODE_L_RATIOS_ADMIN[key] || 0.6).border;
968
+ }
969
+
970
+ function renderNodePalette() {
971
+ const container = document.getElementById("nodeSwatches");
972
+ container.innerHTML = NODE_PALETTE_KEYS.map((key, i) => {
973
+ const bg = nodePalette[i] || NODE_COLORS_ADMIN[key].bg;
974
+ const border = swatchBorder(key, bg);
975
+ return `<div style="position:relative;width:32px;height:32px" title="${esc(NODE_COLORS_ADMIN[key].label)}">
976
+ <div style="width:32px;height:32px;border-radius:50%;background:${bg};border:3px solid ${border};pointer-events:none"></div>
977
+ <input type="color" value="${bg}" data-i="${i}"
978
+ oninput="previewNodeColor(${i},this.value)"
979
+ onchange="commitNodeColor(${i},this.value)"
980
+ style="position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;cursor:pointer;border:none;padding:0" />
981
+ </div>`;
982
+ }).join("");
983
+ }
984
+
985
+ function previewNodeColor(i, hex) {
986
+ nodePalette[i] = hex.toLowerCase();
987
+ const key = NODE_PALETTE_KEYS[i];
988
+ const border = swatchBorder(key, hex.toLowerCase());
989
+ const wrapper = document.getElementById("nodeSwatches").children[i];
990
+ if (wrapper) {
991
+ const swatch = wrapper.querySelector("div");
992
+ if (swatch) {
993
+ swatch.style.background = hex;
994
+ swatch.style.borderColor = border;
995
+ }
996
+ }
997
+ }
998
+
999
+ function commitNodeColor(i, hex) {
1000
+ previewNodeColor(i, hex);
1001
+ }
1002
+
1003
+ function resetNodePalette() {
1004
+ nodePalette = NODE_PALETTE_KEYS.map((k) => NODE_COLORS_ADMIN[k].bg);
1005
+ renderNodePalette();
1006
+ }
1007
+
1008
+ function renderEdgePalette() {
1009
+ const container = document.getElementById("edgeSwatches");
1010
+ container.innerHTML = edgePalette
1011
+ .map(
1012
+ (hex, i) => `
1013
+ <div style="display:flex;flex-direction:column;align-items:center;gap:3px">
1014
+ <div style="width:26px;height:26px;border-radius:50%;background:${hex};border:2px solid rgba(0,0,0,0.15)"></div>
1015
+ <button type="button" data-i="${i}" onclick="removeEdgeColor(+this.dataset.i)"
1016
+ style="font-size:10px;color:#ef4444;cursor:pointer;line-height:1;background:none;border:none"
1017
+ title="Supprimer">✕</button>
1018
+ </div>`,
1019
+ )
1020
+ .join("");
1021
+ }
1022
+
1023
+ function addEdgeColor() {
1024
+ const hex = document.getElementById("newEdgeColor").value.toLowerCase();
1025
+ if (edgePalette.includes(hex)) return;
1026
+ edgePalette = [...edgePalette, hex];
1027
+ renderEdgePalette();
1028
+ }
1029
+
1030
+ function removeEdgeColor(index) {
1031
+ if (edgePalette.length <= 1) return;
1032
+ edgePalette = edgePalette.filter((_, i) => i !== index);
1033
+ renderEdgePalette();
1034
+ }
1035
+
1036
+ function resetEdgePalette() {
1037
+ edgePalette = [...DEFAULT_EDGE_PALETTE_ADMIN];
1038
+ renderEdgePalette();
1039
+ }
1040
+
1041
+ function renderExtraFiles() {
1042
+ const list = document.getElementById("extra-files-list");
1043
+ const empty = document.getElementById("extra-files-empty");
1044
+ if (extraFiles.length === 0) {
1045
+ list.innerHTML = "";
1046
+ empty.style.display = "";
1047
+ return;
1048
+ }
1049
+ empty.style.display = "none";
1050
+ list.innerHTML = extraFiles
1051
+ .map(
1052
+ (f, i) => `
1053
+ <div class="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-50 dark:bg-gray-800">
1054
+ <div class="flex flex-col shrink-0">
1055
+ <button data-i="${i}" data-d="-1" onclick="moveExtraFile(+this.dataset.i, +this.dataset.d)"
1056
+ class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 leading-none disabled:opacity-20"
1057
+ ${i === 0 ? "disabled" : ""}>&#8593;</button>
1058
+ <button data-i="${i}" data-d="1" onclick="moveExtraFile(+this.dataset.i, +this.dataset.d)"
1059
+ class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 leading-none disabled:opacity-20"
1060
+ ${i === extraFiles.length - 1 ? "disabled" : ""}>&#8595;</button>
1061
+ </div>
1062
+ <div class="overflow-x-auto flex-1 min-w-0">
1063
+ <span class="font-mono text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap" title="${esc(f)}">${esc(relativeDisplay(f))}</span>
1064
+ </div>
1065
+ <button data-path="${esc(f)}" onclick="removeExtraFile(this.dataset.path)"
1066
+ class="text-xs text-red-500 hover:text-red-700 dark:hover:text-red-400 shrink-0">Remove</button>
1067
+ </div>`,
1068
+ )
1069
+ .join("");
1070
+ }
1071
+ </script>
1072
+ </body>
1073
+ </html>