reviw 0.11.3 → 0.13.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/README.md +3 -0
- package/cli.cjs +382 -91
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -90,6 +90,9 @@ reviw changes.diff
|
|
|
90
90
|
### Mermaid Fullscreen
|
|
91
91
|

|
|
92
92
|
|
|
93
|
+
### Submit Review Dialog
|
|
94
|
+

|
|
95
|
+
|
|
93
96
|
## Output Example
|
|
94
97
|
|
|
95
98
|
```yaml
|
package/cli.cjs
CHANGED
|
@@ -1254,6 +1254,7 @@ function diffHtmlTemplate(diffData) {
|
|
|
1254
1254
|
<label><input type="checkbox" id="prompt-reviw" checked /> Open in REVIW next time.</label>
|
|
1255
1255
|
<label><input type="checkbox" id="prompt-screenshots" checked /> Update all screenshots and videos.</label>
|
|
1256
1256
|
<label><input type="checkbox" id="prompt-user-feedback-todo" checked /> Add the user's feedback to the Todo list, and do not check it off without the user's approval.</label>
|
|
1257
|
+
<label><input type="checkbox" id="prompt-deep-dive" checked /> Before implementing, deeply probe the user's request. If using Claude Code, start with AskUserQuestion and EnterPlanMode; otherwise achieve the same depth through interactive questions and planning, even if the UI differs.</label>
|
|
1257
1258
|
</div>
|
|
1258
1259
|
<div class="modal-actions">
|
|
1259
1260
|
<button id="modal-cancel">Cancel</button>
|
|
@@ -1628,7 +1629,8 @@ function diffHtmlTemplate(diffData) {
|
|
|
1628
1629
|
{ id: 'prompt-subagents', text: 'All implementation, verification, and report creation will be done by the sub-agents.' },
|
|
1629
1630
|
{ id: 'prompt-reviw', text: 'Open in REVIW next time.' },
|
|
1630
1631
|
{ id: 'prompt-screenshots', text: 'Update all screenshots and videos.' },
|
|
1631
|
-
{ id: 'prompt-user-feedback-todo', text: "Add the user's feedback to the Todo list, and do not check it off without the user's approval." }
|
|
1632
|
+
{ id: 'prompt-user-feedback-todo', text: "Add the user's feedback to the Todo list, and do not check it off without the user's approval." },
|
|
1633
|
+
{ id: 'prompt-deep-dive', text: "Before implementing, deeply probe the user's request. If using Claude Code, start with AskUserQuestion and EnterPlanMode; otherwise achieve the same depth through interactive questions and planning, even if the UI differs." }
|
|
1632
1634
|
];
|
|
1633
1635
|
const PROMPT_STORAGE_KEY = 'reviw-prompt-prefs';
|
|
1634
1636
|
|
|
@@ -3143,6 +3145,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3143
3145
|
<label><input type="checkbox" id="prompt-reviw" checked /> Open in REVIW next time.</label>
|
|
3144
3146
|
<label><input type="checkbox" id="prompt-screenshots" checked /> Update all screenshots and videos.</label>
|
|
3145
3147
|
<label><input type="checkbox" id="prompt-user-feedback-todo" checked /> Add the user's feedback to the Todo list, and do not check it off without the user's approval.</label>
|
|
3148
|
+
<label><input type="checkbox" id="prompt-deep-dive" checked /> Before implementing, deeply probe the user's request. If using Claude Code, start with AskUserQuestion and EnterPlanMode; otherwise achieve the same depth through interactive questions and planning, even if the UI differs.</label>
|
|
3146
3149
|
</div>
|
|
3147
3150
|
<div class="modal-actions">
|
|
3148
3151
|
<button id="modal-cancel">Cancel</button>
|
|
@@ -3563,7 +3566,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3563
3566
|
}, 1500);
|
|
3564
3567
|
}
|
|
3565
3568
|
|
|
3566
|
-
function openCardForSelection() {
|
|
3569
|
+
function openCardForSelection(previewElement) {
|
|
3567
3570
|
if (!selection) return;
|
|
3568
3571
|
// Don't open card while image/video modal is visible
|
|
3569
3572
|
const imageOverlay = document.getElementById('image-fullscreen');
|
|
@@ -3601,10 +3604,50 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3601
3604
|
commentInput.value = existingComment?.text || '';
|
|
3602
3605
|
|
|
3603
3606
|
card.style.display = 'block';
|
|
3607
|
+
// 常にソーステーブルの選択セル位置を基準にカードを配置
|
|
3608
|
+
// これにより、プレビューからクリックしてもソースからクリックしても
|
|
3609
|
+
// 同じ行に対しては同じ位置にダイアログが表示される
|
|
3604
3610
|
positionCardForSelection(startRow, endRow, startCol, endCol);
|
|
3605
3611
|
commentInput.focus();
|
|
3606
3612
|
}
|
|
3607
3613
|
|
|
3614
|
+
// Position card near a clicked preview element (used when clicking from preview pane)
|
|
3615
|
+
// Note: Uses viewport-relative coordinates directly since .md-left/.md-right containers scroll,
|
|
3616
|
+
// not the document. The card uses position:absolute but with no positioned ancestor,
|
|
3617
|
+
// so it's positioned relative to the initial containing block.
|
|
3618
|
+
function positionCardNearElement(element) {
|
|
3619
|
+
const cardWidth = card.offsetWidth || 380;
|
|
3620
|
+
const cardHeight = card.offsetHeight || 220;
|
|
3621
|
+
const rect = element.getBoundingClientRect();
|
|
3622
|
+
const margin = 12;
|
|
3623
|
+
const vw = window.innerWidth;
|
|
3624
|
+
const vh = window.innerHeight;
|
|
3625
|
+
|
|
3626
|
+
// Use viewport-relative coordinates directly from getBoundingClientRect()
|
|
3627
|
+
// No need to add window.scrollX/Y since the containers scroll, not the document
|
|
3628
|
+
let left = rect.right + margin;
|
|
3629
|
+
let top = rect.top;
|
|
3630
|
+
|
|
3631
|
+
// If card would go off the right edge, position it below the element
|
|
3632
|
+
if (left + cardWidth > vw - margin) {
|
|
3633
|
+
left = Math.max(rect.left, margin);
|
|
3634
|
+
left = Math.min(left, vw - cardWidth - margin);
|
|
3635
|
+
top = rect.bottom + margin;
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
// If card would go off the bottom, position it above the element
|
|
3639
|
+
if (top + cardHeight > vh - margin) {
|
|
3640
|
+
top = rect.top - cardHeight - margin;
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
// Ensure card stays within viewport
|
|
3644
|
+
top = Math.max(margin, Math.min(top, vh - cardHeight - margin));
|
|
3645
|
+
left = Math.max(margin, Math.min(left, vw - cardWidth - margin));
|
|
3646
|
+
|
|
3647
|
+
card.style.left = left + 'px';
|
|
3648
|
+
card.style.top = top + 'px';
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3608
3651
|
function positionCardForSelection(startRow, endRow, startCol, endCol) {
|
|
3609
3652
|
const cardWidth = card.offsetWidth || 380;
|
|
3610
3653
|
const cardHeight = card.offsetHeight || 220;
|
|
@@ -3629,6 +3672,9 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3629
3672
|
const sx = window.scrollX;
|
|
3630
3673
|
const sy = window.scrollY;
|
|
3631
3674
|
|
|
3675
|
+
// Check if the selection is within viewport
|
|
3676
|
+
const isInViewport = rect.top >= 0 && rect.top < vh && rect.bottom > 0;
|
|
3677
|
+
|
|
3632
3678
|
const spaceRight = vw - rect.right - margin;
|
|
3633
3679
|
const spaceLeft = rect.left - margin - ROW_HEADER_WIDTH; // Account for row header
|
|
3634
3680
|
const spaceBelow = vh - rect.bottom - margin;
|
|
@@ -3640,28 +3686,38 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3640
3686
|
let left = sx + rect.right + margin;
|
|
3641
3687
|
let top = sy + rect.top;
|
|
3642
3688
|
|
|
3643
|
-
//
|
|
3644
|
-
if (
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
top = sy + clamp(rect.top, margin, vh - cardHeight - margin);
|
|
3648
|
-
} else if (spaceBelow >= cardHeight) {
|
|
3649
|
-
left = sx + clamp(rect.left, minLeft, vw - cardWidth - margin);
|
|
3650
|
-
top = sy + rect.bottom + margin;
|
|
3651
|
-
} else if (spaceAbove >= cardHeight) {
|
|
3652
|
-
left = sx + clamp(rect.left, minLeft, vw - cardWidth - margin);
|
|
3653
|
-
top = sy + rect.top - cardHeight - margin;
|
|
3654
|
-
} else if (spaceLeft >= cardWidth) {
|
|
3655
|
-
left = sx + rect.left - cardWidth - margin;
|
|
3656
|
-
top = sy + clamp(rect.top, margin, vh - cardHeight - margin);
|
|
3689
|
+
// If selection is outside viewport, position card in center of viewport
|
|
3690
|
+
if (!isInViewport) {
|
|
3691
|
+
left = sx + Math.max(vw / 2 - cardWidth / 2, minLeft);
|
|
3692
|
+
top = sy + Math.max(vh / 2 - cardHeight / 2, margin);
|
|
3657
3693
|
} else {
|
|
3658
|
-
//
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3694
|
+
// Priority: right > below > above > left > fallback right
|
|
3695
|
+
if (spaceRight >= cardWidth) {
|
|
3696
|
+
// Prefer right side of selection
|
|
3697
|
+
left = sx + rect.right + margin;
|
|
3698
|
+
top = sy + clamp(rect.top, margin, vh - cardHeight - margin);
|
|
3699
|
+
} else if (spaceBelow >= cardHeight) {
|
|
3700
|
+
left = sx + clamp(rect.left, minLeft, vw - cardWidth - margin);
|
|
3701
|
+
top = sy + rect.bottom + margin;
|
|
3702
|
+
} else if (spaceAbove >= cardHeight) {
|
|
3703
|
+
left = sx + clamp(rect.left, minLeft, vw - cardWidth - margin);
|
|
3704
|
+
top = sy + rect.top - cardHeight - margin;
|
|
3705
|
+
} else if (spaceLeft >= cardWidth) {
|
|
3706
|
+
left = sx + rect.left - cardWidth - margin;
|
|
3707
|
+
top = sy + clamp(rect.top, margin, vh - cardHeight - margin);
|
|
3708
|
+
} else {
|
|
3709
|
+
// Fallback: place to right side even if it means going off screen
|
|
3710
|
+
// Position card at right edge of selection, clamped to viewport
|
|
3711
|
+
left = sx + Math.max(rect.right + margin, minLeft);
|
|
3712
|
+
left = Math.min(left, sx + vw - cardWidth - margin);
|
|
3713
|
+
top = sy + clamp(rect.top, margin, vh - cardHeight - margin);
|
|
3714
|
+
}
|
|
3663
3715
|
}
|
|
3664
3716
|
|
|
3717
|
+
// Final clamp to ensure card stays within viewport
|
|
3718
|
+
left = clamp(left, margin, sx + vw - cardWidth - margin);
|
|
3719
|
+
top = clamp(top, sy + margin, sy + vh - cardHeight - margin);
|
|
3720
|
+
|
|
3665
3721
|
card.style.left = left + 'px';
|
|
3666
3722
|
card.style.top = top + 'px';
|
|
3667
3723
|
}
|
|
@@ -3670,6 +3726,8 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3670
3726
|
card.style.display = 'none';
|
|
3671
3727
|
currentKey = null;
|
|
3672
3728
|
clearSelection();
|
|
3729
|
+
// Re-enable scroll sync when card is closed
|
|
3730
|
+
window._disableScrollSync = false;
|
|
3673
3731
|
}
|
|
3674
3732
|
|
|
3675
3733
|
function setDot(row, col, on) {
|
|
@@ -4081,7 +4139,8 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4081
4139
|
{ id: 'prompt-subagents', text: 'All implementation, verification, and report creation will be done by the sub-agents.' },
|
|
4082
4140
|
{ id: 'prompt-reviw', text: 'Open in REVIW next time.' },
|
|
4083
4141
|
{ id: 'prompt-screenshots', text: 'Update all screenshots and videos.' },
|
|
4084
|
-
{ id: 'prompt-user-feedback-todo', text: "Add the user's feedback to the Todo list, and do not check it off without the user's approval." }
|
|
4142
|
+
{ id: 'prompt-user-feedback-todo', text: "Add the user's feedback to the Todo list, and do not check it off without the user's approval." },
|
|
4143
|
+
{ id: 'prompt-deep-dive', text: "Before implementing, deeply probe the user's request. If using Claude Code, start with AskUserQuestion and EnterPlanMode; otherwise achieve the same depth through interactive questions and planning, even if the UI differs." }
|
|
4085
4144
|
];
|
|
4086
4145
|
const PROMPT_STORAGE_KEY = 'reviw-prompt-prefs';
|
|
4087
4146
|
|
|
@@ -4273,20 +4332,56 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4273
4332
|
})();
|
|
4274
4333
|
|
|
4275
4334
|
// --- Scroll Sync for Markdown Mode ---
|
|
4335
|
+
// Global flag to temporarily disable scroll sync (used by selectSourceRange)
|
|
4336
|
+
window._disableScrollSync = false;
|
|
4337
|
+
// Global RAF ID so we can cancel pending scroll syncs from selectSourceRange
|
|
4338
|
+
window._scrollSyncRafId = null;
|
|
4339
|
+
|
|
4276
4340
|
if (MODE === 'markdown') {
|
|
4277
4341
|
const mdLeft = document.querySelector('.md-left');
|
|
4278
4342
|
const mdRight = document.querySelector('.md-right');
|
|
4279
4343
|
if (mdLeft && mdRight) {
|
|
4280
4344
|
let activePane = null;
|
|
4281
|
-
|
|
4345
|
+
|
|
4346
|
+
// Build anchor map for section-based sync
|
|
4347
|
+
// Maps line numbers to heading elements in preview
|
|
4348
|
+
const headingAnchors = [];
|
|
4349
|
+
const preview = document.querySelector('.md-preview');
|
|
4350
|
+
if (preview) {
|
|
4351
|
+
const headings = preview.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
4352
|
+
headings.forEach(h => {
|
|
4353
|
+
const text = h.textContent.trim();
|
|
4354
|
+
// Find corresponding line in source
|
|
4355
|
+
for (let i = 0; i < DATA.length; i++) {
|
|
4356
|
+
const lineText = (DATA[i][0] || '').trim();
|
|
4357
|
+
if (lineText.match(/^#+\\s/) && lineText.replace(/^#+\\s*/, '').trim() === text) {
|
|
4358
|
+
headingAnchors.push({
|
|
4359
|
+
line: i + 1,
|
|
4360
|
+
sourceEl: mdLeft.querySelector('td[data-row="' + (i + 1) + '"]'),
|
|
4361
|
+
previewEl: h
|
|
4362
|
+
});
|
|
4363
|
+
break;
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
4282
4368
|
|
|
4283
4369
|
function syncScroll(source, target, sourceName) {
|
|
4370
|
+
// Skip if scroll sync is temporarily disabled
|
|
4371
|
+
if (window._disableScrollSync) return;
|
|
4372
|
+
// Skip if scroll sync is disabled until a certain time (for preview click scroll)
|
|
4373
|
+
if (window._scrollSyncDisableUntil && Date.now() < window._scrollSyncDisableUntil) return;
|
|
4374
|
+
|
|
4284
4375
|
// Only sync if this pane initiated the scroll
|
|
4285
4376
|
if (activePane && activePane !== sourceName) return;
|
|
4286
4377
|
activePane = sourceName;
|
|
4287
4378
|
|
|
4288
|
-
if (
|
|
4289
|
-
|
|
4379
|
+
if (window._scrollSyncRafId) cancelAnimationFrame(window._scrollSyncRafId);
|
|
4380
|
+
window._scrollSyncRafId = requestAnimationFrame(() => {
|
|
4381
|
+
// Check again inside RAF in case _disableScrollSync was set after RAF was scheduled
|
|
4382
|
+
if (window._disableScrollSync) return;
|
|
4383
|
+
if (window._scrollSyncDisableUntil && Date.now() < window._scrollSyncDisableUntil) return;
|
|
4384
|
+
|
|
4290
4385
|
const sourceMax = source.scrollHeight - source.clientHeight;
|
|
4291
4386
|
const targetMax = target.scrollHeight - target.clientHeight;
|
|
4292
4387
|
|
|
@@ -4295,20 +4390,81 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4295
4390
|
// Snap to edges for precision
|
|
4296
4391
|
if (source.scrollTop <= 1) {
|
|
4297
4392
|
target.scrollTop = 0;
|
|
4298
|
-
|
|
4393
|
+
setTimeout(() => { activePane = null; }, 100);
|
|
4394
|
+
return;
|
|
4395
|
+
}
|
|
4396
|
+
if (source.scrollTop >= sourceMax - 1) {
|
|
4299
4397
|
target.scrollTop = targetMax;
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4398
|
+
setTimeout(() => { activePane = null; }, 100);
|
|
4399
|
+
return;
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
// Try section-based sync if anchors exist
|
|
4403
|
+
if (headingAnchors.length > 0) {
|
|
4404
|
+
const sourceRect = source.getBoundingClientRect();
|
|
4405
|
+
const viewportTop = sourceRect.top;
|
|
4406
|
+
const viewportMid = viewportTop + sourceRect.height / 3;
|
|
4407
|
+
|
|
4408
|
+
// Find the heading closest to viewport top in source
|
|
4409
|
+
let closestAnchor = null;
|
|
4410
|
+
let closestDistance = Infinity;
|
|
4411
|
+
|
|
4412
|
+
for (const anchor of headingAnchors) {
|
|
4413
|
+
const el = sourceName === 'left' ? anchor.sourceEl : anchor.previewEl;
|
|
4414
|
+
if (!el) continue;
|
|
4415
|
+
const rect = el.getBoundingClientRect();
|
|
4416
|
+
const distance = Math.abs(rect.top - viewportMid);
|
|
4417
|
+
if (distance < closestDistance) {
|
|
4418
|
+
closestDistance = distance;
|
|
4419
|
+
closestAnchor = anchor;
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4423
|
+
if (closestAnchor) {
|
|
4424
|
+
const sourceEl = sourceName === 'left' ? closestAnchor.sourceEl : closestAnchor.previewEl;
|
|
4425
|
+
const targetEl = sourceName === 'left' ? closestAnchor.previewEl : closestAnchor.sourceEl;
|
|
4426
|
+
|
|
4427
|
+
if (sourceEl && targetEl) {
|
|
4428
|
+
const sourceElRect = sourceEl.getBoundingClientRect();
|
|
4429
|
+
const sourceOffset = sourceElRect.top - sourceRect.top;
|
|
4430
|
+
|
|
4431
|
+
// Calculate where target element should be
|
|
4432
|
+
const targetRect = target.getBoundingClientRect();
|
|
4433
|
+
const targetElRect = targetEl.getBoundingClientRect();
|
|
4434
|
+
const currentTargetOffset = targetElRect.top - targetRect.top;
|
|
4435
|
+
|
|
4436
|
+
// Adjust target scroll to align the anchor
|
|
4437
|
+
const adjustment = currentTargetOffset - sourceOffset;
|
|
4438
|
+
target.scrollTop = target.scrollTop + adjustment;
|
|
4439
|
+
|
|
4440
|
+
setTimeout(() => { activePane = null; }, 100);
|
|
4441
|
+
return;
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4303
4444
|
}
|
|
4304
4445
|
|
|
4446
|
+
// Fallback to ratio-based sync
|
|
4447
|
+
const ratio = source.scrollTop / sourceMax;
|
|
4448
|
+
target.scrollTop = Math.round(ratio * targetMax);
|
|
4449
|
+
|
|
4305
4450
|
// Release lock after scroll settles
|
|
4306
4451
|
setTimeout(() => { activePane = null; }, 100);
|
|
4307
4452
|
});
|
|
4308
4453
|
}
|
|
4309
4454
|
|
|
4310
|
-
|
|
4311
|
-
|
|
4455
|
+
// Store scroll handlers for temporary removal
|
|
4456
|
+
const leftScrollHandler = () => syncScroll(mdLeft, mdRight, 'left');
|
|
4457
|
+
const rightScrollHandler = () => syncScroll(mdRight, mdLeft, 'right');
|
|
4458
|
+
mdLeft.addEventListener('scroll', leftScrollHandler, { passive: true });
|
|
4459
|
+
mdRight.addEventListener('scroll', rightScrollHandler, { passive: true });
|
|
4460
|
+
|
|
4461
|
+
// Expose handlers for temporary removal during preview click
|
|
4462
|
+
window._scrollHandlers = {
|
|
4463
|
+
left: leftScrollHandler,
|
|
4464
|
+
right: rightScrollHandler,
|
|
4465
|
+
mdLeft: mdLeft,
|
|
4466
|
+
mdRight: mdRight
|
|
4467
|
+
};
|
|
4312
4468
|
}
|
|
4313
4469
|
}
|
|
4314
4470
|
|
|
@@ -4628,9 +4784,47 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4628
4784
|
const imageClose = document.getElementById('image-close');
|
|
4629
4785
|
if (!imageOverlay || !imageContainer) return;
|
|
4630
4786
|
|
|
4787
|
+
// Collect all images for navigation
|
|
4788
|
+
const allImages = Array.from(preview.querySelectorAll('img'));
|
|
4789
|
+
let currentImageIndex = -1;
|
|
4790
|
+
|
|
4791
|
+
function showImage(index) {
|
|
4792
|
+
if (index < 0 || index >= allImages.length) return;
|
|
4793
|
+
currentImageIndex = index;
|
|
4794
|
+
const img = allImages[index];
|
|
4795
|
+
|
|
4796
|
+
imageContainer.innerHTML = '';
|
|
4797
|
+
const clonedImg = img.cloneNode(true);
|
|
4798
|
+
// CSSで制御するためインラインスタイルはリセット
|
|
4799
|
+
clonedImg.style.width = '';
|
|
4800
|
+
clonedImg.style.height = '';
|
|
4801
|
+
clonedImg.style.maxWidth = '';
|
|
4802
|
+
clonedImg.style.maxHeight = '';
|
|
4803
|
+
clonedImg.style.cursor = 'default';
|
|
4804
|
+
imageContainer.appendChild(clonedImg);
|
|
4805
|
+
|
|
4806
|
+
// Show navigation hint
|
|
4807
|
+
const counter = document.createElement('div');
|
|
4808
|
+
counter.className = 'fullscreen-counter';
|
|
4809
|
+
counter.textContent = \`\${index + 1} / \${allImages.length}\`;
|
|
4810
|
+
counter.style.cssText = 'position:absolute;bottom:20px;left:50%;transform:translateX(-50%);color:#fff;background:rgba(0,0,0,0.6);padding:8px 16px;border-radius:20px;font-size:14px;';
|
|
4811
|
+
imageContainer.appendChild(counter);
|
|
4812
|
+
|
|
4813
|
+
imageOverlay.classList.add('visible');
|
|
4814
|
+
}
|
|
4815
|
+
|
|
4631
4816
|
function closeImageOverlay() {
|
|
4632
4817
|
imageOverlay.classList.remove('visible');
|
|
4633
4818
|
imageContainer.innerHTML = '';
|
|
4819
|
+
currentImageIndex = -1;
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
function navigateImage(direction) {
|
|
4823
|
+
if (!imageOverlay.classList.contains('visible')) return;
|
|
4824
|
+
const newIndex = currentImageIndex + direction;
|
|
4825
|
+
if (newIndex >= 0 && newIndex < allImages.length) {
|
|
4826
|
+
showImage(newIndex);
|
|
4827
|
+
}
|
|
4634
4828
|
}
|
|
4635
4829
|
|
|
4636
4830
|
if (imageClose) {
|
|
@@ -4645,30 +4839,33 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4645
4839
|
}
|
|
4646
4840
|
|
|
4647
4841
|
document.addEventListener('keydown', (e) => {
|
|
4648
|
-
if (
|
|
4649
|
-
|
|
4842
|
+
if (!imageOverlay.classList.contains('visible')) return;
|
|
4843
|
+
|
|
4844
|
+
switch (e.key) {
|
|
4845
|
+
case 'Escape':
|
|
4846
|
+
closeImageOverlay();
|
|
4847
|
+
break;
|
|
4848
|
+
case 'ArrowLeft':
|
|
4849
|
+
case 'ArrowUp':
|
|
4850
|
+
e.preventDefault();
|
|
4851
|
+
navigateImage(-1);
|
|
4852
|
+
break;
|
|
4853
|
+
case 'ArrowRight':
|
|
4854
|
+
case 'ArrowDown':
|
|
4855
|
+
e.preventDefault();
|
|
4856
|
+
navigateImage(1);
|
|
4857
|
+
break;
|
|
4650
4858
|
}
|
|
4651
4859
|
});
|
|
4652
4860
|
|
|
4653
|
-
|
|
4861
|
+
allImages.forEach((img, index) => {
|
|
4654
4862
|
img.style.cursor = 'pointer';
|
|
4655
|
-
img.title = 'Click to view fullscreen';
|
|
4863
|
+
img.title = 'Click to view fullscreen (← → to navigate)';
|
|
4656
4864
|
|
|
4657
4865
|
img.addEventListener('click', (e) => {
|
|
4658
4866
|
// Don't stop propagation - allow select to work
|
|
4659
4867
|
e.preventDefault();
|
|
4660
|
-
|
|
4661
|
-
imageContainer.innerHTML = '';
|
|
4662
|
-
const clonedImg = img.cloneNode(true);
|
|
4663
|
-
// CSSで制御するためインラインスタイルはリセット
|
|
4664
|
-
clonedImg.style.width = '';
|
|
4665
|
-
clonedImg.style.height = '';
|
|
4666
|
-
clonedImg.style.maxWidth = '';
|
|
4667
|
-
clonedImg.style.maxHeight = '';
|
|
4668
|
-
clonedImg.style.cursor = 'default';
|
|
4669
|
-
imageContainer.appendChild(clonedImg);
|
|
4670
|
-
|
|
4671
|
-
imageOverlay.classList.add('visible');
|
|
4868
|
+
showImage(index);
|
|
4672
4869
|
});
|
|
4673
4870
|
});
|
|
4674
4871
|
})();
|
|
@@ -4685,8 +4882,56 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4685
4882
|
|
|
4686
4883
|
const videoExtensions = /\\.(mp4|mov|webm|avi|mkv|m4v|ogv)$/i;
|
|
4687
4884
|
|
|
4885
|
+
// Collect all video links for navigation
|
|
4886
|
+
const allVideoLinks = Array.from(preview.querySelectorAll('a')).filter(link => {
|
|
4887
|
+
const href = link.getAttribute('href');
|
|
4888
|
+
return href && videoExtensions.test(href);
|
|
4889
|
+
});
|
|
4890
|
+
let currentVideoIndex = -1;
|
|
4891
|
+
|
|
4892
|
+
function showVideo(index) {
|
|
4893
|
+
if (index < 0 || index >= allVideoLinks.length) return;
|
|
4894
|
+
currentVideoIndex = index;
|
|
4895
|
+
const link = allVideoLinks[index];
|
|
4896
|
+
const href = link.getAttribute('href');
|
|
4897
|
+
|
|
4898
|
+
// Remove existing video if any
|
|
4899
|
+
const existingVideo = videoContainer.querySelector('video');
|
|
4900
|
+
if (existingVideo) {
|
|
4901
|
+
existingVideo.pause();
|
|
4902
|
+
existingVideo.src = '';
|
|
4903
|
+
existingVideo.remove();
|
|
4904
|
+
}
|
|
4905
|
+
|
|
4906
|
+
// Remove existing counter
|
|
4907
|
+
const existingCounter = videoContainer.querySelector('.fullscreen-counter');
|
|
4908
|
+
if (existingCounter) existingCounter.remove();
|
|
4909
|
+
|
|
4910
|
+
const video = document.createElement('video');
|
|
4911
|
+
video.src = href;
|
|
4912
|
+
video.controls = true;
|
|
4913
|
+
video.autoplay = true;
|
|
4914
|
+
video.style.maxWidth = '100%';
|
|
4915
|
+
video.style.maxHeight = '100%';
|
|
4916
|
+
// Prevent click on video from closing overlay
|
|
4917
|
+
video.addEventListener('click', (e) => e.stopPropagation());
|
|
4918
|
+
videoContainer.appendChild(video);
|
|
4919
|
+
|
|
4920
|
+
// Show navigation hint
|
|
4921
|
+
if (allVideoLinks.length > 1) {
|
|
4922
|
+
const counter = document.createElement('div');
|
|
4923
|
+
counter.className = 'fullscreen-counter';
|
|
4924
|
+
counter.textContent = \`\${index + 1} / \${allVideoLinks.length}\`;
|
|
4925
|
+
counter.style.cssText = 'position:absolute;bottom:20px;left:50%;transform:translateX(-50%);color:#fff;background:rgba(0,0,0,0.6);padding:8px 16px;border-radius:20px;font-size:14px;';
|
|
4926
|
+
videoContainer.appendChild(counter);
|
|
4927
|
+
}
|
|
4928
|
+
|
|
4929
|
+
videoOverlay.classList.add('visible');
|
|
4930
|
+
}
|
|
4931
|
+
|
|
4688
4932
|
function closeVideoOverlay() {
|
|
4689
4933
|
videoOverlay.classList.remove('visible');
|
|
4934
|
+
currentVideoIndex = -1;
|
|
4690
4935
|
// Stop and remove video
|
|
4691
4936
|
const video = videoContainer.querySelector('video');
|
|
4692
4937
|
if (video) {
|
|
@@ -4696,6 +4941,14 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4696
4941
|
}
|
|
4697
4942
|
}
|
|
4698
4943
|
|
|
4944
|
+
function navigateVideo(direction) {
|
|
4945
|
+
if (!videoOverlay.classList.contains('visible')) return;
|
|
4946
|
+
const newIndex = currentVideoIndex + direction;
|
|
4947
|
+
if (newIndex >= 0 && newIndex < allVideoLinks.length) {
|
|
4948
|
+
showVideo(newIndex);
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4699
4952
|
if (videoClose) {
|
|
4700
4953
|
videoClose.addEventListener('click', closeVideoOverlay);
|
|
4701
4954
|
}
|
|
@@ -4708,41 +4961,37 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4708
4961
|
}
|
|
4709
4962
|
|
|
4710
4963
|
document.addEventListener('keydown', (e) => {
|
|
4711
|
-
if (
|
|
4712
|
-
|
|
4964
|
+
if (!videoOverlay.classList.contains('visible')) return;
|
|
4965
|
+
|
|
4966
|
+
switch (e.key) {
|
|
4967
|
+
case 'Escape':
|
|
4968
|
+
closeVideoOverlay();
|
|
4969
|
+
break;
|
|
4970
|
+
case 'ArrowLeft':
|
|
4971
|
+
case 'ArrowUp':
|
|
4972
|
+
e.preventDefault();
|
|
4973
|
+
navigateVideo(-1);
|
|
4974
|
+
break;
|
|
4975
|
+
case 'ArrowRight':
|
|
4976
|
+
case 'ArrowDown':
|
|
4977
|
+
e.preventDefault();
|
|
4978
|
+
navigateVideo(1);
|
|
4979
|
+
break;
|
|
4713
4980
|
}
|
|
4714
4981
|
});
|
|
4715
4982
|
|
|
4716
4983
|
// Intercept video link clicks
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
link.addEventListener('click', (e) => {
|
|
4724
|
-
e.preventDefault();
|
|
4725
|
-
// Don't stop propagation - allow select to work
|
|
4726
|
-
|
|
4727
|
-
// Remove existing video if any
|
|
4728
|
-
const existingVideo = videoContainer.querySelector('video');
|
|
4729
|
-
if (existingVideo) {
|
|
4730
|
-
existingVideo.pause();
|
|
4731
|
-
existingVideo.src = '';
|
|
4732
|
-
existingVideo.remove();
|
|
4733
|
-
}
|
|
4984
|
+
allVideoLinks.forEach((link, index) => {
|
|
4985
|
+
link.style.cursor = 'pointer';
|
|
4986
|
+
link.title = allVideoLinks.length > 1
|
|
4987
|
+
? 'Click to play video fullscreen (← → to navigate)'
|
|
4988
|
+
: 'Click to play video fullscreen';
|
|
4734
4989
|
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
video.style.maxHeight = '100%';
|
|
4741
|
-
videoContainer.appendChild(video);
|
|
4742
|
-
|
|
4743
|
-
videoOverlay.classList.add('visible');
|
|
4744
|
-
});
|
|
4745
|
-
}
|
|
4990
|
+
link.addEventListener('click', (e) => {
|
|
4991
|
+
e.preventDefault();
|
|
4992
|
+
// Don't stop propagation - allow select to work
|
|
4993
|
+
showVideo(index);
|
|
4994
|
+
});
|
|
4746
4995
|
});
|
|
4747
4996
|
})();
|
|
4748
4997
|
|
|
@@ -4926,22 +5175,59 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4926
5175
|
}
|
|
4927
5176
|
|
|
4928
5177
|
// Trigger source cell selection (reuse existing comment flow)
|
|
4929
|
-
|
|
5178
|
+
// When clickedPreviewElement is provided (from preview click), position card near that element
|
|
5179
|
+
function selectSourceRange(startRow, endRow, clickedPreviewElement) {
|
|
5180
|
+
// IMMEDIATELY disable scroll sync at the very start
|
|
5181
|
+
window._disableScrollSync = true;
|
|
5182
|
+
window._scrollSyncDisableUntil = Date.now() + 2000;
|
|
5183
|
+
|
|
5184
|
+
// Cancel any pending scroll sync RAF
|
|
5185
|
+
if (window._scrollSyncRafId) {
|
|
5186
|
+
cancelAnimationFrame(window._scrollSyncRafId);
|
|
5187
|
+
window._scrollSyncRafId = null;
|
|
5188
|
+
}
|
|
5189
|
+
|
|
5190
|
+
// TEMPORARILY REMOVE scroll event listeners to prevent any interference
|
|
5191
|
+
const handlers = window._scrollHandlers;
|
|
5192
|
+
if (handlers) {
|
|
5193
|
+
handlers.mdLeft.removeEventListener('scroll', handlers.left);
|
|
5194
|
+
handlers.mdRight.removeEventListener('scroll', handlers.right);
|
|
5195
|
+
}
|
|
5196
|
+
|
|
4930
5197
|
selection = { startRow, endRow: endRow || startRow, startCol: 1, endCol: 1 };
|
|
4931
5198
|
updateSelectionVisual();
|
|
4932
5199
|
|
|
4933
5200
|
// Clear header selection
|
|
4934
5201
|
document.querySelectorAll('thead th.selected').forEach(el => el.classList.remove('selected'));
|
|
4935
5202
|
|
|
4936
|
-
// Scroll source
|
|
4937
|
-
const
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
5203
|
+
// Scroll source pane FIRST (before opening card) to ensure target is visible
|
|
5204
|
+
const targetRow = startRow;
|
|
5205
|
+
const mdRight = document.querySelector('.md-right');
|
|
5206
|
+
const sourceTd = document.querySelector('td[data-row="' + targetRow + '"][data-col="1"]');
|
|
5207
|
+
if (mdRight && sourceTd) {
|
|
5208
|
+
const tdOffsetTop = sourceTd.offsetTop;
|
|
5209
|
+
const containerHeight = mdRight.clientHeight;
|
|
5210
|
+
const tdHeight = sourceTd.offsetHeight;
|
|
5211
|
+
const scrollTarget = tdOffsetTop - (containerHeight / 2) + (tdHeight / 2);
|
|
5212
|
+
// Use scrollTo with instant behavior to ensure immediate scroll
|
|
5213
|
+
mdRight.scrollTo({
|
|
5214
|
+
top: Math.max(0, scrollTarget),
|
|
5215
|
+
behavior: 'instant'
|
|
5216
|
+
});
|
|
4944
5217
|
}
|
|
5218
|
+
|
|
5219
|
+
// Open the card (synchronously) - now target cell should be visible for positioning
|
|
5220
|
+
openCardForSelection();
|
|
5221
|
+
|
|
5222
|
+
// Re-add scroll handlers after a delay to allow scroll to settle
|
|
5223
|
+
setTimeout(() => {
|
|
5224
|
+
if (handlers) {
|
|
5225
|
+
handlers.mdLeft.addEventListener('scroll', handlers.left, { passive: true });
|
|
5226
|
+
handlers.mdRight.addEventListener('scroll', handlers.right, { passive: true });
|
|
5227
|
+
}
|
|
5228
|
+
window._disableScrollSync = false;
|
|
5229
|
+
window._scrollSyncDisableUntil = 0;
|
|
5230
|
+
}, 500);
|
|
4945
5231
|
}
|
|
4946
5232
|
|
|
4947
5233
|
// Click on block elements
|
|
@@ -4950,7 +5236,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4950
5236
|
if (e.target.tagName === 'IMG') {
|
|
4951
5237
|
const line = findImageSourceLine(e.target.src);
|
|
4952
5238
|
if (line > 0) {
|
|
4953
|
-
selectSourceRange(line);
|
|
5239
|
+
selectSourceRange(line, null, e.target);
|
|
4954
5240
|
}
|
|
4955
5241
|
return;
|
|
4956
5242
|
}
|
|
@@ -4964,7 +5250,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4964
5250
|
const isTableCell = parentBlock.tagName === 'TD' || parentBlock.tagName === 'TH';
|
|
4965
5251
|
const line = isTableCell ? findTableSourceLine(parentBlock.textContent) : findSourceLine(parentBlock.textContent);
|
|
4966
5252
|
if (line > 0) {
|
|
4967
|
-
selectSourceRange(line);
|
|
5253
|
+
selectSourceRange(line, null, parentBlock);
|
|
4968
5254
|
}
|
|
4969
5255
|
}
|
|
4970
5256
|
// Let the link open naturally (target="_blank" is set by marked)
|
|
@@ -4981,7 +5267,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4981
5267
|
const { startLine, endLine } = findCodeBlockRange(code.textContent);
|
|
4982
5268
|
if (startLine > 0) {
|
|
4983
5269
|
e.preventDefault();
|
|
4984
|
-
selectSourceRange(startLine, endLine);
|
|
5270
|
+
selectSourceRange(startLine, endLine, pre);
|
|
4985
5271
|
}
|
|
4986
5272
|
return;
|
|
4987
5273
|
}
|
|
@@ -4995,7 +5281,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4995
5281
|
if (line <= 0) return;
|
|
4996
5282
|
|
|
4997
5283
|
e.preventDefault();
|
|
4998
|
-
selectSourceRange(line);
|
|
5284
|
+
selectSourceRange(line, null, target);
|
|
4999
5285
|
});
|
|
5000
5286
|
|
|
5001
5287
|
// Text selection to open comment for range
|
|
@@ -5015,8 +5301,13 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
5015
5301
|
|
|
5016
5302
|
if (startLine <= 0) return;
|
|
5017
5303
|
|
|
5304
|
+
// Get the element containing the selection for positioning
|
|
5305
|
+
const range = sel.getRangeAt(0);
|
|
5306
|
+
const container = range.commonAncestorContainer;
|
|
5307
|
+
const element = container.nodeType === Node.TEXT_NODE ? container.parentElement : container;
|
|
5308
|
+
|
|
5018
5309
|
sel.removeAllRanges();
|
|
5019
|
-
selectSourceRange(startLine, endLine > 0 ? endLine : startLine);
|
|
5310
|
+
selectSourceRange(startLine, endLine > 0 ? endLine : startLine, element);
|
|
5020
5311
|
}, 10);
|
|
5021
5312
|
});
|
|
5022
5313
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reviw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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": {
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"cli.cjs"
|
|
11
11
|
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest"
|
|
15
|
+
},
|
|
12
16
|
"license": "MIT",
|
|
13
17
|
"author": "kazuph",
|
|
14
18
|
"publishConfig": {
|
|
@@ -27,9 +31,5 @@
|
|
|
27
31
|
"@playwright/test": "^1.57.0",
|
|
28
32
|
"playwright": "^1.57.0",
|
|
29
33
|
"vitest": "^4.0.14"
|
|
30
|
-
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"test": "vitest run",
|
|
33
|
-
"test:watch": "vitest"
|
|
34
34
|
}
|
|
35
|
-
}
|
|
35
|
+
}
|