mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
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>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
10621e7903
commit
9f8e5ba5a1
@@ -0,0 +1,70 @@
|
||||
import { memo } from "react";
|
||||
import { FileCode, Image, Film, Music, File } from "../../icons/SystemIcons";
|
||||
|
||||
interface FileTreeProps {
|
||||
files: string[];
|
||||
activeFile: string | null;
|
||||
onSelectFile: (path: string) => void;
|
||||
}
|
||||
|
||||
const FILE_ICONS: Record<string, { icon: typeof File; color: string }> = {
|
||||
html: { icon: FileCode, color: "#3B82F6" },
|
||||
css: { icon: FileCode, color: "#A855F7" },
|
||||
js: { icon: FileCode, color: "#F59E0B" },
|
||||
ts: { icon: FileCode, color: "#3B82F6" },
|
||||
json: { icon: File, color: "#22C55E" },
|
||||
png: { icon: Image, color: "#22C55E" },
|
||||
jpg: { icon: Image, color: "#22C55E" },
|
||||
svg: { icon: Image, color: "#F97316" },
|
||||
mp4: { icon: Film, color: "#A855F7" },
|
||||
mp3: { icon: Music, color: "#F59E0B" },
|
||||
wav: { icon: Music, color: "#F59E0B" },
|
||||
};
|
||||
|
||||
function getFileIcon(path: string) {
|
||||
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
||||
return FILE_ICONS[ext] ?? { icon: File, color: "#737373" };
|
||||
}
|
||||
|
||||
export const FileTree = memo(function FileTree({ files, activeFile, onSelectFile }: FileTreeProps) {
|
||||
const sorted = [...files].sort((a, b) => {
|
||||
// index.html first, then alphabetical
|
||||
if (a === "index.html") return -1;
|
||||
if (b === "index.html") return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full min-h-0">
|
||||
<div className="px-2.5 py-1.5 border-b border-neutral-800 flex-shrink-0">
|
||||
<span className="text-2xs font-medium text-neutral-500 uppercase tracking-caps">Files</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto py-1">
|
||||
{sorted.map((path) => {
|
||||
const { icon: Icon, color } = getFileIcon(path);
|
||||
const isActive = path === activeFile;
|
||||
const name = path.split("/").pop() ?? path;
|
||||
const dir = path.includes("/") ? path.split("/").slice(0, -1).join("/") + "/" : "";
|
||||
|
||||
return (
|
||||
<button
|
||||
key={path}
|
||||
onClick={() => onSelectFile(path)}
|
||||
className={`w-full flex items-center gap-2 px-2.5 py-1 min-h-7 text-left transition-all duration-press text-xs ${
|
||||
isActive
|
||||
? "bg-neutral-800/60 text-neutral-200"
|
||||
: "text-neutral-500 hover:bg-neutral-800/30 hover:text-neutral-300 active:scale-[0.98]"
|
||||
}`}
|
||||
>
|
||||
<Icon size={12} style={{ color }} className="flex-shrink-0" />
|
||||
<span className="truncate">
|
||||
{dir && <span className="text-neutral-600">{dir}</span>}
|
||||
{name}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,209 @@
|
||||
import { memo } from "react";
|
||||
import { X, MousePointer, Move, Type, Palette, Clock, Eye } from "../../icons/SystemIcons";
|
||||
import { Button, IconButton } from "../ui";
|
||||
import type { PickedElement } from "../../hooks/useElementPicker";
|
||||
|
||||
interface PropertyPanelProps {
|
||||
element: PickedElement | null;
|
||||
isPickMode: boolean;
|
||||
onEnablePick: () => void;
|
||||
onDisablePick: () => void;
|
||||
onClearPick: () => void;
|
||||
onSetStyle: (prop: string, value: string) => void;
|
||||
onSetDataAttr: (attr: string, value: string) => void;
|
||||
onSetText?: (text: string) => void;
|
||||
}
|
||||
|
||||
function PropertyRow({ label, value, onChange }: { label: string; value: string; onChange: (v: string) => void }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xs text-neutral-600 w-16 flex-shrink-0 text-right">{label}</span>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="flex-1 bg-neutral-900 border border-neutral-800 rounded px-1.5 py-0.5 text-2xs text-neutral-200 font-mono outline-none focus:border-neutral-600 min-w-0"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ColorRow({ label, value, onChange }: { label: string; value: string; onChange: (v: string) => void }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xs text-neutral-600 w-16 flex-shrink-0 text-right">{label}</span>
|
||||
<div className="flex items-center gap-1 flex-1">
|
||||
<div className="w-5 h-5 rounded border border-neutral-700 flex-shrink-0" style={{ backgroundColor: value }} />
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="flex-1 bg-neutral-900 border border-neutral-800 rounded px-1.5 py-0.5 text-2xs text-neutral-200 font-mono outline-none focus:border-neutral-600 min-w-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionHeader({ icon: Icon, label }: { icon: typeof Move; label: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 mt-2 mb-1">
|
||||
<Icon size={10} className="text-neutral-600" />
|
||||
<span className="text-2xs font-medium text-neutral-500 uppercase tracking-wider">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const PropertyPanel = memo(function PropertyPanel({
|
||||
element,
|
||||
isPickMode,
|
||||
onEnablePick,
|
||||
onDisablePick,
|
||||
onClearPick,
|
||||
onSetStyle,
|
||||
onSetDataAttr,
|
||||
onSetText,
|
||||
}: PropertyPanelProps) {
|
||||
if (!element) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full px-4 text-center">
|
||||
<MousePointer size={20} className="text-neutral-700 mb-2" />
|
||||
<p className="text-xs text-neutral-500">Click an element in the preview to inspect it</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={isPickMode ? onDisablePick : onEnablePick}
|
||||
className={`mt-3 ${isPickMode ? "bg-blue-500/20 text-blue-400 border-blue-500/30" : ""}`}
|
||||
>
|
||||
{isPickMode ? "Pick mode active..." : "Enable Pick Mode"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const s = element.computedStyles;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full min-h-0">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b border-neutral-800 flex-shrink-0">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<span className="text-2xs font-mono text-blue-400 truncate">{element.selector}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<IconButton
|
||||
icon={<MousePointer size={11} />}
|
||||
aria-label={isPickMode ? "Disable pick mode" : "Enable pick mode"}
|
||||
size="sm"
|
||||
onClick={isPickMode ? onDisablePick : onEnablePick}
|
||||
className={isPickMode ? "text-blue-400 bg-blue-500/10" : ""}
|
||||
/>
|
||||
<IconButton icon={<X size={11} />} aria-label="Clear selection" size="sm" onClick={onClearPick} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Properties */}
|
||||
<div className="flex-1 overflow-y-auto px-3 py-2 space-y-1">
|
||||
{/* Element info */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-2xs text-neutral-300 font-medium">{element.label}</span>
|
||||
<span className="text-2xs text-neutral-600 font-mono"><{element.tagName}></span>
|
||||
</div>
|
||||
|
||||
{/* Position & Size */}
|
||||
<SectionHeader icon={Move} label="Position & Size" />
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<PropertyRow label="X" value={s["left"] ?? "auto"} onChange={(v) => onSetStyle("left", v)} />
|
||||
<PropertyRow label="Y" value={s["top"] ?? "auto"} onChange={(v) => onSetStyle("top", v)} />
|
||||
<PropertyRow label="W" value={s["width"] ?? "auto"} onChange={(v) => onSetStyle("width", v)} />
|
||||
<PropertyRow label="H" value={s["height"] ?? "auto"} onChange={(v) => onSetStyle("height", v)} />
|
||||
</div>
|
||||
|
||||
{/* Typography */}
|
||||
{(element.tagName === "div" ||
|
||||
element.tagName === "span" ||
|
||||
element.tagName === "p" ||
|
||||
element.tagName === "h1" ||
|
||||
element.tagName === "h2") && (
|
||||
<>
|
||||
<SectionHeader icon={Type} label="Typography" />
|
||||
<PropertyRow label="Size" value={s["font-size"] ?? ""} onChange={(v) => onSetStyle("font-size", v)} />
|
||||
<PropertyRow label="Weight" value={s["font-weight"] ?? ""} onChange={(v) => onSetStyle("font-weight", v)} />
|
||||
<PropertyRow
|
||||
label="Family"
|
||||
value={s["font-family"]?.split(",")[0] ?? ""}
|
||||
onChange={(v) => onSetStyle("font-family", v)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Colors */}
|
||||
<SectionHeader icon={Palette} label="Colors" />
|
||||
<ColorRow label="Color" value={s["color"] ?? "#fff"} onChange={(v) => onSetStyle("color", v)} />
|
||||
<ColorRow
|
||||
label="Background"
|
||||
value={s["background-color"] ?? "transparent"}
|
||||
onChange={(v) => onSetStyle("background-color", v)}
|
||||
/>
|
||||
|
||||
{/* Appearance */}
|
||||
<SectionHeader icon={Eye} label="Appearance" />
|
||||
<PropertyRow label="Opacity" value={s["opacity"] ?? "1"} onChange={(v) => onSetStyle("opacity", v)} />
|
||||
<PropertyRow
|
||||
label="Radius"
|
||||
value={s["border-radius"] ?? "0"}
|
||||
onChange={(v) => onSetStyle("border-radius", v)}
|
||||
/>
|
||||
<PropertyRow label="Z-index" value={s["z-index"] ?? "auto"} onChange={(v) => onSetStyle("z-index", v)} />
|
||||
<PropertyRow label="Transform" value={s["transform"] ?? "none"} onChange={(v) => onSetStyle("transform", v)} />
|
||||
|
||||
{/* Timing */}
|
||||
{(element.dataAttributes["start"] || element.dataAttributes["duration"]) && (
|
||||
<>
|
||||
<SectionHeader icon={Clock} label="Timing" />
|
||||
{element.dataAttributes["start"] != null && (
|
||||
<PropertyRow
|
||||
label="Start"
|
||||
value={element.dataAttributes["start"]}
|
||||
onChange={(v) => onSetDataAttr("start", v)}
|
||||
/>
|
||||
)}
|
||||
{element.dataAttributes["duration"] != null && (
|
||||
<PropertyRow
|
||||
label="Duration"
|
||||
value={element.dataAttributes["duration"]}
|
||||
onChange={(v) => onSetDataAttr("duration", v)}
|
||||
/>
|
||||
)}
|
||||
{element.dataAttributes["track-index"] != null && (
|
||||
<PropertyRow
|
||||
label="Track"
|
||||
value={element.dataAttributes["track-index"]}
|
||||
onChange={(v) => onSetDataAttr("track-index", v)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Editable text content */}
|
||||
{element.textContent && (
|
||||
<>
|
||||
<SectionHeader icon={Type} label="Text Content" />
|
||||
<textarea
|
||||
defaultValue={element.textContent}
|
||||
onBlur={(e) => onSetText?.(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && e.metaKey) {
|
||||
(e.target as HTMLTextAreaElement).blur();
|
||||
}
|
||||
}}
|
||||
rows={3}
|
||||
className="w-full bg-neutral-900 border border-neutral-800 rounded px-2 py-1.5 text-xs text-neutral-200 outline-none focus:border-neutral-600 resize-y leading-relaxed"
|
||||
placeholder="Edit text..."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useRef, useCallback, memo } from "react";
|
||||
import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter } from "@codemirror/view";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
||||
import { bracketMatching, foldGutter, indentOnInput } from "@codemirror/language";
|
||||
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
|
||||
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
|
||||
import { oneDark } from "@codemirror/theme-one-dark";
|
||||
import { html } from "@codemirror/lang-html";
|
||||
import { css } from "@codemirror/lang-css";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
|
||||
function getLanguageExtension(language: string) {
|
||||
switch (language) {
|
||||
case "html":
|
||||
return html();
|
||||
case "css":
|
||||
return css();
|
||||
case "javascript":
|
||||
case "js":
|
||||
case "typescript":
|
||||
case "ts":
|
||||
return javascript({ typescript: language === "typescript" || language === "ts" });
|
||||
default:
|
||||
return html();
|
||||
}
|
||||
}
|
||||
|
||||
function detectLanguage(filePath: string): string {
|
||||
const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
|
||||
const map: Record<string, string> = {
|
||||
html: "html",
|
||||
htm: "html",
|
||||
css: "css",
|
||||
js: "javascript",
|
||||
ts: "typescript",
|
||||
jsx: "javascript",
|
||||
tsx: "typescript",
|
||||
json: "javascript",
|
||||
};
|
||||
return map[ext] ?? "html";
|
||||
}
|
||||
|
||||
interface SourceEditorProps {
|
||||
content: string;
|
||||
filePath?: string;
|
||||
language?: string;
|
||||
onChange?: (content: string) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export const SourceEditor = memo(function SourceEditor({
|
||||
content,
|
||||
filePath,
|
||||
language,
|
||||
onChange,
|
||||
readOnly = false,
|
||||
}: SourceEditorProps) {
|
||||
const editorRef = useRef<EditorView | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const onChangeRef = useRef(onChange);
|
||||
onChangeRef.current = onChange;
|
||||
|
||||
const mountEditor = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.destroy();
|
||||
editorRef.current = null;
|
||||
}
|
||||
if (!node) return;
|
||||
containerRef.current = node;
|
||||
|
||||
const lang = language ?? (filePath ? detectLanguage(filePath) : "html");
|
||||
|
||||
const updateListener = EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged && onChangeRef.current) {
|
||||
onChangeRef.current(update.state.doc.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: content,
|
||||
extensions: [
|
||||
lineNumbers(),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
history(),
|
||||
foldGutter(),
|
||||
indentOnInput(),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
highlightSelectionMatches(),
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
]),
|
||||
getLanguageExtension(lang),
|
||||
oneDark,
|
||||
updateListener,
|
||||
EditorState.readOnly.of(readOnly),
|
||||
EditorView.theme({
|
||||
"&": { height: "100%" },
|
||||
".cm-scroller": { overflow: "auto" },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
editorRef.current = new EditorView({ state, parent: node });
|
||||
},
|
||||
[content, filePath, language, readOnly],
|
||||
);
|
||||
|
||||
return <div ref={mountEditor} className="h-full w-full overflow-hidden" />;
|
||||
});
|
||||
Reference in New Issue
Block a user