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
Binary file removed apps/web/public/landing-page-bg.png
Binary file not shown.
Binary file added apps/web/public/landing-page-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/landing-page-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
106 changes: 13 additions & 93 deletions apps/web/src/components/editor/media-panel/tabbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,119 +5,39 @@ import { Tab, tabs, useMediaPanelStore } from "./store";
import { Button } from "@/components/ui/button";
import { ChevronRight, ChevronLeft } from "lucide-react";
import { useRef, useState, useEffect } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";

export function TabBar() {
const { activeTab, setActiveTab } = useMediaPanelStore();
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [isAtEnd, setIsAtEnd] = useState(false);
const [isAtStart, setIsAtStart] = useState(true);

const scrollToEnd = () => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTo({
left: scrollContainerRef.current.scrollWidth,
});
setIsAtEnd(true);
setIsAtStart(false);
}
};

const scrollToStart = () => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTo({
left: 0,
});
setIsAtStart(true);
setIsAtEnd(false);
}
};

const checkScrollPosition = () => {
if (scrollContainerRef.current) {
const { scrollLeft, scrollWidth, clientWidth } =
scrollContainerRef.current;
const isAtEndNow = scrollLeft + clientWidth >= scrollWidth - 1;
const isAtStartNow = scrollLeft <= 1;
setIsAtEnd(isAtEndNow);
setIsAtStart(isAtStartNow);
}
};

// We're using useEffect because we need to sync with external DOM scroll events
useEffect(() => {
const container = scrollContainerRef.current;
if (!container) return;

checkScrollPosition();
container.addEventListener("scroll", checkScrollPosition);

const resizeObserver = new ResizeObserver(checkScrollPosition);
resizeObserver.observe(container);

return () => {
container.removeEventListener("scroll", checkScrollPosition);
resizeObserver.disconnect();
};
}, []);
export function TabBar() {
const { activeTab, setActiveTab } = useMediaPanelStore();

return (
<div className="flex">
<ScrollButton
direction="left"
onClick={scrollToStart}
isVisible={!isAtStart}
/>
<div
ref={scrollContainerRef}
className="h-full px-4 flex flex-col justify-start items-center gap-5 overflow-x-auto scrollbar-x-hidden relative w-full py-4"
>
<div className="h-full px-4 flex flex-col justify-start items-center gap-5 overflow-x-auto scrollbar-x-hidden relative w-full py-4">
{(Object.keys(tabs) as Tab[]).map((tabKey) => {
const tab = tabs[tabKey];
return (
<div
className={cn(
"flex flex-col gap-0.5 items-center cursor-pointer opacity-100 hover:opacity-75",
"flex z-[100] flex-col gap-0.5 items-center cursor-pointer",
activeTab === tabKey ? "text-primary !opacity-100" : "text-muted-foreground"
)}
onClick={() => setActiveTab(tabKey)}
key={tabKey}
>
<tab.icon className="size-[1.1rem]!" />
<Tooltip delayDuration={10}>
<TooltipTrigger asChild>
<tab.icon className="size-[1.1rem]! opacity-100 hover:opacity-75" />
</TooltipTrigger>
<TooltipContent side="right" align="center" variant="sidebar" sideOffset={8}>
<div className="dark:text-base-gray-950 text-black text-sm font-medium leading-none dark:text-white">{tab.label}</div>
</TooltipContent>
</Tooltip>
</div>
);
})}
</div>
<ScrollButton
direction="right"
onClick={scrollToEnd}
isVisible={!isAtEnd}
/>
</div>
);
}

