@helping-ai-workflow/md2doc 2.5.0 → 2.6.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.
Files changed (2) hide show
  1. package/lib/md2doc.js +136 -21
  2. package/package.json +2 -2
package/lib/md2doc.js CHANGED
@@ -706,7 +706,7 @@ ${itemsHtml}
706
706
  const tocTree = buildTocTree(tocItems);
707
707
  tocHtml = `<aside class="reader-sidebar" data-reader-sidebar>
708
708
  <section class="reader-tools">
709
- <label class="reader-search-label" for="doc-search-input">Search this spec</label>
709
+ <label class="reader-search-label" for="doc-search-input">Search</label>
710
710
  <div class="reader-search-row">
711
711
  <input type="search" id="doc-search-input" placeholder="Enter keyword and press Enter">
712
712
  <button id="doc-search-submit" type="button">Search</button>
@@ -733,7 +733,8 @@ ${itemsHtml}
733
733
  </div>
734
734
  ${renderTocNodes(tocTree)}
735
735
  </nav>
736
- </aside>`;
736
+ </aside>
737
+ <div class="sidebar-splitter" id="sidebar-splitter" role="separator" aria-orientation="vertical" aria-label="Resize sidebar"></div>`;
737
738
  }
738
739
  } catch (e) {
739
740
  console.error('[ERROR] marked not found — install with: npm install marked');
@@ -765,7 +766,9 @@ const html = `<!DOCTYPE html>
765
766
  .page-layout {
766
767
  display: flex;
767
768
  align-items: flex-start;
768
- gap: 32px;
769
+ /* Sidebar/content spacing is carried by .sidebar-splitter (32px), not gap,
770
+ so the drag handle can live inside the visual gutter. */
771
+ gap: 0;
769
772
  margin: 0;
770
773
  padding: 24px 24px 48px;
771
774
  max-width: 100%;
@@ -774,9 +777,9 @@ const html = `<!DOCTYPE html>
774
777
  .reader-sidebar {
775
778
  position: sticky;
776
779
  top: 24px;
777
- flex: 0 1 300px;
778
- width: clamp(220px, 22vw, 300px);
779
- min-width: 220px;
780
+ flex: 0 0 auto;
781
+ width: var(--md2doc-sidebar-w, clamp(220px, 22vw, 300px));
782
+ min-width: 0;
780
783
  height: calc(100vh - 48px);
781
784
  overflow: hidden;
782
785
  padding-right: 8px;
@@ -817,6 +820,32 @@ const html = `<!DOCTYPE html>
817
820
  body[data-toc-collapsed] .toc-header-actions {
818
821
  justify-content: center;
819
822
  }
823
+ .sidebar-splitter {
824
+ flex: 0 0 32px;
825
+ align-self: stretch;
826
+ cursor: col-resize;
827
+ display: flex;
828
+ justify-content: center;
829
+ touch-action: none;
830
+ user-select: none;
831
+ }
832
+ .sidebar-splitter::before {
833
+ content: '';
834
+ width: 3px;
835
+ border-radius: 2px;
836
+ background: transparent;
837
+ transition: background 0.15s ease;
838
+ }
839
+ .sidebar-splitter:hover::before,
840
+ .sidebar-splitter.is-dragging::before {
841
+ background: #93c5fd;
842
+ }
843
+ body[data-toc-collapsed] .sidebar-splitter {
844
+ cursor: default;
845
+ }
846
+ body[data-toc-collapsed] .sidebar-splitter::before {
847
+ display: none;
848
+ }
820
849
  #toc-collapse-toggle {
821
850
  margin-left: 0;
822
851
  padding: 2px 8px;
@@ -1032,6 +1061,16 @@ const html = `<!DOCTYPE html>
1032
1061
  font-size: 0.82em;
1033
1062
  color: #57606a;
1034
1063
  line-height: 1.35;
1064
+ display: -webkit-box;
1065
+ -webkit-line-clamp: 2;
1066
+ -webkit-box-orient: vertical;
1067
+ overflow: hidden;
1068
+ }
1069
+ .search-result-snippet mark {
1070
+ background: #fde68a;
1071
+ color: inherit;
1072
+ padding: 0 1px;
1073
+ border-radius: 2px;
1035
1074
  }
1036
1075
  .search-empty {
1037
1076
  margin: 0;
@@ -1056,6 +1095,8 @@ const html = `<!DOCTYPE html>
1056
1095
  flex: 1 1 auto;
1057
1096
  min-height: 0;
1058
1097
  overflow-y: auto;
1098
+ /* Horizontal peek: no scrollbar, position driven by shift+wheel only. */
1099
+ overflow-x: hidden;
1059
1100
  }
