Repository navigation
feat(ui): port the editorial redesign; keep exports smooth - #1835
Merged
Merged
Conversation
…entation UI - One design system on the existing tokens (--bg/--surface/--border/--text/ --muted unchanged) plus surface-2, border-strong, accent-text, on-accent, focus, status, radius and motion tokens; film-red accent used sparingly - System font stack (no font requests); defines the font-heading/display and animate-fade-in utilities that were used everywhere but never existed - Editor layout: preview first with a toolbar below it, edit tools as accordions with value summaries, sticky Output panel with pinned Export - High-contrast theme is now reachable (toggle + prefers-contrast) - Clearer upload, export, error/retry and success states; honest format and size copy; shared .btn/.choice/.field component classes Performance - Remove the universal transition and `transition: all`; reduced-motion support - Stop re-rendering the editor on every timeupdate; debounce URL sync - Thumbnails: capped, abortable, seek timeouts, batched, URLs revoked - Lazy-load Datadog; drop looping Lottie animations; fix privacy-banner CLS - Worker: dedupe progress, always pass -t (#1289 only patched the unused copy), cap encoder threads under cross-origin isolation (core-mt deadlock) - Restart the worker after a failed export; settle loads on terminate Telemetry: privacy-safe export/ffmpeg/thumbnail events and duration vitals, action-name masking, query strings stripped. Tests: 61 new (175 total). Lighthouse mobile (local build): 86 -> 100, LCP 3.40s -> 1.12s, CLS 0.096 -> 0. See docs/PERFORMANCE.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- vercel.json: drop the redundant legacy `routes` block. With it, Vercel compiled the header rule after `handle: filesystem`, so no header reached static pages (confirmed with an offline `vercel build`). Leave HSTS to Vercel's stronger default. - One FFmpeg command builder (src/lib/ffmpegArgs.ts) used by the worker. Sharpness and volume were silently ignored by the copy that actually ran. - Cap decoder/filter/encoder threads only when the multi-threaded core loaded; fall back to the single-threaded core if core-mt fails to load. - Stall watchdog with worker heartbeats: a wedged export fails with an actionable message and retries on the single-threaded core. - WebM: VP8 + Vorbis. libvpx-vp9 and libopus trap in @ffmpeg/core 0.12.10, so WebM exports never succeeded (also on main). - Fix a hung "Exporting" state on id-less worker errors, a leaked log listener, cancel during engine load, and string rejections from wasm traps. - RUM never starts on localhost/LAN/automated browsers unless a non-production env is set; beforeSend scrubs file names, captions, paths and query strings from auto-collected events. - Tests for the real worker command path, deploy config, telemetry env and sanitising; scripts/perf/matrix.js and check-headers.mjs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Palette tokens from the reference (near-black, slate, warm brown, charcoal, warm ivory); warm brown marks selection, film red is kept for Export/Download, playhead, trim range and progress. High contrast intact. - Self-hosted Cormorant Garamond (next/font, no runtime request) for headings, the wordmark and the export percentage; system sans for every control. - Source / Edit / Output sections with parenthetical markers, hairline separators instead of cards, pill buttons, hairline sliders and progress. - Dark "screen" drop zone, numbered two-stage export dialog with a large serif percentage, serif completion panel with a Keep editing action. - CSS-only motion: masked headline rise, staggered fades, frame settle, grid-row disclosures (closed content is visibility:hidden), hover zoom on thumbnails. Reduced motion disables all of it; nothing animates the LCP element or the <video>. - Export dialog no longer announces every percent to screen readers. - Tooling: bench.js detects collapsed panels with checkVisibility(); local server serves contact.html for /contact like Vercel. Stabilised a flaky render-count test baseline. Lighthouse (mobile, median of 5): perf 100, LCP 1.10s -> 1.40s, CLS 0, +78 KB fonts. See docs/PERFORMANCE.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Replace the Google Fonts Cormorant Garamond files (variable 300-700, Latin-1) with subsets of the styles the UI uses: upright 400-500 variable (18 KB, preloaded) and italic 400 static (12 KB), ASCII plus typographic punctuation. Built reproducibly by scripts/fonts/build-cormorant.sh from a pinned google/fonts commit; OFL included. - Lighthouse mobile, median of 5 vs e887f10: LCP 1,404 -> 1,253 ms, transfer 281 -> 235 KB, CLS 0. - Remove role="status" from the download panel: completion was announced twice and the whole panel (file name field included) read out. - Align the mobile/tablet export bar with the page gutters. - docs/PERFORMANCE.md: font audit, corrected LCP element, browser checks, limitations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Production (Netlify) is cross-origin isolated, so every browser loads the multi-threaded core. With FFmpeg choosing its own thread count, core-mt's 32-worker pool deadlocks and every export sits at "Exporting 100%" forever. Reproduced locally with COOP/COEP for MP4, WebM, portrait and no-audio inputs; the single-threaded core was unaffected. Ports the export hardening from the earlier branch (33b2c5a, bf26cf1): - src/lib/ffmpegArgs.ts: one command builder used by the worker (the old copy in ffmpeg.ts was dead code); decoder, filter and encoder threads capped at 4 on the multi-threaded core only - stall watchdog with worker heartbeats: 60 s without activity fails the export with ExportStalledError, and later attempts use the single-threaded core - WebM uses VP8 + Vorbis: libvpx-vp9 and libopus trap in @ffmpeg/core 0.12.10 - SRI hashes for the multi-threaded core files; falls back to the single-threaded core if core-mt fails to load - id-less worker errors, cancel during engine load, and string rejections from wasm traps no longer leave the editor stuck Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ests - Live-recorded files (MediaRecorder screen recordings) report an Infinity duration; seek to the end to find the real one (src/lib/mediaDuration.ts), in the editor and the export. Trim works on them again. - ffmpegArgs: time values are repaired before use (withSafeTimes), -t only for a finite positive length, -shortest for looped music when the length is unknown, and a final check that no argument carries Infinity or NaN. An infinite speed no longer hangs the audio filter builder. - Watchdog: besides 60 s of silence, stop exports whose encoded time stops changing for 120 s while FFmpeg keeps logging (stuck at 100%), or that run far past the expected length. The worker reports encoded time and computes progress from it. The message says what happened and to press Retry. - Telemetry: unknown_duration failure category and stall_reason. - scripts/make-fixtures.sh (test/fixtures/, gitignored), a static server with COOP/COEP (scripts/serve-isolated.mjs), Playwright export tests on the multi- and single-threaded engines, and a Tests workflow running Vitest and Playwright in CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…read default 2 - Threads per stage are min(4, navigator.hardwareConcurrency || 2); the fallback for an unreported core count was 4. - When the multi-threaded engine stalls, exportVideo retries the same export (same file and edits) once on the single-threaded engine instead of failing. The overlay says so, and RUM gets engine_fallback with the stall reason; the attempt's single outcome then carries engine "single" and fell_back. - A single-threaded stall, or a stall after cancelling, is not retried; the error tells the user to press Retry. - The editor passes the duration it already read to the export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
They are untracked local copies (names ending in " 2"), not project files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion-e2e Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # docs/DATADOG.md # src/lib/ffmpeg.ts # src/lib/tests/exportTelemetry.test.ts
export_cancelled carries stuck: true when the progress percentage hadn't changed for 60 s, so monitors can count a user giving up on a hang as a failure instead of an ordinary cancellation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merges the editorial "screening room" UI (031c44f, on top of ec05d58) into the export-fix branch, keeping everything main gained since: - undo/redo controls (in the Edit section header), the project panel and the recovery notice from #1822, restyled to the new design - overlay settings stay in the undoable edit state; the separate overlay localStorage effects from the old UI are dropped - the engine-fallback notice from #1831 in the new export dialog - FFmpeg, worker and telemetry code stay at their current versions; the old copies in the merged commit are not used - the hook keeps ExportAttempt telemetry and saved projects, and takes the redesign's describeExportError, outputDimensions, debounced URL sync and getCurrentTime (no re-render per video frame) page.tsx takes the redesign's version on purpose: main's nests a second <main id="main-content"> inside layout.tsx's, and the GitHub link moves into the header. docs/PERFORMANCE.md and scripts/perf from the old branch are not brought over (superseded by docs/DATADOG.md and the Playwright tests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The worker wraps the output in a Blob and posts it; building it on the
page copied the whole file synchronously (1.2 s for a 300 MB export).
- The worker reads the user's File itself; the page no longer holds a copy
of the input (a 300 MB read caused multi-second GC pauses).
- Export dialog: an estimate of the time left once there is enough signal.
- Preset buttons have accessible names again ("1 is to 1 output ratio,
Square"); the redesign left them unnamed.
- e2e: a sequence of edits (trim, 9:16, rotate, 2x, mute) checked in the
output, including a pixel check that the rotation was applied, and a
smoothness suite: long tasks during export, cancel at both stages with no
leaked object URLs, and trim/scrub responsiveness on the 300 MB clip.
- Before/after screenshots in docs/assets/ui-redesign/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
👋 Thanks for your PR, @magic-peach!Welcome to Reframe — a browser-based video editor built for everyone 🎬 What happens next
Quick checklist
Useful links
Happy coding! 🎉 |
Contributor
|
No content change: main's tree equals 8e24b37, which this branch builds on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
magic-peach
added a commit
that referenced
this pull request
Nov 3, 2026
No content change: main's tree equals d0429cf, which this branch builds on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1832 (which is on #1831), so its Playwright tests and export fixes are included. Merge those first, then change this PR's base to
main.Phase 3: the editorial ("screening room") UI you chose (option C,
031c44f), ported onto current code. Export profiles (option D) follow in a separate PR stacked on this one.Before / after
main, #1825)How it was ported
A real merge of
031c44f(no history rewritten), resolved file by file:--bg,--surface,--border,--textand--mutedare unchanged.VideoEditor.tsx: the redesign's layout, plus everythingmaingained since:ExportAttempttelemetry, saved projects and the stuck-cancel flag. From the redesign it takesdescribeExportError(adds out-of-memory and crash messages),outputDimensions, the debounced URL sync, andgetCurrentTime, so there's no re-render per video frame.src/app/page.tsxtakes the redesign's version. This is a deliberate exception to the CLAUDE.md rule "keep main's page.tsx".main's page wraps the editor in<main id="main-content">insidelayout.tsx's own<main id="main-content">, giving twomainlandmarks with the same id. The redesign also moves the floating "Star on GitHub" badge into the header. Revert this file if you want to keep the rule strictly.docs/PERFORMANCE.md(wrong Datadog queries, superseded bydocs/DATADOG.md) andscripts/perf/*(needs puppeteer-core; replaced by the Playwright suites).Smoothness: measured
New
test/e2e/smoothness.spec.ts, Chromium, M3 Pro:Two main-thread stalls, found by these tests and fixed here:
Blobon the page copied the whole output synchronously: 1.17 s in theBlobconstructor for the 300 MB export. The worker now builds it and posts it by reference.file.arrayBuffer()) before posting it, so a 300 MB buffer lived in the page's heap and triggered long GC pauses. The worker now receives theFileand reads it itself.Not met: the 200 ms budget for the 300 MB clip on the multi-threaded engine. One native task of 0.4–0.9 s remains as the result arrives. CPU contention isn't the cause: a 2-thread cap measured the same. The single-threaded engine doesn't show it. Chrome's sampling profiler can't attribute it (it shows as "(program)"). Finding it needs a full browser trace, and I've stopped here rather than guess.
The remaining long tasks scattered through encoding (50–290 ms) also aren't our code: the main thread does about 300 ms of work over 8 s of encoding, so those come from FFmpeg's threads competing for the CPU.
Also in this PR
Checks run locally
E2E_LARGE=1. All pass after two selector fixes and longer timeouts for the 300 MB clip on the single-threaded engine.