living-ai-documentation 1.8.0 → 1.10.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.
@@ -269,6 +269,28 @@
269
269
  "snippet.collapsible_summary_label": "Summary title",
270
270
  "snippet.collapsible_details_placeholder": "Details",
271
271
  "snippet.collapsible_summary_value": "Details",
272
+ "snippet.collapsible_body_label": "Body Markdown",
273
+ "snippet.collapsible_body_placeholder": "## Title\n\nText",
274
+ "snippet.heading_1": "🇭1 Heading level 1",
275
+ "snippet.heading_2": "🇭2 Heading level 2",
276
+ "snippet.heading_3": "🇭3 Heading level 3",
277
+ "snippet.heading_4": "🇭4 Heading level 4",
278
+ "snippet.heading_text_label": "Heading text",
279
+ "snippet.heading_text_placeholder": "My heading",
280
+ "snippet.inline_edit_btn_heading_1": "Edit heading level 1",
281
+ "snippet.inline_edit_btn_heading_2": "Edit heading level 2",
282
+ "snippet.inline_edit_btn_heading_3": "Edit heading level 3",
283
+ "snippet.inline_edit_btn_heading_4": "Edit heading level 4",
284
+ "snippet.picker_search_placeholder": "Search a snippet…",
285
+ "snippet.picker_back": "Back",
286
+ "snippet.picker_no_results": "No matching snippet.",
287
+ "snippet.picker_cat_structure": "Structure",
288
+ "snippet.picker_cat_lists": "Lists",
289
+ "snippet.picker_cat_lists_code_data": "Lists, code & data",
290
+ "snippet.picker_cat_rich_text": "Rich text",
291
+ "snippet.picker_cat_code_data": "Code & data",
292
+ "snippet.picker_cat_links_media": "Links & media",
293
+ "snippet.picker_cat_advanced": "Advanced",
272
294
  "snippet.link_text_label": "Link text",
273
295
  "snippet.link_text_placeholder": "My link",
274
296
  "snippet.link_url_label": "URL",
@@ -269,6 +269,28 @@
269
269
  "snippet.collapsible_summary_label": "Titre du résumé",
270
270
  "snippet.collapsible_details_placeholder": "Détails",
271
271
  "snippet.collapsible_summary_value": "Détails",
272
+ "snippet.collapsible_body_label": "Contenu du bloc (Markdown)",
273
+ "snippet.collapsible_body_placeholder": "## Titre\n\nTexte",
274
+ "snippet.heading_1": "🇭1 Titre niveau 1",
275
+ "snippet.heading_2": "🇭2 Titre niveau 2",
276
+ "snippet.heading_3": "🇭3 Titre niveau 3",
277
+ "snippet.heading_4": "🇭4 Titre niveau 4",
278
+ "snippet.heading_text_label": "Texte du titre",
279
+ "snippet.heading_text_placeholder": "Mon titre",
280
+ "snippet.inline_edit_btn_heading_1": "Éditer le titre niveau 1",
281
+ "snippet.inline_edit_btn_heading_2": "Éditer le titre niveau 2",
282
+ "snippet.inline_edit_btn_heading_3": "Éditer le titre niveau 3",
283
+ "snippet.inline_edit_btn_heading_4": "Éditer le titre niveau 4",
284
+ "snippet.picker_search_placeholder": "Rechercher un snippet…",
285
+ "snippet.picker_back": "Retour",
286
+ "snippet.picker_no_results": "Aucun snippet ne correspond.",
287
+ "snippet.picker_cat_structure": "Structure",
288
+ "snippet.picker_cat_lists": "Listes",
289
+ "snippet.picker_cat_lists_code_data": "Listes, code & données",
290
+ "snippet.picker_cat_rich_text": "Texte enrichi",
291
+ "snippet.picker_cat_code_data": "Code & données",
292
+ "snippet.picker_cat_links_media": "Liens & médias",
293
+ "snippet.picker_cat_advanced": "Avancés",
272
294
  "snippet.link_text_label": "Texte du lien",
273
295
  "snippet.link_text_placeholder": "Mon lien",
274
296
  "snippet.link_url_label": "URL",
@@ -1429,7 +1429,7 @@
1429
1429
  >
1430
1430
  <div
1431
1431
  id="snippet-modal-card"
1432
- class="bg-white dark:bg-gray-900 rounded-xl shadow-xl w-full max-w-lg mx-4 p-6 space-y-5 max-h-[90vh] overflow-y-auto"
1432
+ class="bg-white dark:bg-gray-900 rounded-xl shadow-xl w-full max-w-6xl mx-4 p-6 space-y-5 max-h-[90vh] overflow-y-auto"
1433
1433
  >
1434
1434
  <h3 class="text-base font-semibold text-gray-900 dark:text-gray-50">
1435
1435
  <span id="snippet-modal-title" data-i18n="snippet.modal_title">🧩 Insert a snippet</span>
@@ -1440,8 +1440,38 @@
1440
1440
  class="hidden rounded-lg px-3 py-2 text-xs"
1441
1441
  ></div>
1442
1442
 
