Skip to content
Open
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
20 changes: 20 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,26 @@ textarea:focus {
}

:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 4px;
}

@keyframes scale-in {
from {
opacity: 0;
transform: scale(0.96);
}

to {
opacity: 1;
transform: scale(1);
}
}

.animate-scale-in {
animation: scale-in 0.2s ease-out;
}
outline: 0;
box-shadow: 0 0 0 3px var(--accent-muted);
border-radius: var(--radius);
Expand Down
107 changes: 107 additions & 0 deletions src/components/KeyboardShortcutsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"use client";

import { Keyboard, X } from "lucide-react";

interface KeyboardShortcutsModalProps {
open: boolean;
onClose: () => void;
}

function Kbd({ children }: { children: React.ReactNode }) {
return (
<kbd className="px-2 py-1 rounded-md border border-white/10 bg-white/5 text-xs font-mono text-white shadow-sm">
{children}
</kbd>
);
}

export default function KeyboardShortcutsModal({
open,
onClose,
}: KeyboardShortcutsModalProps) {
if (!open) return null;

const shortcuts = [
{
keys: ["Ctrl", "Shift", "E"],
label: "Export video",
},
{
keys: ["M"],
label: "Toggle audio mute",
},
{
keys: ["R"],
label: "Reset all settings",
},
{
keys: ["Esc"],
label: "Cancel export / Close modal",
},
{
keys: ["1 - 9"],
label: "Switch presets",
},
{
keys: ["?"],
label: "Open keyboard shortcuts",
},
];

return (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm animate-fade-in">
<div
role="dialog"
aria-modal="true"
aria-labelledby="keyboard-shortcuts-title"
className="relative w-[92%] max-w-xl rounded-2xl border border-white/10 bg-white/10 backdrop-blur-xl shadow-2xl p-6 animate-scale-in"
>
<button
onClick={onClose}
className="absolute top-4 right-4 text-gray-300 hover:text-white transition"
aria-label="Close shortcuts modal"
>
<X size={20} />
</button>

<div className="flex items-center gap-3 mb-6">
<div className="p-2 rounded-lg bg-white/10 border border-white/10">
<Keyboard size={20} className="text-white" />
</div>

<div>
<h2
id="keyboard-shortcuts-title"
className="text-xl font-semibold text-white"
>
Keyboard Shortcuts
</h2>

<p className="text-sm text-gray-300">
Speed up your workflow with shortcuts
</p>
</div>
</div>

<div className="space-y-3">
{shortcuts.map((shortcut) => (
<div
key={shortcut.label}
className="flex items-center justify-between rounded-xl border border-white/10 bg-white/5 px-4 py-3"
>
<span className="text-sm text-gray-200">
{shortcut.label}
</span>

<div className="flex items-center gap-2">
{shortcut.keys.map((key) => (
<Kbd key={key}>{key}</Kbd>
))}
</div>
</div>
))}
</div>
</div>
</div>
);
}
144 changes: 64 additions & 80 deletions src/components/VideoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,20 @@ import { getPresetById } from "@/lib/presets";

import { cn } from "@/lib/utils";
import {
Layers, Crop, Scissors, RotateCw, Volume2, Type,
SlidersHorizontal, Zap, AlertTriangle, Github, Copy
Layers,
Crop,
Scissors,
RotateCw,
Volume2,
SlidersHorizontal,
Zap,
AlertTriangle,
Github,
Copy,
Keyboard,
} from "lucide-react";
import OnboardingTour from "./OnboardingTour";
import KeyboardShortcutsModal from "./KeyboardShortcutsModal";
import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts";

interface SectionProps {
Expand Down Expand Up @@ -52,6 +62,7 @@ function Section({ icon, title, children, delay = 0 }: SectionProps) {
);
}


