Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/cli/src/server/studioServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,13 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
}, opts.seekTime);
const manifestContent = readStudioManualEditManifestContent(opts.project.dir);
await applyStudioManualEditsToThumbnailPage(page, manifestContent, opts.compPath);
await page.evaluate(() => document.fonts?.ready);
await page.evaluate(() => {
void document.fonts?.ready;
const body = document.body;
if (body && getComputedStyle(body).backgroundColor === "rgba(0, 0, 0, 0)") {
body.style.backgroundColor = "#1c2028";
}
});
await new Promise((r) => setTimeout(r, 200));
await reapplyStudioManualEditsToThumbnailPage(page);
let clip: ScreenshotClip | undefined;
Expand Down
50 changes: 50 additions & 0 deletions packages/studio/src/hooks/useRenderClipContent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";
import { normalizeCompositionSrc } from "./useRenderClipContent";

describe("normalizeCompositionSrc", () => {
const origin = "http://localhost:5190";
const pid = "my-project";

it("strips absolute preview URL to relative path", () => {
const result = normalizeCompositionSrc(
"http://localhost:5190/api/projects/my-project/preview/compositions/intro.html",
pid,
origin,
);
expect(result).toBe("compositions/intro.html");
});

it("preserves already-relative paths", () => {
const result = normalizeCompositionSrc("compositions/intro.html", pid, origin);
expect(result).toBe("compositions/intro.html");
});

it("preserves absolute URLs from different origins", () => {
const result = normalizeCompositionSrc(
"https://cdn.example.com/compositions/intro.html",
pid,
origin,
);
expect(result).toBe("https://cdn.example.com/compositions/intro.html");
});

it("preserves absolute URLs for different projects", () => {
const result = normalizeCompositionSrc(
"http://localhost:5190/api/projects/other-project/preview/compositions/intro.html",
pid,
origin,
);
expect(result).toBe(
"http://localhost:5190/api/projects/other-project/preview/compositions/intro.html",
);
});

it("handles nested composition paths", () => {
const result = normalizeCompositionSrc(
"http://localhost:5190/api/projects/my-project/preview/compositions/scenes/hero.html",
pid,
origin,
);
expect(result).toBe("compositions/scenes/hero.html");
});
});
27 changes: 23 additions & 4 deletions packages/studio/src/hooks/useRenderClipContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ import type { TimelineElement } from "../player";
import { AudioWaveform } from "../player/components/AudioWaveform";
import { getTimelineElementLabel } from "../utils/studioHelpers";

export function normalizeCompositionSrc(
compSrc: string,
projectId: string,
origin: string,
): string {
try {
const parsed = new URL(compSrc, origin);
const previewPrefix = `/api/projects/${projectId}/preview/`;
if (parsed.pathname.startsWith(previewPrefix)) {
return parsed.pathname.slice(previewPrefix.length);
}
} catch {
// already relative
}
return compSrc;
}

