Skip to content

Commit b6862ed

Browse files
committed
feat(studio): timeline UI overhaul — flat clips, unified color, working thumbnails
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)
1 parent 692c143 commit b6862ed

7 files changed

Lines changed: 124 additions & 176 deletions

File tree

packages/cli/src/server/studioServer.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,13 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
346346
}, opts.seekTime);
347347
const manifestContent = readStudioManualEditManifestContent(opts.project.dir);
348348
await applyStudioManualEditsToThumbnailPage(page, manifestContent, opts.compPath);
349-
await page.evaluate(() => document.fonts?.ready);
349+
await page.evaluate(() => {
350+
void document.fonts?.ready;
351+
const body = document.body;
352+
if (body && getComputedStyle(body).backgroundColor === "rgba(0, 0, 0, 0)") {
353+
body.style.backgroundColor = "#1c2028";
354+
}
355+
});
350356
await new Promise((r) => setTimeout(r, 200));
351357
await reapplyStudioManualEditsToThumbnailPage(page);
352358
let clip: ScreenshotClip | undefined;

packages/studio/src/hooks/useRenderClipContent.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,18 @@ export function useRenderClipContent({
2323
const pid = projectIdRef.current;
2424
if (!pid) return null;
2525

26-
// Resolve composition source path using the compIdToSrc map
2726
let compSrc = el.compositionSrc;
27+
if (compSrc) {
28+
try {
29+
const parsed = new URL(compSrc, window.location.origin);
30+
const previewPrefix = `/api/projects/${pid}/preview/`;
31+
if (parsed.pathname.startsWith(previewPrefix)) {
32+
compSrc = parsed.pathname.slice(previewPrefix.length);
33+
}
34+
} catch {
35+
// already relative
36+
}
37+
}
2838
if (compSrc && compIdToSrc.size > 0) {
2939
const resolved =
3040
compIdToSrc.get(el.id) ||

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

Lines changed: 11 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ interface CompositionThumbnailProps {
1616

1717
const CLIP_HEIGHT = 66;
1818
const THUMBNAIL_URL_VERSION = "v3";
19-
const COMPOSITION_THUMBNAIL_LABEL_Z_INDEX = 10;
2019

2120
export function buildCompositionThumbnailUrl({
2221
previewUrl,
@@ -53,7 +52,7 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
5352
previewUrl,
5453
label,
5554
labelColor,
56-
accentColor = "#6B7280",
55+
accentColor: _accentColor = "#6B7280",
5756
selector,
5857
selectorIndex,
5958
seekTime = 2,
@@ -110,8 +109,11 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
110109
className="hidden"
111110
/>
112111

113-
{loaded ? (
114-
<div className="absolute inset-0 flex">
112+
{loaded && (
113+
<div
114+
className="absolute inset-0 flex"
115+
style={{ animation: "fadeIn 200ms ease-out", mixBlendMode: "lighten" }}
116+
>
115117
{Array.from({ length: frameCount }).map((_, i) => (
116118
<div
117119
key={i}
@@ -122,59 +124,25 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
122124
src={url}
123125
alt=""
124126
draggable={false}
125-
className="absolute inset-0 h-full w-full object-cover opacity-60"
127+
className="absolute inset-0 h-full w-full object-cover"
128+
style={{ opacity: 0.7 }}
126129
/>
127130
</div>
128131
))}
129132
</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-
/>
138133
)}
139134

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-
>
135+
<div className="absolute left-3 top-0 bottom-0 flex items-center" style={{ zIndex: 10 }}>
151136
<span
152-
className="block max-w-full truncate rounded-md px-1.5 py-0.5 text-[9px] font-semibold uppercase leading-none"
137+
className="block max-w-full truncate text-[10px] font-semibold leading-none"
153138
style={{
154139
color: labelColor,
155-
background: `${accentColor}2e`,
156-
boxShadow: `inset 0 0 0 1px ${accentColor}40`,
140+
textShadow: loaded ? "0 1px 4px rgba(0,0,0,0.9), 0 0 8px rgba(0,0,0,0.6)" : "none",
157141
}}
158142
>
159143
{label}
160144
</span>
161145
</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>
178146
</div>
179147
);
180148
});

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

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

Lines changed: 63 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,14 @@ export const TimelineClip = memo(function TimelineClip({
5151
const handleOpacity = getClipHandleOpacity({ isHovered, isSelected, isDragging });
5252

5353
const borderColor = isSelected
54-
? theme.clipBorderActive
54+
? trackStyle.accent + "60"
5555
: isHovered
5656
? theme.clipBorderHover
5757
: theme.clipBorder;
5858
const boxShadow = isDragging
5959
? theme.clipShadowDragging
6060
: isSelected
61-
? theme.clipShadowActive
61+
? `0 0 0 1px ${trackStyle.accent}40`
6262
: isHovered
6363
? theme.clipShadowHover
6464
: theme.clipShadow;
@@ -77,20 +77,14 @@ export const TimelineClip = memo(function TimelineClip({
7777
top: clipY,
7878
bottom: clipY,
7979
borderRadius: theme.clipRadius,
80-
background: isSelected
81-
? `linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0)), linear-gradient(120deg, ${trackStyle.accent}22, transparent 28%), ${theme.clipBackgroundActive}`
82-
: `linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0)), linear-gradient(120deg, ${trackStyle.accent}1e, transparent 28%), ${theme.clipBackground}`,
83-
backgroundImage:
84-
isComposition && !hasCustomContent
85-
? `repeating-linear-gradient(135deg, transparent, transparent 3px, rgba(255,255,255,0.05) 3px, rgba(255,255,255,0.05) 6px)`
86-
: undefined,
80+
background: trackStyle.clip,
8781
border: `1px solid ${borderColor}`,
8882
boxShadow,
89-
transition:
90-
"border-color 120ms ease-out, box-shadow 140ms ease-out, background 140ms ease-out",
83+
transition: "border-color 100ms, box-shadow 100ms",
9184
zIndex: isDragging ? 20 : isSelected ? 10 : isHovered ? 5 : 1,
9285
cursor: capabilities.canMove ? "grab" : "default",
9386
transform: isDragging ? "translateY(-1px)" : undefined,
87+
opacity: isDragging ? 0.92 : 1,
9488
}}
9589
title={
9690
isComposition
@@ -103,78 +97,80 @@ export const TimelineClip = memo(function TimelineClip({
10397
onClick={onClick}
10498
onDoubleClick={onDoubleClick}
10599
>
100+
{/* Left accent stripe */}
106101
<div
107102
aria-hidden="true"
108-
role="presentation"
109-
onPointerDown={(e) => onResizeStart?.("start", e)}
110103
style={{
111104
position: "absolute",
112105
left: 0,
113106
top: 0,
114107
bottom: 0,
115-
width: 18,
116-
opacity: showHandles && capabilities.canTrimStart ? 1 : 0,
117-
pointerEvents: onResizeStart && capabilities.canTrimStart ? "auto" : "none",
118-
zIndex: 4,
119-
transition: "opacity 120ms ease-out",
120-
cursor: "col-resize",
121-
background:
122-
showHandles && capabilities.canTrimStart
123-
? `linear-gradient(90deg, ${trackStyle.accent}4d 0%, ${trackStyle.accent}22 42%, transparent 100%)`
124-
: "transparent",
108+
width: 3,
109+
background: trackStyle.accent,
110+
opacity: isSelected ? 0.7 : 0.3,
111+
borderRadius: `${theme.clipRadius} 0 0 ${theme.clipRadius}`,
112+
zIndex: 2,
113+
pointerEvents: "none",
125114
}}
126-
>
115+
/>
116+
{/* Left trim handle */}
117+
{showHandles && capabilities.canTrimStart && (
127118
<div
119+
aria-hidden="true"
120+
onPointerDown={(e) => onResizeStart?.("start", e)}
128121
style={{
129122
position: "absolute",
130-
left: 6,
131-
top: 7,
132-
bottom: 7,
133-
width: 3,
134-
borderRadius: 999,
135-
background: theme.handleColor,
136-
boxShadow: `0 0 0 1px ${trackStyle.accent}38, 0 0 12px ${trackStyle.accent}18`,
137-
opacity: handleOpacity,
138-
pointerEvents: "none",
123+
left: 0,
124+
top: 0,
125+
bottom: 0,
126+
width: 14,
127+
cursor: "col-resize",
128+
zIndex: 4,
139129
}}
140-
/>
141-
</div>
142-
<div
143-
aria-hidden="true"
144-
role="presentation"
145-
onPointerDown={(e) => onResizeStart?.("end", e)}
146-
style={{
147-
position: "absolute",
148-
right: 0,
149-
top: 0,
150-
bottom: 0,
151-
width: 18,
152-
opacity: showHandles && capabilities.canTrimEnd ? 1 : 0,
153-
pointerEvents: onResizeStart && capabilities.canTrimEnd ? "auto" : "none",
154-
zIndex: 4,
155-
transition: "opacity 120ms ease-out",
156-
cursor: "col-resize",
157-
background:
158-
showHandles && capabilities.canTrimEnd
159-
? `linear-gradient(270deg, ${trackStyle.accent}4d 0%, ${trackStyle.accent}22 42%, transparent 100%)`
160-
: "transparent",
161-
}}
162-
>
130+
>
131+
<div
132+
style={{
133+
position: "absolute",
134+
left: 4,
135+
top: 6,
136+
bottom: 6,
137+
width: 2,
138+
borderRadius: 1,
139+
background: trackStyle.accent,
140+
opacity: handleOpacity * 0.6,
141+
}}
142+
/>
143+
</div>
144+
)}
145+
{/* Right trim handle */}
146+
{showHandles && capabilities.canTrimEnd && (
163147
<div
148+
aria-hidden="true"
149+
onPointerDown={(e) => onResizeStart?.("end", e)}
164150
style={{
165151
position: "absolute",
166-
right: 6,
167-
top: 7,
168-
bottom: 7,
169-
width: 3,
170-
borderRadius: 999,
171-
background: theme.handleColor,
172-
boxShadow: `0 0 0 1px ${trackStyle.accent}38, 0 0 12px ${trackStyle.accent}18`,
173-
opacity: handleOpacity,
174-
pointerEvents: "none",
152+
right: 0,
153+
top: 0,
154+
bottom: 0,
155+
width: 14,
156+
cursor: "col-resize",
157+
zIndex: 4,
175158
}}
176-
/>
177-
</div>
159+
>
160+
<div
161+
style={{
162+
position: "absolute",
163+
right: 4,
164+
top: 6,
165+
bottom: 6,
166+
width: 2,
167+
borderRadius: 1,
168+
background: trackStyle.accent,
169+
opacity: handleOpacity * 0.6,
170+
}}
171+
/>
172+
</div>
173+
)}
178174
{children}
179175
</div>
180176
);

0 commit comments

Comments
 (0)