From be443734992cce31b5b786d5e3db7d60a8054883 Mon Sep 17 00:00:00 2001 From: KyleTryon Date: Wed, 9 Sep 2026 15:52:10 -0400 Subject: [PATCH 1/5] feat(frontend): improve mobile editor layout and controls --- .../src/components/editor/EditorControls.tsx | 132 ++++++++---- .../editor/EditorEditableTimecode.tsx | 92 +++++---- .../components/editor/EditorEditingTools.tsx | 191 ++++++++++++++++-- .../src/components/editor/EditorLayout.tsx | 63 +++--- .../editor/EditorPlaybackSourcePanel.tsx | 91 +++------ .../editor/EditorPreviewTimecode.tsx | 9 +- .../src/components/editor/EditorScreen.tsx | 66 +++--- .../src/components/editor/EditorTimeline.tsx | 103 +++++++--- .../src/components/frontendWorkflow.test.ts | 17 +- .../src/components/ui/scroll-area.tsx | 14 ++ 10 files changed, 510 insertions(+), 268 deletions(-) diff --git a/apps/frontend/src/components/editor/EditorControls.tsx b/apps/frontend/src/components/editor/EditorControls.tsx index c5a928a5..e9eecb7a 100644 --- a/apps/frontend/src/components/editor/EditorControls.tsx +++ b/apps/frontend/src/components/editor/EditorControls.tsx @@ -1,4 +1,9 @@ -import { useMemo, type CSSProperties, type ReactElement } from "react"; +import { + useMemo, + type CSSProperties, + type ReactElement, + type ReactNode, +} from "react"; import { Camera, Pause, @@ -24,6 +29,7 @@ import { DrawerTrigger, } from "@/components/ui/drawer"; import { EditorEditableTimecode } from "@/components/editor/EditorEditableTimecode"; +import { handleHorizontalScrollKeyDown } from "@/components/ui/scroll-area"; import { EditorPreviewTimecode } from "@/components/editor/EditorPreviewTimecode"; import { formatTime, @@ -34,6 +40,7 @@ type EditorControlsVariant = "desktop" | "mobile"; interface EditorControlsProperties { variant?: EditorControlsVariant; + playbackSourcePanel: ReactNode; playing: boolean; loadingPreview: boolean; togglePlay: () => void; @@ -87,6 +94,7 @@ function ControlTooltip({ export function EditorControls({ variant = "desktop", + playbackSourcePanel, playing, loadingPreview, togglePlay, @@ -131,6 +139,9 @@ export function EditorControls({ }), [duration], ); + const mobileClipMetricStyle: CSSProperties = { + minWidth: `max(8rem, ${formatTimecodeInput(duration).length + 2}ch)`, + }; const volumeRangeFillPercent = `${ Math.min(Math.max(muted ? 0 : volume, 0), 1) * 100 }%`; @@ -170,6 +181,11 @@ export function EditorControls({ > @@ -277,7 +293,7 @@ export function EditorControls({ className={rangeActionButtonClassName} aria-label="Set in point at the playhead" > - In + Set in - Out + Set out - {clipMetrics.map((metric) => ( -
- + {clipMetrics.map((metric) => { + const label = ( + {metric.label} - {metric.onCommit ? ( - - + ); + return ( +
+ {(variant === "desktop" || !metric.onCommit) && label} + {metric.onCommit ? ( + + {variant === "mobile" && label} + + {formatTime(metric.value)} + + + ) : ( + {formatTime(metric.value)} - - ) : ( - - {formatTime(metric.value)} - - )} -
- ))} + )} +
+ ); + })} ); @@ -360,7 +403,7 @@ export function EditorControls({
{playControl}
-
+
{previewTimeControl}
@@ -381,6 +424,7 @@ export function EditorControls({
+
{playbackSourcePanel}
Playback @@ -413,8 +457,14 @@ export function EditorControls({
-
- {editableClipMetrics} +
+
{editableClipMetrics}
{rangeActions} diff --git a/apps/frontend/src/components/editor/EditorEditableTimecode.tsx b/apps/frontend/src/components/editor/EditorEditableTimecode.tsx index fb373e2b..2aa3136f 100644 --- a/apps/frontend/src/components/editor/EditorEditableTimecode.tsx +++ b/apps/frontend/src/components/editor/EditorEditableTimecode.tsx @@ -12,6 +12,8 @@ import { formatTimecodeInput, parseTimecodeInput, } from "@/components/editor/editorUtilities"; +import { cn } from "@/lib/utilities"; +import { Popover } from "radix-ui"; interface EditorEditableTimecodeProperties { ariaLabel: string; @@ -139,30 +141,52 @@ export function EditorEditableTimecode({ }} > {editing ? ( - commitDraft({ restoreFocus: false })} - onChange={(event) => { - setDraftValue(event.target.value); - setInvalid(false); - }} - onKeyDown={handleInputKeyDown} - spellCheck={false} - style={{ width: inputWidth ?? "100%" }} - type="text" - value={draftValue} - /> + + + commitDraft({ restoreFocus: false })} + onChange={(event) => { + setDraftValue(event.target.value); + setInvalid(false); + }} + onKeyDown={handleInputKeyDown} + spellCheck={false} + style={{ width: inputWidth ?? "100%" }} + type="text" + value={draftValue} + /> + + + event.preventDefault()} + onCloseAutoFocus={(event) => event.preventDefault()} + className="z-[60] w-52 max-w-[70vw] rounded-md border border-destructive bg-popover p-2 text-xs font-normal text-popover-foreground shadow-md" + > + Enter seconds (12.5), m:ss (1:23), or h:mm:ss (1:02:03). Press + Escape to discard this edit. + + + ) : ( + ); + const resetButton = ( + + + + + Reset draft + + ); + const resetDescription = + "This removes the saved clip range and subtitle edits from this device. Your original video is unchanged."; + const resetActions = ( +
+ + +
+ ); return ( -
+
- + + {draftNotice} + + {mobile ? ( + + + + + + + Editor options + + Manage this device’s draft and view keyboard shortcuts. + + + {shortcutsButton} + {resetButton} + + + ) : ( + <> + {shortcutsButton} + {resetButton} + + )} + {mobile ? ( + + { + event.preventDefault(); + cancelResetReference.current?.focus(); + }} + onCloseAutoFocus={(event) => { + event.preventDefault(); + menuTriggerReference.current?.focus(); + }} + > + + Reset draft? + {resetDescription} + + {resetActions} + + + ) : ( + setResetOpen(false)} + title="Reset draft?" + description={resetDescription} + closeLabel="Close reset confirmation" + initialFocus={cancelResetReference} + > + {resetActions} + + )} setHelpOpen(false)} diff --git a/apps/frontend/src/components/editor/EditorLayout.tsx b/apps/frontend/src/components/editor/EditorLayout.tsx index f7b78fc3..fe76ad08 100644 --- a/apps/frontend/src/components/editor/EditorLayout.tsx +++ b/apps/frontend/src/components/editor/EditorLayout.tsx @@ -46,13 +46,20 @@ export function EditorPreviewPane({ : cliparrMotionTransitions.fast; const previewStage = (
- {children} + {variant === "mobile" ? ( +
+ {children} +
+ ) : ( + children + )}
); @@ -93,11 +100,6 @@ export function EditorTimelinePane({ hasDuration: boolean; timeline: ReactNode; }) { - const reduceMotion = useReducedMotion(); - const stateTransition = reduceMotion - ? { duration: 0 } - : cliparrMotionTransitions.standard; - return (
{controls} - - {hasDuration ? ( - - {timeline} - - ) : ( - +
+ {timeline} +
+ {!hasDuration && ( +
Waiting for media duration. - +
)} -
+
); } @@ -271,13 +267,11 @@ export function EditorDesktopLayout({ export function EditorMobileLayout({ error, - playbackSourcePanel, previewPane, timelinePane, subtitlePanel, }: { error: string | null; - playbackSourcePanel: ReactNode; previewPane: ReactNode; timelinePane: ReactNode; subtitlePanel: ReactNode; @@ -290,7 +284,6 @@ export function EditorMobileLayout({
)} - {playbackSourcePanel} {previewPane} {timelinePane} {subtitlePanel} diff --git a/apps/frontend/src/components/editor/EditorPlaybackSourcePanel.tsx b/apps/frontend/src/components/editor/EditorPlaybackSourcePanel.tsx index 6272c083..3be93aea 100644 --- a/apps/frontend/src/components/editor/EditorPlaybackSourcePanel.tsx +++ b/apps/frontend/src/components/editor/EditorPlaybackSourcePanel.tsx @@ -1,89 +1,44 @@ -import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { EditorPropertyRow } from "@/components/editor/EditorPropertyControls"; import { cn } from "@/lib/utilities"; -import { cliparrMotionTransitions } from "@/lib/motionPresets"; interface EditorPlaybackSourcePanelProperties { previewSourceLabel: string; fallbackMessage: string | null; - hasHlsSource: boolean; className?: string; } function displaySourceLabel(label: string) { - if (label === "Direct source") { - return "Direct media"; + switch (label) { + case "HLS stream": + case "HLS URL": { + return "HLS"; + } + case "Direct source": { + return "Direct"; + } + default: { + return label.trim() || "Resolving stream"; + } } - - if (!label.trim()) { - return "Resolving stream"; - } - - return label; } -const SOURCE_NOTE_INITIAL = { - opacity: 0, - y: 4, - filter: "blur(6px)", -}; -const SOURCE_NOTE_VISIBLE = { - opacity: 1, - y: 0, - filter: "blur(0px)", -}; -const SOURCE_NOTE_EXIT = { - opacity: 0, - y: -3, - filter: "blur(6px)", -}; - export function EditorPlaybackSourcePanel({ previewSourceLabel, fallbackMessage, - hasHlsSource, className, }: EditorPlaybackSourcePanelProperties) { - const reduceMotion = useReducedMotion(); - const transition = reduceMotion - ? { duration: 0 } - : cliparrMotionTransitions.fast; - const sourceNote = - fallbackMessage ?? - (!hasHlsSource && previewSourceLabel === "Direct source" - ? "Direct media only." - : null); - return ( -
-
-
-
- Preview Source -
-
- -
-
- {displaySourceLabel(previewSourceLabel)} -
- - - {sourceNote && ( - - {sourceNote} - - )} - -
-
+
+ + + {displaySourceLabel(previewSourceLabel)} + + + {fallbackMessage && ( +

+ {fallbackMessage} +

+ )}
); } diff --git a/apps/frontend/src/components/editor/EditorPreviewTimecode.tsx b/apps/frontend/src/components/editor/EditorPreviewTimecode.tsx index 5e686f7c..74aa02c4 100644 --- a/apps/frontend/src/components/editor/EditorPreviewTimecode.tsx +++ b/apps/frontend/src/components/editor/EditorPreviewTimecode.tsx @@ -1,7 +1,9 @@ import { memo, useLayoutEffect, useMemo, useRef, type RefObject } from "react"; +import { cn } from "@/lib/utilities"; interface EditorPreviewTimecodeProperties { ariaHidden?: boolean; + className?: string; currentTime: number; duration: number; } @@ -123,6 +125,7 @@ function arePreviewTimecodePropertiesEqual( ) { return ( previous.ariaHidden === next.ariaHidden && + previous.className === next.className && getPreviewCentiseconds(previous.currentTime) === getPreviewCentiseconds(next.currentTime) && getPreviewCentiseconds(previous.duration) === @@ -171,6 +174,7 @@ const TimecodeShell = memo(function TimecodeShell({ export const EditorPreviewTimecode = memo(function EditorPreviewTimecode({ ariaHidden = false, + className, currentTime, duration, }: EditorPreviewTimecodeProperties) { @@ -222,7 +226,10 @@ export const EditorPreviewTimecode = memo(function EditorPreviewTimecode({