interface UseRenderClipContentOptions {
projectIdRef: { current: string | null };
compIdToSrc: Map<string, string>;
Expand All @@ -23,8 +40,10 @@ export function useRenderClipContent({
const pid = projectIdRef.current;
if (!pid) return null;

// Resolve composition source path using the compIdToSrc map
let compSrc = el.compositionSrc;
if (compSrc) {
compSrc = normalizeCompositionSrc(compSrc, pid, window.location.origin);
}
if (compSrc && compIdToSrc.size > 0) {
const resolved =
compIdToSrc.get(el.id) ||
Expand All @@ -40,7 +59,7 @@ export function useRenderClipContent({
previewUrl: `/api/projects/${pid}/preview/comp/${compSrc}`,
label: getTimelineElementLabel(el),
labelColor: style.label,
accentColor: style.clip,

seekTime: 0,
duration: el.duration,
});
Expand All @@ -53,7 +72,7 @@ export function useRenderClipContent({
previewUrl: activePreviewUrl,
label: getTimelineElementLabel(el),
labelColor: style.label,
accentColor: style.clip,

selector: el.selector,
selectorIndex: el.selectorIndex,
seekTime: el.start,
Expand Down Expand Up @@ -109,7 +128,7 @@ export function useRenderClipContent({
previewUrl: `/api/projects/${pid}/preview`,
label: getTimelineElementLabel(el),
labelColor: style.label,
accentColor: style.clip,

selector: el.selector,
selectorIndex: el.selectorIndex,
seekTime: el.start,
Expand Down
54 changes: 10 additions & 44 deletions packages/studio/src/player/components/CompositionThumbnail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ interface CompositionThumbnailProps {
previewUrl: string;
label: string;
labelColor: string;
accentColor?: string;
selector?: string;
selectorIndex?: number;
seekTime?: number;
Expand All @@ -16,7 +15,6 @@ interface CompositionThumbnailProps {

const CLIP_HEIGHT = 66;
const THUMBNAIL_URL_VERSION = "v3";
const COMPOSITION_THUMBNAIL_LABEL_Z_INDEX = 10;

export function buildCompositionThumbnailUrl({
previewUrl,
Expand Down Expand Up @@ -53,7 +51,6 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
previewUrl,
label,
labelColor,
accentColor = "#6B7280",
selector,
selectorIndex,
seekTime = 2,
Expand Down Expand Up @@ -110,8 +107,11 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
className="hidden"
/>

{loaded ? (
<div className="absolute inset-0 flex">
{loaded && (
<div
className="absolute inset-0 flex"
style={{ animation: "hf-thumb-fade 200ms ease-out", mixBlendMode: "lighten" }}
>
{Array.from({ length: frameCount }).map((_, i) => (
<div
key={i}
Expand All @@ -122,59 +122,25 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
src={url}
alt=""
draggable={false}
className="absolute inset-0 h-full w-full object-cover opacity-60"
className="absolute inset-0 h-full w-full object-cover"
style={{ opacity: 0.7 }}
/>
</div>
))}
</div>
) : (
<div
className="absolute inset-0 animate-pulse"
style={{
background:
"linear-gradient(90deg, rgba(255,255,255,0.02) 0%, rgba(255,255,255,0.05) 50%, rgba(255,255,255,0.02) 100%)",
}}
/>
)}

<div
className="absolute inset-0"
style={{
background: `linear-gradient(120deg, ${accentColor}2e, transparent 34%), linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.08))`,
}}
/>

<div
className="absolute left-2 top-2"
style={{ zIndex: COMPOSITION_THUMBNAIL_LABEL_Z_INDEX }}
>
<div className="absolute left-3 top-0 bottom-0 flex items-center" style={{ zIndex: 10 }}>
<span
className="block max-w-full truncate rounded-md px-1.5 py-0.5 text-[9px] font-semibold uppercase leading-none"
className="block max-w-full truncate text-[10px] font-semibold leading-none"
style={{
color: labelColor,
background: `${accentColor}2e`,
boxShadow: `inset 0 0 0 1px ${accentColor}40`,
textShadow: loaded ? "0 1px 4px rgba(0,0,0,0.9), 0 0 8px rgba(0,0,0,0.6)" : "none",
}}
>
{label}
</span>
</div>

<div
className="absolute bottom-0 left-0 right-0 px-1.5 pb-0.5 pt-3"
style={{
zIndex: COMPOSITION_THUMBNAIL_LABEL_Z_INDEX,
background:
"linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.4) 60%, transparent 100%)",
}}
>
<span
className="block truncate text-[9px] font-semibold leading-tight"
style={{ color: labelColor, textShadow: "0 1px 2px rgba(0,0,0,0.9)" }}
>
{label}
</span>
</div>
</div>
);
});
32 changes: 9 additions & 23 deletions packages/studio/src/player/components/TimelineCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { getRenderedTimelineElement, type TimelineTheme } from "./timelineTheme"
import { GUTTER, TRACK_H, RULER_H, CLIP_Y, CLIP_HANDLE_W } from "./timelineLayout";
import type { TimelineElement } from "../store/playerStore";
import type { DraggedClipState, ResizingClipState, BlockedClipState } from "./useTimelineClipDrag";
import { formatTime } from "../lib/time";
import type { TrackVisualStyle } from "./timelineIcons";

interface TimelineCanvasProps {
Expand Down Expand Up @@ -134,28 +133,16 @@ export const TimelineCanvas = memo(function TimelineCanvas({
className={
renderClipContent
? "absolute inset-0 overflow-hidden"
: "flex flex-col justify-center overflow-hidden flex-1 min-w-0 px-6"
: "flex items-center overflow-hidden flex-1 min-w-0 px-3 gap-2"
}
>
{renderClipContent?.(element, clipStyle) ?? (
<div className="flex h-full min-h-0 flex-col justify-between py-3">
<span
className="max-w-full truncate rounded-md px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.08em] leading-none"
style={{
color: clipStyle.label,
background: `${clipStyle.accent}26`,
boxShadow: `inset 0 0 0 1px ${clipStyle.accent}33`,
}}
>
{element.tag}
</span>
<span
className="max-w-full truncate rounded-md px-1.5 py-0.5 text-[10px] font-medium tabular-nums leading-none"
style={{ color: theme.textSecondary, background: "rgba(255,255,255,0.04)" }}
>
{formatTime(element.start)} {"→"} {formatTime(element.start + element.duration)}
</span>
</div>
<span
className="truncate text-[10px] font-medium leading-none"
style={{ color: clipStyle.label }}
>
{element.label || element.id || element.tag}
</span>
)}
</div>
</>
Expand Down Expand Up @@ -221,10 +208,9 @@ export const TimelineCanvas = memo(function TimelineCanvas({
paddingLeft: 16,
color: ts.label,
fontSize: 11,
letterSpacing: "0.08em",
letterSpacing: "0.06em",
textTransform: "uppercase",
background: `linear-gradient(90deg, ${ts.accent}14, transparent 28%)`,
boxShadow: `inset 0 0 0 1px ${ts.accent}24`,
opacity: 0.5,
}}
>
New track
Expand Down
Loading
Loading