diff --git a/app/assets/styles.css b/app/assets/styles.css
index 1241fd9..cfdb777 100644
--- a/app/assets/styles.css
+++ b/app/assets/styles.css
@@ -182,6 +182,23 @@ p {
max-width: 640px;
}
+.app-nav {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--spacing-md);
+}
+
+.app-link {
+ color: var(--color-primary);
+ font-weight: var(--font-weight-semibold);
+ font-size: var(--font-size-sm);
+ text-decoration: none;
+}
+
+.app-link:hover {
+ text-decoration: underline;
+}
+
.app-grid {
display: grid;
gap: var(--spacing-lg);
@@ -515,6 +532,229 @@ p {
gap: var(--spacing-xl);
}
+.trim-grid {
+ align-items: start;
+}
+
+.trim-preview {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+}
+
+.trim-hint {
+ margin: 0;
+}
+
+.trim-track {
+ position: relative;
+ height: 96px;
+ border-radius: var(--radius-xl);
+ border: 1px solid var(--color-border);
+ background: linear-gradient(
+ 90deg,
+ var(--color-border-muted) 0%,
+ var(--color-border-muted) 2%,
+ transparent 2%,
+ transparent 20%
+ );
+ background-size: 20% 100%;
+ overflow: hidden;
+}
+
+.trim-waveform {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ color: var(--color-text-faint);
+ opacity: 0.7;
+ z-index: 1;
+}
+
+.trim-track--disabled {
+ opacity: 0.6;
+ pointer-events: none;
+}
+
+.trim-track--skeleton {
+ min-height: 96px;
+ background: linear-gradient(
+ 90deg,
+ var(--color-background) 0%,
+ var(--color-border) 45%,
+ var(--color-background) 90%
+ );
+ background-size: 200% 100%;
+ animation: shimmer 1.8s infinite;
+}
+
+.trim-range {
+ position: absolute;
+ top: 18px;
+ height: 60px;
+ border-radius: var(--radius-md);
+ border: 1px solid var(--color-warning-border);
+ background: var(--color-warning-surface);
+ left: var(--range-left);
+ width: var(--range-width);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--spacing-sm);
+ padding: 0 var(--spacing-lg);
+ box-shadow: inset 0 0 0 1px
+ color-mix(in srgb, var(--color-warning-border) 40%, transparent);
+ z-index: 2;
+}
+
+.trim-range.is-selected {
+ box-shadow: 0 0 0 2px
+ color-mix(in srgb, var(--color-primary) 55%, transparent);
+}
+
+.trim-range-label {
+ font-size: var(--font-size-xs);
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-text-secondary);
+}
+
+.trim-handle {
+ position: absolute;
+ top: 50%;
+ width: 14px;
+ height: 52px;
+ border-radius: var(--radius-sm);
+ background: var(--color-primary);
+ border: 1px solid var(--color-primary-active);
+ box-shadow: var(--shadow-sm);
+ cursor: ew-resize;
+ transform: translate(-50%, -50%);
+}
+
+.trim-handle--start {
+ left: 0;
+}
+
+.trim-handle--end {
+ right: 0;
+ transform: translate(50%, -50%);
+}
+
+.trim-handle:focus-visible {
+ outline: 2px solid var(--color-border-accent);
+ outline-offset: 2px;
+}
+
+.trim-handle-label {
+ position: absolute;
+ top: -18px;
+ font-size: var(--font-size-xs);
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-text-inverse);
+ background: var(--color-surface-inverse);
+ padding: 2px 6px;
+ border-radius: var(--radius-pill);
+ white-space: nowrap;
+}
+
+.trim-handle-label--start {
+ left: 0;
+ transform: translateX(-50%);
+}
+
+.trim-handle-label--end {
+ right: 0;
+ transform: translateX(50%);
+}
+
+.trim-playhead {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 2px;
+ left: var(--playhead);
+ background: var(--color-primary);
+ box-shadow: 0 0 0 1px
+ color-mix(in srgb, var(--color-primary) 40%, transparent);
+ z-index: 3;
+}
+
+.trim-range-list {
+ gap: var(--spacing-sm);
+}
+
+.trim-range-row {
+ border: 1px solid var(--color-border);
+ padding: var(--spacing-md);
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-md);
+ background: var(--color-surface);
+}
+
+.trim-range-summary {
+ border: none;
+ padding: 0;
+ background: transparent;
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-xs);
+ text-align: left;
+ cursor: pointer;
+}
+
+.trim-range-time {
+ font-weight: var(--font-weight-semibold);
+}
+
+.trim-range-fields {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr)) auto;
+ gap: var(--spacing-sm);
+ align-items: end;
+}
+
+.trim-waveform-meta {
+ display: flex;
+ justify-content: flex-end;
+}
+
+.trim-time-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--spacing-md);
+ flex-wrap: wrap;
+}
+
+.trim-progress {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-md);
+}
+
+.trim-progress progress {
+ flex: 1;
+ height: 10px;
+}
+
+.trim-command-card {
+ gap: var(--spacing-lg);
+}
+
+.trim-command-actions {
+ display: flex;
+ gap: var(--spacing-sm);
+ flex-wrap: wrap;
+}
+
+.trim-output {
+ max-height: 240px;
+ overflow-y: auto;
+}
+
.timeline-header {
display: flex;
align-items: flex-start;
@@ -955,4 +1195,8 @@ p {
.timeline-controls {
grid-template-columns: 1fr;
}
+
+ .trim-range-fields {
+ grid-template-columns: 1fr;
+ }
}
diff --git a/app/client/app.tsx b/app/client/app.tsx
index 60b3247..6f4b62d 100644
--- a/app/client/app.tsx
+++ b/app/client/app.tsx
@@ -1,5 +1,14 @@
+import type { Handle } from 'remix/component'
import { EditingWorkspace } from './editing-workspace.tsx'
+import { TrimPoints } from './trim-points.tsx'
-export function App() {
- return () =>
+export function App(handle: Handle) {
+ return () => {
+ const pathname =
+ typeof window === 'undefined' ? '/' : window.location.pathname
+ if (pathname.startsWith('/trim-points')) {
+ return
+ }
+ return
+ }
}
diff --git a/app/client/editing-workspace.tsx b/app/client/editing-workspace.tsx
index 3bb1589..5711509 100644
--- a/app/client/editing-workspace.tsx
+++ b/app/client/editing-workspace.tsx
@@ -492,14 +492,18 @@ export function EditingWorkspace(handle: Handle) {
void requestQueue('/mark-done', { method: 'POST' })
}
+ const cancelActiveTask = (taskId: string) => {
+ void requestQueue(`/task/${encodeURIComponent(taskId)}`, {
+ method: 'DELETE',
+ })
+ }
+
const clearCompletedTasks = () => {
void requestQueue('/clear-completed', { method: 'POST' })
}
const removeTask = (taskId: string) => {
- void requestQueue(`/task/${encodeURIComponent(taskId)}`, {
- method: 'DELETE',
- })
+ cancelActiveTask(taskId)
}
const syncVideoToPlayhead = (value: number) => {
@@ -601,6 +605,11 @@ export function EditingWorkspace(handle: Handle) {
Review transcript-based edits, refine command windows, and prepare
the final CLI export in one place.
+
@@ -761,6 +770,20 @@ export function EditingWorkspace(handle: Handle) {
>
Mark running done
+