summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/components/editor')
-rw-r--r--apps/web/src/components/editor/media-panel/index.tsx5
-rw-r--r--apps/web/src/components/editor/media-panel/views/audio.tsx18
-rw-r--r--apps/web/src/components/editor/media-panel/views/media.tsx8
-rw-r--r--apps/web/src/components/editor/preview-panel.tsx514
-rw-r--r--apps/web/src/components/editor/timeline/index.tsx (renamed from apps/web/src/components/editor/timeline.tsx)15
-rw-r--r--apps/web/src/components/editor/timeline/timeline-element.tsx (renamed from apps/web/src/components/editor/timeline-element.tsx)8
-rw-r--r--apps/web/src/components/editor/timeline/timeline-playhead.tsx (renamed from apps/web/src/components/editor/timeline-playhead.tsx)0
-rw-r--r--apps/web/src/components/editor/timeline/timeline-track.tsx (renamed from apps/web/src/components/editor/timeline-track.tsx)0
8 files changed, 290 insertions, 278 deletions
diff --git a/apps/web/src/components/editor/media-panel/index.tsx b/apps/web/src/components/editor/media-panel/index.tsx
index 9f7322c7..90ac84f0 100644
--- a/apps/web/src/components/editor/media-panel/index.tsx
+++ b/apps/web/src/components/editor/media-panel/index.tsx
@@ -4,15 +4,14 @@ import { TabBar } from "./tabbar";
import { MediaView } from "./views/media";
import { useMediaPanelStore, Tab } from "./store";
import { TextView } from "./views/text";
+import { AudioView } from "./views/audio";
export function MediaPanel() {
const { activeTab } = useMediaPanelStore();
const viewMap: Record<Tab, React.ReactNode> = {
media: <MediaView />,
- audio: (
- <div className="p-4 text-muted-foreground">Audio view coming soon...</div>
- ),
+ audio: <AudioView />,
text: <TextView />,
stickers: (
<div className="p-4 text-muted-foreground">
diff --git a/apps/web/src/components/editor/media-panel/views/audio.tsx b/apps/web/src/components/editor/media-panel/views/audio.tsx
new file mode 100644
index 00000000..7ddf465c
--- /dev/null
+++ b/apps/web/src/components/editor/media-panel/views/audio.tsx
@@ -0,0 +1,18 @@
+"use client";
+
+import { Input } from "@/components/ui/input";
+import { useState } from "react";
+
+export function AudioView() {
+ const [search, setSearch] = useState("");
+ return (
+ <div className="h-full flex flex-col gap-2 p-4">
+ <Input
+ placeholder="Search songs and artists"
+ value={search}
+ onChange={(e) => setSearch(e.target.value)}
+ />
+ <div className="flex flex-col gap-2"></div>
+ </div>
+ );
+}
diff --git a/apps/web/src/components/editor/media-panel/views/media.tsx b/apps/web/src/components/editor/media-panel/views/media.tsx
index 631fe39a..0189720f 100644
--- a/apps/web/src/components/editor/media-panel/views/media.tsx
+++ b/apps/web/src/components/editor/media-panel/views/media.tsx
@@ -208,10 +208,10 @@ export function MediaView() {
{/* Search and filter controls */}
<div className="flex gap-2">
<Select value={mediaFilter} onValueChange={setMediaFilter}>
- <SelectTrigger className="w-[80px] h-full text-xs">
+ <SelectTrigger className="w-[80px] h-9 text-xs">
<SelectValue />
</SelectTrigger>
- <SelectContent className="">
+ <SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="video">Video</SelectItem>
<SelectItem value="audio">Audio</SelectItem>
@@ -221,7 +221,7 @@ export function MediaView() {
<Input
type="text"
placeholder="Search media..."
- className="min-w-[60px] flex-1 h-full text-xs"
+ className="min-w-[60px] flex-1 h-9 text-xs"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
@@ -230,7 +230,7 @@ export function MediaView() {
size="lg"
onClick={handleFileSelect}
disabled={isProcessing}
- className="flex-none bg-transparent min-w-[30px] whitespace-nowrap overflow-hidden px-2 justify-center items-center"
+ className="flex-none bg-transparent min-w-[30px] whitespace-nowrap overflow-hidden px-2 justify-center items-center h-9"
>
{isProcessing ? (
<Loader2 className="h-4 w-4 animate-spin" />
diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx
index af603e97..2cba1374 100644
--- a/apps/web/src/components/editor/preview-panel.tsx
+++ b/apps/web/src/components/editor/preview-panel.tsx
@@ -44,40 +44,30 @@ export function PreviewPanel() {
const [isExpanded, setIsExpanded] = useState(false);
const { activeProject } = useProjectStore();
- // Calculate optimal preview size that fits in container while maintaining aspect ratio
useEffect(() => {
const updatePreviewSize = () => {
if (!containerRef.current) return;
- let container;
let availableWidth, availableHeight;
if (isExpanded) {
- // Use full viewport when expanded, accounting for controls and margin
- const controlsHeight = 80; // Approximate height of controls area
- const marginSpace = 24; // m-3 = 12px on each side = 24px total
+ const controlsHeight = 80;
+ const marginSpace = 24;
availableWidth = window.innerWidth - marginSpace;
availableHeight = window.innerHeight - controlsHeight - marginSpace;
} else {
- container = containerRef.current.getBoundingClientRect();
+ const container = containerRef.current.getBoundingClientRect();
const computedStyle = getComputedStyle(containerRef.current);
-
- // Get padding values
const paddingTop = parseFloat(computedStyle.paddingTop);
const paddingBottom = parseFloat(computedStyle.paddingBottom);
const paddingLeft = parseFloat(computedStyle.paddingLeft);
const paddingRight = parseFloat(computedStyle.paddingRight);
-
- // Get gap value (gap-4 = 1rem = 16px)
const gap = parseFloat(computedStyle.gap) || 16;
-
- // Get toolbar height if it exists
const toolbar = containerRef.current.querySelector("[data-toolbar]");
const toolbarHeight = toolbar
? toolbar.getBoundingClientRect().height
: 0;
- // Calculate available space after accounting for padding, gap, and toolbar
availableWidth = container.width - paddingLeft - paddingRight;
availableHeight =
container.height -
@@ -89,16 +79,13 @@ export function PreviewPanel() {
const targetRatio = canvasSize.width / canvasSize.height;
const containerRatio = availableWidth / availableHeight;
-
let width, height;
if (containerRatio > targetRatio) {
- // Container is wider - constrain by height
- height = availableHeight * (isExpanded ? 0.95 : 1); // Use more space when expanded
+ height = availableHeight * (isExpanded ? 0.95 : 1);
width = height * targetRatio;
} else {
- // Container is taller - constrain by width
- width = availableWidth * (isExpanded ? 0.95 : 1); // Use more space when expanded
+ width = availableWidth * (isExpanded ? 0.95 : 1);
height = width / targetRatio;
}
@@ -106,13 +93,10 @@ export function PreviewPanel() {
};
updatePreviewSize();
-
const resizeObserver = new ResizeObserver(updatePreviewSize);
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
-
- // Listen for window resize when expanded
if (isExpanded) {
window.addEventListener("resize", updatePreviewSize);
}
@@ -125,7 +109,6 @@ export function PreviewPanel() {
};
}, [canvasSize.width, canvasSize.height, isExpanded]);
- // Handle escape key to exit expanded mode
useEffect(() => {
const handleEscapeKey = (event: KeyboardEvent) => {
if (event.key === "Escape" && isExpanded) {
@@ -135,7 +118,6 @@ export function PreviewPanel() {
if (isExpanded) {
document.addEventListener("keydown", handleEscapeKey);
- // Prevent body scroll when expanded
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
@@ -151,73 +133,7 @@ export function PreviewPanel() {
setIsExpanded((prev) => !prev);
}, []);
- // Check if there are any elements in the timeline at all
const hasAnyElements = tracks.some((track) => track.elements.length > 0);
-
- // Handle keyboard shortcuts for timeline navigation
- useEffect(() => {
- const handleKeyDown = (event: KeyboardEvent) => {
- // Only handle shortcuts when preview panel is focused or no input is focused
- const activeElement = document.activeElement;
- const isInputFocused =
- activeElement?.tagName === "INPUT" ||
- activeElement?.tagName === "TEXTAREA" ||
- activeElement?.contentEditable === "true";
-
- if (isInputFocused) return;
-
- switch (event.key) {
- case " ":
- event.preventDefault();
- if (hasAnyElements) {
- toggle();
- }
- break;
- case "ArrowLeft":
- event.preventDefault();
- if (hasAnyElements) {
- const frameTime = 1 / (activeProject?.fps || 30);
- const totalDuration = getTotalDuration();
- const newTime = Math.max(
- 0,
- currentTime - (event.shiftKey ? frameTime : 1),
- );
- setCurrentTime(newTime);
- }
- break;
- case "ArrowRight":
- event.preventDefault();
- if (hasAnyElements) {
- const frameTime = 1 / (activeProject?.fps || 30);
- const totalDuration = getTotalDuration();
- const newTime = Math.min(
- totalDuration,
- currentTime + (event.shiftKey ? frameTime : 1),
- );
- setCurrentTime(newTime);
- }
- break;
- case "Home":
- event.preventDefault();
- if (hasAnyElements) {
- setCurrentTime(0);
- }
- break;
- case "End":
- event.preventDefault();
- if (hasAnyElements) {
- const totalDuration = getTotalDuration();
- setCurrentTime(totalDuration);
- }
- break;
- }
- };
-
- document.addEventListener("keydown", handleKeyDown);
- return () => document.removeEventListener("keydown", handleKeyDown);
- }, [hasAnyElements, toggle, currentTime, setCurrentTime, activeProject?.fps]);
-
- // Get active elements at current time
const getActiveElements = (): ActiveElement[] => {
const activeElements: ActiveElement[] = [];
@@ -230,16 +146,13 @@ export function PreviewPanel() {
if (currentTime >= elementStart && currentTime < elementEnd) {
let mediaItem = null;
-
- // Only get media item for media elements
if (element.type === "media") {
mediaItem =
element.mediaId === "test"
- ? null // Test elements don't have a real media item
+ ? null
: mediaItems.find((item) => item.id === element.mediaId) ||
null;
}
-
activeElements.push({ element, track, mediaItem });
}
});
@@ -257,7 +170,7 @@ export function PreviewPanel() {
element.type === "media" &&
mediaItem &&
(mediaItem.type === "video" || mediaItem.type === "image") &&
- element.mediaId !== "test", // Exclude test elements
+ element.mediaId !== "test" // Exclude test elements
);
};
@@ -476,10 +389,9 @@ export function PreviewPanel() {
</div>
) : (
activeElements.map((elementData, index) =>
- renderElement(elementData, index),
+ renderElement(elementData, index)
)
)}
- {/* Show message when blur is selected but no media available */}
{activeProject?.backgroundType === "blur" &&
blurBackgroundElements.length === 0 &&
activeElements.length > 0 && (
@@ -504,62 +416,29 @@ export function PreviewPanel() {
</div>
</div>
- {/* Expanded/Fullscreen Mode */}
{isExpanded && (
- <div className="fixed inset-0 z-[9999] flex flex-col">
- <div className="flex-1 flex items-center justify-center bg-background">
- <div
- className="relative overflow-hidden border border-border m-3"
- style={{
- width: previewDimensions.width,
- height: previewDimensions.height,
- backgroundColor:
- activeProject?.backgroundType === "blur"
- ? "#1a1a1a"
- : activeProject?.backgroundColor || "#1a1a1a",
- }}
- >
- {renderBlurBackground()}
- {activeElements.length === 0 ? (
- <div className="absolute inset-0 flex items-center justify-center text-white/60">
- No elements at current time
- </div>
- ) : (
- activeElements.map((elementData, index) =>
- renderElement(elementData, index),
- )
- )}
- {/* Show message when blur is selected but no media available */}
- {activeProject?.backgroundType === "blur" &&
- blurBackgroundElements.length === 0 &&
- activeElements.length > 0 && (
- <div className="absolute bottom-2 left-2 right-2 bg-black/70 text-white text-xs p-2 rounded">
- Add a video or image to use blur background
- </div>
- )}
- </div>
- </div>
- <div className="p-4 bg-black">
- <PreviewToolbar
- hasAnyElements={hasAnyElements}
- onToggleExpanded={toggleExpanded}
- isExpanded={isExpanded}
- currentTime={currentTime}
- setCurrentTime={setCurrentTime}
- toggle={toggle}
- getTotalDuration={getTotalDuration}
- />
- </div>
- </div>
+ <FullscreenPreview
+ previewDimensions={previewDimensions}
+ activeProject={activeProject}
+ renderBlurBackground={renderBlurBackground}
+ activeElements={activeElements}
+ renderElement={renderElement}
+ blurBackgroundElements={blurBackgroundElements}
+ hasAnyElements={hasAnyElements}
+ toggleExpanded={toggleExpanded}
+ currentTime={currentTime}
+ setCurrentTime={setCurrentTime}
+ toggle={toggle}
+ getTotalDuration={getTotalDuration}
+ />
)}
</>
);
}
-function PreviewToolbar({
+function FullscreenToolbar({
hasAnyElements,
onToggleExpanded,
- isExpanded,
currentTime,
setCurrentTime,
toggle,
@@ -567,38 +446,20 @@ function PreviewToolbar({
}: {
hasAnyElements: boolean;
onToggleExpanded: () => void;
- isExpanded: boolean;
currentTime: number;
setCurrentTime: (time: number) => void;
toggle: () => void;
getTotalDuration: () => number;
}) {
const { isPlaying } = usePlaybackStore();
- const { setCanvasSize, setCanvasSizeToOriginal } = useEditorStore();
const { activeProject } = useProjectStore();
- const {
- currentPreset,
- isOriginal,
- getOriginalAspectRatio,
- getDisplayName,
- canvasPresets,
- } = useAspectRatio();
-
- const handlePresetSelect = (preset: { width: number; height: number }) => {
- setCanvasSize({ width: preset.width, height: preset.height });
- };
-
- const handleOriginalSelect = () => {
- const aspectRatio = getOriginalAspectRatio();
- setCanvasSizeToOriginal(aspectRatio);
- };
+ const [isDragging, setIsDragging] = useState(false);
const totalDuration = getTotalDuration();
const progress = totalDuration > 0 ? (currentTime / totalDuration) * 100 : 0;
const handleTimelineClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (!hasAnyElements) return;
-
const rect = e.currentTarget.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const percentage = Math.max(0, Math.min(1, clickX / rect.width));
@@ -608,16 +469,12 @@ function PreviewToolbar({
const handleTimelineDrag = (e: React.MouseEvent<HTMLDivElement>) => {
if (!hasAnyElements) return;
-
e.preventDefault();
e.stopPropagation();
-
const rect = e.currentTarget.getBoundingClientRect();
- let isDragging = true;
+ setIsDragging(true);
const handleMouseMove = (moveEvent: MouseEvent) => {
- if (!isDragging) return;
-
moveEvent.preventDefault();
const dragX = moveEvent.clientX - rect.left;
const percentage = Math.max(0, Math.min(1, dragX / rect.width));
@@ -625,48 +482,35 @@ function PreviewToolbar({
setCurrentTime(Math.max(0, Math.min(newTime, totalDuration)));
};
- const handleMouseUp = (moveEvent: MouseEvent) => {
- isDragging = false;
+ const handleMouseUp = () => {
+ setIsDragging(false);
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
document.body.style.userSelect = "";
};
- // Prevent text selection during drag
document.body.style.userSelect = "none";
-
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
-
- // Also handle the initial click position
handleMouseMove(e.nativeEvent);
};
const skipBackward = () => {
- const newTime = Math.max(0, currentTime - 1); // Skip 1 second back
+ const newTime = Math.max(0, currentTime - 1);
setCurrentTime(newTime);
};
const skipForward = () => {
- const newTime = Math.min(totalDuration, currentTime + 1); // Skip 1 second forward
+ const newTime = Math.min(totalDuration, currentTime + 1);
setCurrentTime(newTime);
};
return (
<div
data-toolbar
- className={cn(
- "flex items-center gap-2 p-1 pt-2 w-full",
- isExpanded && "text-white",
- )}
+ className="flex items-center gap-2 p-1 pt-2 w-full text-white"
>
- {/* Time Display */}
- <div
- className={cn(
- "flex items-center gap-1 text-[0.70rem] tabular-nums",
- isExpanded ? "text-white/90" : "text-muted-foreground",
- )}
- >
+ <div className="flex items-center gap-1 text-[0.70rem] tabular-nums text-white/90">
<span className="text-primary">
{formatTimeCode(currentTime, "HH:MM:SS:FF", activeProject?.fps || 30)}
</span>
@@ -675,22 +519,18 @@ function PreviewToolbar({
{formatTimeCode(
totalDuration,
"HH:MM:SS:FF",
- activeProject?.fps || 30,
+ activeProject?.fps || 30
)}
</span>
</div>
- {/* Transport Controls */}
<div className="flex items-center gap-1">
<Button
variant="text"
size="icon"
onClick={skipBackward}
disabled={!hasAnyElements}
- className={cn(
- "h-auto p-0",
- isExpanded && "text-white hover:text-white/80",
- )}
+ className="h-auto p-0 text-white hover:text-white/80"
title="Skip backward 1s"
>
<SkipBack className="h-3 w-3" />
@@ -700,10 +540,7 @@ function PreviewToolbar({
size="icon"
onClick={toggle}
disabled={!hasAnyElements}
- className={cn(
- "h-auto p-0",
- isExpanded && "text-white hover:text-white/80",
- )}
+ className="h-auto p-0 text-white hover:text-white/80"
>
{isPlaying ? (
<Pause className="h-3 w-3" />
@@ -716,23 +553,18 @@ function PreviewToolbar({
size="icon"
onClick={skipForward}
disabled={!hasAnyElements}
- className={cn(
- "h-auto p-0",
- isExpanded && "text-white hover:text-white/80",
- )}
+ className="h-auto p-0 text-white hover:text-white/80"
title="Skip forward 1s"
>
<SkipForward className="h-3 w-3" />
</Button>
</div>
- {/* Timeline Scrubber */}
<div className="flex-1 flex items-center gap-2">
<div
className={cn(
- "relative h-1 rounded-full cursor-pointer flex-1",
- isExpanded ? "bg-white/20" : "bg-muted",
- !hasAnyElements && "opacity-50 cursor-not-allowed",
+ "relative h-1 rounded-full cursor-pointer flex-1 bg-white/20",
+ !hasAnyElements && "opacity-50 cursor-not-allowed"
)}
onClick={hasAnyElements ? handleTimelineClick : undefined}
onMouseDown={hasAnyElements ? handleTimelineDrag : undefined}
@@ -740,73 +572,239 @@ function PreviewToolbar({
>
<div
className={cn(
- "absolute top-0 left-0 h-full rounded-full transition-all duration-100",
- isExpanded ? "bg-white" : "bg-primary",
+ "absolute top-0 left-0 h-full rounded-full bg-white",
+ !isDragging && "duration-100"
)}
style={{ width: `${progress}%` }}
/>
<div
- className={cn(
- "absolute top-1/2 w-3 h-3 rounded-full -translate-y-1/2 -translate-x-1/2 shadow-sm",
- isExpanded
- ? "bg-white border border-black/20"
- : "bg-primary border border-background",
- )}
+ className="absolute top-1/2 w-3 h-3 rounded-full -translate-y-1/2 -translate-x-1/2 shadow-sm bg-white border border-black/20"
style={{ left: `${progress}%` }}
/>
</div>
</div>
- {/* Aspect Ratio and Fullscreen Controls */}
- <div className="flex items-center gap-3">
- {!isExpanded && (
- <>
- <BackgroundSettings />
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- size="sm"
- className="!bg-panel-accent text-foreground/85 text-[0.70rem] h-4 rounded-none border border-muted-foreground px-0.5 py-0 font-light"
- disabled={!hasAnyElements}
- >
- {getDisplayName()}
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end">
- <DropdownMenuItem
- onClick={handleOriginalSelect}
- className={cn("text-xs", isOriginal && "font-semibold")}
- >
- Original
- </DropdownMenuItem>
- <DropdownMenuSeparator />
- {canvasPresets.map((preset) => (
- <DropdownMenuItem
- key={preset.name}
- onClick={() => handlePresetSelect(preset)}
- className={cn(
- "text-xs",
- currentPreset?.name === preset.name && "font-semibold",
- )}
- >
- {preset.name}
- </DropdownMenuItem>
- ))}
- </DropdownMenuContent>
- </DropdownMenu>
- </>
+ <Button
+ variant="text"
+ size="icon"
+ className="!size-4 text-white/80 hover:text-white"
+ onClick={onToggleExpanded}
+ title="Exit fullscreen (Esc)"
+ >
+ <Expand className="!size-4" />
+ </Button>
+ </div>
+ );
+}
+
+function FullscreenPreview({
+ previewDimensions,
+ activeProject,
+ renderBlurBackground,
+ activeElements,
+ renderElement,
+ blurBackgroundElements,
+ hasAnyElements,
+ toggleExpanded,
+ currentTime,
+ setCurrentTime,
+ toggle,
+ getTotalDuration,
+}: {
+ previewDimensions: { width: number; height: number };
+ activeProject: any;
+ renderBlurBackground: () => React.ReactNode;
+ activeElements: ActiveElement[];
+ renderElement: (elementData: ActiveElement, index: number) => React.ReactNode;
+ blurBackgroundElements: ActiveElement[];
+ hasAnyElements: boolean;
+ toggleExpanded: () => void;
+ currentTime: number;
+ setCurrentTime: (time: number) => void;
+ toggle: () => void;
+ getTotalDuration: () => number;
+}) {
+ return (
+ <div className="fixed inset-0 z-[9999] flex flex-col">
+ <div className="flex-1 flex items-center justify-center bg-background">
+ <div
+ className="relative overflow-hidden border border-border m-3"
+ style={{
+ width: previewDimensions.width,
+ height: previewDimensions.height,
+ backgroundColor:
+ activeProject?.backgroundType === "blur"
+ ? "#1a1a1a"
+ : activeProject?.backgroundColor || "#1a1a1a",
+ }}
+ >
+ {renderBlurBackground()}
+ {activeElements.length === 0 ? (
+ <div className="absolute inset-0 flex items-center justify-center text-white/60">
+ No elements at current time
+ </div>
+ ) : (
+ activeElements.map((elementData, index) =>
+ renderElement(elementData, index)
+ )
+ )}
+ {activeProject?.backgroundType === "blur" &&
+ blurBackgroundElements.length === 0 &&
+ activeElements.length > 0 && (
+ <div className="absolute bottom-2 left-2 right-2 bg-black/70 text-white text-xs p-2 rounded">
+ Add a video or image to use blur background
+ </div>
+ )}
+ </div>
+ </div>
+ <div className="p-4 bg-black">
+ <FullscreenToolbar
+ hasAnyElements={hasAnyElements}
+ onToggleExpanded={toggleExpanded}
+ currentTime={currentTime}
+ setCurrentTime={setCurrentTime}
+ toggle={toggle}
+ getTotalDuration={getTotalDuration}
+ />
+ </div>
+ </div>
+ );
+}
+
+function PreviewToolbar({
+ hasAnyElements,
+ onToggleExpanded,
+ isExpanded,
+ currentTime,
+ setCurrentTime,
+ toggle,
+ getTotalDuration,
+}: {
+ hasAnyElements: boolean;
+ onToggleExpanded: () => void;
+ isExpanded: boolean;
+ currentTime: number;
+ setCurrentTime: (time: number) => void;
+ toggle: () => void;
+ getTotalDuration: () => number;
+}) {
+ const { isPlaying } = usePlaybackStore();
+ const { setCanvasSize, setCanvasSizeToOriginal } = useEditorStore();
+ const { activeProject } = useProjectStore();
+ const {
+ currentPreset,
+ isOriginal,
+ getOriginalAspectRatio,
+ getDisplayName,
+ canvasPresets,
+ } = useAspectRatio();
+
+ const handlePresetSelect = (preset: { width: number; height: number }) => {
+ setCanvasSize({ width: preset.width, height: preset.height });
+ };
+
+ const handleOriginalSelect = () => {
+ const aspectRatio = getOriginalAspectRatio();
+ setCanvasSizeToOriginal(aspectRatio);
+ };
+
+ if (isExpanded) {
+ return (
+ <FullscreenToolbar
+ {...{
+ hasAnyElements,
+ onToggleExpanded,
+ currentTime,
+ setCurrentTime,
+ toggle,
+ getTotalDuration,
+ }}
+ />
+ );
+ }
+
+ return (
+ <div
+ data-toolbar
+ className="flex items-end justify-between gap-2 p-1 pt-2 w-full"
+ >
+ <div>
+ <p
+ className={cn(
+ "text-[0.75rem] text-muted-foreground flex items-center gap-1",
+ !hasAnyElements && "opacity-50"
+ )}
+ >
+ <span className="text-primary tabular-nums">
+ {formatTimeCode(
+ currentTime,
+ "HH:MM:SS:FF",
+ activeProject?.fps || 30
+ )}
+ </span>
+ <span className="opacity-50">/</span>
+ <span className="tabular-nums">
+ {formatTimeCode(
+ getTotalDuration(),
+ "HH:MM:SS:FF",
+ activeProject?.fps || 30
+ )}
+ </span>
+ </p>
+ </div>
+ <Button
+ variant="text"
+ size="icon"
+ onClick={toggle}
+ disabled={!hasAnyElements}
+ className="h-auto p-0"
+ >
+ {isPlaying ? (
+ <Pause className="h-3 w-3" />
+ ) : (
+ <Play className="h-3 w-3" />
)}
+ </Button>
+ <div className="flex items-center gap-3">
+ <BackgroundSettings />
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ size="sm"
+ className="!bg-panel-accent text-foreground/85 text-[0.70rem] h-4 rounded-none border border-muted-foreground px-0.5 py-0 font-light"
+ disabled={!hasAnyElements}
+ >
+ {getDisplayName()}
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end">
+ <DropdownMenuItem
+ onClick={handleOriginalSelect}
+ className={cn("text-xs", isOriginal && "font-semibold")}
+ >
+ Original
+ </DropdownMenuItem>
+ <DropdownMenuSeparator />
+ {canvasPresets.map((preset) => (
+ <DropdownMenuItem
+ key={preset.name}
+ onClick={() => handlePresetSelect(preset)}
+ className={cn(
+ "text-xs",
+ currentPreset?.name === preset.name && "font-semibold"
+ )}
+ >
+ {preset.name}
+ </DropdownMenuItem>
+ ))}
+ </DropdownMenuContent>
+ </DropdownMenu>
<Button
variant="text"
size="icon"
- className={cn(
- "!size-4",
- isExpanded
- ? "text-white/80 hover:text-white"
- : "text-muted-foreground",
- )}
+ className="!size-4 text-muted-foreground"
onClick={onToggleExpanded}
- title={isExpanded ? "Exit fullscreen (Esc)" : "Enter fullscreen"}
+ title="Enter fullscreen"
>
<Expand className="!size-4" />
</Button>
diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline/index.tsx
index 9e68fd4d..f9a6cb8c 100644
--- a/apps/web/src/components/editor/timeline.tsx
+++ b/apps/web/src/components/editor/timeline/index.tsx
@@ -1,7 +1,7 @@
"use client";
-import { ScrollArea } from "../ui/scroll-area";
-import { Button } from "../ui/button";
+import { ScrollArea } from "../../ui/scroll-area";
+import { Button } from "../../ui/button";
import {
Scissors,
ArrowLeftToLine,
@@ -24,13 +24,13 @@ import {
TooltipContent,
TooltipTrigger,
TooltipProvider,
-} from "../ui/tooltip";
+} from "../../ui/tooltip";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
-} from "../ui/context-menu";
+} from "../../ui/context-menu";
import { useTimelineStore } from "@/stores/timeline-store";
import { useMediaStore } from "@/stores/media-store";
import { usePlaybackStore } from "@/stores/playback-store";
@@ -44,9 +44,9 @@ import {
TimelinePlayhead,
useTimelinePlayheadRuler,
} from "./timeline-playhead";
-import { SelectionBox } from "./selection-box";
+import { SelectionBox } from "../selection-box";
import { useSelectionBox } from "@/hooks/use-selection-box";
-import { SnapIndicator } from "./snap-indicator";
+import { SnapIndicator } from "../snap-indicator";
import { SnapPoint } from "@/hooks/use-timeline-snapping";
import type { DragData, TimelineTrack } from "@/types/timeline";
import {
@@ -433,7 +433,6 @@ export function Timeline() {
// Action handlers for toolbar
const handleSplitSelected = () => {
if (selectedElements.length === 0) {
- toast.error("No elements selected");
return;
}
let splitCount = 0;
@@ -459,7 +458,6 @@ export function Timeline() {
const handleDuplicateSelected = () => {
if (selectedElements.length === 0) {
- toast.error("No elements selected");
return;
}
const canDuplicate = selectedElements.length === 1;
@@ -553,7 +551,6 @@ export function Timeline() {
const handleDeleteSelected = () => {
if (selectedElements.length === 0) {
- toast.error("No elements selected");
return;
}
selectedElements.forEach(({ trackId, elementId }) => {
diff --git a/apps/web/src/components/editor/timeline-element.tsx b/apps/web/src/components/editor/timeline/timeline-element.tsx
index eb9f1c01..7cc72b10 100644
--- a/apps/web/src/components/editor/timeline-element.tsx
+++ b/apps/web/src/components/editor/timeline/timeline-element.tsx
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
-import { Button } from "../ui/button";
+import { Button } from "../../ui/button";
import {
MoreVertical,
Scissors,
@@ -17,7 +17,7 @@ import {
import { useMediaStore } from "@/stores/media-store";
import { useTimelineStore } from "@/stores/timeline-store";
import { usePlaybackStore } from "@/stores/playback-store";
-import AudioWaveform from "./audio-waveform";
+import AudioWaveform from "../audio-waveform";
import { toast } from "sonner";
import { TimelineElementProps, TrackType } from "@/types/timeline";
import { useTimelineElementResize } from "@/hooks/use-timeline-element-resize";
@@ -35,14 +35,14 @@ import {
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
-} from "../ui/dropdown-menu";
+} from "../../ui/dropdown-menu";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
-} from "../ui/context-menu";
+} from "../../ui/context-menu";
export function TimelineElement({
element,
diff --git a/apps/web/src/components/editor/timeline-playhead.tsx b/apps/web/src/components/editor/timeline/timeline-playhead.tsx
index 7a357067..7a357067 100644
--- a/apps/web/src/components/editor/timeline-playhead.tsx
+++ b/apps/web/src/components/editor/timeline/timeline-playhead.tsx
diff --git a/apps/web/src/components/editor/timeline-track.tsx b/apps/web/src/components/editor/timeline/timeline-track.tsx
index 82ca0b98..82ca0b98 100644
--- a/apps/web/src/components/editor/timeline-track.tsx
+++ b/apps/web/src/components/editor/timeline/timeline-track.tsx