Skip to content
Open
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
142 changes: 126 additions & 16 deletions src/components/VideoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,72 @@ function Section({ icon, title, children, delay = 0 }: SectionProps) {
);
}

/** Accordion section with collapsible content. */
function AccordionSection({
id,
icon,
title,
children,
isOpen,
onToggle,
delay = 0,
}: {
id: string;
icon: React.ReactNode;
title: string;
children: React.ReactNode;
isOpen: boolean;
onToggle: () => void;
delay?: number;
}) {
const contentRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
if (!contentRef.current) return;
if (isOpen) {
contentRef.current.style.maxHeight = `${contentRef.current.scrollHeight}px`;
} else {
contentRef.current.style.maxHeight = `0px`;
}
}, [isOpen]);

return (
<div className="animate-fade-in" style={{ animationDelay: `${delay}ms` }}>
<button
type="button"
aria-expanded={isOpen}
aria-controls={`${id}-panel`}
onClick={onToggle}
className="w-full flex items-center justify-between px-3 py-2 text-left hover:bg-[var(--border)] transition-colors duration-150"
>
<div className="flex items-center gap-2">
<span className="text-film-500 opacity-80">{icon}</span>
<span className="text-sm font-heading font-bold uppercase tracking-widest text-[var(--muted)]">{title}</span>
</div>
<svg
aria-hidden="true"
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
className={cn("text-[var(--muted)] transition-transform duration-200", isOpen && "rotate-180")}
>
<path d="M2 4l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>

<div
id={`${id}-panel`}
ref={contentRef}
className="overflow-hidden transition-all duration-200"
style={{ maxHeight: isOpen ? undefined : 0 }}
>
<div className="px-3 pt-3 pb-0">{children}</div>
</div>
</div>
);
}

/** Inline keyboard hint badge. */
function Kbd({ children }: { children: React.ReactNode }) {
return (
Expand Down Expand Up @@ -166,6 +232,16 @@ export default function VideoEditor() {

const [copied, setCopied] = useState(false);
const [shareCopied, setShareCopied] = useState(false);
const [openSections, setOpenSections] = useState({
resize: true,
trim: false,
rotation: false,
audio: false,
export: false,
});

const toggleSection = (key: keyof typeof openSections) =>
setOpenSections((prev) => ({ ...prev, [key]: !prev[key] }));
const downloadRef = useRef<HTMLDivElement>(null);

const handleCopyLink = () => {
Expand Down Expand Up @@ -273,23 +349,44 @@ export default function VideoEditor() {
isProcessing && "pointer-events-none opacity-50"
)}>
<div className="bg-[var(--surface)] rounded-xl border border-[var(--border)] p-5 space-y-6">
<Section icon={<Scissors size={12} />} title="Trim" delay={50}>
<AccordionSection
id="trim"
icon={<Scissors size={12} />}
title="Trim"
isOpen={openSections.trim}
onToggle={() => toggleSection("trim")}
delay={50}
>
<TrimControl
recipe={recipe}
onChange={updateRecipe}
duration={duration}
file={file}
file={file}
/>
</Section>
<Section icon={<RotateCw size={12} />} title="Rotate" delay={100}>
</AccordionSection>

<AccordionSection
id="rotation"
icon={<RotateCw size={12} />}
title="Rotation"
isOpen={openSections.rotation}
onToggle={() => toggleSection("rotation")}
delay={100}
>
<RotateControl recipe={recipe} onChange={updateRecipe} />
</Section>
</AccordionSection>
</div>
<div className="bg-[var(--surface)] rounded-xl border border-[var(--border)] p-5 space-y-6">
<Section icon={<Volume2 size={12} />} title="Audio & Speed" delay={150}>

<AccordionSection
id="audio"
icon={<Volume2 size={12} />}
title="Audio & Speed"
isOpen={openSections.audio}
onToggle={() => toggleSection("audio")}
delay={150}
>
<AudioSpeedControl recipe={recipe} onChange={updateRecipe} />
</Section>
</AccordionSection>
<Section
icon={<SlidersHorizontal size={12} />}
title="Adjustments"
Expand Down Expand Up @@ -376,9 +473,16 @@ export default function VideoEditor() {
<Section icon={<SlidersHorizontal size={12} />} title="Output format" delay={190}>
<FormatSelector recipe={recipe} onChange={updateRecipe} />
</Section>
<Section icon={<SlidersHorizontal size={12} />} title="Export quality" delay={200}>
<AccordionSection
id="export"
icon={<SlidersHorizontal size={12} />}
title="Export"
isOpen={openSections.export}
onToggle={() => toggleSection("export")}
delay={200}
>
<ExportSettings recipe={recipe} duration={duration} onChange={updateRecipe} />
</Section>
</AccordionSection>
<Section icon={<Layers size={12} />} title="Image overlay" delay={120}>
<ImageOverlay
overlayFile={overlayFile}
Expand Down Expand Up @@ -442,7 +546,14 @@ export default function VideoEditor() {
isProcessing && "pointer-events-none opacity-50"
)}>
<div className="bg-[var(--surface)] rounded-xl border border-[var(--border)] p-5 space-y-6 animate-fade-in" style={{ animationDelay: "50ms" }}>
<Section icon={<Layers size={12} />} title="Output size">
<AccordionSection
id="resize"
icon={<Layers size={12} />}
title="Resize & Aspect Ratio"
isOpen={openSections.resize}
onToggle={() => toggleSection("resize")}
delay={50}
>
{recommendedPreset && (
<div className="mb-4 rounded-2xl border border-film-200 bg-film-50 p-3 text-sm text-film-700">
<p>
Expand All @@ -451,11 +562,10 @@ export default function VideoEditor() {
</div>
)}
<PresetSelector recipe={recipe} onChange={updateRecipe} />
</Section>

<Section icon={<Crop size={12} />} title="Framing" delay={100}>
<FramingControl recipe={recipe} onChange={updateRecipe} />
</Section>
<div className="mt-3">
<FramingControl recipe={recipe} onChange={updateRecipe} />
</div>
</AccordionSection>

<div className="pt-2 flex justify-between items-center">
<button
Expand Down
Loading