1060
1101
  .toc ul {
1061
1102
  list-style: none;
@@ -1074,21 +1115,19 @@ const html = `<!DOCTYPE html>
1074
1115
  }
1075
1116
  .toc a {
1076
1117
  display: block;
1077
- max-width: 100%;
1118
+ /* Full natural width (no ellipsis) so shift+wheel can peek clipped tails;
1119
+ min-width keeps hover/match backgrounds covering the whole row. */
1120
+ width: max-content;
1121
+ min-width: 100%;
1078
1122
  color: #57606a;
1079
1123
  text-decoration: none;
1080
1124
  padding: 2px 0;
1081
1125
  line-height: 1.4;
1082
1126
  white-space: nowrap;
1083
- overflow: hidden;
1084
- text-overflow: ellipsis;
1085
1127
  box-sizing: border-box;
1086
1128
  }
1087
- /* Parent rows put the anchor inside a flex <summary>; default flex min-width
1088
- is auto, which would block ellipsis. Allow the anchor to shrink + clip. */
1089
1129
  .toc summary > a {
1090
- min-width: 0;
1091
- flex: 0 1 auto;
1130
+ flex: 0 0 auto;
1092
1131
  }
1093
1132
  .toc summary {
1094
1133
  min-width: 0;
@@ -1387,6 +1426,7 @@ const html = `<!DOCTYPE html>
1387
1426
  flex: initial;
1388
1427
  }
1389
1428
  body[data-sidebar-open] .reader-sidebar { transform: translateX(0); }
1429
+ .sidebar-splitter { display: none; }
1390
1430
  .heading-anchor { opacity: 1; }
1391
1431
  }
1392
1432
  @media print {
@@ -1399,6 +1439,7 @@ const html = `<!DOCTYPE html>
1399
1439
  }
1400
1440
  .reader-sidebar,
1401
1441
  .sidebar-toggle,
1442
+ .sidebar-splitter,
1402
1443
  .sidebar-scrim { display: none !important; }
1403
1444
  .content { max-width: 100%; }
1404
1445
  .heading-anchor { display: none; }
@@ -1615,14 +1656,24 @@ ${mermaidInitTag}` : ''}
1615
1656
  const lower = haystack.toLowerCase();
1616
1657
  const index = lower.indexOf(query);
1617
1658
  if (index === -1) {
1618
- return haystack.slice(0, 140);
1659
+ return { text: haystack.slice(0, 80), matchStart: -1, matchLength: 0 };
1619
1660
  }
1620
- const start = Math.max(0, index - 50);
1621
- const end = Math.min(haystack.length, index + query.length + 70);
1622
- let snippet = haystack.slice(start, end).trim();
1623
- if (start > 0) snippet = '…' + snippet;
1624
- if (end < haystack.length) snippet += '…';
1625
- return snippet;
1661
+ const start = Math.max(0, index - 25);
1662
+ const end = Math.min(haystack.length, index + query.length + 45);
1663
+ let text = haystack.slice(start, end);
1664
+ let matchStart = index - start;
1665
+ if (start > 0) { text = '…' + text; matchStart += 1; }
1666
+ if (end < haystack.length) text += '…';
1667
+ return { text, matchStart, matchLength: query.length };
1668
+ }
1669
+
1670
+ function renderSnippetHtml(snippet) {
1671
+ if (!snippet) return '';
1672
+ if (snippet.matchStart < 0) return escapeHtml(snippet.text);
1673
+ const before = snippet.text.slice(0, snippet.matchStart);
1674
+ const hit = snippet.text.slice(snippet.matchStart, snippet.matchStart + snippet.matchLength);
1675
+ const after = snippet.text.slice(snippet.matchStart + snippet.matchLength);
1676
+ return escapeHtml(before) + '<mark>' + escapeHtml(hit) + '</mark>' + escapeHtml(after);
1626
1677
  }
1627
1678
 
1628
1679
  function clearMatchedTocState() {
@@ -1684,7 +1735,7 @@ ${mermaidInitTag}` : ''}
1684
1735
  list.innerHTML = readerState.results.map((result, index) => (
1685
1736
  '<button class="search-result-item' + (index === readerState.selectedResultIndex ? ' is-active' : '') + '" data-result-index="' + index + '" type="button">'
1686
1737
  + '<span class="search-result-title">' + escapeHtml(result.title) + '</span>'
1687
- + '<span class="search-result-snippet">' + escapeHtml(result.snippet) + '</span>'
1738
+ + '<span class="search-result-snippet">' + renderSnippetHtml(result.snippet) + '</span>'
1688
1739
  + '</button>'
1689
1740
  )).join('');
1690
1741
  }
@@ -1946,6 +1997,70 @@ ${mermaidInitTag}` : ''}
1946
1997
  }
