Skip to content

Commit d2a2ac6

Browse files
miguel-heygenclaude
andcommitted
feat(studio): timeline hidden by default with toggle in header + player controls
Adds timelineVisible state (defaults false) with synced toggle buttons in both the header toolbar and the PlayerControls bar. Toggle icon turns teal when timeline is visible. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 3741bc5 commit d2a2ac6

3 files changed

Lines changed: 111 additions & 39 deletions

File tree

packages/studio/src/App.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,7 @@ export function StudioApp() {
535535
const [rightWidth, setRightWidth] = useState(400);
536536
const [leftCollapsed, setLeftCollapsed] = useState(false);
537537
const [rightCollapsed, setRightCollapsed] = useState(false);
538+
const [timelineVisible, setTimelineVisible] = useState(false);
538539
const panelDragRef = useRef<{
539540
side: "left" | "right";
540541
startX: number;
@@ -829,6 +830,29 @@ export function StudioApp() {
829830
<path d="M9 3v18" />
830831
</svg>
831832
</button>
833+
<button
834+
onClick={() => setTimelineVisible((v) => !v)}
835+
className={`h-7 w-7 flex items-center justify-center rounded-md border transition-colors ${
836+
timelineVisible
837+
? "text-[#3CE6AC] bg-[#3CE6AC]/10 border-[#3CE6AC]/30"
838+
: "bg-transparent border-transparent text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800"
839+
}`}
840+
title={timelineVisible ? "Hide timeline" : "Show timeline"}
841+
>
842+
<svg
843+
width="14"
844+
height="14"
845+
viewBox="0 0 24 24"
846+
fill="none"
847+
stroke="currentColor"
848+
strokeWidth="1.5"
849+
strokeLinecap="round"
850+
>
851+
<rect x="3" y="13" width="18" height="8" rx="1" />
852+
<line x1="3" y1="9" x2="21" y2="9" />
853+
<line x1="3" y1="5" x2="21" y2="5" />
854+
</svg>
855+
</button>
832856
<button
833857
onClick={() => setRightCollapsed((v) => !v)}
834858
className={`h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border transition-colors ${
@@ -927,6 +951,8 @@ export function StudioApp() {
927951
onIframeRef={(iframe) => {
928952
previewIframeRef.current = iframe;
929953
}}
954+
timelineVisible={timelineVisible}
955+
onToggleTimeline={() => setTimelineVisible((v) => !v)}
930956
/>
931957
</div>
932958

packages/studio/src/components/nle/NLELayout.tsx

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ interface NLELayoutProps {
2929
) => ReactNode;
3030
/** Exposes the compIdToSrc map for parent components (e.g., useRenderClipContent) */
3131
onCompIdToSrcChange?: (map: Map<string, string>) => void;
32+
/** Whether the timeline panel is visible (default: true) */
33+
timelineVisible?: boolean;
34+
/** Callback to toggle timeline visibility */
35+
onToggleTimeline?: () => void;
3236
}
3337

3438
const MIN_TIMELINE_H = 100;
@@ -47,6 +51,8 @@ export const NLELayout = memo(function NLELayout({
4751
onCompositionChange,
4852
renderClipContent,
4953
onCompIdToSrcChange,
54+
timelineVisible,
55+
onToggleTimeline,
5056
}: NLELayoutProps) {
5157
const {
5258
iframeRef,
@@ -324,47 +330,56 @@ export const NLELayout = memo(function NLELayout({
324330
{previewOverlay}
325331
</div>
326332

327-
{/* Resize divider */}
328-
<div
329-
className="h-1 flex-shrink-0 bg-neutral-800 hover:bg-blue-500 cursor-row-resize transition-colors active:bg-blue-400 z-10"
330-
style={{ touchAction: "none" }}
331-
onPointerDown={handleDividerPointerDown}
332-
onPointerMove={handleDividerPointerMove}
333-
onPointerUp={handleDividerPointerUp}
334-
/>
333+
{(timelineVisible ?? true) && (
334+
<>
335+
{/* Resize divider */}
336+
<div
337+
className="h-1 flex-shrink-0 bg-neutral-800 hover:bg-blue-500 cursor-row-resize transition-colors active:bg-blue-400 z-10"
338+
style={{ touchAction: "none" }}
339+
onPointerDown={handleDividerPointerDown}
340+
onPointerMove={handleDividerPointerMove}
341+
onPointerUp={handleDividerPointerUp}
342+
/>
335343

336-
{/* Timeline section — fixed height, resizable */}
337-
<div className="flex flex-col flex-shrink-0" style={{ height: timelineH }}>
338-
{/* Breadcrumb + Player controls */}
339-
<div className="bg-neutral-950 border-t border-neutral-800/50 flex-shrink-0">
340-
{compositionStack.length > 1 && (
341-
<CompositionBreadcrumb
342-
stack={compositionStack}
343-
onNavigate={handleNavigateComposition}
344-
/>
345-
)}
346-
<PlayerControls onTogglePlay={togglePlay} onSeek={seek} />
347-
</div>
344+
{/* Timeline section — fixed height, resizable */}
345+
<div className="flex flex-col flex-shrink-0" style={{ height: timelineH }}>
346+
{/* Breadcrumb + Player controls */}
347+
<div className="bg-neutral-950 border-t border-neutral-800/50 flex-shrink-0">
348+
{compositionStack.length > 1 && (
349+
<CompositionBreadcrumb
350+
stack={compositionStack}
351+
onNavigate={handleNavigateComposition}
352+
/>
353+
)}
354+
<PlayerControls
355+
onTogglePlay={togglePlay}
356+
onSeek={seek}
357+
timelineVisible={timelineVisible ?? true}
358+
onToggleTimeline={onToggleTimeline}
359+
/>
360+
</div>
348361

349-
{/* Timeline tracks */}
350-
<div
351-
className="flex-1 min-h-0 overflow-y-auto bg-neutral-950"
352-
onDoubleClick={(e) => {
353-
if ((e.target as HTMLElement).closest("[data-clip]")) return;
354-
if (compositionStack.length > 1) {
355-
updateCompositionStack((prev) => prev.slice(0, -1));
356-
}
357-
}}
358-
>
359-
{timelineToolbar}
360-
<Timeline
361-
onSeek={seek}
362-
onDrillDown={handleDrillDown}
363-
renderClipContent={renderClipContent}
364-
/>
365-
{timelineFooter}
366-
</div>
367-
</div>
362+
{/* Timeline tracks */}
363+
<div
364+
className="flex-1 min-h-0 overflow-y-auto bg-neutral-950"
365+
onDoubleClick={(e) => {
366+
if ((e.target as HTMLElement).closest("[data-clip]")) return;
367+
if (compositionStack.length > 1) {
368+
updateCompositionStack((prev) => prev.slice(0, -1));
369+
}
370+
}}
371+
>
372+
{timelineToolbar}
373+
<Timeline
374+
onSeek={seek}
375+
onDrillDown={handleDrillDown}
376+
renderClipContent={renderClipContent}
377+
/>
378+
{timelineFooter}
379+
</div>
380+
</div>
381+
</>
382+
)}
368383
</div>
369384
);
370385
});

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ const SPEED_OPTIONS = [0.25, 0.5, 1, 1.5, 2] as const;
88
interface PlayerControlsProps {
99
onTogglePlay: () => void;
1010
onSeek: (time: number) => void;
11+
timelineVisible?: boolean;
12+
onToggleTimeline?: () => void;
1113
}
1214

1315
export const PlayerControls = memo(function PlayerControls({
1416
onTogglePlay,
1517
onSeek,
18+
timelineVisible,
19+
onToggleTimeline,
1620
}: PlayerControlsProps) {
1721
// Subscribe to only the fields we render — each selector prevents cascading re-renders
1822
const isPlaying = usePlayerStore((s) => s.isPlaying);
@@ -224,6 +228,33 @@ export const PlayerControls = memo(function PlayerControls({
224228
</div>
225229
)}
226230
</div>
231+
232+
{/* Timeline toggle */}
233+
{onToggleTimeline !== undefined && (
234+
<button
235+
onClick={onToggleTimeline}
236+
className={`w-7 h-7 flex items-center justify-center rounded-md border transition-colors ${
237+
timelineVisible
238+
? "text-[#3CE6AC] bg-[#3CE6AC]/10 border-[#3CE6AC]/30"
239+
: "border-neutral-700 text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800"
240+
}`}
241+
title={timelineVisible ? "Hide timeline" : "Show timeline"}
242+
>
243+
<svg
244+
width="13"
245+
height="13"
246+
viewBox="0 0 24 24"
247+
fill="none"
248+
stroke="currentColor"
249+
strokeWidth="2"
250+
strokeLinecap="round"
251+
>
252+
<rect x="3" y="13" width="18" height="8" rx="1" />
253+
<line x1="3" y1="9" x2="21" y2="9" />
254+
<line x1="3" y1="5" x2="21" y2="5" />
255+
</svg>
256+
</button>
257+
)}
227258
</div>
228259
);
229260
});

0 commit comments

Comments
 (0)