living-documentation 4.0.0 → 4.1.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.
@@ -350,14 +350,16 @@
350
350
  </div>
351
351
  </div>
352
352
  </div>
353
+ <!-- Search match notice (inside sticky header) -->
354
+ <div
355
+ id="search-notice"
356
+ class="hidden mt-4 rounded-lg bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 text-sm text-yellow-800 dark:text-yellow-300 overflow-hidden"
357
+ >
358
+ <div id="search-notice-title" class="px-3 py-2 font-medium border-b border-yellow-200 dark:border-yellow-800"></div>
359
+ <ol id="search-notice-list" class="max-h-40 overflow-y-auto divide-y divide-yellow-100 dark:divide-yellow-900/40 list-none m-0 p-0"></ol>
360
+ </div>
353
361
  </header>
354
362
 
355
- <!-- Search match notice -->
356
- <div
357
- id="search-notice"
358
- class="hidden mb-4 px-3 py-2 rounded-lg bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 text-sm text-yellow-800 dark:text-yellow-300"
359
- ></div>
360
-
361
363
  <!-- Rendered markdown -->
362
364
  <div
363
365
  id="doc-content"
@@ -692,8 +694,8 @@
692
694
  // Highlight search matches in content
693
695
  const notice = document.getElementById("search-notice");
694
696
  if (searchQuery) {
695
- highlightMatches(contentEl, searchQuery);
696
- notice.textContent = `Showing matches for "${searchQuery}"`;
697
+ const matches = highlightMatches(contentEl, searchQuery);
698
+ buildSearchNotice(matches, searchQuery);
697
699
  notice.classList.remove("hidden");
698
700
  } else {
699
701
  notice.classList.add("hidden");
@@ -890,16 +892,50 @@
890
892
  const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
891
893
  const nodes = [];
892
894
  while (walker.nextNode()) nodes.push(walker.currentNode);
893
- const re = new RegExp(
894
- `(${q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`,
895
- "gi",
896
- );
895
+ const re = new RegExp(`(${q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
896
+ let idx = 0;
897
897
  nodes.forEach((node) => {
898
898
  if (!node.textContent.toLowerCase().includes(q.toLowerCase())) return;
899
899
  const span = document.createElement("span");
900
- span.innerHTML = node.textContent.replace(re, "<mark>$1</mark>");
900
+ span.innerHTML = node.textContent.replace(re, (m) => `<mark id="match-${idx++}">${m}</mark>`);
901
901
  node.parentNode.replaceChild(span, node);
902
902
  });
903
+ // Build snippet list from inserted marks
904
+ const matches = [];
905
+ el.querySelectorAll("mark[id^='match-']").forEach((mark) => {
906
+ const block = mark.closest("p, li, td, h1, h2, h3, h4, h5, h6, pre") || mark.parentElement;
907
+ const full = block.textContent.trim();
908
+ const pos = full.toLowerCase().indexOf(q.toLowerCase());
909
+ const start = Math.max(0, pos - 40);
910
+ const end = Math.min(full.length, pos + q.length + 40);
911
+ const snippet = (start > 0 ? "…" : "") + full.slice(start, end) + (end < full.length ? "…" : "");
912
+ matches.push({ id: mark.id, snippet });
913
+ });
914
+ return matches;
915
+ }
916
+
917
+ function buildSearchNotice(matches, q) {
918
+ document.getElementById("search-notice-title").textContent =
919
+ `Showing ${matches.length} match${matches.length === 1 ? "" : "es"} for "${q}"`;
920
+ const list = document.getElementById("search-notice-list");
921
+ list.innerHTML = matches.map((m, i) =>
922
+ `<li>
923
+ <button onclick="scrollToMatch('${m.id}')"
924
+ class="w-full text-left px-3 py-1.5 hover:bg-yellow-100 dark:hover:bg-yellow-900/30 transition-colors">
925
+ <span class="text-yellow-600 dark:text-yellow-500 font-mono text-xs mr-2">${i + 1}.</span
926
+ ><span class="text-xs">${esc(m.snippet)}</span>
927
+ </button>
928
+ </li>`
929
+ ).join("");
930
+ }
931
+
932
+ function scrollToMatch(id) {
933
+ const mark = document.getElementById(id);
934
+ const container = document.getElementById("content-area");
935
+ if (!mark || !container) return;
936
+ const markTop = mark.getBoundingClientRect().top;
937
+ const containerTop = container.getBoundingClientRect().top;
938
+ container.scrollTop += markTop - containerTop - container.clientHeight / 3;
903
939
  }
904
940
 
905
941
  function esc(str) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "living-documentation",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "A CLI tool that serves a local Markdown documentation viewer",
5
5
  "main": "dist/src/server.js",
6
6
  "bin": {