1947
1998
  });
1948
1999
 
2000
+ // ── Sidebar splitter: drag to resize, persisted; double-click resets ─────
2001
+ const sidebarEl = document.querySelector('.reader-sidebar');
2002
+ const splitterEl = document.getElementById('sidebar-splitter');
2003
+ const SIDEBAR_WIDTH_KEY = 'md2doc.sidebar.width';
2004
+ function clampSidebarWidth(px) {
2005
+ return Math.min(Math.max(px, 180), Math.round(window.innerWidth * 0.5));
2006
+ }
2007
+ function applySidebarWidth(px) {
2008
+ document.documentElement.style.setProperty('--md2doc-sidebar-w', px + 'px');
2009
+ }
2010
+ try {
2011
+ const storedWidth = parseInt(localStorage.getItem(SIDEBAR_WIDTH_KEY), 10);
2012
+ if (Number.isFinite(storedWidth)) applySidebarWidth(clampSidebarWidth(storedWidth));
2013
+ } catch (e) { /* storage unavailable */ }
2014
+ if (splitterEl && sidebarEl) {
2015
+ let dragging = false;
2016
+ let dragMoved = false;
2017
+ let dragStartX = 0;
2018
+ let dragStartWidth = 0;
2019
+ splitterEl.addEventListener('pointerdown', (event) => {
2020
+ if (document.body.hasAttribute('data-toc-collapsed')) return;
2021
+ dragging = true;
2022
+ dragMoved = false;
2023
+ dragStartX = event.clientX;
2024
+ dragStartWidth = sidebarEl.offsetWidth;
2025
+ splitterEl.classList.add('is-dragging');
2026
+ if (splitterEl.setPointerCapture) splitterEl.setPointerCapture(event.pointerId);
2027
+ event.preventDefault();
2028
+ });
2029
+ splitterEl.addEventListener('pointermove', (event) => {
2030
+ if (!dragging) return;
2031
+ const delta = event.clientX - dragStartX;
2032
+ if (Math.abs(delta) >= 3) dragMoved = true;
2033
+ if (dragMoved) applySidebarWidth(clampSidebarWidth(dragStartWidth + delta));
2034
+ });
2035
+ function endSidebarDrag() {
2036
+ if (!dragging) return;
2037
+ dragging = false;
2038
+ splitterEl.classList.remove('is-dragging');
2039
+ if (!dragMoved) return;
2040
+ try { localStorage.setItem(SIDEBAR_WIDTH_KEY, String(sidebarEl.offsetWidth)); }
2041
+ catch (e) { /* storage unavailable */ }
2042
+ }
2043
+ splitterEl.addEventListener('pointerup', endSidebarDrag);
2044
+ splitterEl.addEventListener('pointercancel', endSidebarDrag);
2045
+ splitterEl.addEventListener('dblclick', () => {
2046
+ document.documentElement.style.removeProperty('--md2doc-sidebar-w');
2047
+ try { localStorage.removeItem(SIDEBAR_WIDTH_KEY); }
2048
+ catch (e) { /* storage unavailable */ }
2049
+ });
2050
+ }
2051
+
2052
+ // ── TOC horizontal peek: shift+wheel scrolls a hidden-overflow x-axis ─────
2053
+ const tocScrollEl = document.querySelector('.toc > .toc-list');
2054
+ if (tocScrollEl) {
2055
+ tocScrollEl.addEventListener('wheel', (event) => {
2056
+ if (!event.shiftKey) return;
2057
+ const delta = event.deltaX !== 0 ? event.deltaX : event.deltaY;
2058
+ if (!delta) return;
2059
+ event.preventDefault();
2060
+ tocScrollEl.scrollLeft += delta;
2061
+ }, { passive: false });
2062
+ }
2063
+
1949
2064
  // ── Zoom / resize scroll anchoring ────────────────────────────────────────
1950
2065
  // Browser zoom (and any window resize) reflows the column but leaves the
1951
2066
  // pixel scroll offset untouched, so the passage being read slides out of
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helping-ai-workflow/md2doc",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, and Graphviz support",
5
5
  "keywords": [
6
6
  "markdown",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "scripts": {
38
38
  "preinstall": "node scripts/preinstall.js",
39
- "test": "node test/md2doc.test.js && node test/images.test.js && node test/scroll-anchor.test.js && node test/lightbox.test.js && node test/cli.test.js && node test/code-operator.test.js"
39
+ "test": "node test/md2doc.test.js && node test/images.test.js && node test/scroll-anchor.test.js && node test/lightbox.test.js && node test/reader-panels.test.js && node test/cli.test.js && node test/code-operator.test.js"
40
40
  },
41
41
  "repository": {
42
42
  "type": "git",