Skip to content

VideoPress: rebuild the first-run and upload experience - #51268

Open
adampickering wants to merge 44 commits into
trunkfrom
add/videopress-upload-onboarding
Open

VideoPress: rebuild the first-run and upload experience#51268
adampickering wants to merge 44 commits into
trunkfrom
add/videopress-upload-onboarding

Conversation

@adampickering

@adampickering adampickering commented Aug 14, 2026

Copy link
Copy Markdown

Proposed changes

This rebuilds what happens the first time someone opens VideoPress, and what happens when they upload a video.

Before: you landed on a stats page full of zeros. The upload button was a small button in the top right corner. If you found it and uploaded something, you got a progress bar, then a form to fill in, then a "published" page, and only after all that could you actually see or edit your video. If you already had videos sitting in your WordPress media library, nothing ever told you they could be moved to VideoPress.

After: you land on the upload screen. There's one big drop zone and one button. That's the whole page.

Here's the full journey now:

A welcome modal introduces the product. It plays a short film inside the actual VideoPress player, so the thing making the argument is the product itself. Underneath: three short claims (a player you own, bring existing videos, share it anywhere) and two buttons. If your media library already has videos that aren't on VideoPress, the second button counts them and says "Move 3 videos over", and takes you to your library filtered to exactly those videos, where you can move them across in bulk. If you have nothing to move, it's a "Learn more" link instead.

Dropping one video takes you straight into editing it. No progress page, no form, no success screen. The edit screen appears immediately and you can type the title and description while the file is still uploading. The upload itself shows in the player area, so you can see where it's up to. When the video finishes registering, you're handed over to that video's own page — the same full screen you'd get from the library — with everything you typed still there and Save now available. When it's ready to play, the player appears and a small message confirms it's live.

Dropping several videos takes you to the library instead. Each one shows its progress in the list, and a small panel in the bottom corner tracks the batch: "Uploading 3 videos — 47%". Open it and you get each file with its own cancel or retry, and when they're done, links to go and add details to each one. It follows you around the dashboard, so you can carry on working while they upload.

If something fails, the error appears where the video should be, with a retry button, and anything you'd already typed stays where it is.

Deleting your last video puts you back on the home screen with the drop zone waiting, rather than an empty list.

Other things fixed along the way

  • Uploads on connected sites now go through the real VideoPress uploader (resumable, and it survives you navigating around the dashboard). The old path created attachments the edit screen couldn't even open.
  • The video no longer reloads and stops playing the moment transcoding finishes.
  • Deleting a video now asks first. Deletion is permanent, there's no trash.
  • Cancelling an upload actually stops it, rather than leaving it running in the background.
  • On the free plan, dropping more files than your plan allows now tells you so, with the number. It used to silently throw them away.
  • The thumbnail and subtitle panels hold their shape while a video processes instead of appearing late and shoving the page around.
  • Clicking "Upload video" from the home screen used to bounce you straight back to the library if you already had videos. It works now.
  • Reloading the library page no longer redirects you somewhere else.
  • The tab bar no longer reshuffles underneath you while you're mid-upload.

Related product discussion/links

Does this pull request change what data or activity we track or use?

No.

Testing instructions

You need a site connected to WordPress.com. A Jurassic Ninja site with Jetpack connected works.

Start empty. Delete any videos in the library first, so you get the first-run experience.

  1. Go to Jetpack → VideoPress. You should land on the upload screen with the welcome modal over it. The film should play in the modal.
  2. Close the modal. You should see one drop zone, nothing else.
  3. Drop a single video. You should land on the edit screen straight away.
    • Type a title while it's still uploading.
    • Watch the URL. Once the video registers it should change to /video/<id> and the page should become that video's own screen, with your title still in the field.
    • When it's done processing, the player should appear and a message should confirm the video is live.
  4. Press play while it's still processing (if the player lets you). It should keep playing when processing finishes, not reset.
  5. Delete the video from the ⋯ menu. It should ask first, then put you on the home screen with the drop zone.
  6. Drop two or more videos (needs a paid plan). You should land on the library with each one showing progress, and a panel in the bottom corner tracking the batch. Open it, cancel one, retry one, then use "Add details" to walk through them.
  7. On a free plan, drop two videos at once. It should upload the first and tell you the other one needs an upgrade.
  8. To see the migration prompt: upload a video to the WordPress media library (Media → Add New) so it exists but isn't on VideoPress. Clear the modal's dismissal (see below) and reopen VideoPress. The modal's second button should count it and offer to move it. Clicking it should take you to the library filtered to local videos, where "Upload to VideoPress" works on one or several at once.

To see the modal again after dismissing it, add &welcome=1 to the dashboard URL. It reopens it and clears the saved dismissal.

Notes for whoever picks this up

A few things are deliberate but worth knowing:

  • "Learn more" opens jetpack.com/videopress. It doesn't reopen the modal. It also disappears entirely when you have videos to migrate, because that slot becomes the migrate button.
  • Returning users land on the home screen, not the upload screen.
  • The modal is a single view. There's no second tab of feature screenshots.
  • The "Video Title" helper text ("This title will appear with your thumbnail") is on the old upload form, which now only appears on sites that aren't connected to WordPress.com. The edit screen everyone actually uses has a plain "Title" field with no helper text.
  • The migration prompt only exists in the modal. Dismiss it and you won't see it again — there's nothing in the library itself.
  • The intro film is a placeholder. Its footage and music are stand-ins and it needs replacing with a properly licensed video before release. There's a note on it in the code.
  • routes/upload/stage.tsx still carries the old "Try a sample" code, which nothing renders. It's left in place rather than deleted in this PR.
  • &welcome=1 works in production, not just locally. It clears the saved dismissal.

Known things we didn't fix here

We audited the whole dashboard for polish before handing this over. Most of what came back was either pre-existing code this PR doesn't touch, or needed a decision rather than a patch. Listing it so it isn't rediscovered from scratch:

