Mobile QA Testing Guide¶
Comprehensive manual testing checklist for the Sapari mobile app. Run before every major release.
Tests are organized by workflow order and grouped to avoid redundant retesting. Each section builds on previous ones — complete them in order. Sections marked with [P+L] require testing in both portrait and landscape orientations.
Prerequisites¶
- Mobile browser or Chrome DevTools device emulation (iPhone 14 Pro recommended)
- Window width < 768px (mobile breakpoint)
- Test video file (30s+, with speech and silence)
- Test assets: one video, one audio, one image
- Backend running locally or connected to staging
- For landscape testing: enable auto-rotate (or use DevTools rotation toggle)
- For haptic testing: use a physical iOS/Android device
0. Onboarding (First-Time User)¶
Upload Step Tour (4 steps)¶
- First login → spotlight tour on upload step
- Step 1: Upload zone highlighted (tooltip below)
- Step 2: Assets tab in bottom nav (tooltip above)
- Step 3: Projects tab (tooltip above)
- Step 4: Settings tab (tooltip above)
- Skip/Done dismisses, doesn't show again
Configure Step Tour (8 steps, auto tab-switching)¶
- First time reaching Configure → second tour starts
- Step 1: Tab toggle (CUTS/AUDIO/ASSETS) highlighted
- Step 2: Pacing Style (stays on Cuts tab)
- Step 3: False Starts (stays on Cuts tab)
- Step 4: Clean Audio (switches to Audio tab)
- Step 5: Caption Language (stays on Audio tab)
- Step 6: Profanity (stays on Audio tab)
- Step 7: Asset injection (switches to Assets tab)
- Step 8: START PROCESSING button
- After dismiss → resets to Cuts tab
- Touch targets work — tooltip doesn't cover key actions
- Dark mode correct
0b. Playback Speed Control¶
- Speed button beside volume (bottom-left of video)
- Tap button to toggle vertical slider (same pattern as volume)
- Works in both portrait and landscape
- Button shows current speed, orange when not 1x
- Timeline overview (Layers) button hidden when no edits
CC Toggle (Video Preview)¶
- CC button visible bottom-left of video (next to speed button)
- Tap CC → captions hidden, button dims to white
- Tap CC again → captions shown, button turns orange
- Toggle is preview-only — does NOT affect the export panel's "captions enabled" setting (export burn-in stays on while preview overlay is hidden, and vice versa)
- Works in both portrait and landscape
1. Authentication & Settings¶
Login¶
- Enter valid email + password → redirects to project list
- Enter invalid credentials → error message appears
- Submit with empty fields → validation prevents submit
- Keyboard submit works (tap Done/Go on mobile keyboard)
- "Remember me" checkbox visible and tappable (44px target)
- Login with "Remember me" → persistent session (~30 days)
- Login without → standard session (~8h)
- Login as unverified user → "Resend verification email" tappable link appears (44px target)
- Tap "Resend verification email" → status changes to "Verification email sent. Check your inbox."
- After ~10 fast failed login attempts → "Too many failed login attempts" message (not "Incorrect password")
Email verification¶
- Open verification link from email on mobile → "Verified" success page renders
- Tap expired link → "Invalid or expired verification link" (no false success)
Change Password (Account Settings)¶
- Change password with correct current password → success message, auto-redirects to login after ~2s
- Login with new password works
- Wrong current password → error message
- OAuth user → can set password without current password
Change Email (Account Settings)¶
- Collapsible "Change Email" in Account tab (collapsed by default)
- Enter new email + password → success message
- Wrong password → error
- Touch targets 44px on all inputs and button
- Confirmation page at
/confirm-email-change?token=...works
Change Name (Account Settings)¶
- Tap name in profile → inline edit → OK saves, X cancels
- Name updates immediately after save
- Touch targets 44px on edit input and buttons
Delete Account (Account Settings)¶
- "Delete Account" button in Account tab
- Email/password user: tap → password field + "Type DELETE" confirmation; both required
- Email/password user: wrong password → error message
- Email/password user: correct password + DELETE → account deactivated, logged out
- OAuth user (Google/GitHub, no local password): tap → only "Type DELETE" field (no password input)
- OAuth user: typing DELETE enables Confirm Delete; tap → account deactivated, logged out
- Cancel resets all fields
- Touch targets 44px on all interactive elements
Settings Tab (4 tabs: Account / Billing / Credits / Prefs)¶
- Account tab: avatar, username, email, Sign Out
- Prefs tab: theme toggle (Light / Dark / Auto), language selector
- Theme preference persists after page reload
- Dark mode: all backgrounds, text, borders use dark variants
- Sign Out → returns to login screen
- For Billing + Credits tab tests, see Mobile Billing QA
2. Project Management¶
Create & List¶
- Tap NEW (orange + button) → new project appears
- Empty state shows appropriate message
- Projects sorted newest-first
- Project cards show: thumbnail (or letter fallback), name, status badge, date
- After first clip processed, project card shows video frame thumbnail
Delete¶
- Trash icon on each project card (right side)
- Tap trash → inline DELETE / CANCEL confirmation (date hidden to save space)
- Tap DELETE → project removed, card disappears
- Tap CANCEL → confirmation dismissed, card returns to normal
- Tapping project card while confirmation showing does nothing (no accidental navigation)
Resume¶
- Tap project card → opens project in editor
- Status badges show correctly: COMPLETE (green), PROCESSING (yellow)
- Tapping a different project while another is already open shows a brief "Loading project…" placeholder while the new project's clips are restored, then renders the editor — no flash of empty editor and no 403s in the network tab from clip-proxy URL fetches keyed on the previous project.
3. File Upload & Import¶
Upload¶
- Tap upload area → file picker opens
- Select video → upload progress bar shows
- Upload completes → file listed with checkmark
- Upload fails → error message displayed
Multipart Upload (files ≥ 25 MiB)¶
- Pick a 100 MB video from camera roll → multipart path; CANCEL UPLOAD button visible next to progress bar
- Tap CANCEL UPLOAD mid-upload → progress disappears, placeholder removed (no "failed" toast)
- Pick the same file again after cancel → resumes from the next missing part (don't re-PUT the parts already on R2)
- Background the app mid-upload (switch to home screen) → return to app → upload resumes / progress catches up via SSE or polling
- Tab kill / safari purge mid-upload → reopen project, pick same file → IndexedDB-backed resume kicks in
- Successful complete → pick same file again → fresh upload (no resume because state was cleared on success)
Start-new gate (re-entering upload step with a project loaded)¶
- Open an existing project (review or export step). Tap the hamburger in the wizard header to open the BottomNav slide-up, then tap
home(BottomNav is hidden by default in review/export — the hamburger is the only entry point). - When the upload step appears with a project still loaded, the file picker / YouTube import are replaced by a "START NEW PROJECT" CTA that explains "You have a project loaded. Tap below to start fresh, or go to Projects to revisit it."
- Tap START NEW PROJECT → previous project is cleared, file picker reappears.
- Mid-upload (file picked, upload still in flight): the gate does NOT appear. An in-progress upload is never blocked.
- The "+ NEW" button in the projects tab still works as before (clears state and lands on the upload step ungated).
Clip Reorder¶
- Upload 2+ clips → drag handles visible
- Drag clip to new position → clips reorder
- Refresh → order persists
YouTube Import¶
- Paste YouTube URL → IMPORT button activates
- Tap IMPORT → download progress shown
- Successful import → asset appears in library
- Invalid URL → error message
3b. Failed-Clip Recovery [P+L]¶
The same Dashboard orchestrator runs on mobile, so the failure surface is the shared full-page screen — NOT a panel inside the timeline / SwipeReview view. When any clip in the open project has status === 'failed' OR file_status === 'failed', the entire dashboard is replaced by the failure screen with one row per failed clip and Retry / Remove buttons. The dual-state check (isClipFailed in frontend/features/clips/types.ts) covers both the per-project lifecycle and the underlying ClipFile artifact state.
Retry calls POST /api/v1/projects/{project_uuid}/clips/{clip_uuid}/retry, which dispatches to one of three workers depending on the failure stage:
- YouTube import failed at download — clip has
youtube_video_idset but nostorage_key. Re-queuesdownload_youtube_video; clip status resets toIMPORTING. - Upload-flow processing failed — file uploaded but artifact processing failed (no
audio_keyyet). Re-queuesprocess_clip_artifacts; status resets toUPLOADED. - Proxy generation failed — artifacts done but proxy missing (
audio_keyset,proxy_keyNULL,web_compatible=falsein metadata). Re-queuesgenerate_clip_proxy; status resets toUPLOADED.
Modes 2 and 3 are hard to provoke without dev-tools access — flag those rows as requires staging or dev env if you can't reproduce them.
Failure Screen Appearance¶
- Project with at least one failed clip → full-page failure screen replaces the wizard / SwipeReview / editor — BottomNav and MobileWizard header are not visible (the screen is its own layout, not a step inside the wizard)
- Heading "Clip processing failed" with subtext "We couldn't process this clip." (one) or "We couldn't process N clips." (multiple) plus the "Retry to try again — already-processed parts are reused so it's usually fast." line
- Each failed clip is a row: filename on the left (truncates with ellipsis on narrow screens), Retry + Remove on the right
- Layout doesn't overflow on a 375px-wide viewport — long filenames truncate, button column stays anchored to the right
- Failure screen takes priority over the "Loading project…" sync placeholder
- Gated on
filesMatchProject— switching from one project to another with the project picker never shows a different project's failed clips during the resume window - Dark mode: heading + subtext + clip rows + buttons all use dark variants
Touch Targets¶
- Retry button: vertical hit area meets the 44px WCAG 2.5.5 minimum (the
px-3 py-1style withfont-mono text-[10px]is small visually — verify by tapping near the button edges on a real device, not by emulating with a mouse) - Remove button: same 44px check
- "← Back to projects" link: same 44px check
- Buttons are spaced enough that an adjacent tap doesn't trigger the wrong action
Retry Per Failure Mode¶
- YouTube download failure (provoke: import a known-broken YouTube URL — private video, deleted video, or an unsupported URL pattern): tap Retry →
POST /clips/{uuid}/retryreturns 200 → SSE clears the failure → screen reflows to the normal mobile dashboard (upload step / configure step / SwipeReview, depending on project state) - Upload-flow processing failure (requires staging or dev env): tap Retry →
process_clip_artifactsre-runs → failure clears - Proxy generation failure (requires staging or dev env): tap Retry →
generate_clip_proxyre-runs → failure clears - Network tab confirms exactly ONE retry POST per tap (no duplicate dispatches from a fast double-tap)
Retry Button States¶
- Retry is disabled while the mutation is in flight — rapid double-tap is a no-op (prevents two parallel worker dispatches)
- Disabled state visibly distinct (opacity reduced, no hover/active feedback)
- After the mutation resolves, button re-enables OR the row disappears if the retry cleared the failure
Remove Button¶
- Tap Remove → only that clip is removed from the project
- Other (non-failed) clips remain intact in the backend
- Removing the last failed clip → screen reflows to normal mobile dashboard (no manual refresh)
- Removing one of several failed clips → row disappears, remaining failed clips stay, count in subtext updates
Back to Projects¶
- Tap "← Back to projects" → navigates to
/projects - Re-opening the same project from the projects list while failures still exist → failure screen renders again (server-derived state, not session-scoped)
Recovery to Normal View¶
- All failed clips removed (or successfully retried) → dashboard returns to the normal mobile flow — no page reload required
- Orientation: rotating the device while the failure screen is visible → layout reflows; no stuck state. Failure screen is identical in portrait and landscape (no special compact mode)
BottomNav / Wizard Interaction¶
- The failure screen is NOT a wizard step, so the BottomNav-hidden-in-immersive-modes behaviour from §13c does not apply here. The screen renders a full viewport with no nav and no hamburger.
- No interaction with FocusMode / CutFocusMode / AssetFocusMode — those overlays are entered from SwipeReview, which the failure screen pre-empts entirely.
4. Analysis Settings¶
Silence & False Starts¶
- Pacing buttons: Off / Natural / Balanced / Fast → selected button highlighted
- False start buttons: Off / Moderate / Aggressive → selected button highlighted
- Selecting Off disables that detection type
Audio & Captions¶
- Clean Sweep toggle: on/off visually distinct
- Caption language dropdown: selection persists
Presets¶
- No presets by default -- only "Custom" shown initially
- Save current settings as preset → appears in preset list
- Tap saved preset → pacing, false starts, and other settings auto-apply
- Delete preset → removed from list
Cost Estimate & Analysis Mode¶
- Nothing toggled → shows "MANUAL - FREE" above START PROCESSING
- Set only language → shows "CAPTIONS - ~X AI MIN" (0.5x)
- Enable pacing or false starts → shows "AI EDIT - ~X AI MIN" (1.0x)
- Hint text below cost (no tooltip on mobile)
- Switching settings updates cost in real time
Run Analysis¶
- Tap ANALYZE → processing begins
- Progress updates shown in real-time
- Analysis completes → transitions to review screen
Re-analyze & Run Switching¶
- Back button from Review → Configure step (re-analyze flow)
- Configure shows collapsible "Previous Analyses" dropdown when runs exist
- Dropdown collapsed by default, chevron toggle to expand
- Run cards with orange active state, "N cuts · M inserts" labels
- Tap any run → navigates to Review with that run's edits
- Tap already-active run → still navigates to Review
- Tap inactive run → loading spinner, backend activates, edits switch
- Touch targets min 44px on run cards
- Dark mode: run cards styled correctly
- Re-analyze: adjust settings, tap START PROCESSING → new run created, old preserved
5. Swipe Review — Edits Tab [P+L]¶
Card Swiping¶
[image to be added]
- Swipe right → card rotates clockwise, green tint → edit accepted
- Swipe left → card rotates counter-clockwise, red tint → edit dismissed
- Card snaps back if swipe isn't decisive enough
- Edit counter updates (e.g., "1 / 45")
- Status badge shows "ACCEPTED" or "DISMISSED"
Card Details¶
The card uses a header / body / footer disposition. Content distributes via justify-between so the card adapts to whatever vertical space the column gives it.
- Header (always visible, top-pinned): edit type badge on the left (SILENCE orange, FALSE_START purple, MANUAL cyan, ASSET blue, KEEP yellow). Focus + Trash action icons on the right.
- Body (conditional, vertically centered): description text + "Outside keep region" warning. Renders only when there's actual content. SILENCE edits with no useful description: body is omitted; header at top + footer at bottom with breathing space.
- Footer (always visible, bottom-pinned): time range with duration on the left (e.g.,
00:05.250 → 00:08.100 · 2.9s). Status pill on the right (ACCEPTEDorange /DISMISSEDgray). - Description text is center-aligned and bold when present.
- Warning ("Outside keep region") shown as a yellow-bordered pill with
EyeOfficon, only when the edit falls outside an active keep region. - Keep cards have dashed yellow border (distinct from solid borders on other types).
- When the card has lots of vertical space (cramped landscape with sparse content): header + footer space apart with empty middle. When constrained: stacks tight.
Add Edit Menu¶
- Portrait: +ADD button opens dropdown with "Cut here" and "Keep here" options
- Dropdown dismisses on tap outside
- "Cut here" creates a manual cut at current playback position
- "Keep here" creates a keep region at current playback position
- Landscape: inline CUT (scissors) and KEEP (check) icon buttons — no dropdown
- Keep regions render on timeline as dashed yellow border with transparent fill
- Keep regions dim areas outside them (dark overlay on non-keep areas)
Inline Actions¶
- Accept button → activates edit
- Dismiss button → deactivates edit
- Trash icon on card → triggers bottom bar DELETE/CANCEL confirmation (single confirmation, not duplicated)
- Focus button (asset edits) → opens AssetFocusMode
List View¶
- Toggle list view → all edits shown as scrollable list
- Auto-scroll highlights current edit
- Tap list item → navigates to that edit
- Keep edits show yellow label in list
Landscape Layout¶
- Controls column on the visual LEFT, video on the RIGHT (mirrors iMovie/LumaFusion convention). Implemented via
flex-row-reverseso the back arrow + mode tabs + toolbar live in the screen's top-left where users instinctively look for navigation. - Video column width adapts to source aspect ratio (narrow for 9:16, wider for 16:9). Right side of screen.
- Swipe left/right on the card to navigate edits (gesture-on-card preserved).
- Back arrow (
<) appears as the first item in the toolbar row, not in a dedicated header bar (MobileWizard's compact-mode header is suppressed in landscape review). - ACCEPT / DISMISS / DELETE action bar pinned at the bottom of the controls column.
- Card stretches to fill the right-column height available between toolbar and action bar (
flex-1). Header / body / footer disposition adapts: sparse SILENCE edits get header-top + footer-bottom + empty middle; rich asset edits get header + body + footer distributed evenly. - List view, focus button, and delete all work in landscape.
6. Swipe Review — Format Tab [P+L]¶
Format Selection¶
[image to be added]
The 5 cards (Original, YouTube, TikTok, Square, Custom) auto-switch between two layouts:
- 2-row mode (3-2 grid) — default in portrait when Custom is NOT selected. Top row: Original / YouTube / TikTok. Bottom row: Square / Custom. Each card is
h-20(80px) with the icon centered on top and label + desc below. All cards equalflex-1width within their row. - 1-row mode (single horizontal) — used in landscape (compact) AND in portrait when Custom is selected (the W/H aspect-ratio picker takes a row below). Selected card expands to ~38% of the row width, others collapse to icon-only. Width animates 200ms.
QA items:
- On first paint in portrait with no prior preset, layout is the 3-2 grid. Original card has
bg-black text-white(selected). Other 4 cards havebg-white border-gray-300. - All 5 cards always show: shape (rectangle whose proportions reflect the actual aspect ratio) + label + desc.
- Original card's icon shape reflects the detected source aspect ratio when available; falls back to 16:9 if unknown.
- Tap Original → native aspect ratio (no letterboxing). Layout stays in 3-2 grid mode.
- Tap YouTube → 16:9. Previously-selected card un-highlights; YouTube highlights. Still 3-2 grid.
- Tap TikTok → 9:16 (TikTok / Reels). Still 3-2 grid.
- Tap Square → 1:1 (Instagram). Still 3-2 grid.
- Tap Custom → layout switches to single-row mode. Custom card expands; other 4 collapse to icon-only. The W/H aspect-ratio picker row appears below the format row.
- From Custom-selected (single-row), tap any standard format → layout switches back to 3-2 grid; W/H picker disappears.
- In landscape (compact mode), layout is single-row regardless of which card is selected.
- Selection state uses brutalist active:
bg-black text-whitewith icon border in white. Inactive cards:bg-white border-gray-300. - Width transitions are 200ms in single-row mode; layout-mode switches (2-row ↔ 1-row) are instant (no smooth animation across the structural change).
Custom Format¶
- Tap Custom card → layout flips to single-row, W/H number inputs appear below.
- Enter valid values (1-32 range) → custom ratio applied; the Custom card's icon shape morphs to the new ratio in real-time.
- Invalid values rejected (clamped to 1-32).
- Tap a different format card → layout flips back to 3-2 grid, W/H input row disappears. The W/H values are remembered for next time Custom is tapped.
- Saved presets with non-standard ratios (e.g.,
4:3) light up the Custom card (single-row mode) and seed the W/H inputs from the preset value on first paint.
Letterbox Background¶
- Color swatches: black, white → tap to select.
- Custom color (rainbow conic-gradient) picker → opens system color picker.
- Selected color shown as letterbox bars in video preview.
- BG row is hidden when CROP mode is active (no bars to color).
Presets¶
- Save current format + color as preset (max 5)
- Load preset → restores format and color
- Delete preset → removed from list
- "Make default" checkbox in save dialog
Landscape Layout¶
- Single row of 5 cards renders the same as portrait, just denser (smaller icons + smaller fonts) — selected card stays expanded with
min-w-[80px], collapsed cardsw-10. - Color swatches remain tappable.
- Custom W/H input row, CROP/BARS toggle, and BG row all fit without horizontal scroll on a typical landscape phone (~700-900px wide right column).
7. Swipe Review — Style Tab [P+L]¶
Caption Styling¶
[image to be added]
- Style buttons: Default / Minimal / Bold → preview updates
- Position buttons: Bottom (↓) / Center (—) / Top (↑) → caption moves in preview
- Font buttons: SAN / SER / MON → font family changes
- Font size slider → "A...A" label, numeric value displayed
- Words per line: Short / Medium / Long → line wrapping changes
- Color presets: White, Yellow, Orange, etc. → text color changes
- Color picker → custom text color
- "MODIFIED" badge appears if any setting changed from default
Landscape Layout¶
- Controls remain usable with tighter spacing
8. Swipe Review — Captions Tab [P+L]¶
Caption Cards¶
- Swipeable caption cards (swipe left/right to navigate)
- Time range displayed: "HH:MM:SS → HH:MM:SS"
- Caption text shown on card
Caption Editing¶
- Tap EDIT → textarea opens with current text
- Type new text, tap Enter → saves
- Tap Esc → cancels edit
- "MODIFIED" badge appears on edited captions
- "Original: ..." hint shown below modified captions
- RESET button → reverts to original text
Caption Deletion¶
- DELETE button → confirmation dialog
- Confirm → caption removed
- Cancel → caption stays
List View¶
- Toggle list → all captions shown
- Color-coded cut indicators (% of caption affected by cuts)
- Navigation: PREV / NEXT buttons (disabled at boundaries)
9. Swipe Review — Video & Waveform [P+L]¶
Video Playback¶
- Tap video → toggles play/pause
- Video respects selected aspect ratio and letterboxing
Video Window Controls¶
- Play/Pause button with icon toggle
- Volume button (speaker icon)
- Layers button → opens FocusMode (timeline overview)
- Time display updates during playback
Landscape Video Sizing¶
- 9:16 video → narrow left column (~25% width)
- 1:1 video → medium left column (~35% width)
- 16:9 video → wider left column (~45% width)
- Video fills available height in landscape
Playback URL Refresh (Worker-Fronted Clips) [P+L]¶
Same retry-as-contract layer as desktop — see docs/qa/desktop/editor.md §5c for mechanism background and the TTL shortening setup. Mobile paths plumb onRequestRefresh through MobileDashboard → SwipeReview → FocusMode → VideoWindow, so refresh fires in portrait and landscape, in both SwipeReview and FocusMode layouts.
Setup (mirrors desktop §5c): SSH staging, set MEDIA_TOKEN_TTL_SECONDS=60 in .env, docker compose restart backend. Revert to 300 when done.
- Portrait SwipeReview, play a clip, wait ~30s. Second
/proxy+ second/media/v1/<new-jwt>fire in Network tab. Playback continues; brief buffering overlay acceptable — sprite-tile poster when the clip has a sprite (SwipeReview, FocusMode, and AssetFocusMode all route through<ClipVideo>), otherwise "Buffering..." text.CutFocusModestill renders a bare<video>and shows the text overlay (follow-up). - Landscape SwipeReview, same test. Refresh fires; playback continues.
- FocusMode (Layers button → enter FocusMode), same test. The FocusMode VideoWindow refreshes its URL the same way as SwipeReview's.
- Banner in mobile layout: stop backend mid-session, wait for refresh failure.
ClipPlaybackErrorBannerappears at the top of the mobile content area (above whatever tab/mode is currently rendered — Edits, Format, Style, Captions, or FocusMode). - Banner copy matches desktop — "Server problem..." for 5xx, "Connection issue..." for offline.
- Tap Retry in banner:
/proxyrequest fires. Once for each errored clip. - Tap Dismiss (X icon): banner hides. Session-scoped; re-shows on next distinct error.
- Restart backend: playback recovers. Banner (if dismissed) auto-resets so next failure surfaces again.
Asset playback. Asset video URLs route through the Cloudflare Worker (/media/v1/<jwt>, same path as clips, dispatched to the assets bucket via the JWT's bkt claim). MobileAssetEditor does not yet mount the retry-as-contract refresh layer (useProxyUrlSwap) over its asset <video>, so JWT expiry mid-playback won't auto-refresh — the next URL fetch on user interaction picks up a fresh token. Don't gate this section on a refresh-loop test until that wiring lands; verify only that asset playback works on a fresh URL fetch.
10. FocusMode — Timeline Overview [P+L]¶
Entry & Exit¶
- Enter via Layers button on VideoWindow
- Full-screen overlay appears
- Close (X) button → returns to SwipeReview
Video & Caption¶
[image to be added]
- Video preview at top (tap to play/pause)
- Caption text displayed below video, updates in real-time
- A/B toggle (ORIGINAL vs EDITED) available
- Asset overlays render on top of the clip preview (image/video overlays, watermark) — both portrait and landscape
<VideoWindow>instances receiveassetEdits+assetsfromSwipeReview. Toggling A/B to ORIGINAL hides the overlays. - Captions render ON TOP of asset overlays (place an overlay at bottom-center where the caption sits → caption text stays readable above the overlay; export burn-in matches preview).
Filter Bar¶
- ALL → shows all edits (two-row waveform when overlapping)
- CUTS → shows only cut/false-start edits (single row)
- INSERTS → shows only asset edits (single row)
- Filter buttons color-coded (orange for CUTS, blue for INSERTS)
Zoom Controls¶
- + button → zoom in (hold for continuous zoom)
- - button → zoom out (hold for continuous zoom)
- Zoom level displayed (e.g., "4.0x")
- Range: 1x to 16x
Timeline Waveform¶
- Waveform renders with 50% opacity background
- Edit segments rendered as colored boxes with duration labels
- Caption markers at bottom (gray bars)
- Playhead (orange vertical line with circle)
- Tap on waveform → seek to that position
- Pan/scroll through zoomed timeline
Edit Interactions¶
- Drag edit body → moves edit (duration preserved)
- Drag left handle → adjusts start time
- Drag right handle → adjusts end time
- Minimum duration enforced (edit stays visible)
- INSERT-mode asset with duration > total video (e.g., 30s INSERT on a 21s project): touch-drag the body → splice point clamps to
[0, totalDurationMs]; duration preserved andend_msextends past total (timeline grows at render). Edit remains visible/selectable. - OVERLAY/REPLACE asset, body drag (full-length): when
mainDuration >= naturalVisible, end_ms auto-extends tomin(newStart + naturalVisible, total)so the visible portion grows when dragging into more available space. Drag-left increases visible duration; drag-right decreases it. (This is the one path that clampsend_mstototalDurationMs.) - OVERLAY/REPLACE asset, body drag (trimmed): when the asset was previously trimmed below its natural visible length, moving it preserves the trimmed duration (does NOT resize back to full source duration).
- Non-insert asset dragged INSIDE an INSERT region (audio/overlay/replace, merge mode): drop position is encoded as
start_ms = splice + offset_into_insert, which can legitimately exceedtotalDurationMswhen the insert extends past the main video. End isstart_ms + mainDurationwith no clamp to total — clamping here producesend_ms <= start_msand the card disappears on release. Fixed by issue #234. - Non-asset edits and OVERLAY/REPLACE: move preserves duration; only the at-natural-length OVERLAY/REPLACE auto-extend path clamps
end_mstototalDurationMs. - Drag-to-delete (asset edits only, touch): long-press to enter move mode, then drag past any edge of the tracks surface by
DRAG_OFF_TRACK_DELETE_PX→ release past the edge deletes the edit instead of committing position. The past-edge check uses the full SwipeReview/tracks rect, not the single waveform row, so cross-track touch movement doesn't trigger an accidental delete.
Mini-Map¶
- Appears when zoomed >1x
- Shows full timeline with viewport rectangle
- Drag viewport rectangle → timeline pans
- Tap mini-map → viewport jumps to position
Bottom Bar — Navigation¶
- ← Prev button → navigates to previous edit
- Index counter (e.g., "3 / 12")
- → Next button → navigates to next edit
- Disabled at boundaries (first/last edit)
Bottom Bar — Actions¶
- CUT button (orange) → toggles edit to "cut" state
- KEEP button (gray) → toggles edit to "keep" state
- Haptic feedback on state toggle
- Undo button → reverts last edit action
- Redo button → re-applies undone action
- Both disabled when no actions available
Bottom Bar — Create¶
- +CUT button (orange) → creates manual cut at playhead position
- +CUT disabled when INSERTS filter active
- +INSERT button (blue) → opens MobileAssetPicker at playhead
- +INSERT disabled when CUTS filter active
- Picking a non-insert asset (overlay / replace / audio-only) while the playhead is inside an existing INSERT region creates the asset anchored to that insert via the schema fields
inside_insert_edit_id+insert_offset_ms, withinsert_overlap_modes = {<insertEditId>: 'merge'}— it plays during the insert (and overflows past it, for audio soundtracking). Common case: audio asset placed under an inserted video clip. INSERT-mode picks inside an existing INSERT bail silently (no nesting).
Overlap Controls¶
- MERGE/SPLIT/ANCHOR buttons appear for asset edits overlapping inserts (3-way cycle on tap)
- MERGE (default, gray) → asset plays through insert continuously
- SPLIT (yellow) → asset pauses during insert, resumes after
- ANCHOR (blue) → asset only plays during the insert region; portions outside are clipped from export and dimmed in the editor (issue #234)
- Toggle cycles tap-by-tap: MERGE → SPLIT → ANCHOR → MERGE
Landscape Layout¶
- Compact header (small close button, badge, reset)
- Video + caption on left column
- Timeline + mini-map + actions on right column (full height)
- Left column width adapts to video aspect ratio
- All controls remain functional
11. CutFocusMode — Cut/False Start Editor [P+L]¶
Entry¶
- Tap cut/false-start edit card in SwipeReview → opens CutFocusMode directly
- Full-screen overlay appears
Header¶
[image to be added]
- Close button (X) → returns to SwipeReview
- Edit type badge with color (SILENCE/FALSE_START/MANUAL/KEEP)
- Confidence percentage shown (if available)
- Time range displayed
Video Preview¶
- Video plays the edit's time range
- Letterboxed according to selected format
- Time overlay shows "MM:SS / MM:SS" (current / end)
WaveformStrip¶
- Play/Pause button (icon toggles)
- Loop toggle (orange when active, gray when inactive)
- Waveform visualization with edit range highlighted
- Orange bracket bars at edit boundaries (top and bottom)
- Orange handles (left/right) for trimming
- Dimmed overlay outside edit range
- White playhead (triangle + vertical line)
- Time labels at bottom corners
- Tap within edit range → seek to position (haptic light)
- Drag handles → trim edit start/end
- Loop active → playback loops within edit, skipping the cut (simulates final video)
Footer Actions¶
- DISMISS button → deactivates edit (if active), closes
- ACCEPT button (orange) → activates edit (if inactive), closes
- Haptic feedback (medium) on both actions
Landscape Layout¶
- Compact header (small X, badge, precise time)
- Video on left (width adapts to aspect ratio)
- WaveformStrip on right (full height, compact mode: 56px height)
- Action buttons stacked vertically on right below waveform
12. AssetFocusMode — Asset Insert Editor [P+L]¶
Entry¶
- Tap asset edit card in SwipeReview → opens AssetFocusMode directly
- Full-screen overlay appears
Header¶
[image to be added]
- Close button (X) → returns to SwipeReview
- Asset name displayed
- Reset button (rotate icon) → resets all changes
Video & Overlay Preview¶
- Main video visible (unless replace mode)
- Overlay/replace asset renders on top of or replacing main video
- Letterbox background applied based on format settings
Overlay Gestures (Overlay Mode Only)¶
- Drag: Touch and move overlay → repositions in real-time
- Long-press: Hold overlay → enables snap-to-grid (grid appears, haptic medium)
- Snap-to-grid: 3x3 dot grid + dashed lines appear, overlay snaps to nearest position
- Pinch-resize: Two-finger pinch → scales overlay (percentage shown in badge)
- Rotation: Tap rotation handle (circle at bottom center) → rotates +15 degrees
- Rotation badge shows "snap 45°" (orange) or current angle (black)
- Resize handles: Four corners with L-bracket icons (white stroke), draggable
- Past-center clamp: Drag a corner handle past the overlay's center — size shrinks to MIN and stays there (no oscillation back up). Mirrors the desktop fix from
useResizeOverlay.
Visual Mode Controls (Video/Image Assets)¶
- FULL button → asset replaces entire frame
- OVERLAY button → asset renders as picture-in-picture
- INSERT button → asset spliced into timeline
- Image assets display as static image during insert duration
- Video assets play during insert duration
- Playback continues seamlessly through insert regions
Duration Slider¶
- Duration slider visible for image overlays, video assets, and audio assets
- Slider min is
FOCUS_MODE.MIN_ASSET_DURATION_MS(0.5s) - Slider step is
FOCUS_MODE.ASSET_DURATION_STEP_MS(0.1s) - Label shows current duration via
formatDurationShort(e.g.Duration: 1.5s) - Drag updates the edit's
end_mslive; release commits viaonAdjustEditEnd - Slider max per asset type:
- Image overlay → max is the remaining timeline (
totalDurationMs - edit.start_ms) - Audio → max is the remaining timeline (audio loops on render)
- Video → max is
min(remainingTimeline, asset.duration_ms - assetOffsetMs). INSERT-mode is the exception: cap isasset.duration_ms - assetOffsetMsonly (timeline extends at render). Video frames can't loop, so the asset's own duration is always the upper bound.
Overlay Settings (Overlay Mode)¶
- Position grid (3x3 dots) → select overlay position
- Opacity slider → adjusts overlay transparency (% displayed)
Audio Mode Controls¶
- Video assets: ORIGINAL / MIX / REPLACE buttons
- Audio-only assets: MIX / REPLACE buttons
- Insert mode: mode-specific options from constants
- Selected mode visually highlighted
Volume & Ducking¶
- Volume slider visible when mix or asset-only mode selected
- Percentage displayed above slider
- "Duck main audio" checkbox (mix mode only)
Overlap Controls¶
- MERGE/SPLIT/ANCHOR buttons per overlapping insert (if overlaps exist); tap cycles through all three
- Gray when MERGE, yellow when SPLIT, blue when ANCHOR
WaveformStrip¶
- Same behavior as CutFocusMode (see section 11)
- Play/pause, loop toggle, seek, trim handles
Controls Panel (Portrait)¶
- "Hide Controls" / "Show Controls" toggle with chevron icon
- Controls section scrollable when visible
- Waveform always visible regardless of collapse state
Footer Actions¶
- DISMISS button → deactivates edit, closes
- ACCEPT button (orange) → activates edit, closes
- Haptic feedback on both
Landscape Layout¶
- Compact header (asset name, reset)
- Video + waveform on left column (width adapts to aspect ratio)
- WaveformStrip below video (compact mode)
- All controls on right column (scrollable, no collapse toggle needed)
- Footer buttons at bottom of right column
13. MobileAssetPicker — Insert Flow [P+L]¶
Step 1: Asset Selection¶
[image to be added]
- Search field with magnifying glass icon
- Assets grouped by type: Video (film icon), Audio (mic icon), Image (image icon)
- Each group shows asset count
- Asset buttons show: thumbnail (or placeholder icon), name, duration
- Color-coded left border: blue (video), green (audio), gray (image)
- Tap asset → selects it, advances to Step 2
Step 2: Configure Insertion¶
- Back button (chevron) + "Configure" title + insertion time displayed
- Selected asset info: thumbnail/icon, name, duration, type
- Visual mode buttons: FULL / OVERLAY / INSERT (video/image assets)
- Switching visual mode resets audio defaults (replace: original/0%, overlay: mix/50%/duck, insert: mix/100%)
- Audio mode: MIX / REPLACE (audio-only assets)
- Insert audio controls: TWO switches (NOT the legacy MIX/ASSET ONLY/MUTE enum) — one for the insert's own audio (
insert_audio_enabled), one for overlapping anchored audio (insert_allow_overlapping_audio). Volume slider follows the first switch. Backend rejectsaudio_modeon visual_mode=insert; verify switching from REPLACE to INSERT clears the legacy field and shows the two switches. - Overlay settings: position grid, size slider, opacity slider (if overlay)
- Volume slider (when applicable)
- Duck checkbox (mix mode)
Footer¶
- CANCEL button → closes picker
- BACK button → returns to Step 1
- INSERT button → creates edit at playhead, closes picker
13b. In-App Notifications [P+L]¶
Bell Icon (MobileWizard Header)¶
- Bell icon visible at right end of MobileWizard header
- Hidden in compact mode (landscape review)
- Red dot when unread count > 0
Notification Panel (Compact Dropdown)¶
- Tap bell opens dropdown card (max 60vh, not full-screen)
- Tap outside closes panel
- Panel shows all unread + last 5 read
- "Read all" button when unread exist
- Dark header with X close button
Swipe Gestures¶
- Swipe right on unread notification → green hint → marks as read
- Swipe left on any notification → red hint → dismisses/archives
- Swipe hint text visible at bottom of panel
- Swipe threshold feels natural (80px)
Real-Time¶
- Analysis completes → notification appears without refresh
- Export completes → notification appears without refresh
- Tab title updates with unread count
Dark Mode¶
- Panel colors correct in dark mode
- Orange border, dark header, correct text contrast
13c. BottomNav Visibility — Immersive Modes [P+L]¶
The BottomNav is hidden by default in the three immersive wizard steps (configure, review, export) so the editing surface gets the ~64px back. A hamburger button in MobileWizard's header (next to the bell) toggles a slide-up that brings it into view. Auto-closes on route change.
Visibility per step (portrait)¶
- Upload step: BottomNav visible by default. Hamburger NOT shown in the wizard header (only the bell).
- Configure step: BottomNav hidden by default. Hamburger visible in the wizard header (left of bell).
- Processing step: BottomNav visible (passive wait state — user can browse). Hamburger NOT shown.
- Review step (portrait): BottomNav hidden by default. Hamburger visible.
- Export step: BottomNav hidden by default. Hamburger visible.
Hamburger toggle (configure / review / export, portrait)¶
- Tap hamburger → BottomNav slides up into view; hamburger icon swaps to X.
- Tap X → BottomNav hides again; icon swaps back to hamburger.
- With nav visible, tap any nav tab → route changes AND nav auto-closes (next render at the new route shows the default visibility for that route).
-
aria-expandedreflects current state (true when nav visible, false otherwise) — verify via DevTools accessibility inspector. -
aria-labeltoggles between "Open menu" / "Close menu".
Wizard footer offset¶
- In a step with a wizard footer (e.g. configure's START PROCESSING button), with BottomNav hidden, the footer sits flush at the bottom of the viewport (
bottom-0) — no dead band. - Open the hamburger to show the BottomNav: the footer reflows to sit at
bottom-16(64px above viewport bottom), clearing the nav. No overlap or visual collision. - Close the hamburger again: the footer drops back to
bottom-0.
Landscape review (the strict exception)¶
- In landscape review, BottomNav is hidden AND the hamburger is also hidden (the toolbar can't spare the slot). Only the bell remains in
headerRight. - The back-arrow at the toolbar's leftmost position is the way out (preserves the existing landscape exit pattern — see section 5).
FocusMode / AssetFocusMode / CutFocusMode¶
- These render full-screen overlays at
z-[100]+, which already cover the BottomNav whether or not it's rendered. Their own X close button is the exit. No interaction with the hamburger.
14. Asset Library [P+L]¶
Search & Filter¶
- Search input with magnifying glass → filters by name (real-time)
- Type filter: All / Video / Audio / Image (toggle buttons)
- Sort options: Newest, Oldest, Name A-Z, Name Z-A
Asset Groups¶
- Tap group card → expands/collapses
- Pin button → group stays at top
- + NEW GROUP button → creates empty group
- "Show More Groups" pagination
Group Actions (Expanded)¶
- Rename: pencil icon → inline text edit
- Delete: trash icon → confirmation dialog
- Pin/unpin toggle
Assets Within Groups¶
- Thumbnail + name + duration displayed
- Color-coded left border: blue (video), green (audio), gray (image)
- Settings button (sliders icon) → opens MobileBehaviorEditor
- Rename button (pencil icon) → inline edit
- Delete button (trash icon) → confirmation
- "Show More Assets" pagination per group
Thumbnails¶
- Video assets show frame thumbnail
- Image assets show scaled thumbnail
- Audio assets show icon (no thumbnail)
- Asset picker shows thumbnails
YouTube Import¶
- URL input field visible
- IMPORT button → starts download
- Progress indicator during import
- Success → asset appears in group
- Error → message displayed
15. Asset Behavior Configuration¶
Mode Selection¶
- AUTO-INSERT button (orange bolt icon) → guides to auto-type selection
- NO AUTO-INSERT button (gray pencil icon) → sets to manual placement
Auto Type Selection¶
- FIXED POSITION button (purple pin) → opens position grid
- AI DECIDES button (blue spark) → enables AI-directed placement
Fixed Position Grid¶
- Intro / Outro / Watermark / Background Audio buttons (3-column grid)
- Selected position visually highlighted
AI Hint¶
- Optional textarea: placeholder "e.g., Insert when speaker mentions travel"
Display Options (Non-Audio Assets)¶
- REPLACE / OVERLAY / INSERT buttons
- Selected option highlighted
Overlay Settings (Overlay Mode)¶
- Position grid (3x3 dots)
- Size slider (% displayed)
- Opacity slider (% displayed)
Audio Mode¶
- Video assets: ORIGINAL / MIX / REPLACE / NONE buttons
- Volume slider (when mix/asset_only)
- Ducking checkbox (mix mode)
Inclusion Default¶
- Always (locked) / Default (can disable) / Off (manual only)
Save¶
- SAVE CHANGES button (full-width) → saves and closes sheet
15c. Vertical Crop [P+L]¶
CROP/BARS Toggle (FormatPanel in FORMAT tab)¶
- CROP/BARS toggle is always visible in the Format tab (not gated on a non-native ratio choice — Original is selected by default, so a format is always picked).
- Tap CROP → video auto-fills, enters adjusting mode.
- Tap BARS → letterbox mode.
- BG color row appears below CROP/BARS only when BARS is active (no point picking a bar color in CROP mode where there are no bars).
Zoom + Pan¶
- Zoom slider works (1x-10x)
- Drag to pan — smooth, touch-action none prevents scroll
- Axis locking works (mostly-horizontal locks vertical)
- Play button hidden during adjusting
Accept¶
- Tap DONE → crop locked
- Tap ADJUST CROP → re-enters adjusting
- Export with crop matches preview
Overlays Stay Visible Under Crop (regression for #119)¶
- Crop enabled +
caption_position=top→ captions visible at top of cropped frame (not pushed off-screen) - Crop enabled +
caption_position=bottom→ captions visible at bottom of cropped frame - Crop enabled + asset insert overlay → overlay visible at its configured position, not cropped out
- Zoom/pan doesn't scale or shift overlays (only the video moves)
15d. Per-Asset Crop (issue #235) [P+L]¶
Reframe control on REPLACE / INSERT video / image asset edits in AssetFocusMode. Independent of the main-video crop in 15c. Audio-only and OVERLAY modes are out of scope.
Reframe ControlSection Visibility (AssetFocusMode)¶
- Select a REPLACE / INSERT video or image asset edit where source aspect differs from project's target → "Reframe" ControlSection appears with CROP/BARS toggle.
- Source aspect matches target → ControlSection hidden.
- Audio-only asset → ControlSection not rendered.
- OVERLAY mode → ControlSection not rendered.
Toggle On¶
- Tap CROP → zoom snaps to fill, pan resets to (0, 0), asset fills frame.
- Tap BARS → reverts to letterbox.
Zoom + Pan (touch)¶
- Zoom slider works (1x – 10x).
- Single-finger drag on preview pans —
touch-action: noneblocks page scroll. - Axis snap engages on mostly-horizontal / mostly-vertical drags.
- Pan center-snap engages near (0, 0).
- Play button hidden while reframe is adjusting so drag works.
Swap-Mode Reset¶
- With crop enabled on an asset edit, swap to a different asset → crop disables, all four fields reset.
- Re-enable on the new asset → snaps to the new asset's fill zoom.
Intro / Outro / INSERT With Crop (Layer Split)¶
- Enable CROP on an intro asset edit → intro fills the frame; main clips remain unaffected.
- With main-video crop also enabled, the intro is NOT double-cropped — per-asset zoom is the only visible zoom on the intro.
- Toggle CROP off on the intro → letterboxes; main-video crop continues to apply to main clips.
- Same scenarios for outro and for INSERT-mode asset edits.
- Toggle a per-asset crop in AssetFocusMode → persists across reload (regression guard for camelCase→snake_case translation).
Project Aspect Change¶
- Persist crop on an asset in 16:9, switch project to 9:16 → preview re-fills, no letterbox.
- Export after aspect change matches preview.
Legacy Assets¶
- Asset uploaded before migration
a4c9e2b6f085(NULLwidth/height) → CROP toggle is disabled with re-upload tooltip. - Renders fall back to letterbox without error.
Landscape Layout¶
- AssetFocusMode in landscape still surfaces the Reframe section; CROP toggle reachable without horizontal scroll.
- Drag-to-pan works the same in landscape (touch-action, axis snap, center snap).
Export Parity¶
- Export with per-asset crop → output matches preview, no letterbox bars on the cropped asset.
- Mixed export (some cropped, some letterboxed) renders correctly.
16. Export [P+L]¶
Export History¶
- Past exports listed with status badges: QUEUED (yellow), RENDERING (orange), READY (green), FAILED (red)
- READY exports: download button (orange)
- RENDERING exports: spinner animation
- Delete button → inline confirmation (YES/NO)
Format Review (Read-Only)¶
- Platform name displayed (YouTube, TikTok, Instagram, Original)
- Aspect ratio shown
- Background color swatch + hex (when non-16:9)
Caption Review (Read-Only)¶
- "INCLUDED" green badge (when captions enabled)
- Collapsed by default: single row with inline summary (
{Style} · {size}px · {Position} · {Font}) + color swatch + chevron - Tap row → expands to show Style / Font / Size / Position / Color rows + hint "Edit caption style on the STYLE tab in the editor."
- Tap again → collapses back to summary
Export Configuration¶
- Export name input (optional text field)
- Format selection: Original / YouTube / TikTok / Instagram (2-column grid)
- Captions toggle: "Captions On" (black) / "No Captions" (gray)
- Main Volume slider (% displayed)
- Quality buttons: 720p / 1080p
Stats Summary¶
- Duration (final video length)
- Removed (time cut, red text)
- Original (source duration)
- Cuts (number applied)
Export Progress¶
- Circular progress indicator (orange fill)
- Percentage in center (e.g., "45%")
- "Exporting..." heading
- "This may take a few minutes" subheading
Create Export¶
- "+ NEW EXPORT" button (full-width, black) → starts export
- Export appears in history with QUEUED status
17. Rotation Hint¶
Appearance¶
- Portrait video (9:16) viewed in portrait → hint appears: "Rotate for a better view"
- Landscape video (16:9) viewed in landscape → hint appears: "Try portrait mode"
- Near-square video → no hint shown
Behavior¶
- Toast appears at top center with rotate icon (orange) + text + close button
- Fade-in animation (0.3s)
- Auto-dismisses after 4 seconds
- Tap X → dismisses immediately
- Dismissal persists server-side (
onboarding_seen) with localStorage cache → no hint on reload or new device - Each orientation (portrait/landscape) tracked independently
18. Dark Mode [P+L]¶
Global¶
- All backgrounds switch to dark variants (gray-900, gray-800, etc.)
- All text switches to light variants (white, gray-300, etc.)
- Borders use dark variants (gray-700, gray-600)
Component-Specific¶
- SwipeReview cards: dark backgrounds with light text
- FocusMode: dark toolbar, dark filter bar
- CutFocusMode: dark header, dark footer
- AssetFocusMode: dark controls panel
- MobileAssetsView: dark group cards, dark asset rows
- MobileBehaviorEditor: dark sheet, dark buttons
- MobileExport: dark panel, dark status badges
- Settings: dark theme toggle shows current state
19. Browser Tab Context¶
- No project → tab shows "Sapari"
- Open project → tab shows "ProjectName - Sapari"
- Analyze → tab shows "Analyzing..."
- Notification arrives → tab shows "(N) ..."
19b. Welcome Modal (Trial Users)¶
- First login as trial → welcome modal appears before onboarding
- Click "GET STARTED" → modal dismisses, onboarding starts
- Refresh → modal does not reappear
19c. Shortcut Discovery¶
- Settings > Preferences tab > Help section shows "Keyboard Shortcuts" button
- Tap → ShortcutOverlay opens
19d. Error Handling¶
- Trigger an error → generic message + support ID shown
- No internal details visible
20. Edge Cases & Regression¶
Orientation Transitions¶
- Rotate during video playback → playback continues, layout adapts
- Rotate while in FocusMode → layout switches without losing state
- Rotate while dragging edit → drag cancels cleanly (no stuck state)
- Rotate in CutFocusMode → video and waveform reflow correctly
- Rotate in AssetFocusMode → overlay position preserved
Safe Areas (Notched Devices)¶
- Landscape: no content hidden behind notch (left/right safe insets)
- Portrait: no content hidden behind home indicator (bottom safe inset)
Touch Interactions¶
- Threshold-based drag (>4px movement) vs tap distinction works
- Rapid play/pause (20x) → no stuck audio states
- Pinch-resize during playback → smooth, no jank
- Two-finger gestures don't conflict with browser gestures (pinch-zoom)
Empty States¶
- No edits detected → swipe review shows appropriate message
- No assets → asset library shows empty state
- No captions (language disabled) → captions tab hidden or empty
- No exports → export panel shows empty state with create button
Boundary Conditions¶
- Very short edit (<0.5s) → still visible and selectable in focus modes
- Edit at time 0 → renders correctly (no off-by-one)
- Edit at video end → renders correctly
- Resize edit to minimum duration → segment stays visible, handles accessible
- Maximum zoom (16x) in FocusMode → timeline remains usable
Undo/Redo Integrity¶
- Undo drag in FocusMode → edit returns to previous position
- Undo accept/dismiss → edit state reverts
- Undo delete → edit fully restored
- Undo delete of an INSERT-mode asset (regression for EditCreate validator: recreate must send
insert_audio_enabled+insert_allow_overlapping_audio, NOTaudio_mode). Includes preserving all 4asset_crop_*fields. - Redo after undo → change re-applied
Intro / Outro As INSERT Asset (analysis pipeline)¶
- In MobileAssets, set a video asset behavior
visual_mode='insert'and assign asfixed_position='intro'for a project. - Run analysis → completes without error. The analysis worker's
_build_asset_edit_createmust translate legacyaudio_modeto the two INSERT-audio booleans instead of passing it through to EditCreate. - Same for outro asset configured as INSERT.
Performance¶
- 50+ edits in FocusMode → no lag during scroll/zoom
- Drag edit → smooth movement (no jank)
- Swipe cards → smooth rotation animation
- Waveform rendering → no visible delay
Haptic Feedback (Physical Device Only)¶
- Light haptic on: waveform seek, card swipe threshold
- Medium haptic on: accept/dismiss, snap-to-grid activation, state toggles
- No haptic on: simple taps, navigation