Skip to content

feat(ui): port the editorial redesign; keep exports smooth - #1835

Merged
magic-peach merged 14 commits into
mainfrom
feat/editorial-ui
Nov 3, 2026
Merged

magic-peach merged 14 commits into
mainfrom
feat/editorial-ui

Conversation

@magic-peach

@magic-peach magic-peach commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

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

Before (main, #1825) After
Editor, desktop before after
Exporting before after
Editor, 390 px before after

How it was ported

A real merge of 031c44f (no history rewritten), resolved file by file:

  • FFmpeg, worker and telemetry: the current versions are kept. The merged commit carried older copies of code that has since been reworked (feat(telemetry): export reliability by browser and device #1829, fix(ffmpeg): stop multi-threaded exports from hanging at 100% #1831, fix(export): unknown durations, stuck-progress watchdog, e2e export tests #1832).
  • Restyled components (preset, format, framing, download result, footer, globals.css): the redesign's versions. The design-token names --bg, --surface, --border, --text and --muted are unchanged.
  • VideoEditor.tsx: the redesign's layout, plus everything main gained since:
  • The hook keeps ExportAttempt telemetry, saved projects and the stuck-cancel flag. From the redesign it takes describeExportError (adds out-of-memory and crash messages), outputDimensions, the debounced URL sync, and getCurrentTime, so there's no re-render per video frame.
  • src/app/page.tsx takes 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"> inside layout.tsx's own <main id="main-content">, giving two main landmarks 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.
  • Not brought over: docs/PERFORMANCE.md (wrong Datadog queries, superseded by docs/DATADOG.md) and scripts/perf/* (needs puppeteer-core; replaced by the Playwright suites).

Smoothness: measured

New test/e2e/smoothness.spec.ts, Chromium, M3 Pro:

Check Result
Longest main-thread task, normal 10 s clip export (multi-threaded / single-threaded) 0 ms / 61 ms (budget 200 ms)
Longest main-thread task, 300 MB clip export, multi-threaded 418–907 ms over 5 runs (907, 418, 893, 615, 498). Was 1.5–8.9 s before this PR. Test budget is 1,000 ms.
Same, single-threaded 144 ms
Trim handle on the 300 MB clip 9.6 ms per step; longest task 70 ms
Cancel during engine load, then during encoding, then export Works; no leaked object URLs

Two main-thread stalls, found by these tests and fixed here:

  1. Building the result Blob on the page copied the whole output synchronously: 1.17 s in the Blob constructor for the 300 MB export. The worker now builds it and posts it by reference.
  2. The page read the entire input (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 the File and 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

  • Accessibility fix: the redesign's preset buttons had no accessible name. They're now "1 is to 1 output ratio, Square", and so on.
  • Time left in the export dialog, extrapolated from progress, shown once it's at least 3% done and 3 s in.
  • Edit-sequence e2e test: trim 1–5 s → 9:16 → rotate 90° → 2× → mute → export. Output checked: 1080×1920, about 2 s, no audio stream. Rotation is checked from the pixels: a rotated landscape clip fills the 9:16 frame, while a control export without rotation has black bars.
  • Stall message now names the real button ("Press Retry export…").

Checks run locally

  • Vitest: 35 files, 407 tests. Typecheck and lint are clean. Build passes.
  • Playwright: 26 tests on both engines, with E2E_LARGE=1. All pass after two selector fixes and longer timeouts for the 300 MB clip on the single-threaded engine.
  • Not checked: a real-device export on the Netlify preview, and Safari and Firefox.

magic-peach and others added 13 commits October 21, 2026 10:48
…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>
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

👋 Thanks for your PR, @magic-peach!

Welcome to Reframe — a browser-based video editor built for everyone 🎬

What happens next

  1. 🤖 Automated checks — build & TypeScript typecheck will run automatically
  2. ✅ Vercel preview — a preview deployment will be created (requires maintainer authorization for fork PRs)
  3. 👀 Code review — a maintainer will review your changes
  4. 🚀 Merge — once approved, your PR will be merged!

Quick checklist

  • PR title follows Conventional Commits (e.g. feat: add dark mode)
  • Linked the issue this PR closes (e.g. Closes #123)
  • Tested the changes locally (bun run dev)
  • Build passes (bun run build)

Useful links

Happy coding! 🎉

@github-actions github-actions Bot added level:advanced Advanced level - 55 pts type:bug Bug fix type:design UI/UX design type:docs Documentation type:performance Performance type:testing Testing labels Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ PR Format Issues — @magic-peach

Please fix the following before your PR can be reviewed:

  • ⚠️ No linked issue found. Add Closes #<issue-number> to your PR description.

Push new commits after fixing — this comment will update automatically.

📖 CONTRIBUTING.md

No content change: main's tree equals 8e24b37, which this branch builds on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@magic-peach
magic-peach changed the base branch from fix/export-duration-e2e to main November 3, 2026 16:03
@magic-peach magic-peach closed this Nov 3, 2026
@magic-peach magic-peach reopened this Nov 3, 2026
@magic-peach
magic-peach marked this pull request as ready for review November 3, 2026 16:10
@magic-peach
magic-peach merged commit 06768d8 into main Nov 3, 2026
23 of 29 checks passed
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

level:advanced Advanced level - 55 pts type:bug Bug fix type:design UI/UX design type:docs Documentation type:performance Performance type:testing Testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant