Skip to content

Commit 099e125

Browse files
Merge pull request #994 from heygen-com/worktree-feat-timeline-ui
feat(studio): timeline UI overhaul — flat clips, thumbnails, visual cleanup
2 parents be9b61a + 8c3dfb8 commit 099e125

9 files changed

Lines changed: 191 additions & 189 deletions

File tree

packages/cli/src/server/studioServer.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,13 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
349349
}, opts.seekTime);
350350
const manifestContent = readStudioManualEditManifestContent(opts.project.dir);
351351
await applyStudioManualEditsToThumbnailPage(page, manifestContent, opts.compPath);
352-
await page.evaluate(() => document.fonts?.ready);
352+
await page.evaluate(() => {
353+
void document.fonts?.ready;
354+
const body = document.body;
355+
if (body && getComputedStyle(body).backgroundColor === "rgba(0, 0, 0, 0)") {
356+
body.style.backgroundColor = "#1c2028";
357+
}
358+
});
353359
await new Promise((r) => setTimeout(r, 200));
354360
await reapplyStudioManualEditsToThumbnailPage(page);
355361
let clip: ScreenshotClip | undefined;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, expect, it } from "vitest";
2+
import { normalizeCompositionSrc } from "./useRenderClipContent";
3+
4+
describe("normalizeCompositionSrc", () => {
5+
const origin = "http://localhost:5190";
6+
const pid = "my-project";
7+
8+
it("strips absolute preview URL to relative path", () => {
9+
const result = normalizeCompositionSrc(
10+
"http://localhost:5190/api/projects/my-project/preview/compositions/intro.html",
11+
pid,
12+
origin,
13+
);
14+
expect(result).toBe("compositions/intro.html");
15+
});
16+
17+
it("preserves already-relative paths", () => {
18+
const result = normalizeCompositionSrc("compositions/intro.html", pid, origin);
19+
expect(result).toBe("compositions/intro.html");
20+
});
21+
22+
it("preserves absolute URLs from different origins", () => {
23+
const result = normalizeCompositionSrc(
24+
"https://cdn.example.com/compositions/intro.html",
25+
pid,
26+
origin,
27+
);
28+
expect(result).toBe("https://cdn.example.com/compositions/intro.html");
29+
});
30+
31+
it("preserves absolute URLs for different projects", () => {
32+
const result = normalizeCompositionSrc(
33+
"http://localhost:5190/api/projects/other-project/preview/compositions/intro.html",
34+
pid,
35+
origin,
36+
);
37+
expect(result).toBe(
38+
"http://localhost:5190/api/projects/other-project/preview/compositions/intro.html",
39+
);
40+
});
41+
42+
it("handles nested composition paths", () => {
43+
const result = normalizeCompositionSrc(
44+
"http://localhost:5190/api/projects/my-project/preview/compositions/scenes/hero.html",
45+
pid,
46+
origin,
47+
);
48+
expect(result).toBe("compositions/scenes/hero.html");
49+
});
50+
});

packages/studio/src/hooks/useRenderClipContent.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ import type { TimelineElement } from "../player";
55
import { AudioWaveform } from "../player/components/AudioWaveform";
66
import { getTimelineElementLabel } from "../utils/studioHelpers";
77