The two worth doing next:

  • The edit screen's header runs out of room below about 780px. Save gets cut off and the ⋯ menu goes off the right edge, with no horizontal scroll to reach either — so on a narrow window you can't save or delete. It comes from the shared admin page header, not from this code, and the fix needs a second scoping hook on a file this PR doesn't own.
  • Moving between screens drops keyboard focus to the top of the page and announces nothing. Real problem, but the fix has to tell a route change apart from a first page load or it steals focus every time the dashboard loads.

Smaller, all self-contained:

  • Analytics passes its labels in as VIEWS, TITLE, WATCH TIME — capitals baked into the translatable string — and then applies uppercase in CSS on top. Six strings to sentence case, and the KPI tiles read out as "VIEWS0" because the label and value run together with no separator. Its two ranking cards also aren't named in parallel ("Most viewed" vs "Top videos by watch time"), and "Views trends" isn't quite English.
  • Analytics chart tooltips use the browser's date format instead of the site's, so they disagree with every other date on screen.
  • File sizes are measured two ways — the Library counts in 1024s, the storage meter in 1000s — so the same file reads differently in each place.
  • Deleting a video, deleting several, and cancelling an upload all use the browser's own confirm dialog. The bulk one says "Permanently delete 3 videos?" without saying which three.
  • Upload errors say what failed, not what to do — "Upload failed with HTTP status 413" should say the file is bigger than the plan allows. None of them offer a retry. The at-limit notice, which attaches an "Upgrade" button, is the pattern to copy.
  • Settings saves with no confirmation, and while one toggle is saving the other one silently stops responding.
  • The upload drop zone's button is genuinely disabled at the plan limit, so keyboard users tab straight past it with no explanation. Same for the tooltip on the Home and Library upload buttons — the reason only exists on hover, so touch users get nothing.
  • The video player iframe stays in the tab order underneath the processing overlay, and the step flow's outgoing card is still focusable for a fraction of a second during the transition. Both want inert, which React 18.3 here doesn't accept as a plain boolean.
  • The Library grid card's one piece of metadata is "Orientation". Home shows date, privacy and play count for the same video.
  • routes/video-editor/style.scss defines its own --vp-* colour and radius variables instead of using the design system's, and Analytics and Library still have hardcoded hex greys. routes/upload/style.scss is fully converted and shows what the tokens should be.
  • One label pair in the video details card doesn't match — the Title field comes from one component library and Description from another, so they're different weights with a different gap underneath. There's a comment in the code explaining it.

Adam Pickering and others added 29 commits August 9, 2026 12:10
Extracts the first-run work from a mixed local branch onto current trunk.

- New /upload route: dropzone, real wp/v2/media upload with progress,
  details step, and a success step with a copyable share link.
- Tab order responds to library state so an empty library leads with Upload.
- Welcome modal on first visit, dismissal persisted per site and user.
- Admin_Menu::enqueue_design_tokens() so --wpds-* tokens resolve on the
  VideoPress admin page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
Splits the dashboard into two shapes and picks between them from one rule:
first run only when nothing is published and the library is empty.

- New Home screen (routes/home): a recent-videos rail with upload and
  add-to-a-post actions, and no KPI tiles. No card may render a fabricated
  "0 views" — when stats are unknown the slot shows nothing.
- Upload stops being a tab once you have videos and becomes a header button,
  so the action stays one click away instead of disappearing.
- Welcome modal rebuilt on @wordpress/ui's Dialog, led by a video of the
  player rather than a two-tab description of it.

Fixes a redirect that ejected users mid-flow: every successful upload
invalidates the library query before the details step, which on a connected
paid site flipped the count and threw the user to the Library before details
and success could render.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
The video details screen was a 660px column — the narrowest surface in the
product, on the one screen people write on. It now uses the same 1344px
container as the rest of the dashboard, split into a writing canvas and a
settings rail.

- Group the file name, title, description, chapters and thumbnail control
  into a single "Video details" card below the player. ThumbnailCard keeps
  only its read-outs and becomes VideoInfoCard.
- Move "Add to a post or page" into the page header, where it can now create
  a new page as well as a new post. It renders only when both a GUID and a
  content nonce are present, so its items can no longer open a blank editor.
- Show the in-progress title in the page heading as it is typed, falling back
  to "Untitled" so the page never loses its accessible name. Typing does not
  save.
- Split the rating options into a label and a description, so a screen reader
  announces "G" rather than the whole sentence as the option's name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
The player led the canvas at full width: 828x466 at a 1440 viewport and
892x502 at 1920, which is over half the visible page before a single field
has been read. The settings it pushed into the 380px rail did not fit --
roughly 900px of cards against a cap of 584px on a 1440x900 laptop -- so the
rail grew its own scrollbar, with no border, background or scroll shadow to
tell it apart from the page's. Two scrollbars, no boundary.

Both are the same problem. Swapping what each column holds fixes both: the
player becomes a 380x214 preview at the head of the right column, and every
control moves to the wide canvas. The right column is now ~346px, so the
height cap, the sticky positioning, the two viewport-chrome custom
properties and the flex-shrink guard all go with it. One scroll surface.

The canvas reads in the order someone works: what the video says, what it
looks like, who can see it.

* Thumbnail is a card of its own and shows the current poster next to the
  control that replaces it. It was a label and a single button, which is what
  it had been reduced to; the poster goes through usePosterUrl so a private
  video shows a skeleton until its playback token lands rather than a broken
  image.
* Subtitles is a card of its own. Both ways in were nearly invisible: a
  minimal small button reading "Manage" -- the full string was only in an
  aria-label -- at the foot of the second rail card, below the fold at common
  heights and absent without a GUID, plus an iconless item in an unlabelled
  kebab. The kebab keeps its entry.
* File name moves to Video info, next to the link, shortcode and upload
  date. It is a fact about the upload, derived from the source URL, not
  something a person writes next to the title.

The player is a grid sibling of both columns rather than a child of either,
placed by area, so the stacked layout below 1100px can lead with it while the
read-outs stay last. Row 2 is a flexible track: the canvas spans both rows,
and two intrinsic rows would distribute its excess height across them and
strand the read-outs halfway down the page.

