living-ai-documentation 1.6.0 → 1.9.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,18 @@
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",
272
284
  "snippet.link_text_label": "Link text",
273
285
  "snippet.link_text_placeholder": "My link",
274
286
  "snippet.link_url_label": "URL",
@@ -314,6 +326,24 @@
314
326
  "snippet.inline_delete_detail": "This action will remove the selected Markdown block.",
315
327
  "snippet.inline_delete_confirm_btn": "Delete",
316
328
  "snippet.inline_delete_failed": "Inline block deletion failed: ",
329
+ "snippet.inline_insert_btn": "Insert a snippet here",
330
+ "snippet.inline_insert_modal_title": "Insert a snippet",
331
+ "snippet.inline_insert_failed": "Inline insertion failed: ",
332
+ "snippet.inline_edit_btn_table": "Edit table",
333
+ "snippet.inline_edit_btn_code_block": "Edit code block",
334
+ "snippet.inline_edit_btn_blockquote": "Edit blockquote",
335
+ "snippet.inline_edit_btn_ordered_list": "Edit numbered list",
336
+ "snippet.inline_edit_btn_unordered_list": "Edit bullet list",
337
+ "snippet.inline_edit_btn_tree": "Edit tree",
338
+ "snippet.inline_edit_btn_colored_section": "Edit colored section",
339
+ "snippet.inline_edit_btn_colored_text": "Edit colored text",
340
+ "snippet.inline_edit_btn_collapsible": "Edit collapsible block",
341
+ "snippet.inline_edit_btn_link": "Edit link",
342
+ "snippet.inline_edit_btn_doc_link": "Edit document link",
343
+ "snippet.inline_edit_btn_anchor_link": "Edit anchor link",
344
+ "snippet.inline_edit_btn_anchor_doc_link": "Edit document + anchor link",
345
+ "snippet.inline_edit_btn_image": "Edit image",
346
+ "snippet.inline_edit_btn_separator": "Edit separator",
317
347
  "snippet.diagram_existing": "Existing diagram",
318
348
  "snippet.diagram_new": "New diagram",
319
349
  "snippet.diagram_select_label": "Select diagram",
@@ -269,6 +269,18 @@
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",
272
284
  "snippet.link_text_label": "Texte du lien",
273
285
  "snippet.link_text_placeholder": "Mon lien",
274
286
  "snippet.link_url_label": "URL",
@@ -314,6 +326,24 @@
314
326
  "snippet.inline_delete_detail": "Cette action retirera le bloc Markdown sélectionné.",
315
327
  "snippet.inline_delete_confirm_btn": "Supprimer",
316
328
  "snippet.inline_delete_failed": "Échec de la suppression inline : ",
329
+ "snippet.inline_insert_btn": "Insérer un snippet ici",
330
+ "snippet.inline_insert_modal_title": "Insérer un snippet",
331
+ "snippet.inline_insert_failed": "Échec de l'insertion inline : ",
332
+ "snippet.inline_edit_btn_table": "Éditer le tableau",
333
+ "snippet.inline_edit_btn_code_block": "Éditer le bloc de code",
334
+ "snippet.inline_edit_btn_blockquote": "Éditer la citation",
335
+ "snippet.inline_edit_btn_ordered_list": "Éditer la liste numérotée",
336
+ "snippet.inline_edit_btn_unordered_list": "Éditer la liste à puces",
337
+ "snippet.inline_edit_btn_tree": "Éditer l'arborescence",
338
+ "snippet.inline_edit_btn_colored_section": "Éditer la section colorée",
339
+ "snippet.inline_edit_btn_colored_text": "Éditer le texte coloré",
340
+ "snippet.inline_edit_btn_collapsible": "Éditer le bloc repliable",
341
+ "snippet.inline_edit_btn_link": "Éditer le lien",
342
+ "snippet.inline_edit_btn_doc_link": "Éditer le lien vers un document",
343
+ "snippet.inline_edit_btn_anchor_link": "Éditer le lien d'ancre",
344
+ "snippet.inline_edit_btn_anchor_doc_link": "Éditer le lien document + ancre",
345
+ "snippet.inline_edit_btn_image": "Éditer l'image",
346
+ "snippet.inline_edit_btn_separator": "Éditer le séparateur",
317
347
  "snippet.diagram_existing": "Diagramme existant",
318
348
  "snippet.diagram_new": "Nouveau diagramme",
319
349
  "snippet.diagram_select_label": "Sélectionner un diagramme",
@@ -1453,6 +1453,10 @@
1453
1453
  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
1454
  >
1455
1455
  <option data-i18n="snippet.diagram" value="diagram" selected>Diagram</option>
