fix: CSS-grid layout to preserve video on toggle + magnification enhancements (brighter, larger, minor ticks)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
282
src/App.svelte
282
src/App.svelte
@@ -37,6 +37,8 @@
|
||||
let sidebarResizeStartWidth = $state(0);
|
||||
let windowWidth = $state(window.innerWidth);
|
||||
|
||||
let isLeft = $derived(preferences.clipListPosition === 'left');
|
||||
|
||||
$effect(() => {
|
||||
loadPreferences();
|
||||
});
|
||||
@@ -238,106 +240,79 @@
|
||||
<button class="prefs-btn" onclick={() => (showPreferences = true)} title="Preferences">⚙</button>
|
||||
</header>
|
||||
|
||||
{#if preferences.clipListPosition === 'bottom'}
|
||||
<main class="content">
|
||||
<main class="content" class:layout-left={isLeft} style:--sidebar-width="{preferences.clipListWidth}px">
|
||||
<!-- Sidebar clip list: visible only in left mode -->
|
||||
<div class="sidebar-area" class:hidden={!isLeft}>
|
||||
<ClipList onExport={() => (showExportDialog = true)} position="left" />
|
||||
</div>
|
||||
<div
|
||||
class="sidebar-handle"
|
||||
class:hidden={!isLeft}
|
||||
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) => {
|
||||
const maxWidth = Math.round(windowWidth * 0.4);
|
||||
const minWidth = Math.min(150, maxWidth);
|
||||
if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
setClipListWidth(
|
||||
Math.max(minWidth, Math.min(maxWidth, preferences.clipListWidth + 10)),
|
||||
);
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
setClipListWidth(
|
||||
Math.max(minWidth, Math.min(maxWidth, preferences.clipListWidth - 10)),
|
||||
);
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
|
||||
<!-- Video + Transport: always rendered, never destroyed -->
|
||||
<div class="video-area">
|
||||
<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>
|
||||
|
||||
<!-- Timeline section: always full width -->
|
||||
<div class="lower-section" bind:this={splitAreaEl}>
|
||||
<div
|
||||
class="resize-handle"
|
||||
class:active={isResizing}
|
||||
role="slider"
|
||||
aria-orientation="horizontal"
|
||||
aria-label="Resize 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>
|
||||
</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) => {
|
||||
const maxWidth = Math.round(windowWidth * 0.4);
|
||||
const minWidth = Math.min(150, maxWidth);
|
||||
if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
setClipListWidth(
|
||||
Math.max(minWidth, Math.min(maxWidth, preferences.clipListWidth + 10)),
|
||||
);
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
setClipListWidth(
|
||||
Math.max(minWidth, Math.min(maxWidth, preferences.clipListWidth - 10)),
|
||||
);
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
<div class="right-column">
|
||||
<VideoPlayer />
|
||||
<TransportControls bind:this={transportControls} />
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
</div>
|
||||
|
||||
<!-- Bottom clip list: visible only in bottom mode -->
|
||||
<div class="bottom-cliplist" class:hidden={isLeft}>
|
||||
<ClipList onExport={() => (showExportDialog = true)} />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<StatusBar onOpenAbout={() => (showAboutDialog = true)} />
|
||||
</div>
|
||||
@@ -358,20 +333,78 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* --- Grid layout: bottom mode (default) --- */
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"video"
|
||||
"lower"
|
||||
"bottom-clips";
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr auto auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.timeline-clip-area {
|
||||
.video-area {
|
||||
grid-area: video;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lower-section {
|
||||
grid-area: lower;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.bottom-cliplist {
|
||||
grid-area: bottom-clips;
|
||||
min-height: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Sidebar elements hidden in bottom mode */
|
||||
.sidebar-area {
|
||||
grid-area: sidebar;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
border-right: 1px solid var(--border);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.sidebar-handle {
|
||||
grid-area: handle;
|
||||
width: 5px;
|
||||
background: var(--border);
|
||||
cursor: ew-resize;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.sidebar-handle:hover,
|
||||
.sidebar-handle.active {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* --- Grid layout: left mode --- */
|
||||
.content.layout-left {
|
||||
grid-template-areas:
|
||||
"sidebar handle video"
|
||||
"lower lower lower"
|
||||
"bottom-clips bottom-clips bottom-clips";
|
||||
grid-template-columns: var(--sidebar-width, 220px) 5px 1fr;
|
||||
grid-template-rows: 1fr auto 0;
|
||||
}
|
||||
|
||||
.timeline-pane {
|
||||
flex-shrink: 0;
|
||||
min-height: 40px;
|
||||
@@ -391,55 +424,6 @@
|
||||
background: var(--accent, #89b4fa);
|
||||
}
|
||||
|
||||
.cliplist-pane {
|
||||
flex: 1;
|
||||
min-height: 20px;
|
||||
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;
|
||||
|
||||
@@ -28,8 +28,8 @@ const WAVEFORM_LANE_HEIGHT = 40;
|
||||
const CONTROLS_LANE_HEIGHT = 30;
|
||||
const TICK_HEIGHT = 10;
|
||||
const BASE_FONT_SIZE = 12;
|
||||
const MAGNIFY_BONUS = 6;
|
||||
const MAGNIFY_RADIUS = 80;
|
||||
const MAGNIFY_BONUS = 10;
|
||||
const MAGNIFY_RADIUS = 100;
|
||||
|
||||
export function drawTimeline(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
@@ -82,6 +82,21 @@ function drawTimeTicks(
|
||||
ctx.textAlign = 'center';
|
||||
const baselineY = state.height - CONTROLS_LANE_HEIGHT + TICK_HEIGHT + 14;
|
||||
|
||||
// Draw minor ticks first (behind major ticks)
|
||||
const minorInterval = tickInterval / 4;
|
||||
const minorTickHeight = TICK_HEIGHT * 0.5;
|
||||
for (let t = startTick - tickInterval; t <= state.visibleEnd + minorInterval; t += minorInterval) {
|
||||
if (t < 0 || t > duration) continue;
|
||||
if (Math.abs(t % tickInterval) < 0.001 || Math.abs(t % tickInterval - tickInterval) < 0.001) continue;
|
||||
const x = timeToX(t, state);
|
||||
ctx.strokeStyle = '#35354a';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, state.height - CONTROLS_LANE_HEIGHT);
|
||||
ctx.lineTo(x, state.height - CONTROLS_LANE_HEIGHT + minorTickHeight);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw major ticks with labels
|
||||
for (let t = startTick; t <= state.visibleEnd; t += tickInterval) {
|
||||
if (t < 0 || t > duration) continue;
|
||||
const x = timeToX(t, state);
|
||||
@@ -93,15 +108,25 @@ function drawTimeTicks(
|
||||
ctx.stroke();
|
||||
|
||||
let fontSize = BASE_FONT_SIZE;
|
||||
let scale = 0;
|
||||
if (mouseX !== null) {
|
||||
const distance = Math.abs(x - mouseX);
|
||||
if (distance < MAGNIFY_RADIUS) {
|
||||
const scale = (1 + Math.cos((Math.PI * distance) / MAGNIFY_RADIUS)) / 2;
|
||||
scale = (1 + Math.cos((Math.PI * distance) / MAGNIFY_RADIUS)) / 2;
|
||||
fontSize = BASE_FONT_SIZE + MAGNIFY_BONUS * scale;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillStyle = '#6c7086';
|
||||
// Interpolate color from muted (#6c7086) to bright (#cdd6f4)
|
||||
if (scale > 0) {
|
||||
const r = Math.round(108 + (205 - 108) * scale);
|
||||
const g = Math.round(112 + (214 - 112) * scale);
|
||||
const b = Math.round(134 + (244 - 134) * scale);
|
||||
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
|
||||
} else {
|
||||
ctx.fillStyle = '#6c7086';
|
||||
}
|
||||
|
||||
ctx.font = `${Math.round(fontSize)}px -apple-system, sans-serif`;
|
||||
|
||||
const label = formatTickLabel(t);
|
||||
|
||||
Reference in New Issue
Block a user