feat(studio): timeline hidden by default with toggle in header + player (#142)

## Summary

The timeline is now hidden by default. A toggle button appears in both the header (alongside panel toggles) and the player controls bar. Both sync the same state and turn teal when the timeline is visible.

## Changes

- **`timelineVisible` state** in `App.tsx`, defaults to `false`
- **Header toggle**: icon button between sidebar toggle and Renders button
- **Player controls toggle**: icon button at the right end of the controls bar
- **NLELayout**: `timelineVisible` and `onToggleTimeline` props gate the timeline + resize divider
- **Player controls stay visible**: moved from inside the timeline section to inside the preview area, so hiding the timeline doesn't hide play/seek/timecode

## Behavior

| State | Preview | Player controls | Timeline |
|---|---|---|---|
| Timeline hidden (default) | Full height |  Visible | Hidden |
| Timeline visible | Shorter |  Visible | Shown with resize handle |

Both toggle buttons show identical teal active state (`#3CE6AC/10` bg + `#3CE6AC/30` border).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Miguel Ángel
2026-03-31 04:01:01 +02:00
committed by GitHub
parent 2f99e33bbe
commit 1bc83c62a5
4 changed files with 146 additions and 96 deletions
@@ -29,6 +29,10 @@ interface NLELayoutProps {
) => ReactNode;
/** Exposes the compIdToSrc map for parent components (e.g., useRenderClipContent) */
onCompIdToSrcChange?: (map: Map<string, string>) => void;
/** Whether the timeline panel is visible (default: true) */
timelineVisible?: boolean;
/** Callback to toggle timeline visibility */
onToggleTimeline?: () => void;
}
const MIN_TIMELINE_H = 100;
@@ -47,6 +51,8 @@ export const NLELayout = memo(function NLELayout({
onCompositionChange,
renderClipContent,
onCompIdToSrcChange,
timelineVisible,
onToggleTimeline,
}: NLELayoutProps) {
const {
iframeRef,
@@ -311,31 +317,20 @@ export const NLELayout = memo(function NLELayout({
onKeyDown={handleKeyDown}
tabIndex={-1}
>
{/* Preview — takes remaining space above timeline */}
<div className="flex-1 min-h-0 relative">
<NLEPreview
projectId={projectId}
iframeRef={iframeRef}
onIframeLoad={onIframeLoad}
portrait={portrait}
directUrl={directUrl}
refreshKey={refreshKey}
/>
{previewOverlay}
</div>
{/* Resize divider */}
<div
className="h-1 flex-shrink-0 bg-neutral-800 hover:bg-blue-500 cursor-row-resize transition-colors active:bg-blue-400 z-10"
style={{ touchAction: "none" }}
onPointerDown={handleDividerPointerDown}
onPointerMove={handleDividerPointerMove}
onPointerUp={handleDividerPointerUp}
/>
{/* Timeline section — fixed height, resizable */}
<div className="flex flex-col flex-shrink-0" style={{ height: timelineH }}>
{/* Breadcrumb + Player controls */}
{/* Preview + player controls — takes remaining space above timeline */}
<div className="flex-1 min-h-0 flex flex-col">
<div className="flex-1 min-h-0 relative">
<NLEPreview
projectId={projectId}
iframeRef={iframeRef}
onIframeLoad={onIframeLoad}
portrait={portrait}
directUrl={directUrl}
refreshKey={refreshKey}
/>
{previewOverlay}
</div>
{/* Player controls always visible, regardless of timeline state */}
<div className="bg-neutral-950 border-t border-neutral-800/50 flex-shrink-0">
{compositionStack.length > 1 && (
<CompositionBreadcrumb
@@ -343,28 +338,49 @@ export const NLELayout = memo(function NLELayout({
onNavigate={handleNavigateComposition}
/>
)}
<PlayerControls onTogglePlay={togglePlay} onSeek={seek} />
</div>
{/* Timeline tracks */}
<div
className="flex-1 min-h-0 overflow-y-auto bg-neutral-950"
onDoubleClick={(e) => {
if ((e.target as HTMLElement).closest("[data-clip]")) return;
if (compositionStack.length > 1) {
updateCompositionStack((prev) => prev.slice(0, -1));
}
}}
>
{timelineToolbar}
<Timeline
<PlayerControls
onTogglePlay={togglePlay}
onSeek={seek}
onDrillDown={handleDrillDown}
renderClipContent={renderClipContent}
timelineVisible={timelineVisible ?? true}
onToggleTimeline={onToggleTimeline}
/>
{timelineFooter}
</div>
</div>
{(timelineVisible ?? true) && (
<>
{/* Resize divider */}
<div
className="h-1 flex-shrink-0 bg-neutral-800 hover:bg-blue-500 cursor-row-resize transition-colors active:bg-blue-400 z-10"
style={{ touchAction: "none" }}
onPointerDown={handleDividerPointerDown}
onPointerMove={handleDividerPointerMove}
onPointerUp={handleDividerPointerUp}
/>
{/* Timeline section — fixed height, resizable */}
<div className="flex flex-col flex-shrink-0" style={{ height: timelineH }}>
{/* Timeline tracks */}
<div
className="flex-1 min-h-0 overflow-y-auto bg-neutral-950"
onDoubleClick={(e) => {
if ((e.target as HTMLElement).closest("[data-clip]")) return;
if (compositionStack.length > 1) {
updateCompositionStack((prev) => prev.slice(0, -1));
}
}}
>
{timelineToolbar}
<Timeline
onSeek={seek}
onDrillDown={handleDrillDown}
renderClipContent={renderClipContent}
/>
{timelineFooter}
</div>
</div>
</>
)}
</div>
);
});