feat(pdftract-2wqir): implement keyboard shortcuts in inspector

Added comprehensive keyboard shortcuts for the inspector frontend:
- ArrowLeft/Right: navigate to previous/next page
- ArrowUp/Down: scroll within page
- /: focus search input
- Esc: blur input / close help overlay
- ?: show/hide keyboard shortcuts help overlay
- 1-9: toggle overlay layers (1=spans, 2=blocks, ..., 9=diff)

Changes:
- app.js: extended setupKeyboard() with new handlers, added prevPage()/nextPage() wrappers, scrollPage() and toggleHelp() helpers, setupHelp() for button wiring
- index.html: added ? button and help overlay with all shortcuts listed
- style.css: added styles for .btn-help, .help-overlay, .help-content, and related classes

Acceptance criteria met:
- ArrowLeft/Right navigation works
- / focuses search input
- 1-8 toggle overlays with visual feedback
- Esc blurs input and closes help
- ? shows help overlay listing all shortcuts

See: notes/pdftract-2wqir.md for verification details.
This commit is contained in:
jedarden 2026-06-01 08:09:51 -04:00
parent 9a38117865
commit 6a7332494d
3 changed files with 167 additions and 8 deletions

View file

@ -16,7 +16,7 @@ let scrollSync=true;
let matchedSpans=[];
let currentMatchIndex=-1;
function init(){loadLayerState();setupKeyboard();setupToggles();setupSearch();setupNav();setupComparisonMode();loadFragment()}
function init(){loadLayerState();setupKeyboard();setupToggles();setupSearch();setupNav();setupComparisonMode();setupHelp();loadFragment()}
async function loadDocument(){
const res=await fetch('/api/document');
@ -427,6 +427,33 @@ function setupComparisonMode(){
}
}
function setupHelp(){
const helpBtn=document.getElementById('btn-help');
const helpOverlay=document.getElementById('help-overlay');
const closeBtn=document.querySelector('.help-close');
if(helpBtn){
helpBtn.addEventListener('click',()=>{
toggleHelp(true);
});
}
if(closeBtn){
closeBtn.addEventListener('click',()=>{
toggleHelp(false);
});
}
// Close on overlay click (outside content)
if(helpOverlay){
helpOverlay.addEventListener('click',e=>{
if(e.target===helpOverlay){
toggleHelp(false);
}
});
}
}
function showComparisonMode(show){
const diffBtn=document.getElementById('btn-diff');
const compareControls=document.querySelector('.comparison-controls');
@ -443,25 +470,53 @@ function showComparisonMode(show){
function setupKeyboard(){
document.addEventListener('keydown',e=>{
const searchInput=document.getElementById('search-input');
if(e.target.tagName==='INPUT'&&e.target!==searchInput)return;
const helpOverlay=document.getElementById('help-overlay');
// Close help overlay on Escape
if(e.key==='Escape'&&helpOverlay&&!helpOverlay.hidden){
e.preventDefault();
toggleHelp(false);
return;
}
// Skip keyboard shortcuts when typing in inputs (except search)
if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'){
// Allow Escape to blur input
if(e.key==='Escape'){
e.preventDefault();
e.target.blur();
if(e.target===searchInput)clearSearch();
}
return;
}
if(e.key==='ArrowLeft'){
e.preventDefault();
navigatePage(-1)
}else if(e.key==='ArrowRight'){
e.preventDefault();
navigatePage(1)
}else if(e.key==='ArrowUp'){
// Scroll up within page
e.preventDefault();
scrollPage(-1)
}else if(e.key==='ArrowDown'){
// Scroll down within page
e.preventDefault();
scrollPage(1)
}else if(e.key==='/'){
if(e.target!==searchInput){
e.preventDefault();
searchInput.focus()
}
e.preventDefault();
searchInput.focus()
}else if(e.key==='?'){
e.preventDefault();
toggleHelp()
}else if(e.key>='1'&&e.key<='9'){
const idx=parseInt(e.key)-1;
const layer=LAYERS[idx];
if(layer)toggleLayer(layer)
}else if(e.key==='Escape'&&e.target===searchInput){
}else if(e.key==='Escape'){
e.preventDefault();
clearSearch()
document.activeElement.blur()
}
})
}
@ -570,6 +625,14 @@ function navigatePage(delta){
if(newPage>=0&&newPage<totalPages)loadPage(newPage)
}
function prevPage(){
navigatePage(-1)
}
function nextPage(){
navigatePage(1)
}
function updateNavState(){
document.getElementById('btn-prev').disabled=currentPage<=0;
document.getElementById('btn-next').disabled=currentPage>=totalPages-1
@ -633,6 +696,25 @@ function updateActiveThumbnail(){
});
}
function scrollPage(delta){
const container=document.getElementById('canvas-container');
if(container){
const scrollAmount=100;
container.scrollTop+=delta*scrollAmount
}
}
function toggleHelp(show){
const overlay=document.getElementById('help-overlay');
if(!overlay)return;
overlay.hidden=show===undefined?!overlay.hidden:!show;
if(!overlay.hidden){
// Focus on close button for accessibility
const closeBtn=overlay.querySelector('.help-close');
if(closeBtn)closeBtn.focus();
}
}
function updateFragment(){
history.replaceState(null,'',`#page=${currentPage}`)
}