Verified against the real components and the compiled stylesheet in a
headless harness at five viewports: player 380x214, right column 346px with
no inner scrollbar, no horizontal overflow, and player -> canvas -> read-outs
when stacked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
…psibles

Four refinements from Adam's design pass on the preview-player layout:

* Privacy & sharing and Rating move under Video info in the right column.
  The split becomes authoring vs. configuring: the canvas holds what a
  person writes (title, description, chapters, thumbnail, subtitles), the
  column holds the video, the values that address it, and settings picked
  once from a fixed set.

* Both settings cards become CollapsibleCards, collapsed by default, with
  the current value (privacy level, rating) carried in the header via
  HeaderDescription. Expanded, the column ran 407px past the canvas beside
  it; collapsed, the two columns are within 4px. Collapsing is not lossy:
  CollapsibleCard.Content defaults to hidden-until-found, so find-in-page
  still reaches the controls, and the header summary reaches assistive
  tech as the trigger's accessible description.

* The thumbnail dropdown becomes a row of 16:9 tiles: current poster,
  Upload image, Select from video — the choice is visual, so a menu that
  hides both options behind a label was the wrong control. The design
  system has no media picker, upload tile or dashed container (the survey
  is documented in thumbnail-tile.tsx), so the tile borrows Button's
  unstyled variant for the interaction and paints the frame from WPDS
  tokens. thumbnail-update-button.tsx is removed.

* The Subtitles card stacks its state line above the button — it shared a
  row with it, which read as fragments ("None [Manage subtitles]") — with
  the copy softened to "No subtitles yet." and a Skeleton while the
  tracks query resolves.

Adds first-ever test coverage for the Privacy & sharing and Rating cards
(collapsed default, header value, expand-and-edit, find-in-page survival).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
…un state

Checkpoint of the in-progress first-run work before the modal is rebuilt
to the new Figma design (VideoPress Revival, node 56-12912): Dialog-based
modal with intro video band, useSettledFirstRunState() so returning users
aren't misrouted while the library count loads, and upload-stage tweaks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
The first-run modal now leads with the product itself: the intro film
playing in a full-bleed 16:9 band over the brand wireframe artwork, then
the pitch on white — "Your Video. Your Player.", three hairline-separated
value cards, and a Learn more / Upload a Video footer.

- Band background is the committed wireframe artwork on deep green, so
  the modal never flashes a foreign colour while the film loads and
  still reads as designed if the video is missing.
- IntroVideo prefers a VideoPress GUID (the player selling itself) and
  falls back to the bundled film; getAssetUrl no longer throws the whole
  dashboard down on a malformed buildUrl.
- Value cards use @wordpress/icons glyphs with the one brand-green
  accent; columns collapse to a stack below 540px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
Three independent design audits (spacing, typography, DS craft) over the
rendered modal, all findings fixed:

- Headline and lede now actually render at the spec's 32/40 and 15/24 —
  Dialog.Title/Description pin their own internal variants and route
  font-size through the design system's global-css-defense custom
  properties, so the tokens are applied through both channels.
- The cards list dropped the user-agent's 40px list indent that pushed
  the columns out of alignment with the headline; hairlines are now
  centered pseudo-elements so all three columns share one content width.
- Column collapse is a container query on the modal's own width, not a
  viewport media query — the popup shrinks long before any phone
  breakpoint.
- 24px rhythm: lede to columns and columns to footer, with the
  cards-to-footer distance split across the scroll seam so content
  clears the separator when the body scrolls.
- Close button: rendered before the player so it is one Tab away,
  white focus ring for the dark band, 44px hit target, 16px inset.
- Autoplay parity between the embed and the bundled fallback, both
  gated on prefers-reduced-motion.
- Card titles use heading-lg instead of a hand-rolled weight override;
  middle card copy tightened so all columns hold two lines; "Upload a
  video" in sentence case; role="list" restores Safari list semantics.
- Bundled fallback film updated to the v8 master: 4K-derived, scored,
  with embedded cover art.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
Card 2 promises "move library videos over in one click" — now the footer
delivers on it. On sites whose media library holds videos VideoPress
could host, the secondary button becomes "Move N videos over" with the
real count, deep-linking to the Library pre-filtered to local videos
where the existing bulk "Upload to VideoPress" action does the moving.
Empty sites keep the "Learn more" link.

- New use-onboarding-counts hook: two 1-row count queries (VideoPress /
  local) following the first-run count idiom; nothing acts until both
  settle, so the modal never opens with a wrong label.
- The modal's gate widens to "no VideoPress videos yet" — the migration
  pitch is exactly for sites full of local videos. The landing redirect
  and tab order keep the stricter zero-of-any-videos rule.
- The primary CTA now navigates to /upload: with the wider gate the
  modal can open over Library or Home, where nothing upload-shaped is
  underneath.
- The Library seeds its type filter from the route's ?type=local search
  param (read via the router — wp-admin encodes app search inside p=).
  Seed-once; filters stay unpersisted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
