living-documentation 7.9.0 → 7.11.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.
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "common.save": "Enregistrer",
3
3
  "common.cancel": "Annuler",
4
+ "common.confirm": "Confirmer",
4
5
  "common.reset": "Réinitialiser",
5
6
  "common.loading": "Chargement…",
6
7
  "common.remove": "Supprimer",
@@ -28,8 +29,31 @@
28
29
  "nav.new_document": "Nouveau document",
29
30
  "nav.word_cloud": "☁ Nuage de mots",
30
31
  "nav.diagram": "◇ Diagramme",
32
+ "nav.files": "📁 Fichiers Métadonnées",
31
33
  "nav.admin": "⚙ Admin",
32
34
 
35
+ "files.title": "📁 Fichiers joints",
36
+ "files.empty": "Aucun fichier — téléversez-en un depuis un document (trombone, glisser-déposer ou coller).",
37
+ "files.replace": "Remplacer",
38
+ "files.delete": "Supprimer",
39
+ "files.open": "Ouvrir",
40
+ "files.confirm_replace": "Remplacer « {name} » par « {newName} » ? La version précédente sera perdue.",
41
+ "files.confirm_replace_title": "Remplacer le fichier",
42
+ "files.confirm_replace_message": "Remplacer « {name} » par « {newName} » ?",
43
+ "files.confirm_replace_detail": "La version précédente sera perdue.",
44
+ "files.confirm_delete": "Supprimer définitivement « {name} » ?",
45
+ "files.confirm_delete_title": "Supprimer le fichier",
46
+ "files.confirm_delete_message": "Supprimer définitivement « {name} » ?",
47
+ "files.confirm_delete_detail": "Cette action est irréversible.",
48
+ "files.replacing": "Remplacement…",
49
+ "files.deleting": "Suppression…",
50
+ "files.error_load": "Impossible de charger les fichiers : ",
51
+ "files.error_replace": "Échec du remplacement : ",
52
+ "files.error_delete": "Échec de la suppression : ",
53
+ "files.size_bytes": "{n} o",
54
+ "files.size_kb": "{n} Ko",
55
+ "files.size_mb": "{n} Mo",
56
+
33
57
  "welcome.title": "Sélectionner un document",
34
58
  "welcome.hint": "Choisissez un document dans la barre latérale pour commencer à lire.",
35
59
  "welcome.pattern_hint": "Modèle de nom de fichier attendu",
@@ -126,21 +150,21 @@
126
150
 
127
151
  "snippet.modal_title": "🧩 Insérer un snippet",
128
152
  "snippet.type_label": "Type de snippet",
129
- "snippet.diagram": "Diagramme",
130
- "snippet.collapsible": "Bloc repliable (détails)",
131
- "snippet.link": "Lien",
132
- "snippet.link_doc": "Lien vers un document",
133
- "snippet.link_anchor": "Lien vers une ancre",
134
- "snippet.link_doc_anchor": "Lien vers un document avec ancre",
135
- "snippet.numbered_list": "Liste numérotée",
136
- "snippet.bullet_list": "Liste à puces",
137
- "snippet.code_block": "Bloc de code",
138
- "snippet.blockquote": "Citation (blockquote)",
139
- "snippet.separator": "Séparateur horizontal",
140
- "snippet.image": "Image",
141
- "snippet.table": "Tableau",
142
- "snippet.tree": "Arborescence",
143
- "snippet.colored_section": "Section colorée",
153
+ "snippet.diagram": "Diagramme",
154
+ "snippet.collapsible": "Bloc repliable (détails)",
155
+ "snippet.link": "🔗 Lien",
156
+ "snippet.link_doc": "📄 Lien vers un document",
157
+ "snippet.link_anchor": "Lien vers une ancre",
158
+ "snippet.link_doc_anchor": "📄⚓ Lien vers un document avec ancre",
159
+ "snippet.numbered_list": "🔢 Liste numérotée",
160
+ "snippet.bullet_list": "Liste à puces",
161
+ "snippet.code_block": "💻 Bloc de code",
162
+ "snippet.blockquote": "Citation (blockquote)",
163
+ "snippet.separator": "Séparateur horizontal",
164
+ "snippet.image": "🖼 Image",
165
+ "snippet.table": "Tableau",
166
+ "snippet.tree": "🌳 Arborescence",
167
+ "snippet.colored_section": "🎨 Section colorée",
144
168
  "snippet.colored_section_color_label": "Couleur",
