feat: adjustable clip list layout (bottom/left sidebar)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 16:49:35 -04:00
parent 0b8bec2496
commit b371ec6c3e
3 changed files with 225 additions and 35 deletions

View File

@@ -10,7 +10,7 @@
import ExportDialog from '$lib/components/ExportDialog.svelte';
import AboutDialog from '$lib/components/AboutDialog.svelte';
import ProcessingModal from '$lib/components/ProcessingModal.svelte';
import { loadPreferences } from '$lib/stores/preferences.svelte';
import { loadPreferences, preferences, setClipListPosition, setClipListWidth } from '$lib/stores/preferences.svelte';
import { getSelectedClipId, removeClip } from '$lib/stores/clips.svelte';
import { adjustShuttle, resetShuttleRate } from '$lib/transport/playback';
import { session } from '$lib/stores/videoSession.svelte';
@@ -32,6 +32,11 @@
let resizeStartHeight = $state(0);
let splitAreaEl = $state<HTMLDivElement | null>(null);
let isSidebarResizing = $state(false);
let sidebarResizeStartX = $state(0);
let sidebarResizeStartWidth = $state(0);
let windowWidth = $state(window.innerWidth);
$effect(() => {
loadPreferences();
});
@@ -74,6 +79,33 @@
isResizing = false;
}
function toggleClipListPosition() {
const next = preferences.clipListPosition === 'bottom' ? 'left' : 'bottom';
setClipListPosition(next);
}
function handleSidebarResizeStart(e: MouseEvent) {
e.preventDefault();
isSidebarResizing = true;
sidebarResizeStartX = e.clientX;
sidebarResizeStartWidth = preferences.clipListWidth;
}
function handleSidebarResizeMove(e: MouseEvent) {
if (!isSidebarResizing) return;
const delta = e.clientX - sidebarResizeStartX;
const maxWidth = Math.round(windowWidth * 0.4);
const newWidth = Math.max(150, Math.min(maxWidth, sidebarResizeStartWidth + delta));
preferences.clipListWidth = newWidth;
}
function handleSidebarResizeEnd() {
if (isSidebarResizing) {
isSidebarResizing = false;
setClipListWidth(preferences.clipListWidth);
}
}
function handleGlobalKeydown(e: KeyboardEvent) {
const target = e.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
@@ -156,8 +188,17 @@
<svelte:window
onkeydown={handleGlobalKeydown}
onmousemove={handleResizeMove}
onmouseup={handleResizeEnd}
onmousemove={(e) => {
handleResizeMove(e);
handleSidebarResizeMove(e);
}}
onmouseup={() => {
handleResizeEnd();
handleSidebarResizeEnd();
}}
onresize={() => {
windowWidth = window.innerWidth;
}}
/>
{#if showSetupWizard}
@@ -183,43 +224,115 @@
<div class="app-shell">
<header class="toolbar">
<UrlInput />
<button
class="layout-btn"
onclick={toggleClipListPosition}
title={preferences.clipListPosition === 'bottom'
? 'Move clip list to sidebar'
: 'Move clip list to bottom'}
>
{preferences.clipListPosition === 'bottom' ? '⬒' : '⬓'}
</button>
<button class="about-btn" onclick={() => (showAboutDialog = true)} title="About">ℹ</button>
<button class="prefs-btn" onclick={() => (showPreferences = true)} title="Preferences">⚙</button>
</header>
<main class="content">
<VideoPlayer />
<TransportControls bind:this={transportControls} />
<div class="timeline-clip-area" bind:this={splitAreaEl}>
<div class="timeline-pane" style="height: {timelineHeight}px">
<Timeline />
{#if preferences.clipListPosition === 'bottom'}
<main class="content">
<VideoPlayer />
<TransportControls bind:this={transportControls} />
<div class="timeline-clip-area" bind:this={splitAreaEl}>
<div class="timeline-pane" style="height: {timelineHeight}px">
<Timeline />
</div>
<div
class="resize-handle"
class:active={isResizing}
role="slider"
aria-orientation="horizontal"
aria-label="Resize timeline and clip list"
aria-valuenow={timelineHeight}
aria-valuemin={40}
aria-valuemax={500}
tabindex="0"
onmousedown={handleResizeStart}
onkeydown={(e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
timelineHeight = Math.min(500, timelineHeight + 10);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
timelineHeight = Math.max(40, timelineHeight - 10);
}
}}
></div>
<div class="cliplist-pane">
<ClipList onExport={() => (showExportDialog = true)} />
</div>
</div>
<div
class="resize-handle"
class:active={isResizing}
role="slider"
aria-orientation="horizontal"
aria-label="Resize timeline and clip list"
aria-valuenow={timelineHeight}
aria-valuemin={40}
aria-valuemax={500}
tabindex="0"
onmousedown={handleResizeStart}
onkeydown={(e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
timelineHeight = Math.min(500, timelineHeight + 10);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
timelineHeight = Math.max(40, timelineHeight - 10);
}
}}
></div>
<div class="cliplist-pane">
<ClipList onExport={() => (showExportDialog = true)} />
</main>
{:else}
<main class="content content-left">
<div class="upper-section">
<div class="cliplist-sidebar" style="width: {preferences.clipListWidth}px">
<ClipList onExport={() => (showExportDialog = true)} position="left" />
</div>
<div
class="sidebar-resize-handle"
class:active={isSidebarResizing}
role="slider"
aria-orientation="vertical"
aria-label="Resize clip list sidebar"
aria-valuenow={preferences.clipListWidth}
aria-valuemin={150}
aria-valuemax={Math.round(windowWidth * 0.4)}
tabindex="0"
onmousedown={handleSidebarResizeStart}
onkeydown={(e) => {
if (e.key === 'ArrowRight') {
e.preventDefault();
setClipListWidth(
Math.min(Math.round(windowWidth * 0.4), preferences.clipListWidth + 10),
);
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
setClipListWidth(Math.max(150, preferences.clipListWidth - 10));
}
}}
></div>
<div class="right-column">
<VideoPlayer />
<TransportControls bind:this={transportControls} />
</div>
</div>
</div>
</main>
<div class="timeline-clip-area" bind:this={splitAreaEl}>
<div
class="resize-handle"
class:active={isResizing}
role="slider"
aria-orientation="horizontal"
aria-label="Resize upper section and timeline"
aria-valuenow={timelineHeight}
aria-valuemin={40}
aria-valuemax={500}
tabindex="0"
onmousedown={handleResizeStart}
onkeydown={(e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
timelineHeight = Math.min(500, timelineHeight + 10);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
timelineHeight = Math.max(40, timelineHeight - 10);
}
}}
></div>
<div class="timeline-pane" style="height: {timelineHeight}px">
<Timeline />
</div>
</div>
</main>
{/if}
<StatusBar onOpenAbout={() => (showAboutDialog = true)} />
</div>
@@ -279,6 +392,54 @@
overflow: hidden;
}
.content-left {
display: flex;
flex-direction: column;
overflow: hidden;
}
.upper-section {
flex: 1;
display: flex;
flex-direction: row;
min-height: 0;
overflow: hidden;
}
.cliplist-sidebar {
flex-shrink: 0;
overflow-y: auto;
overflow-x: hidden;
border-right: 1px solid var(--border);
background: var(--bg-secondary);
}
.sidebar-resize-handle {
width: 5px;
background: var(--border);
cursor: ew-resize;
flex-shrink: 0;
transition: background 0.1s;
}
.sidebar-resize-handle:hover,
.sidebar-resize-handle.active {
background: var(--accent);
}
.right-column {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
.layout-btn {
font-size: 16px;
padding: 4px 8px;
}
.about-btn {
font-size: 16px;
padding: 4px 8px;