feat(ui): global keyboard shortcuts for record toggle + Settings (T7.4, FR-UX-3)

Ctrl+Shift+R toggles recording, Ctrl+, toggles Settings, Escape closes
whichever overlay (consent notice, then Settings) is open. Shortcuts
never hijack typing in an input/textarea/select/contenteditable target;
Escape is exempt from that guard so it always closes an open modal.

"Template apply" from the roadmap item isn't included — there's no
template-selection UI yet (that lands with T8.1 in Phase 8).
This commit is contained in:
iamdoubz
2026-07-01 22:38:09 -05:00
parent bbe8a28144
commit b95ffa5bc1
+52 -5
View File
@@ -34,7 +34,12 @@
systemPrefersDark = media.matches;
const onSystemThemeChange = (e: MediaQueryListEvent) => (systemPrefersDark = e.matches);
media.addEventListener("change", onSystemThemeChange);
return () => media.removeEventListener("change", onSystemThemeChange);
window.addEventListener("keydown", onGlobalKeydown);
return () => {
media.removeEventListener("change", onSystemThemeChange);
window.removeEventListener("keydown", onGlobalKeydown);
};
});
function startRecording() {
@@ -42,6 +47,39 @@
recording.start(undefined, settings.settings.default_record);
}
function toggleRecording() {
if (recording.state === "idle") startRecording();
else recording.stop();
}
// Global shortcuts (T7.4, FR-UX-3): record start/stop, view toggles.
// "Template apply" isn't included — there's no template UI yet (Phase 8, T8.1).
function isEditableTarget(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return false;
return (
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.tagName === "SELECT" ||
target.isContentEditable
);
}
function onGlobalKeydown(e: KeyboardEvent) {
if (e.key === "Escape") {
if (showConsent) showConsent = false;
else if (showSettings) showSettings = false;
return;
}
if (isEditableTarget(e.target)) return; // never hijack typing in a field
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "r") {
e.preventDefault();
toggleRecording();
} else if (e.ctrlKey && e.key === ",") {
e.preventDefault();
showSettings = !showSettings;
}
}
// Mid-meeting retention toggle (FR-REC-1); gates on the same one-time
// consent notice as the Settings default-record toggle (FR-REC-2).
function onToggleRetention(e: Event) {
@@ -67,9 +105,17 @@
<span class="muted">local · private</span>
<div class="spacer"></div>
{#if recording.state === "idle"}
<button onclick={startRecording}>● Record</button>
<button
onclick={startRecording}
title="Start recording (Ctrl+Shift+R)"
aria-keyshortcuts="Control+Shift+R">● Record</button
>
{:else}
<button onclick={() => recording.stop()}>■ Stop</button>
<button
onclick={() => recording.stop()}
title="Stop recording (Ctrl+Shift+R)"
aria-keyshortcuts="Control+Shift+R">■ Stop</button
>
<span class="rec">Recording…</span>
<LevelMeter rms={recording.levelRms} peak={recording.levelPeak} />
{#if settings.hardware}
@@ -99,8 +145,9 @@
<button
class="icon"
aria-label="Settings"
title="Settings"
onclick={() => (showSettings = true)}>⚙</button
title="Settings (Ctrl+,)"
aria-keyshortcuts="Control+,"
onclick={() => (showSettings = !showSettings)}>⚙</button
>
</header>