feat(editor): fps-aware render, FPS selector in new-seq dialog, keyboard help overlay

- openSequence() and applyHistory() now pass state.seq.frame_rate to
  Timeline.render() instead of hardcoded 59.94 — clips render on the
  correct frame grid for every sequence
- New-sequence panel gains a frame-rate selector (23.976 / 24 / 25 /
  29.97 / 30 / 50 / 59.94 / 60); createNewSequence() posts frame_rate
  to the API
- Press ? to open a keyboard shortcut help overlay; Escape to close
This commit is contained in:
Zac Gaetano 2026-05-19 23:20:10 -04:00
parent 81c771a7be
commit bfc2649909

View file

@ -219,6 +219,85 @@
transition: background var(--t-fast);
}
.topbar-seq-name:hover { background: var(--bg-hover); }
/* ── Keyboard shortcut help overlay ───────────────────────── */
.kbd-help-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 900;
}
.kbd-help-backdrop.open { display: block; }
.kbd-help-panel {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--r-lg);
box-shadow: 0 24px 64px rgba(0,0,0,0.45);
width: 480px;
max-width: 90vw;
z-index: 901;
flex-direction: column;
}
.kbd-help-panel.open { display: flex; }
.kbd-help-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--sp-4) var(--sp-5);
border-bottom: 1px solid var(--border);
font-weight: 600;
font-size: var(--text-sm);
color: var(--text-primary);
}
.kbd-help-body {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--sp-5);
padding: var(--sp-5);
}
.kbd-section-title {
font-size: var(--text-xs);
font-weight: 500;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--text-tertiary);
margin-bottom: var(--sp-3);
}
.kbd-row {
display: flex;
align-items: center;
gap: var(--sp-3);
margin-bottom: var(--sp-2);
font-size: var(--text-sm);
color: var(--text-secondary);
}
kbd {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 7px;
background: var(--bg-raised);
border: 1px solid var(--border-strong, var(--border));
border-radius: var(--r-sm);
font-family: var(--font-mono, monospace);
font-size: 11px;
color: var(--text-primary);
white-space: nowrap;
min-width: 24px;
flex-shrink: 0;
}
</style>
</head>
<body>
@ -358,6 +437,8 @@
<div class="tl-sep"></div>
<button class="tl-tool-btn" id="undoBtn" title="Undo (Ctrl+Z)">&#8617;</button>
<button class="tl-tool-btn" id="redoBtn" title="Redo (Ctrl+Shift+Z)">&#8618;</button>
<div class="tl-sep"></div>
<button class="tl-tool-btn" id="helpBtn" title="Keyboard shortcuts (?)">?</button>
<span class="tl-save-status" id="saveStatus"></span>
</div>
<div class="timeline-container" id="timelineContainer"></div>
@ -384,6 +465,19 @@
<label class="form-label" for="newSeqName">Sequence name</label>
<input type="text" id="newSeqName" placeholder="e.g. Rough Cut v1">
</div>
<div class="form-group">
<label class="form-label" for="newSeqFps">Frame rate</label>
<select id="newSeqFps" class="form-select">
<option value="23.976">23.976 fps (Cinema)</option>
<option value="24">24 fps</option>
<option value="25">25 fps (PAL)</option>
<option value="29.97">29.97 fps (NTSC)</option>
<option value="30">30 fps</option>
<option value="50">50 fps</option>
<option value="59.94" selected>59.94 fps (HD default)</option>
<option value="60">60 fps</option>
</select>
</div>
</div>
<div class="slide-panel-footer">
<button class="btn btn-ghost" id="cancelSeqBtn">Cancel</button>
@ -391,6 +485,39 @@
</div>
</div>
<!-- Keyboard shortcut help overlay -->
<div class="kbd-help-backdrop" id="kbdHelpBackdrop"></div>
<div class="kbd-help-panel" id="kbdHelpPanel" role="dialog" aria-label="Keyboard shortcuts">
<div class="kbd-help-header">
<span>Keyboard Shortcuts</span>
<button class="btn btn-ghost btn-sm" id="closeKbdHelp" style="padding:0;width:28px;height:28px;">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 3l10 10M13 3L3 13"/></svg>
</button>
</div>
<div class="kbd-help-body">
<div>
<div class="kbd-section-title">Playback</div>
<div class="kbd-row"><kbd>Space</kbd><span>Play / Pause</span></div>
<div class="kbd-row"><kbd>J</kbd><span>Stop</span></div>
<div class="kbd-row"><kbd>L</kbd><span>Play</span></div>
<div class="kbd-section-title" style="margin-top:var(--sp-4)">In / Out</div>
<div class="kbd-row"><kbd>I</kbd><span>Mark In</span></div>
<div class="kbd-row"><kbd>O</kbd><span>Mark Out</span></div>
</div>
<div>
<div class="kbd-section-title">Tools</div>
<div class="kbd-row"><kbd>V</kbd><span>Select</span></div>
<div class="kbd-row"><kbd>C</kbd><span>Razor</span></div>
<div class="kbd-row"><kbd>H</kbd><span>Hand</span></div>
<div class="kbd-section-title" style="margin-top:var(--sp-4)">Edit</div>
<div class="kbd-row"><kbd>Ctrl+Z</kbd><span>Undo</span></div>
<div class="kbd-row"><kbd>Ctrl+&#8679;+Z</kbd><span>Redo</span></div>
<div class="kbd-row"><kbd>Ctrl+S</kbd><span>Save</span></div>
<div class="kbd-row"><kbd>?</kbd><span>This help</span></div>
</div>
</div>
</div>
<script src="js/api.js?v=6"></script>
<script src="js/timecode.js"></script>
<script src="js/timeline.js"></script>
@ -514,7 +641,7 @@ async function openSequence(id) {
state.history = [cloneClips(state.seq.clips)];
state.historyIdx = 0;
document.getElementById('seqSelect').value = id;
Timeline.render(state.seq.clips, { fps: 59.94 });
Timeline.render(state.seq.clips, { fps: state.seq.frame_rate || 59.94 });
updateProgramScrub();
}
@ -865,7 +992,7 @@ function redo() {
function applyHistory() {
const clips = cloneClips(state.history[state.historyIdx]);
state.seq.clips = clips;
Timeline.render(clips, { fps: 59.94 });
Timeline.render(clips, { fps: state.seq.frame_rate || 59.94 });
markDirty();
}
@ -891,6 +1018,7 @@ function setupToolbar() {
document.getElementById('undoBtn').onclick = undo;
document.getElementById('redoBtn').onclick = redo;
document.getElementById('saveBtn').onclick = saveSequence;
document.getElementById('helpBtn').onclick = openKbdHelp;
document.getElementById('exportEdlBtn').onclick = function() {
if (!state.seq) { toast('No sequence open', '', 'warning'); return; }
exportSequenceEDL(state.seq.id, (state.seq.name || 'sequence') + '.edl');
@ -902,6 +1030,9 @@ function setupKeyboard() {
const tag = document.activeElement.tagName;
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag)) return;
if (e.key === 'Escape') { closeKbdHelp(); return; }
if (e.key === '?') { e.preventDefault(); toggleKbdHelp(); return; }
if (e.code === 'Space') { e.preventDefault(); togglePgmPlay(); return; }
if (e.key === 'j' || e.key === 'J') { stopPgm(); return; }
if (e.key === 'k' || e.key === 'K') { stopPgm(); return; }
@ -929,6 +1060,27 @@ function updateToolbarActive(tool) {
Timeline.setTool(tool);
}
// ════════════════════════════════════════════════════════════════
// KEYBOARD HELP OVERLAY
// ════════════════════════════════════════════════════════════════
function openKbdHelp() {
document.getElementById('kbdHelpPanel').classList.add('open');
document.getElementById('kbdHelpBackdrop').classList.add('open');
}
function closeKbdHelp() {
document.getElementById('kbdHelpPanel').classList.remove('open');
document.getElementById('kbdHelpBackdrop').classList.remove('open');
}
function toggleKbdHelp() {
if (document.getElementById('kbdHelpPanel').classList.contains('open')) closeKbdHelp();
else openKbdHelp();
}
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('closeKbdHelp').onclick = closeKbdHelp;
document.getElementById('kbdHelpBackdrop').onclick = closeKbdHelp;
});
// ════════════════════════════════════════════════════════════════
// NEW SEQUENCE PANEL
// ════════════════════════════════════════════════════════════════
@ -942,7 +1094,8 @@ function setupNewSeqPanel() {
async function createNewSequence() {
const name = document.getElementById('newSeqName').value.trim() || 'Sequence ' + (state.sequences.length + 1);
const r = await createSequence({ project_id: state.projectId, name: name });
const fps = parseFloat(document.getElementById('newSeqFps').value) || 59.94;
const r = await createSequence({ project_id: state.projectId, name: name, frame_rate: fps });
if (!r.success) { toast('Failed to create sequence', r.error, 'error'); return; }
closePanel('seq');
document.getElementById('newSeqName').value = '';