Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion components/ImportTranscriptOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,14 @@ export default function ImportTranscriptOption() {
}
// Do not set source to "import" until a file is chosen. Close the menu
// before the OS dialog so cancel cannot leave it pinned open.
// Open the picker in this same user-gesture turn — deferring to rAF
// drops Chrome's user activation and the dialog never appears.
pickGenRef.current += 1;
setPicking(true);
setError(null);
const input = fileRef.current;
ctx.closeMenu();
requestAnimationFrame(() => fileRef.current?.click());
input?.click();
}}
>
<ImportTrigger
Expand Down
27 changes: 20 additions & 7 deletions components/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,13 @@ type PopoverProps = {
padding?: number;
/** Portal content to document.body. Default true. */
portal?: boolean;
/** Full-screen dismiss catcher for Electron drag regions. Default false. */
/**
* Full-screen dismiss catcher for Electron drag regions (and a reliable
* outside-press target on web). Rendered in a portal under the panel.
* Default false.
*/
backdrop?: boolean;
/** Stacking class for the portaled backdrop; keep below PopoverContent. */
backdropZClassName?: string;
/** Stop Escape from bubbling (nested flyouts). Default false. */
escapeStopPropagation?: boolean;
Expand Down Expand Up @@ -128,16 +133,24 @@ export default function Popover({
};
}, [open, onOpenChange, refs, escapeStopPropagation]);

// Portal the backdrop with the panel so both share the same stacking root
// (document.body). An in-tree fixed backdrop can end up above the portaled
// panel when an ancestor creates a stacking context — clicks then hit the
// dismiss layer instead of links / controls inside the menu.
const backdropNode =
backdrop && open ? (
<PopupDismissBackdrop
onDismiss={() => onOpenChange(false)}
zClassName={backdropZClassName}
/>
) : null;

return (
<PopoverContext.Provider
value={{ open, setReference, setFloating, floatingStyles, portal }}
>
{backdrop && open && (
<PopupDismissBackdrop
onDismiss={() => onOpenChange(false)}
zClassName={backdropZClassName}
/>
)}
{backdropNode &&
(portal ? <FloatingPortal>{backdropNode}</FloatingPortal> : backdropNode)}
{children}
</PopoverContext.Provider>
);
Expand Down
4 changes: 4 additions & 0 deletions components/SettingsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ export default function SettingsMenu() {
href={href}
target="_blank"
rel="noopener noreferrer"
// Keep the click on the anchor — popover dismiss listeners must
// not treat this as an outside press or swallow navigation.
onPointerDown={(e) => e.stopPropagation()}
onClick={() => setOpen(false)}
className="flex items-center gap-2.5 px-3 py-2 text-[13px] text-zinc-700 transition hover:bg-zinc-50 dark:text-zinc-300 dark:hover:bg-zinc-800/80"
>
<span className="shrink-0 text-zinc-400 dark:text-zinc-500">
Expand Down
28 changes: 14 additions & 14 deletions components/TranscriptPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,25 +334,25 @@ export default function TranscriptPanel() {
)}
{(status === "ready" || status === "error" || status === "transcribing") && (
<>
<button
onClick={() => importInputRef.current?.click()}
<label
title="Replace transcript from SRT, VTT, or JSON"
className="flex cursor-pointer h-7 items-center gap-1.5 rounded-lg px-2 text-xs text-zinc-500 transition hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800"
>
<ArrowUpFromLine size={14} />
<span className="hidden sm:inline">Import</span>
</button>
<input
ref={importInputRef}
type="file"
accept={TRANSCRIPT_ACCEPT}
className="hidden"
onChange={(e) => {
const files = e.target.files;
e.target.value = "";
void handleImportTranscript(files);
}}
/>
<input
ref={importInputRef}
type="file"
accept={TRANSCRIPT_ACCEPT}
// Keep in the layout tree — display:none can block the OS picker.
className="sr-only"
onChange={(e) => {
const files = e.target.files;
e.target.value = "";
void handleImportTranscript(files);
}}
/>
</label>
</>
)}
<button
Expand Down
32 changes: 25 additions & 7 deletions components/UploadScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useId, useRef, useState } from "react";
import Image from "next/image";
import {
AudioLines,
Clapperboard,
Film,
Loader2,
Lock,

Check warning on line 10 in components/UploadScreen.tsx

View workflow job for this annotation

GitHub Actions / check

'Lock' is defined but never used
Music,
Scissors,
ShieldAlert,
Expand Down Expand Up @@ -159,6 +159,7 @@
onFile: (file: File, options?: { words?: Word[] }) => void;
}) {
const inputRef = useRef<HTMLInputElement>(null);
const inputId = useId();
const [dragging, setDragging] = useState(false);
const [projects, setProjects] = useState<ProjectMeta[]>([]);
const [busyId, setBusyId] = useState<string | null>(null);
Expand Down Expand Up @@ -286,12 +287,25 @@
</div>
</div>
)}
<div
role="button"
{/*
Native <label htmlFor> opens the file dialog without a synthetic
input.click(). display:none inputs + .click() fail in some Chromium
setups (DnD still works), which matches "browse does nothing".
*/}
<label
htmlFor={inputId}
aria-disabled={!ready}
tabIndex={ready ? 0 : -1}
onClick={() => ready && inputRef.current?.click()}
onKeyDown={(e) => ready && e.key === "Enter" && inputRef.current?.click()}
onClick={(e) => {
if (!ready) e.preventDefault();
}}
onKeyDown={(e) => {
if (!ready) return;
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
inputRef.current?.click();
}
}}
onDragOver={(e) => {
e.preventDefault();
if (ready) setDragging(true);
Expand Down Expand Up @@ -358,13 +372,17 @@
</>
)}
<input
id={inputId}
ref={inputRef}
type="file"
accept={MEDIA_ACCEPT}
className="hidden"
disabled={!ready}
// Visually hidden but present in the layout tree — display:none
// breaks programmatic / label-activated pickers in some browsers.
className="sr-only"
onChange={(e) => handleFiles(e.target.files)}
/>
</div>
</label>

{ready && (
<RecentProjects
Expand Down
Loading