8+
export function normalizeCompositionSrc(
9+
compSrc: string,
10+
projectId: string,
11+
origin: string,
12+
): string {
13+
try {
14+
const parsed = new URL(compSrc, origin);
15+
const previewPrefix = `/api/projects/${projectId}/preview/`;
16+
if (parsed.pathname.startsWith(previewPrefix)) {
17+
return parsed.pathname.slice(previewPrefix.length);
18+
}
19+
} catch {
20+
// already relative
21+
}
22+
return compSrc;
23+
}
24+
825
interface UseRenderClipContentOptions {
926
projectIdRef: { current: string | null };
1027
compIdToSrc: Map<string, string>;
@@ -23,8 +40,10 @@ export function useRenderClipContent({
2340
const pid = projectIdRef.current;
2441
if (!pid) return null;
2542

26-
// Resolve composition source path using the compIdToSrc map
2743
let compSrc = el.compositionSrc;
44+
if (compSrc) {
45+
compSrc = normalizeCompositionSrc(compSrc, pid, window.location.origin);
46+
}
2847
if (compSrc && compIdToSrc.size > 0) {
2948
const resolved =
3049
compIdToSrc.get(el.id) ||
@@ -40,7 +59,7 @@ export function useRenderClipContent({
4059
previewUrl: `/api/projects/${pid}/preview/comp/${compSrc}`,
4160
label: getTimelineElementLabel(el),
4261
labelColor: style.label,
43-
accentColor: style.clip,
62+
4463
seekTime: 0,
4564
duration: el.duration,
4665
});
@@ -53,7 +72,7 @@ export function useRenderClipContent({
5372
previewUrl: activePreviewUrl,
5473
label: getTimelineElementLabel(el),
5574
labelColor: style.label,
56-
accentColor: style.clip,
75+
5776
selector: el.selector,
5877
selectorIndex: el.selectorIndex,
5978
seekTime: el.start,
@@ -109,7 +128,7 @@ export function useRenderClipContent({
109128
previewUrl: `/api/projects/${pid}/preview`,
110129
label: getTimelineElementLabel(el),
111130
labelColor: style.label,
112-
accentColor: style.clip,
131+
113132
selector: el.selector,
114133
selectorIndex: el.selectorIndex,
115134
seekTime: el.start,

packages/studio/src/player/components/CompositionThumbnail.tsx

Lines changed: 10 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ interface CompositionThumbnailProps {
55
previewUrl: string;
66
label: string;
77
labelColor: string;
8-
accentColor?: string;
98
selector?: string;
109
selectorIndex?: number;
1110
seekTime?: number;
@@ -16,7 +15,6 @@ interface CompositionThumbnailProps {
1615

1716
const CLIP_HEIGHT = 66;
1817
const THUMBNAIL_URL_VERSION = "v3";
19-
const COMPOSITION_THUMBNAIL_LABEL_Z_INDEX = 10;
2018

2119
export function buildCompositionThumbnailUrl({
2220
previewUrl,
@@ -53,7 +51,6 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
5351
previewUrl,
5452
label,
5553
labelColor,
56-
accentColor = "#6B7280",
5754
selector,
5855
selectorIndex,
5956
seekTime = 2,
@@ -110,8 +107,11 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
110107
className="hidden"
111108
/>
112109

113-
{loaded ? (
114-
<div className="absolute inset-0 flex">
110+
{loaded && (
111+
<div
112+
className="absolute inset-0 flex"
113+
style={{ animation: "hf-thumb-fade 200ms ease-out", mixBlendMode: "lighten" }}
114+
>
115115
{Array.from({ length: frameCount }).map((_, i) => (
116116
<div
117117
key={i}
@@ -122,59 +122,25 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
122122
src={url}
123123
alt=""
124124
draggable={false}
125-
className="absolute inset-0 h-full w-full object-cover opacity-60"
125+
className="absolute inset-0 h-full w-full object-cover"
126+
style={{ opacity: 0.7 }}
126127
/>
127128
</div>
128129
))}
129130
</div>
130-
) : (
131-
<div
132-
className="absolute inset-0 animate-pulse"
133-
style={{
134-
background:
135-
"linear-gradient(90deg, rgba(255,255,255,0.02) 0%, rgba(255,255,255,0.05) 50%, rgba(255,255,255,0.02) 100%)",
136-
}}
137-
/>
138131
)}
139132

140-
<div
141-
className="absolute inset-0"
142-
style={{
143-
background: `linear-gradient(120deg, ${accentColor}2e, transparent 34%), linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.08))`,
144-
}}
145-
/>
146-
147-
<div
148-
className="absolute left-2 top-2"
149-
style={{ zIndex: COMPOSITION_THUMBNAIL_LABEL_Z_INDEX }}
150-
>
133+
<div className="absolute left-3 top-0 bottom-0 flex items-center" style={{ zIndex: 10 }}>
151134
<span
152-
className="block max-w-full truncate rounded-md px-1.5 py-0.5 text-[9px] font-semibold uppercase leading-none"
135+
className="block max-w-full truncate text-[10px] font-semibold leading-none"
153136
style={{
154137
color: labelColor,
155-
background: `${accentColor}2e`,
156-
boxShadow: `inset 0 0 0 1px ${accentColor}40`,
138+
textShadow: loaded ? "0 1px 4px rgba(0,0,0,0.9), 0 0 8px rgba(0,0,0,0.6)" : "none",
157139
}}
158140
>
159141
{label}
160142
</span>
161143
</div>
162-
163-
<div
164-
className="absolute bottom-0 left-0 right-0 px-1.5 pb-0.5 pt-3"
165-
style={{
166-
zIndex: COMPOSITION_THUMBNAIL_LABEL_Z_INDEX,
167-
background:
168-
"linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.4) 60%, transparent 100%)",
169-
}}
170-
>
171-
<span
172-
className="block truncate text-[9px] font-semibold leading-tight"
173-
style={{ color: labelColor, textShadow: "0 1px 2px rgba(0,0,0,0.9)" }}
174-
>
175-
{label}
176-
</span>
177-
</div>
178144
</div>
179145
);
180146
});

packages/studio/src/player/components/TimelineCanvas.tsx

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { getRenderedTimelineElement, type TimelineTheme } from "./timelineTheme"
1010
import { GUTTER, TRACK_H, RULER_H, CLIP_Y, CLIP_HANDLE_W } from "./timelineLayout";
1111
import type { TimelineElement } from "../store/playerStore";
1212
import type { DraggedClipState, ResizingClipState, BlockedClipState } from "./useTimelineClipDrag";
13-
import { formatTime } from "../lib/time";
1413
import type { TrackVisualStyle } from "./timelineIcons";
1514

1615
interface TimelineCanvasProps {
@@ -134,28 +133,16 @@ export const TimelineCanvas = memo(function TimelineCanvas({
134133
className={
135134
renderClipContent
136135
? "absolute inset-0 overflow-hidden"
137-
: "flex flex-col justify-center overflow-hidden flex-1 min-w-0 px-6"
136+
: "flex items-center overflow-hidden flex-1 min-w-0 px-3 gap-2"
138137
}
139138
>
140139
{renderClipContent?.(element, clipStyle) ?? (
141-
<div className="flex h-full min-h-0 flex-col justify-between py-3">
142-
<span
143-
className="max-w-full truncate rounded-md px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.08em] leading-none"
144-
style={{
145-
color: clipStyle.label,
146-
background: `${clipStyle.accent}26`,
147-
boxShadow: `inset 0 0 0 1px ${clipStyle.accent}33`,
148-
}}
149-
>
150-
{element.tag}
151-
</span>
152-
<span
153-
className="max-w-full truncate rounded-md px-1.5 py-0.5 text-[10px] font-medium tabular-nums leading-none"
154-
style={{ color: theme.textSecondary, background: "rgba(255,255,255,0.04)" }}
155-
>
156-
{formatTime(element.start)} {"→"} {formatTime(element.start + element.duration)}
157-
</span>
158-
</div>
140+
<span
141+
className="truncate text-[10px] font-medium leading-none"
142+
style={{ color: clipStyle.label }}
143+
>
144+
{element.label || element.id || element.tag}
145+
</span>
159146
)}
160147
</div>
161148
</>
@@ -221,10 +208,9 @@ export const TimelineCanvas = memo(function TimelineCanvas({
221208
paddingLeft: 16,
222209
color: ts.label,
223210
fontSize: 11,
224-
letterSpacing: "0.08em",
211+
letterSpacing: "0.06em",
225212
textTransform: "uppercase",
226-
background: `linear-gradient(90deg, ${ts.accent}14, transparent 28%)`,
227-
boxShadow: `inset 0 0 0 1px ${ts.accent}24`,
213+
opacity: 0.5,
228214
}}
229215
>
230216
New track

0 commit comments

Comments
 (0)