diff --git a/packages/cli/src/server/studioServer.ts b/packages/cli/src/server/studioServer.ts index 8cb36772b..63daca28e 100644 --- a/packages/cli/src/server/studioServer.ts +++ b/packages/cli/src/server/studioServer.ts @@ -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; diff --git a/packages/studio/src/hooks/useRenderClipContent.test.ts b/packages/studio/src/hooks/useRenderClipContent.test.ts new file mode 100644 index 000000000..1239ecbc3 --- /dev/null +++ b/packages/studio/src/hooks/useRenderClipContent.test.ts @@ -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"); + }); +}); diff --git a/packages/studio/src/hooks/useRenderClipContent.ts b/packages/studio/src/hooks/useRenderClipContent.ts index 467cbab75..f065e8276 100644 --- a/packages/studio/src/hooks/useRenderClipContent.ts +++ b/packages/studio/src/hooks/useRenderClipContent.ts @@ -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; @@ -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) || @@ -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, }); @@ -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, @@ -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, diff --git a/packages/studio/src/player/components/CompositionThumbnail.tsx b/packages/studio/src/player/components/CompositionThumbnail.tsx index 02f73db15..605d56070 100644 --- a/packages/studio/src/player/components/CompositionThumbnail.tsx +++ b/packages/studio/src/player/components/CompositionThumbnail.tsx @@ -5,7 +5,6 @@ interface CompositionThumbnailProps { previewUrl: string; label: string; labelColor: string; - accentColor?: string; selector?: string; selectorIndex?: number; seekTime?: number; @@ -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, @@ -53,7 +51,6 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({ previewUrl, label, labelColor, - accentColor = "#6B7280", selector, selectorIndex, seekTime = 2, @@ -110,8 +107,11 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({ className="hidden" /> - {loaded ? ( -
+ {loaded && ( +
{Array.from({ length: frameCount }).map((_, i) => (
))}
- ) : ( -
)} -
- -
+
{label}
- -
- - {label} - -
); }); diff --git a/packages/studio/src/player/components/TimelineCanvas.tsx b/packages/studio/src/player/components/TimelineCanvas.tsx index d6d17a36e..e9af933ed 100644 --- a/packages/studio/src/player/components/TimelineCanvas.tsx +++ b/packages/studio/src/player/components/TimelineCanvas.tsx @@ -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 { @@ -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) ?? ( -
- - {element.tag} - - - {formatTime(element.start)} {"→"} {formatTime(element.start + element.duration)} - -
+ + {element.label || element.id || element.tag} + )}
@@ -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 diff --git a/packages/studio/src/player/components/TimelineClip.tsx b/packages/studio/src/player/components/TimelineClip.tsx index de0c583b0..a003a5cb0 100644 --- a/packages/studio/src/player/components/TimelineClip.tsx +++ b/packages/studio/src/player/components/TimelineClip.tsx @@ -51,14 +51,14 @@ export const TimelineClip = memo(function TimelineClip({ const handleOpacity = getClipHandleOpacity({ isHovered, isSelected, isDragging }); const borderColor = isSelected - ? theme.clipBorderActive + ? trackStyle.accent + "60" : isHovered ? theme.clipBorderHover : theme.clipBorder; const boxShadow = isDragging ? theme.clipShadowDragging : isSelected - ? theme.clipShadowActive + ? `0 0 0 1px ${trackStyle.accent}40` : isHovered ? theme.clipShadowHover : theme.clipShadow; @@ -77,20 +77,14 @@ export const TimelineClip = memo(function TimelineClip({ top: clipY, bottom: clipY, borderRadius: theme.clipRadius, - background: isSelected - ? `linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0)), linear-gradient(120deg, ${trackStyle.accent}22, transparent 28%), ${theme.clipBackgroundActive}` - : `linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0)), linear-gradient(120deg, ${trackStyle.accent}1e, transparent 28%), ${theme.clipBackground}`, - backgroundImage: - isComposition && !hasCustomContent - ? `repeating-linear-gradient(135deg, transparent, transparent 3px, rgba(255,255,255,0.05) 3px, rgba(255,255,255,0.05) 6px)` - : undefined, + background: trackStyle.clip, border: `1px solid ${borderColor}`, boxShadow, - transition: - "border-color 120ms ease-out, box-shadow 140ms ease-out, background 140ms ease-out", + transition: "border-color 100ms, box-shadow 100ms", zIndex: isDragging ? 20 : isSelected ? 10 : isHovered ? 5 : 1, cursor: capabilities.canMove ? "grab" : "default", transform: isDragging ? "translateY(-1px)" : undefined, + opacity: isDragging ? 0.92 : 1, }} title={ isComposition @@ -103,78 +97,80 @@ export const TimelineClip = memo(function TimelineClip({ onClick={onClick} onDoubleClick={onDoubleClick} > + {/* Left accent stripe */}