function ScrollButton({
direction,
onClick,
isVisible,
}: {
direction: "left" | "right";
onClick: () => void;
isVisible: boolean;
}) {
if (!isVisible) return null;

const Icon = direction === "left" ? ChevronLeft : ChevronRight;

return (
<div className="bg-panel-accent w-12 h-full flex items-center justify-center">
<Button
size="icon"
className="rounded-[0.4rem] w-4 h-7 bg-foreground/10!"
onClick={onClick}
>
<Icon className="size-4! text-foreground" />
</Button>
</div>
);
}
117 changes: 102 additions & 15 deletions apps/web/src/components/editor/timeline/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,24 +167,71 @@ export function Timeline() {
const handleTimelineMouseDown = useCallback((e: React.MouseEvent) => {
// Only track mouse down on timeline background areas (not elements)
const target = e.target as HTMLElement;
console.log(
JSON.stringify({
debug_mousedown: "START",
target_class: target.className,
target_parent_class: target.parentElement?.className,
clientX: e.clientX,
clientY: e.clientY,
timeStamp: e.timeStamp,
})
);

const isTimelineBackground =
!target.closest(".timeline-element") &&
!playheadRef.current?.contains(target) &&
!target.closest("[data-track-labels]");

console.log(
JSON.stringify({
debug_mousedown: "CHECK",
isTimelineBackground,
hasTimelineElement: !!target.closest(".timeline-element"),
hasPlayhead: !!playheadRef.current?.contains(target),
hasTrackLabels: !!target.closest("[data-track-labels]"),
})
);

if (isTimelineBackground) {
mouseTrackingRef.current = {
isMouseDown: true,
downX: e.clientX,
downY: e.clientY,
downTime: e.timeStamp,
};
console.log(
JSON.stringify({
debug_mousedown: "TRACKED",
mouseTracking: mouseTrackingRef.current,
})
);
} else {
console.log(
JSON.stringify({
debug_mousedown: "IGNORED - not timeline background",
})
);
}
}, []);

// Timeline content click to seek handler
const handleTimelineContentClick = useCallback(
(e: React.MouseEvent) => {
console.log(
JSON.stringify({
debug_click: "START",
target: (e.target as HTMLElement).className,
target_parent: (e.target as HTMLElement).parentElement?.className,
mouseTracking: mouseTrackingRef.current,
isSelecting,
justFinishedSelecting,
clickX: e.clientX,
clickY: e.clientY,
timeStamp: e.timeStamp,
})
);

const { isMouseDown, downX, downY, downTime } = mouseTrackingRef.current;

// Reset mouse tracking
Expand All @@ -199,8 +246,8 @@ export function Timeline() {
if (!isMouseDown) {
console.log(
JSON.stringify({
ignoredClickWithoutMouseDown: true,
timeStamp: e.timeStamp,
debug_click: "REJECTED - no mousedown",
mouseTracking: mouseTrackingRef.current,
})
);
return;
Expand All @@ -214,64 +261,105 @@ export function Timeline() {
if (deltaX > 5 || deltaY > 5 || deltaTime > 500) {
console.log(
JSON.stringify({
ignoredDragNotClick: true,
debug_click: "REJECTED - movement too large",
deltaX,
deltaY,
deltaTime,
timeStamp: e.timeStamp,
})
);
return;
}

// Don't seek if this was a selection box operation
if (isSelecting || justFinishedSelecting) {
console.log(
JSON.stringify({
debug_click: "REJECTED - selection operation",
isSelecting,
justFinishedSelecting,
})
);
return;
}

// Don't seek if clicking on timeline elements, but still deselect
if ((e.target as HTMLElement).closest(".timeline-element")) {
console.log(
JSON.stringify({
debug_click: "REJECTED - clicked timeline element",
})
);
return;
}

// Don't seek if clicking on playhead
if (playheadRef.current?.contains(e.target as Node)) {
console.log(
JSON.stringify({
debug_click: "REJECTED - clicked playhead",
})
);
return;
}

// Don't seek if clicking on track labels
if ((e.target as HTMLElement).closest("[data-track-labels]")) {
console.log(
JSON.stringify({
debug_click: "REJECTED - clicked track labels",
})
);
clearSelectedElements();
return;
}

// Clear selected elements when clicking empty timeline area
console.log(JSON.stringify({ clearingSelectedElements: true }));
console.log(
JSON.stringify({
debug_click: "PROCEEDING - clearing elements",
clearingSelectedElements: true,
})
);
clearSelectedElements();

// Determine if we're clicking in ruler or tracks area
const isRulerClick = (e.target as HTMLElement).closest(
"[data-ruler-area]"
);

console.log(
JSON.stringify({
debug_click: "CALCULATING POSITION",
isRulerClick,
clientX: e.clientX,
clientY: e.clientY,
target_element: (e.target as HTMLElement).tagName,
target_class: (e.target as HTMLElement).className,
})
);

let mouseX: number;
let scrollLeft = 0;

if (isRulerClick) {
// Calculate based on ruler position
const rulerContent = rulerScrollRef.current?.querySelector(
"[data-radix-scroll-area-viewport]"
) as HTMLElement;
if (!rulerContent) return;
const rulerContent = rulerScrollRef.current;
if (!rulerContent) {
console.log(
JSON.stringify({
debug_click: "ERROR - no ruler container found",
})
);
return;
}
const rect = rulerContent.getBoundingClientRect();
mouseX = e.clientX - rect.left;
scrollLeft = rulerContent.scrollLeft;
} else {
// Calculate based on tracks content position
const tracksContent = tracksScrollRef.current?.querySelector(
"[data-radix-scroll-area-viewport]"
) as HTMLElement;
if (!tracksContent) return;
const tracksContent = tracksScrollRef.current;
if (!tracksContent) {
return;
}
const rect = tracksContent.getBoundingClientRect();
mouseX = e.clientX - rect.left;
scrollLeft = tracksContent.scrollLeft;
Expand All @@ -289,7 +377,6 @@ export function Timeline() {
// Use frame snapping for timeline clicking
const projectFps = activeProject?.fps || 30;
const time = snapTimeToFrame(rawTime, projectFps);

seek(time);
},
[
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Image from "next/image";
export function Header() {
const leftContent = (
<Link href="/" className="flex items-center gap-3">
<Image src="/logo.svg" alt="OpenCut Logo" width={32} height={32} />
<Image src="/logo.svg" alt="OpenCut Logo" className="invert dark:invert-0" width={32} height={32} />
<span className="text-xl font-medium hidden md:block">OpenCut</span>
</Link>
);
Expand Down Expand Up @@ -40,7 +40,7 @@ export function Header() {
return (
<div className="mx-4 md:mx-0">
<HeaderBase
className="bg-accent border rounded-2xl max-w-3xl mx-auto mt-4 pl-4 pr-[14px]"
className="bg-background border rounded-2xl max-w-3xl mx-auto mt-4 pl-4 pr-[14px]"
leftContent={leftContent}
rightContent={rightContent}
/>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/landing/handlebars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function Handlebars({ children }: HandlebarsProps) {
<div ref={containerRef} className="relative -rotate-[2.76deg] mt-0.5">
<div className="absolute inset-0 w-full h-full rounded-2xl border border-yellow-500 flex justify-between z-1">
<motion.div
className="absolute z-10 left-0 h-full border border-yellow-500 w-7 rounded-full bg-accent flex items-center justify-center select-none"
className="absolute z-10 left-0 h-full border border-yellow-500 w-7 rounded-full bg-background flex items-center justify-center select-none"
style={{
x: leftHandleX,
}}
Expand All @@ -66,7 +66,7 @@ export function Handlebars({ children }: HandlebarsProps) {
</motion.div>

<motion.div
className="absolute z-10 -left-[30px] h-full border border-yellow-500 w-7 rounded-full bg-accent flex items-center justify-center select-none"
className="absolute z-10 -left-[30px] h-full border border-yellow-500 w-7 rounded-full bg-background flex items-center justify-center select-none"
style={{
x: rightHandleX,
}}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/landing/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function Hero() {
return (
<div className="min-h-[calc(100vh-4.5rem)] supports-[height:100dvh]:min-h-[calc(100dvh-4.5rem)] flex flex-col justify-between items-center text-center px-4">
<Image
className="absolute top-0 left-0 -z-50 size-full object-cover"
className="absolute top-0 left-0 -z-50 size-full object-cover invert dark:invert-0 opacity-85"
src="/landing-page-bg.png"
height={1903.5}
width={1269}
Expand Down
Loading
Loading