reviw 0.6.0 → 0.7.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/cli.cjs +418 -0
  2. package/package.json +1 -1
package/cli.cjs CHANGED
@@ -1842,7 +1842,141 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1842
1842
  header { flex-direction: column; align-items: flex-start; }
1843
1843
  .comment-list { width: calc(100% - 24px); right: 12px; }
1844
1844
  }
1845
+ /* Mermaid diagram styles */
1846
+ .mermaid-container {
1847
+ position: relative;
1848
+ margin: 16px 0;
1849
+ background: var(--panel);
1850
+ border: 1px solid var(--border);
1851
+ border-radius: 8px;
1852
+ padding: 16px;
1853
+ overflow: hidden;
1854
+ }
1855
+ .mermaid-container .mermaid {
1856
+ display: flex;
1857
+ justify-content: center;
1858
+ }
1859
+ .mermaid-container .mermaid svg {
1860
+ max-width: 100%;
1861
+ height: auto;
1862
+ }
1863
+ .mermaid-fullscreen-btn {
1864
+ position: absolute;
1865
+ top: 8px;
1866
+ right: 8px;
1867
+ background: var(--selected-bg);
1868
+ border: 1px solid var(--border);
1869
+ border-radius: 6px;
1870
+ padding: 6px 10px;
1871
+ cursor: pointer;
1872
+ color: var(--text);
1873
+ font-size: 12px;
1874
+ z-index: 2;
1875
+ display: flex;
1876
+ align-items: center;
1877
+ gap: 4px;
1878
+ }
1879
+ .mermaid-fullscreen-btn:hover { background: var(--hover-bg); }
1880
+ /* Fullscreen overlay */
1881
+ .fullscreen-overlay {
1882
+ position: fixed;
1883
+ inset: 0;
1884
+ background: var(--bg);
1885
+ z-index: 1000;
1886
+ display: none;
1887
+ flex-direction: column;
1888
+ }
1889
+ .fullscreen-overlay.visible { display: flex; }
1890
+ .fullscreen-header {
1891
+ display: flex;
1892
+ justify-content: space-between;
1893
+ align-items: center;
1894
+ padding: 12px 20px;
1895
+ background: var(--panel-alpha);
1896
+ border-bottom: 1px solid var(--border);
1897
+ }
1898
+ .fullscreen-header h3 { margin: 0; font-size: 14px; }
1899
+ .fullscreen-controls { display: flex; gap: 8px; align-items: center; }
1900
+ .fullscreen-controls button {
1901
+ background: var(--selected-bg);
1902
+ border: 1px solid var(--border);
1903
+ border-radius: 6px;
1904
+ padding: 6px 12px;
1905
+ cursor: pointer;
1906
+ color: var(--text);
1907
+ font-size: 13px;
1908
+ }
1909
+ .fullscreen-controls button:hover { background: var(--hover-bg); }
1910
+ .fullscreen-controls .zoom-info { font-size: 12px; color: var(--muted); min-width: 50px; text-align: center; }
1911
+ .fullscreen-content {
1912
+ flex: 1;
1913
+ overflow: hidden;
1914
+ position: relative;
1915
+ cursor: grab;
1916
+ }
1917
+ .fullscreen-content:active { cursor: grabbing; }
1918
+ .fullscreen-content .mermaid-wrapper {
1919
+ position: absolute;
1920
+ transform-origin: 0 0;
1921
+ padding: 40px;
1922
+ }
1923
+ .fullscreen-content .mermaid svg {
1924
+ display: block;
1925
+ }
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
+ /* Error toast */
1960
+ .mermaid-error-toast {
1961
+ position: fixed;
1962
+ bottom: 20px;
1963
+ left: 50%;
1964
+ transform: translateX(-50%);
1965
+ background: #dc3545;
1966
+ color: white;
1967
+ padding: 12px 24px;
1968
+ border-radius: 8px;
1969
+ font-size: 13px;
1970
+ max-width: 80%;
1971
+ z-index: 2000;
1972
+ display: none;
1973
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
1974
+ white-space: pre-wrap;
1975
+ font-family: monospace;
1976
+ }
1977
+ .mermaid-error-toast.visible { display: block; }
1845
1978
  </style>
