living-documentation 3.7.0 → 4.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.

Potentially problematic release.


This version of living-documentation might be problematic. Click here for more details.

@@ -210,6 +210,47 @@
210
210
  }
211
211
  #rh-label-rotate:active { cursor: grabbing; }
212
212
  .dark #rh-label-rotate { background: #374151; }
213
+
214
+ /* Toast notifications */
215
+ #toastContainer {
216
+ position: fixed;
217
+ bottom: 1.5rem;
218
+ right: 1.5rem;
219
+ display: flex;
220
+ flex-direction: column-reverse;
221
+ gap: 0.5rem;
222
+ z-index: 1000;
223
+ pointer-events: none;
224
+ }
225
+ .ld-toast {
226
+ pointer-events: all;
227
+ padding: 0.6rem 1rem;
228
+ border-radius: 0.5rem;
229
+ font-size: 0.8rem;
230
+ font-weight: 500;
231
+ box-shadow: 0 4px 16px rgba(0,0,0,0.18);
232
+ cursor: pointer;
233
+ opacity: 0;
234
+ transform: translateY(0.5rem);
235
+ transition: opacity 0.2s ease, transform 0.2s ease;
236
+ background: #1c1917;
237
+ color: #fafaf9;
238
+ border: 1px solid #292524;
239
+ }
240
+ .dark .ld-toast {
241
+ background: #fafaf9;
242
+ color: #1c1917;
243
+ border-color: #e7e5e4;
244
+ }
245
+ .ld-toast--error {
246
+ background: #7f1d1d;
247
+ color: #fef2f2;
248
+ border-color: #991b1b;
249
+ }
250
+ .ld-toast--visible {
251
+ opacity: 1;
252
+ transform: translateY(0);
253
+ }
213
254
  </style>
214
255
  </head>
215
256
  <body
@@ -224,10 +265,10 @@
224
265
  <header
225
266
  class="flex items-center gap-1 px-2 h-12 shrink-0 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 shadow-sm z-10"
226
267
  >
227
- <a
228
- href="/"
268
+ <button
269
+ onclick="history.back()"
229
270
  class="tool-btn !w-auto px-2 text-xs font-medium text-gray-500 dark:text-gray-400"
230
- >← Docs</a
271
+ >← Back</button
231
272
  >
232
273
  <div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
233
274
  <button id="btnSidebar" class="tool-btn" title="Mes diagrammes">
@@ -358,6 +399,17 @@
358
399
  >
359
400
  T
360
401
  </button>
402
+ <button
403
+ id="toolImage"
404
+ class="tool-btn"
405
+ title="Image — double-clic sur le canvas pour choisir un fichier, ou coller (⌘V) depuis le presse-papier"
406
+ >
407
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
408
+ <rect x="1" y="1" width="12" height="12" rx="1.5"/>
409
+ <circle cx="4.5" cy="4.5" r="1.2"/>
410
+ <path d="M1 9.5 L4 6.5 L6.5 9 L9 7 L13 10.5"/>
411
+ </svg>
412
+ </button>
361
413
  <div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
362
414
 
363
415
  <button
@@ -538,7 +590,14 @@
538
590
  style="bottom: -5px; right: -5px; cursor: se-resize"
539
591
  ></div>
540
592
  <div id="rh-rotate" title="Rotation forme">↻</div>
541
- <div id="rh-label-rotate" title="Rotation texte" style="left: 0; top: -28px;">T↻</div>
593
+ <div id="rh-label-rotate" title="Rotation texte" style="left: 0; top: -28px;">
594
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
595
+ <g transform="rotate(25,5,5)">
596
+ <line x1="2.5" y1="3" x2="7.5" y2="3"/>
597
+ <line x1="5" y1="3" x2="5" y2="8"/>
598
+ </g>
599
+ </svg>
600
+ </div>
542
601
  </div>
543
602
 
544
603
  <!-- Node panel -->
@@ -626,6 +685,16 @@
626
685
  >
627
686
 
628
687
  </button>