The one-shot guard was a module variable, but each route ships its own
bundle with its own copy of the module — so "once" was really "once per
route bundle". Landing on Home burned Home's copy; navigating to the
Library loaded a fresh copy that fired the "landing" redirect again and
bounced straight back, making the Library unreachable from Home (and
swallowing the welcome modal's new deep-link). A window-scoped flag is
shared across bundle copies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
…onboarding

# Conflicts:
#	projects/packages/videopress/src/dashboard/components/add-to-content-menu/index.tsx
- The File Name fields never saved anywhere (the publish PATCH carries
  only title/description) — an input that edits nothing is a lie. The
  filename now renders as static text in the row header.
- StepFlow froze its wrapper height at step entry, so content growing
  within a step (error banners, per-row messages) overflowed the frame.
  A ResizeObserver now tracks the active card for its whole life.
- Copy buttons on the success step used navigator.clipboard directly,
  which is undefined on plain-HTTP dev sites; they now fall back to the
  hidden-textarea path the clipboard libraries use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
Succeeded uploads now stay in the shared queue carrying their media
result ({ id, guid, src }) until a consumer acknowledges them, instead
of self-deleting after two seconds — which threw away the only handle
anything downstream had on the finished video. This is the substrate
for the upload pill and the upload→edit transition, both of which need
to link a completed upload to /video/:id.

- UploadItem gains `media`, populated from the tus success payload.
- New `acknowledgeUpload( id )` removes settled items only; in-flight
  items are untouched (cancellation is a different operation).
- The Library's in-flight row splice already excludes success rows, so
  retention adds no duplicate rows there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
The /upload flow ran its own plain wp/v2/media XHR — a documented local-
dev stand-in that produced attachments the edit screen cannot open and
uploads invisible to the rest of the dashboard. On connected sites it
now feeds the shared tus queue: the real VideoPress uploader, resumable,
observed by the Library's in-flight rows and useFreeTier alike.

- Queue items for the current batch render through the existing step UI
  via a mapped view; when the batch settles, results are snapshotted
  into flow-local state and acknowledged out of the queue.
- Back from details/success acknowledges settled rows; in-flight rows
  keep uploading (Back is not Cancel).
- The XHR path survives solely as the disconnected-site fallback, keyed
  off the new isWpcomConnected() helper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
Drop one video and the flow cross-fades immediately to the same editing
surface the /video route uses — in a draft session: title and
description are editable from the first second (client state needs no
attachment), the player slot is the upload's stage (progress →
processing → a one-time "Your video is live" moment with the share link
and Add to a post), and Save enables the moment the real record binds.
Typed edits survive the draft→real swap. Failure keeps the form intact
with Retry in the player slot. Multi-file drops keep the batch path.

- Editor extracted from routes/video into video-details/editor.tsx
  (+ co-located styles) so both routes render one surface; the video
  route's behavior is unchanged.
- useVideo now polls GUID-less records too — previously a video could
  gain its GUID and the screen would never notice.
- useVideoDetailsForm gains preserve-dirty-on-rebind for the id swap.
- Cards that need a GUID render skeletons during the session instead of
  popping in; the ⋯ menu waits for a real attachment.
- markFirstPublish fires when the live moment first shows — this flow's
  publish moment; there is no separate publish button.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
Live testing caught a self-inflicted remount: the FIRST upload on a
first-run site flips the first-run state mid-flight (the library count
goes 1), the tab order changes, and the flow component remounts —
wiping the component-state pointer to the in-flight batch while the
window-scoped queue kept uploading without a listener. The edit surface
then sat on "Uploading… 0%" forever.

Queue items now carry a context tag naming the flow that started them,
and the flow adopts its own unacknowledged items from the queue on
mount — the queue is the durable truth; component state is just a view
of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
Second remount casualty from live testing: the attachment id was copied
into EditStep state at bind time and the queue row acknowledged away —
so when the first-run flip remounted the subtree, the new instance had
neither the state nor the row, and the player stage sat on
"Uploading… 0%" over an already-finished video.

The id is now derived from the queue row on every render, and the row
lives for the whole edit session (acknowledged on the flow's exit
paths, not at bind). Component state is a view; the queue is the truth.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
Design feedback from live review:
- The "Get your first video online" tile card duplicated the dropzone
  beneath it (and its only working tile just opened the same picker);
  the import tile was a disabled promise. One card, one action.
- The welcome modal's cards-to-buttons distance widens from 24px to
  32px, split across the scroll seam as before.
- The modal sits at 42% rather than dead center — dialog convention
  weights the empty space toward the bottom, and the band-heavy layout
  reads better lifted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
The dropzone is the page's one action; at full container width it read
as a void. 718px per design review, centered. Only the dropzone card
narrows — the edit surface that follows it in the same flow keeps the
full width.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
Design review needs to re-see the modal at will; hand-clearing
localStorage is nobody's workflow. The `welcome=1` search param opens
the modal past the dismissal flag and the library gates, and forgets
the stored dismissal so plain loads behave fresh again afterwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
The stage panel inherits the player frame's width: 100% and adds its
own padding — as content-box, the padding landed outside the 380px
preview track, hanging the panel 40px past the layout (off-screen on
smaller windows). border-box keeps the padding inside the frame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
On a fresh page load the router only parses search it finds inside the
encoded `p` path param, so `p=%2Fhome%3Fwelcome%3D1` never reached
useSearch. The raw query string check makes `&welcome=1` on admin.php
work — which is also the easier URL to hand around for review.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
Dropping 2+ videos now lands on the Library — whose in-flight rows
already show per-file progress — with one floating pill (bottom-right,
every dashboard screen) carrying the batch: collapsed it reads
"Uploading 3 videos — 47%" with a combined bar; expanded it offers
per-file cancel/retry and, once settled, "Add details" links that walk
the batch through the edit screen. Failed rows never auto-dismiss;
dismissing the pill is not cancelling (cancel is per-row, confirmed).

- cancelUpload joins the shared queue: pending rows are removed, the
  active upload aborts through the owning instance's tus handle; a
  non-owner cancel marks the row and the owner cleans up on its next
  callback — no zombies, no cross-instance handle passing.
- The pill stands down while the /upload edit session is the progress
  surface (a context-tag suppression rule, not route sniffing).
- Free plan: files discarded by the one-video slice now say so at drop
  time, with the real count — previously they vanished wordlessly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
Design review: nobody adds a video to a post from the Home overview —
that action lives with the video itself, so the header keeps only
Upload video. And a user whose library has been emptied now gets the
actual drag-and-drop experience inline on Home instead of a button
that bounces to another page: dropping a file starts the shared-queue
upload and lands directly in the edit session (the /upload stage now
resumes into the edit step when it adopts a single in-flight item).

The dropzone itself is extracted to a shared component — one drop
surface, one free-plan slice + message, used by /upload and Home.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
The empty-state Home stacked four upload invitations: the header
button, an EmptyState text block, the dropzone's heading, and the
dropzone's button — under a "Recent videos" label with no recents.
Now the dropzone IS the empty state: the text block and section label
go, its useful promise ("ready to share or drop into a post") moves
into the dropzone sub-copy, and the header's Upload button stands down
until videos exist. Status stays announced via a visually-hidden
heading. The /upload page keeps its own first-run copy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
…fecycle