145
169
  "snippet.colored_section_swatch_info": "Info (bleu)",
146
170
  "snippet.colored_section_swatch_success": "Succès (vert)",
@@ -150,7 +174,7 @@
150
174
  "snippet.colored_section_swatch_neutral": "Neutre (gris)",
151
175
  "snippet.colored_section_content_label": "Contenu",
152
176
  "snippet.colored_section_content_placeholder": "Votre texte ici…",
153
- "snippet.colored_text": "Texte coloré",
177
+ "snippet.colored_text": "🖍 Texte coloré",
154
178
  "snippet.colored_text_color_label": "Couleur",
155
179
  "snippet.colored_text_content_label": "Texte",
156
180
  "snippet.colored_text_content_placeholder": "Votre texte…",
@@ -197,6 +221,17 @@
197
221
  "snippet.saving_btn": "Enregistrement…",
198
222
  "snippet.detected_msg": "✓ Type détecté : {type}. Les champs ont été pré-remplis — modifiez puis cliquez sur Insérer pour remplacer la sélection.",
199
223
  "snippet.unknown_type_msg": "⚠ Le texte sélectionné ne correspond à aucun type de snippet reconnu. Choisissez un type ci-dessous — le snippet remplacera quand même la sélection.",
224
+ "snippet.emojis": "😀 Émojis",
225
+ "snippet.emoji_selected_label": "Émojis sélectionnés",
226
+ "snippet.emoji_clear_btn": "Vider",
227
+ "snippet.emoji_cat_smileys": "Smileys & personnes",
228
+ "snippet.emoji_cat_gestures": "Gestes & corps",
229
+ "snippet.emoji_cat_hearts": "Cœurs & étincelles",
230
+ "snippet.emoji_cat_objects": "Tech & outils",
231
+ "snippet.emoji_cat_office": "Bureau & documents",
232
+ "snippet.emoji_cat_symbols": "Symboles & flèches",
233
+ "snippet.emoji_search_placeholder": "Rechercher un émoji… (ex : coeur, etoile, fusee)",
234
+ "snippet.emoji_no_results": "Aucun émoji ne correspond à cette recherche.",
200
235
  "snippet.attachment": "📎 Pièce jointe",
201
236
  "snippet.attachment_help": "Cliquez sur <strong>Insérer</strong> pour choisir un fichier. Il sera téléversé dans le dossier <code>files/</code> et inséré sous forme de lien <i class=\"fa-solid fa-paperclip\"></i> dans votre document.",
202
237
  "snippet.attachment_alt": "Astuce : vous pouvez aussi glisser-déposer un fichier sur l'éditeur ou le coller depuis le presse-papiers.",
@@ -54,6 +54,8 @@
54
54
  <script defer src="/diagram-link-modal.js"></script>
55
55
  <script defer src="/new-folder-modal.js"></script>
56
56
  <script defer src="/new-doc-modal.js"></script>
57
+ <script defer src="/confirm-modal.js"></script>
58
+ <script defer src="/files-modal.js"></script>
57
59
  <script defer src="/boot.js"></script>
58
60
 
59
61
  <script>
@@ -309,6 +311,15 @@
309
311
  &#9671; Diagram
310
312
  </a>
311
313
 
314
+ <!-- Files -->
315
+ <button
316
+ onclick="openFilesModal()"
317
+ data-i18n="nav.files"
318
+ class="text-sm px-3 py-1.5 rounded-lg text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors font-medium"
319
+ >
320
+ &#128193; Files
321
+ </button>
322
+
312
323
  <!-- Admin link -->
313
324
  <a
314
325
  href="/admin"
@@ -1283,6 +1294,7 @@
1283
1294
  <option data-i18n="snippet.tree" value="tree">Tree</option>
1284
1295
  <option data-i18n="snippet.colored_section" value="colored-section">Colored section</option>
1285
1296
  <option data-i18n="snippet.colored_text" value="colored-text">Colored text</option>
1297
+ <option data-i18n="snippet.emojis" value="emojis">😀 Emojis</option>
1286
1298
  <option data-i18n="snippet.attachment" value="attachment">📎 File attachment</option>
1287
1299
  </select>
1288
1300
  </div>
@@ -1806,13 +1818,52 @@
1806
1818
  </div>
