mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
style: apply oxfmt baseline formatting across all source files (#25)
## Summary - Run `oxfmt .` across the entire codebase to establish formatted baseline - 299 files changed — mechanical formatting only, no logic changes - Double quotes, semicolons, 2-space indent, trailing commas, 100 print width Part 3/4 of [VA-851](https://linear.app/heygen/issue/VA-851/pre-migration-configure-eslint-prettier-and-conventional-commits) ## Test plan - [x] `pnpm format:check` — all 426 files pass - [x] `pnpm -r typecheck` — all packages pass - [x] `pnpm build` — all packages build - [x] All 348 tests pass
This commit is contained in:
@@ -18,33 +18,31 @@ interface AgentActivityTrackProps {
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export const AgentActivityTrack = memo(function AgentActivityTrack({ agents, duration }: AgentActivityTrackProps) {
|
||||
export const AgentActivityTrack = memo(function AgentActivityTrack({
|
||||
agents,
|
||||
duration,
|
||||
}: AgentActivityTrackProps) {
|
||||
if (agents.length === 0 || duration <= 0) return null;
|
||||
|
||||
return (
|
||||
<div className="border-t border-neutral-800/30">
|
||||
{/* Section header */}
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 text-[9px] text-neutral-600 font-medium uppercase tracking-wider">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="4" /></svg>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
</svg>
|
||||
Agent Activity
|
||||
</div>
|
||||
|
||||
{agents.map((agent) => (
|
||||
<div
|
||||
key={agent.agentId}
|
||||
className="relative flex"
|
||||
style={{ height: TRACK_H }}
|
||||
>
|
||||
<div key={agent.agentId} className="relative flex" style={{ height: TRACK_H }}>
|
||||
{/* Gutter: agent name */}
|
||||
<div
|
||||
className="flex-shrink-0 flex items-center justify-center"
|
||||
style={{ width: GUTTER }}
|
||||
title={agent.name}
|
||||
>
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: agent.color }}
|
||||
/>
|
||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: agent.color }} />
|
||||
</div>
|
||||
|
||||
{/* Lane */}
|
||||
@@ -77,10 +75,7 @@ export const AgentActivityTrack = memo(function AgentActivityTrack({ agents, dur
|
||||
style={{ left: `${leftPct}%` }}
|
||||
>
|
||||
{event.type === "create" ? (
|
||||
<div
|
||||
className="w-2 h-2 rotate-45"
|
||||
style={{ backgroundColor: agent.color }}
|
||||
/>
|
||||
<div className="w-2 h-2 rotate-45" style={{ backgroundColor: agent.color }} />
|
||||
) : (
|
||||
<div
|
||||
className="w-1.5 h-1.5 rounded-full"
|
||||
|
||||
@@ -11,110 +11,120 @@ interface PlayerProps {
|
||||
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);
|
||||
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));
|
||||
}, []);
|
||||
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);
|
||||
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?.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 };
|
||||
// Listen for stage-size messages from the runtime
|
||||
const handleMessage = (e: MessageEvent) => {
|
||||
const data = e.data;
|
||||
if (
|
||||
(data?.source === "hf-preview" || 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);
|
||||
// Recalc scale with new dims
|
||||
const el = containerRef.current;
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
setScale(Math.min(rect.width / dw, rect.height / dh));
|
||||
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 {
|
||||
// Cross-origin
|
||||
}
|
||||
} catch {
|
||||
// Cross-origin
|
||||
}
|
||||
|
||||
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 });
|
||||
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]);
|
||||
onLoad();
|
||||
}, [onLoad, ref]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="w-full h-full max-w-full max-h-full overflow-hidden shadow-float border border-neutral-800 bg-black flex items-center justify-center rounded-card-inner"
|
||||
>
|
||||
<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",
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: "center center",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="w-full h-full max-w-full max-h-full overflow-hidden shadow-float border border-neutral-800 bg-black flex items-center justify-center rounded-card-inner"
|
||||
>
|
||||
<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",
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: "center center",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Player.displayName = "Player";
|
||||
|
||||
@@ -16,7 +16,11 @@ interface PlayerControlsProps {
|
||||
onSeek: (time: number) => void;
|
||||
}
|
||||
|
||||
export const PlayerControls = memo(function PlayerControls({ onTogglePlay, onSeek, ...overrides }: PlayerControlsProps) {
|
||||
export const PlayerControls = memo(function PlayerControls({
|
||||
onTogglePlay,
|
||||
onSeek,
|
||||
...overrides
|
||||
}: PlayerControlsProps) {
|
||||
// Subscribe to only the fields we render — each selector prevents cascading re-renders
|
||||
const storeIsPlaying = usePlayerStore((s) => s.isPlaying);
|
||||
const storeDuration = usePlayerStore((s) => s.duration);
|
||||
@@ -165,9 +169,14 @@ export const PlayerControls = memo(function PlayerControls({ onTogglePlay, onSee
|
||||
{SPEED_OPTIONS.map((rate) => (
|
||||
<button
|
||||
key={rate}
|
||||
onClick={() => { setPlaybackRate(rate); setShowSpeedMenu(false); }}
|
||||
onClick={() => {
|
||||
setPlaybackRate(rate);
|
||||
setShowSpeedMenu(false);
|
||||
}}
|
||||
className={`block w-full px-3 py-1 text-xs text-left font-mono tabular-nums transition-colors ${
|
||||
rate === playbackRate ? "text-white bg-neutral-800" : "text-neutral-400 hover:text-white hover:bg-neutral-800"
|
||||
rate === playbackRate
|
||||
? "text-white bg-neutral-800"
|
||||
: "text-neutral-400 hover:text-white hover:bg-neutral-800"
|
||||
}`}
|
||||
>
|
||||
{rate}x
|
||||
|
||||
@@ -36,7 +36,6 @@ export function PreviewPanel({
|
||||
renderStatus,
|
||||
children,
|
||||
}: PreviewPanelProps) {
|
||||
|
||||
const renderState = renderStatus?.state ?? "idle";
|
||||
|
||||
return (
|
||||
@@ -52,71 +51,102 @@ export function PreviewPanel({
|
||||
{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
|
||||
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}
|
||||
/>
|
||||
<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" }}>
|
||||
<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}%` }}
|
||||
/>
|
||||
{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>
|
||||
<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"
|
||||
)}
|
||||
{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"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<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}
|
||||
@@ -140,7 +170,9 @@ export function PreviewPanel({
|
||||
</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>
|
||||
<p className="text-xs text-neutral-700 mt-1">
|
||||
Send a message to generate a video composition
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -25,7 +25,16 @@ interface TrackStyle {
|
||||
/* ── Icons from Figma HyperFrames design system ── */
|
||||
const ICON_BASE = "/icons/timeline";
|
||||
function TimelineIcon({ src }: { src: string }) {
|
||||
return <img src={src} alt="" width={12} height={12} style={{ filter: "brightness(0) invert(1)" }} draggable={false} />;
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
width={12}
|
||||
height={12}
|
||||
style={{ filter: "brightness(0) invert(1)" }}
|
||||
draggable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const IconCaptions = <TimelineIcon src={`${ICON_BASE}/captions.svg`} />;
|
||||
const IconImage = <TimelineIcon src={`${ICON_BASE}/image.svg`} />;
|
||||
@@ -125,7 +134,9 @@ function generateTicks(duration: number): { major: number[]; minor: number[] } {
|
||||
const minor: number[] = [];
|
||||
for (let t = 0; t <= duration + 0.001; t += minorInterval) {
|
||||
const rounded = Math.round(t * 100) / 100;
|
||||
const isMajor = Math.abs(rounded % majorInterval) < 0.01 || Math.abs(rounded % majorInterval - majorInterval) < 0.01;
|
||||
const isMajor =
|
||||
Math.abs(rounded % majorInterval) < 0.01 ||
|
||||
Math.abs((rounded % majorInterval) - majorInterval) < 0.01;
|
||||
if (isMajor) major.push(rounded);
|
||||
else minor.push(rounded);
|
||||
}
|
||||
@@ -198,10 +209,14 @@ export const Timeline = memo(function Timeline({ onSeek, onDrillDown }: Timeline
|
||||
[seekFromX],
|
||||
);
|
||||
const handlePointerMove = useCallback(
|
||||
(e: React.PointerEvent) => { if (isDragging.current) seekFromX(e.clientX); },
|
||||
(e: React.PointerEvent) => {
|
||||
if (isDragging.current) seekFromX(e.clientX);
|
||||
},
|
||||
[seekFromX],
|
||||
);
|
||||
const handlePointerUp = useCallback(() => { isDragging.current = false; }, []);
|
||||
const handlePointerUp = useCallback(() => {
|
||||
isDragging.current = false;
|
||||
}, []);
|
||||
|
||||
const tracks = useMemo(() => {
|
||||
const map = new Map<number, typeof elements>();
|
||||
@@ -226,7 +241,11 @@ export const Timeline = memo(function Timeline({ onSeek, onDrillDown }: Timeline
|
||||
|
||||
if (!timelineReady) return null;
|
||||
if (elements.length === 0) {
|
||||
return <div className="px-3 py-3 text-2xs text-neutral-600 border-t border-neutral-800/50">No timeline elements</div>;
|
||||
return (
|
||||
<div className="px-3 py-3 text-2xs text-neutral-600 border-t border-neutral-800/50">
|
||||
No timeline elements
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const totalH = RULER_H + tracks.length * TRACK_H;
|
||||
@@ -243,22 +262,48 @@ export const Timeline = memo(function Timeline({ onSeek, onDrillDown }: Timeline
|
||||
>
|
||||
<div className="relative" style={{ height: totalH }}>
|
||||
{/* Grid lines */}
|
||||
<svg className="absolute pointer-events-none" style={{ left: GUTTER }} width={`calc(100% - ${GUTTER}px)`} height={totalH}>
|
||||
<svg
|
||||
className="absolute pointer-events-none"
|
||||
style={{ left: GUTTER }}
|
||||
width={`calc(100% - ${GUTTER}px)`}
|
||||
height={totalH}
|
||||
>
|
||||
{major.map((t) => (
|
||||
<line key={`g-${t}`} x1={`${(t / duration) * 100}%`} y1={RULER_H} x2={`${(t / duration) * 100}%`} y2={totalH} stroke="rgba(255,255,255,0.035)" strokeWidth="1" />
|
||||
<line
|
||||
key={`g-${t}`}
|
||||
x1={`${(t / duration) * 100}%`}
|
||||
y1={RULER_H}
|
||||
x2={`${(t / duration) * 100}%`}
|
||||
y2={totalH}
|
||||
stroke="rgba(255,255,255,0.035)"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
|
||||
{/* Ruler */}
|
||||
<div className="relative border-b border-neutral-800/40" style={{ height: RULER_H, marginLeft: GUTTER }}>
|
||||
<div
|
||||
className="relative border-b border-neutral-800/40"
|
||||
style={{ height: RULER_H, marginLeft: GUTTER }}
|
||||
>
|
||||
{minor.map((t) => (
|
||||
<div key={`m-${t}`} className="absolute bottom-0" style={{ left: `${(t / duration) * 100}%` }}>
|
||||
<div
|
||||
key={`m-${t}`}
|
||||
className="absolute bottom-0"
|
||||
style={{ left: `${(t / duration) * 100}%` }}
|
||||
>
|
||||
<div className="w-px h-[3px] bg-neutral-700/40" />
|
||||
</div>
|
||||
))}
|
||||
{major.map((t) => (
|
||||
<div key={`M-${t}`} className="absolute bottom-0 flex flex-col items-center" style={{ left: `${(t / duration) * 100}%` }}>
|
||||
<span className="text-[9px] text-neutral-500 font-mono tabular-nums leading-none mb-0.5">{formatTick(t)}</span>
|
||||
<div
|
||||
key={`M-${t}`}
|
||||
className="absolute bottom-0 flex flex-col items-center"
|
||||
style={{ left: `${(t / duration) * 100}%` }}
|
||||
>
|
||||
<span className="text-[9px] text-neutral-500 font-mono tabular-nums leading-none mb-0.5">
|
||||
{formatTick(t)}
|
||||
</span>
|
||||
<div className="w-px h-[5px] bg-neutral-600/60" />
|
||||
</div>
|
||||
))}
|
||||
@@ -268,9 +313,16 @@ export const Timeline = memo(function Timeline({ onSeek, onDrillDown }: Timeline
|
||||
{tracks.map(([trackNum, els]) => {
|
||||
const ts = trackStyles.get(trackNum) ?? DEFAULT;
|
||||
return (
|
||||
<div key={trackNum} className="relative flex" style={{ height: TRACK_H, backgroundColor: ts.row }}>
|
||||
<div
|
||||
key={trackNum}
|
||||
className="relative flex"
|
||||
style={{ height: TRACK_H, backgroundColor: ts.row }}
|
||||
>
|
||||
{/* Gutter: colored icon badge (Figma HyperFrames style) */}
|
||||
<div className="flex-shrink-0 flex items-center justify-center" style={{ width: GUTTER }}>
|
||||
<div
|
||||
className="flex-shrink-0 flex items-center justify-center"
|
||||
style={{ width: GUTTER }}
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-center"
|
||||
style={{
|
||||
@@ -314,7 +366,9 @@ export const Timeline = memo(function Timeline({ onSeek, onDrillDown }: Timeline
|
||||
backgroundImage: isComposition
|
||||
? `repeating-linear-gradient(135deg, transparent, transparent 3px, rgba(255,255,255,0.08) 3px, rgba(255,255,255,0.08) 6px)`
|
||||
: undefined,
|
||||
border: isSelected ? `2px solid rgba(255,255,255,0.9)` : `1px solid rgba(255,255,255,${isHovered ? 0.3 : 0.15})`,
|
||||
border: isSelected
|
||||
? `2px solid rgba(255,255,255,0.9)`
|
||||
: `1px solid rgba(255,255,255,${isHovered ? 0.3 : 0.15})`,
|
||||
boxShadow: isSelected
|
||||
? `0 0 0 1px ${style.clip}, 0 2px 8px rgba(0,0,0,0.4)`
|
||||
: isBeingEdited
|
||||
@@ -327,9 +381,11 @@ export const Timeline = memo(function Timeline({ onSeek, onDrillDown }: Timeline
|
||||
transform: isHovered && !isSelected ? "scaleY(1.04)" : "scaleY(1)",
|
||||
zIndex: isSelected ? 10 : isHovered ? 5 : 1,
|
||||
}}
|
||||
title={isComposition
|
||||
? `${el.compositionSrc} \u2022 Double-click to open`
|
||||
: `${el.id || el.tag} \u2022 ${el.start.toFixed(1)}s \u2013 ${(el.start + el.duration).toFixed(1)}s`}
|
||||
title={
|
||||
isComposition
|
||||
? `${el.compositionSrc} \u2022 Double-click to open`
|
||||
: `${el.id || el.tag} \u2022 ${el.start.toFixed(1)}s \u2013 ${(el.start + el.duration).toFixed(1)}s`
|
||||
}
|
||||
onPointerEnter={() => setHoveredClip(clipKey)}
|
||||
onPointerLeave={() => setHoveredClip(null)}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
@@ -370,8 +426,19 @@ export const Timeline = memo(function Timeline({ onSeek, onDrillDown }: Timeline
|
||||
}}
|
||||
>
|
||||
{/* Mini cursor arrow */}
|
||||
<svg width="8" height="10" viewBox="0 0 12 16" fill="none" style={{ flexShrink: 0 }}>
|
||||
<path d="M1 1L11 7L6 8L4 14L1 1Z" fill={activeEdit.agentColor} stroke="white" strokeWidth="0.8" />
|
||||
<svg
|
||||
width="8"
|
||||
height="10"
|
||||
viewBox="0 0 12 16"
|
||||
fill="none"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<path
|
||||
d="M1 1L11 7L6 8L4 14L1 1Z"
|
||||
fill={activeEdit.agentColor}
|
||||
stroke="white"
|
||||
strokeWidth="0.8"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="text-[8px] font-semibold px-1 py-px rounded whitespace-nowrap"
|
||||
@@ -416,13 +483,15 @@ export const Timeline = memo(function Timeline({ onSeek, onDrillDown }: Timeline
|
||||
>
|
||||
<div className="absolute top-0 bottom-0 left-1/2 -translate-x-1/2 w-px bg-white/90" />
|
||||
<div className="absolute left-1/2 -translate-x-1/2" style={{ top: 0 }}>
|
||||
<div style={{
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft: "5px solid transparent",
|
||||
borderRight: "5px solid transparent",
|
||||
borderTop: "7px solid rgba(255,255,255,0.95)",
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft: "5px solid transparent",
|
||||
borderRight: "5px solid transparent",
|
||||
borderTop: "7px solid rgba(255,255,255,0.95)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user