199 lines
10 KiB
Markdown
199 lines
10 KiB
Markdown
# v0.1.3 Polish & Tweaks — Design Spec
|
||
|
||
**Date:** 2026-09-22
|
||
**Version:** 0.1.2 → 0.1.3
|
||
|
||
## Overview
|
||
|
||
Three UI polish features for v0.1.3: adjustable clip-list layout, improved timeline timestamp labels with a mouse-proximity magnification effect, and a custom right-click context menu on the timeline.
|
||
|
||
---
|
||
|
||
## Feature 1: Adjustable Layout (Clip List Position)
|
||
|
||
### Current State
|
||
|
||
The main content area is a vertical stack:
|
||
|
||
```
|
||
┌──────────────────────────────┐
|
||
│ Toolbar │
|
||
├──────────────────────────────┤
|
||
│ Video Preview │
|
||
├──────────────────────────────┤
|
||
│ Transport Controls │
|
||
├──────────────────────────────┤
|
||
│ Timeline / Thumbs / Waveform │
|
||
├──────────────────────────────┤
|
||
│ Clip List │
|
||
├──────────────────────────────┤
|
||
│ Status Bar │
|
||
└──────────────────────────────┘
|
||
```
|
||
|
||
### Proposed: Two Layout Modes
|
||
|
||
A new `clipListPosition` preference (`'bottom' | 'left'`) controls where the clip list appears. Default: `'bottom'` (current behavior, unchanged).
|
||
|
||
When set to `'left'`, the layout becomes:
|
||
|
||
```
|
||
┌──────────────────────────────────────────┐
|
||
│ Toolbar │
|
||
├──────────┬───────────────────────────────┤
|
||
│ Clip │ Video Preview │
|
||
│ List │ │
|
||
│ │ │
|
||
│ ├───────────────────────────────┤
|
||
│ │ Transport Controls │
|
||
├──────────┴───────────────────────────────┤
|
||
│ Timeline / Thumbs / Waveform (full) │
|
||
├──────────────────────────────────────────┤
|
||
│ Status Bar │
|
||
└──────────────────────────────────────────┘
|
||
```
|
||
|
||
Key details:
|
||
|
||
- The clip list shares horizontal space only with the video preview and transport controls. The timeline remains full-width underneath — no horizontal space is sacrificed for timeline real estate.
|
||
- A vertical drag-to-resize handle sits between the clip list and the video area, allowing the user to adjust the sidebar width. Default width: ~220px. Min: 150px. Max: 40% of window width.
|
||
- The existing vertical resize handle between timeline and clip list only appears in `'bottom'` mode. In `'left'` mode, the timeline height is controlled by the existing `timelineHeight` state (still resizable via a horizontal drag handle between the upper section and the timeline).
|
||
|
||
### Toggle Button
|
||
|
||
A button in the toolbar (alongside ⚙ and ℹ) toggles between `'bottom'` and `'left'` modes. The icon visually hints at the current layout — e.g. a stacked-rows icon for bottom mode, a sidebar icon for left mode. Clicking it switches immediately and persists the preference.
|
||
|
||
### Persistence
|
||
|
||
- Add `clipListPosition: 'bottom' | 'left'` to the `Preferences` interface in `preferences.svelte.ts`.
|
||
- Add `clipListWidth: number` (default 220) for the sidebar width in left mode.
|
||
- Both are saved/loaded via the existing Tauri store mechanism.
|
||
|
||
### Implementation Notes
|
||
|
||
- `App.svelte` is the only component that needs structural changes. `ClipList.svelte`, `VideoPlayer.svelte`, and `Timeline.svelte` remain unchanged — they just get placed in different containers.
|
||
- In `'left'` mode, the `.content` area becomes:
|
||
- A flex column with two children:
|
||
1. **Upper section** (flex row): `[ClipList sidebar | flex column of [VideoPlayer, TransportControls]]`
|
||
2. **Lower section** (same as before): timeline pane with resize handle
|
||
- The vertical resize handle between the upper and lower sections works the same as the existing one.
|
||
- In `'bottom'` mode: the current layout is preserved exactly.
|
||
- The `ClipList` component needs no changes; it already fills its container. The only adjustment is removing the `border-top` when in left mode (the sidebar should have a `border-right` instead), which can be handled via a CSS class or prop.
|
||
|
||
---
|
||
|
||
## Feature 2: Timeline Timestamp Labels
|
||
|
||
### Current State
|
||
|
||
Timestamp labels are rendered at `10px` font size in `drawTimeTicks()` in `renderer.ts`. They are small and hard to read.
|
||
|
||
### Proposed Changes
|
||
|
||
1. **Increase base font size** from `10px` to `12px`.
|
||
2. **Mouse-proximity magnification effect:** As the cursor moves along the timeline, labels near the cursor smoothly scale up. This creates an invisible "magnifying lens" effect for timestamps.
|
||
|
||
### Magnification Behavior
|
||
|
||
- **Radius:** ~80px from cursor (in canvas pixel space).
|
||
- **Peak size:** ~18px directly under the cursor.
|
||
- **Falloff:** Smooth cosine interpolation from peak to base size. A label 80px away renders at base size; a label directly under the cursor renders at peak size.
|
||
- **Vertical alignment:** Labels stay bottom-aligned as they grow — the text baseline remains constant, so larger labels grow upward. This prevents layout jitter.
|
||
- **Cursor leaves timeline:** When the mouse leaves the canvas (`onmouseleave`), `mouseX` is set to `null` and all labels render at the base size. No magnification in this state.
|
||
|
||
### Implementation Notes
|
||
|
||
- `Timeline.svelte` already handles `onmousemove`. Add tracking of the mouse X coordinate (a simple `let mouseX = $state<number | null>(null)`), updated on `mousemove` and cleared on `mouseleave`.
|
||
- Pass `mouseX` into `drawTimeline()` → `drawTimeTicks()`.
|
||
- `drawTimeTicks()` computes the per-label font size based on distance from `mouseX`:
|
||
```
|
||
distance = abs(labelX - mouseX)
|
||
if distance > MAGNIFY_RADIUS: scale = 0
|
||
else: scale = (1 + cos(PI * distance / MAGNIFY_RADIUS)) / 2
|
||
fontSize = BASE_FONT_SIZE + BONUS_FONT_SIZE * scale
|
||
```
|
||
- Trigger a redraw whenever `mouseX` changes. Since `requestDraw()` already coalesces via `requestAnimationFrame`, this adds no extra frame overhead — it simply piggybacks on the next scheduled frame.
|
||
|
||
---
|
||
|
||
## Feature 3: Timeline Right-Click Context Menu
|
||
|
||
### Current State
|
||
|
||
Right-clicking the timeline shows the default web/Tauri context menu (Reload, Inspect Element), which is not useful.
|
||
|
||
### Proposed: Custom Context Menu
|
||
|
||
A styled context menu appears on right-click anywhere on the timeline canvas. Right-clicking also moves the playhead to the clicked position.
|
||
|
||
### Menu Items
|
||
|
||
| # | Label | Behavior | Availability |
|
||
|---|-------|----------|-------------|
|
||
| 1 | **Mark In** | Sets in-point at the right-clicked time (calls `markInPoint`) | Always |
|
||
| 2 | **Mark Out** | Sets out-point at the right-clicked time (calls `markOutPoint`) | Always |
|
||
| — | *(separator)* | | |
|
||
| 3 | **Go to Clip Start** | Seeks playhead to `selectedClip.startTime` | Disabled if no clip selected |
|
||
| 4 | **Go to Clip End** | Seeks playhead to `selectedClip.endTime` | Disabled if no clip selected |
|
||
| — | *(separator)* | | |
|
||
| 5 | **Delete Clip** | Removes the selected clip | Hidden if no clip selected |
|
||
|
||
### Behavior
|
||
|
||
- **Right-click on canvas:** `contextmenu` event is intercepted, default is prevented. The playhead moves to the clicked time position. The context menu appears at the cursor's screen coordinates.
|
||
- **Selecting an item:** Executes the action and closes the menu.
|
||
- **Closing without action:** Click anywhere outside the menu, press Escape, or scroll — the menu closes.
|
||
- **Positioning:** The menu is rendered as a positioned `<div>` appended at the `App.svelte` level (or uses a portal pattern) so it isn't clipped by any `overflow: hidden` containers. If the menu would extend beyond the window edge, it flips direction (e.g. opens leftward or upward).
|
||
|
||
### Component: `TimelineContextMenu.svelte`
|
||
|
||
Props:
|
||
- `x: number` — screen X position
|
||
- `y: number` — screen Y position
|
||
- `time: number` — the timeline time at the right-click position
|
||
- `selectedClipId: string | null` — to determine which items are enabled/hidden
|
||
- `onAction: (action: ContextMenuAction) => void` — callback for menu item selection
|
||
- `onClose: () => void` — callback to close the menu
|
||
|
||
Actions (discriminated union):
|
||
```typescript
|
||
type ContextMenuAction =
|
||
| { type: 'mark-in' }
|
||
| { type: 'mark-out' }
|
||
| { type: 'go-to-clip-start' }
|
||
| { type: 'go-to-clip-end' }
|
||
| { type: 'delete-clip' };
|
||
```
|
||
|
||
### Styling
|
||
|
||
- Dark theme matching the app's existing style (dark background, light text, hover highlights).
|
||
- Subtle border/shadow to float above the timeline.
|
||
- Disabled items: dimmed text, no hover effect, not clickable.
|
||
- Separators: thin horizontal lines using `var(--border)`.
|
||
|
||
### Implementation Notes
|
||
|
||
- `Timeline.svelte` adds an `oncontextmenu` handler on the canvas element.
|
||
- The handler computes the time from the click X coordinate (using existing `computeClickTime`), seeks the playhead, and emits an event / sets state to show the context menu.
|
||
- The context menu component is rendered conditionally in `Timeline.svelte` (or `App.svelte` if clipping is an issue).
|
||
- Keyboard shortcut hints can optionally be shown in the menu items (e.g. "Mark In I") for discoverability.
|
||
|
||
---
|
||
|
||
## Version Bump
|
||
|
||
All three locations updated from `0.1.2` → `0.1.3`:
|
||
- `package.json` — `version` field
|
||
- `src-tauri/tauri.conf.json` — `version` field
|
||
- `src-tauri/Cargo.toml` — `version` field
|
||
|
||
---
|
||
|
||
## Scope Boundaries
|
||
|
||
- No changes to the backend (Rust). All three features are purely frontend.
|
||
- No changes to clip store logic — existing `markInPoint`, `markOutPoint`, `removeClip`, `selectClip` functions are reused as-is.
|
||
- No new dependencies.
|
||
- The context menu does NOT appear on the minimap — only on the main timeline canvas.
|