1807
1819
  </div>
1808
1820
 
1821
+ <!-- Panel: emojis -->
1822
+ <div id="snip-panel-emojis" class="hidden space-y-3">
1823
+ <div class="space-y-1.5">
1824
+ <label
1825
+ data-i18n="snippet.emoji_selected_label"
1826
+ class="block text-xs font-medium text-gray-500 dark:text-gray-400"
1827
+ >Selected emojis</label
1828
+ >
1829
+ <div class="flex gap-1">
1830
+ <input
1831
+ id="snip-emoji-string"
1832
+ type="text"
1833
+ oninput="snippetUpdatePreview()"
1834
+ class="flex-1 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"
1835
+ />
1836
+ <button
1837
+ type="button"
1838
+ id="snip-emoji-clear"
1839
+ onclick="emojiClear()"
1840
+ data-i18n="snippet.emoji_clear_btn"
1841
+ class="px-3 py-2 text-xs rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"
1842
+ >Clear</button>
1843
+ </div>
1844
+ </div>
1845
+ <div class="space-y-1.5">
1846
+ <input
1847
+ id="snip-emoji-search"
1848
+ type="text"
1849
+ data-i18n-placeholder="snippet.emoji_search_placeholder"
1850
+ placeholder="Search emojis… (e.g. heart, star, rocket)"
1851
+ 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"
1852
+ />
1853
+ </div>
1854
+ <div
1855
+ id="snip-emoji-grid"
1856
+ class="space-y-2 max-h-64 overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-lg p-2 bg-white dark:bg-gray-900"
1857
+ ></div>
1858
+ </div>
1859
+
1809
1860
  <!-- Panel: file attachment -->
1810
1861
  <div id="snip-panel-attachment" class="hidden space-y-3">
1811
1862
  <div class="rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 px-3 py-3 text-sm text-blue-900 dark:text-blue-100 space-y-2">
1812
- <p data-i18n="snippet.attachment_help">
1863
+ <p data-i18n-html="snippet.attachment_help">
1813
1864
  Click <strong>Insert</strong> to choose a file. It will be uploaded under the <code>files/</code> folder and inserted as a <i class="fa-solid fa-paperclip"></i> link in your document.
1814
1865
  </p>
1815
- <p data-i18n="snippet.attachment_alt" class="text-xs text-blue-700 dark:text-blue-300">
1866
+ <p data-i18n-html="snippet.attachment_alt" class="text-xs text-blue-700 dark:text-blue-300">
1816
1867
  Tip: you can also drag &amp; drop a file onto the editor, or paste it from the clipboard.
1817
1868
  </p>
1818
1869
  </div>
@@ -2519,5 +2570,62 @@
2519
2570
  </div>
2520
2571
  </div>
2521
2572
  </div>
2573
+
2574
+ <!-- ── Generic confirmation modal (used by showConfirm) ── -->
2575
+ <div
2576
+ id="confirm-modal"
2577
+ class="hidden fixed inset-0 z-[60] bg-black/50 flex items-center justify-center p-4"
2578
+ onclick="_confirmModalBackdrop(event)"
2579
+ >
2580
+ <div
2581
+ class="w-full max-w-md bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-2xl p-5 flex flex-col gap-3"
2582
+ onclick="event.stopPropagation()"
2583
+ >
2584
+ <h3 id="confirm-modal-title" class="font-semibold text-gray-900 dark:text-gray-100"></h3>
2585
+ <p id="confirm-modal-message" class="text-sm text-gray-700 dark:text-gray-200"></p>
2586
+ <p id="confirm-modal-detail" class="text-xs text-gray-500 dark:text-gray-400 italic break-all"></p>
2587
+ <div class="flex justify-end gap-2 mt-2">
2588
+ <button
2589
+ id="confirm-modal-cancel"
2590
+ type="button"
2591
+ class="text-sm px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
2592
+ ></button>
2593
+ <button
2594
+ id="confirm-modal-ok"
2595
+ type="button"
2596
+ class="text-sm px-4 py-1.5 rounded-lg text-white font-semibold transition-colors"
2597
+ ></button>
2598
+ </div>
2599
+ </div>
2600
+ </div>
2601
+
2602
+ <!-- ── Files modal ── -->
2603
+ <div
2604
+ id="files-modal"
2605
+ class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40"
2606
+ onclick="if(event.target===this)closeFilesModal()"
2607
+ >
2608
+ <div
2609
+ class="bg-white dark:bg-gray-900 rounded-xl shadow-xl w-[min(720px,92vw)] max-h-[85vh] flex flex-col border border-gray-200 dark:border-gray-800"
2610
+ >
2611
+ <div
2612
+ class="flex items-center justify-between px-5 py-3 border-b border-gray-200 dark:border-gray-800 shrink-0"
2613
+ >
2614
+ <h2 data-i18n="files.title" class="font-semibold text-base text-gray-900 dark:text-gray-100">
2615
+ &#128193; Files
2616
+ </h2>
2617
+ <button
2618
+ onclick="closeFilesModal()"
2619
+ data-i18n-title="common.close"
2620
+ class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 text-xl leading-none px-2"
2621
+ >
2622
+ &times;
2623
+ </button>
2624
+ </div>
2625
+ <div id="files-modal-body" class="flex-1 overflow-y-auto p-5">
2626
+ <p class="text-sm text-gray-400" data-i18n="common.loading">Loading…</p>
2627
+ </div>
2628
+ </div>
2629
+ </div>
2522
2630
  </body>
