Skip to content

Polish four framework surfaces for plugin authors#229

Merged
AllTerrainDeveloper merged 3 commits into
trunkfrom
widget-polishing
May 16, 2026
Merged

Polish four framework surfaces for plugin authors#229
AllTerrainDeveloper merged 3 commits into
trunkfrom
widget-polishing

Conversation

@AllTerrainDeveloper
Copy link
Copy Markdown
Collaborator

@AllTerrainDeveloper AllTerrainDeveloper commented May 16, 2026

Four small, additive APIs requested by the ODD plugin author. Each one removes a workaround that third-party code was carrying.

What changed

  • wp.desktop.widgets.redock( id ) — stable namespace; WidgetLayer.redock() promoted from private, so wp.desktop.widgetLayer.redock( id ) also works. Idempotent + null-safe.
  • HOOKS.DESKTOP_ICONS_RENDERED payload now ships { ids, container, tiles }. tiles is a frozen id → tile <button> map, mirroring DOCK_AFTER_RENDER.tileElements. ids retained for back-compat — existing subscribers keep working.
  • HOOKS.WINDOW_GEOMETRY (new filter) — last call before WindowConfig is baked. Signature: ( geometry, ctx ) => geometry, with ctx = { windowId, baseId, hasSavedGeometry, callerPinned, desktopRect }. The booleans are the only useful distinctions a filter actually needs: hasSavedGeometry flags a restored user layout (bail to respect their choice); callerPinned flags caller-passed dims (usually true for native windows because their registry passes default width/height; the filter is free to override). Lets plugins place / size their own windows declaratively instead of compensating after open() lands. Filter return is re-clamped to minWidth/minHeight.
  • No new universal "surface rendered" hook — wallpaper, widget, and dock already pass containers; icons were the only gap and the payload change above closes it. Will revisit if a cross-cutting need crystallizes.

Files

  • src/widgets/layer.tsprivate redock()public redock().
  • src/api/facade.ts, src/desktop.ts — add wp.desktop.widgets namespace + reserved key.
  • src/desktop-icons.ts — build a tiles map during render; include container + tiles in the payload.
  • src/hooks.ts — new WINDOW_GEOMETRY constant; updated DESKTOP_ICONS_RENDERED JSDoc.
  • src/window-manager/index.tsapplyFilters( HOOKS.WINDOW_GEOMETRY, … ) after geometry resolution; exports ResolvedWindowGeometry, WindowGeometryContext.

Docs

  • docs/javascript-reference.mdwp.desktop.widgets.redock section, WINDOW_GEOMETRY row in the window-hook table + a full recipe section.
  • docs/examples/register-icon.md — decorator recipe using tiles from the icons payload.

Test plan

  • ./node_modules/.bin/tsc --noEmit — clean.

  • npm run lint — clean.

  • npm run test:js — 1327/1327 (11 new tests across widgets.test.ts, desktop-icons-render.test.ts, window-manager-hooks.test.ts).

  • npm run build — all 17 bundles compile; WINDOW_GEOMETRY constant, applyFilters call site, hasSavedGeometry + callerPinned fields all confirmed in assets/js/desktop.min.js.

  • Manual: open Heartbeat widget, liberate it, call wp.desktop.widgets.redock( 'desktop-mode/heartbeat' ) from console — card snaps back into the column. (Note the namespaced id — the redock is idempotent + silent on unknown ids, so a bare 'heartbeat' no-ops without error.)

  • Manual (optional — filter is exhaustively unit-tested): drop the following snippet into the browser console BEFORE opening Posts (or after closing it first), then click Posts in the dock. It should land in the bottom-right corner. Drag it somewhere else, close, reopen — it stays where YOU put it (filter bails on hasSavedGeometry: true).

    wp.desktop.hooks.addFilter(
        wp.desktop.HOOKS.WINDOW_GEOMETRY,
        'manual-qa/bottom-right',
        ( g, ctx ) => {
            // Bail if the user has previously dragged/resized — respect their layout.
            if ( ctx.hasSavedGeometry ) return g;
            const W = 520, H = 360;
            return {
                ...g,
                width: W, height: H,
                x: ctx.desktopRect.width  - W - 20,
                y: ctx.desktopRect.height - H - 20,
            };
        }
    );

    Want to scope to a single window (e.g. just Posts)? Add an early bail:

    if ( ctx.baseId !== 'desktop-mode-posts' ) return g;