1456
+ <option data-i18n="snippet.heading_1" value="heading-1">Title level 1</option>
1457
+ <option data-i18n="snippet.heading_2" value="heading-2">Title level 2</option>
1458
+ <option data-i18n="snippet.heading_3" value="heading-3">Title level 3</option>
1459
+ <option data-i18n="snippet.heading_4" value="heading-4">Title level 4</option>
1456
1460
  <option data-i18n="snippet.collapsible" value="collapsible">Collapsible block (details)</option>
1457
1461
  <option data-i18n="snippet.link" value="link">Link</option>
1458
1462
  <option data-i18n="snippet.link_doc" value="doc-link">Link to document</option>
@@ -1476,6 +1480,26 @@
1476
1480
  </select>
1477
1481
  </div>
1478
1482
 
1483
+ <!-- Panel: heading (shared by heading-1..heading-4) -->
1484
+ <div id="snip-panel-heading" class="hidden space-y-3">
1485
+ <div class="space-y-1.5">
1486
+ <label
1487
+ data-i18n="snippet.heading_text_label"
1488
+ for="snip-heading-content"
1489
+ class="block text-xs font-medium text-gray-500 dark:text-gray-400"
1490
+ >Title text</label
1491
+ >
1492
+ <input
1493
+ id="snip-heading-content"
1494
+ type="text"
1495
+ oninput="snippetUpdatePreview()"
1496
+ data-i18n-placeholder="snippet.heading_text_placeholder"
1497
+ placeholder="My title"
1498
+ 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"
1499
+ />
1500
+ </div>
1501
+ </div>
1502
+
1479
1503
  <!-- Panel: collapsible -->
1480
1504
  <div id="snip-panel-collapsible" class="space-y-3">
1481
1505
  <div class="space-y-1.5">
@@ -1492,6 +1516,22 @@
1492
1516
  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
1517
  />
1494
1518
  </div>
1519
+ <div class="space-y-1.5">
1520
+ <label
1521
+ data-i18n="snippet.collapsible_body_label"
1522
+ for="snip-collapsible-body"
1523
+ class="block text-xs font-medium text-gray-500 dark:text-gray-400"
1524
+ >Body Markdown</label
1525
+ >
1526
+ <textarea
1527
+ id="snip-collapsible-body"
1528
+ rows="6"
1529
+ oninput="snippetUpdatePreview()"
1530
+ data-i18n-placeholder="snippet.collapsible_body_placeholder"
1531
+ placeholder="## Titre&#10;&#10;Texte"
1532
+ 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"
1533
+ ></textarea>
1534
+ </div>
1495
1535
  </div>
1496
1536
 
1497
1537
  <!-- Panel: link -->
@@ -19,8 +19,40 @@ 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
 
