diff options
Diffstat (limited to 'apps/web/src/components/editor/panels')
3 files changed, 178 insertions, 213 deletions
diff --git a/apps/web/src/components/editor/panels/timeline/index.tsx b/apps/web/src/components/editor/panels/timeline/index.tsx index ae9e19c9..20e9faf1 100644 --- a/apps/web/src/components/editor/panels/timeline/index.tsx +++ b/apps/web/src/components/editor/panels/timeline/index.tsx @@ -61,7 +61,6 @@ import { SELECTED_TRACK_ROW_CLASS } from "./theme"; import { computeTrackExpansionHeight, getTrackExpandedRows, - getExpansionHeight, getPropertyLabel, type ExpandedRow, } from "./expanded-layout"; @@ -75,6 +74,7 @@ import { TimelineBookmarksRow } from "./bookmarks"; import { useBookmarkDrag } from "@/hooks/timeline/use-bookmark-drag"; import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll"; import { useInitialScrollBottom } from "@/hooks/timeline/use-initial-scroll-bottom"; +import { useTimelineResize } from "@/hooks/timeline/use-timeline-resize"; import { useTimelineStore } from "@/stores/timeline-store"; import { useEditor } from "@/hooks/use-editor"; import { useTimelinePlayhead } from "@/hooks/timeline/use-timeline-playhead"; @@ -119,7 +119,9 @@ export function Timeline() { } = useElementSelection(); const editor = useEditor(); const timeline = editor.timeline; - const scene = useEditor((currentEditor) => currentEditor.scenes.getActiveSceneOrNull()); + const scene = useEditor((currentEditor) => + currentEditor.scenes.getActiveSceneOrNull(), + ); const tracks = useMemo<TimelineTrack[]>( () => scene @@ -140,7 +142,6 @@ export function Timeline() { const playheadRef = useRef<HTMLDivElement>(null); const trackLabelsScrollRef = useRef<HTMLDivElement>(null); - const [isResizing, setIsResizing] = useState(false); const [currentSnapPoint, setCurrentSnapPoint] = useState<SnapPoint | null>( null, ); @@ -148,15 +149,6 @@ export function Timeline() { const handleSnapPointChange = useCallback((snapPoint: SnapPoint | null) => { setCurrentSnapPoint(snapPoint); }, []); - const handleResizeStateChange = useCallback( - ({ isResizing: nextIsResizing }: { isResizing: boolean }) => { - setIsResizing(nextIsResizing); - if (!nextIsResizing) { - setCurrentSnapPoint(null); - } - }, - [], - ); const timelineDuration = timeline.getTotalDuration() || 0; const minZoomLevel = getTimelineZoomMin({ @@ -176,6 +168,10 @@ export function Timeline() { tracksScrollRef, rulerScrollRef, }); + const { isResizing, handleResizeStart } = useTimelineResize({ + zoomLevel, + onSnapPointChange: handleSnapPointChange, + }); const expandedElementIds = useTimelineStore((s) => s.expandedElementIds); @@ -366,7 +362,10 @@ export function Timeline() { const containerWidth = tracksContainerRef.current?.clientWidth || FALLBACK_CONTAINER_WIDTH; - const contentWidth = timelineTimeToPixels({ time: timelineDuration, zoomLevel }); + const contentWidth = timelineTimeToPixels({ + time: timelineDuration, + zoomLevel, + }); const paddingPx = getTimelinePaddingPx({ containerWidth, zoomLevel, @@ -513,7 +512,10 @@ export function Timeline() { TRACKS_CONTAINER_HEIGHT.min, Math.min( TRACKS_CONTAINER_HEIGHT.max, - getTotalTracksHeight({ tracks, getExtraHeight: getTrackExpansionHeight }), + getTotalTracksHeight({ + tracks, + getExtraHeight: getTrackExpansionHeight, + }), ), ) + TIMELINE_CONTENT_TOP_PADDING_PX }px`, @@ -533,14 +535,13 @@ export function Timeline() { }} > {tracks.length > 0 && ( - <TimelineTrackRows - mainTrackId={mainTrackId} + <TimelineTrackRows + mainTrackId={mainTrackId} zoomLevel={zoomLevel} dragState={dragState} tracksScrollRef={tracksScrollRef} lastMouseXRef={lastMouseXRef} - onSnapPointChange={handleSnapPointChange} - onResizeStateChange={handleResizeStateChange} + onResizeStart={handleResizeStart} onElementMouseDown={handleElementMouseDown} onElementClick={handleElementClick} onTrackMouseDown={(event) => { @@ -718,8 +719,7 @@ function TimelineTrackRows({ dragState, tracksScrollRef, lastMouseXRef, - onSnapPointChange, - onResizeStateChange, + onResizeStart, onElementMouseDown, onElementClick, onTrackMouseDown, @@ -733,8 +733,9 @@ function TimelineTrackRows({ dragState: ElementDragState; tracksScrollRef: React.RefObject<HTMLDivElement | null>; lastMouseXRef: React.RefObject<number>; - onSnapPointChange: (snapPoint: SnapPoint | null) => void; - onResizeStateChange: (params: { isResizing: boolean }) => void; + onResizeStart: React.ComponentProps< + typeof TimelineTrackContent + >["onResizeStart"]; onElementMouseDown: React.ComponentProps< typeof TimelineTrackContent >["onElementMouseDown"]; @@ -798,8 +799,7 @@ function TimelineTrackRows({ <div className={cn( "absolute right-0 left-0 transition-colors", - tracksWithSelection.has(track.id) && - SELECTED_TRACK_ROW_CLASS, + tracksWithSelection.has(track.id) && SELECTED_TRACK_ROW_CLASS, )} style={{ top: `${TIMELINE_CONTENT_TOP_PADDING_PX + getCumulativeHeightBefore({ tracks, trackIndex: index, getExtraHeight: getTrackExpansionHeight })}px`, @@ -813,8 +813,7 @@ function TimelineTrackRows({ rulerScrollRef={tracksScrollRef} tracksScrollRef={tracksScrollRef} lastMouseXRef={lastMouseXRef} - onSnapPointChange={onSnapPointChange} - onResizeStateChange={onResizeStateChange} + onResizeStart={onResizeStart} onElementMouseDown={onElementMouseDown} onElementClick={onElementClick} onTrackMouseDown={onTrackMouseDown} @@ -929,12 +928,10 @@ function TrackToggleIcon({ function PropertyTree({ rows }: { rows: ExpandedRow[] }) { return ( <div className="flex flex-col overflow-hidden"> - {rows.map((row, index) => ( + {rows.map((row) => ( <div key={row.propertyPath} - className={cn( - "flex shrink-0 items-center px-3 bg-muted/50", - )} + className={cn("flex shrink-0 items-center px-3 bg-muted/50")} style={{ height: `${KEYFRAME_LANE_HEIGHT_PX}px` }} > <span className="text-muted-foreground truncate text-xs leading-none"> diff --git a/apps/web/src/components/editor/panels/timeline/timeline-element.tsx b/apps/web/src/components/editor/panels/timeline/timeline-element.tsx index a3555e94..3ebd3efa 100644 --- a/apps/web/src/components/editor/panels/timeline/timeline-element.tsx +++ b/apps/web/src/components/editor/panels/timeline/timeline-element.tsx @@ -10,7 +10,6 @@ import { } from "@/hooks/timeline/element/use-keyframe-drag"; import { useKeyframeSelection } from "@/hooks/timeline/element/use-keyframe-selection"; import { useKeyframeBoxSelect } from "@/hooks/timeline/element/use-keyframe-box-select"; -import { useTimelineElementResize } from "@/hooks/timeline/element/use-element-resize"; import { SelectionBox } from "@/lib/selection/selection-box"; import { getElementKeyframes } from "@/lib/animation"; import { @@ -86,7 +85,6 @@ import { getExpansionHeight, type ExpandedRow, } from "./expanded-layout"; -import type { SnapPoint } from "@/lib/timeline/snap-utils"; const KEYFRAME_INDICATOR_MIN_WIDTH_PX = 40; const ELEMENT_RING_WIDTH_PX = 1.5; @@ -197,8 +195,12 @@ interface TimelineElementProps { track: TimelineTrack; zoomLevel: number; isSelected: boolean; - onSnapPointChange?: (snapPoint: SnapPoint | null) => void; - onResizeStateChange?: (params: { isResizing: boolean }) => void; + onResizeStart: (params: { + event: React.MouseEvent; + element: TimelineElementType; + track: TimelineTrack; + side: "left" | "right"; + }) => void; onElementMouseDown: ( event: React.MouseEvent, element: TimelineElementType, @@ -216,8 +218,7 @@ export function TimelineElement({ track, zoomLevel, isSelected, - onSnapPointChange, - onResizeStateChange, + onResizeStart, onElementMouseDown, onElementClick, dragState, @@ -231,18 +232,6 @@ export function TimelineElement({ elementId: element.id, fallback: element, }); - const { - currentDuration, - currentStartTime, - handleResizeStart, - isResizing, - } = useTimelineElementResize({ - element, - track, - zoomLevel, - onSnapPointChange, - onResizeStateChange, - }); let mediaAsset: MediaAsset | null = null; @@ -267,13 +256,9 @@ export function TimelineElement({ const elementStartTime = isBeingDragged && dragState.isDragging ? dragState.currentTime + dragTimeOffset - : isResizing - ? currentStartTime - : renderElement.startTime; + : renderElement.startTime; const displayedStartTime = elementStartTime; - const displayedDuration = isResizing - ? currentDuration - : renderElement.duration; + const displayedDuration = renderElement.duration; const elementWidth = timelineTimeToPixels({ time: displayedDuration, zoomLevel, @@ -282,22 +267,6 @@ export function TimelineElement({ time: displayedStartTime, zoomLevel, }); - const handleElementResizeStart = ({ - event, - element, - side, - }: { - event: React.MouseEvent; - element: TimelineElementType; - track: TimelineTrack; - side: "left" | "right"; - }) => { - handleResizeStart({ - event, - elementId: element.id, - side, - }); - }; const keyframeIndicators = isSelected ? getKeyframeIndicators({ keyframes: getElementKeyframes({ animations: element.animations }), @@ -329,9 +298,7 @@ export function TimelineElement({ ); const expandedRows = useMemo( () => - isExpanded - ? getExpandedRows({ animations: element.animations }) - : [], + isExpanded ? getExpandedRows({ animations: element.animations }) : [], [isExpanded, element.animations], ); @@ -422,7 +389,7 @@ export function TimelineElement({ expandedContent={expandedContent} onElementClick={onElementClick} onElementMouseDown={onElementMouseDown} - onResizeStart={handleElementResizeStart} + onResizeStart={onResizeStart} isDropTarget={isDropTarget} /> {isSelected && ( @@ -811,8 +778,7 @@ function ExpandedKeyframeLanes({ [...keyframes] .sort( (a, b) => - a.time - b.time || - a.propertyPath.localeCompare(b.propertyPath), + a.time - b.time || a.propertyPath.localeCompare(b.propertyPath), ) .map((kf) => ({ trackId, @@ -824,6 +790,8 @@ function ExpandedKeyframeLanes({ ); return ( + // biome-ignore lint/a11y/noStaticElementInteractions: expanded keyframe lanes are a pointer-only editing surface + // biome-ignore lint/a11y/useKeyWithClickEvents: expanded keyframe lanes are a pointer-only editing surface <div ref={containerRef} className="relative flex flex-col" @@ -837,9 +805,7 @@ function ExpandedKeyframeLanes({ return ( <div key={row.propertyPath} - className={cn( - "relative flex items-center bg-muted/50", - )} + className={cn("relative flex items-center bg-muted/50")} style={{ height: `${KEYFRAME_LANE_HEIGHT_PX}px` }} > {laneKeyframes.map((kf) => { @@ -849,8 +815,9 @@ function ExpandedKeyframeLanes({ propertyPath: row.propertyPath, keyframeId: kf.id, }; - const isBeingDragged = - keyframeDragState.draggingKeyframeIds.has(kf.id); + const isBeingDragged = keyframeDragState.draggingKeyframeIds.has( + kf.id, + ); const kfLeft = timelineTimeToSnappedPixels({ time: displayedStartTime + kf.time, zoomLevel, @@ -898,9 +865,7 @@ function ExpandedKeyframeLanes({ icon={KeyframeIcon} className={cn( "size-3.5 text-black mr-1", - isSelected - ? "fill-primary" - : "fill-white", + isSelected ? "fill-primary" : "fill-white", )} strokeWidth={1.5} /> diff --git a/apps/web/src/components/editor/panels/timeline/timeline-track.tsx b/apps/web/src/components/editor/panels/timeline/timeline-track.tsx index 888e1496..32b1b93d 100644 --- a/apps/web/src/components/editor/panels/timeline/timeline-track.tsx +++ b/apps/web/src/components/editor/panels/timeline/timeline-track.tsx @@ -1,129 +1,132 @@ -"use client";
-
-import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
-import { TimelineElement } from "./timeline-element";
-import type { TimelineTrack } from "@/lib/timeline";
-import type { TimelineElement as TimelineElementType } from "@/lib/timeline";
-import type { SnapPoint } from "@/lib/timeline/snap-utils";
-import { TIMELINE_LAYERS } from "./layers";
-import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
-import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
-import type { ElementDragState } from "@/lib/timeline";
-import { useEditor } from "@/hooks/use-editor";
-
-interface TimelineTrackContentProps {
- track: TimelineTrack;
- zoomLevel: number;
- dragState: ElementDragState;
- rulerScrollRef: React.RefObject<HTMLDivElement | null>;
- tracksScrollRef: React.RefObject<HTMLDivElement | null>;
- lastMouseXRef: React.RefObject<number>;
- onSnapPointChange?: (snapPoint: SnapPoint | null) => void;
- onResizeStateChange?: (params: { isResizing: boolean }) => void;
- onElementMouseDown: (params: {
- event: React.MouseEvent;
- element: TimelineElementType;
- track: TimelineTrack;
- }) => void;
- onElementClick: (params: {
- event: React.MouseEvent;
- element: TimelineElementType;
- track: TimelineTrack;
- }) => void;
- onTrackMouseDown?: (event: React.MouseEvent) => void;
- onTrackMouseUp?: (event: React.MouseEvent) => void;
- shouldIgnoreClick?: () => boolean;
- targetElementId?: string | null;
-}
-
-export function TimelineTrackContent({
- track,
- zoomLevel,
- dragState,
- rulerScrollRef,
- tracksScrollRef,
- lastMouseXRef,
- onSnapPointChange,
- onResizeStateChange,
- onElementMouseDown,
- onElementClick,
- onTrackMouseDown,
- onTrackMouseUp,
- shouldIgnoreClick,
- targetElementId = null,
-}: TimelineTrackContentProps) {
- const { isElementSelected } = useElementSelection();
- const duration = useEditor((e) => e.timeline.getTotalDuration());
-
- useEdgeAutoScroll({
- isActive: dragState.isDragging,
- getMouseClientX: () => lastMouseXRef.current ?? 0,
- rulerScrollRef,
- tracksScrollRef,
- contentWidth: duration * BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel,
- });
-
- return (
- <div className="relative size-full">
- <button
- type="button"
- className="absolute inset-0 m-0 size-full appearance-none border-0 bg-transparent p-0"
- aria-label={`Select ${track.name} track`}
- onMouseUp={(event) => {
- if (shouldIgnoreClick?.()) return;
- onTrackMouseUp?.(event);
- }}
- onMouseDown={(event) => {
- event.preventDefault();
- onTrackMouseDown?.(event);
- }}
- />
- {/* biome-ignore lint/a11y/noStaticElementInteractions: empty track area is a pointer-only seek surface */}
- <div
- className="relative h-full min-w-full"
- style={{ zIndex: TIMELINE_LAYERS.trackContent }}
- onMouseUp={(event) => {
- if (event.target !== event.currentTarget) return;
- if (shouldIgnoreClick?.()) return;
- onTrackMouseUp?.(event);
- }}
- onMouseDown={(event) => {
- if (event.target !== event.currentTarget) return;
- event.preventDefault();
- onTrackMouseDown?.(event);
- }}
- >
- {track.elements.length === 0 ? (
- <div className="text-muted-foreground border-muted/30 pointer-events-none flex size-full items-center justify-center rounded-sm border-2 border-dashed text-xs" />
- ) : (
- track.elements.map((element) => {
- const isSelected = isElementSelected({
- trackId: track.id,
- elementId: element.id,
- });
-
- return (
- <TimelineElement
- key={element.id}
- element={element}
- track={track}
- zoomLevel={zoomLevel}
- isSelected={isSelected}
- onSnapPointChange={onSnapPointChange}
- onResizeStateChange={onResizeStateChange}
- onElementMouseDown={(event, element) =>
- onElementMouseDown({ event, element, track })
- }
- onElementClick={(event, element) =>
- onElementClick({ event, element, track })
- }
- dragState={dragState}
- isDropTarget={element.id === targetElementId}
- />
- );
- })
- )}
- </div>
- </div>
- );
-}
+"use client"; + +import { useElementSelection } from "@/hooks/timeline/element/use-element-selection"; +import { TimelineElement } from "./timeline-element"; +import type { TimelineTrack } from "@/lib/timeline"; +import type { TimelineElement as TimelineElementType } from "@/lib/timeline"; +import { TIMELINE_LAYERS } from "./layers"; +import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale"; +import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll"; +import type { ElementDragState } from "@/lib/timeline"; +import { useEditor } from "@/hooks/use-editor"; + +interface TimelineTrackContentProps { + track: TimelineTrack; + zoomLevel: number; + dragState: ElementDragState; + rulerScrollRef: React.RefObject<HTMLDivElement | null>; + tracksScrollRef: React.RefObject<HTMLDivElement | null>; + lastMouseXRef: React.RefObject<number>; + onResizeStart: (params: { + event: React.MouseEvent; + element: TimelineElementType; + track: TimelineTrack; + side: "left" | "right"; + }) => void; + onElementMouseDown: (params: { + event: React.MouseEvent; + element: TimelineElementType; + track: TimelineTrack; + }) => void; + onElementClick: (params: { + event: React.MouseEvent; + element: TimelineElementType; + track: TimelineTrack; + }) => void; + onTrackMouseDown?: (event: React.MouseEvent) => void; + onTrackMouseUp?: (event: React.MouseEvent) => void; + shouldIgnoreClick?: () => boolean; + targetElementId?: string | null; +} + +export function TimelineTrackContent({ + track, + zoomLevel, + dragState, + rulerScrollRef, + tracksScrollRef, + lastMouseXRef, + onResizeStart, + onElementMouseDown, + onElementClick, + onTrackMouseDown, + onTrackMouseUp, + shouldIgnoreClick, + targetElementId = null, +}: TimelineTrackContentProps) { + const { isElementSelected } = useElementSelection(); + const duration = useEditor((e) => e.timeline.getTotalDuration()); + + useEdgeAutoScroll({ + isActive: dragState.isDragging, + getMouseClientX: () => lastMouseXRef.current ?? 0, + rulerScrollRef, + tracksScrollRef, + contentWidth: duration * BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel, + }); + + return ( + <div className="relative size-full"> + <button + type="button" + className="absolute inset-0 m-0 size-full appearance-none border-0 bg-transparent p-0" + aria-label={`Select ${track.name} track`} + onMouseUp={(event) => { + if (shouldIgnoreClick?.()) return; + onTrackMouseUp?.(event); + }} + onMouseDown={(event) => { + event.preventDefault(); + onTrackMouseDown?.(event); + }} + /> + {/* biome-ignore lint/a11y/noStaticElementInteractions: empty track area is a pointer-only seek surface */} + <div + className="relative h-full min-w-full" + style={{ zIndex: TIMELINE_LAYERS.trackContent }} + onMouseUp={(event) => { + if (event.target !== event.currentTarget) return; + if (shouldIgnoreClick?.()) return; + onTrackMouseUp?.(event); + }} + onMouseDown={(event) => { + if (event.target !== event.currentTarget) return; + event.preventDefault(); + onTrackMouseDown?.(event); + }} + > + {track.elements.length === 0 ? ( + <div className="text-muted-foreground border-muted/30 pointer-events-none flex size-full items-center justify-center rounded-sm border-2 border-dashed text-xs" /> + ) : ( + track.elements.map((element) => { + const isSelected = isElementSelected({ + trackId: track.id, + elementId: element.id, + }); + + return ( + <TimelineElement + key={element.id} + element={element} + track={track} + zoomLevel={zoomLevel} + isSelected={isSelected} + onResizeStart={({ event, element, side }) => + onResizeStart({ event, element, track, side }) + } + onElementMouseDown={(event, element) => + onElementMouseDown({ event, element, track }) + } + onElementClick={(event, element) => + onElementClick({ event, element, track }) + } + dragState={dragState} + isDropTarget={element.id === targetElementId} + /> + ); + }) + )} + </div> + </div> + ); +} |