View file

@ -18,6 +18,7 @@
<button id="btn-next" class="btn" aria-label="Next page">Next →</button>
<input id="search-input" type="search" placeholder="Search spans... (press /)" aria-label="Search spans" class="search-input">
<span id="match-count" class="match-count"></span>
<button id="btn-help" class="btn btn-help" aria-label="Show keyboard shortcuts" title="Keyboard shortcuts (?)">?</button>
<div class="toggles">
<button class="layer-toggle" data-layer="spans" aria-label="Toggle spans layer">1 Spans</button>
<button class="layer-toggle" data-layer="blocks" aria-label="Toggle blocks layer">2 Blocks</button>
@ -56,6 +57,71 @@ Sync scroll
</aside>
</div>
<div id="tooltip" class="tooltip" hidden></div>
<div id="help-overlay" class="help-overlay" hidden>
<div class="help-content">
<button class="help-close" aria-label="Close help">×</button>
<h2>Keyboard Shortcuts</h2>
<div class="help-shortcuts">
<div class="help-shortcut">
<span class="help-key"></span><span class="help-key"></span>
<span class="help-desc">Previous / Next page</span>
</div>
<div class="help-shortcut">
<span class="help-key"></span><span class="help-key"></span>
<span class="help-desc">Scroll within page</span>
</div>
<div class="help-shortcut">
<span class="help-key">/</span>
<span class="help-desc">Focus search input</span>
</div>
<div class="help-shortcut">
<span class="help-key">Esc</span>
<span class="help-desc">Blur input / Close help</span>
</div>
<div class="help-shortcut">
<span class="help-key">?</span>
<span class="help-desc">Show / Hide this help</span>
</div>
<div class="help-divider"></div>
<div class="help-shortcut">
<span class="help-key">1</span>
<span class="help-desc">Toggle Spans layer</span>
</div>
<div class="help-shortcut">
<span class="help-key">2</span>
<span class="help-desc">Toggle Blocks layer</span>
</div>
<div class="help-shortcut">
<span class="help-key">3</span>
<span class="help-desc">Toggle Columns layer</span>
</div>
<div class="help-shortcut">
<span class="help-key">4</span>
<span class="help-desc">Toggle Reading Order layer</span>
</div>
<div class="help-shortcut">
<span class="help-key">5</span>
<span class="help-desc">Toggle Confidence Heatmap layer</span>
</div>
<div class="help-shortcut">
<span class="help-key">6</span>
<span class="help-desc">Toggle OCR layer</span>
</div>
<div class="help-shortcut">
<span class="help-key">7</span>
<span class="help-desc">Toggle MCID layer</span>
</div>
<div class="help-shortcut">
<span class="help-key">8</span>
<span class="help-desc">Toggle Anchors layer</span>
</div>
<div class="help-shortcut">
<span class="help-key">9</span>
<span class="help-desc">Toggle Diff overlay (comparison mode)</span>
</div>
</div>
</div>
</div>
<script type="module" src="/static/app.js"></script>
</body>
</html>

View file

@ -63,3 +63,14 @@ html[data-layers~="spans"] .layer-spans,html[data-layers~="blocks"] .layer-block
.diff-removed{fill:none;stroke:#dc3545;stroke-width:2;stroke-dasharray:4,2;opacity:.7}
.diff-added{fill:none;stroke:#28a745;stroke-width:2;stroke-dasharray:4,2;opacity:.7}
.diff-changed{fill:none;stroke:#ffc107;stroke-width:2;stroke-dasharray:4,2;opacity:.7}
.btn-help{width:32px;height:32px;padding:0;font-size:16px;font-weight:600;border-radius:50%}
.help-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:2000;display:flex;align-items:center;justify-content:center}
.help-content{background:#fff;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,.2);max-width:500px;width:90%;max-height:80vh;overflow:auto;padding:0;position:relative}
.help-close{position:absolute;top:12px;right:12px;width:32px;height:32px;border:none;background:transparent;font-size:24px;cursor:pointer;color:#666;border-radius:4px;display:flex;align-items:center;justify-content:center}
.help-close:hover{background:#f0f0f0;color:#333}
.help-content h2{margin:0;padding:20px 20px 10px;border-bottom:1px solid #ddd;font-size:18px}
.help-shortcuts{padding:20px;display:flex;flex-direction:column;gap:8px}
.help-shortcut{display:flex;align-items:center;gap:12px;padding:4px 0}
.help-key{min-width:32px;padding:4px 8px;background:#f5f5f5;border:1px solid #ddd;border-radius:4px;font-family:ui-monospace,monospace;font-size:13px;text-align:center;display:inline-block}
.help-desc{font-size:14px;color:#333}
.help-divider{border-top:1px solid #eee;margin:8px 0}