Files
hyperframes/packages/studio/src/player/components/PreviewPanel.tsx
T
Vance IngallsandClaude Opus 4.6 9f8e5ba5a1 initial code (#2)
* feat: initial code port from hyperframes-internal

Port all OSS-ready packages from the internal monorepo:
- @hyperframes/core — shared types, HTML generation, GSAP utilities, runtime
- @hyperframes/cli — CLI for creating, previewing, and rendering compositions
- @hyperframes/engine — framework-agnostic rendering engine (BeginFrame + FFmpeg)
- @hyperframes/producer — video rendering pipeline (Puppeteer + FFmpeg)
- @hyperframes/ui-player — browser-based video player component
- @hyperframes/studio — composition editor (React frontend + Hono backend)

Includes regression test suite with Docker-based test harness.

All HeyGen-internal references, deployment infrastructure, and
proprietary assets have been removed. Package names migrated
from @app/* to @hyperframes/*.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: scrub internal codenames and stale references from OSS port

- Replace static.heygen.ai runtime URLs in test fixtures
- Remove internal CDN publish script (publish-hyperframe-runtime.ts)
- Replace sandbox-studio, sandbox-interceptor, __magicEditRuntime
  with neutral names (studio, hyperframe-runtime, __hyperframeRuntime)
- Fix stale Vault API / localhost references in docs
- Remove broken deprecated_studio link

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove remaining internal codenames and stale references

- Delete stale producer README.md and PIPELINE.md (referenced nonexistent files)
- Replace "Cerberus" codename with "HyperFrames" in test design reviews
- Replace magic-edit postMessage identifiers with hf-preview/hf-parent
- Rename debug-magic-edit-timeline.ts to debug-timeline.ts
- Replace "Motion Cut" with "HyperFrames" in Timeline comments
- Fix studio/CLI references to nonexistent archive package
  (use local data/projects/ dir, stub render proxy)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 22:43:56 -07:00

150 lines
5.6 KiB
TypeScript

import type { ReactNode, Ref } from "react";
import { Player } from "./Player";
import { PlayerControls } from "./PlayerControls";
import { Timeline } from "./Timeline";
interface RenderStatus {
state: "idle" | "rendering" | "complete" | "error";
stage?: string;
progress?: number;
error?: string;
onRender?: () => void;
}
interface PreviewPanelProps {
projectId: string | null;
hasProject: boolean;
portrait: boolean;
iframeRef: Ref<HTMLIFrameElement>;
onIframeLoad: () => void;
onTogglePlay: () => void;
onSeek: (t: number) => void;
/** Optional render status — pass to show rendering progress/state */
renderStatus?: RenderStatus;
/** Optional slot for custom content below the timeline */
children?: ReactNode;
}
export function PreviewPanel({
projectId,
hasProject,
portrait,
iframeRef,
onIframeLoad,
onTogglePlay,
onSeek,
renderStatus,
children,
}: PreviewPanelProps) {
const renderState = renderStatus?.state ?? "idle";
return (
<div
className="min-w-0 overflow-hidden"
style={{
display: "grid",
gridTemplateRows: hasProject && projectId ? "1fr auto auto auto" : "1fr",
height: "100%",
minHeight: 0,
}}
>
{hasProject && projectId ? (
<>
{/* Player — takes all remaining space, constrained for portrait */}
<div className="flex items-center justify-center p-2 overflow-hidden" style={{ minHeight: 0, minWidth: 0 }}>
<Player ref={iframeRef} projectId={projectId} onLoad={onIframeLoad} portrait={portrait} />
</div>
{/* Controls — fixed height */}
<div className="bg-neutral-950 border-t border-neutral-800 flex-shrink-0">
<PlayerControls
onTogglePlay={onTogglePlay}
onSeek={onSeek}
/>
</div>
{/* Timeline — capped height, internal scroll */}
<div className="bg-neutral-950 flex-shrink-0 overflow-y-auto" style={{ maxHeight: "100px" }}>
<Timeline onSeek={onSeek} />
</div>
{/* Render status — only shown when actively rendering, complete, or error */}
{renderStatus && (renderState === "rendering" || renderState === "complete" || renderState === "error") && (
<div className="bg-neutral-950 border-t border-neutral-800 px-4 py-2 flex items-center justify-end gap-2 flex-shrink-0">
{renderState === "rendering" && (
<div className="flex-1">
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 bg-neutral-800 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 rounded-full transition-[width] duration-200"
style={{ width: `${renderStatus.progress ?? 0}%` }}
/>
</div>
<span className="text-xs text-neutral-400 flex-shrink-0">
{renderStatus.stage || "Rendering..."}
</span>
</div>
</div>
)}
{renderState === "complete" && (
<div className="flex items-center gap-1.5 text-xs text-green-400">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
<span>Complete</span>
</div>
)}
{renderState === "error" && (
<div className="flex items-center gap-2 text-xs text-red-400">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
<span className="truncate">{renderStatus.error}</span>
{renderStatus.onRender && (
<button
type="button"
onClick={renderStatus.onRender}
className="flex-shrink-0 px-2 py-0.5 text-xs text-neutral-300 hover:text-white hover:bg-neutral-800 rounded transition-colors"
>
Retry
</button>
)}
</div>
)}
</div>
)}
{/* Optional custom slot */}
{children}
</>
) : (
<div className="flex items-center justify-center w-full min-w-0">
<div className="text-center w-full">
<div className="w-16 h-16 mx-auto mb-4 rounded-card bg-neutral-900 flex items-center justify-center">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className="text-neutral-600"
>
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
</div>
<p className="text-sm text-neutral-600">Preview will appear here</p>
<p className="text-xs text-neutral-700 mt-1">Send a message to generate a video composition</p>
</div>
</div>
)}
</div>
);
}