Smooth desktop repaint on adds and removes#262
Conversation
Eliminates the visible "desktop reloaded" flash on file creation,
shortcut drop, and tile deletion. The renderer used to fall through
to a wholesale `container.replaceChildren()` rebuild for any
structural change, repainting every tile from scratch even when
only one had appeared or disappeared.
Adds an incremental path (`tryPatchIncremental`) between the
existing position-only fast path and the wholesale rebuild:
1. tryPatchPositions — same set, only X/Y changed (drag-within).
2. tryPatchIncremental — adds and/or removes, no structural
changes on shared tiles. Preserves DOM identity for the
unchanged tiles.
3. Wholesale rebuild — last resort for pin-flag flips, parent-
folder moves, file-type changes.
Per-tile setup is extracted into `wireTile()` so all three paths
share one source of truth — wholesale + incremental call the same
helper to build + position + wire new tiles. Layout (pinned slots
+ displacement) factored into `computeLayout()` so it can run on
the live DOM without a rebuild.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`desktop_mode_files_trash_folder()` runs a JOIN between `wp_desktop_mode_folders f` and `wp_desktop_mode_file_placements p` to find nested folders that need to cascade-trash. The SELECT list just said `id` — and both tables have an `id` column — so MySQL rejected it with "Column 'id' in field list is ambiguous". Symptom: trashing any folder that contained other folders dumped `<div id="error"><p class="wpdberror">…</p></div>` into the REST response (visible to the user when the wpdberror block got prepended before headers, breaking the JSON parse on the client), and the cascade silently skipped the nested folders so they stayed visible on the desktop after the parent was trashed. Fix: select `f.id` explicitly. One-character change. Found via Docker debug.log; reported by user after dropping a folder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
✅ WordPress Plugin Check Report
📊 ReportAll checks passed! No errors or warnings found. 🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check |
The Edit User window splits into a 280px sidebar + a `minmax(0, 1fr)` main column. The main column's content (`<wpd-form>`) caps at 720px for readability, but with no `margin-inline` it sat flush-left inside its cell — on wide windows the right edge of the form sat hundreds of pixels short of the right edge of the window, with the empty band reading as "this form is broken / forgot to fill the width". `margin-inline: auto` + `width: 100%` keeps the 720px line-length cap intact and pushes the unused horizontal space equally to both sides instead of pooling it on the right. The Edit User window is now visually balanced regardless of viewport width. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The title-bar `.desktop-mode-window__meta-btn` buttons (Help, etc.) already use flex centering on the parent, but the dashicon span inside inherits `display: inline-block` from core's Dashicons rules and renders the glyph via text layout. The font's baseline isn't quite the geometric centre of its em-box, so the visible character drifts a px or two off-centre — visible on the `?` (editor-help) glyph as a slight up-and-left lean inside the 28×28 button. Making the span itself a flex centerer (`display: inline-flex` + `align-items: center` + `justify-content: center` + `line-height: 1`) re-anchors the glyph to the geometric centre regardless of the font's intrinsic metrics. Same span dimensions, same icon size — just precise centring. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Media Library enhancement script (`assets/js/media-library-
enhanced.js`, since 0.14.0) makes attachment tiles draggable with
a rich DataTransfer payload: `text/html` (an `<img>` tag),
`text/uri-list`, `text/plain`, AND a custom
`application/x-wp-media-attachment` MIME carrying the full WP
attachment record. It also `postMessage`s the same record to the
parent shell via `desktop-mode-drag-start` so the bridge can keep
the rich payload available across iframe hops.
Symptom: dragging a media tile out of the Media Library window
and dropping on an open Gutenberg post window does nothing — the
tile lifts, ghost tracks, but Gutenberg never inserts a block.
Root cause: Chromium strips non-standard DataTransfer MIMEs at
the iframe boundary, so by the time `drop` fires inside the
Gutenberg iframe, `application/x-wp-media-attachment` is gone and
Gutenberg's own drop handler doesn't recognize what's left as a
media insert.
Fix wires the existing DragBridge end-to-end:
1. drag-bridge.ts now normalizes the legacy
`{id, url, title, alt, mime, sizes, thumbnailUrl}` shape into
the tagged `{kind: 'attachment', ...}` union as soon as the
payload lands at the parent shell. Downstream receivers only
handle one shape.
2. drag/iframe-drop-targets.ts forwards
`DRAG_BRIDGE_EVENTS.START` / `.END` to every iframe as
`desktop-mode-drag-over` / `-drag-leave` messages so the
receiver inside knows a cross-frame drag is in flight and
stashes the payload up-front.
3. gutenberg-drop-receiver.ts stashes the bridge payload on
incoming `desktop-mode-drag-over` and adds a native HTML5
`drop` handler (capture phase, `stopImmediatePropagation`) that
uses the stashed payload to insert the matching block via
`wp.data.dispatch('core/block-editor').insertBlocks(…)`.
Pairs with a `dragover` listener that calls `preventDefault`
so the browser treats the iframe as a valid drop target while
the bridge session is live.
End-to-end: Media Library tile drag → parent bridge stores +
normalizes → fans `drag-over` to every iframe → user drops on
Gutenberg → receiver wins the native drop event, inserts the
image. No change to media-library-enhanced.js; no risk to
in-Gutenberg native drag (no stashed payload → no interception).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Top-priority bugs found by a plugin author working against the public API: 1. HOOKS.IFRAME_READY never fired. The parent listens for `desktop-mode-ready` (`src/window/iframe-bridge.ts:187-191`) but no iframe-side code emitted it. Two consumers (connection-rearm in desktop.ts, devtools replay) silently never ran. Now both `includes/render/chromeless-bridge.php` and `src/iframe-bridge-standalone.ts` post the message after listener wiring completes. 2. `wp.desktop.iframe.publish` silently dropped when no connection was open. Now console.warns the dropped topic + why, instead of vanishing. 3. iframeContent handshake race — fixed automatically by #1 AND backstopped by new `Window.whenContentReady(): Promise<void>` on the Window facade so plugin code can `await win.whenContentReady()` without wiring iframe.load themselves. 4. Component classes / npm package — fixed without bloating the shipped bundle. The components are already in desktop.min.js (side-effect imports via src/ui/components/index.ts), and already re-exported from src/public-api.ts. Added `"import"` to the package.json `exports` map so plugin bundlers can resolve `import { WpdLog } from 'desktop-mode'` via a local `file:../desktop-mode` dep — no npm publish needed. Documented the workflow in `docs/use-from-a-plugin.md`. 5. No public lookup of a Connection by id. Added `wp.desktop.getConnection( connectionId )` returning the live `WindowConnection` (or null). Also ships `connection: conn` in the HOOKS.CONNECTION_OPENED payload so iframe-initiated connections can be subscribed to directly from the hook. 6. Iframe didn't know its own window id. The parent's handshake message now carries `targetWindowId`; the iframe stashes it and exposes `wp.desktop.iframe.windowId` (sync) + `whenWindowId(): Promise<string>` (resolves on first handshake). Replaces the brittle `iframe.contentWindow ===` walk plugin code had to do parent-side. Doc gaps found along the way: 8. `docs/components-reference.md` was referenced by the runtime missing-import warner and 3 other doc files but didn't exist. Created the canonical mapping: every wpd-* tag → exported class → source file → one-line purpose, grouped by surface (layout, form, buttons, menus, display, lists, tabs, color). 9. No example for the iframe-initiated topology (PluginSidebar wants to open a Preview window). Existing docs/examples/connect-to-window.md only covered the inverse case. Added `docs/examples/iframe-initiated-window.md` with a full Live Preview recipe wiring `wp.desktop.iframe.publish` ↔ `wp.desktop.connect`. 10. Cross-origin bridge support was undefined. Three bridges hard-filter on `window.location.origin` and silently drop everything cross-origin. Documented as an explicit non-goal in `docs/bridge-protocol.md` with the security rationale, and added `wp.desktop.iframe.isParentReachable()` so plugins can fail fast / degrade gracefully instead of debugging vanishing messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@claude those are many fixes as part of a polish we are doing for a demo, please review all. |
|
Claude finished @AllTerrainDeveloper's task in 4m 39s —— View job PR Review — Smooth desktop repaint on adds and removes
This PR bundles eight commits' worth of polish across four distinct feature areas. I've reviewed every changed file. Overall the code is clean and the architecture is sound — the main path changes are correct and well-guarded. A few things need attention before merge. 1. Duplicate comment block —
|
| Area | Status |
|---|---|
Core incremental repaint (layer.ts) |
✅ Correct, one duplicate comment to clean up |
| Iframe-to-iframe drag bridge | ✅ Correct |
| Legacy Media Library payload normalization | |
| Gutenberg native drop receiver | ✅ Correct, minor perf note for prod |
windowId / whenWindowId / isParentReachable |
|
Connection getConnection + targetWindowId |
✅ Clean |
| Dock flat-structure refactor | ✅ Correct, scroll regression acknowledged |
| GH#258 reversion | ❓ Intentional? Needs comment if so |
PHP trash.php |
✅ Bug fix, correct |
| CSS (dashicon, user-edit, window-chrome) | ✅ |
| Documentation | ✅ Accurate, needs whenWindowId hang warning |
Three items worth addressing before merge: the duplicate comment (trivial), the normalizeLegacyPayload fragility (small), and the whenWindowId no-reject documentation gap. The GH#258 reversion needs clarification of intent.
|
@claude please fix all by yourself |
|
Claude finished @AllTerrainDeveloper's task in 3m 1s —— View job Review fixes applied ✓All actionable items from the code review have been fixed in commit
|
- Remove duplicate 'Wholesale rebuild' comment block in layer.ts - Harden normalizeLegacyPayload pass-through: check obj.kind !== undefined/null instead of an explicit known-kinds allowlist that needs updating per new kind - Add whenWindowId() non-reject warning + isParentReachable() guard pattern to docs/use-from-a-plugin.md and docs/examples/iframe-initiated-window.md Co-authored-by: Daniel López Sánchez (prismiwi2015) <AllTerrainDeveloper@users.noreply.github.com>
Summary
Eliminates the visible "desktop just reloaded" flash on file creation, shortcut drop on the wallpaper, and tile deletion. The renderer used to fall through to a wholesale
container.replaceChildren()rebuild for any structural change, repainting every tile from scratch even when only one had appeared or disappeared.Adds a middle render path (
tryPatchIncremental) between the existing position-only fast path and the wholesale rebuild:tryPatchPositions— same set, only X/Y changed (drag-within-folder, auto-arrange). Unchanged.tryPatchIncremental(new) — adds and/or removes, no structural changes on shared tiles. Surgically removes gone tiles (with their drop-target deregistrations) and appends new ones (fully wired). DOM identity preserved for everything unchanged.Why this is safe
wireTile()helper, called by both the wholesale rebuild and the incremental path. No risk of drift between paths.computeLayout()) is the same function feeding all three paths — layouts are bit-identical.false, falls through to wholesale) on any shared tile whosedata-file-type,data-file-ref, or pinned flag differs from the new placement. That's the same conservative gatetryPatchPositionsalready uses; it just relaxes the same-set requirement.Test plan
npm run lintcleannpm run typecheckcleannpm run test:js— 1485 passing (no test changes; existing covers the fingerprint + tryPatchPositions surface)npm run build:desktopclean (~+1KB on desktop.min.js, accounted for by the new helper)🤖 Generated with Claude Code