688
+ <button
689
+ id="btnNodeLink"
690
+ class="tool-btn !w-6 !h-6"
691
+ title="Ajouter / modifier un lien"
692
+ >
693
+ <svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
694
+ <path d="M5 7.5a3.5 3.5 0 0 0 5 0l1.5-1.5a3.5 3.5 0 0 0-5-5L5 2.5"/>
695
+ <path d="M8 5.5a3.5 3.5 0 0 0-5 0L1.5 7a3.5 3.5 0 0 0 5 5L8 10.5"/>
696
+ </svg>
697
+ </button>
629
698
  <div class="panel-sep"></div>
630
699
  <button
631
700
  id="btnNodeFontDecrease"
@@ -814,40 +883,125 @@
814
883
 
815
884
  <div class="panel-sep"></div>
816
885
 
817
- <!-- Stamp: copy color -->
886
+ <!-- Stamp: copy color (goutte) -->
818
887
  <button
819
888
  id="btnStampColor"
820
889
  class="tool-btn !w-7 !h-6"
821
890
  title="Tampon couleur — sélectionner les cibles, cliquer ici, puis cliquer la source"
822
891
  >
823
892
  <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
824
- <rect x="1" y="1" width="6" height="6" rx="1" fill="currentColor" stroke="none" opacity="0.5"/>
825
- <rect x="7" y="7" width="6" height="6" rx="1" fill="currentColor" stroke="none" opacity="0.5"/>
826
- <path d="M7 3.5h4M3.5 7v4"/>
827
- <circle cx="10.5" cy="10.5" r="1" fill="currentColor" stroke="none"/>
893
+ <path d="M7 2 C7 2 3 6.5 3 9 a4 4 0 0 0 8 0 C11 6.5 7 2 7 2 Z" fill="currentColor" fill-opacity="0.25"/>
828
894
  </svg>
829
895
  </button>
830
896
 
831
- <!-- Stamp: copy rotation -->
897
+ <!-- Stamp: copy font size -->
898
+ <button
899
+ id="btnStampFontSize"
900
+ class="tool-btn !w-7 !h-6 font-mono text-xs font-bold"
901
+ title="Tampon taille police — sélectionner les cibles, cliquer ici, puis cliquer la source"
902
+ >Aa</button>
903
+
904
+ <div class="panel-sep"></div>
905
+
906
+ <!-- Rotation anti-horaire 10° -->
907
+ <button
908
+ id="btnRotateCCW"
909
+ class="tool-btn !w-7 !h-6"
910
+ title="Rotation anti-horaire 10°"
911
+ style="font-size:15px; line-height:1;"
912
+ >↺</button>
913
+
914
+ <!-- Rotation horaire 10° -->
832
915
  <button
833
- id="btnStampRotation"
916
+ id="btnRotateCW"
834
917
  class="tool-btn !w-7 !h-6"
835
- title="Tampon rotation — sélectionner les cibles, cliquer ici, puis cliquer la source"
918
+ title="Rotation horaire 10°"
919
+ style="font-size:15px; line-height:1;"
920
+ >↻</button>
921
+
922
+ <div class="panel-sep"></div>
923
+
924
+ <!-- Copy as PNG -->
925
+ <button
926
+ id="btnCopyPng"
927
+ class="tool-btn !h-6 px-1.5 font-mono text-xs font-semibold flex items-center gap-0.5"
928
+ title="Copier la sélection en PNG (⌘⇧C)"
836
929
  >
837
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
838
- <path d="M2 7a5 5 0 1 0 1.5-3.5"/>
839
- <polyline points="1,1 1.5,3.5 4,3"/>
840
- <line x1="7" y1="4" x2="7" y2="7" stroke-width="1.8"/>
841
- <line x1="7" y1="7" x2="9" y2="7" stroke-width="1.8"/>
930
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
931
+ <line x1="5" y1="7" x2="5" y2="1"/>
932
+ <polyline points="2,4 5,1 8,4"/>
933
+ <line x1="1" y1="9" x2="9" y2="9"/>
842
934
  </svg>
935
+ PNG
843
936
  </button>
844
937
 
845
- <!-- Stamp: copy font size -->
938
+ <div class="panel-sep"></div>
939
+
940
+ <!-- Group -->
846
941
  <button
