Refine clip editor flow

This commit is contained in:
Codex
2026-04-23 15:09:24 -05:00
parent 3347483092
commit 9387556490
2 changed files with 46 additions and 35 deletions

View File

@@ -16,6 +16,7 @@ const MEDIA_ACCEPT =
".mp3,.m4a,.aac,.wav,.ogg,.oga,.flac,.mp4,.m4v,.mov,audio/*,video/*,application/octet-stream"; ".mp3,.m4a,.aac,.wav,.ogg,.oga,.flac,.mp4,.m4v,.mov,audio/*,video/*,application/octet-stream";
const DEFAULT_CLIP_LENGTH_MS = 30_000; const DEFAULT_CLIP_LENGTH_MS = 30_000;
const END_SHORTCUT_LENGTH_MS = 90_000; const END_SHORTCUT_LENGTH_MS = 90_000;
const SAVE_FADE_OUT_MS = 1000;
const TRIM_NUDGE_MS = 100; const TRIM_NUDGE_MS = 100;
const TRIM_STEP_MS = 100; const TRIM_STEP_MS = 100;
const TRIM_ZOOM_WINDOW_MS = 3_000; const TRIM_ZOOM_WINDOW_MS = 3_000;
@@ -64,6 +65,7 @@ export function LibraryPage() {
const { const {
activeKey: previewKey, activeKey: previewKey,
currentTimeMs: previewTimeMs, currentTimeMs: previewTimeMs,
fadeOutClip,
playClip: playClipPreview, playClip: playClipPreview,
stopClip: stopPreview, stopClip: stopPreview,
} = useClipPlayback(); } = useClipPlayback();
@@ -315,6 +317,7 @@ export function LibraryPage() {
teamId={teamId} teamId={teamId}
playerId={playerId} playerId={playerId}
previewTimeMs={previewTimeMs} previewTimeMs={previewTimeMs}
fadeOutPreview={fadeOutClip}
playPreview={playPreview} playPreview={playPreview}
onClose={closeCreateWalkupClip} onClose={closeCreateWalkupClip}
stopPreview={stopPreview} stopPreview={stopPreview}
@@ -374,6 +377,7 @@ function WalkupClipModal({
teamId, teamId,
playerId, playerId,
previewTimeMs, previewTimeMs,
fadeOutPreview,
playPreview, playPreview,
onClose, onClose,
stopPreview, stopPreview,
@@ -385,6 +389,7 @@ function WalkupClipModal({
teamId: string; teamId: string;
playerId: string; playerId: string;
previewTimeMs: number | null; previewTimeMs: number | null;
fadeOutPreview: (durationMs?: number) => void;
playPreview: (clip: AudioClip, startMs?: number, endMs?: number) => Promise<void>; playPreview: (clip: AudioClip, startMs?: number, endMs?: number) => Promise<void>;
onClose: () => void; onClose: () => void;
stopPreview: () => void; stopPreview: () => void;
@@ -594,23 +599,19 @@ function WalkupClipModal({
</button> </button>
</div> </div>
<div className="walkup-modal-body"> <div className="walkup-modal-body">
<div className="walkup-stepper"> <nav aria-label="Walkup clip steps">
<div className={`walkup-step${step === "source" ? " is-active" : " is-complete"}`}>1. Source</div> <ol className="breadcrumb walkup-step-breadcrumb mb-0">
<div className={`walkup-step${step === "editor" ? " is-active" : ""}`}>2. Trim and metadata</div> <li className={`breadcrumb-item${step === "source" ? " active" : ""}`} aria-current={step === "source" ? "page" : undefined}>
</div> Source
</li>
<li className={`breadcrumb-item${step === "editor" ? " active" : ""}`} aria-current={step === "editor" ? "page" : undefined}>
Trim and metadata
</li>
</ol>
</nav>
{step === "source" ? ( {step === "source" ? (
<form className="stack" onSubmit={handleSourceSubmit} aria-busy={createSourceMutation.isPending}> <form className="stack" onSubmit={handleSourceSubmit} aria-busy={createSourceMutation.isPending}>
<label className="field">
Walkup clip name
<input
value={draftLabel}
onChange={(event) => setDraftLabel(event.target.value)}
placeholder="Optional clip name"
autoComplete="off"
disabled={createSourceMutation.isPending}
/>
</label>
<ul className="nav nav-tabs" role="tablist" aria-label="Walkup clip source"> <ul className="nav nav-tabs" role="tablist" aria-label="Walkup clip source">
<li className="nav-item" role="presentation"> <li className="nav-item" role="presentation">
<button <button
@@ -623,7 +624,7 @@ function WalkupClipModal({
disabled={createSourceMutation.isPending} disabled={createSourceMutation.isPending}
onClick={() => setSourceMode("upload")} onClick={() => setSourceMode("upload")}
> >
Upload file File
</button> </button>
</li> </li>
<li className="nav-item" role="presentation"> <li className="nav-item" role="presentation">
@@ -637,7 +638,7 @@ function WalkupClipModal({
disabled={createSourceMutation.isPending} disabled={createSourceMutation.isPending}
onClick={() => setSourceMode("url")} onClick={() => setSourceMode("url")}
> >
Import URL URL
</button> </button>
</li> </li>
<li className="nav-item" role="presentation"> <li className="nav-item" role="presentation">
@@ -651,7 +652,7 @@ function WalkupClipModal({
disabled={createSourceMutation.isPending} disabled={createSourceMutation.isPending}
onClick={() => setSourceMode("existing")} onClick={() => setSourceMode("existing")}
> >
Existing media Existing
</button> </button>
</li> </li>
</ul> </ul>
@@ -663,6 +664,7 @@ function WalkupClipModal({
className={`tab-pane fade${sourceMode === "upload" ? " show active" : ""}`} className={`tab-pane fade${sourceMode === "upload" ? " show active" : ""}`}
> >
<div className="stack"> <div className="stack">
<div className="muted">Upload a local audio file to create a new walkup clip.</div>
<label className="field"> <label className="field">
Media title Media title
<input <input
@@ -692,6 +694,7 @@ function WalkupClipModal({
className={`tab-pane fade${sourceMode === "url" ? " show active" : ""}`} className={`tab-pane fade${sourceMode === "url" ? " show active" : ""}`}
> >
<div className="stack"> <div className="stack">
<div className="muted">Paste a link and we will download the audio for clip creation.</div>
<label className="field"> <label className="field">
Media title Media title
<input <input
@@ -723,6 +726,7 @@ function WalkupClipModal({
className={`tab-pane fade${sourceMode === "existing" ? " show active" : ""}`} className={`tab-pane fade${sourceMode === "existing" ? " show active" : ""}`}
> >
<div className="stack"> <div className="stack">
<div className="muted">Pick an existing media file to turn into a walkup clip.</div>
<label className="field"> <label className="field">
Existing media file Existing media file
<select <select
@@ -775,11 +779,17 @@ function WalkupClipModal({
previewTimeMs={previewTimeMs} previewTimeMs={previewTimeMs}
playerId={playerId} playerId={playerId}
onSaveComplete={async () => { onSaveComplete={async () => {
if (previewTimeMs !== null) {
fadeOutPreview(SAVE_FADE_OUT_MS);
await new Promise<void>((resolve) => {
window.setTimeout(resolve, SAVE_FADE_OUT_MS);
});
}
await Promise.all([ await Promise.all([
queryClient.invalidateQueries({ queryKey: ["assets", teamId, playerId] }), queryClient.invalidateQueries({ queryKey: ["assets", teamId, playerId] }),
queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }), queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }),
]); ]);
handleClose(); onClose();
}} }}
saveButtonLabel={isCreateMode ? "Save walk up clip" : "Save changes"} saveButtonLabel={isCreateMode ? "Save walk up clip" : "Save changes"}
introText={ introText={

View File

@@ -174,28 +174,29 @@ select {
padding: 1.25rem; padding: 1.25rem;
} }
.walkup-stepper { .walkup-step-breadcrumb {
display: flex; --bs-breadcrumb-divider: "";
gap: 0.75rem; align-items: center;
flex-wrap: wrap;
}
.walkup-step {
padding: 0.4rem 0.7rem;
border-radius: 999px;
background: rgba(19, 34, 56, 0.08);
color: var(--muted); color: var(--muted);
font-size: 0.85rem; margin-bottom: 0;
} }
.walkup-step.is-active { .walkup-step-breadcrumb .breadcrumb-item {
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.walkup-step-breadcrumb .breadcrumb-item + .breadcrumb-item::before {
color: var(--muted);
}
.walkup-step-breadcrumb .breadcrumb-item.active {
padding: 0.32rem 0.65rem;
border-radius: 999px;
background: var(--accent-soft); background: var(--accent-soft);
color: var(--ink); color: var(--ink);
} font-weight: 600;
.walkup-step.is-complete {
background: rgba(47, 158, 68, 0.14);
color: #25643b;
} }
.walkup-modal-actions { .walkup-modal-actions {