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.
- package/cli.cjs +418 -0
- 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>`;
|