Surface WordPress update notices once in the desktop shell (with release artwork) - #340
Conversation
WordPress core prints "WordPress X is available!" on every admin
screen, so in desktop mode the same nag repeated in every open window.
Now the plugin detaches core's per-window update/maintenance nags
inside chromeless windows and re-surfaces the update once, as a single
persistent (non-dismissible) toast in the shell.
- includes/core/routing.php: chromeless-gated remove_action() for
update_nag / maintenance_nag, plus a .update-nag CSS net.
- includes/update-notice.php: desktop_mode_get_core_update() computes a
{ version, url } descriptor from authoritative update state
(update_core-gated; desktop_mode_core_update_notice filter to
suppress) and ships it as config.coreUpdate.
- src/update-notice.ts: shows one persistent toast; "Update now" opens
update-core.php. Clears via the action or once the update installs.
- src/toast.ts: new `persistent` option (never auto-dismiss).
- wpd-toast: light border + inset highlight so the toast reads as a
distinct surface over the widget cards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the core-update notification: a routine (minor) release still
shows the plain persistent toast, but a major release becomes the
release's own music-themed art — the album sleeve with a CSS-drawn
vinyl that slides out and spins.
The flat release PNG is used only as the square sleeve; the record is
drawn entirely in CSS (grooves, sheen, tinted label), so it's a real
separate object that can emerge and turn — no image surgery.
- includes/update-notice.php: desktop_mode_is_major_update() (X.Y bump
vs installed) + desktop_mode_core_update_release() registry (bundled
Armstrong 7.0 / Gene 6.9 art, desktop_mode_core_update_release filter
to extend/override). Descriptor gains `major` + `release`.
- assets/releases/{7.0,6.9}.jpg: square sleeve crops of the release art.
- src/ui/components/wpd-release-card: the vinyl web component; static
under prefers-reduced-motion.
- src/release-card.ts + src/update-notice.ts: picker — vinyl for a major
with art, plain toast otherwise.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The vinyl release moment now pulls each release's album art from the wordpress.org/news announcement (the post's featured image) at runtime, so past and future releases work without a plugin update — no bundled art, no hardcoded registry. - includes/update-notice.php: desktop_mode_core_update_release() reads a cached descriptor; a cold cache schedules a background fetch (wp_schedule_single_event) so the admin render never blocks on an external request. The fetcher searches the news REST API, matches the major-announcement title (WordPress X.Y "Codename"), and takes the post's featured image (medium_large). Hits cache a week, misses 6h. The desktop_mode_core_update_release filter still overrides everything. - assets/releases/*: removed (no longer bundled). - wpd-release-card: the sleeve art is cropped left (object-position), which extracts the square sleeve from the landscape release image for any release; classic cream label + WordPress-blue button by default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ved accent
Reworks the core-update notification behavior:
- Show the vinyl for ANY update in a branch that has release art, not
just a fresh major — a minor (7.0 -> 7.0.1) reuses its major's album
art. Falls back to the toast only when no art is available.
- Branch-aware wording, computed server-side: crossing into a new major
(6.9 -> 7.0 or 7.0.1) reads "WordPress 7.0 'Armstrong' is available"
(branch version + codename); a same-branch minor (7.0 -> 7.0.1) reads
"WordPress 7.0.1 is available" (exact version, no codename).
- Accent color is sampled from the sleeve art at runtime (its dominant
vivid color) and tints the record label + Update button; the CDN
allows CORS so this is done client-side, with a filter override and a
WordPress-blue default. Verified: Armstrong -> coral, Gene -> blue.
Descriptor is now { version, name, branch, url, release:{artUrl} };
`major` is gone (the shell keys off release art). The fallback sprintf
gained positional-token (%1$s) support.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eeve Some release covers (e.g. 6.8 Cecil) ship a white frame around the artwork in their featured image, unlike 7.0/6.9 which bleed to the edge. Cropping the left square then showed that white padding. The sleeve is now painted via canvas: trim any uniform near-white border first, then draw the left square (the sleeve). Full-bleed art has non-white edges so nothing is trimmed; padded art (Cecil) crops cleanly. Accent extraction runs on the same painted sleeve. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-wrap - .cover border-radius 10px -> 2px - remove the .spine gradient element - .disc-wrap: top 2px, 148x148 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- notification width 240px -> 268px - replace the "W" + version text on the record label with the WordPress logo on the accent-colored label (accent-ink glyph); drop the spindle hole. The label color follows the art-derived accent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#15161b -> #34373f — the near-black disc was blending into the near-black card. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the outer drop shadow to the non-rotating .disc-wrap (made round); .disc keeps only its radially-symmetric inner shadows, so the shadow stays fixed while the record spins. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… shell) A backtick inside the css`` tagged template terminated the literal, so `.disc-wrap` in a comment parsed as JS (`.disc` minus `wrap`) → "wrap is not defined" at boot, blanking the shell. Plain-text the comment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…con animation Add a close button. Dismissing plays a two-stage animation: the record slides back into the sleeve, then the whole card collapses into the admin-bar Updates icon (with a small bounce on the icon it lands in; falls back to the admin bar / top-right if the node isn't present). Respects prefers-reduced-motion (instant removal). Pending animation timers are cancelled on disconnect. Dismissal persists per release branch (localStorage), and the picker skips a release the user already dismissed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PHP now only reports that a core update is pending ({version, branch,
url, crossing}); it no longer fetches the news feed (removed the cron,
transient, wp_remote_get parse and the desktop_mode_core_update_release
filter). The shell resolves the album art + codename client-side:
- src/release-art.ts: resolveReleaseArt(branch) fetches the news REST API
(CORS-enabled, via trackedFetch), matches the announcement title, takes
the featured image, caches in localStorage (hit forever, miss 6h);
preloadImage() waits for the image to decode.
- src/update-notice.ts: maybeShowUpdate() is async — resolve art, wait for
it to load, THEN show the vinyl once (no placeholder flash). Falls back
to the toast only when art can't be resolved.
Net effect: the notification appears a beat later but already as the
vinyl, instead of flashing a toast and swapping to the vinyl on reload.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The release card is an internal, single-use, feature-specific surface, so it no longer needs to be a custom element. Convert `<wpd-release-card>` to a plain module in src/release-card.ts: it builds its DOM directly and injects one scoped stylesheet (everything under `.dm-release-card`), removing the custom-element + shadow-DOM lifecycle that the imperative canvas/animation code had to work around (no more queueMicrotask/rAF dance; render is synchronous). Behavior + animation are unchanged. Also: `.label svg` sizing 60% -> 59%. - Remove src/ui/components/wpd-release-card/ + its barrel/tags entries. - release-card.ts owns build + paint + accent + dismiss animation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the two-stage dismiss (record-back-to-sleeve + collapse-into-the- Updates-icon). The close button now just fades the card out (~200ms) and persists the dismissal; reduced-motion removes it instantly. Removes the collapseToTarget logic and the data-collapsing CSS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A relevance-ranked news search for an older branch (e.g. 6.5) buries the major announcement below newer 6.5.x maintenance posts + betas, past the first 20 results — so it fell back to the toast. Fetch up to 100 (the REST max) and version the localStorage cache key so stale misses from the old logic are re-fetched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dismissing 6.5.1 also hid 6.5.2 because the dismissal was keyed by branch. A newer point release is a new notice (and core itself re-nags per version), so key the dismissal on the exact available version. PHP now ships `available` (the exact version) alongside the display `version`; the client keys "dismissed" on it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR centralizes WordPress core update notifications so they appear once in the Desktop Mode shell (instead of repeating in every chromeless admin window), with an enhanced “release artwork” presentation when art can be resolved from WordPress.org.
Changes:
- Adds a server-side core update descriptor (
coreUpdate) to the shell config and suppresses core’s per-window update/maintenance nags in chromeless requests. - Introduces a shell-side update notice picker that resolves/caches release art client-side and shows either a vinyl-style release card or a persistent toast.
- Extends toast behavior to support persistent toasts and documents the new surfaces/hooks.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/phpunit/tests/updateNotice.php | Adds PHPUnit coverage for the core update descriptor and chromeless nag suppression. |
| src/update-notice.ts | Implements the shell-side update notification decision flow (release card vs persistent toast). |
| src/update-notice.test.ts | Adds Vitest coverage for update notice selection, dismissal keys, and “Update now” behavior. |
| src/ui/components/wpd-toast/wpd-toast.styles.ts | Tweaks toast styling (border/shadow) for better surface separation. |
| src/types.ts | Extends DesktopConfig with optional coreUpdate descriptor shape. |
| src/toast.ts | Adds persistent toast support (no auto-dismiss timer) and documents behavior. |
| src/release-card.ts | Implements the vinyl-style release card UI and accent extraction from cover art. |
| src/release-card.test.ts | Adds Vitest coverage for release-card DOM, update action, and dismissal persistence. |
| src/release-art.ts | Adds client-side resolver for release art + codename via WordPress.org news feed with caching. |
| src/release-art.test.ts | Adds Vitest coverage for parsing and caching behavior of release-art resolver. |
| src/i18n.ts | Extends sprintf fallback to support positional tokens (e.g. %1$s). |
| src/desktop.ts | Boots the update notice picker from shell init using config.coreUpdate. |
| includes/update-notice.php | Adds server-side core update descriptor computation and desktop_mode_core_update_notice filter. |
| includes/render/assets.php | Ships coreUpdate in the shell config payload. |
| includes/core/routing.php | Suppresses core update/maintenance nags inside chromeless admin requests. |
| docs/javascript-reference.md | Documents persistent toasts and the core-update notice behavior. |
| docs/hooks-reference.md | Documents desktop_mode_core_update_notice filter and core-update notice behavior. |
| desktop-mode.php | Loads the new includes/update-notice.php module. |
| assets/css/chromeless.css | Hides any update nags that slip past server-side suppression in chromeless windows. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- release-card: always draw the sleeve; make white-frame trim + accent best-effort (no blank sleeve if pixel reads are disallowed). - release-card: render the message as one translated string (passed in by the picker via updateMessage) instead of English fragments; localize the close button's aria-label. - toast: correct a stale comment (persistent toasts have no × button). - docs/PHPDoc/types: add the `available` field to the coreUpdate descriptor everywhere it was omitted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Trim the verbose docblocks across the core-update-notice feature and rename the `desktop_mode_core_update_notice` descriptor filter to `desktop_mode_show_core_update_notice` — a plain boolean (default true) that turns the notification off. Bump the feature's @SInCE to 0.9.4 and adapt the docs accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the over-explained comments across the core-update-notice feature and reset the release-art localStorage cache prefix from `v2` to `v1` (the feature never shipped, so there was no v1 to supersede). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Thanks @mmtr, this looks great! I have tested it, and it works as expected, I see the new upgrade toast, and it looks nice. The fallback toast also looks good, but I miss a dismissal icon there. Could it be added?
| Upgrade toast | Default toast |
|---|---|
![]() |
![]() |
I have also added a _fields query param as part of f6a2f72 to reduce the payload when getting the releaseArt artifact.
_fields=title,_links,_embedded + _embed=wp:featuredmedia cuts the per-branch response from ~1.3 MB (full rendered content of 100 posts) to ~115 KB, with _links kept in _fields because the REST API only embeds relations whose links survive the field filter. Parse results are unchanged, so no cache version bump.
Review feedback: the plain fallback toast (shown when no release art resolves) had no way to be dismissed. Add a `dismissible` option to `<wpd-toast>` / `showToast()` that renders a close (×) button emitting `wpd-toast-dismiss`, plus an `onDismiss` callback. The core-update fallback toast now uses it and persists the dismissal on the same exact-version key as the vinyl card, so closing it keeps it closed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>