Four independent reviews of the upload work found two designer-reported
symptoms sharing three root causes. This fixes all of them.

The handoff (the design truth): a single upload no longer lives out its
life in an editor embedded under the dashboard tab strip. The upload
screen is now a bridge — it shows progress and takes the title you type
— and the moment the video registers with WordPress.com it hands you to
/video/:id, the same dedicated screen the Library opens, carrying your
typed edits and the "your video is live" moment with it. The ~150 lines
where the bridge duplicated that route's save/delete/captions wiring are
gone.

The queue's lifecycle: rows were durable but nothing owned their end, so
a deleted video left a row behind that haunted the pill ("Add details"
to a 404) and let /upload resurrect a dead session as an eternal
"processing" screen. Deletes now reconcile the queue, adoption ignores
settled rows, and every upload session ends by acknowledging its row.
Uploads also gained real per-session identity: cancelling during the
token round trip no longer leaks a headless upload or writes one video's
result onto another's row, and two mounts can no longer dispatch the
same file twice.

The navigation gates: /upload no longer ejects users who own videos —
which silently broke Home's own Upload button and the welcome modal's
CTA. The landing redirect only fires for a genuine landing (a bare URL,
no app path), the tab order freezes for the life of a screen instead of
reshuffling mid-flow, and welcome=1 is consumed once instead of
reopening the modal on every navigation.

Also: deletes confirm before destroying (they are permanent, no trash);
deleting your last video lands on Home, which has the dropzone, rather
than an empty Library; the thumbnail and subtitle cards hold skeletons
through the whole upload-and-processing wait instead of appearing late;
Home's dropzone gets the same card and measure as the upload screen; the
celebration announces itself and takes focus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
The post-upload celebration was junk of my own making. It stacked a raw
URL, Copy link, Add to a post or page, and Watch video into the player's
slot — and every one of those already existed on the same screen: its
Copy link called the same helper as the Link to video field beneath it,
its Add to a post or page was the *same component* as the header's (both
mounted at once, identically labelled, so screen readers announced two
identical triggers), and Watch video existed only to dismiss the
celebration. Worse, it didn't overlay the player, it replaced it: at the
one moment the product should prove "your video, your player", there was
no player.

Now the upload's processing stage hands the slot straight back to the
player, and a snackbar marks the moment for the flow that earns one — a
single upload, not each file of a batch chained through "Add details".

Underneath, the share plumbing that produced the duplication:
- One link resolver. `linkForVideo` fell back to the attachment id,
  producing videopress.com/v/<id> — a dead link behind a Copy button
  that copied it anyway. Home's resolver (null when there's no honest
  link) wins and moves to utils/video-links.ts; the info card hides the
  field rather than offering a lie.
- The queue's `celebrated` flag and its setter go with the celebration;
  a row is now consumed by standing on its video's page.

Left deliberately: Home's card actions (a list's quick actions, not
duplicates on one video), the Privacy & sharing Share toggle, and the
player's own share menu — the preview should show what viewers see.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
The team already had this one on its list — "transcoded video interrupts
playback, forces the user to hit play again" — and our code was causing
it: the player slot SWAPPED the embed for the upload stage, so the
instant `isProcessing` flipped, the iframe was torn down and rebuilt and
whatever was playing stopped dead. Our 2s polling made that flip land
promptly, so we made it easier to hit, not harder.

The stage now overlays a player that stays mounted for the whole
session. The regression test asserts element identity across the
transition — anything weaker passes on the old code.

Also documents the bundled intro film as a placeholder: its footage and
music are stand-ins, not cleared for release, and a fully licensed video
replaces it before release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
…d-onboarding

# Conflicts:
#	projects/packages/videopress/src/dashboard/components/overview/free-tier-notice.tsx
@adampickering
adampickering requested a review from a team as a code owner August 14, 2026 02:02
@adampickering adampickering added Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Status] Needs Review This PR is ready for review. labels Aug 14, 2026
@adampickering adampickering self-assigned this Aug 14, 2026
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack), and enable the add/videopress-upload-onboarding branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack add/videopress-upload-onboarding

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!


Jetpack plugin:

The Jetpack plugin has different release cadences depending on the platform:

  • WordPress.com Simple releases happen as soon as you deploy your changes after merging this PR (PCYsg-Jjm-p2).
  • WoA releases happen weekly.
  • Releases to self-hosted sites happen monthly:
    • Scheduled release: September 7, 2026

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Videopress plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

The upload card sat at 718px while the free-plan notice above it ran the
full 1344px container, so the empty screen read as a wide bar over a
narrow card, and neither matched the Settings tab beside it. Both now
take the canonical 660px Jetpack settings measure, centered — the width
Settings already uses (routes/settings/style.scss). Only these narrow;
the edit surface in the same flow still needs the full width.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
@jp-launch-control

jp-launch-control Bot commented Aug 14, 2026

Copy link
Copy Markdown

Code Coverage Summary

Coverage changed in 21 files. Only the first 5 are listed here.

File Coverage Δ% Δ Uncovered
projects/packages/videopress/src/class-admin-ui.php 64/292 (21.92%) -0.08% 1 ❤️‍🩹
projects/packages/videopress/routes/library/upload-drop.ts 11/11 (100.00%) 0.00% 0 💚
projects/packages/videopress/src/dashboard/components/dashboard-tabs/index.tsx 8/8 (100.00%) 0.00% 0 💚
projects/packages/videopress/src/dashboard/components/free-tier-notice/index.tsx 8/8 (100.00%) 0.00% 0 💚
projects/packages/videopress/src/dashboard/components/video-details/use-video-details-form.ts 34/34 (100.00%) 0.00% 0 💚

22 files are newly checked for coverage. Only the first 5 are listed here.

File Coverage
projects/packages/videopress/routes/home/route.tsx 0/1 (0.00%) 💔
projects/packages/videopress/routes/upload/route.tsx 0/1 (0.00%) 💔
projects/packages/videopress/src/dashboard/components/onboarding-modal/intro-video.tsx 10/20 (50.00%) 💚
projects/packages/videopress/routes/upload/stage.tsx 246/303 (81.19%) 💚
projects/packages/videopress/routes/home/stage.tsx 34/40 (85.00%) 💚

