reviw 0.7.1 → 0.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.
Files changed (3) hide show
  1. package/README.md +4 -4
  2. package/cli.cjs +383 -98
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -72,16 +72,16 @@ reviw changes.diff
72
72
  ## Screenshots
73
73
 
74
74
  ### CSV View
75
- ![CSV View](./assets/screenshot-csv.png)
75
+ ![CSV View](./assets/screenshot-csv.png?v=2)
76
76
 
77
77
  ### Markdown View
78
- ![Markdown View](./assets/screenshot-md.png)
78
+ ![Markdown View](./assets/screenshot-md.png?v=2)
79
79
 
80
80
  ### Diff View
81
- ![Diff View](./assets/screenshot-diff.png)
81
+ ![Diff View](./assets/screenshot-diff.png?v=2)
82
82
 
83
83
  ### Mermaid Fullscreen
84
- ![Mermaid Fullscreen](./assets/screenshot-mermaid.png)
84
+ ![Mermaid Fullscreen](./assets/screenshot-mermaid.png?v=2)
85
85
 
86
86
  ## Output Example
87
87
 
package/cli.cjs CHANGED
@@ -403,7 +403,60 @@ function loadMarkdown(filePath) {
403
403
  const raw = fs.readFileSync(filePath);
404
404
  const text = decodeBuffer(raw);
405
405
  const lines = text.split(/\r?\n/);
406
- const preview = marked.parse(text, { breaks: true });
406
+
407
+ // Parse YAML frontmatter
408
+ let frontmatterHtml = '';
409
+ let contentStart = 0;
410
+
411
+ if (lines[0] && lines[0].trim() === '---') {
412
+ let frontmatterEnd = -1;
413
+ for (let i = 1; i < lines.length; i++) {
414
+ if (lines[i].trim() === '---') {
415
+ frontmatterEnd = i;
416
+ break;
417
+ }
418
+ }
419
+
420
+ if (frontmatterEnd > 0) {
421
+ const frontmatterLines = lines.slice(1, frontmatterEnd);
422
+ const frontmatterText = frontmatterLines.join('\n');
423
+
424
+ try {
425
+ const frontmatter = yaml.load(frontmatterText);
426
+ if (frontmatter && typeof frontmatter === 'object') {
427
+ // Create HTML table for frontmatter
428
+ frontmatterHtml = '<div class="frontmatter-table"><table>';
429
+ frontmatterHtml += '<colgroup><col style="width:12%"><col style="width:88%"></colgroup>';
430
+ frontmatterHtml += '<thead><tr><th colspan="2">Document Metadata</th></tr></thead>';
431
+ frontmatterHtml += '<tbody>';
432
+
433
+ function renderValue(val) {
434
+ if (Array.isArray(val)) {
435
+ return val.map(v => '<span class="fm-tag">' + escapeHtmlChars(String(v)) + '</span>').join(' ');
436
+ }
437
+ if (typeof val === 'object' && val !== null) {
438
+ return '<pre>' + escapeHtmlChars(JSON.stringify(val, null, 2)) + '</pre>';
439
+ }
440
+ return escapeHtmlChars(String(val));
441
+ }
442
+
443
+ for (const [key, val] of Object.entries(frontmatter)) {
444
+ frontmatterHtml += '<tr><th>' + escapeHtmlChars(key) + '</th><td>' + renderValue(val) + '</td></tr>';
445
+ }
446
+
447
+ frontmatterHtml += '</tbody></table></div>';
448
+ contentStart = frontmatterEnd + 1;
449
+ }
450
+ } catch (e) {
451
+ // Invalid YAML, skip frontmatter rendering
452
+ }
453
+ }
454
+ }
455
+
456
+ // Parse markdown content (without frontmatter)
457
+ const contentText = lines.slice(contentStart).join('\n');
458
+ const preview = frontmatterHtml + marked.parse(contentText, { breaks: true });
459
+
407
460
  return {
408
461
  rows: lines.map((line) => [line]),
409
462
  cols: 1,
@@ -412,6 +465,14 @@ function loadMarkdown(filePath) {
412
465
  };
413
466
  }
414
467
 
468
+ function escapeHtmlChars(str) {
469
+ return str
470
+ .replace(/&/g, '&amp;')
471
+ .replace(/</g, '&lt;')
472
+ .replace(/>/g, '&gt;')
473
+ .replace(/"/g, '&quot;');
474
+ }
475
+
415
476
  function loadData(filePath) {
416
477
  const ext = path.extname(filePath).toLowerCase();
417
478
  if (ext === '.csv' || ext === '.tsv') {
@@ -1751,10 +1812,201 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1751
1812
  .md-preview img { max-width: 100%; height: auto; border-radius: 8px; }
1752
1813
  .md-preview code { background: rgba(255,255,255,0.08); padding: 2px 4px; border-radius: 4px; }
1753
1814
  .md-preview pre {
1754
- background: rgba(255,255,255,0.06);
1755
- padding: 8px 10px;
1815
+ background: var(--code-bg);
1816
+ padding: 12px 16px;
1756
1817
  border-radius: 8px;
1757
1818
  overflow: auto;
1819
+ border: 1px solid var(--border);
1820
+ }
1821
+ .md-preview pre code {
1822
+ background: none;
1823
+ padding: 0;
1824
+ font-size: 13px;
1825
+ line-height: 1.5;
1826
+ }
1827
+ .md-preview pre code.hljs {
1828
+ background: transparent;
1829
+ padding: 0;
1830
+ }
1831
+ /* YAML Frontmatter table */
1832
+ .frontmatter-table {
1833
+ margin-bottom: 20px;
1834
+ border-radius: 8px;
1835
+ overflow: hidden;
1836
+ border: 1px solid var(--border);
1837
+ background: var(--panel);
1838
+ }
1839
+ .frontmatter-table table {
1840
+ width: 100%;
1841
+ border-collapse: collapse;
1842
+ table-layout: fixed;
1843
+ }
1844
+ .frontmatter-table thead th {
1845
+ background: linear-gradient(135deg, rgba(147, 51, 234, 0.15), rgba(96, 165, 250, 0.15));
1846
+ color: var(--text);
1847
+ font-size: 12px;
1848
+ font-weight: 600;
1849
+ padding: 10px 16px;
1850
+ text-align: left;
1851
+ border-bottom: 1px solid var(--border);
1852
+ }
1853
+ .frontmatter-table tbody th {
1854
+ background: rgba(147, 51, 234, 0.08);
1855
+ color: #c084fc;
1856
+ font-weight: 500;
1857
+ font-size: 12px;
1858
+ padding: 8px 10px;
1859
+ text-align: left;
1860
+ border-bottom: 1px solid var(--border);
1861
+ vertical-align: top;
1862
+ }
1863
+ .frontmatter-table tbody td {
1864
+ padding: 8px 14px;
1865
+ font-size: 13px;
1866
+ border-bottom: 1px solid var(--border);
1867
+ word-break: break-word;
1868
+ }
1869
+ .frontmatter-table tbody tr:last-child th,
1870
+ .frontmatter-table tbody tr:last-child td {
1871
+ border-bottom: none;
1872
+ }
1873
+ .frontmatter-table .fm-tag {
1874
+ display: inline-block;
1875
+ background: rgba(96, 165, 250, 0.15);
1876
+ color: var(--accent);
1877
+ padding: 2px 8px;
1878
+ border-radius: 12px;
1879
+ font-size: 11px;
1880
+ margin-right: 4px;
1881
+ margin-bottom: 4px;
1882
+ }
1883
+ .frontmatter-table pre {
1884
+ margin: 0;
1885
+ background: var(--code-bg);
1886
+ padding: 8px;
1887
+ border-radius: 4px;
1888
+ font-size: 11px;
1889
+ }
1890
+ [data-theme="light"] .frontmatter-table tbody th {
1891
+ color: #7c3aed;
1892
+ }
1893
+ /* Markdown tables in preview */
1894
+ .md-preview table:not(.frontmatter-table table) {
1895
+ width: 100%;
1896
+ border-collapse: collapse;
1897
+ margin: 16px 0;
1898
+ border: 1px solid var(--border);
1899
+ border-radius: 8px;
1900
+ overflow: hidden;
1901
+ }
1902
+ .md-preview table:not(.frontmatter-table table) th,
1903
+ .md-preview table:not(.frontmatter-table table) td {
1904
+ width: 50%;
1905
+ padding: 10px 16px;
1906
+ text-align: left;
1907
+ border-bottom: 1px solid var(--border);
1908
+ }
1909
+ .md-preview table:not(.frontmatter-table table) th {
1910
+ background: rgba(255,255,255,0.05);
1911
+ }
1912
+ .md-preview table:not(.frontmatter-table table) th {
1913
+ background: var(--panel);
1914
+ font-weight: 600;
1915
+ font-size: 13px;
1916
+ }
1917
+ .md-preview table:not(.frontmatter-table table) td {
1918
+ font-size: 13px;
1919
+ }
1920
+ .md-preview table:not(.frontmatter-table table) tr:last-child td {
1921
+ border-bottom: none;
1922
+ }
1923
+ .md-preview table:not(.frontmatter-table table) tr:hover td {
1924
+ background: var(--hover-bg);
1925
+ }
1926
+ /* Source table (右ペイン) */
1927
+ .table-box table {
1928
+ table-layout: fixed;
1929
+ width: 100%;
1930
+ }
1931
+ .table-box th,
1932
+ .table-box td {
1933
+ word-break: break-word;
1934
+ min-width: 140px;
1935
+ }
1936
+ .table-box th:first-child,
1937
+ .table-box td:first-child {
1938
+ min-width: 320px;
1939
+ max-width: 480px;
1940
+ }
1941
+ /* Image fullscreen overlay */
1942
+ .image-fullscreen-overlay {
1943
+ position: fixed;
1944
+ inset: 0;
1945
+ background: rgba(0, 0, 0, 0.9);
1946
+ z-index: 1001;
1947
+ display: none;
1948
+ justify-content: center;
1949
+ align-items: center;
1950
+ }
1951
+ .image-fullscreen-overlay.visible {
1952
+ display: flex;
1953
+ }
1954
+ .image-close-btn {
1955
+ position: absolute;
1956
+ top: 14px;
1957
+ right: 14px;
1958
+ width: 40px;
1959
+ height: 40px;
1960
+ display: flex;
1961
+ align-items: center;
1962
+ justify-content: center;
1963
+ background: rgba(0, 0, 0, 0.55);
1964
+ border: 1px solid rgba(255, 255, 255, 0.25);
1965
+ border-radius: 50%;
1966
+ cursor: pointer;
1967
+ color: #fff;
1968
+ font-size: 18px;
1969
+ z-index: 10;
1970
+ backdrop-filter: blur(4px);
1971
+ transition: background 120ms ease, transform 120ms ease;
1972
+ }
1973
+ .image-close-btn:hover {
1974
+ background: rgba(0, 0, 0, 0.75);
1975
+ transform: scale(1.04);
1976
+ }
1977
+ .image-container {
1978
+ max-width: 90vw;
1979
+ max-height: 90vh;
1980
+ display: flex;
1981
+ justify-content: center;
1982
+ align-items: center;
1983
+ }
1984
+ .image-container img {
1985
+ max-width: 100%;
1986
+ max-height: 90vh;
1987
+ object-fit: contain;
1988
+ border-radius: 8px;
1989
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
1990
+ }
1991
+ /* Copy notification toast */
1992
+ .copy-toast {
1993
+ position: fixed;
1994
+ bottom: 60px;
1995
+ left: 50%;
1996
+ transform: translateX(-50%) translateY(20px);
1997
+ background: var(--accent);
1998
+ color: var(--text-inverse);
1999
+ padding: 8px 16px;
2000
+ border-radius: 8px;
2001
+ font-size: 13px;
2002
+ opacity: 0;
2003
+ pointer-events: none;
2004
+ transition: opacity 200ms ease, transform 200ms ease;
2005
+ z-index: 1000;
2006
+ }
2007
+ .copy-toast.visible {
2008
+ opacity: 1;
2009
+ transform: translateX(-50%) translateY(0);
1758
2010
  }
1759
2011
  @media (max-width: 960px) {
1760
2012
  .md-layout { flex-direction: column; }
@@ -1859,6 +2111,8 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1859
2111
  .mermaid-container .mermaid svg {
1860
2112
  max-width: 100%;
1861
2113
  height: auto;
2114
+ cursor: pointer;
2115
+ pointer-events: auto;
1862
2116
  }
1863
2117
  .mermaid-fullscreen-btn {
1864
2118
  position: absolute;
@@ -1923,39 +2177,6 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1923
2177
  .fullscreen-content .mermaid svg {
1924
2178
  display: block;
1925
2179
  }
1926
- /* Minimap */
1927
- .minimap {
1928
- position: absolute;
1929
- top: 70px;
1930
- right: 20px;
1931
- width: 200px;
1932
- height: 150px;
1933
- background: var(--panel-alpha);
1934
- border: 1px solid var(--border);
1935
- border-radius: 8px;
1936
- overflow: hidden;
1937
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1938
- }
1939
- .minimap-content {
1940
- width: 100%;
1941
- height: 100%;
1942
- display: flex;
1943
- align-items: center;
1944
- justify-content: center;
1945
- padding: 8px;
1946
- }
1947
- .minimap-content svg {
1948
- max-width: 100%;
1949
- max-height: 100%;
1950
- opacity: 0.6;
1951
- }
1952
- .minimap-viewport {
1953
- position: absolute;
1954
- border: 2px solid var(--accent);
1955
- background: rgba(102, 126, 234, 0.2);
1956
- pointer-events: none;
1957
- border-radius: 2px;
1958
- }
1959
2180
  /* Error toast */
1960
2181
  .mermaid-error-toast {
1961
2182
  position: fixed;
@@ -1977,6 +2198,9 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1977
2198
  .mermaid-error-toast.visible { display: block; }
1978
2199
  </style>
1979
2200
  <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
2201
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github-dark.min.css" id="hljs-theme-dark">
2202
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github.min.css" id="hljs-theme-light" disabled>
2203
+ <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js"></script>
1980
2204
  </head>
1981
2205
  <body>
1982
2206
  <header>
@@ -2106,12 +2330,13 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2106
2330
  <div class="fullscreen-content" id="fs-content">
2107
2331
  <div class="mermaid-wrapper" id="fs-wrapper"></div>
2108
2332
  </div>
2109
- <div class="minimap" id="fs-minimap">
2110
- <div class="minimap-content" id="fs-minimap-content"></div>
2111
- <div class="minimap-viewport" id="fs-minimap-viewport"></div>
2112
- </div>
2113
2333
  </div>
2114
2334
  <div class="mermaid-error-toast" id="mermaid-error-toast"></div>
2335
+ <div class="copy-toast" id="copy-toast">Copied to clipboard!</div>
2336
+ <div class="image-fullscreen-overlay" id="image-fullscreen">
2337
+ <button class="image-close-btn" id="image-close" aria-label="Close image" title="Close (ESC)">✕</button>
2338
+ <div class="image-container" id="image-container"></div>
2339
+ </div>
2115
2340
 
2116
2341
  <script>
2117
2342
  const DATA = ${serialized};
@@ -2133,12 +2358,18 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2133
2358
  }
2134
2359
 
2135
2360
  function setTheme(theme) {
2361
+ const hljsDark = document.getElementById('hljs-theme-dark');
2362
+ const hljsLight = document.getElementById('hljs-theme-light');
2136
2363
  if (theme === 'light') {
2137
2364
  document.documentElement.setAttribute('data-theme', 'light');
2138
2365
  themeIcon.textContent = '☀️';
2366
+ if (hljsDark) hljsDark.disabled = true;
2367
+ if (hljsLight) hljsLight.disabled = false;
2139
2368
  } else {
2140
2369
  document.documentElement.removeAttribute('data-theme');
2141
2370
  themeIcon.textContent = '🌙';
2371
+ if (hljsDark) hljsDark.disabled = false;
2372
+ if (hljsLight) hljsLight.disabled = true;
2142
2373
  }
2143
2374
  localStorage.setItem('reviw-theme', theme);
2144
2375
  }
@@ -2182,11 +2413,14 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2182
2413
  const freezeRowCheck = document.getElementById('freeze-row-check');
2183
2414
 
2184
2415
  const ROW_HEADER_WIDTH = 28;
2185
- const MIN_COL_WIDTH = 80;
2186
- const MAX_COL_WIDTH = 420;
2187
- const DEFAULT_COL_WIDTH = 120;
2416
+ const MIN_COL_WIDTH = 140;
2417
+ const MAX_COL_WIDTH = 520;
2418
+ const DEFAULT_COL_WIDTH = 240;
2188
2419
 
2189
2420
  let colWidths = Array.from({ length: MAX_COLS }, () => DEFAULT_COL_WIDTH);
2421
+ if (MODE !== 'csv' && MAX_COLS === 1) {
2422
+ colWidths[0] = 480;
2423
+ }
2190
2424
  let panelOpen = false;
2191
2425
  let filters = {}; // colIndex -> predicate
2192
2426
  let filterTargetCol = null;
@@ -2343,6 +2577,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2343
2577
  const th = document.createElement('th');
2344
2578
  th.textContent = rIdx + 1;
2345
2579
  tr.appendChild(th);
2580
+
2346
2581
  for (let c = 0; c < MAX_COLS; c += 1) {
2347
2582
  const td = document.createElement('td');
2348
2583
  const val = row[c] || '';
@@ -2422,11 +2657,48 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2422
2657
  isDragging = false;
2423
2658
  document.body.classList.remove('dragging');
2424
2659
  if (selection) {
2660
+ // Copy selected text to clipboard
2661
+ copySelectionToClipboard(selection);
2425
2662
  openCardForSelection();
2426
2663
  }
2427
2664
  });
2428
2665
  }
2429
2666
 
2667
+ // Copy selected range to clipboard
2668
+ function copySelectionToClipboard(sel) {
2669
+ const { startRow, endRow, startCol, endCol } = sel;
2670
+ const lines = [];
2671
+ for (let r = startRow; r <= endRow; r++) {
2672
+ const rowData = [];
2673
+ for (let c = startCol; c <= endCol; c++) {
2674
+ const td = tbody.querySelector('td[data-row="' + r + '"][data-col="' + c + '"]');
2675
+ if (td) {
2676
+ // Get text content (strip HTML tags from inline code highlighting)
2677
+ rowData.push(td.textContent || '');
2678
+ }
2679
+ }
2680
+ lines.push(rowData.join('\\t'));
2681
+ }
2682
+ const text = lines.join('\\n');
2683
+ if (text && navigator.clipboard) {
2684
+ navigator.clipboard.writeText(text).then(() => {
2685
+ showCopyToast();
2686
+ }).catch(() => {
2687
+ // Fallback: silent fail
2688
+ });
2689
+ }
2690
+ }
2691
+
2692
+ // Show copy toast notification
2693
+ function showCopyToast() {
2694
+ const toast = document.getElementById('copy-toast');
2695
+ if (!toast) return;
2696
+ toast.classList.add('visible');
2697
+ setTimeout(() => {
2698
+ toast.classList.remove('visible');
2699
+ }, 1500);
2700
+ }
2701
+
2430
2702
  function openCardForSelection() {
2431
2703
  if (!selection) return;
2432
2704
  const { startRow, endRow, startCol, endCol } = selection;
@@ -3162,15 +3434,12 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3162
3434
  const fsWrapper = document.getElementById('fs-wrapper');
3163
3435
  const fsContent = document.getElementById('fs-content');
3164
3436
  const fsZoomInfo = document.getElementById('fs-zoom-info');
3165
- const minimapContent = document.getElementById('fs-minimap-content');
3166
- const minimapViewport = document.getElementById('fs-minimap-viewport');
3167
3437
  let currentZoom = 1;
3168
3438
  let initialZoom = 1;
3169
3439
  let panX = 0, panY = 0;
3170
3440
  let isPanning = false;
3171
3441
  let startX, startY;
3172
3442
  let svgNaturalWidth = 0, svgNaturalHeight = 0;
3173
- let minimapScale = 1;
3174
3443
 
3175
3444
  function openFullscreen(mermaidEl) {
3176
3445
  const svg = mermaidEl.querySelector('svg');
@@ -3179,11 +3448,6 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3179
3448
  const clonedSvg = svg.cloneNode(true);
3180
3449
  fsWrapper.appendChild(clonedSvg);
3181
3450
 
3182
- // Setup minimap
3183
- minimapContent.innerHTML = '';
3184
- const minimapSvg = svg.cloneNode(true);
3185
- minimapContent.appendChild(minimapSvg);
3186
-
3187
3451
  // Get SVG's intrinsic/natural size from viewBox or attributes
3188
3452
  const viewBox = svg.getAttribute('viewBox');
3189
3453
  let naturalWidth, naturalHeight;
@@ -3200,11 +3464,6 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3200
3464
  svgNaturalWidth = naturalWidth;
3201
3465
  svgNaturalHeight = naturalHeight;
3202
3466
 
3203
- // Calculate minimap scale
3204
- const minimapMaxWidth = 184; // 200 - 16 padding
3205
- const minimapMaxHeight = 134; // 150 - 16 padding
3206
- minimapScale = Math.min(minimapMaxWidth / naturalWidth, minimapMaxHeight / naturalHeight);
3207
-
3208
3467
  clonedSvg.style.width = naturalWidth + 'px';
3209
3468
  clonedSvg.style.height = naturalHeight + 'px';
3210
3469
 
@@ -3236,48 +3495,6 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3236
3495
  function updateTransform() {
3237
3496
  fsWrapper.style.transform = 'translate(' + panX + 'px, ' + panY + 'px) scale(' + currentZoom + ')';
3238
3497
  fsZoomInfo.textContent = Math.round(currentZoom * 100) + '%';
3239
- updateMinimap();
3240
- }
3241
-
3242
- function updateMinimap() {
3243
- if (!svgNaturalWidth || !svgNaturalHeight) return;
3244
-
3245
- const viewportWidth = window.innerWidth - 40;
3246
- const viewportHeight = window.innerHeight - 80;
3247
-
3248
- // Minimap dimensions
3249
- const mmWidth = 184;
3250
- const mmHeight = 134;
3251
- const mmPadding = 8;
3252
-
3253
- // SVG size in minimap (centered)
3254
- const mmSvgWidth = svgNaturalWidth * minimapScale;
3255
- const mmSvgHeight = svgNaturalHeight * minimapScale;
3256
- const mmSvgLeft = (mmWidth - mmSvgWidth) / 2 + mmPadding;
3257
- const mmSvgTop = (mmHeight - mmSvgHeight) / 2 + mmPadding;
3258
-
3259
- // Calculate visible area in SVG coordinates (accounting for transform origin at 0,0)
3260
- // panX/panY are the translation values, currentZoom is the scale
3261
- // The visible area starts at -panX/currentZoom in SVG coordinates
3262
- const visibleLeft = Math.max(0, -panX / currentZoom);
3263
- const visibleTop = Math.max(0, (-panY + 60) / currentZoom);
3264
- const visibleWidth = viewportWidth / currentZoom;
3265
- const visibleHeight = viewportHeight / currentZoom;
3266
-
3267
- // Clamp to SVG bounds
3268
- const clampedLeft = Math.min(visibleLeft, svgNaturalWidth);
3269
- const clampedTop = Math.min(visibleTop, svgNaturalHeight);
3270
-
3271
- // Position viewport indicator in minimap coordinates
3272
- const vpLeft = mmSvgLeft + clampedLeft * minimapScale;
3273
- const vpTop = mmSvgTop + clampedTop * minimapScale;
3274
- const vpWidth = Math.min(mmWidth - vpLeft + mmPadding, visibleWidth * minimapScale);
3275
- const vpHeight = Math.min(mmHeight - vpTop + mmPadding, visibleHeight * minimapScale);
3276
-
3277
- minimapViewport.style.left = vpLeft + 'px';
3278
- minimapViewport.style.top = vpTop + 'px';
3279
- minimapViewport.style.width = Math.max(20, vpWidth) + 'px';
3280
- minimapViewport.style.height = Math.max(15, vpHeight) + 'px';
3281
3498
  }
3282
3499
 
3283
3500
  // Use multiplicative zoom for consistent behavior
@@ -3350,6 +3567,74 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3350
3567
  }
3351
3568
  });
3352
3569
  })();
3570
+
3571
+ // --- Highlight.js Initialization ---
3572
+ (function initHighlightJS() {
3573
+ if (typeof hljs === 'undefined') return;
3574
+
3575
+ // Highlight all code blocks in preview (skip mermaid blocks)
3576
+ const preview = document.querySelector('.md-preview');
3577
+ if (preview) {
3578
+ preview.querySelectorAll('pre code').forEach(block => {
3579
+ // Skip if inside mermaid container or already highlighted
3580
+ if (block.closest('.mermaid-container') || block.classList.contains('hljs')) {
3581
+ return;
3582
+ }
3583
+ hljs.highlightElement(block);
3584
+ });
3585
+ }
3586
+ })();
3587
+
3588
+ // --- Image Fullscreen ---
3589
+ (function initImageFullscreen() {
3590
+ const preview = document.querySelector('.md-preview');
3591
+ if (!preview) return;
3592
+
3593
+ const imageOverlay = document.getElementById('image-fullscreen');
3594
+ const imageContainer = document.getElementById('image-container');
3595
+ const imageClose = document.getElementById('image-close');
3596
+ if (!imageOverlay || !imageContainer) return;
3597
+
3598
+ function closeImageOverlay() {
3599
+ imageOverlay.classList.remove('visible');
3600
+ imageContainer.innerHTML = '';
3601
+ }
3602
+
3603
+ if (imageClose) {
3604
+ imageClose.addEventListener('click', closeImageOverlay);
3605
+ }
3606
+
3607
+ if (imageOverlay) {
3608
+ imageOverlay.addEventListener('click', (e) => {
3609
+ if (e.target === imageOverlay) closeImageOverlay();
3610
+ });
3611
+ }
3612
+
3613
+ document.addEventListener('keydown', (e) => {
3614
+ if (e.key === 'Escape' && imageOverlay.classList.contains('visible')) {
3615
+ closeImageOverlay();
3616
+ }
3617
+ });
3618
+
3619
+ preview.querySelectorAll('img').forEach(img => {
3620
+ img.style.cursor = 'pointer';
3621
+ img.title = 'Click to view fullscreen';
3622
+
3623
+ img.addEventListener('click', (e) => {
3624
+ e.stopPropagation();
3625
+
3626
+ imageContainer.innerHTML = '';
3627
+ const clonedImg = img.cloneNode(true);
3628
+ clonedImg.style.maxWidth = '90vw';
3629
+ clonedImg.style.maxHeight = '90vh';
3630
+ clonedImg.style.objectFit = 'contain';
3631
+ clonedImg.style.cursor = 'default';
3632
+ imageContainer.appendChild(clonedImg);
3633
+
3634
+ imageOverlay.classList.add('visible');
3635
+ });
3636
+ });
3637
+ })();
3353
3638
  </script>
3354
3639
  </body>
3355
3640
  </html>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reviw",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "Lightweight file reviewer with in-browser comments for CSV, TSV, Markdown, and Git diffs.",
5
5
  "type": "module",
6
6
  "bin": {