/** Accordion section with collapsible content. */
function AccordionSection({
id,
Expand Down Expand Up @@ -117,84 +128,8 @@ function Kbd({ children }: { children: React.ReactNode }) {
);
}

/** Collapsible panel that lists all keyboard shortcuts. */
function KeyboardShortcutsPanel() {
const [open, setOpen] = useState(false);

const shortcuts: { keys: React.ReactNode[]; label: string }[] = [
{
keys: [
<Kbd key="ctrl">Ctrl</Kbd>,
<span key="plus1" className="text-[var(--muted)] text-xs">+</span>,
<Kbd key="shift">Shift</Kbd>,
<span key="plus2" className="text-[var(--muted)] text-xs">+</span>,
<Kbd key="e">E</Kbd>
],
label: "Export video",
},
{
keys: [<Kbd key="m">M</Kbd>],
label: "Toggle audio mute",
},
{
keys: [<Kbd key="r">R</Kbd>],
label: "Reset all settings",
},
{
keys: [<Kbd key="esc">Esc</Kbd>],
label: "Cancel export",
},
{
keys: [<Kbd key="1">1</Kbd>, <span key="dash" className="text-[var(--muted)] text-xs">–</span>, <Kbd key="9">9</Kbd>],
label: "Switch preset by index",
},
{
keys: [<Kbd key="question">?</Kbd>],
label: "Toggle this panel",
},
];

return (
<div className="rounded-xl border border-[var(--border)] bg-[var(--surface)] animate-fade-in overflow-hidden">
<button
type="button"
aria-expanded={open}
aria-controls="keyboard-shortcuts-list"
onClick={() => setOpen((v) => !v)}
className="w-full flex items-center justify-between px-4 py-3 text-left hover:bg-[var(--border)] transition-colors duration-150"
>
<span className="text-[10px] font-heading font-bold uppercase tracking-widest text-[var(--muted)] flex items-center gap-2">
<Kbd>⌨</Kbd>
Keyboard Shortcuts
</span>
<svg
aria-hidden="true"
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
className={cn("text-[var(--muted)] transition-transform duration-200", open && "rotate-180")}
>
<path d="M2 4l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>

{open && (
<ul
id="keyboard-shortcuts-list"
className="px-4 pb-3 space-y-2 border-t border-[var(--border)]"
>
{shortcuts.map(({ keys, label }) => (
<li key={label} className="flex items-center justify-between gap-3 pt-2">
<span className="text-xs text-[var(--muted)]">{label}</span>
<span className="flex items-center gap-1 shrink-0">{keys}</span>
</li>
))}
</ul>
)}
</div>
);
}
/** Collapsible panel that lists all keyboard shortcuts. */

export default function VideoEditor() {
const {
Expand All @@ -220,10 +155,12 @@ export default function VideoEditor() {
handleExport,
status,
cancelExport,
onToggleShortcutsModal: () => {},
onToggleShortcutsModal: () =>
setShortcutsOpen((prev) => !prev),
});

const [copied, setCopied] = useState(false);
const [shortcutsOpen, setShortcutsOpen] = useState(false);
const [shareCopied, setShareCopied] = useState(false);
const [selectedTextId, setSelectedTextId] = useState<string | null>(null);
const [openSections, setOpenSections] = useState({
Expand Down Expand Up @@ -308,6 +245,20 @@ export default function VideoEditor() {
};
}, [videoSrc]);

useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === "Escape" && shortcutsOpen) {
setShortcutsOpen(false);
}
};

window.addEventListener("keydown", handleEsc);

return () => {
window.removeEventListener("keydown", handleEsc);
};
}, [shortcutsOpen]);

return (
<div className="min-h-screen relative flex flex-col" style={{ background: "var(--bg)" }}>
<ExportOverlay
Expand All @@ -317,6 +268,10 @@ export default function VideoEditor() {
onCancel={cancelExport}
/>
<OnboardingTour />
<KeyboardShortcutsModal
open={shortcutsOpen}
onClose={() => setShortcutsOpen(false)}
/>

<div aria-live="polite" aria-atomic="true" className="sr-only">
{status === "exporting" && `Exporting video: ${progress}%`}
Expand All @@ -325,6 +280,35 @@ export default function VideoEditor() {
</div>

<div className="max-w-6xl mx-auto px-4 py-8 pb-6 flex-1 w-full">


<header className="mb-10 flex items-end justify-between animate-fade-in">
<div
className="inline-block px-5 py-3 rounded-xl border border-[var(--border)] bg-[var(--surface)] shadow-sm border-l-4 border-l-film-600"
aria-label="Reframe — video editor"
>
<h1 className="font-display text-6xl leading-none tracking-widest2 text-[var(--text)]">
REFRAME
</h1>
<p className="font-heading text-sm text-[var(--muted)] mt-1 uppercase tracking-widest">
Your video, any format
</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => setShortcutsOpen(true)}
className="p-2 rounded-xl border border-[var(--border)] bg-[var(--surface)] hover:bg-white/5 transition"
aria-label="Open keyboard shortcuts"
>
<Keyboard size={18} />
</button>

<div className="hidden sm:flex items-center gap-2 text-sm font-heading font-semibold uppercase tracking-widest text-[var(--muted)] pb-1">
<span className="w-1.5 h-1.5 rounded-full bg-green-400 inline-block animate-pulse" />
No login. No ads. 100% private - your video never leaves your device.
</div>
</div>
</header>
<header className="mb-10 flex flex-col items-center justify-center gap-4 animate-fade-in">
<div
className="inline-block rounded-xl border border-[var(--border)] bg-[var(--surface)] shadow-sm border-l-4 border-l-film-600 mx-auto w-fit min-w-min"
Expand Down