847
- id="btnStampFontSize"
848
- class="tool-btn !w-7 !h-6 font-mono text-xs font-bold"
849
- title="Tampon taille police — sélectionner les cibles, cliquer ici, puis cliquer la source"
850
- >Aa</button>
942
+ id="btnGroup"
943
+ class="tool-btn !w-8 !h-7"
944
+ title="Grouper la sélection"
945
+ >
946
+ <svg width="22" height="14" viewBox="0 0 22 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
947
+ <rect x="0.7" y="1" width="20.6" height="12" rx="2" stroke-dasharray="2.5,1.5"/>
948
+ <rect x="4" y="4.5" width="3" height="5" rx="1" fill="currentColor"/>
949
+ <rect x="15" y="4.5" width="3" height="5" rx="1" fill="currentColor"/>
950
+ </svg>
951
+ </button>
952
+
953
+ <!-- Ungroup -->
954
+ <button
955
+ id="btnUngroup"
956
+ class="tool-btn !w-8 !h-7"
957
+ title="Dégrouper"
958
+ >
959
+ <svg width="26" height="14" viewBox="0 0 26 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
960
+ <rect x="0.7" y="1" width="9" height="12" rx="2" stroke-dasharray="2.5,1.5"/>
961
+ <rect x="3" y="4.5" width="3" height="5" rx="1" fill="currentColor"/>
962
+ <rect x="16.3" y="1" width="9" height="12" rx="2" stroke-dasharray="2.5,1.5"/>
963
+ <rect x="19" y="4.5" width="3" height="5" rx="1" fill="currentColor"/>
964
+ </svg>
965
+ </button>
966
+ </div>
967
+
968
+ <!-- Link panel -->
969
+ <div id="linkPanel" class="float-panel hidden" style="min-width:260px; flex-direction:column; align-items:stretch; gap:0.5rem; padding:0.75rem;">
970
+ <div class="text-xs font-semibold text-gray-500 dark:text-gray-400 mb-1">Lien sur cette forme</div>
971
+
972
+ <label class="flex items-center gap-2 text-xs cursor-pointer">
973
+ <input type="radio" name="linkType" id="linkTypeUrl" value="url" class="accent-orange-500"/>
974
+ URL externe
975
+ </label>
976
+ <div id="linkUrlRow">
977
+ <input id="linkUrlInput" type="url" placeholder="https://…"
978
+ class="w-full text-xs border border-gray-300 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-orange-400"/>
979
+ </div>
980
+
981
+ <label class="flex items-center gap-2 text-xs cursor-pointer">
982
+ <input type="radio" name="linkType" id="linkTypeDiagram" value="diagram" class="accent-orange-500"/>
983
+ Diagramme existant
984
+ </label>
985
+ <div id="linkDiagramRow" class="hidden">
986
+ <select id="linkDiagramSelect"
987
+ class="w-full text-xs border border-gray-300 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-orange-400">
988
+ </select>
989
+ </div>
990
+
991
+ <label class="flex items-center gap-2 text-xs cursor-pointer">
992
+ <input type="radio" name="linkType" id="linkTypeNew" value="new" class="accent-orange-500"/>
993
+ Nouveau diagramme
994
+ </label>
995
+ <div id="linkNewRow" class="hidden">
996
+ <input id="linkNewName" type="text" placeholder="Nom du diagramme…"
997
+ class="w-full text-xs border border-gray-300 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-orange-400"/>
998
+ </div>
999
+
1000
+ <div class="flex gap-1 mt-1">
1001
+ <button id="btnLinkSave" class="flex-1 text-xs bg-orange-500 hover:bg-orange-600 text-white rounded px-2 py-1">Enregistrer</button>
1002
+ <button id="btnLinkRemove" class="text-xs border border-gray-300 dark:border-gray-600 rounded px-2 py-1 hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-600">Retirer</button>
1003
+ <button id="btnLinkCancel" class="text-xs border border-gray-300 dark:border-gray-600 rounded px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-700">✕</button>
1004
+ </div>
851
1005
  </div>
852
1006
 
853
1007
  <!-- Edge panel -->
@@ -969,6 +1123,7 @@
969
1123
  </div>
970
1124
  </div>
971
1125
 
1126
+ <div id="toastContainer"></div>
972
1127
  <script type="module" src="/diagram/main.js"></script>
973
1128
  </body>
974
1129
  </html>
