diff --git a/components/Editor.tsx b/components/Editor.tsx index e7a20b4..b5fa16d 100644 --- a/components/Editor.tsx +++ b/components/Editor.tsx @@ -30,6 +30,7 @@ import ModelSelector, { } from "./ModelSelector"; import ImportTranscriptOption from "./ImportTranscriptOption"; import { MODEL_ORDER } from "@/lib/models"; +import { isTypingTarget } from "@/lib/keyboard"; /** How long the desktop mode-change overlay stays up. Matches the macOS * `setBounds(..., animate)` duration plus a small buffer so the layout @@ -254,9 +255,7 @@ export default function Editor() { // (which would double-toggle playback and look like the hotkey "didn't work"). useEffect(() => { const handler = (e: KeyboardEvent) => { - const target = e.target as HTMLElement; - if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) - return; + if (isTypingTarget(e.target)) return; const s = useEditorStore.getState(); if (e.code === "Space" && s.videoEl && !s.exportOpen) { e.preventDefault(); diff --git a/components/TranscriptPanel.tsx b/components/TranscriptPanel.tsx index b5243a5..9534161 100644 --- a/components/TranscriptPanel.tsx +++ b/components/TranscriptPanel.tsx @@ -42,6 +42,7 @@ import { useTranscriptPlayheadFollow } from "@/hooks/useTranscriptPlayheadFollow import { useWordAnchorFloating } from "@/hooks/useWordAnchorFloating"; import { useCutRanges } from "@/hooks/useCutRanges"; import { findActiveWordId, groupWordsBySpeaker } from "@/lib/transcript"; +import { isTypingTarget } from "@/lib/keyboard"; const WordSpan = memo(function WordSpan({ word, @@ -306,9 +307,7 @@ export default function TranscriptPanel() { useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key !== "Escape") return; - const target = e.target as HTMLElement; - if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) - return; + if (isTypingTarget(e.target)) return; if (selectedWordIds.length === 0) return; e.preventDefault(); clearSelection(); @@ -321,9 +320,7 @@ export default function TranscriptPanel() { useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key !== "@" || e.metaKey || e.ctrlKey || e.altKey) return; - const target = e.target as HTMLElement; - if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) - return; + if (isTypingTarget(e.target)) return; if (!selection || assigningSpeaker || correcting) return; e.preventDefault(); openSpeakerAssign(); diff --git a/lib/keyboard.ts b/lib/keyboard.ts new file mode 100644 index 0000000..3f21191 --- /dev/null +++ b/lib/keyboard.ts @@ -0,0 +1,14 @@ +/** + * True when a keystroke belongs to a text field rather than the global + * shortcuts. File inputs are excluded on purpose: the desktop File › Open + * Project… menu and the transcript Import label both click a hidden + * ``, which keeps focus after the picker closes — treating + * that as "typing" silently killed the spacebar play/pause hotkey. + */ +export function isTypingTarget(target: EventTarget | null): boolean { + const el = target as HTMLElement | null; + if (!el) return false; + if (el.isContentEditable) return true; + if (el.tagName === "TEXTAREA") return true; + return el.tagName === "INPUT" && (el as HTMLInputElement).type !== "file"; +}