Full summary · PHP report · JS report

If appropriate, add one of these labels to override the failing coverage check: Covered by non-unit tests Use to ignore the Code coverage requirement check when E2Es or other non-unit tests cover the code Coverage tests to be added later Use to ignore the Code coverage requirement check when tests will be added in a follow-up PR I don't care about code coverage for this PR Use this label to ignore the check for insufficient code coveage.

@adampickering

Copy link
Copy Markdown
Author

Pushed a width fix: the upload area and the free-plan notice now take the same 660px measure as the Settings tab, centered.

Before, the upload card sat at 718px while the free-plan notice above it ran the full container width, so the screen read as a wide bar over a narrower card, and neither lined up with Settings next to it. Both are now 660px — the same value routes/settings/style.scss already uses.

Only those two narrow. The edit screen that follows an upload still uses the full width, since it needs the room for the two-column layout.

Stats, Home and the Upload page all sit in a 1344px column with a 24px
gutter; the Library had no container at all, so its listing ran edge to
edge and didn't line up with the tabs either side of it. It now takes
the same measure, as a flex column with `flex: 1` and `min-block-size: 0`
so the DataViews viewport still grows into the tab panel and keeps its
pagination pinned to the bottom.

The at-limit notice loses its own gutter margin, since the container's
padding supplies it now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
@adampickering

Copy link
Copy Markdown
Author

Width audit across every tab, measured on a live site at a 1600px viewport:

Page Container Content Left edge
Stats 1344px 1296px 204px
Home 1344px 1296px 204px
Library 1344px 1296px 204px
Upload 1344px (card 660px) 204px
Settings 660px 660px 546px

Stats and Home already agreed. The Library was the outlier — it had no container at all, so its listing ran edge to edge while the tabs either side of it sat in a column. It now takes the same 1344px measure and 24px gutter, kept as a flex column so the DataViews listing still fills the tab panel and keeps its pagination pinned to the bottom.

Upload and Settings intentionally keep the narrower 660px measure for their content: Settings because that's the canonical Jetpack settings width, Upload because the drop zone is the page's only action and reads as a void at full width. Upload's outer container still matches the others, so the page starts at the same left edge as its neighbours.

Adam Pickering and others added 5 commits August 13, 2026 22:37
Three fixes from a walkthrough:

The upgrade notice said two different things depending on which tab you
were on — the free-plan sentence on Home and Stats, a shorter at-limit
line on Library, Settings and Upload. Every surface now shows the same
sentence. The short form survives only where a full sentence doesn't
fit: the disabled-upload tooltips and the rejected-drop toast. Home was
hard-coding its own copy of that tooltip string; it now shares the one
constant.

Opening VideoPress from the WordPress menu lands on a bare URL, which
resolves to the Library because Library owns `/`. The landing redirect
then sends you to Home or Upload, so the Library painted first and was
yanked away — it read as the page loading twice. The body now waits for
that decision, but only on the bare landing and only while the count is
unknown, so a failed count still renders rather than hanging.

Home's free-plan notice only takes the narrow measure when the dropzone
is beneath it. Next to the recents rail it stays full width; centred at
660 over left-aligned cards it just looked misaligned.

