feat: timeline changes

This commit is contained in:
Simon Orzel
2025-07-17 02:53:25 +02:00
parent 98b2103286
commit 1505c1b12f
7 changed files with 48 additions and 93 deletions
@@ -129,7 +129,6 @@ export function Timeline() {
useTimelineScrollSync({ useTimelineScrollSync({
rulerScrollRef, rulerScrollRef,
tracksScrollRef, tracksScrollRef,
trackLabelsScrollRef,
}); });
// Wheel event handling // Wheel event handling
@@ -1,11 +0,0 @@
export { Timeline } from "../timeline";
export { TimelineToolbar } from "./timeline-toolbar";
export { TimelineContent } from "./timeline-content";
export { TimelineRuler } from "./timeline-ruler";
export { TimelineTracksArea } from "./timeline-tracks-area";
export { TrackIcon } from "./track-icon";
export { useTimelineDragHandlers } from "./timeline-drag-handlers";
export { useTimelineActionHandlers } from "./timeline-action-handlers";
export { useTimelineScrollSync } from "./timeline-scroll-sync";
export { useTimelineContentClick } from "./timeline-content-click";
export { useTimelineWheelHandler } from "./timeline-wheel-handler";
@@ -59,27 +59,39 @@ export function TimelinePlayhead({
const leftPosition = playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; const leftPosition = playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
return ( return (
<div <>
ref={playheadRef} {/* Playhead line container */}
className="absolute pointer-events-auto z-[95]"
style={{
left: `${trackLabelsWidth + leftPosition}px`,
top: 0,
bottom: 0,
width: "2px", // Slightly wider for better click target
}}
onMouseDown={handlePlayheadMouseDown}
>
{/* The playhead line spanning full height */}
<div <div
className={`absolute left-0 w-0.5 cursor-col-resize h-full ${isSnappingToPlayhead ? "bg-primary" : "bg-foreground"}`} ref={playheadRef}
/> className="absolute pointer-events-auto z-[95]"
style={{
left: `${trackLabelsWidth + leftPosition}px`,
top: 0,
bottom: 0,
width: "2px", // Slightly wider for better click target
}}
onMouseDown={handlePlayheadMouseDown}
>
{/* The playhead line spanning full height */}
<div
className={`absolute left-0 w-0.5 cursor-col-resize h-full ${isSnappingToPlayhead ? "bg-primary" : "bg-foreground"}`}
/>
</div>
{/* Playhead dot indicator at the top (in ruler area) */} {/* Playhead dot indicator - separate container with highest z-index */}
<div <div
className={`sticky top-1 left-1 -translate-x-[40%] transform w-3 h-3 rounded-full shadow-sm ${isSnappingToPlayhead ? "bg-primary border-primary" : "bg-foreground border-foreground"}`} className="absolute pointer-events-none z-[100]"
/> style={{
</div> left: `${trackLabelsWidth + leftPosition}px`,
top: 0,
bottom: 0,
}}
>
<div
className={`sticky top-1 left-1 -translate-x-[40%] transform w-3 h-3 rounded-full shadow-sm ${isSnappingToPlayhead ? "bg-primary border-primary" : "bg-foreground border-foreground"}`}
/>
</div>
</>
); );
} }
@@ -83,7 +83,7 @@ export function TimelineRuler({
markers.push( markers.push(
<div <div
key={i} key={i}
className={`absolute top-0 bottom-0 z-[97] ${ className={`absolute top-0 bottom-0 z-[99] ${
isMainMarker isMainMarker
? "border-l border-muted-foreground/40" ? "border-l border-muted-foreground/40"
: "border-l border-muted-foreground/20" : "border-l border-muted-foreground/20"
@@ -91,7 +91,7 @@ export function TimelineRuler({
style={{ left: `${leftPosition}px` }} style={{ left: `${leftPosition}px` }}
> >
<span <span
className={`absolute top-1 left-1 text-[0.6rem] z-[97] ${ className={`absolute top-1 left-1 text-[0.6rem] z-[99] ${
isMainMarker isMainMarker
? "text-muted-foreground font-medium" ? "text-muted-foreground font-medium"
: "text-muted-foreground/70" : "text-muted-foreground/70"
@@ -108,14 +108,14 @@ export function TimelineRuler({
return ( return (
<div <div
className="sticky top-0 bg-card/[0.99] border-b border-muted/30 z-[97]" className="sticky top-0 bg-card/[0.99] border-b border-muted/30 z-[99]"
onMouseDown={handleSelectionMouseDown} onMouseDown={handleSelectionMouseDown}
onClick={handleTimelineContentClick} onClick={handleTimelineContentClick}
data-ruler-area data-ruler-area
> >
<div <div
ref={rulerRef} ref={rulerRef}
className="relative h-5 select-none cursor-default pb-1 z-[97]" className="relative h-5 select-none cursor-default pb-1 z-[99]"
onMouseDown={handleRulerMouseDown} onMouseDown={handleRulerMouseDown}
> >
{rulerMarkers} {rulerMarkers}
@@ -3,20 +3,16 @@ import { useEffect, useRef } from "react";
export interface TimelineScrollSyncProps { export interface TimelineScrollSyncProps {
rulerScrollRef: React.RefObject<HTMLDivElement>; rulerScrollRef: React.RefObject<HTMLDivElement>;
tracksScrollRef: React.RefObject<HTMLDivElement>; tracksScrollRef: React.RefObject<HTMLDivElement>;
trackLabelsScrollRef: React.RefObject<HTMLDivElement>;
} }
export function useTimelineScrollSync({ export function useTimelineScrollSync({
rulerScrollRef, rulerScrollRef,
tracksScrollRef, tracksScrollRef,
trackLabelsScrollRef,
}: TimelineScrollSyncProps) { }: TimelineScrollSyncProps) {
const isUpdatingRef = useRef(false); const isUpdatingRef = useRef(false);
const lastRulerSync = useRef(0); const lastSyncRef = useRef(0);
const lastTracksSync = useRef(0);
const lastVerticalSync = useRef(0);
// --- Scroll synchronization effect --- // --- Horizontal scroll synchronization between ruler and tracks ---
useEffect(() => { useEffect(() => {
const rulerViewport = rulerScrollRef.current?.querySelector( const rulerViewport = rulerScrollRef.current?.querySelector(
"[data-radix-scroll-area-viewport]" "[data-radix-scroll-area-viewport]"
@@ -24,76 +20,38 @@ export function useTimelineScrollSync({
const tracksViewport = tracksScrollRef.current?.querySelector( const tracksViewport = tracksScrollRef.current?.querySelector(
"[data-radix-scroll-area-viewport]" "[data-radix-scroll-area-viewport]"
) as HTMLElement; ) as HTMLElement;
const trackLabelsViewport = trackLabelsScrollRef.current?.querySelector(
"[data-radix-scroll-area-viewport]"
) as HTMLElement;
if (!rulerViewport || !tracksViewport) return; if (!rulerViewport || !tracksViewport) return;
// Horizontal scroll synchronization between ruler and tracks // Throttled scroll handlers for better performance
const handleRulerScroll = () => { const handleRulerScroll = () => {
const now = Date.now(); const now = Date.now();
if (isUpdatingRef.current || now - lastRulerSync.current < 16) return; if (isUpdatingRef.current || now - lastSyncRef.current < 16) return; // 60fps throttling
lastRulerSync.current = now; lastSyncRef.current = now;
isUpdatingRef.current = true; isUpdatingRef.current = true;
tracksViewport.scrollLeft = rulerViewport.scrollLeft; tracksViewport.scrollLeft = rulerViewport.scrollLeft;
isUpdatingRef.current = false; isUpdatingRef.current = false;
}; };
const handleTracksScroll = () => { const handleTracksScroll = () => {
const now = Date.now(); const now = Date.now();
if (isUpdatingRef.current || now - lastTracksSync.current < 16) return; if (isUpdatingRef.current || now - lastSyncRef.current < 16) return; // 60fps throttling
lastTracksSync.current = now; lastSyncRef.current = now;
isUpdatingRef.current = true; isUpdatingRef.current = true;
rulerViewport.scrollLeft = tracksViewport.scrollLeft; rulerViewport.scrollLeft = tracksViewport.scrollLeft;
isUpdatingRef.current = false; isUpdatingRef.current = false;
}; };
rulerViewport.addEventListener("scroll", handleRulerScroll); rulerViewport.addEventListener("scroll", handleRulerScroll, { passive: true });
tracksViewport.addEventListener("scroll", handleTracksScroll); tracksViewport.addEventListener("scroll", handleTracksScroll, { passive: true });
// Vertical scroll synchronization between track labels and tracks content
if (trackLabelsViewport) {
const handleTrackLabelsScroll = () => {
const now = Date.now();
if (isUpdatingRef.current || now - lastVerticalSync.current < 16)
return;
lastVerticalSync.current = now;
isUpdatingRef.current = true;
tracksViewport.scrollTop = trackLabelsViewport.scrollTop;
isUpdatingRef.current = false;
};
const handleTracksVerticalScroll = () => {
const now = Date.now();
if (isUpdatingRef.current || now - lastVerticalSync.current < 16)
return;
lastVerticalSync.current = now;
isUpdatingRef.current = true;
trackLabelsViewport.scrollTop = tracksViewport.scrollTop;
isUpdatingRef.current = false;
};
trackLabelsViewport.addEventListener("scroll", handleTrackLabelsScroll);
tracksViewport.addEventListener("scroll", handleTracksVerticalScroll);
return () => {
rulerViewport.removeEventListener("scroll", handleRulerScroll);
tracksViewport.removeEventListener("scroll", handleTracksScroll);
trackLabelsViewport.removeEventListener(
"scroll",
handleTrackLabelsScroll
);
tracksViewport.removeEventListener(
"scroll",
handleTracksVerticalScroll
);
};
}
return () => { return () => {
rulerViewport.removeEventListener("scroll", handleRulerScroll); rulerViewport.removeEventListener("scroll", handleRulerScroll);
tracksViewport.removeEventListener("scroll", handleTracksScroll); tracksViewport.removeEventListener("scroll", handleTracksScroll);
}; };
}, []); }, [rulerScrollRef, tracksScrollRef]);
return null; return null;
} }
@@ -35,7 +35,7 @@ export function TimelineTracksArea({
<Fragment key={track.id}> <Fragment key={track.id}>
{/* Left Column (Sticky Track Labels) */} {/* Left Column (Sticky Track Labels) */}
<div <div
className="sticky left-0 flex items-center border-b border-panel border-inset group bg-card/[0.99] z-[96]" className="sticky left-0 flex items-center border-b border-panel border-inset group bg-card/[0.99] z-[98]"
style={{ height: `${getTrackHeight(track.type)}px` }} style={{ height: `${getTrackHeight(track.type)}px` }}
> >
<div className="flex items-center gap-2 px-2"> <div className="flex items-center gap-2 px-2">
@@ -11,17 +11,14 @@ export function useTimelineWheelHandler({
isInTimeline, isInTimeline,
handleWheel, handleWheel,
}: TimelineWheelHandlerProps) { }: TimelineWheelHandlerProps) {
// Add wheel event listeners with passive: false to allow preventDefault
useEffect(() => { useEffect(() => {
const timelineContainer = timelineRef.current; const timelineContainer = timelineRef.current;
if (!timelineContainer || !isInTimeline) return; if (!timelineContainer || !isInTimeline) return;
const handleWheelCapture = (e: WheelEvent) => { const handleWheelCapture = (e: WheelEvent) => {
// Call the existing handleWheel function
handleWheel(e as any); handleWheel(e as any);
}; };
// Add wheel event listener with passive: false to allow preventDefault
timelineContainer.addEventListener("wheel", handleWheelCapture, { passive: false }); timelineContainer.addEventListener("wheel", handleWheelCapture, { passive: false });
return () => { return () => {