1979
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
1846
1980
  </head>
1847
1981
  <body>
1848
1982
  <header>
@@ -1958,6 +2092,27 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1958
2092
  </div>
1959
2093
  </div>
1960
2094
 
2095
+ <div class="fullscreen-overlay" id="mermaid-fullscreen">
2096
+ <div class="fullscreen-header">
2097
+ <h3>Mermaid Diagram</h3>
2098
+ <div class="fullscreen-controls">
2099
+ <button id="fs-zoom-out">−</button>
2100
+ <span class="zoom-info" id="fs-zoom-info">100%</span>
2101
+ <button id="fs-zoom-in">+</button>
2102
+ <button id="fs-reset">Reset</button>
2103
+ <button id="fs-close">Close (ESC)</button>
2104
+ </div>
2105
+ </div>
2106
+ <div class="fullscreen-content" id="fs-content">
2107
+ <div class="mermaid-wrapper" id="fs-wrapper"></div>
2108
+ </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
+ </div>
2114
+ <div class="mermaid-error-toast" id="mermaid-error-toast"></div>
2115
+
1961
2116
  <script>
1962
2117
  const DATA = ${serialized};
1963
2118
  const MAX_COLS = ${cols};
@@ -2932,6 +3087,269 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2932
3087
  mdRight.addEventListener('scroll', () => syncScroll(mdRight, mdLeft, 'right'), { passive: true });
2933
3088
  }
2934
3089
  }