(The zoom on that same load is WordPress's own admin view transitions —
`@view-transition { navigation: auto }` from core's
wp-view-transitions-admin-inline-css — not this dashboard.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcAb177MwHKkw7D5RPrSvq
An audit of the dashboard turned up 46 things; these are the ones worth
fixing on this branch. The rest are pre-existing code outside this PR's
diff, or need a decision rather than a patch.

The one real bug: `&welcome=1` — the URL in this PR's own testing
instructions — did not open the modal on a site that already had videos.
A bare admin URL carries no app path, so it resolves to Library and the
landing redirect fires; each route is its own bundle, so that navigation
remounts the modal, by which point the one-shot param had already been
consumed and `isOpen` fell through to the empty-library check. It only
appeared to work on an empty site because the modal would have opened
there anyway. The window latch is now two values: consumption still
drives the one-shot `clearDismissal()`, while a separate flag keeps the
modal open for the rest of the page load. A dismissal still sticks —
`isDismissed` re-seeds from storage on every mount.

Screen readers:
* The upload panel wrapped the percentage in `aria-live`, so it spoke a
  new number on every progress tick. Now only the phase is announced,
  matching what the upload pill already does; the percentage stays
  visible.
* The video-details cards rendered their titles as plain divs, leaving
  the screen with one heading. They are h2s now, as the design system's
  own docblock prescribes.
* Both copy buttons in the video-info card were named "Copy"; they now
  name their field.
* The rating radio group and the frame scrubber had no accessible name.
* The free-plan notice is permanent, but was being spoken aloud on all
  five screens on every navigation.

Visible fixes:
* The same video read `0:34` on Home and `00:34` in the Library. Home
  had its own duration formatter; deleted in favour of the shared one.
* At the free-plan limit, the upload button was styled as unavailable on
  Library but looked completely live on Home.
* Picking a video frame made the *other* thumbnail tile say "Updating…".
  The busy state now sits on the thing that is actually changing.
* Four straight apostrophes among curly ones, all in strings new here.

Also corrects a docblock in video-details-card that confidently claimed
the two label styles already agree. They do not — measured 600 vs 499
weight and a different label-to-field gap. The comment now says so, and
why it is being tolerated for now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpbCbrkUsQUCDsaLxyMsd4
Two testers walked the product as customers — one brand new, one
returning with a library. These are the things that stopped them.

**"Add to a post or page" created an empty draft.** The button opens
post-new.php with the video's GUID, and a `default_content` filter is
supposed to turn that into a video block. The filter was registered
*after* an early return that fires whenever the standalone VideoPress
plugin is absent — so on any site running VideoPress through the Jetpack
plugin it was never hooked at all, and the editor opened on an untouched
empty string. The guard is right for the shortcode registrations beside
it, which would otherwise collide with the ones the Jetpack module
already makes; this filter has nothing to collide with, so it now
registers unconditionally.

**Dropping a file painted two screens on top of each other.** The
outgoing card is absolutely positioned for the cross-fade, and `inset: 0`
pinned its bottom edge too, stretching it to the height of the editor
arriving underneath. Its vertically-centred contents drifted into the
middle of that editor, so "Drag and drop your video here" printed
through the Description field. It now pins only the top and sides and
keeps its own height.

**At the plan limit the drop zone silently ate files.** Three separate
floors — the component dropped its handlers while disabled, and both
callers returned early — under a surface still reading "Drag and drop
your video here". Refusal now lives in the component, so every drop,
click and pick raises the same notice, with the upgrade action the
library already uses. The button matches the surface instead of
contradicting it, and it explains itself on click rather than only on
hover.

**A .txt renamed .mp4 uploaded all the way through**, took the free
plan's only video slot, and left the thumbnail and subtitle panels
loading forever. The Library route already had the right check; it now
lives beside the shared drop zone so every entry point gets it. The
panels also stop pretending: a video that never resolves now says so
instead of animating indefinitely. Polling is unchanged — this is a
display bound, not a claim about the backend.

**Arriving from the WordPress menu flashed the wrong navigation.** An
earlier fix held the body back but not the tab strip, so the first-run
tabs still painted for ~400ms and then renamed themselves. Both are now
held over the same window. A brand-new user still gets the optimistic
first-run strip immediately on every other arrival.

**The modal's background artwork never loaded.** The stylesheet asked
for `url(images/…)`, but the CSS is injected by JS, so it resolved
against /wp-admin/ and 404'd on every load. It now goes through the same
build-URL helper as the film, keeping the flat green underneath for when
that can't be resolved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpbCbrkUsQUCDsaLxyMsd4
Follow-up to the walkthrough fixes. Testers re-ran the journeys and
found three of them incomplete, plus one thing the last round broke.

**The file check didn't catch the case it was written for.** It trusted
`File.type` — but browsers derive that from the extension, so a text
file renamed `.mp4` reports `video/mp4` and passed. It now reads the
first 64 bytes and checks the container signature: ISO-BMFF for the
mp4/mov family, EBML for WebM and Matroska, and Ogg. It fails open on
purpose — an extension with no known signature rides on its extension,
and an unreadable file is accepted, because refusing somebody's real
video is far worse than accepting a renamed text file. `.avi`, `.wmv`
and the MPEG family still have no signature and so still pass on their
extension alone; those formats want a follow-up rather than a
half-remembered magic number that rejects real footage.

An invalid file also now says it is invalid even when you are at the
plan limit, instead of blaming the limit for it.

**The tab strip stopped guessing.** It was deriving first-run state from
a count that reads 0 until it loads, so every in-app arrival painted the
first-run tabs and then corrected itself — a rename on one route, a tab
appearing and vanishing on another. That guess is now only made when
there is nothing better: the first settled count proving a library is
not empty is remembered, and later loads answer from it immediately. A
brand-new user is unaffected and still gets the first-run strip on the
first render, which was the reason the guess existed.

The same absence of a record was behind the second bug: the "has
published" flag was only ever written by an upload made in this browser,
so anyone whose videos predate it looked brand new the moment they
deleted their last one.

**The welcome modal no longer opens over a running upload.** Emptying
your library does legitimately restore the first-run condition, but
somebody with a file in the queue has already found the one thing the
modal is selling. It stands down for the rest of the page load.

Also: the disabled upload button inside the drop zone now looks disabled
and explains itself on hover, matching the header buttons rather than
contradicting them; refusal notices replace each other instead of
stacking; and the landing hold now reads as loading rather than as a
page that has finished rendering nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpbCbrkUsQUCDsaLxyMsd4
Adam Pickering and others added 5 commits August 14, 2026 10:03
…ether

Last round of fixes from the walkthroughs.

**A bare landing no longer paints a screen it is about to leave.** The
hold added previously released on three conditions that all flip before
the destination paints — the settled count commits a render before the
effect that navigates can run, and the redirect rewrites the URL while
Library is still the mounted route. So the Library rendered anyway, for
about 400ms, complete with a live "Upload video" button, on the one path
where Library is never the answer. How the mount arrived is now captured
once, so the hold is constant for its life: spinner, then the real
destination. A deliberate later click on Library is still never hijacked.

The structural fix is for Library to stop owning `/` — there is a note
saying so where the next person will find it.

**A returning user is recognised from any route.** Both first-run flags
were written from components that only some routes mount, so opening a
video link in a fresh browser recorded nothing, and deleting that video
looked exactly like a first run. They now have one writer, mounted in
the wrapper every route shares. The two flags stay separate on purpose:
the modal should still greet a site full of local videos with the
migration pitch, which "library seen" proves and "has published" does
not.

**A real WebM is no longer told it isn't a video.** The refusal was
right — the backend doesn't accept it — but the message was a lie, and
the file picker was offering the formats it would then reject. The
picker now offers only what the backend takes, and the two refusals read
differently: an unsupported container says which formats to convert to,
a file lying about its extension says it isn't a video.

**The drop no longer fades two layouts through each other.** Both cards
animated over the same window, so through the middle both were painted —
and a drop zone and an editor form have no shared line to fade between.
The handover is now sequenced: the old card leaves, then the new one
arrives, in the same total time. Transitions between similarly-shaped
steps keep the cross-fade, which is what it was designed for.

Also fixes a test helper that shipped literal NUL bytes, which had been
making git treat that file as binary and hiding its diffs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpbCbrkUsQUCDsaLxyMsd4
Handover tidy-up, driven by the coverage report for this PR. No
behaviour changes — deletions and tests only.

**Deleted the parked "Try a sample" block.** `SampleVideoModal` and its
six exclusive helpers were referenced by nothing but each other, behind
an eslint-disable saying so. Around 250 lines of unreachable code in the
first file anyone reads, plus the stylesheet block for an element that no
longer exists. Every symbol was checked against the whole package before
removal. `routes/upload/stage.tsx` is down from 1742 lines to 1491.

**Tested the disconnected-site upload path, which had never once run.**
Its tests pinned `isWpcomConnected` to true and never flipped it, and the
live walkthroughs all ran against connected sites — so the fallback that
uploads through `wp/v2/media` had no coverage from either direction. It
is also the path anyone running a local install hits first. It turns out
to work: the drop, the upload, the details step and the success step all
pass, and so do the failure paths — a network error, a non-2xx with and
without a usable server message, and a missing nonce. Six tests, plus a
direct one for the connection helper, which reads as untested only
because every caller mocks it.

**Tested the Library's refusal gates.** The same gates on the upload
screen were covered; these were not, on the surface where a silently
eaten drop was one of the bugs this branch set out to fix. Seven tests
over the not-a-video message, the plan-limit notice and its upgrade
action, the partial-selection count, and the header button's refusal.
Neutering the four gates fails six of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpbCbrkUsQUCDsaLxyMsd4
Sequencing the hand-off removed the double-print, and a live capture
confirmed it: not one frame paints both cards. But it exposed the
wrapper's height transition, which starts when the step changes and so
eases open during the exit — and through a sequenced hand-off both cards
are at opacity 0, so what expands is an empty box. Measured on a real
drop, the wrapper had reached 654 of 734px with nothing painted in it,
turning a card-sized pause into a full-width blank about 470px tall.

Delaying the height by the exit duration holds it until the outgoing card
has actually gone, then eases it open with the card arriving to fill it.
Scoped to the sequenced case: the same-shape steps want the height to
track the card already fading in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpbCbrkUsQUCDsaLxyMsd4
The previous commit was measured on a live site and made no difference to
a single pixel. Holding the wrapper's height was the wrong lever: the
blank the user sees is bounded by the white admin panel behind the flow,
not by the wrapper, which only reserves space below itself. A frame diff
of before and after came back at 0.00% changed. That change is reverted
here along with the constant it needed.

The gap was never the height. It was the incoming card being held at
opacity 0 until the outgoing one had finished leaving — two timings butted
end to end, with ~105ms of nothing between them.

So stop animating the outgoing card at all. On a shape-changing handover
it is now simply not drawn, exactly as the reduced-motion branch has
always done, and the editor fades in immediately over the full duration.
A drop is a deliberate act with its own feedback; the dropzone does not
need a farewell. One thing moves instead of two, there is no dead band
between two clocks to fall into, and "the two cards never co-occupy" is
true because one of them was never painted rather than because the timings
happen to line up.

Same-shape steps keep their cross-fade, which is what it was designed for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpbCbrkUsQUCDsaLxyMsd4
Every entry on this branch was filed against packages/videopress only. A
package entry reaches that package's changelog and nowhere else — the
plugins that bundle it get, at most, the release tooling's generic
"Update package dependencies" line, which never reaches readme.txt. Since
users, release posts and support docs all read the plugin changelog, a
whole rebuild of the first-run and upload experience would have shipped
invisible.

Adds entries to both plugins that ship the package, written from their own
users' point of view rather than copied: the new first-run and
upload-to-edit experience, and the add-to-a-post fix, which is the one
change here that a user could have hit as a plain bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpbCbrkUsQUCDsaLxyMsd4
@github-actions github-actions Bot added [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ [Plugin] VideoPress A standalone plugin to add high-quality VideoPress videos to your site. labels Aug 14, 2026
Adam Pickering and others added 3 commits August 14, 2026 14:01
The entry I hand-wrote used `fixed`, which is valid for a package and for
the VideoPress plugin but not for the Jetpack plugin — that one defines its
own set, where the equivalent is `bugfix`. Changelogger validity caught it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpbCbrkUsQUCDsaLxyMsd4
Tests only, no behaviour change. The coverage report flagged both files as
carrying more uncovered lines than trunk despite higher percentages, and the
uncovered lines were not incidental.

In the upload queue they were the cancel/abandon guards the file's comments
spend paragraphs justifying: a progress or error event arriving for a row the
user already cancelled, a tus handle turning up after its session ended, and
cancelling in the window before tus has produced a handle at all. Those guards
are the reason a cancelled upload can't have its media attributed to the next
row in the queue. That is the worst bug this file can produce and the hardest
to reproduce, and two people are about to build on it, so it should not have
rested on the comments alone.

In the dashboard layout it was the guard that keeps the active tab in the
order when the order has dropped it — which the comment above it describes as
reachable in normal use, because the first successful upload flips the state
to `home` and drops `upload` while the user is still standing on it. Without
the guard the test renders a blank page, which is exactly what it prevents.

Two of the new tests pass whether or not their guard is present, and are
labelled as such rather than counted as proof: `onError`'s abandon branch is
currently unobservable, because falling through releases the queue anyway and
the abort it would call is undefined in every state the store can presently
reach. It is defensive, not load-bearing — unlike its counterpart in
`onProgress`, which is the real one. If a future change lets a live row be
removed while its session still holds a handle, that branch starts mattering
and its test starts being able to fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpbCbrkUsQUCDsaLxyMsd4
…icon

Two things from a review of the home and upload screens.

The recent-video card's picture did nothing. It is the biggest target on
the card and the thing people aim at, but only the title underneath opened
the video. It opens it now. It stays out of the accessibility tree rather
than becoming a second button: the title already goes to the same screen,
and two tab stops onto one destination is worse for a keyboard user than
one — the thumbnail is decorative either way, since the title is the card's
accessible name.

The upload icon was a bare glyph where the rest of the dashboard puts this
kind of icon in a circle. It now uses the design system's own
`EmptyState.Icon`, which is where Jetpack Social gets the same treatment,
so the circle, border and fill come from the tokens rather than from a
shape drawn here. Its trailing margin is dropped because that spacing is
meant for the design system's empty-state column and this surface already
sets its rhythm with a flex gap; left in, the two stack and the icon floats
away from its heading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpbCbrkUsQUCDsaLxyMsd4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Package] VideoPress [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ [Plugin] VideoPress A standalone plugin to add high-quality VideoPress videos to your site. [Status] Needs Review This PR is ready for review. [Tests] Includes Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants