summaryrefslogtreecommitdiff
path: root/apps/web/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/components')
-rw-r--r--apps/web/src/components/editor-header.tsx2
-rw-r--r--apps/web/src/components/editor/audio-waveform.tsx10
-rw-r--r--apps/web/src/components/editor/media-panel/tabbar.tsx4
-rw-r--r--apps/web/src/components/editor/preview-panel.tsx18
-rw-r--r--apps/web/src/components/keyboard-shortcuts-help.tsx4
-rw-r--r--apps/web/src/components/panel-preset-selector.tsx91
-rw-r--r--apps/web/src/components/ui/editable-timecode.tsx274
7 files changed, 249 insertions, 154 deletions
diff --git a/apps/web/src/components/editor-header.tsx b/apps/web/src/components/editor-header.tsx
index 77202c36..e197f2a4 100644
--- a/apps/web/src/components/editor-header.tsx
+++ b/apps/web/src/components/editor-header.tsx
@@ -29,6 +29,7 @@ import { useRouter } from "next/navigation";
import { FaDiscord } from "react-icons/fa6";
import { useTheme } from "next-themes";
import { usePlaybackStore } from "@/stores/playback-store";
+import { PanelPresetSelector } from "./panel-preset-selector";
export function EditorHeader() {
const { getTotalDuration } = useTimelineStore();
@@ -147,6 +148,7 @@ export function EditorHeader() {
const rightContent = (
<nav className="flex items-center gap-2">
+ <PanelPresetSelector />
<KeyboardShortcutsHelp />
<Button
size="sm"
diff --git a/apps/web/src/components/editor/audio-waveform.tsx b/apps/web/src/components/editor/audio-waveform.tsx
index 372a31c5..79bf401d 100644
--- a/apps/web/src/components/editor/audio-waveform.tsx
+++ b/apps/web/src/components/editor/audio-waveform.tsx
@@ -20,7 +20,7 @@ const AudioWaveform: React.FC<AudioWaveformProps> = ({
useEffect(() => {
let mounted = true;
let ws = wavesurfer.current;
-
+
const initWaveSurfer = async () => {
if (!waveformRef.current || !audioUrl) return;
@@ -90,7 +90,7 @@ const AudioWaveform: React.FC<AudioWaveformProps> = ({
const wsToDestroy = ws;
// Detach from ref immediately
wavesurfer.current = null;
-
+
// Wait a tick to destroy so any pending operations can complete
requestAnimationFrame(() => {
try {
@@ -111,13 +111,13 @@ const AudioWaveform: React.FC<AudioWaveformProps> = ({
return () => {
// Mark component as unmounted
mounted = false;
-
+
// Store reference to current wavesurfer instance
const wsToDestroy = wavesurfer.current;
-
+
// Immediately clear the ref to prevent accessing it after unmount
wavesurfer.current = null;
-
+
// If we have an instance to clean up, do it safely
if (wsToDestroy) {
// Delay destruction to avoid race conditions
diff --git a/apps/web/src/components/editor/media-panel/tabbar.tsx b/apps/web/src/components/editor/media-panel/tabbar.tsx
index fc4f677d..6fab0abf 100644
--- a/apps/web/src/components/editor/media-panel/tabbar.tsx
+++ b/apps/web/src/components/editor/media-panel/tabbar.tsx
@@ -77,7 +77,9 @@ export function TabBar() {
<div
className={cn(
"flex flex-col gap-0.5 items-center cursor-pointer opacity-100 hover:opacity-75",
- activeTab === tabKey ? "text-primary !opacity-100" : "text-muted-foreground"
+ activeTab === tabKey
+ ? "text-primary !opacity-100"
+ : "text-muted-foreground"
)}
onClick={() => setActiveTab(tabKey)}
key={tabKey}
diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx
index 58cc5d5b..96166d43 100644
--- a/apps/web/src/components/editor/preview-panel.tsx
+++ b/apps/web/src/components/editor/preview-panel.tsx
@@ -350,18 +350,20 @@ export function PreviewPanel() {
style={{
left: `${
50 +
- ((dragState.isDragging && dragState.elementId === element.id
- ? dragState.currentX
- : element.x) /
- canvasSize.width) *
+ (
+ (dragState.isDragging && dragState.elementId === element.id
+ ? dragState.currentX
+ : element.x) / canvasSize.width
+ ) *
100
}%`,
top: `${
50 +
- ((dragState.isDragging && dragState.elementId === element.id
- ? dragState.currentY
- : element.y) /
- canvasSize.height) *
+ (
+ (dragState.isDragging && dragState.elementId === element.id
+ ? dragState.currentY
+ : element.y) / canvasSize.height
+ ) *
100
}%`,
transform: `translate(-50%, -50%) rotate(${element.rotation}deg) scale(${scaleRatio})`,
diff --git a/apps/web/src/components/keyboard-shortcuts-help.tsx b/apps/web/src/components/keyboard-shortcuts-help.tsx
index d4d121d9..542c7734 100644
--- a/apps/web/src/components/keyboard-shortcuts-help.tsx
+++ b/apps/web/src/components/keyboard-shortcuts-help.tsx
@@ -94,9 +94,7 @@ const EditableShortcutKey = ({
variant="text"
size="sm"
className={`inline-flex font-sans text-xs rounded px-2 min-w-6 min-h-6 leading-none items-center justify-center shadow-xs border mr-1 cursor-pointer hover:bg-opacity-80 ${
- isRecording
- ? "border-primary bg-primary/10"
- : "border bg-accent"
+ isRecording ? "border-primary bg-primary/10" : "border bg-accent"
}`}
onClick={handleClick}
title={
diff --git a/apps/web/src/components/panel-preset-selector.tsx b/apps/web/src/components/panel-preset-selector.tsx
new file mode 100644
index 00000000..faec916d
--- /dev/null
+++ b/apps/web/src/components/panel-preset-selector.tsx
@@ -0,0 +1,91 @@
+"use client";
+
+import { Button } from "./ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "./ui/dropdown-menu";
+import { ChevronDown, RotateCcw, LayoutPanelTop } from "lucide-react";
+import { usePanelStore, type PanelPreset } from "@/stores/panel-store";
+
+const PRESET_LABELS: Record<PanelPreset, string> = {
+ default: "Default",
+ media: "Media",
+ inspector: "Inspector",
+ "vertical-preview": "Vertical Preview",
+};
+
+const PRESET_DESCRIPTIONS: Record<PanelPreset, string> = {
+ default: "Media, preview, and inspector on top row, timeline on bottom",
+ media: "Full height media on left, preview and inspector on top row",
+ inspector: "Full height inspector on right, media and preview on top row",
+ "vertical-preview": "Full height preview on right for vertical videos",
+};
+
+export function PanelPresetSelector() {
+ const { activePreset, setActivePreset, resetPreset } = usePanelStore();
+
+ const handlePresetChange = (preset: PanelPreset) => {
+ setActivePreset(preset);
+ };
+
+ const handleResetPreset = (preset: PanelPreset, event: React.MouseEvent) => {
+ event.stopPropagation();
+ resetPreset(preset);
+ };
+
+ return (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ variant="secondary"
+ size="sm"
+ className="h-8 px-2 flex items-center gap-1 text-xs"
+ title="Panel Presets"
+ >
+ <LayoutPanelTop className="h-4 w-4" />
+ <ChevronDown className="h-3 w-3" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="start" className="w-64">
+ <div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">
+ Panel Presets
+ </div>
+ <DropdownMenuSeparator />
+ {(Object.keys(PRESET_LABELS) as PanelPreset[]).map((preset) => (
+ <DropdownMenuItem
+ key={preset}
+ onClick={() => handlePresetChange(preset)}
+ className="flex items-start justify-between gap-2 py-2 cursor-pointer"
+ >
+ <div className="flex-1 min-w-0">
+ <div className="flex items-center gap-2">
+ <span className="font-medium text-sm">
+ {PRESET_LABELS[preset]}
+ </span>
+ {activePreset === preset && (
+ <div className="h-2 w-2 rounded-full bg-primary" />
+ )}
+ </div>
+ <p className="text-xs text-muted-foreground mt-0.5 leading-tight">
+ {PRESET_DESCRIPTIONS[preset]}
+ </p>
+ </div>
+ <Button
+ variant="secondary"
+ size="icon"
+ className="h-6 w-6 shrink-0 opacity-60 hover:opacity-100"
+ onClick={(e) => handleResetPreset(preset, e)}
+ title={`Reset ${PRESET_LABELS[preset]} preset`}
+ >
+ <RotateCcw className="h-3 w-3" />
+ </Button>
+ </DropdownMenuItem>
+ ))}
+ </DropdownMenuContent>
+ </DropdownMenu>
+ );
+}
diff --git a/apps/web/src/components/ui/editable-timecode.tsx b/apps/web/src/components/ui/editable-timecode.tsx
index f6d29f49..55375e33 100644
--- a/apps/web/src/components/ui/editable-timecode.tsx
+++ b/apps/web/src/components/ui/editable-timecode.tsx
@@ -1,137 +1,137 @@
-"use client";
-
-import { useState, useRef, useEffect } from "react";
-import { cn } from "@/lib/utils";
-import { formatTimeCode, parseTimeCode } from "@/lib/time";
-
-interface EditableTimecodeProps {
- time: number;
- duration?: number;
- format?: "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF";
- fps?: number;
- onTimeChange?: (time: number) => void;
- className?: string;
- disabled?: boolean;
-}
-
-export function EditableTimecode({
- time,
- duration,
- format = "HH:MM:SS:FF",
- fps = 30,
- onTimeChange,
- className,
- disabled = false,
-}: EditableTimecodeProps) {
- const [isEditing, setIsEditing] = useState(false);
- const [inputValue, setInputValue] = useState("");
- const [hasError, setHasError] = useState(false);
- const inputRef = useRef<HTMLInputElement>(null);
- const enterPressedRef = useRef(false);
-
- const formattedTime = formatTimeCode(time, format, fps);
-
- const startEditing = () => {
- if (disabled) return;
- setIsEditing(true);
- setInputValue(formattedTime);
- setHasError(false);
- enterPressedRef.current = false;
- };
-
- const cancelEditing = () => {
- setIsEditing(false);
- setInputValue("");
- setHasError(false);
- enterPressedRef.current = false;
- };
-
- const applyEdit = () => {
- const parsedTime = parseTimeCode(inputValue, format, fps);
-
- if (parsedTime === null) {
- setHasError(true);
- return;
- }
-
- // Clamp time to valid range
- const clampedTime = Math.max(
- 0,
- duration ? Math.min(duration, parsedTime) : parsedTime
- );
-
- onTimeChange?.(clampedTime);
- setIsEditing(false);
- setInputValue("");
- setHasError(false);
- enterPressedRef.current = false;
- };
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === "Enter") {
- e.preventDefault();
- enterPressedRef.current = true;
- applyEdit();
- } else if (e.key === "Escape") {
- e.preventDefault();
- cancelEditing();
- }
- };
-
- const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- setInputValue(e.target.value);
- setHasError(false);
- };
-
- const handleBlur = () => {
- // Only apply edit if Enter wasn't pressed (to avoid double processing)
- if (!enterPressedRef.current && isEditing) {
- applyEdit();
- }
- };
-
- // Focus input when entering edit mode
- useEffect(() => {
- if (isEditing && inputRef.current) {
- inputRef.current.focus();
- inputRef.current.select();
- }
- }, [isEditing]);
-
- if (isEditing) {
- return (
- <input
- ref={inputRef}
- type="text"
- value={inputValue}
- onChange={handleInputChange}
- onKeyDown={handleKeyDown}
- onBlur={handleBlur}
- className={cn(
- "text-xs font-mono bg-transparent border-none outline-none",
- "focus:bg-background focus:border focus:border-primary focus:px-1 focus:rounded",
- "tabular-nums text-primary",
- hasError && "text-destructive focus:border-destructive",
- className
- )}
- style={{ width: `${formattedTime.length + 1}ch` }}
- placeholder={formattedTime}
- />
- );
- }
-
- return (
- <span
- onClick={startEditing}
- className={cn(
- "text-xs font-mono tabular-nums text-primary cursor-pointer",
- "hover:bg-muted/50 hover:rounded px-1 -mx-1 transition-colors",
- disabled && "cursor-default hover:bg-transparent",
- className
- )}
- title={disabled ? undefined : "Click to edit time"}
- >
- {formattedTime}
- </span>
- );
-}
+"use client";
+
+import { useState, useRef, useEffect } from "react";
+import { cn } from "@/lib/utils";
+import { formatTimeCode, parseTimeCode } from "@/lib/time";
+
+interface EditableTimecodeProps {
+ time: number;
+ duration?: number;
+ format?: "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF";
+ fps?: number;
+ onTimeChange?: (time: number) => void;
+ className?: string;
+ disabled?: boolean;
+}
+
+export function EditableTimecode({
+ time,
+ duration,
+ format = "HH:MM:SS:FF",
+ fps = 30,
+ onTimeChange,
+ className,
+ disabled = false,
+}: EditableTimecodeProps) {
+ const [isEditing, setIsEditing] = useState(false);
+ const [inputValue, setInputValue] = useState("");
+ const [hasError, setHasError] = useState(false);
+ const inputRef = useRef<HTMLInputElement>(null);
+ const enterPressedRef = useRef(false);
+
+ const formattedTime = formatTimeCode(time, format, fps);
+
+ const startEditing = () => {
+ if (disabled) return;
+ setIsEditing(true);
+ setInputValue(formattedTime);
+ setHasError(false);
+ enterPressedRef.current = false;
+ };
+
+ const cancelEditing = () => {
+ setIsEditing(false);
+ setInputValue("");
+ setHasError(false);
+ enterPressedRef.current = false;
+ };
+
+ const applyEdit = () => {
+ const parsedTime = parseTimeCode(inputValue, format, fps);
+
+ if (parsedTime === null) {
+ setHasError(true);
+ return;
+ }
+
+ // Clamp time to valid range
+ const clampedTime = Math.max(
+ 0,
+ duration ? Math.min(duration, parsedTime) : parsedTime
+ );
+
+ onTimeChange?.(clampedTime);
+ setIsEditing(false);
+ setInputValue("");
+ setHasError(false);
+ enterPressedRef.current = false;
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ enterPressedRef.current = true;
+ applyEdit();
+ } else if (e.key === "Escape") {
+ e.preventDefault();
+ cancelEditing();
+ }
+ };
+
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ setInputValue(e.target.value);
+ setHasError(false);
+ };
+
+ const handleBlur = () => {
+ // Only apply edit if Enter wasn't pressed (to avoid double processing)
+ if (!enterPressedRef.current && isEditing) {
+ applyEdit();
+ }
+ };
+
+ // Focus input when entering edit mode
+ useEffect(() => {
+ if (isEditing && inputRef.current) {
+ inputRef.current.focus();
+ inputRef.current.select();
+ }
+ }, [isEditing]);
+
+ if (isEditing) {
+ return (
+ <input
+ ref={inputRef}
+ type="text"
+ value={inputValue}
+ onChange={handleInputChange}
+ onKeyDown={handleKeyDown}
+ onBlur={handleBlur}
+ className={cn(
+ "text-xs font-mono bg-transparent border-none outline-none",
+ "focus:bg-background focus:border focus:border-primary focus:px-1 focus:rounded",
+ "tabular-nums text-primary",
+ hasError && "text-destructive focus:border-destructive",
+ className
+ )}
+ style={{ width: `${formattedTime.length + 1}ch` }}
+ placeholder={formattedTime}
+ />
+ );
+ }
+
+ return (
+ <span
+ onClick={startEditing}
+ className={cn(
+ "text-xs font-mono tabular-nums text-primary cursor-pointer",
+ "hover:bg-muted/50 hover:rounded px-1 -mx-1 transition-colors",
+ disabled && "cursor-default hover:bg-transparent",
+ className
+ )}
+ title={disabled ? undefined : "Click to edit time"}
+ >
+ {formattedTime}
+ </span>
+ );
+}