1443
- <!-- Snippet type selector -->
1444
- <div class="space-y-1.5">
1443
+ <!-- Snippet picker (insertion mode) -->
1444
+ <div id="snippet-picker" class="hidden space-y-4">
1445
+ <input
1446
+ id="snippet-picker-search"
1447
+ type="text"
1448
+ data-i18n-placeholder="snippet.picker_search_placeholder"
1449
+ placeholder="Search a snippet…"
1450
+ oninput="snippetPickerSearchChanged()"
1451
+ onkeydown="snippetPickerSearchKeydown(event)"
1452
+ 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"
1453
+ />
1454
+ <div id="snippet-picker-categories" class="space-y-4 max-h-[60vh] overflow-y-auto pr-1"></div>
1455
+ <div
1456
+ id="snippet-picker-no-results"
1457
+ data-i18n="snippet.picker_no_results"
1458
+ class="hidden text-center text-sm text-gray-500 dark:text-gray-400 py-4"
1459
+ >No matching snippet.</div>
1460
+ </div>
1461
+
1462
+ <!-- Back to picker (panel mode after picker selection) -->
1463
+ <button
1464
+ type="button"
1465
+ id="snippet-picker-back"
1466
+ onclick="snippetPickerBack()"
1467
+ class="hidden inline-flex items-center gap-2 text-xs font-medium text-blue-700 dark:text-blue-300 hover:underline"
1468
+ >
1469
+ <i class="fa-solid fa-arrow-left" aria-hidden="true"></i>
1470
+ <span data-i18n="snippet.picker_back">Back</span>
1471
+ </button>
1472
+
1473
+ <!-- Snippet type selector (hidden in picker UX, kept for state binding) -->
1474
+ <div id="snippet-type-wrapper" class="hidden space-y-1.5">
1445
1475
  <label
1446
1476
  data-i18n="snippet.type_label"
1447
1477
  class="block text-xs font-medium text-gray-500 dark:text-gray-400"
@@ -1453,6 +1483,10 @@
1453
1483
  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"
1454
1484
  >
1455
1485
  <option data-i18n="snippet.diagram" value="diagram" selected>Diagram</option>
1486
+ <option data-i18n="snippet.heading_1" value="heading-1">Title level 1</option>
1487
+ <option data-i18n="snippet.heading_2" value="heading-2">Title level 2</option>
1488
+ <option data-i18n="snippet.heading_3" value="heading-3">Title level 3</option>
1489
+ <option data-i18n="snippet.heading_4" value="heading-4">Title level 4</option>
1456
1490
  <option data-i18n="snippet.collapsible" value="collapsible">Collapsible block (details)</option>
1457
1491
  <option data-i18n="snippet.link" value="link">Link</option>
1458
1492
  <option data-i18n="snippet.link_doc" value="doc-link">Link to document</option>
@@ -1476,6 +1510,26 @@
1476
1510
  </select>
1477
1511
  </div>
1478
1512
 
1513
+ <!-- Panel: heading (shared by heading-1..heading-4) -->
1514
+ <div id="snip-panel-heading" class="hidden space-y-3">
1515
+ <div class="space-y-1.5">
1516
+ <label
1517
+ data-i18n="snippet.heading_text_label"
1518
+ for="snip-heading-content"
1519
+ class="block text-xs font-medium text-gray-500 dark:text-gray-400"
1520
+ >Title text</label
1521
+ >
1522
+ <input
1523
+ id="snip-heading-content"
1524
+ type="text"
1525
+ oninput="snippetUpdatePreview()"
1526
+ data-i18n-placeholder="snippet.heading_text_placeholder"
1527
+ placeholder="My title"
1528
+ 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"
1529
+ />
1530
+ </div>
1531
+ </div>
1532
+
1479
1533
  <!-- Panel: collapsible -->
1480
1534
  <div id="snip-panel-collapsible" class="space-y-3">
1481
1535
  <div class="space-y-1.5">
@@ -1492,6 +1546,22 @@
1492
1546
  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"
1493
1547
  />
1494
1548
  </div>
1549
+ <div class="space-y-1.5">
1550
+ <label
1551
+ data-i18n="snippet.collapsible_body_label"
1552
+ for="snip-collapsible-body"
1553
+ class="block text-xs font-medium text-gray-500 dark:text-gray-400"
1554
+ >Body Markdown</label
1555
+ >
1556
+ <textarea
1557
+ id="snip-collapsible-body"
1558
+ rows="6"
1559
+ oninput="snippetUpdatePreview()"
1560
+ data-i18n-placeholder="snippet.collapsible_body_placeholder"
1561
+ placeholder="## Titre&#10;&#10;Texte"
1562
+ class="w-full px-3 py-2 text-sm font-mono 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"
1563
+ ></textarea>
1564
+ </div>
1495
1565
  </div>
1496
1566
 
1497
1567
  <!-- Panel: link -->
@@ -19,6 +19,10 @@ const _INLINE_SNIPPET_TYPES = new Set([
19
19
  "separator",
20
20
  "ordered-list",
21
21
  "unordered-list",
22
+ "heading-1",
23
+ "heading-2",
24
+ "heading-3",
25
+ "heading-4",
22
26
  ]);
23
27
 