Defensive coverage for WINDOW_GEOMETRY

The filter is wrapped in try/catch + a defensive coalesce so a buggy plugin can't break window opens. Tests pin:

  • Default-resolved geometry → can be fully overridden.
  • Caller-pinned geometry (callerPinned: true) → filter still runs and can override (regression test for the native-window case).
  • hasSavedGeometry: true set when localStorage has a saved geometry for the window — plugins use this to respect the user's saved layout.
  • Filter return below minWidth/minHeight → re-clamped.
  • Partial return (e.g. { width } only) → missing fields fall through to pre-filter values.
  • NaN / Infinity returns → falls through.
  • Throwing filter → window still opens, error reported on HOOKS.SHELL_ERROR.
  • Non-object / undefined return → falls through.
  • Native windows → filter fires identically.
  • Multiple subscribers → chain in priority order.
Open WordPress Playground Preview

@AllTerrainDeveloper
Copy link
Copy Markdown
Collaborator Author

@claude do your awesome review here

@claude
Copy link
Copy Markdown

claude Bot commented May 16, 2026

Claude finished @AllTerrainDeveloper's task in 2m 30s —— View job


PR #229 Review

  • Read and understand changed files
  • Review src/widgets/layer.ts — redock visibility change
  • Review src/api/facade.ts + src/desktop.ts — widgets namespace
  • Review src/desktop-icons.ts — tiles map in payload
  • Review src/hooks.ts — new WINDOW_GEOMETRY constant + JSDoc
  • Review src/window-manager/index.ts — geometry filter
  • Review tests — coverage quality
  • Review docs — correctness and completeness

Overall: This is solid, additive work that follows the codebase's own patterns well. Four changes, each self-contained. I found one real doc inaccuracy, one misleading JSDoc phrase, and one missing test case. No bugs in the implementation.


wp.desktop.widgets.redock( id )

layer.ts — clean promotion from private to public. The onRedock callback in mountById already called this.redock( id ), so no callers had to move. The docblock's @since 0.7.0 (private) / @since 0.25.0 (public) dual-epoch style is unconventional but unambiguous.

facade.ts — null-safe wrapper widgetLayer?.redock( id ) is correct. RESERVED_NAMESPACE_KEYS updated. WpDesktopPublicApi.widgets interface defined in desktop.ts and matches the runtime object exactly.

Test covers: public call works, idempotency (docked widget no-ops), unknown-id silent no-op. All three cases matter, all three pass. ✅


DESKTOP_ICONS_RENDERED payload enrichment ✅

desktop-icons.tstiles is built during the same ordered loop that populates the container, so the map and the DOM are in sync. Passing it as ReadonlyMap at the doAction call-site is the right defensive choice — subscribers can't accidentally mutate the shell's internal map. ids retained for back-compat. ✅

Minor nit: buildIcon() returns HTMLElement but the element is specifically an HTMLButtonElement (document.createElement('button')). The JSDoc says "id → tile <button>" which is correct. The map type Map<string, HTMLElement> loses that precision, but given subscribers don't normally need HTMLButtonElement-specific API, it's an acceptable trade-off.

Test covers payload shape, tile node identity (same nodes in map and in DOM — not clones), container class check. ✅


HOOKS.WINDOW_GEOMETRY filter ✅ (with one doc fix needed)