Why
WordPress core shows the "a new version is available, please update" nag on every admin screen. In desktop mode each screen is its own window, so that one nag ends up repeated in every open window — cluttered and noisy. This makes the update surface once, in the shell, and turns it into something that fits the desktop.
What changes (from a user's point of view)
Screen.Recording.2026-07-14.at.17.54.46.mov
Screen.Recording.2026-07-14.at.17.53.34.mov
How to test
WordPress can't be behind by default, so fake an available update.
Turn on desktop mode.
Add this as a must-use plugin (
wp-content/mu-plugins/fake-update.php), then reload the desktop:Check the windows: open a few admin windows (Dashboard, Posts, Tools). The "update available" nag should not appear inside any of them.
Check the notification (top-right of the shell):
currentto a higher patch of a real release line — e.g.7.0.9if a 7.0 exists (or6.5.9on/against a 6.5 line). You should see the album-sleeve / vinyl notification with that release's artwork.currentto a made-up version with no release — e.g.9.9— to see the plain notification fallback.Update now: click it — the Updates screen opens in a window.
Dismiss: click the × — the notification fades out. Reload: it stays gone. Now change
currentto a newer version (e.g.7.0.10) and reload — it notifies you again.Remove the mu-plugin when you're done.
🤖 Generated with Claude Code