@@ -17,6 +17,8 @@
17
17
 
18
18
  <!-- WordCloud2.js — vendored from npm, no CDN dependency -->
19
19
  <script src="/vendor/wordcloud2.js"></script>
20
+ <!-- Word Cloud logic — stop words, browser, rendering -->
21
+ <script src="/wordcloud.js"></script>
20
22
 
21
23
  <script>
22
24
  tailwind.config = {
@@ -281,7 +283,7 @@
281
283
  <article id="doc-view" class="hidden max-w-4xl mx-auto px-6 py-8">
282
284
  <!-- Doc header -->
283
285
  <header
284
- class="mb-8 pb-6 border-b border-gray-100 dark:border-gray-800"
286
+ class="sticky top-0 z-10 bg-gray-50 dark:bg-gray-950 -mx-6 px-6 pt-8 mb-8 pb-6 border-b border-gray-300 dark:border-gray-800"
285
287
  >
286
288
  <div class="flex items-start justify-between gap-4 flex-wrap">
287
289
  <div class="flex-1 min-w-0">
@@ -391,6 +393,73 @@
391
393
  &#10005; Close
392
394
  </button>
393
395
  </div>
396
+ <!-- Search root toolbar -->
397
+ <div class="border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shrink-0">
398
+ <!-- Row 1: path + browse + launch -->
399
+ <div class="flex items-center gap-3 px-6 py-3">
400
+ <label class="text-xs font-medium text-gray-500 dark:text-gray-400 shrink-0">Search root</label>
401
+ <input
402
+ id="wc-root"
403
+ type="text"
404
+ readonly
405
+ spellcheck="false"
406
+ class="flex-1 px-3 py-1.5 text-sm rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono cursor-default focus:outline-none"
407
+ />
408
+ <button
409
+ onclick="wcToggleBrowser()"
410
+ 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 shrink-0"
411
+ >
412
+ &#128193; Browse
413
+ </button>
414
+ <button
415
+ onclick="launchWordCloud()"
416
+ class="text-sm px-4 py-1.5 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold transition-colors shrink-0"
417
+ >
418
+ &#9654; Launch
419
+ </button>
420
+ </div>
421
+ <!-- Row 2: extension checkboxes -->
422
+ <div class="flex items-center gap-x-4 gap-y-2 px-6 pb-3 flex-wrap">
423
+ <span class="text-xs font-medium text-gray-500 dark:text-gray-400 shrink-0">Extensions</span>
424
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="md" checked /> .md</label>
425
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="txt" /> .txt</label>
426
+ <span class="text-gray-200 dark:text-gray-700 text-xs">|</span>
427
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="ts" /> .ts</label>
428
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="tsx" /> .tsx</label>
429
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="js" /> .js</label>
430
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="jsx" /> .jsx</label>
431
+ <span class="text-gray-200 dark:text-gray-700 text-xs">|</span>
432
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="java" /> .java</label>
433
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="kt" /> .kt</label>
434
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="py" /> .py</label>
435
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="go" /> .go</label>
436
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="rs" /> .rs</label>
437
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="cs" /> .cs</label>
438
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="swift" /> .swift</label>
439
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="rb" /> .rb</label>
440
+ <span class="text-gray-200 dark:text-gray-700 text-xs">|</span>
441
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="html" /> .html</label>
442
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="css" /> .css</label>
443
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="scss" /> .scss</label>
444
+ <span class="text-gray-200 dark:text-gray-700 text-xs">|</span>
445
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="yml" /> .yml</label>
446
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="yaml" /> .yaml</label>
447
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="json" /> .json</label>
448
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="xml" /> .xml</label>
449
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="toml" /> .toml</label>
450
+ </div>
451
+ <!-- Row 3: inline folder browser (collapsed by default) -->
452
+ <div id="wc-browser" class="hidden border-t border-gray-200 dark:border-gray-700">
453
+ <div 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">
454
+ <button id="wc-browse-up" onclick="wcBrowseUp()"
455
+ class="text-xs text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-30 disabled:pointer-events-none shrink-0">
456
+ &#8593; Up
457
+ </button>
458
+ <span id="wc-browse-path" class="font-mono text-xs text-gray-400 dark:text-gray-500 truncate flex-1 text-right"></span>
459
+ </div>
460
+ <div id="wc-browse-list" class="divide-y divide-gray-100 dark:divide-gray-800 max-h-52 overflow-y-auto"></div>
461
+ </div>
462
+ </div>
394
463
  <div
395
464
  id="wc-body"
396
465
  class="flex-1 relative overflow-hidden flex items-center justify-center"
@@ -419,6 +488,7 @@
419
488
  applyDarkMode(loadDarkPref());
420
489
  setupDarkToggle();
421
490
  setupSearch();
491
+ wcRestorePrefs();
422
492
  await loadConfig();
423
493
  await loadDocuments();
424
494
 
@@ -685,7 +755,10 @@
685
755
  }
