Files
hyperframes/packages/studio/src/player/components/Player.tsx
T
Miguel Ángel 40bd159103 feat(studio): render queue, layout restructure, home page, hover preview (#95)
## Summary
- Add render queue panel with progress tracking, download, and delete actions
- Restructure App layout: home page with project picker, session-based routing
- Add ExpandOnHover component for preview-on-hover interactions (uses motion/react)
- CompositionsTab now supports hover preview with expanded iframe view
- Vite config: guard setInterval cleanup to dev-only (fixes CI build timeout)
- Add favicon and update studio package deps

## Test plan
- [x] Render queue shows progress, completes, and allows download
- [x] Home page lists projects and navigates to session view
- [x] ExpandOnHover shows expanded preview on mouse hover with spring animation
- [x] `vite build` exits cleanly (no hanging process from setInterval)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-03-28 21:28:43 +01:00

132 lines
4.2 KiB
TypeScript

import { forwardRef, useRef, useState, useCallback } from "react";
import { useMountEffect } from "../../hooks/useMountEffect";
const NATIVE_W = 1920;
const NATIVE_H = 1080;
interface PlayerProps {
projectId?: string;
directUrl?: string;
onLoad: () => void;
portrait?: boolean;
}
export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
({ projectId, directUrl, onLoad, portrait }, ref) => {
const containerRef = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(1);
const dimsRef = useRef({
w: portrait ? NATIVE_H : NATIVE_W,
h: portrait ? NATIVE_W : NATIVE_H,
});
const [dims, setDims] = useState(dimsRef.current);
const loadCountRef = useRef(0);
const updateScale = useCallback(() => {
const el = containerRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const d = dimsRef.current;
setScale(Math.min(rect.width / d.w, rect.height / d.h));
}, []);
useMountEffect(() => {
updateScale();
const ro = new ResizeObserver(updateScale);
if (containerRef.current) ro.observe(containerRef.current);
// Listen for stage-size messages from the runtime
const handleMessage = (e: MessageEvent) => {
const data = e.data;
if (
data?.source === "hf-preview" &&
data?.type === "stage-size" &&
data.width > 0 &&
data.height > 0
) {
if (dimsRef.current.w !== data.width || dimsRef.current.h !== data.height) {
dimsRef.current = { w: data.width, h: data.height };
setDims(dimsRef.current);
updateScale();
}
}
};
window.addEventListener("message", handleMessage);
return () => {
ro.disconnect();
window.removeEventListener("message", handleMessage);
};
});
const handleLoad = useCallback(() => {
loadCountRef.current++;
// Auto-detect dimensions from the composition's data-width/data-height
try {
const iframeEl = typeof ref === "function" ? null : ref?.current;
const doc = iframeEl?.contentDocument;
if (doc) {
const root = doc.querySelector("[data-composition-id]");
if (root) {
const dw = parseInt(root.getAttribute("data-width") || "0", 10);
const dh = parseInt(root.getAttribute("data-height") || "0", 10);
if (dw > 0 && dh > 0 && (dw !== dimsRef.current.w || dh !== dimsRef.current.h)) {
dimsRef.current = { w: dw, h: dh };
setDims(dimsRef.current);
// Recalc scale with new dims
const el = containerRef.current;
if (el) {
const rect = el.getBoundingClientRect();
setScale(Math.min(rect.width / dw, rect.height / dh));
}
}
}
}
} catch (err) {
console.warn("[Player] Could not read iframe dimensions (cross-origin)", err);
}
if (loadCountRef.current > 1) {
const el = containerRef.current;
if (el) {
el.classList.remove("preview-revealing");
void el.offsetWidth;
el.classList.add("preview-revealing");
const onEnd = () => el.classList.remove("preview-revealing");
el.addEventListener("animationend", onEnd, { once: true });
}
}
onLoad();
}, [onLoad, ref]);
return (
<div
ref={containerRef}
className="w-full h-full max-w-full max-h-full overflow-hidden bg-black flex items-center justify-center"
>
<iframe
ref={ref}
src={directUrl || `/api/projects/${projectId}/preview`}
onLoad={handleLoad}
sandbox="allow-scripts allow-same-origin"
allow="autoplay; fullscreen"
referrerPolicy="no-referrer"
title="Project Preview"
style={{
width: dims.w,
height: dims.h,
border: "none",
outline: "1px solid black",
transform: `scale(${scale})`,
transformOrigin: "center center",
flexShrink: 0,
}}
/>
</div>
);
},
);
Player.displayName = "Player";