From a63f644002bc0ca3d4a34bfd4e11dbb65eb8e483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 30 Mar 2026 19:37:01 +0200 Subject: [PATCH] feat(studio): inline autoplay on render thumbnail hover (no expand) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On hover the thumbnail slot swaps from the static extracted frame to a live muted looping video in the same w-20 h-[45px] cell — no overlay, no expansion. Static frame fades out (150ms opacity) as the video takes over; video is only mounted while hovered so it doesn't run in the background. --- .../components/renders/RenderQueueItem.tsx | 80 +++++++------------ 1 file changed, 29 insertions(+), 51 deletions(-) diff --git a/packages/studio/src/components/renders/RenderQueueItem.tsx b/packages/studio/src/components/renders/RenderQueueItem.tsx index 220a245233..b1d7c6cead 100644 --- a/packages/studio/src/components/renders/RenderQueueItem.tsx +++ b/packages/studio/src/components/renders/RenderQueueItem.tsx @@ -1,6 +1,4 @@ import { memo, useCallback, useState, useEffect, useRef } from "react"; -import { ExpandOnHover } from "../ui/ExpandOnHover"; -import { ExpandedVideoPreview } from "../ui/ExpandedVideoPreview"; import type { RenderJob } from "./useRenderQueue"; interface RenderQueueItemProps { @@ -21,7 +19,7 @@ function formatTimeAgo(timestamp: number): string { return `${Math.floor(diff / 3600000)}h ago`; } -/** Extracts a single JPEG frame from a video URL using a hidden video + canvas. */ +/** Static frame extracted once via hidden video + canvas. */ function RenderThumbnail({ src }: { src: string }) { const [frame, setFrame] = useState(null); const didExtract = useRef(false); @@ -44,7 +42,6 @@ function RenderThumbnail({ src }: { src: string }) { }; video.addEventListener("loadedmetadata", () => { - // Seek to ~10% in so we get a representative non-black frame video.currentTime = Math.min(2, video.duration * 0.1 || 2); }); @@ -58,7 +55,6 @@ function RenderThumbnail({ src }: { src: string }) { }); video.addEventListener("error", cleanup); - video.src = src; video.load(); @@ -94,22 +90,45 @@ export const RenderQueueItem = memo(function RenderQueueItem({ ); const viewSrc = `/api/render/${job.id}/view`; + const isComplete = job.status === "complete"; - const row = ( + return (
setHovered(true)} onPointerLeave={() => setHovered(false)} + onClick={isComplete ? handleOpen : undefined} className={[ "px-3 py-2.5 border-b border-neutral-800/30 last:border-0 transition-colors duration-150", - job.status === "complete" ? "cursor-pointer hover:bg-neutral-800/30" : "", + isComplete ? "cursor-pointer hover:bg-neutral-800/30" : "", ] .filter(Boolean) .join(" ")} >
- {/* Thumbnail — matches CompCard sizing (w-20 h-[45px]) */} + {/* Thumbnail — static frame; swaps to live video on hover */}
- {job.status === "complete" && } + {isComplete && ( + <> + {/* Live video — visible on hover */} + {hovered && ( +