686
756
 
687
757
  // ── Edit mode ──────────────────────────────────────────────
758
+ let _editScrollTop = 0;
759
+
688
760
  function enterEditMode() {
761
+ _editScrollTop = document.getElementById("content-area").scrollTop;
689
762
  const editor = document.getElementById("doc-editor");
690
763
  editor.value = currentDocContent;
691
764
  document.getElementById("doc-content").classList.add("hidden");
@@ -704,6 +777,7 @@
704
777
  document.getElementById("edit-actions").classList.add("hidden");
705
778
  document.getElementById("view-actions").classList.remove("hidden");
706
779
  document.getElementById("edit-save-msg").textContent = "";
780
+ document.getElementById("content-area").scrollTop = _editScrollTop;
707
781
  }
708
782
 
709
783
  async function handleEditorPaste(e) {
@@ -837,244 +911,6 @@
837
911
  .replace(/'/g, "&#39;");
838
912
  }
839
913
 
840
- // ── Word Cloud ─────────────────────────────────────────────
841
- const WC_STOP_WORDS = new Set([
842
- // English
843
- "the",
844
- "and",
845
- "for",
846
- "are",
847
- "but",
848
- "not",
849
- "you",
850
- "all",
851
- "this",
852
- "that",
853
- "with",
854
- "have",
855
- "from",
856
- "they",
857
- "will",
858
- "one",
859
- "been",
860
- "can",
861
- "has",
862
- "was",
863
- "more",
864
- "also",
865
- "when",
866
- "there",
867
- "their",
868
- "what",
869
- "about",
870
- "which",
871
- "would",
872
- "into",
873
- "than",
874
- "then",
875
- "each",
876
- "just",
877
- "over",
878
- "after",
879
- "such",
880
- "here",
881
- "its",
882
- "your",
883
- "our",
884
- "some",
885
- "were",
886
- "very",
887
- "only",
888
- "out",
889
- "had",
890
- "she",
891
- "his",
892
- "her",
893
- "him",
894
- "who",
895
- "how",
896
- "any",
897
- "other",
898
- "these",
899
- "those",
900
- "being",
901
- "may",
902
- "use",
903
- "used",
904
- "using",
905
- "should",
906
- "could",
907
- "would",
908
- "shall",
909
- "must",
910
- "need",
911
- "via",
912
- "per",
913
- "like",
914
- // French
915
- "les",
916
- "des",
917
- "une",
918
- "pour",
919
- "pas",
920
- "sur",
921
- "par",
922
- "est",
923
- "qui",
924
- "que",
925
- "dans",
926
- "avec",
927
- "sont",
928
- "plus",
929
- "tout",
930
- "aux",
931
- "mais",
932
- "comme",
933
- "vous",
934
- "nous",
935
- "leur",
936
- "lui",
937
- "elle",
938
- "ils",
939
- "elles",
940
- "ces",
941
- "ses",
942
- "mon",
943
- "ton",
944
- "son",
945
- "mes",
946
- "tes",
947
- "ainsi",
948
- "donc",
949
- "alors",
950
- "car",
951
- "peut",
952
- "fait",
953
- "encore",
954
- "bien",
955
- "aussi",
956
- "très",
957
- "même",
958
- "entre",
959
- "vers",
960
- "dont",
961
- "sans",
962
- "sous",
963
- ]);
964
-
965
- async function openWordCloud() {
966
- const overlay = document.getElementById("wc-overlay");
967
- const status = document.getElementById("wc-status");
968
- const canvas = document.getElementById("wc-canvas");
969
- overlay.classList.remove("hidden");
970
- status.textContent = "Loading documents…";
971
- status.classList.remove("hidden");
972
- canvas.classList.add("hidden");
973
-
974
- try {
975
- const docs = allDocs.length
976
- ? allDocs
977
- : await fetch("/api/documents").then((r) => r.json());
978
- status.textContent = `Analyzing ${docs.length} documents…`;
979
-
980
- const results = await Promise.all(
981
- docs.map((doc) =>
982
- fetch("/api/documents/" + doc.id)
983
- .then((r) => (r.ok ? r.json() : null))
984
- .catch(() => null),
985
- ),
986
- );
987
-
988
- const freq = {};
989
- for (const doc of results) {
990
- console.log("doc = ", doc);
991
- if (!doc?.content) continue;
992
- for (const w of extractWordsFromMarkdown(doc.content)) {
993
- freq[w] = (freq[w] || 0) + 1;
994
- }
995
- }
996
-
997
- const list = Object.entries(freq)
998
- .filter(([, n]) => n >= 2)
999
- .sort((a, b) => b[1] - a[1])
1000
- .slice(0, 150);
1001
-
1002
- if (!list.length) {
1003
- status.textContent = "Not enough words found.";
1004
- return;
1005
- }
1006
-
1007
- renderWordCloud(list);
1008
- } catch (err) {
1009
- status.textContent = "Error: " + err.message;
1010
- }
1011
- }
1012
-
1013
- function extractWordsFromMarkdown(text) {
1014
- return text
1015
- .replace(/```[\s\S]*?```/g, "")
1016
- .replace(/`[^`\n]+`/g, "")
1017
- .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
1018
- .replace(/https?:\/\/\S+/g, "")
1019
- .replace(/[#*_~>`|!\[\](){}=\-+]/g, " ")
1020
- .toLowerCase()
1021
- .split(/[^a-zàâäéèêëïîôùûü']+/)
1022
- .map((w) => w.replace(/^'+|'+$/g, ""))
1023
- .filter((w) => w.length > 3 && !WC_STOP_WORDS.has(w));
1024
- }
1025
-
1026
- function renderWordCloud(list) {
1027
- const canvas = document.getElementById("wc-canvas");
1028
- const body = document.getElementById("wc-body");
1029
- canvas.width = body.clientWidth;
1030
- canvas.height = body.clientHeight;
1031
-
1032
- const isDark = document.documentElement.classList.contains("dark");
1033
- const colors = isDark
1034
- ? [
1035
- "#60a5fa",
1036
- "#34d399",
1037
- "#f9a8d4",
1038
- "#a78bfa",
1039
- "#fbbf24",
1040
- "#6ee7b7",
1041
- "#93c5fd",
1042
- "#fb923c",
1043
- ]
1044
- : [
1045
- "#1d4ed8",
1046
- "#047857",
1047
- "#7c3aed",
1048
- "#b45309",
1049
- "#be123c",
1050
- "#0369a1",
1051
- "#4338ca",
1052
- "#c2410c",
1053
- ];
1054
-
1055
- const maxFreq = list[0][1];
1056
- const wordList = list.map(([w, n]) => [
1057
- w,
1058
- Math.max(10, Math.round((72 * n) / maxFreq)),
1059
- ]);
1060
-
1061
- document.getElementById("wc-status").classList.add("hidden");
1062
- canvas.classList.remove("hidden");
1063
-
1064
- WordCloud(canvas, {
1065
- list: wordList,
1066
- gridSize: Math.round((8 * canvas.width) / 1024),
1067
- fontFamily: "ui-sans-serif, system-ui, sans-serif",
1068
- color: () => colors[Math.floor(Math.random() * colors.length)],
1069
- backgroundColor: isDark ? "#030712" : "#ffffff",
1070
- rotateRatio: 0.3,
1071
- minSize: 10,
1072
- });
1073
- }
1074
-
1075
- function closeWordCloud() {
1076
- document.getElementById("wc-overlay").classList.add("hidden");
1077
- }
1078
914
 
1079
915
  // Browser back/forward
1080
916
  window.addEventListener("popstate", (e) => {