summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/panels/timeline
diff options
context:
space:
mode:
authorMaze <167211895+mazeincoding@users.noreply.github.com>2026-02-23 03:24:02 +0100
committerGitHub <noreply@github.com>2026-02-23 03:24:02 +0100
commit93d1e3383ce47e45157d70f9ba6251239dae6bc2 (patch)
tree1e33a2d7533b65f0e7c8927f03905a1a99937bd1 /apps/web/src/components/editor/panels/timeline
parentfca99d6126c31fbb18ed9f1034cee6f940b040e8 (diff)
downloadopencut-93d1e3383ce47e45157d70f9ba6251239dae6bc2.tar.gz
feat: major editor overhaul (assets, properties, timeline, fonts) (#709)
* feat: major editor overhaul (assets, properties, timeline, fonts) Refactor editor core systems to standardize UI architecture and improve performance. Assets & Properties: - Replace monolithic property items with composable `Section` architecture. - Add specialized sections for Transform, Blending, and Text. - Implement `NumberField` with scrubbing and math evaluation. - Add new ColorPicker with EyeDropper and multiple format support. - Standardize asset panels using new `PanelView` layout. Fonts & Stickers: - Implement custom font atlas/sprite system for high-performance previews. - Add virtualized FontPicker with search and favorites. - Refactor stickers to use a provider-based architecture (icons, emoji, flags, shapes). - Standardize sticker IDs to `provider:value` format. Timeline & Interaction: - Convert bookmarks to rich objects with notes, colors, and duration. - Refactor drag-and-drop to use Command pattern (enabling proper undo/redo). - Add Shift modifier to disable snapping during moves/resizes. - Add new overlays for layout guides and text editing. Renderer: - Add support for multi-line text, custom line-height, and letter-spacing. - Implement global composite operation (blend modes). - Update sticker node to resolve dynamic provider IDs. Infrastructure: - Add storage migrations (v3->v6) for text weights, sticker IDs, and bookmarks. - Update global styles and core UI components (Button, Input, Popover). * add ts-nocheck directive to settings-legacy.tsx to suppress TypeScript errors * fix: correct global composite operation assignment in TextNode to ensure proper blend mode handling * deleted shadcn components with errors * formatting * fix linter issues * migrate from next middleware to proxy * add missing component back * add breadcrumb back * chore: add @radix-ui/react-primitive deps * chore: more deps * chore: add missing env vars to bun-ci * next env
Diffstat (limited to 'apps/web/src/components/editor/panels/timeline')
-rw-r--r--apps/web/src/components/editor/panels/timeline/bookmarks.tsx409
-rw-r--r--apps/web/src/components/editor/panels/timeline/index.tsx29
-rw-r--r--apps/web/src/components/editor/panels/timeline/snap-indicator.tsx2
-rw-r--r--apps/web/src/components/editor/panels/timeline/timeline-element.tsx25
-rw-r--r--apps/web/src/components/editor/panels/timeline/timeline-playhead.tsx12
-rw-r--r--apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx5
6 files changed, 404 insertions, 78 deletions
diff --git a/apps/web/src/components/editor/panels/timeline/bookmarks.tsx b/apps/web/src/components/editor/panels/timeline/bookmarks.tsx
index cfcb1e23..64824f9a 100644
--- a/apps/web/src/components/editor/panels/timeline/bookmarks.tsx
+++ b/apps/web/src/components/editor/panels/timeline/bookmarks.tsx
@@ -1,21 +1,75 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import type { EditorCore } from "@/core";
import { useEditor } from "@/hooks/use-editor";
-import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import type { BookmarkDragState } from "@/hooks/timeline/use-bookmark-drag";
+import { BOOKMARK_TIME_EPSILON } from "@/lib/timeline/bookmarks";
+import {
+ DEFAULT_BOOKMARK_COLOR,
+ TIMELINE_CONSTANTS,
+} from "@/constants/timeline-constants";
+import { DEFAULT_FPS } from "@/constants/project-constants";
import { getSnappedSeekTime } from "@/lib/time";
-import { Bookmark02Icon } from "@hugeicons/core-free-icons";
+import {
+ ArrowTurnBackwardIcon,
+ Delete02Icon,
+} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
+import type { Bookmark } from "@/types/timeline";
+import {
+ Popover,
+ PopoverAnchor,
+ PopoverContent,
+} from "@/components/ui/popover";
+import { Input } from "@/components/ui/input";
+import { ColorPicker } from "@/components/ui/color-picker";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { uppercase } from "@/utils/string";
+import { clamp } from "@/utils/math";
+
+const PIXELS_PER_SECOND = TIMELINE_CONSTANTS.PIXELS_PER_SECOND;
+const MIN_BOOKMARK_WIDTH_PX = 2;
+const BOOKMARK_MARKER_WIDTH_PX = 12;
+const BOOKMARK_MARKER_HEIGHT_PX = 15;
+const BOOKMARK_HALF_WIDTH_PX = BOOKMARK_MARKER_WIDTH_PX / 2;
+const BOOKMARK_MARKER_CLIP_PATH =
+ "polygon(50% 100%, 12% 72%, 12% 10%, 88% 10%, 88% 72%)";
+
+function seekToBookmarkTime({
+ editor,
+ time,
+}: {
+ editor: EditorCore;
+ time: number;
+}) {
+ const activeProject = editor.project.getActive();
+ const duration = editor.timeline.getTotalDuration();
+ const fps = activeProject?.settings.fps ?? DEFAULT_FPS;
+ const snappedTime = getSnappedSeekTime({ rawTime: time, duration, fps });
+ editor.playback.seek({ time: snappedTime });
+}
interface TimelineBookmarksRowProps {
zoomLevel: number;
dynamicTimelineWidth: number;
- handleWheel: (e: React.WheelEvent) => void;
- handleTimelineContentClick: (e: React.MouseEvent) => void;
- handleRulerTrackingMouseDown: (e: React.MouseEvent) => void;
- handleRulerMouseDown: (e: React.MouseEvent) => void;
+ dragState: BookmarkDragState;
+ onBookmarkMouseDown: (params: {
+ event: React.MouseEvent;
+ bookmark: Bookmark;
+ }) => void;
+ handleWheel: (event: React.WheelEvent) => void;
+ handleTimelineContentClick: (event: React.MouseEvent) => void;
+ handleRulerTrackingMouseDown: (event: React.MouseEvent) => void;
+ handleRulerMouseDown: (event: React.MouseEvent) => void;
}
export function TimelineBookmarksRow({
zoomLevel,
dynamicTimelineWidth,
+ dragState,
+ onBookmarkMouseDown,
handleWheel,
handleTimelineContentClick,
handleRulerTrackingMouseDown,
@@ -34,17 +88,23 @@ export function TimelineBookmarksRow({
aria-label="Timeline ruler"
type="button"
onWheel={handleWheel}
- onClick={handleTimelineContentClick}
+ onClick={(event) => {
+ if (!event.currentTarget.contains(event.target as Node)) return;
+ handleTimelineContentClick(event);
+ }}
onMouseDown={(event) => {
+ if (!event.currentTarget.contains(event.target as Node)) return;
handleRulerMouseDown(event);
handleRulerTrackingMouseDown(event);
}}
>
- {activeScene.bookmarks.map((time: number) => (
+ {activeScene.bookmarks.map((bookmark) => (
<TimelineBookmark
- key={`bookmark-row-${time}`}
- time={time}
+ key={`bookmark-${bookmark.time}`}
+ bookmark={bookmark}
zoomLevel={zoomLevel}
+ dragState={dragState}
+ onBookmarkMouseDown={onBookmarkMouseDown}
/>
))}
</button>
@@ -52,60 +112,305 @@ export function TimelineBookmarksRow({
);
}
-export function TimelineBookmark({
- time,
+function TimelineBookmark({
+ bookmark,
zoomLevel,
+ dragState,
+ onBookmarkMouseDown,
}: {
- time: number;
+ bookmark: Bookmark;
zoomLevel: number;
+ dragState: BookmarkDragState;
+ onBookmarkMouseDown: (params: {
+ event: React.MouseEvent;
+ bookmark: Bookmark;
+ }) => void;
}) {
const editor = useEditor();
- const activeProject = editor.project.getActive();
const duration = editor.timeline.getTotalDuration();
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const isDragging =
+ dragState.isDragging &&
+ dragState.bookmarkTime !== null &&
+ Math.abs(dragState.bookmarkTime - bookmark.time) < BOOKMARK_TIME_EPSILON;
+
+ const displayTime = isDragging ? dragState.currentTime : bookmark.time;
+ const time = bookmark.time;
+ const bookmarkDuration = bookmark.duration ?? 0;
+ const durationWidth =
+ bookmarkDuration > 0 ? bookmarkDuration * PIXELS_PER_SECOND * zoomLevel : 0;
+ const hasDurationRange = durationWidth > MIN_BOOKMARK_WIDTH_PX;
+ const bookmarkWidth = BOOKMARK_MARKER_WIDTH_PX + Math.max(durationWidth, 0);
+ const left = displayTime * PIXELS_PER_SECOND * zoomLevel;
+ const bookmarkLeft = left - BOOKMARK_HALF_WIDTH_PX;
+ const rightHalfLeft = BOOKMARK_HALF_WIDTH_PX + Math.max(durationWidth, 0);
+ const iconColor = bookmark.color ?? DEFAULT_BOOKMARK_COLOR;
+
+ const handleSeek = () => seekToBookmarkTime({ editor, time });
+
+ const handleClick = (event: React.MouseEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+ if (event.detail === 2) {
+ setIsPopoverOpen(true);
+ } else {
+ handleSeek();
+ }
+ };
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key !== "Enter" && event.key !== " ") return;
+ event.preventDefault();
+ handleSeek();
+ };
- const handleBookmarkActivate = ({
- event,
- }: {
- event:
- | React.MouseEvent<HTMLButtonElement>
- | React.KeyboardEvent<HTMLButtonElement>;
- }) => {
+ const handleMouseDown = (event: React.MouseEvent) => {
+ onBookmarkMouseDown({ event, bookmark });
+ event.preventDefault();
event.stopPropagation();
- const framesPerSecond = activeProject?.settings.fps ?? 30;
- const snappedTime = getSnappedSeekTime({
- rawTime: time,
- duration,
- fps: framesPerSecond,
- });
- editor.playback.seek({ time: snappedTime });
};
return (
- <button
- className="absolute top-0 h-10 w-0.5 cursor-pointer border-0 bg-transparent p-0"
- style={{
- left: `${time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`,
- }}
- aria-label={`Seek to bookmark at ${time}s`}
- type="button"
- onMouseDown={(event) => {
- event.preventDefault();
- event.stopPropagation();
- }}
- onClick={(event) => handleBookmarkActivate({ event })}
- onKeyDown={(event) => {
- if (event.key !== "Enter" && event.key !== " ") return;
- event.preventDefault();
- handleBookmarkActivate({ event });
- }}
- >
- <div className="text-primary absolute top-[-1px] left-[-5px]">
- <HugeiconsIcon
- icon={Bookmark02Icon}
- aria-hidden="true"
- className="fill-primary size-3"
+ <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
+ <PopoverAnchor asChild>
+ <button
+ className={`absolute top-0 h-full min-w-0.5 border-0 bg-transparent p-0 ${isDragging ? "cursor-grabbing" : "cursor-grab"}`}
+ style={{
+ left: `${bookmarkLeft}px`,
+ width: `${bookmarkWidth}px`,
+ }}
+ aria-label={`Bookmark at ${time.toFixed(1)}s`}
+ type="button"
+ onMouseDown={handleMouseDown}
+ onClick={handleClick}
+ onKeyDown={handleKeyDown}
+ >
+ {hasDurationRange ? (
+ <div
+ className="absolute opacity-30"
+ style={{
+ top: 1.5,
+ height: BOOKMARK_MARKER_HEIGHT_PX - 2.5,
+ left: BOOKMARK_HALF_WIDTH_PX,
+ width: durationWidth,
+ backgroundColor: iconColor,
+ }}
+ />
+ ) : null}
+ <div
+ className="absolute top-0 overflow-hidden"
+ style={{
+ left: 0,
+ width: BOOKMARK_HALF_WIDTH_PX,
+ height: BOOKMARK_MARKER_HEIGHT_PX,
+ }}
+ >
+ <div
+ className="absolute inset-0"
+ style={{
+ width: BOOKMARK_MARKER_WIDTH_PX,
+ height: BOOKMARK_MARKER_HEIGHT_PX,
+ clipPath: BOOKMARK_MARKER_CLIP_PATH,
+ backgroundColor: "hsl(var(--background))",
+ }}
+ />
+ <div
+ className="absolute"
+ style={{
+ top: 1,
+ left: 1,
+ width: BOOKMARK_MARKER_WIDTH_PX - 2,
+ height: BOOKMARK_MARKER_HEIGHT_PX - 2,
+ clipPath: BOOKMARK_MARKER_CLIP_PATH,
+ backgroundColor: iconColor,
+ }}
+ />
+ </div>
+ <div
+ className="absolute top-0 overflow-hidden"
+ style={{
+ left: rightHalfLeft,
+ width: BOOKMARK_HALF_WIDTH_PX,
+ height: BOOKMARK_MARKER_HEIGHT_PX,
+ }}
+ >
+ <div
+ className="absolute top-0"
+ style={{
+ left: -BOOKMARK_HALF_WIDTH_PX,
+ width: BOOKMARK_MARKER_WIDTH_PX,
+ height: BOOKMARK_MARKER_HEIGHT_PX,
+ clipPath: BOOKMARK_MARKER_CLIP_PATH,
+ backgroundColor: "hsl(var(--background))",
+ }}
+ />
+ <div
+ className="absolute"
+ style={{
+ top: 1,
+ left: 1 - BOOKMARK_HALF_WIDTH_PX,
+ width: BOOKMARK_MARKER_WIDTH_PX - 2,
+ height: BOOKMARK_MARKER_HEIGHT_PX - 2,
+ clipPath: BOOKMARK_MARKER_CLIP_PATH,
+ backgroundColor: iconColor,
+ }}
+ />
+ </div>
+ </button>
+ </PopoverAnchor>
+ <PopoverContent
+ className="w-64 flex flex-col gap-3 p-3"
+ align="start"
+ side="bottom"
+ sideOffset={8}
+ onOpenAutoFocus={(event) => event.preventDefault()}
+ >
+ <BookmarkPopoverContent
+ bookmark={bookmark}
+ time={time}
+ timelineDuration={duration}
+ onPopoverClose={() => setIsPopoverOpen(false)}
/>
+ </PopoverContent>
+ </Popover>
+ );
+}
+
+function BookmarkPopoverContent({
+ bookmark,
+ time,
+ timelineDuration,
+ onPopoverClose,
+}: {
+ bookmark: Bookmark;
+ time: number;
+ timelineDuration: number;
+ onPopoverClose: () => void;
+}) {
+ const editor = useEditor();
+ const [draftColorHex, setDraftColorHex] = useState(
+ (bookmark.color ?? DEFAULT_BOOKMARK_COLOR).replace("#", "").toUpperCase(),
+ );
+
+ useEffect(() => {
+ setDraftColorHex(
+ (bookmark.color ?? DEFAULT_BOOKMARK_COLOR).replace("#", "").toUpperCase(),
+ );
+ }, [bookmark.color]);
+
+ const handleRemove = () => {
+ editor.scenes.removeBookmark({ time });
+ onPopoverClose();
+ };
+
+ const handleUpdate = ({
+ note,
+ color,
+ duration,
+ }: Partial<{ note: string; color: string; duration: number }>) => {
+ const updates: Partial<{ note: string; color: string; duration: number }> =
+ {};
+ if (note !== undefined && note !== bookmark.note) updates.note = note;
+ if (
+ color !== undefined &&
+ color.toUpperCase() !== (bookmark.color ?? "").toUpperCase()
+ ) {
+ updates.color = color;
+ }
+ if (duration !== undefined && duration !== bookmark.duration) {
+ updates.duration = duration;
+ }
+ if (Object.keys(updates).length === 0) return;
+ editor.scenes.updateBookmark({ time, updates });
+ };
+
+ return (
+ <>
+ <div className="flex flex-col gap-2">
+ <Label className="text-xs">Note</Label>
+ <Input
+ placeholder="Add a note..."
+ value={bookmark.note ?? ""}
+ onChange={(event) => handleUpdate({ note: event.target.value })}
+ className="h-8 text-sm"
+ />
+ </div>
+ <div className="flex flex-col gap-2">
+ <Label className="text-xs">Color</Label>
+ <div className="relative">
+ <ColorPicker
+ value={uppercase({ string: draftColorHex })}
+ onChange={(color) => setDraftColorHex(uppercase({ string: color }))}
+ onChangeEnd={(color) =>
+ handleUpdate({ color: `#${uppercase({ string: color })}` })
+ }
+ className="bg-background border"
+ />
+ {bookmark.color &&
+ bookmark.color.replace(/^#/, "").toUpperCase() !==
+ DEFAULT_BOOKMARK_COLOR.replace(/^#/, "").toUpperCase() && (
+ <Button
+ type="button"
+ variant="text"
+ size="text"
+ aria-label="Reset to default color"
+ className="absolute top-1/2 right-1 -translate-y-1/2 mr-1"
+ onClick={() =>
+ editor.scenes.updateBookmark({
+ time,
+ updates: { color: undefined },
+ })
+ }
+ >
+ <HugeiconsIcon
+ icon={ArrowTurnBackwardIcon}
+ className="!size-3.5"
+ />
+ </Button>
+ )}
+ </div>
</div>
- </button>
+ <div className="flex flex-col gap-2">
+ <Label className="text-xs">Duration</Label>
+ <div className="flex items-center gap-1.5">
+ <Input
+ type="number"
+ min={0}
+ step={0.1}
+ value={bookmark.duration ?? 0}
+ onChange={(event) => {
+ const parsed = parseFloat(event.target.value);
+ const value = Number.isNaN(parsed)
+ ? 0
+ : clamp({
+ value: parsed,
+ min: 0,
+ max: Math.max(0, timelineDuration - time),
+ });
+ handleUpdate({ duration: value });
+ }}
+ className="h-8 text-sm"
+ containerClassName="w-full"
+ />
+ </div>
+ </div>
+ <Button
+ type="button"
+ variant="outline"
+ size="sm"
+ className="text-destructive hover:bg-destructive/10"
+ onClick={handleRemove}
+ onKeyDown={(event) => {
+ if (event.key === "Enter" || event.key === " ") {
+ handleRemove();
+ }
+ }}
+ aria-label="delete bookmark"
+ >
+ <HugeiconsIcon icon={Delete02Icon} className="!size-3.5" />
+ Delete
+ </Button>
+ </>
);
}
diff --git a/apps/web/src/components/editor/panels/timeline/index.tsx b/apps/web/src/components/editor/panels/timeline/index.tsx
index 94edc2d0..7e6ff326 100644
--- a/apps/web/src/components/editor/panels/timeline/index.tsx
+++ b/apps/web/src/components/editor/panels/timeline/index.tsx
@@ -47,6 +47,8 @@ import { useTimelineSeek } from "@/hooks/timeline/use-timeline-seek";
import { useTimelineDragDrop } from "@/hooks/timeline/use-timeline-drag-drop";
import { TimelineRuler } from "./timeline-ruler";
import { TimelineBookmarksRow } from "./bookmarks";
+import { useBookmarkDrag } from "@/hooks/timeline/use-bookmark-drag";
+import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
import { useTimelineStore } from "@/stores/timeline-store";
import { useEditor } from "@/hooks/use-editor";
import { useTimelinePlayhead } from "@/hooks/timeline/use-timeline-playhead";
@@ -126,6 +128,17 @@ export function Timeline() {
onSnapPointChange: handleSnapPointChange,
});
+ const {
+ dragState: bookmarkDragState,
+ handleBookmarkMouseDown,
+ lastMouseXRef: bookmarkLastMouseXRef,
+ } = useBookmarkDrag({
+ zoomLevel,
+ scrollRef: tracksScrollRef,
+ snappingEnabled,
+ onSnapPointChange: handleSnapPointChange,
+ });
+
const { handleRulerMouseDown: handlePlayheadRulerMouseDown } =
useTimelinePlayhead({
zoomLevel,
@@ -168,10 +181,18 @@ export function Timeline() {
containerWidth,
);
+ useEdgeAutoScroll({
+ isActive: bookmarkDragState.isDragging,
+ getMouseClientX: () => bookmarkLastMouseXRef.current,
+ rulerScrollRef: tracksScrollRef,
+ tracksScrollRef,
+ contentWidth: dynamicTimelineWidth,
+ });
+
const showSnapIndicator =
snappingEnabled &&
currentSnapPoint !== null &&
- (dragState.isDragging || isResizing);
+ (dragState.isDragging || bookmarkDragState.isDragging || isResizing);
const {
handleTracksMouseDown,
@@ -351,7 +372,7 @@ export function Timeline() {
>
<div
ref={timelineHeaderRef}
- className="bg-background sticky top-0 z-30 flex flex-col"
+ className="bg-background sticky top-0 flex flex-col"
>
<TimelineRuler
zoomLevel={zoomLevel}
@@ -366,6 +387,8 @@ export function Timeline() {
<TimelineBookmarksRow
zoomLevel={zoomLevel}
dynamicTimelineWidth={dynamicTimelineWidth}
+ dragState={bookmarkDragState}
+ onBookmarkMouseDown={handleBookmarkMouseDown}
handleWheel={handleWheel}
handleTimelineContentClick={handleRulerClick}
handleRulerTrackingMouseDown={handleRulerMouseDown}
@@ -433,7 +456,7 @@ export function Timeline() {
/>
</div>
</ContextMenuTrigger>
- <ContextMenuContent className="z-200 w-40">
+ <ContextMenuContent className="w-40">
<ContextMenuItem
icon={<HugeiconsIcon icon={TaskAdd02Icon} />}
onClick={(e) => {
diff --git a/apps/web/src/components/editor/panels/timeline/snap-indicator.tsx b/apps/web/src/components/editor/panels/timeline/snap-indicator.tsx
index 914edccb..e70f0cd7 100644
--- a/apps/web/src/components/editor/panels/timeline/snap-indicator.tsx
+++ b/apps/web/src/components/editor/panels/timeline/snap-indicator.tsx
@@ -38,7 +38,7 @@ export function SnapIndicator({
return (
<div
- className="pointer-events-none absolute z-90"
+ className="pointer-events-none absolute"
style={{
left: `${leftPosition}px`,
top: topPosition,
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 59ce88da..628855f6 100644
--- a/apps/web/src/components/editor/panels/timeline/timeline-element.tsx
+++ b/apps/web/src/components/editor/panels/timeline/timeline-element.tsx
@@ -29,6 +29,7 @@ import type { MediaAsset } from "@/types/assets";
import { mediaSupportsAudio } from "@/lib/media/media-utils";
import { getActionDefinition, type TAction, invokeAction } from "@/lib/actions";
import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
+import { resolveStickerId } from "@/lib/stickers";
import Image from "next/image";
import {
ScissorIcon,
@@ -139,9 +140,7 @@ export function TimelineElement({
<ContextMenu>
<ContextMenuTrigger asChild>
<div
- className={`absolute top-0 h-full select-none ${
- isBeingDragged ? "z-30" : "z-10"
- }`}
+ className={`absolute top-0 h-full select-none`}
style={{
left: `${elementLeft}px`,
width: `${elementWidth}px`,
@@ -155,7 +154,6 @@ export function TimelineElement({
element={element}
track={track}
isSelected={isSelected}
- isBeingDragged={isBeingDragged}
hasAudio={hasAudio}
isMuted={isMuted}
mediaAssets={mediaAssets}
@@ -165,7 +163,7 @@ export function TimelineElement({
/>
</div>
</ContextMenuTrigger>
- <ContextMenuContent className="z-200 w-64">
+ <ContextMenuContent className="w-64">
<ActionMenuItem
action="split"
icon={<HugeiconsIcon icon={ScissorIcon} />}
@@ -227,7 +225,6 @@ function ElementInner({
element,
track,
isSelected,
- isBeingDragged,
hasAudio,
isMuted,
mediaAssets,
@@ -238,7 +235,6 @@ function ElementInner({
element: TimelineElementType;
track: TimelineTrack;
isSelected: boolean;
- isBeingDragged: boolean;
hasAudio: boolean;
isMuted: boolean;
mediaAssets: MediaAsset[];
@@ -248,7 +244,7 @@ function ElementInner({
element: TimelineElementType,
) => void;
handleResizeStart: (params: {
- e: React.MouseEvent;
+ event: React.MouseEvent;
elementId: string;
side: "left" | "right";
}) => void;
@@ -259,7 +255,7 @@ function ElementInner({
{
type: track.type,
},
- )} ${isBeingDragged ? "z-30" : "z-10"} ${canElementBeHidden(element) && element.hidden ? "opacity-50" : ""}`}
+ )} ${canElementBeHidden(element) && element.hidden ? "opacity-50" : ""}`}
>
<button
type="button"
@@ -321,7 +317,7 @@ function ResizeHandle({
side: "left" | "right";
elementId: string;
handleResizeStart: (params: {
- e: React.MouseEvent;
+ event: React.MouseEvent;
elementId: string;
side: "left" | "right";
}) => void;
@@ -330,8 +326,8 @@ function ResizeHandle({
return (
<button
type="button"
- className={`bg-primary absolute top-0 bottom-0 z-50 flex w-[0.6rem] items-center justify-center ${isLeft ? "left-0 cursor-w-resize" : "right-0 cursor-e-resize"}`}
- onMouseDown={(e) => handleResizeStart({ e, elementId, side })}
+ className={`bg-primary absolute top-0 bottom-0 flex w-[0.6rem] items-center justify-center ${isLeft ? "left-0 cursor-w-resize" : "right-0 cursor-e-resize"}`}
+ onMouseDown={(event) => handleResizeStart({ event, elementId, side })}
aria-label={`${isLeft ? "Left" : "Right"} resize handle`}
>
<div className="bg-foreground h-[1.5rem] w-[0.2rem] rounded-full" />
@@ -362,7 +358,10 @@ function ElementContent({
return (
<div className="flex size-full items-center gap-2 pl-2">
<Image
- src={`https://api.iconify.design/${element.iconName}.svg?width=20&height=20`}
+ src={resolveStickerId({
+ stickerId: element.stickerId,
+ options: { width: 20, height: 20 },
+ })}
alt={element.name}
className="size-5 shrink-0"
width={20}
diff --git a/apps/web/src/components/editor/panels/timeline/timeline-playhead.tsx b/apps/web/src/components/editor/panels/timeline/timeline-playhead.tsx
index 8af35dc9..ebb2a37e 100644
--- a/apps/web/src/components/editor/panels/timeline/timeline-playhead.tsx
+++ b/apps/web/src/components/editor/panels/timeline/timeline-playhead.tsx
@@ -72,20 +72,22 @@ export function TimelinePlayhead({
aria-valuemax={duration}
aria-valuenow={playheadPosition}
tabIndex={0}
- className="pointer-events-auto absolute z-60"
+ className="pointer-events-none absolute z-5"
style={{
left: `${leftPosition}px`,
top: 0,
height: `${totalHeight}px`,
width: "2px",
}}
- onMouseDown={handlePlayheadMouseDown}
onKeyDown={handlePlayheadKeyDown}
>
- <div className="bg-foreground absolute left-0 h-full w-0.5 cursor-col-resize" />
+ <div className="bg-foreground pointer-events-none absolute left-0 h-full w-0.5" />
- <div
- className={`absolute top-1 left-1/2 size-3 -translate-x-1/2 transform rounded-full border-2 shadow-xs ${isSnappingToPlayhead ? "bg-foreground border-foreground" : "bg-foreground border-foreground/50"}`}
+ <button
+ type="button"
+ aria-label="Drag playhead"
+ className={`pointer-events-auto absolute top-1 left-1/2 size-3 -translate-x-1/2 transform cursor-col-resize rounded-full border-2 shadow-xs ${isSnappingToPlayhead ? "bg-foreground border-foreground" : "bg-foreground border-foreground/50"}`}
+ onMouseDown={handlePlayheadMouseDown}
/>
</div>
);
diff --git a/apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx b/apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx
index 0b02c63f..03ce88eb 100644
--- a/apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx
+++ b/apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx
@@ -171,7 +171,7 @@ function SceneSelector() {
<SplitButtonLeft>{currentScene?.name || "No Scene"}</SplitButtonLeft>
<SplitButtonSeparator />
<ScenesView>
- <SplitButtonRight onClick={() => {}} type="button">
+ <SplitButtonRight onClick={() => {}}>
<HugeiconsIcon icon={Layers01Icon} className="size-4" />
</SplitButtonRight>
</ScenesView>
@@ -222,7 +222,6 @@ function ToolbarRightSection({
<Button
variant="text"
size="icon"
- type="button"
onClick={() => onZoom({ direction: "out" })}
>
<HugeiconsIcon icon={SearchMinusIcon} />
@@ -240,7 +239,6 @@ function ToolbarRightSection({
<Button
variant="text"
size="icon"
- type="button"
onClick={() => onZoom({ direction: "in" })}
>
<HugeiconsIcon icon={SearchAddIcon} />
@@ -269,7 +267,6 @@ function ToolbarButton({
<Button
variant={isActive ? "secondary" : "text"}
size="icon"
- type="button"
onClick={(event) => onClick({ event })}
className={cn(
"rounded-sm",