24
28
  const _INLINE_EDIT_AFFORDANCE_BY_TYPE = {
@@ -37,6 +41,10 @@ const _INLINE_EDIT_AFFORDANCE_BY_TYPE = {
37
41
  "anchor-doc-link": { labelKey: "snippet.inline_edit_btn_anchor_doc_link", iconClass: "fa-solid fa-anchor" },
38
42
  image: { labelKey: "snippet.inline_edit_btn_image", iconClass: "fa-solid fa-image" },
39
43
  separator: { labelKey: "snippet.inline_edit_btn_separator", iconClass: "fa-solid fa-minus" },
44
+ "heading-1": { labelKey: "snippet.inline_edit_btn_heading_1", iconClass: "fa-solid fa-heading" },
45
+ "heading-2": { labelKey: "snippet.inline_edit_btn_heading_2", iconClass: "fa-solid fa-heading" },
46
+ "heading-3": { labelKey: "snippet.inline_edit_btn_heading_3", iconClass: "fa-solid fa-heading" },
47
+ "heading-4": { labelKey: "snippet.inline_edit_btn_heading_4", iconClass: "fa-solid fa-heading" },
40
48
  };
41
49
 
42
50
  function _inlineEditAffordance(type) {
@@ -55,6 +63,10 @@ const _INLINE_TYPE_SELECTORS = [
55
63
  { types: ["separator"], selector: "hr" },
56
64
  { types: ["ordered-list"], selector: "ol" },
57
65
  { types: ["unordered-list"], selector: "ul" },
66
+ { types: ["heading-1"], selector: "h1" },
67
+ { types: ["heading-2"], selector: "h2" },
68
+ { types: ["heading-3"], selector: "h3" },
69
+ { types: ["heading-4"], selector: "h4" },
58
70
  { types: ["image"], selector: "img" },
59
71
  {
60
72
  types: ["anchor-doc-link", "doc-link", "anchor-link", "link"],
@@ -65,6 +77,12 @@ const _INLINE_TYPE_SELECTORS = [
65
77
  let _inlineSnippetPopup = null;
66
78
 
67
79
  function _inlineRangesOverlap(a, b) {
80
+ // Reject only same-position duplicates (different regex matched the same span)
81
+ // and partial overlaps. Allow strict nesting so a container can coexist with
82
+ // its inner snippets (the deepest-mapped DOM ancestor wins at click time).
83
+ if (a.start === b.start && a.end === b.end) return true;
84
+ if (a.start >= b.start && a.end <= b.end) return false;
85
+ if (b.start >= a.start && b.end <= a.end) return false;
68
86
  return a.start < b.end && b.start < a.end;
69
87
  }
70
88
 
@@ -123,13 +141,21 @@ function _inlineAddCodeBlockRanges(ranges, content) {
123
141
  function _inlineCollectSnippetRanges(content) {
124
142
  const ranges = [];
125
143
 
126
- _inlineAddCodeBlockRanges(ranges, content);
144
+ // Container ranges first so inner snippets (code, lists, links…) that overlap
145
+ // with a container get skipped — the user edits the container as a whole.
127
146
  _inlineAddRegexRanges(ranges, content, /<details[\s\S]*?<\/details>/gi);
128
147
  _inlineAddRegexRanges(
129
148
  ranges,
130
149
  content,
131
150
  /<div\b[^>]*border-left[^>]*>[\s\S]*?<\/div>/gi,
132
151
  );
152
+ _inlineAddCodeBlockRanges(ranges, content);
153
+ _inlineAddRegexRanges(
154
+ ranges,
155
+ content,
156
+ /(?:^|\n\n)(#{1,4} [^\n]+)/g,
157
+ 1,
158
+ );
133
159
  _inlineAddRegexRanges(
134
160
  ranges,
135
161
  content,
@@ -18,6 +18,8 @@ function detectSnippetType(text) {
18
18
  if (/^```/.test(t)) return "code-block";
19
19
  if (/^(\| *.*? *\|)+\n(\| *-+.*\|)+/.test(t)) return "table";
20
20
  if (/^> /.test(t)) return "blockquote";
21
+ const headingMatch = /^(#{1,4}) [^\n]+$/.exec(t);
22
+ if (headingMatch) return `heading-${headingMatch[1].length}`;
21
23
  if (/^(---|\n---\n)$/.test(t)) return "separator";
22
24
  if (/^1\. /.test(t)) return "ordered-list";
23
25
  if (/^- /.test(t)) return "unordered-list";
@@ -10,6 +10,7 @@ let _snippetInlineEdit = false;
10
10
  let _snippetInlineInsert = false;
11
11
  let _snippetInlineIndent = "";
12
12
  const _SNIPPET_PANELS = [
13
+ "heading",
13
14
  "collapsible",
14
15
  "link",
15
16
  "doc-link",
@@ -29,6 +30,356 @@ const _SNIPPET_PANELS = [
29
30
  "attachment",
30
31
  ];
31
32
 
33
+ const _SNIPPET_TYPE_TO_PANEL = {
34
+ "heading-1": "heading",
35
+ "heading-2": "heading",
36
+ "heading-3": "heading",
37
+ "heading-4": "heading",
38
+ };
39
+
40
+ function _snippetPanelForType(type) {
41
+ return _SNIPPET_TYPE_TO_PANEL[type] || type;
42
+ }
43
+
44
+ const _SNIPPET_PICKER_ICONS = {
45
+ "heading-1": "fa-solid fa-heading",
46
+ "heading-2": "fa-solid fa-heading",
47
+ "heading-3": "fa-solid fa-heading",
48
+ "heading-4": "fa-solid fa-heading",
49
+ separator: "fa-solid fa-minus",
50
+ collapsible: "fa-solid fa-caret-right",
51
+ "unordered-list": "fa-solid fa-list-ul",
52
+ "ordered-list": "fa-solid fa-list-ol",
53
+ tree: "fa-solid fa-folder-tree",
54
+ "colored-text": "fa-solid fa-highlighter",
55
+ "colored-section": "fa-solid fa-fill-drip",
56
+ blockquote: "fa-solid fa-quote-right",
57
+ "code-block": "fa-solid fa-code",
58
+ table: "fa-solid fa-table-cells",
59
+ link: "fa-solid fa-link",
60
+ "doc-link": "fa-solid fa-file-lines",
61
+ "anchor-link": "fa-solid fa-anchor",
62
+ "anchor-doc-link": "fa-solid fa-anchor",
63
+ image: "fa-solid fa-image",
64
+ diagram: "fa-solid fa-diagram-project",
65
+ emojis: "fa-solid fa-face-smile",
66
+ attachment: "fa-solid fa-paperclip",
67
+ "local-search": "fa-solid fa-magnifying-glass",
68
+ };
69
+
70
+ const _SNIPPET_PICKER_CATEGORIES = [
71
+ {
72
+ key: "structure",
73
+ labelKey: "snippet.picker_cat_structure",
74
+ types: ["heading-1", "heading-2", "heading-3", "heading-4", "separator", "collapsible"],
75
+ },
76
+ {
77
+ key: "lists-code-data",
78
+ labelKey: "snippet.picker_cat_lists_code_data",
79
+ types: ["unordered-list", "ordered-list", "tree", "code-block", "table", "diagram"],
80
+ },
81
+ {
82
+ key: "rich-text",
83
+ labelKey: "snippet.picker_cat_rich_text",
84
+ types: ["colored-text", "colored-section", "blockquote", "emojis", "local-search"],
85
+ },
86
+ {
87
+ key: "links-media",
88
+ labelKey: "snippet.picker_cat_links_media",
89
+ types: ["link", "doc-link", "anchor-link", "anchor-doc-link", "image", "attachment"],
90
+ },
91
+ ];
92
+
93
+ const _SNIPPET_PICKER_PALETTE = {
94
+ blueLight: {
95
+ card:
96
+ "border-sky-200 dark:border-sky-800 bg-sky-50 dark:bg-sky-950/40 hover:bg-sky-100 dark:hover:bg-sky-900/50 hover:border-sky-300 dark:hover:border-sky-700",
97
+ icon: "text-sky-700 dark:text-sky-300",
98
+ },
99
+ blue: {
100
+ card:
101
+ "border-blue-200 dark:border-blue-800 bg-blue-50 dark:bg-blue-950/40 hover:bg-blue-100 dark:hover:bg-blue-900/50 hover:border-blue-300 dark:hover:border-blue-700",
102
+ icon: "text-blue-700 dark:text-blue-300",
103
+ },
104
+ blueDark: {
105
+ card:
106
+ "border-indigo-200 dark:border-indigo-800 bg-indigo-50 dark:bg-indigo-950/40 hover:bg-indigo-100 dark:hover:bg-indigo-900/50 hover:border-indigo-300 dark:hover:border-indigo-700",
107
+ icon: "text-indigo-700 dark:text-indigo-300",
108
+ },
109
+ blueDarker: {
110
+ card:
111
+ "border-blue-300 dark:border-blue-700 bg-blue-100 dark:bg-blue-900/50 hover:bg-blue-200 dark:hover:bg-blue-800/60 hover:border-blue-400 dark:hover:border-blue-600",
112
+ icon: "text-blue-900 dark:text-blue-100",
113
+ },
114
+ gray: {
115
+ card:
116
+ "border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600",
117
+ icon: "text-gray-700 dark:text-gray-200",
118
+ },
119
+ violet: {
120
+ card:
121
+ "border-violet-200 dark:border-violet-800 bg-violet-50 dark:bg-violet-950/40 hover:bg-violet-100 dark:hover:bg-violet-900/50 hover:border-violet-300 dark:hover:border-violet-700",
122
+ icon: "text-violet-700 dark:text-violet-300",
123
+ },
124
+ green: {
125
+ card:
126
+ "border-emerald-200 dark:border-emerald-800 bg-emerald-50 dark:bg-emerald-950/40 hover:bg-emerald-100 dark:hover:bg-emerald-900/50 hover:border-emerald-300 dark:hover:border-emerald-700",
127
+ icon: "text-emerald-700 dark:text-emerald-300",
128
+ },
129
+ greenDark: {
130
+ card:
131
+ "border-green-300 dark:border-green-700 bg-green-100 dark:bg-green-900/50 hover:bg-green-200 dark:hover:bg-green-800/60 hover:border-green-400 dark:hover:border-green-600",
132
+ icon: "text-green-900 dark:text-green-100",
133
+ },
134
+ teal: {
135
+ card:
136
+ "border-teal-200 dark:border-teal-800 bg-teal-50 dark:bg-teal-950/40 hover:bg-teal-100 dark:hover:bg-teal-900/50 hover:border-teal-300 dark:hover:border-teal-700",
137
+ icon: "text-teal-700 dark:text-teal-300",
138
+ },
139
+ dark: {
140
+ card:
141
+ "border-slate-300 dark:border-slate-600 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 hover:border-slate-400 dark:hover:border-slate-500",
142
+ icon: "text-slate-900 dark:text-slate-100",
143
+ },
144
+ orange: {
145
+ card:
146
+ "border-orange-200 dark:border-orange-800 bg-orange-50 dark:bg-orange-950/40 hover:bg-orange-100 dark:hover:bg-orange-900/50 hover:border-orange-300 dark:hover:border-orange-700",
147
+ icon: "text-orange-700 dark:text-orange-300",
148
+ },
149
+ orangeLight: {
150
+ card:
151
+ "border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-950/40 hover:bg-amber-100 dark:hover:bg-amber-900/50 hover:border-amber-300 dark:hover:border-amber-700",
152
+ icon: "text-amber-700 dark:text-amber-300",
153
+ },
154
+ orangeDark: {
155
+ card:
156
+ "border-orange-300 dark:border-orange-700 bg-orange-100 dark:bg-orange-900/50 hover:bg-orange-200 dark:hover:bg-orange-800/60 hover:border-orange-400 dark:hover:border-orange-600",
157
+ icon: "text-orange-900 dark:text-orange-100",
158
+ },
159
+ red: {
160
+ card:
161
+ "border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-950/40 hover:bg-red-100 dark:hover:bg-red-900/50 hover:border-red-300 dark:hover:border-red-700",
162
+ icon: "text-red-700 dark:text-red-300",
163
+ },
164
+ pink: {
165
+ card:
166
+ "border-pink-200 dark:border-pink-800 bg-pink-50 dark:bg-pink-950/40 hover:bg-pink-100 dark:hover:bg-pink-900/50 hover:border-pink-300 dark:hover:border-pink-700",
167
+ icon: "text-pink-700 dark:text-pink-300",
168
+ },
169
+ amber: {
170
+ card:
171
+ "border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-950/40 hover:bg-amber-100 dark:hover:bg-amber-900/50 hover:border-amber-300 dark:hover:border-amber-700",
172
+ icon: "text-amber-700 dark:text-amber-300",
173
+ },
174
+ yellow: {
175
+ card:
176
+ "border-yellow-300 dark:border-yellow-700 bg-yellow-100 dark:bg-yellow-900/50 hover:bg-yellow-200 dark:hover:bg-yellow-800/60 hover:border-yellow-400 dark:hover:border-yellow-600",
177
+ icon: "text-yellow-700 dark:text-yellow-200",
178
+ },
179
+ indigo: {
180
+ card:
181
+ "border-indigo-200 dark:border-indigo-800 bg-indigo-50 dark:bg-indigo-950/40 hover:bg-indigo-100 dark:hover:bg-indigo-900/50 hover:border-indigo-300 dark:hover:border-indigo-700",
182
+ icon: "text-indigo-700 dark:text-indigo-300",
183
+ },
184
+ cyan: {
185
+ card:
186
+ "border-cyan-200 dark:border-cyan-800 bg-cyan-50 dark:bg-cyan-950/40 hover:bg-cyan-100 dark:hover:bg-cyan-900/50 hover:border-cyan-300 dark:hover:border-cyan-700",
187
+ icon: "text-cyan-700 dark:text-cyan-300",
188
+ },
189
+ };
190
+
191
+ const _SNIPPET_PICKER_TYPE_PALETTE = {
192
+ "heading-1": "blueLight",
193
+ "heading-2": "blue",
194
+ "heading-3": "blueDark",
195
+ "heading-4": "blueDarker",
196
+ separator: "gray",
197
+ collapsible: "dark",
198
+ "unordered-list": "green",
199
+ "ordered-list": "greenDark",
200
+ tree: "cyan",
201
+ "code-block": "dark",
202
+ table: "orange",
203
+ diagram: "red",
204
+ "colored-text": "pink",
205
+ "colored-section": "violet",
206
+ blockquote: "gray",
207
+ link: "orangeLight",
208
+ "doc-link": "orange",
209
+ "anchor-link": "orangeDark",
210
+ "anchor-doc-link": "orangeDark",
211
+ image: "amber",
212
+ attachment: "orange",
213
+ emojis: "yellow",
214
+ "local-search": "cyan",
215
+ };
216
+
217
+ const _SNIPPET_TYPE_I18N_KEY = {
218
+ "heading-1": "snippet.heading_1",
219
+ "heading-2": "snippet.heading_2",
220
+ "heading-3": "snippet.heading_3",
221
+ "heading-4": "snippet.heading_4",
222
+ separator: "snippet.separator",
223
+ collapsible: "snippet.collapsible",
224
+ "unordered-list": "snippet.bullet_list",
225
+ "ordered-list": "snippet.numbered_list",
226
+ tree: "snippet.tree",
227
+ "colored-text": "snippet.colored_text",
228
+ "colored-section": "snippet.colored_section",
229
+ blockquote: "snippet.blockquote",
230
+ "code-block": "snippet.code_block",
231
+ table: "snippet.table",
232
+ link: "snippet.link",
233
+ "doc-link": "snippet.link_doc",
234
+ "anchor-link": "snippet.link_anchor",
235
+ "anchor-doc-link": "snippet.link_doc_anchor",
236
+ image: "snippet.image",
237
+ diagram: "snippet.diagram",
238
+ emojis: "snippet.emojis",
239
+ attachment: "snippet.attachment",
240
+ "local-search": "snippet.local_search",
241
+ };
242
+
243
+ function _snippetPickerCleanLabel(raw) {
244
+ if (!raw) return "";
245
+ const parts = raw.split(/\s+/);
246
+ if (parts.length > 1 && !/^[\p{L}\p{N}]/u.test(parts[0])) {
247
+ return parts.slice(1).join(" ");
248
+ }
249
+ return raw;
250
+ }
251
+
252
+ function _snippetPickerLabel(type) {
253
+ const key = _SNIPPET_TYPE_I18N_KEY[type];
254
+ const raw = key ? window.t(key) : type;
255
+ return _snippetPickerCleanLabel(raw);
256
+ }
257
+
258
+ function _renderSnippetPicker() {
259
+ const container = document.getElementById("snippet-picker-categories");
260
+ if (!container) return;
261
+ container.innerHTML = "";
262
+ for (const cat of _SNIPPET_PICKER_CATEGORIES) {
263
+ const section = document.createElement("section");
264
+ section.dataset.snippetCategory = cat.key;
265
+ section.className = "space-y-2";
266
+
267
+ const h3 = document.createElement("h3");
268
+ h3.className =
269
+ "text-[11px] font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400";
270
+ h3.textContent = window.t(cat.labelKey);
271
+ section.appendChild(h3);
272
+
273
+ const grid = document.createElement("div");
274
+ grid.className =
275
+ "grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-7 xl:grid-cols-8 gap-2";
276
+
277
+ for (const type of cat.types) {
278
+ const paletteName = _SNIPPET_PICKER_TYPE_PALETTE[type] || "indigo";
279
+ const colorClasses =
280
+ _SNIPPET_PICKER_PALETTE[paletteName] ||
281
+ _SNIPPET_PICKER_PALETTE.indigo;
282
+ const btn = document.createElement("button");
283
+ btn.type = "button";
284
+ btn.dataset.snippetType = type;
285
+ btn.className =
286
+ "snippet-card min-h-[76px] flex flex-col items-center justify-center gap-2 p-2 rounded-lg border text-gray-800 dark:text-gray-100 transition-colors " +
287
+ colorClasses.card;
288
+ btn.onclick = () => snippetPickerSelect(type);
289
+
290
+ const i = document.createElement("i");
291
+ i.className =
292
+ (_SNIPPET_PICKER_ICONS[type] || "fa-solid fa-puzzle-piece") +
293
+ " text-xl " +
294
+ colorClasses.icon;
295
+ i.setAttribute("aria-hidden", "true");
296
+ btn.appendChild(i);
297
+
298
+ const label = document.createElement("span");
299
+ label.className = "text-xs font-medium text-center leading-tight";
300
+ label.textContent = _snippetPickerLabel(type);
301
+ btn.appendChild(label);
302
+
303
+ grid.appendChild(btn);
304
+ }
305
+ section.appendChild(grid);
306
+ container.appendChild(section);
307
+ }
308
+ }
309
+
310
+ function _showSnippetPicker() {
311
+ _renderSnippetPicker();
312
+ document.getElementById("snippet-picker").classList.remove("hidden");
313
+ document.getElementById("snippet-picker-back").classList.add("hidden");
314
+ document.getElementById("snippet-submit-btn").classList.add("hidden");
315
+ _SNIPPET_PANELS.forEach((p) => {
316
+ const panel = document.getElementById("snip-panel-" + p);
317
+ if (panel) panel.classList.add("hidden");
318
+ });
319
+ const previewWrap = document.getElementById("snippet-preview-wrap");
320
+ if (previewWrap) previewWrap.classList.add("hidden");
321
+ const search = document.getElementById("snippet-picker-search");
322
+ if (search) {
323
+ search.value = "";
324
+ _snippetPickerFilter("");
325
+ setTimeout(() => search.focus(), 50);
326
+ }
327
+ }
328
+
329
+ function _showSnippetPanelOnly() {
330
+ document.getElementById("snippet-picker").classList.add("hidden");
331
+ document.getElementById("snippet-submit-btn").classList.remove("hidden");
332
+ const back = document.getElementById("snippet-picker-back");
333
+ if (back) back.classList.toggle("hidden", _snippetInlineEdit);
334
+ }
335
+
336
+ function _snippetPickerFilter(query) {
337
+ const container = document.getElementById("snippet-picker-categories");
338
+ if (!container) return;
339
+ const q = (query || "").toLowerCase().trim();
340
+ let totalVisible = 0;
341
+ container.querySelectorAll("[data-snippet-category]").forEach((section) => {
342
+ let visibleCount = 0;
343
+ section.querySelectorAll("[data-snippet-type]").forEach((card) => {
344
+ const label = (card.querySelector("span")?.textContent || "").toLowerCase();
345
+ const match = !q || label.includes(q);
346
+ card.classList.toggle("hidden", !match);
347
+ if (match) visibleCount += 1;
348
+ });
349
+ section.classList.toggle("hidden", visibleCount === 0);
350
+ totalVisible += visibleCount;
351
+ });
352
+ const noResults = document.getElementById("snippet-picker-no-results");
353
+ if (noResults) noResults.classList.toggle("hidden", totalVisible > 0);
354
+ }
355
+
356
+ function snippetPickerSearchChanged() {
357
+ const input = document.getElementById("snippet-picker-search");
358
+ _snippetPickerFilter(input ? input.value : "");
359
+ }
360
+
361
+ function snippetPickerSearchKeydown(event) {
362
+ if (event.key === "Enter") {
363
+ event.preventDefault();
364
+ const firstVisible = document.querySelector(
365
+ "#snippet-picker-categories [data-snippet-type]:not(.hidden)",
366
+ );
367
+ if (firstVisible) snippetPickerSelect(firstVisible.dataset.snippetType);
368
+ }
369
+ }
370
+
371
+ function snippetPickerSelect(type) {
372
+ document.getElementById("snippet-type").value = type;
373
+ snippetTypeChanged();
374
+ _showSnippetPanelOnly();
375
+ }
376
+
377
+ function snippetPickerBack() {
378
+ const msgEl = document.getElementById("snippet-detect-msg");
379
+ if (msgEl) msgEl.classList.add("hidden");
380
+ _showSnippetPicker();
381
+ }
382
+
32
383
  // Each emoji has search tags (bilingual FR/EN, space-separated, lowercase).
33
384
  // Filter matches a 2+ char query against any tag prefix or substring.
34
385
  const _EMOJI_CATEGORIES = [
@@ -664,8 +1015,8 @@ function _setSnippetModalMode(mode) {
664
1015
  }
665
1016
  const card = document.getElementById("snippet-modal-card");
666
1017
  if (card) {
667
- card.classList.toggle("max-w-lg", !isInline);
668
- card.classList.toggle("max-w-5xl", isInline);
1018
+ card.classList.toggle("max-w-6xl", !isInlineEdit);
1019
+ card.classList.toggle("max-w-5xl", isInlineEdit);
669
1020
  }
670
1021
  }
671
1022
 
@@ -679,45 +1030,22 @@ function _openSnippetsModalForText(selectedText, detectedOverride = null) {
679
1030
  snippetAnchorDocChanged();
680
1031
 
681
1032
  const msgEl = document.getElementById("snippet-detect-msg");
1033
+ if (msgEl) msgEl.classList.add("hidden");
1034
+ const detected = selectedText
1035
+ ? detectedOverride || detectSnippetType(selectedText)
1036
+ : null;
682
1037
 
683
- if (selectedText) {
684
- const detected = detectedOverride || detectSnippetType(selectedText);
685
- if (detected) {
686
- document.getElementById("snippet-type").value = detected;
687
- snippetTypeChanged();
688
- parseAndFillSnippet(selectedText, detected);
689
- const labels = {
690
- collapsible: window.t('snippet.collapsible'),
691
- link: window.t('snippet.link'),
692
- "doc-link": window.t('snippet.link_doc'),
693
- "anchor-link": window.t('snippet.link_anchor'),
694
- "anchor-doc-link": window.t('snippet.link_doc_anchor'),
695
- "ordered-list": window.t('snippet.numbered_list'),
696
- "unordered-list": window.t('snippet.bullet_list'),
697
- "code-block": window.t('snippet.code_block'),
698
- blockquote: window.t('snippet.blockquote'),
699
- separator: window.t('snippet.separator'),
700
- image: window.t('snippet.image'),
701
- table: window.t('snippet.table'),
702
- tree: window.t('snippet.tree'),
703
- "colored-section": window.t('snippet.colored_section'),
704
- "colored-text": window.t('snippet.colored_text'),
705
- };
706
- msgEl.textContent = window.t('snippet.detected_msg').replace('{type}', labels[detected] ?? detected);
707
- msgEl.className =
708
- "rounded-lg px-3 py-2 text-xs bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 border border-green-200 dark:border-green-800";
709
- } else {
710
- document.getElementById("snippet-type").value = "diagram";
711
- snippetTypeChanged();
712
- msgEl.textContent = window.t('snippet.unknown_type_msg');
713
- msgEl.className =
714
- "rounded-lg px-3 py-2 text-xs bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-300 border border-amber-200 dark:border-amber-800";
715
- }
716
- msgEl.classList.remove("hidden");
717
- } else {
718
- msgEl.classList.add("hidden");
719
- document.getElementById("snippet-type").value = "diagram";
1038
+ if (_snippetInlineInsert) {
1039
+ _showSnippetPicker();
1040
+ } else if (detected) {
1041
+ document.getElementById("snippet-type").value = detected;
720
1042
  snippetTypeChanged();
1043
+ parseAndFillSnippet(selectedText, detected);
1044
+ _showSnippetPanelOnly();
1045
+ } else if (selectedText) {
1046
+ _showSnippetPicker();
1047
+ } else {
1048
+ _showSnippetPicker();
721
1049
  }
722
1050
 
723
1051
  document.getElementById("snippets-modal").classList.remove("hidden");
@@ -760,9 +1088,10 @@ function closeSnippetsModal() {
760
1088
 
761
1089
  function snippetTypeChanged() {
762
1090
  const type = document.getElementById("snippet-type").value;
1091
+ const activePanel = _snippetPanelForType(type);
763
1092
  _SNIPPET_PANELS.forEach((p) => {
764
1093
  const panel = document.getElementById("snip-panel-" + p);
765
- if (panel) panel.classList.toggle("hidden", p !== type);
1094
+ if (panel) panel.classList.toggle("hidden", p !== activePanel);
766
1095
  });
767
1096
  const previewWrap = document.getElementById("snippet-preview-wrap");
768
1097
  if (previewWrap) {
@@ -776,7 +1105,9 @@ function snippetTypeChanged() {
776
1105
  type === "unordered-list" ||
777
1106
  type === "colored-section" ||
778
1107
  type === "colored-text" ||
779
- type === "tree",
1108
+ type === "tree" ||
1109
+ type === "collapsible" ||
1110
+ type.startsWith("heading-"),
780
1111
  );
781
1112
  }
782
1113
 
@@ -861,7 +1192,11 @@ function buildSnippetMarkdown() {
861
1192
  const summary =
862
1193
  document.getElementById("snip-collapsible-summary").value ||
863
1194
  window.t('snippet.collapsible_summary_value');
864
- return `<details>\n<summary>${summary}</summary>\n\n## Titre\n\nTexte\n\n</details>`;
1195
+ const bodyEl = document.getElementById("snip-collapsible-body");
1196
+ const body = bodyEl && bodyEl.value.trim() !== ""
1197
+ ? bodyEl.value
1198
+ : "## Titre\n\nTexte";
1199
+ return `<details>\n<summary>${summary}</summary>\n\n${body}\n\n</details>`;
865
1200
  }
866
1201
  case "link": {
867
1202
  const text =
@@ -973,6 +1308,17 @@ function buildSnippetMarkdown() {
973
1308
  }
974
1309
  case "separator":
975
1310
  return `\n---\n`;
1311
+ case "heading-1":
1312
+ case "heading-2":
1313
+ case "heading-3":
1314
+ case "heading-4": {
1315
+ const level = Number(type.slice(-1));
1316
+ const text =
1317
+ document.getElementById("snip-heading-content").value.trim() ||
1318
+ (window.t && window.t("snippet.heading_text_placeholder")) ||
1319
+ "Titre";
1320
+ return `${"#".repeat(level)} ${text}`;
1321
+ }
976
1322
  case "image": {
977
1323
  const alt =
978
1324
  document.getElementById("snip-image-alt").value || "image";
@@ -1198,10 +1544,16 @@ function parseAndFillSnippet(text, type) {
1198
1544
  const t = text.trim();
1199
1545
  switch (type) {
1200
1546
  case "collapsible": {
1201
- const m = t.match(/<summary>([\s\S]*?)<\/summary>/i);
1202
- if (m)
1547
+ const summaryMatch = t.match(/<summary>([\s\S]*?)<\/summary>/i);
1548
+ if (summaryMatch) {
1203
1549
  document.getElementById("snip-collapsible-summary").value =
1204
- m[1].trim();
1550
+ summaryMatch[1].trim();
1551
+ }
1552
+ const bodyMatch = t.match(
1553
+ /<details\b[^>]*>[\s\S]*?<\/summary>\s*\n?([\s\S]*?)\s*<\/details>\s*$/i,
1554
+ );
1555
+ const bodyEl = document.getElementById("snip-collapsible-body");
1556
+ if (bodyEl) bodyEl.value = bodyMatch ? bodyMatch[1].trim() : "";
1205
1557
  break;
1206
1558
  }
1207
1559
  case "link": {
@@ -1326,6 +1678,17 @@ function parseAndFillSnippet(text, type) {
1326
1678
  }
1327
1679
  break;
1328
1680
  }
1681
+ case "heading-1":
1682
+ case "heading-2":
1683
+ case "heading-3":
1684
+ case "heading-4": {
1685
+ const level = Number(type.slice(-1));
1686
+ const re = new RegExp(`^#{${level}}\\s+(.+)$`);
1687
+ const m = t.match(re);
1688
+ const headingEl = document.getElementById("snip-heading-content");
1689
+ if (headingEl) headingEl.value = m ? m[1].trim() : "";
1690
+ break;
1691
+ }
1329
1692
  case "table": {
1330
1693
  const allLines = t
1331
1694
  .split("\n")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "living-ai-documentation",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "Local Markdown documentation hub with a built-in MCP server — coding agents create ADRs, draw diagrams and detect drift while you code.",
5
5
  "main": "dist/src/server.js",
6
6
  "bin": {