3090
+
3091
+ // --- Mermaid Initialization ---
3092
+ (function initMermaid() {
3093
+ if (typeof mermaid === 'undefined') return;
3094
+
3095
+ const errorToast = document.getElementById('mermaid-error-toast');
3096
+ let errorTimeout;
3097
+
3098
+ function showError(msg) {
3099
+ errorToast.textContent = msg;
3100
+ errorToast.classList.add('visible');
3101
+ console.error('[Mermaid Error]', msg);
3102
+ clearTimeout(errorTimeout);
3103
+ errorTimeout = setTimeout(() => errorToast.classList.remove('visible'), 8000);
3104
+ }
3105
+
3106
+ mermaid.initialize({
3107
+ startOnLoad: false,
3108
+ theme: document.documentElement.getAttribute('data-theme') === 'light' ? 'default' : 'dark',
3109
+ securityLevel: 'loose',
3110
+ logLevel: 'error'
3111
+ });
3112
+
3113
+ // Find all mermaid code blocks in preview
3114
+ const preview = document.querySelector('.md-preview');
3115
+ if (!preview) return;
3116
+
3117
+ const codeBlocks = preview.querySelectorAll('pre code.language-mermaid, pre code');
3118
+ codeBlocks.forEach((code, idx) => {
3119
+ const pre = code.parentElement;
3120
+ const text = code.textContent.trim();
3121
+
3122
+ // Check if it's mermaid content
3123
+ if (!code.classList.contains('language-mermaid') && !text.match(/^(graph|flowchart|sequenceDiagram|classDiagram|stateDiagram|erDiagram|journey|gantt|pie|gitGraph|mindmap|timeline)/)) {
3124
+ return;
3125
+ }
3126
+
3127
+ // Create container
3128
+ const container = document.createElement('div');
3129
+ container.className = 'mermaid-container';
3130
+ container.style.cursor = 'pointer';
3131
+ container.title = 'Click to view fullscreen';
3132
+
3133
+ const mermaidDiv = document.createElement('div');
3134
+ mermaidDiv.className = 'mermaid';
3135
+ mermaidDiv.id = 'mermaid-' + idx;
3136
+ mermaidDiv.textContent = text;
3137
+
3138
+ // Click anywhere on container to open fullscreen
3139
+ container.addEventListener('click', () => openFullscreen(mermaidDiv));
3140
+
3141
+ container.appendChild(mermaidDiv);
3142
+ pre.replaceWith(container);
3143
+ });
3144
+
3145
+ // Render all mermaid diagrams with error handling
3146
+ mermaid.run().catch(err => {
3147
+ showError('Mermaid Syntax Error: ' + (err.message || err));
3148
+ });
3149
+
3150
+ // Watch for render errors in DOM
3151
+ setTimeout(() => {
3152
+ document.querySelectorAll('.mermaid').forEach(el => {
3153
+ if (el.querySelector('.error-text, .error-icon')) {
3154
+ const errText = el.textContent;
3155
+ showError('Mermaid Parse Error: ' + errText.slice(0, 200));
3156
+ }
3157
+ });
3158
+ }, 500);
3159
+
3160
+ // Fullscreen functionality
3161
+ const fsOverlay = document.getElementById('mermaid-fullscreen');
3162
+ const fsWrapper = document.getElementById('fs-wrapper');
3163
+ const fsContent = document.getElementById('fs-content');
3164
+ const fsZoomInfo = document.getElementById('fs-zoom-info');
3165
+ const minimapContent = document.getElementById('fs-minimap-content');
3166
+ const minimapViewport = document.getElementById('fs-minimap-viewport');
3167
+ let currentZoom = 1;
3168
+ let initialZoom = 1;
3169
+ let panX = 0, panY = 0;
3170
+ let isPanning = false;
3171
+ let startX, startY;
3172
+ let svgNaturalWidth = 0, svgNaturalHeight = 0;
3173
+ let minimapScale = 1;
3174
+
3175
+ function openFullscreen(mermaidEl) {
3176
+ const svg = mermaidEl.querySelector('svg');
3177
+ if (!svg) return;
3178
+ fsWrapper.innerHTML = '';
3179
+ const clonedSvg = svg.cloneNode(true);
3180
+ fsWrapper.appendChild(clonedSvg);
3181
+
3182
+ // Setup minimap
3183
+ minimapContent.innerHTML = '';
3184
+ const minimapSvg = svg.cloneNode(true);
3185
+ minimapContent.appendChild(minimapSvg);
3186
+
3187
+ // Get SVG's intrinsic/natural size from viewBox or attributes
3188
+ const viewBox = svg.getAttribute('viewBox');
3189
+ let naturalWidth, naturalHeight;
3190
+
3191
+ if (viewBox) {
3192
+ const parts = viewBox.split(/[\\s,]+/);
3193
+ naturalWidth = parseFloat(parts[2]) || 800;
3194
+ naturalHeight = parseFloat(parts[3]) || 600;
3195
+ } else {
3196
+ naturalWidth = parseFloat(svg.getAttribute('width')) || svg.getBoundingClientRect().width || 800;
3197
+ naturalHeight = parseFloat(svg.getAttribute('height')) || svg.getBoundingClientRect().height || 600;
3198
+ }
3199
+
3200
+ svgNaturalWidth = naturalWidth;
3201
+ svgNaturalHeight = naturalHeight;
3202
+
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
+ clonedSvg.style.width = naturalWidth + 'px';
3209
+ clonedSvg.style.height = naturalHeight + 'px';
3210
+
3211
+ // Calculate fit-to-viewport zoom
3212
+ const viewportHeight = window.innerHeight - 80;
3213
+ const viewportWidth = window.innerWidth - 40;
3214
+
3215
+ const zoomForHeight = viewportHeight / naturalHeight;
3216
+ const zoomForWidth = viewportWidth / naturalWidth;
3217
+ const fitZoom = Math.min(zoomForHeight, zoomForWidth);
3218
+
3219
+ currentZoom = fitZoom;
3220
+ initialZoom = fitZoom;
3221
+
3222
+ // Center the SVG in viewport
3223
+ const scaledWidth = naturalWidth * currentZoom;
3224
+ const scaledHeight = naturalHeight * currentZoom;
3225
+ panX = (viewportWidth - scaledWidth) / 2 + 20;
3226
+ panY = (viewportHeight - scaledHeight) / 2 + 60;
3227
+
3228
+ updateTransform();
3229
+ fsOverlay.classList.add('visible');
3230
+ }
3231
+
3232
+ function closeFullscreen() {
3233
+ fsOverlay.classList.remove('visible');
3234
+ }
3235
+
3236
+ function updateTransform() {
3237
+ fsWrapper.style.transform = 'translate(' + panX + 'px, ' + panY + 'px) scale(' + currentZoom + ')';
3238
+ 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
+ }
3282
+
3283
+ // Use multiplicative zoom for consistent behavior
3284
+ function zoomAt(factor, clientX, clientY) {
3285
+ const oldZoom = currentZoom;
3286
+ currentZoom = Math.max(0.1, Math.min(10, currentZoom * factor));
3287
+
3288
+ // Zoom around mouse position
3289
+ const fsRect = fsContent.getBoundingClientRect();
3290
+ const mouseX = clientX - fsRect.left;
3291
+ const mouseY = clientY - fsRect.top;
3292
+
3293
+ const zoomRatio = currentZoom / oldZoom;
3294
+ panX = mouseX - (mouseX - panX) * zoomRatio;
3295
+ panY = mouseY - (mouseY - panY) * zoomRatio;
3296
+
3297
+ updateTransform();
3298
+ }
3299
+
3300
+ function zoom(factor) {
3301
+ const fsRect = fsContent.getBoundingClientRect();
3302
+ zoomAt(factor, fsRect.left + fsRect.width / 2, fsRect.top + fsRect.height / 2);
3303
+ }
3304
+
3305
+ document.getElementById('fs-zoom-in').addEventListener('click', () => zoom(1.25));
3306
+ document.getElementById('fs-zoom-out').addEventListener('click', () => zoom(0.8));
3307
+ document.getElementById('fs-reset').addEventListener('click', () => {
3308
+ currentZoom = initialZoom;
3309
+ const viewportHeight = window.innerHeight - 80;
3310
+ const viewportWidth = window.innerWidth - 40;
3311
+ const scaledWidth = svgNaturalWidth * currentZoom;
3312
+ const scaledHeight = svgNaturalHeight * currentZoom;
3313
+ panX = (viewportWidth - scaledWidth) / 2 + 20;
3314
+ panY = (viewportHeight - scaledHeight) / 2 + 60;
3315
+ updateTransform();
3316
+ });
3317
+ document.getElementById('fs-close').addEventListener('click', closeFullscreen);
3318
+
3319
+ // Pan with mouse drag
3320
+ fsContent.addEventListener('mousedown', (e) => {
3321
+ isPanning = true;
3322
+ startX = e.clientX - panX;
3323
+ startY = e.clientY - panY;
3324
+ fsContent.style.cursor = 'grabbing';
3325
+ });
3326
+
3327
+ document.addEventListener('mousemove', (e) => {
3328
+ if (!isPanning) return;
3329
+ panX = e.clientX - startX;
3330
+ panY = e.clientY - startY;
3331
+ updateTransform();
3332
+ });
3333
+
3334
+ document.addEventListener('mouseup', () => {
3335
+ isPanning = false;
3336
+ fsContent.style.cursor = 'grab';
3337
+ });
3338
+
3339
+ // Zoom with mouse wheel - use multiplicative factor
3340
+ fsContent.addEventListener('wheel', (e) => {
3341
+ e.preventDefault();
3342
+ const factor = e.deltaY > 0 ? 0.9 : 1.1;
3343
+ zoomAt(factor, e.clientX, e.clientY);
3344
+ }, { passive: false });
3345
+
3346
+ // ESC to close
3347
+ document.addEventListener('keydown', (e) => {
3348
+ if (e.key === 'Escape' && fsOverlay.classList.contains('visible')) {
3349
+ closeFullscreen();
3350
+ }
3351
+ });
3352
+ })();
2935
3353
  </script>
2936
3354
  </body>
2937
3355
  </html>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reviw",
3
- "version": "0.6.0",
3
+ "version": "0.7.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": {