2523
2631
  </html>
@@ -1,6 +1,11 @@
1
1
  // ── Search + result highlighting ─────────────────────────────────────────────
2
2
 
3
3
  let searchTimer = null;
4
+ const METADATA_SEARCH_PREFIX = "metadata://";
5
+
6
+ function _isMetadataQuery(q) {
7
+ return typeof q === "string" && q.toLowerCase().startsWith(METADATA_SEARCH_PREFIX);
8
+ }
4
9
 
5
10
  function setupSearch() {
6
11
  ["header-search", "sidebar-search"].forEach((id) => {
@@ -20,23 +25,55 @@ function setupSearch() {
20
25
  searchQuery = "";
21
26
  searchResults = null;
22
27
  renderSidebar(allDocs);
28
+ if (window.refreshSearchInCurrentDoc) window.refreshSearchInCurrentDoc();
23
29
  return;
24
30
  }
25
31
  searchQuery = q;
26
- // Immediate client-side filter for snappy UX
27
- const local = allDocs.filter(
28
- (d) =>
29
- d.title.toLowerCase().includes(q.toLowerCase()) ||
30
- d.category.toLowerCase().includes(q.toLowerCase()),
31
- );
32
- searchResults = local;
33
- renderSidebar(local);
32
+ if (_isMetadataQuery(q)) {
33
+ // Title/category can't match a metadata:// query — skip the local
34
+ // filter and wait for the server response.
35
+ searchResults = [];
36
+ renderSidebar([]);
37
+ } else {
38
+ // Immediate client-side filter for snappy UX
39
+ const local = allDocs.filter(
40
+ (d) =>
41
+ d.title.toLowerCase().includes(q.toLowerCase()) ||
42
+ d.category.toLowerCase().includes(q.toLowerCase()),
43
+ );
44
+ searchResults = local;
45
+ renderSidebar(local);
46
+ }
47
+ // Refresh highlights inside the currently open doc
48
+ if (window.refreshSearchInCurrentDoc) window.refreshSearchInCurrentDoc();
34
49
  // Then full-text search from server
35
50
  searchTimer = setTimeout(() => doSearch(q), 350);
36
51
  });
37
52
  });
38
53
  }
39
54
 
55
+ // Programmatic trigger used by the Files modal after a replace/delete.
56
+ // Fills both search inputs and runs the server search immediately (no debounce).
57
+ function runSearchImmediate(q) {
58
+ const trimmed = (q || "").trim();
59
+ ["header-search", "sidebar-search"].forEach((id) => {
60
+ const el = document.getElementById(id);
61
+ if (el) el.value = trimmed;
62
+ });
63
+ clearTimeout(searchTimer);
64
+ if (!trimmed) {
65
+ searchQuery = "";
66
+ searchResults = null;
67
+ renderSidebar(allDocs);
68
+ return;
69
+ }
70
+ searchQuery = trimmed;
71
+ searchResults = [];
72
+ renderSidebar([]);
73
+ doSearch(trimmed);
74
+ }
75
+ window.runSearchImmediate = runSearchImmediate;
76
+
40
77
  async function doSearch(q) {
41
78
  try {
42
79
  const results = await fetch(