Implementation (window-manager/index.ts):

  • Source classification ('explicit' / 'restored' / 'default') is clean: ANY caller-pinned axis → 'explicit'; saved localStorage geometry present → 'restored'; pure defaults → 'default'.
  • Re-clamp of width/height but NOT x/y after the filter — intentional, documented, correct. Plugins that want off-screen placement (a tooltip-style companion, a slide-in panel) need that freedom.
  • applyFilters<ResolvedWindowGeometry, [WindowGeometryContext]> is typed correctly.
  • The filter runs in both open() and openNew() paths via createWindow(). openNew() always produces source: 'explicit' (it hard-codes initialState: 'normal' and a cascade slot), which is the right behaviour — a deliberately-spawned duplicate carries explicit geometry.

Tests cover: 'default' source + geometry override, 'explicit' source when caller pins dimensions, min-clamp enforcement. ✅

⚠️ Doc inaccuracy in docs/javascript-reference.md:

The ResolvedWindowGeometry signature snippet shows:

state?: 'maximized';            // optional initial state

But ResolvedWindowGeometry.state is typed as state?: WindowState (src/window-manager/index.ts:105), and the hooks.ts JSDoc explicitly mentions returning 'snapped-left' as a use case. The doc should either use WindowState or list the full union:

state?: 'normal' | 'maximized' | 'minimized' | 'snapped-left' | 'snapped-right';

Fix this →

Minor wording nit in hooks.ts JSDoc for WINDOW_GEOMETRY:

'explicit' when the caller passed at least one of {x, y, width, height, initialState} (session restore, openNew with explicit dims)

The parenthetical "session restore" as an example of 'explicit' is misleading — most readers will associate "session restore" with 'restored'. The intent is "a session-restore call that passes explicit geometry via the config object." A clearer phrasing:

'explicit' when the caller passed at least one of {x, y, width, height, initialState} — e.g. a programmatic open() call that supplies its own position, or openNew().

Missing test — source: 'restored':

The three new window-manager-hooks.test.ts tests cover 'default' and 'explicit' but not 'restored'. The geometry-loading path works (it's exercised indirectly by other tests), but a test that seeds localStorage before opening, installs a filter, and asserts ctx.source === 'restored' would give complete coverage of the three-branch contract. Not a blocker, but worth adding to close the gap.


Docs ✅

docs/examples/register-icon.md — the new "Decorating the rendered grid" section is accurate, idiomatic, and the code example matches the payload shape exactly (destructures { ids, container, tiles }, null-checks tiles.get(), explains when container is useful vs a single tile). ✅

docs/javascript-reference.md — the wp.desktop.widgets.redock section, the WINDOW_GEOMETRY row in the hook table, and the full recipe section are all present and correct (modulo the state type nit above). The "Guarantees" callout block is a good idea — makes the re-clamping and x/y behavior explicit for filter authors.


Summary

Area Status
redock() promotion + facade + tests ✅ Ready
DESKTOP_ICONS_RENDERED payload ✅ Ready
WINDOW_GEOMETRY implementation + types ✅ Ready
WINDOW_GEOMETRY docs (state type) ⚠️ One inaccuracy (see above)
JSDoc phrasing in hooks.ts 💬 Minor wording nit
Test for source: 'restored' 💬 Nice-to-have gap

The doc fix for state?: is the only change I'd consider before merge — it's a public API guarantee that will confuse plugin authors when they discover 'snapped-left' isn't in the declared type union. Everything else is correct and well-structured.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 16, 2026

✅ WordPress Plugin Check Report

✅ Status: Passed

📊 Report

All checks passed! No errors or warnings found.


🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

@AllTerrainDeveloper
Copy link
Copy Markdown
Collaborator Author

Manual testing passed :)

@AllTerrainDeveloper AllTerrainDeveloper enabled auto-merge (squash) May 16, 2026 15:10
@AllTerrainDeveloper AllTerrainDeveloper merged commit a651d30 into trunk May 16, 2026
5 checks passed
@AllTerrainDeveloper AllTerrainDeveloper deleted the widget-polishing branch May 16, 2026 15:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant