From ef2ff298b60f353d850c151efc5fded5725e6dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 20 May 2026 18:55:31 -0400 Subject: [PATCH 1/3] =?UTF-8?q?feat(studio):=20timeline=20UI=20overhaul=20?= =?UTF-8?q?=E2=80=94=20flat=20clips,=20unified=20color,=20working=20thumbn?= =?UTF-8?q?ails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visual redesign of the timeline: - Flat solid clip backgrounds, no gradients or multi-layer shadows - Unified neutral color palette — all clips use the same base color - Clean 6px border-radius instead of organic asymmetric radii - Single-line labels, no redundant tag badge or time range - 3px teal accent stripe on the left edge of every clip - Simplified trim handles (2px accent bars) Thumbnail fixes: - Fixed broken thumbnail URLs — compositionSrc was an absolute URL that got nested inside the preview/comp path. Now normalized to relative path before constructing the thumbnail URL - Thumbnail background changed from #000 to #1c2028 so transparent overlay compositions render visible content against a matching dark background - mix-blend-mode: lighten on thumbnail layer — dark backgrounds blend away, bright content shows through - Removed double-label in CompositionThumbnail (was showing both a badge at top and text at bottom) --- packages/cli/src/server/studioServer.ts | 8 +- .../studio/src/hooks/useRenderClipContent.ts | 12 +- .../components/CompositionThumbnail.tsx | 54 ++------ .../src/player/components/TimelineCanvas.tsx | 32 ++--- .../src/player/components/TimelineClip.tsx | 130 +++++++++--------- .../src/player/components/timelineTheme.ts | 60 +++----- packages/studio/vite.browser.ts | 4 +- 7 files changed, 124 insertions(+), 176 deletions(-) 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.ts b/packages/studio/src/hooks/useRenderClipContent.ts index 467cbab75..53f334fb5 100644 --- a/packages/studio/src/hooks/useRenderClipContent.ts +++ b/packages/studio/src/hooks/useRenderClipContent.ts @@ -23,8 +23,18 @@ 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) { + try { + const parsed = new URL(compSrc, window.location.origin); + const previewPrefix = `/api/projects/${pid}/preview/`; + if (parsed.pathname.startsWith(previewPrefix)) { + compSrc = parsed.pathname.slice(previewPrefix.length); + } + } catch { + // already relative + } + } if (compSrc && compIdToSrc.size > 0) { const resolved = compIdToSrc.get(el.id) || diff --git a/packages/studio/src/player/components/CompositionThumbnail.tsx b/packages/studio/src/player/components/CompositionThumbnail.tsx index 02f73db15..8876be1b1 100644 --- a/packages/studio/src/player/components/CompositionThumbnail.tsx +++ b/packages/studio/src/player/components/CompositionThumbnail.tsx @@ -16,7 +16,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 +52,7 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({ previewUrl, label, labelColor, - accentColor = "#6B7280", + accentColor: _accentColor = "#6B7280", selector, selectorIndex, seekTime = 2, @@ -110,8 +109,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 */}