28
+ const _INLINE_EDIT_AFFORDANCE_BY_TYPE = {
29
+ table: { labelKey: "snippet.inline_edit_btn_table", iconClass: "fa-solid fa-table-cells" },
30
+ "code-block": { labelKey: "snippet.inline_edit_btn_code_block", iconClass: "fa-solid fa-code" },
31
+ blockquote: { labelKey: "snippet.inline_edit_btn_blockquote", iconClass: "fa-solid fa-quote-right" },
32
+ "ordered-list": { labelKey: "snippet.inline_edit_btn_ordered_list", iconClass: "fa-solid fa-list-ol" },
33
+ "unordered-list": { labelKey: "snippet.inline_edit_btn_unordered_list", iconClass: "fa-solid fa-list-ul" },
34
+ tree: { labelKey: "snippet.inline_edit_btn_tree", iconClass: "fa-solid fa-folder-tree" },
35
+ "colored-section": { labelKey: "snippet.inline_edit_btn_colored_section", iconClass: "fa-solid fa-fill-drip" },
36
+ "colored-text": { labelKey: "snippet.inline_edit_btn_colored_text", iconClass: "fa-solid fa-highlighter" },
37
+ collapsible: { labelKey: "snippet.inline_edit_btn_collapsible", iconClass: "fa-solid fa-caret-right" },
38
+ link: { labelKey: "snippet.inline_edit_btn_link", iconClass: "fa-solid fa-link" },
39
+ "doc-link": { labelKey: "snippet.inline_edit_btn_doc_link", iconClass: "fa-solid fa-file-lines" },
40
+ "anchor-link": { labelKey: "snippet.inline_edit_btn_anchor_link", iconClass: "fa-solid fa-anchor" },
41
+ "anchor-doc-link": { labelKey: "snippet.inline_edit_btn_anchor_doc_link", iconClass: "fa-solid fa-anchor" },
42
+ image: { labelKey: "snippet.inline_edit_btn_image", iconClass: "fa-solid fa-image" },
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" },
48
+ };
49
+
50
+ function _inlineEditAffordance(type) {
51
+ const known = _INLINE_EDIT_AFFORDANCE_BY_TYPE[type];
52
+ if (known) return known;
53
+ return { labelKey: "snippet.inline_edit_btn", iconClass: "fa-solid fa-pen-to-square" };
54
+ }
55
+
24
56
  const _INLINE_TYPE_SELECTORS = [
25
57
  { types: ["collapsible"], selector: "details" },
26
58
  { types: ["colored-section"], selector: 'div[style*="border-left"]' },
@@ -31,6 +63,10 @@ const _INLINE_TYPE_SELECTORS = [
31
63
  { types: ["separator"], selector: "hr" },
32
64
  { types: ["ordered-list"], selector: "ol" },
33
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" },
34
70
  { types: ["image"], selector: "img" },
35
71
  {
36
72
  types: ["anchor-doc-link", "doc-link", "anchor-link", "link"],
@@ -41,6 +77,12 @@ const _INLINE_TYPE_SELECTORS = [
41
77
  let _inlineSnippetPopup = null;
42
78
 
43
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;
44
86
  return a.start < b.end && b.start < a.end;
45
87
  }
46
88
 
@@ -67,16 +109,53 @@ function _inlineAddRegexRanges(ranges, content, regex, groupIndex = 0) {
67
109
  }
68
110
  }
69
111
 
112
+ function _inlineLineIndentBefore(content, idx) {
113
+ let i = idx;
114
+ while (i > 0 && content[i - 1] !== "\n") i -= 1;
115
+ const prefix = content.slice(i, idx);
116
+ return /^[ \t]+$/.test(prefix) ? prefix : "";
117
+ }
118
+
119
+ function _inlineAddCodeBlockRanges(ranges, content) {
120
+ const regex = /```[\s\S]*?```/g;
121
+ let match;
122
+ while ((match = regex.exec(content))) {
123
+ const raw = match[0];
124
+ if (!raw.trim()) {
125
+ if (raw.length === 0) regex.lastIndex += 1;
126
+ continue;
127
+ }
128
+ const type = detectSnippetType(raw);
129
+ if (!type || !_INLINE_SNIPPET_TYPES.has(type)) continue;
130
+ const indent = _inlineLineIndentBefore(content, match.index);
131
+ const start = match.index - indent.length;
132
+ const end = match.index + raw.length;
133
+ const candidate = { start, end, type, indent };
134
+ if (ranges.some((existing) => _inlineRangesOverlap(existing, candidate))) {
135
+ continue;
136
+ }
137
+ ranges.push(candidate);
138
+ }
139
+ }
140
+
70
141
  function _inlineCollectSnippetRanges(content) {
71
142
  const ranges = [];
72
143
 
73
- _inlineAddRegexRanges(ranges, content, /```[\s\S]*?```/g);
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.
74
146
  _inlineAddRegexRanges(ranges, content, /<details[\s\S]*?<\/details>/gi);
75
147
  _inlineAddRegexRanges(
76
148
  ranges,
77
149
  content,
78
150
  /<div\b[^>]*border-left[^>]*>[\s\S]*?<\/div>/gi,
79
151
  );
152
+ _inlineAddCodeBlockRanges(ranges, content);
153
+ _inlineAddRegexRanges(
154
+ ranges,
155
+ content,
156
+ /(?:^|\n\n)(#{1,4} [^\n]+)/g,
157
+ 1,
158
+ );
80
159
  _inlineAddRegexRanges(
81
160
  ranges,
82
161
  content,
@@ -144,10 +223,12 @@ function _inlineClosePopup() {
144
223
  }
145
224
  }
146
225
 
147
- function _inlineShowPopup(event, range) {
226
+ function _inlineShowPopup(event, { iconClass, labelKey, onActivate, dataAction, dataType }) {
148
227
  _inlineClosePopup();
149
228
  const popup = document.createElement("div");
150
229
  popup.id = "inline-snippet-popup";
230
+ if (dataAction) popup.dataset.action = dataAction;
231
+ if (dataType) popup.dataset.snippetType = dataType;
151
232
  popup.className =
152
233
  "fixed z-50 rounded-lg border border-blue-200 dark:border-blue-800 bg-white dark:bg-gray-900 shadow-lg p-1";
153
234
  const btn = document.createElement("button");
@@ -155,16 +236,16 @@ function _inlineShowPopup(event, range) {
155
236
  btn.className =
156
237
  "inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-semibold text-blue-700 dark:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/30";
157
238
  const icon = document.createElement("i");
158
- icon.className = "fa-solid fa-pen-to-square";
239
+ icon.className = iconClass;
159
240
  icon.setAttribute("aria-hidden", "true");
160
241
  const label = document.createElement("span");
161
- label.textContent = window.t("snippet.inline_edit_btn");
242
+ label.textContent = window.t(labelKey);
162
243
  btn.append(icon, label);
163
244
  btn.addEventListener("click", (e) => {
164
245
  e.preventDefault();
165
246
  e.stopPropagation();
166
247
  _inlineClosePopup();
167
- openSnippetsModalForInlineEdit(range);
248
+ onActivate();
168
249
  });
169
250
  popup.appendChild(btn);
170
251
  document.body.appendChild(popup);
@@ -181,6 +262,124 @@ function _inlineShowPopup(event, range) {
181
262
  _inlineSnippetPopup = popup;
182
263
  }
183
264
 
265
+ function _inlineTopLevelBlock(contentEl, target) {
266
+ let block = target instanceof Element ? target : null;
267
+ while (block && block.parentElement && block.parentElement !== contentEl) {
268
+ block = block.parentElement;
269
+ }
270
+ if (!block || block === contentEl || block.parentElement !== contentEl) {
271
+ return null;
272
+ }
273
+ return block;
274
+ }
275
+
276
+ function _inlineNearestBlockByY(contentEl, clientY) {
277
+ if (typeof clientY !== "number") return null;
278
+ const children = Array.from(contentEl.children);
279
+ if (!children.length) return null;
280
+ let above = null;
281
+ let below = null;
282
+ for (const child of children) {
283
+ const rect = child.getBoundingClientRect();
284
+ if (rect.bottom < clientY) above = child;
285
+ else if (rect.top > clientY) {
286
+ if (!below) below = child;
287
+ } else return child;
288
+ }
289
+ return above || below;
290
+ }
291
+
292
+ function _inlineResolveBlockEnd(block, candidates) {
293
+ if (block.matches("[data-inline-snippet-index]")) {
294
+ const range = candidates[Number(block.dataset.inlineSnippetIndex)];
295
+ if (range) return range.end;
296
+ }
297
+ let anchorIdx = null;
298
+ const desc = Array.from(
299
+ block.querySelectorAll("[data-inline-snippet-index]"),
300
+ );
301
+ if (desc.length > 0) {
302
+ let maxEnd = -1;
303
+ for (const d of desc) {
304
+ const range = candidates[Number(d.dataset.inlineSnippetIndex)];
305
+ if (range && range.end > maxEnd) maxEnd = range.end;
306
+ }
307
+ if (maxEnd >= 0) anchorIdx = maxEnd;
308
+ } else {
309
+ const text = (block.textContent || "").trim();
310
+ if (text) {
311
+ const firstLine = text.split("\n")[0].trim().slice(0, 60);
312
+ if (firstLine) {
313
+ const idx = currentDocContent.indexOf(firstLine);
314
+ if (idx >= 0) anchorIdx = idx;
315
+ }
316
+ }
317
+ }
318
+ if (anchorIdx === null) return null;
319
+ const blankIdx = currentDocContent.indexOf("\n\n", anchorIdx);
320
+ return blankIdx >= 0 ? blankIdx : currentDocContent.length;
321
+ }
322
+
323
+ function _inlineResolveBlockStart(block, candidates) {
324
+ if (block.matches("[data-inline-snippet-index]")) {
325
+ const range = candidates[Number(block.dataset.inlineSnippetIndex)];
326
+ if (range) return range.start;
327
+ }
328
+ const desc = Array.from(
329
+ block.querySelectorAll("[data-inline-snippet-index]"),
330
+ );
331
+ if (desc.length > 0) {
332
+ let minStart = Infinity;
333
+ for (const d of desc) {
334
+ const range = candidates[Number(d.dataset.inlineSnippetIndex)];
335
+ if (range && range.start < minStart) minStart = range.start;
336
+ }
337
+ if (minStart < Infinity) return minStart;
338
+ }
339
+ const text = (block.textContent || "").trim();
340
+ if (text) {
341
+ const firstLine = text.split("\n")[0].trim().slice(0, 60);
342
+ if (firstLine) {
343
+ const idx = currentDocContent.indexOf(firstLine);
344
+ if (idx >= 0) return idx;
345
+ }
346
+ }
347
+ return null;
348
+ }
349
+
350
+ function _inlineFindInsertPosition(contentEl, target, candidates, clientY) {
351
+ if (typeof currentDocContent !== "string") return null;
352
+ const topBlock =
353
+ _inlineTopLevelBlock(contentEl, target) ||
354
+ _inlineNearestBlockByY(contentEl, clientY);
355
+ if (!topBlock) return currentDocContent.length;
356
+
357
+ const directEnd = _inlineResolveBlockEnd(topBlock, candidates);
358
+ if (directEnd !== null) return directEnd;
359
+
360
+ let prev = topBlock.previousElementSibling;
361
+ while (prev) {
362
+ const end = _inlineResolveBlockEnd(prev, candidates);
363
+ if (end !== null) {
364
+ const next = currentDocContent.indexOf("\n\n", end + 1);
365
+ return next >= 0 ? next : currentDocContent.length;
366
+ }
367
+ prev = prev.previousElementSibling;
368
+ }
369
+
370
+ let nxt = topBlock.nextElementSibling;
371
+ while (nxt) {
372
+ const start = _inlineResolveBlockStart(nxt, candidates);
373
+ if (start !== null) {
374
+ const prevBlank = currentDocContent.lastIndexOf("\n\n", start);
375
+ return prevBlank >= 0 ? prevBlank : 0;
376
+ }
377
+ nxt = nxt.nextElementSibling;
378
+ }
379
+
380
+ return currentDocContent.length;
381
+ }
382
+
184
383
  function initInlineSnippetEditing(contentEl) {
185
384
  if (!contentEl || typeof currentDocContent !== "string") return;
186
385
  const candidates = _inlineCollectSnippetRanges(currentDocContent);
@@ -194,11 +393,35 @@ function initInlineSnippetEditing(contentEl) {
194
393
  }
195
394
  contentEl._inlineSnippetContextHandler = (event) => {
196
395
  const target = event.target.closest("[data-inline-snippet-index]");
197
- if (!target || !contentEl.contains(target)) return;
198
- const range = candidates[Number(target.dataset.inlineSnippetIndex)];
199
- if (!range) return;
396
+ if (target && contentEl.contains(target)) {
397
+ const range = candidates[Number(target.dataset.inlineSnippetIndex)];
398
+ if (!range) return;
399
+ event.preventDefault();
400
+ const affordance = _inlineEditAffordance(range.type);
401
+ _inlineShowPopup(event, {
402
+ iconClass: affordance.iconClass,
403
+ labelKey: affordance.labelKey,
404
+ dataAction: "edit",
405
+ dataType: range.type,
406
+ onActivate: () => openSnippetsModalForInlineEdit(range),
407
+ });
408
+ return;
409
+ }
410
+ if (!contentEl.contains(event.target)) return;
411
+ const insertPos = _inlineFindInsertPosition(
412
+ contentEl,
413
+ event.target,
414
+ candidates,
415
+ event.clientY,
416
+ );
417
+ if (insertPos === null) return;
200
418
  event.preventDefault();
201
- _inlineShowPopup(event, range);
419
+ _inlineShowPopup(event, {
420
+ iconClass: "fa-solid fa-plus",
421
+ labelKey: "snippet.inline_insert_btn",
422
+ dataAction: "insert",
423
+ onActivate: () => openSnippetsModalForInlineInsert(insertPos),
424
+ });
202
425
  };
203
426
  contentEl.addEventListener(
204
427
  "contextmenu",
@@ -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";
@@ -7,7 +7,10 @@
7
7
  let _snippetSelStart = 0;
8
8
  let _snippetSelEnd = 0;
9
9
  let _snippetInlineEdit = false;
10
+ let _snippetInlineInsert = false;
11
+ let _snippetInlineIndent = "";
10
12
  const _SNIPPET_PANELS = [
13
+ "heading",
11
14
  "collapsible",
12
15
  "link",
13
16
  "doc-link",
@@ -27,6 +30,17 @@ const _SNIPPET_PANELS = [
27
30
  "attachment",
28
31
  ];
29
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
+
30
44
  // Each emoji has search tags (bilingual FR/EN, space-separated, lowercase).
31
45
  // Filter matches a 2+ char query against any tag prefix or substring.
32
46
  const _EMOJI_CATEGORIES = [
@@ -630,34 +644,40 @@ async function snippetAnchorDocChanged() {
630
644
  snippetUpdatePreview();
631
645
  }
632
646
 
633
- function _setSnippetModalMode(isInlineEdit) {
634
- _snippetInlineEdit = !!isInlineEdit;
647
+ function _setSnippetModalMode(mode) {
648
+ const isInlineEdit = mode === "inline-edit";
649
+ const isInlineInsert = mode === "inline-insert";
650
+ const isInline = isInlineEdit || isInlineInsert;
651
+ _snippetInlineEdit = isInlineEdit;
652
+ _snippetInlineInsert = isInlineInsert;
653
+ if (!isInlineEdit) _snippetInlineIndent = "";
635
654
  const title = document.getElementById("snippet-modal-title");
636
655
  if (title) {
637
- title.textContent = window.t(
638
- _snippetInlineEdit ? "snippet.inline_modal_title" : "snippet.modal_title",
639
- );
656
+ let key = "snippet.modal_title";
657
+ if (isInlineEdit) key = "snippet.inline_modal_title";
658
+ else if (isInlineInsert) key = "snippet.inline_insert_modal_title";
659
+ title.textContent = window.t(key);
640
660
  }
641
661
  const submit = document.getElementById("snippet-submit-btn");
642
662
  if (submit) {
643
663
  submit.textContent = window.t(
644
- _snippetInlineEdit ? "snippet.inline_save_btn" : "snippet.insert_btn",
664
+ isInlineEdit ? "snippet.inline_save_btn" : "snippet.insert_btn",
645
665
  );
646
666
  }
647
667
  const typeSelect = document.getElementById("snippet-type");
648
668
  if (typeSelect) {
649
- typeSelect.disabled = _snippetInlineEdit;
650
- typeSelect.classList.toggle("cursor-not-allowed", _snippetInlineEdit);
651
- typeSelect.classList.toggle("opacity-70", _snippetInlineEdit);
669
+ typeSelect.disabled = isInlineEdit;
670
+ typeSelect.classList.toggle("cursor-not-allowed", isInlineEdit);
671
+ typeSelect.classList.toggle("opacity-70", isInlineEdit);
652
672
  }
653
673
  const deleteBtn = document.getElementById("snippet-delete-btn");
654
674
  if (deleteBtn) {
655
- deleteBtn.classList.toggle("hidden", !_snippetInlineEdit);
675
+ deleteBtn.classList.toggle("hidden", !isInlineEdit);
656
676
  }
657
677
  const card = document.getElementById("snippet-modal-card");
658
678
  if (card) {
659
- card.classList.toggle("max-w-lg", !_snippetInlineEdit);
660
- card.classList.toggle("max-w-5xl", _snippetInlineEdit);
679
+ card.classList.toggle("max-w-lg", !isInline);
680
+ card.classList.toggle("max-w-5xl", isInline);
661
681
  }
662
682
  }
663
683
 
@@ -692,6 +712,10 @@ function _openSnippetsModalForText(selectedText, detectedOverride = null) {
692
712
  image: window.t('snippet.image'),
693
713
  table: window.t('snippet.table'),
694
714
  tree: window.t('snippet.tree'),
715
+ "heading-1": window.t('snippet.heading_1'),
716
+ "heading-2": window.t('snippet.heading_2'),
717
+ "heading-3": window.t('snippet.heading_3'),
718
+ "heading-4": window.t('snippet.heading_4'),
695
719
  "colored-section": window.t('snippet.colored_section'),
696
720
  "colored-text": window.t('snippet.colored_text'),
697
721
  };
@@ -719,7 +743,7 @@ function openSnippetsModal() {
719
743
  const editor = document.getElementById("doc-editor");
720
744
  _snippetSelStart = editor.selectionStart;
721
745
  _snippetSelEnd = editor.selectionEnd;
722
- _setSnippetModalMode(false);
746
+ _setSnippetModalMode("insert");
723
747
  _openSnippetsModalForText(editor.value.slice(_snippetSelStart, _snippetSelEnd));
724
748
  }
725
749
 
@@ -727,21 +751,35 @@ function openSnippetsModalForInlineEdit(range) {
727
751
  if (!range || typeof currentDocContent !== "string") return;
728
752
  _snippetSelStart = range.start;
729
753
  _snippetSelEnd = range.end;
730
- _setSnippetModalMode(true);
754
+ _setSnippetModalMode("inline-edit");
755
+ _snippetInlineIndent = range.indent || "";
731
756
  const selectedText = currentDocContent.slice(_snippetSelStart, _snippetSelEnd);
732
757
  _openSnippetsModalForText(selectedText, range.type || null);
733
758
  }
734
759
 
760
+ function openSnippetsModalForInlineInsert(insertPos) {
761
+ if (typeof currentDocContent !== "string") return;
762
+ const pos = Math.max(
763
+ 0,
764
+ Math.min(currentDocContent.length, Number(insertPos) || 0),
765
+ );
766
+ _snippetSelStart = pos;
767
+ _snippetSelEnd = pos;
768
+ _setSnippetModalMode("inline-insert");
769
+ _openSnippetsModalForText("");
770
+ }
771
+
735
772
  function closeSnippetsModal() {
736
773
  document.getElementById("snippets-modal").classList.add("hidden");
737
- _setSnippetModalMode(false);
774
+ _setSnippetModalMode("insert");
738
775
  }
739
776
 
740
777
  function snippetTypeChanged() {
741
778
  const type = document.getElementById("snippet-type").value;
779
+ const activePanel = _snippetPanelForType(type);
742
780
  _SNIPPET_PANELS.forEach((p) => {
743
781
  const panel = document.getElementById("snip-panel-" + p);
744
- if (panel) panel.classList.toggle("hidden", p !== type);
782
+ if (panel) panel.classList.toggle("hidden", p !== activePanel);
745
783
  });
746
784
  const previewWrap = document.getElementById("snippet-preview-wrap");
747
785
  if (previewWrap) {
@@ -755,7 +793,9 @@ function snippetTypeChanged() {
755
793
  type === "unordered-list" ||
756
794
  type === "colored-section" ||
757
795
  type === "colored-text" ||
758
- type === "tree",
796
+ type === "tree" ||
797
+ type === "collapsible" ||
798
+ type.startsWith("heading-"),
759
799
  );
760
800
  }
761
801
 
@@ -840,7 +880,11 @@ function buildSnippetMarkdown() {
840
880
  const summary =
841
881
  document.getElementById("snip-collapsible-summary").value ||
842
882
  window.t('snippet.collapsible_summary_value');
843
- return `<details>\n<summary>${summary}</summary>\n\n## Titre\n\nTexte\n\n</details>`;
883
+ const bodyEl = document.getElementById("snip-collapsible-body");
884
+ const body = bodyEl && bodyEl.value.trim() !== ""
885
+ ? bodyEl.value
886
+ : "## Titre\n\nTexte";
887
+ return `<details>\n<summary>${summary}</summary>\n\n${body}\n\n</details>`;
844
888
  }
845
889
  case "link": {
846
890
  const text =
@@ -932,7 +976,14 @@ function buildSnippetMarkdown() {
932
976
  const lang = document.getElementById("snip-code-lang").value || "";
933
977
  const code =
934
978
  document.getElementById("snip-code-content").value || "// code ici";
935
- return `\`\`\`${lang}\n${code}\n\`\`\``;
979
+ const block = `\`\`\`${lang}\n${code}\n\`\`\``;
980
+ if (_snippetInlineEdit && _snippetInlineIndent) {
981
+ return block
982
+ .split("\n")
983
+ .map((line) => _snippetInlineIndent + line)
984
+ .join("\n");
985
+ }
986
+ return block;
936
987
  }
937
988
  case "blockquote": {
938
989
  const content =
@@ -945,6 +996,17 @@ function buildSnippetMarkdown() {
945
996
  }
946
997
  case "separator":
947
998
  return `\n---\n`;
999
+ case "heading-1":
1000
+ case "heading-2":
1001
+ case "heading-3":
1002
+ case "heading-4": {
1003
+ const level = Number(type.slice(-1));
1004
+ const text =
1005
+ document.getElementById("snip-heading-content").value.trim() ||
1006
+ (window.t && window.t("snippet.heading_text_placeholder")) ||
1007
+ "Titre";
1008
+ return `${"#".repeat(level)} ${text}`;
1009
+ }
948
1010
  case "image": {
949
1011
  const alt =
950
1012
  document.getElementById("snip-image-alt").value || "image";
@@ -1001,7 +1063,10 @@ function snippetUpdatePreview() {
1001
1063
 
1002
1064
  async function insertSnippet() {
1003
1065
  const type = document.getElementById("snippet-type").value;
1004
- if (_snippetInlineEdit && (type === "diagram" || type === "attachment")) {
1066
+ if (
1067
+ (_snippetInlineEdit || _snippetInlineInsert) &&
1068
+ (type === "diagram" || type === "attachment")
1069
+ ) {
1005
1070
  return;
1006
1071
  }
1007
1072
  if (type === "diagram") {
@@ -1015,6 +1080,7 @@ async function insertSnippet() {
1015
1080
  }
1016
1081
  const text = buildSnippetMarkdown();
1017
1082
  const wasInlineEdit = _snippetInlineEdit;
1083
+ const wasInlineInsert = _snippetInlineInsert;
1018
1084
  closeSnippetsModal();
1019
1085
  if (wasInlineEdit) {
1020
1086
  const before = currentDocContent.slice(0, _snippetSelStart);
@@ -1029,6 +1095,32 @@ async function insertSnippet() {
1029
1095
  }
1030
1096
  return;
1031
1097
  }
1098
+ if (wasInlineInsert) {
1099
+ const before = currentDocContent.slice(0, _snippetSelStart);
1100
+ const after = currentDocContent.slice(_snippetSelStart);
1101
+ const leadingBlank =
1102
+ before.length === 0 || /\n\n$/.test(before)
1103
+ ? ""
1104
+ : before.endsWith("\n")
1105
+ ? "\n"
1106
+ : "\n\n";
1107
+ const trailingBlank =
1108
+ after.length === 0 || /^\n\n/.test(after)
1109
+ ? ""
1110
+ : after.startsWith("\n")
1111
+ ? "\n"
1112
+ : "\n\n";
1113
+ const payload = leadingBlank + text + trailingBlank;
1114
+ try {
1115
+ await saveCurrentDocumentContent(before + payload + after);
1116
+ } catch (err) {
1117
+ alert(
1118
+ window.t("snippet.inline_insert_failed") +
1119
+ (err && err.message ? err.message : String(err)),
1120
+ );
1121
+ }
1122
+ return;
1123
+ }
1032
1124
  const editor = document.getElementById("doc-editor");
1033
1125
  const before = editor.value.slice(0, _snippetSelStart);
1034
1126
  const after = editor.value.slice(_snippetSelEnd);
@@ -1140,10 +1232,16 @@ function parseAndFillSnippet(text, type) {
1140
1232
  const t = text.trim();
1141
1233
  switch (type) {
1142
1234
  case "collapsible": {
1143
- const m = t.match(/<summary>([\s\S]*?)<\/summary>/i);
1144
- if (m)
1235
+ const summaryMatch = t.match(/<summary>([\s\S]*?)<\/summary>/i);
1236
+ if (summaryMatch) {
1145
1237
  document.getElementById("snip-collapsible-summary").value =
1146
- m[1].trim();
1238
+ summaryMatch[1].trim();
1239
+ }
1240
+ const bodyMatch = t.match(
1241
+ /<details\b[^>]*>[\s\S]*?<\/summary>\s*\n?([\s\S]*?)\s*<\/details>\s*$/i,
1242
+ );
1243
+ const bodyEl = document.getElementById("snip-collapsible-body");
1244
+ if (bodyEl) bodyEl.value = bodyMatch ? bodyMatch[1].trim() : "";
1147
1245
  break;
1148
1246
  }
1149
1247
  case "link": {
@@ -1237,9 +1335,20 @@ function parseAndFillSnippet(text, type) {
1237
1335
  break;
1238
1336
  }
1239
1337
  case "code-block": {
1240
- const m = t.match(/^```\s*([^\n]*)\n([\s\S]*?)\n```$/);
1338
+ const m = t.match(/^```[ \t]*([^\n]*)\n([\s\S]*?)\n[ \t]*```$/);
1241
1339
  document.getElementById("snip-code-lang").value = m ? m[1].trim() : "";
1242
- document.getElementById("snip-code-content").value = m ? m[2] : "";
1340
+ let codeContent = m ? m[2] : "";
1341
+ if (m && _snippetInlineIndent) {
1342
+ const escapedIndent = _snippetInlineIndent.replace(
1343
+ /[.*+?^${}()|[\]\\]/g,
1344
+ "\\$&",
1345
+ );
1346
+ codeContent = codeContent.replace(
1347
+ new RegExp("^" + escapedIndent, "gm"),
1348
+ "",
1349
+ );
1350
+ }
1351
+ document.getElementById("snip-code-content").value = codeContent;
1243
1352
  break;
1244
1353
  }
1245
1354
  case "blockquote": {
@@ -1257,6 +1366,17 @@ function parseAndFillSnippet(text, type) {
1257
1366
  }
1258
1367
  break;
1259
1368
  }
1369
+ case "heading-1":
1370
+ case "heading-2":
1371
+ case "heading-3":
1372
+ case "heading-4": {
1373
+ const level = Number(type.slice(-1));
1374
+ const re = new RegExp(`^#{${level}}\\s+(.+)$`);
1375
+ const m = t.match(re);
1376
+ const headingEl = document.getElementById("snip-heading-content");
1377
+ if (headingEl) headingEl.value = m ? m[1].trim() : "";
1378
+ break;
1379
+ }
1260
1380
  case "table": {
1261
1381
  const allLines = t
1262
1382
  .split("\n")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "living-ai-documentation",
3
- "version": "1.6.0",
3
+ "version": "1.9.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": {