(function() { "use strict"; function getWpHooks() { const hooks = window.wp?.hooks; if (!hooks) { throw new Error( "[desktop-mode] `window.wp.hooks` is not available. The plugin declares `wp-hooks` as a script dependency; if you are seeing this error, verify the enqueue order." ); } return hooks; } function addAction(hookName2, namespace, callback, priority) { getWpHooks().addAction( hookName2, namespace, callback, priority ); } function removeAction(hookName2, namespace) { return getWpHooks().removeAction(hookName2, namespace); } function applyFilters(hookName2, value, ...args) { return getWpHooks().applyFilters(hookName2, value, ...args); } function doAction(hookName2, ...args) { getWpHooks().doAction(hookName2, ...args); } const HOOKS = { /** Action, fires once after shell boot; plugins register here. */ INIT: "desktop-mode.init", /** Filter, receives the wallpaper registry array. */ WALLPAPERS: "desktop-mode.wallpapers", /** Filter, receives the unfocused-window effect registry array. */ UNFOCUS_EFFECTS: "desktop-mode.unfocus-effects", /** Action before a canvas wallpaper mounts. */ WALLPAPER_MOUNTING: "desktop-mode.wallpaper.mounting", /** Action after a canvas wallpaper mounts successfully. */ WALLPAPER_MOUNTED: "desktop-mode.wallpaper.mounted", /** Action before a canvas wallpaper tears down. */ WALLPAPER_UNMOUNTING: "desktop-mode.wallpaper.unmounting", /** Action when a canvas wallpaper's mount throws / rejects. */ WALLPAPER_MOUNT_FAILED: "desktop-mode.wallpaper.mount-failed", /** Action mirroring document.visibilitychange for active canvas wallpapers. */ WALLPAPER_VISIBILITY: "desktop-mode.wallpaper.visibility", // ------------------------------------------------------------------ // Observability — iframe errors, iframe network, shell-side errors, // monitor entry aggregation. Designed for dashboard / debug widget // plugins that want genuine admin observability (Gutenberg save // failures, admin-ajax 500s, plugin exceptions) rather than just the // shell's own console-error surface. // ------------------------------------------------------------------ /** * Action, fires once per iframe when the chromeless bridge * script has finished wiring its message listeners. Payload: * `{ windowId: string }`. Subscribers get a reliable "safe to * talk to this iframe" signal — the browser's native `load` * event fires before our bridge attaches, so messages sent on * `load` can be dropped on the floor. Use this instead when * timing matters (first-focus dispatch, auto-fill handshakes). * * @since 0.5.0 */ IFRAME_READY: "desktop-mode.iframe.ready", /** * Action, fires when a chromeless iframe's `error` or * `unhandledrejection` handler catches an exception. Payload: `{ * windowId: string, kind: 'error' | 'unhandledrejection', message: * string, filename: string | null, lineno: number | null, colno: * number | null, stack: string | null }`. Origin-filtered at the * parent shell; cross-origin iframe errors never reach here. */ IFRAME_ERROR: "desktop-mode.iframe.error", /** * Action, fires when a `fetch` or `XMLHttpRequest` inside a * chromeless iframe completes (success OR failure). Payload: `{ * windowId: string, method: string, url: string, status: number, * duration: number, failed: boolean }`. Subscribers get a faithful * view of admin-ajax + REST calls that previously never left the * iframe boundary. `status === 0` indicates a network failure with * no response received. */ IFRAME_NETWORK_COMPLETED: "desktop-mode.iframe.network-completed", /** * Action, fires when one of the shell's own try/catch barriers * catches an exception. Payload: `{ scope: * 'widget-mount' | 'widget-teardown' | 'window-open' | 'wallpaper-mount' | * 'wallpaper-teardown' | 'session-save' | 'menu-refresh' | string, * id?: string, error: unknown }`. Paired with the existing * `console.error` calls — a monitor widget can surface these as * first-class entries. */ SHELL_ERROR: "desktop-mode.shell.error", /** * Action, fires once per `wp.desktop.broadcast()` call with the * fully-resolved `{ topic, payload }` detail. Lets plugins log, * mirror, or augment broadcast traffic without subscribing for * every individual topic. */ BROADCAST: "desktop-mode.broadcast", /** * Filter, applies to a `MonitorEntry` before a monitor widget * renders it. Plugins can mutate the entry (rewrite the message, * add `extra` fields) or return `null` to suppress it. Used by * monitor widgets to converge every plugin on the same shape — * see `MonitorEntry` in `src/types.ts`. */ MONITOR_ENTRY: "desktop-mode.monitor.entry", /** * Filter, applies to the list of "solid" surfaces wallpapers * should consider for collision / accumulation effects (snow * piling, leaves settling, rain splash). Seeded by the shell * with: every visible (non-minimized) window's top edge; the * desktop-area floor; the dock's outward-facing edge; and every * mounted widget card's top edge. * * Plugins that own their own DOM (e.g. floating pickers, * custom overlays) can push additional surfaces so snow * accumulates on them too. * * Each entry is a `WallpaperSurface` — see * `src/wallpapers/surfaces.ts` for the shape. Rects are in * viewport coordinates (clientX / clientY), matching what a * canvas mounted inside `#desktop-mode-wallpaper` reads. */ WALLPAPER_SURFACES: "desktop-mode.wallpaper.surfaces", // ------------------------------------------------------------------ // Window lifecycle actions. All payloads share a `windowId: string` // field; additional fields are documented per-hook in the JS // reference. These mirror the existing `desktop-mode-window-*` // CustomEvents but ship under the hook bus so plugins can use one // idiomatic API for everything the shell emits. // ------------------------------------------------------------------ /** * Filter, last call before a window's resolved geometry (x, y, * width, height, initialState) is baked into the `WindowConfig` * passed to the `Window` constructor. Lets a plugin override * default placement for windows it owns, snap restored bounds to * a different region, or force a particular initial state. * * Signature: * * ( geometry: ResolvedWindowGeometry, ctx: WindowGeometryContext ) * => ResolvedWindowGeometry * * Where `ResolvedWindowGeometry = { x, y, width, height, state? }` * and `ctx = { windowId, baseId, hasSavedGeometry, callerPinned, * desktopRect }`. * * - `hasSavedGeometry` is `true` when the user previously * dragged or resized this window and the resolved geometry * includes those restored values. Plugins that want to * "leave the user's saved layout alone" should bail when * this is true. * - `callerPinned` is `true` when the caller of `manager.open()` * passed at least one of `{ x, y, width, height, initialState }` * explicitly. For NATIVE windows this is usually true (the * framework's native-window opener passes the registry's * declared dimensions); for admin-page iframe windows opened * from the dock this is usually false. The filter is free to * override registry defaults — `callerPinned: true` does NOT * mean "leave it alone." * * The shell re-clamps `width`/`height` to the registered * `minWidth`/`minHeight` after the filter returns — a buggy * filter cannot ship a sub-minimum window. `x` and `y` are * NOT re-clamped to the desktop rect after the filter (plugins * sometimes want to place windows partially off-screen for * deliberate stylistic reasons); the filter is responsible for * its own viewport math when it cares. * * Companion of `desktop_mode_register_window` server-side * defaults — runs every time a window opens, not just at * registration. * * @since 0.8.6 */ WINDOW_GEOMETRY: "desktop-mode.window.geometry", /** Action, fires when a window is added to the stack. */ WINDOW_OPENED: "desktop-mode.window.opened", /** * Action, fires when a window's body enters the loading state — at * construction (every window starts loading) and whenever a plugin * calls {@link NativeRenderContext.window.markLoading} or * `Window.markContentLoading()` mid-life. Payload: `{ windowId }`. * * The shell shows a `` overlay while the window is in * the loading state and fades content in on the loaded transition. * Subscribe to this hook (or to {@link WINDOW_CONTENT_LOADED}) when * you need to react to either edge — analytics, instrumentation, * decorating the spinner with a per-window message. * * Edge-triggered: idempotent calls don't re-fire. The matching * `desktop-mode-window-content-loading` CustomEvent dispatches on * `document` with the same payload. * * @since 0.6.0 */ WINDOW_CONTENT_LOADING: "desktop-mode.window.content-loading", /** * Action, fires when a window's body content becomes ready — for * iframe windows the moment the chromeless bridge announces * `desktop-mode-ready`, for native windows after the user's * `render( body )` callback (or its returned promise) resolves, and * whenever a plugin calls {@link NativeRenderContext.window.markReady} * or `Window.markContentLoaded()` mid-life. Payload: `{ windowId }`. * * The unified "window content is ready" signal across both render * strategies — use this instead of branching on iframe vs. native. * Iframe-only consumers can still subscribe to {@link IFRAME_READY}, * which fires alongside this hook for iframe windows. The shell * removes the loading overlay and fades the content in on this * transition. * * Edge-triggered: only fires on a loading → ready transition. * The matching `desktop-mode-window-content-loaded` CustomEvent * dispatches on `document` with the same payload. * * @since 0.6.0 */ WINDOW_CONTENT_LOADED: "desktop-mode.window.content-loaded", /** * Filter, applied to the loading-overlay HTMLElement just after * the shell paints its default `` and after any * per-window inline customization (`config.loading.render`) * runs. Receives the overlay element; context: `{ windowId, * config }`. Plugins may mutate the element (e.g. * `host.replaceChildren( myBrandedLoader )` to swap out the * default entirely, or `host.querySelector('wpd-spinner')!. * setAttribute('preset', 'comet')` to retune the spinner) or * return a different element to replace the overlay wholesale. * * Use cases: a brand-skin plugin that overrides every window's * spinner with its own logo; a status-bar plugin that adds * "Loading… 47% — fetching posts" text; an A/B-test framework * that swaps the loader during an experiment. * * Resolution order for the loading overlay: * 1. Default content (``) is painted. * 2. Per-window `config.loading.render( host, ctx )` runs. * 3. This filter runs. * 4. The result is appended to the window body. * * @since 0.6.0 */ WINDOW_LOADING_OVERLAY: "desktop-mode.window.loading-overlay", /** * Action, fires when `manager.open(...)` is called for a baseId * whose window already exists on the active desktop. This is the * unambiguous "user requested to open this window again" signal * — distinct from focus changes (which double-fire on alt-tab and * skip when already focused) and from `WINDOW_OPENED` (which only * fires on first creation). Payload: * `{ windowId: string, baseId: string, wasMinimized: boolean }`. * * Plugins that hold per-window state (e.g. the code-editor's * active file) should listen here to re-orient the existing * window's content to whatever the caller wants to show — the * open-window call is synchronous, so any state the caller sets * BEFORE invoking `openWindow` is already in place when this * fires. */ WINDOW_REOPENED: "desktop-mode.window.reopened", /** * Action, fires BEFORE the window's element is detached from the * DOM but AFTER the manager has already removed it from the stack. * Payload: `{ windowId: string, element: HTMLElement }`. * * Use this for cleanup that needs a reference to the live * element (removing anchored snow, wallpaper particles pinned to * window tops, measurement caches keyed by element). `WINDOW_CLOSED` * fires immediately after and only carries the id, which means * subscribers would otherwise have to re-query the DOM — by then * the element is gone, so they can't match at all. */ WINDOW_CLOSING: "desktop-mode.window.closing", /** Action, fires when a window is removed from the stack. */ WINDOW_CLOSED: "desktop-mode.window.closed", /** Action, fires when focus changes to a different window. */ WINDOW_FOCUSED: "desktop-mode.window.focused", /** * Action, fires for the window that LOST focus when another * window takes over. Symmetric counterpart to * `WINDOW_FOCUSED`. Payload: `{ windowId: string, focusedTo: * string | null }` — `focusedTo` identifies the new top of * the stack so blur subscribers can ignore alt-tabs to a * sibling they own. * * No-op when there's no previously-focused window (initial * boot, all-windows-closed). Manager fires this BEFORE * `WINDOW_FOCUSED` so subscribers see "blur old, focus new" * in deterministic order. * * @since 0.5.5 */ WINDOW_BLURRED: "desktop-mode.window.blurred", /** * Action, fires when a window is minimized. Payload: * `{ windowId: string, element: HTMLElement }`. * * The element ride-along matches {@link WINDOW_CLOSING}'s shape so * wallpaper plugins anchored to window tops (snow, leaves, rain * splash) can match stuck particles by element identity and run * their teardown — minimized windows render at `opacity: 0` so * `offsetParent === null` checks miss them. */ WINDOW_MINIMIZED: "desktop-mode.window.minimized", /** * Action, fires when a window is restored from minimized. Payload: * `{ windowId: string, element: HTMLElement }`. */ WINDOW_RESTORED: "desktop-mode.window.restored", /** * Action, fires when a window is maximized (fills desktop area). * Payload: `{ windowId: string, element: HTMLElement }`. */ WINDOW_MAXIMIZED: "desktop-mode.window.maximized", /** * Action, fires when a window exits maximized state. Payload: * `{ windowId: string, element: HTMLElement }`. */ WINDOW_UNMAXIMIZED: "desktop-mode.window.unmaximized", /** * Action, fires when a window enters fullscreen / focus mode. * Payload: `{ windowId: string, element: HTMLElement }`. */ WINDOW_FULLSCREEN_ENTERED: "desktop-mode.window.fullscreen-entered", /** * Action, fires when a window exits fullscreen / focus mode. * Payload: `{ windowId: string, element: HTMLElement }`. */ WINDOW_FULLSCREEN_EXITED: "desktop-mode.window.fullscreen-exited", /** * Filter, decides whether a fullscreen ("focus mode") window * should auto-exit when focus moves to a different window. * * Default is `true` so a newly-focused window is never silently * occluded by a fullscreen one (its `z-index` sits above all * other windows). Plugins whose fullscreen surface is meant to * persist across focus changes — slideshows, video players, * immersive games — can return `false` to keep their window * fullscreen. * * Signature: * * ( shouldExit: boolean, ctx: { * windowId: string, // the fullscreen window * focusedTo: string, // the window gaining focus * } ) => boolean * * @since 0.8.6 */ WINDOW_AUTO_EXIT_FULLSCREEN: "desktop-mode.window.auto-exit-fullscreen", /** * Action, fires at most once per animation frame during an * active drag or resize with the live geometry. Payload: `{ * windowId: string, x: number, y: number, width: number, * height: number, state: WindowState, phase: 'drag' | 'resize' }`. * * Intended for per-frame collision-aware wallpapers (snow piling * on window tops, rain splash on edges) that would otherwise * poll `getBoundingClientRect` every rAF. Coalesced via * `requestAnimationFrame` so a pointermove storm collapses to * one fire per paint — matches the cadence a wallpaper's own * ticker runs at. * * NOT fired at drag/resize end — `WINDOW_DRAG_END` / * `WINDOW_RESIZE_END` handle the settled geometry. Subscribers * that only want the final position should listen to those * instead. */ WINDOW_BOUNDS_CHANGED: "desktop-mode.window.bounds-changed", /** Action, fires at drag-end with the final `{ x, y }` position. */ WINDOW_MOVED: "desktop-mode.window.moved", /** Action, fires at resize-end with the final `{ width, height }`. */ WINDOW_RESIZED: "desktop-mode.window.resized", /** Action, fires when title-bar drag begins. */ WINDOW_DRAG_START: "desktop-mode.window.drag-start", /** Action, fires when title-bar drag ends. Payload mirrors WINDOW_MOVED. */ WINDOW_DRAG_END: "desktop-mode.window.drag-end", /** Action, fires when the resize handle is first pressed. */ WINDOW_RESIZE_START: "desktop-mode.window.resize-start", /** Action, fires when resize completes. Payload mirrors WINDOW_RESIZED. */ WINDOW_RESIZE_END: "desktop-mode.window.resize-end", /** Action, fires when the user "detaches" a window to a classic tab. */ WINDOW_DETACHED: "desktop-mode.window.detached", /** * Action, fires when the user clicks the title-bar reload button * on an iframe-backed window. Payload: `{ windowId: string, url: * string }` where `url` is the URL being reloaded (the active * primary or external sub-tab). Subscribers can use this to * invalidate their own cache, force a save before navigation, * track usage as a UX signal, or sync state across companion * surfaces. Native windows do not fire this — they own their * DOM directly and the reload button doesn't apply. */ WINDOW_RELOADED: "desktop-mode.window.reloaded", /** Action, fires when iframe title updates change the window title. */ WINDOW_TITLE_CHANGED: "desktop-mode.window.title-changed", /** * Action, fires when a window's `setHighlight()` mode changes. * Payload: `{ windowId: string, mode: 'preview' | 'persistent' | null, * color?: string }`. Lets onboarding / guidance / drag-bridge * plugins react when another module flagged one of their * windows as the focus of a multi-step interaction without * having to observe DOM mutations. * * @since 0.6.0 */ WINDOW_HIGHLIGHT_CHANGED: "desktop-mode.window.highlight-changed", /** * Action, fires when a window's body element's dimensions * change — mount, user resize, viewport reflow. Payload: `{ * windowId: string, width: number, height: number }`. Body * dimensions exclude the title bar + tab strip, matching what a * canvas or layout engine inside the body would measure. */ WINDOW_BODY_RESIZED: "desktop-mode.window.body-resized", // ------------------------------------------------------------------ // Native-window lifecycle. These fire ONLY for windows constructed // with `native: true` — iframe windows have no render phase to // intercept. Use them to wrap / instrument / cancel the paint of // plugin-contributed native windows (the Calculator, Jorvy, custom // native launchers). // ------------------------------------------------------------------ /** * Filter, applied to the body element a native window will render * into, just BEFORE the user's `render( body )` callback runs. * Payload: the `HTMLElement`; context: `{ windowId, config }`. * * Return the same element (or a wrapper) to intercept. Subscribers * commonly use this to inject a consistent shell (padding, * background, decorative chrome) around every native window * without every plugin re-implementing the pattern. */ NATIVE_WINDOW_BEFORE_RENDER: "desktop-mode.native-window.before-render", /** * Action, fires AFTER a native window's `render( body )` callback * returns. Payload: `{ windowId, body, config }`. Observability * hook — analytics / auto-focus / post-render measurement. */ NATIVE_WINDOW_AFTER_RENDER: "desktop-mode.native-window.after-render", /** * Filter, applied when a native window is about to start its * close animation. Return `false` to CANCEL the close — the * window stays open. Payload: `true`; context: `{ windowId, * config }`. Any non-`false` return (including `undefined`) lets * the close proceed. * * Intended for "unsaved changes" guards: a calculator with a * pending operation can prompt the user and abort the close * mid-flight. Does NOT apply to iframe windows — their close is * driven by browser navigation patterns the shell doesn't own. */ NATIVE_WINDOW_BEFORE_CLOSE: "desktop-mode.native-window.before-close", // ------------------------------------------------------------------ // Window-chrome customization framework. Plugins drive per-window // appearance (theme, controls, slots, full chrome render) through // the `wp.desktop.registerWindow*` registries; these hooks expose // every resolution step so plugins can mutate or observe the // chrome pipeline without owning a registration. // // Layers 1-3 (theme, controls, slots) are Stable. Layer 4 (chrome // render) is Experimental — `WINDOW_CHROME_RENDER` may change. // ------------------------------------------------------------------ /** * Filter, applied to the resolved CSS-variable map for a window. * Receives `Record< string, string >`; context: `{ windowId, * config }`. Plugins return a mutated map to override or augment * the per-window theme tokens — e.g. tint every Gutenberg * window's title bar to brand colour. * * Stable since 0.6.0. */ WINDOW_CHROME_THEME: "desktop-mode.window.chrome.theme", /** * Filter, applied to the resolved control list for a window. * Receives `WindowControlDef[]`; context: `{ windowId, config, * placement: 'left' | 'right' | 'controls' }`. Plugins return a * mutated array to reorder, hide, or inject controls per-window. * * Stable since 0.6.0. */ WINDOW_CHROME_CONTROLS: "desktop-mode.window.chrome.controls", /** * Filter, applied per slot when the chrome paints. Receives the * slot host element; context: `{ windowId, slot, config }`. * Plugins can mutate `host` (append decorative children, set * inline styles) without owning a `WindowSlotDef` registration. * The shell never reads the return value — this is an action- * shaped filter so existing `addFilter` plumbing applies. * * Stable since 0.6.0. */ WINDOW_CHROME_SLOT: "desktop-mode.window.chrome.slot", /** * Filter, applied to the chrome id selected for a window. * Receives the resolved id (defaults to `'core/standard'`); * context: `{ windowId, config }`. Returning a different id * swaps the chrome registration. **Experimental** — chrome * render contract may change. * * @since 0.6.0 */ WINDOW_CHROME_RENDER: "desktop-mode.window.chrome.render", /** * Action, fires after a window chrome layer has been mounted / * remounted. Payload: `{ windowId, layer: 'chrome' | 'controls' * | 'slots', chromeId? }` — `chromeId` is present only when * `layer` is `'chrome'`. Subscribers can post-decorate the * chrome (attach observers, anchor pickers). * * @since 0.6.0 */ WINDOW_CHROME_APPLIED: "desktop-mode.window.chrome.applied", /** * Action, fires after a window's theme tokens are applied to its * outer element. Payload: `{ windowId, themeId, tokens }`. Lets * plugins react to theme changes without diffing CSS variables. * * @since 0.6.0 */ WINDOW_CHROME_THEME_CHANGED: "desktop-mode.window.chrome.theme-changed", /** * Action, fires when a user clicks a desktop icon (a shortcut * tile registered server-side via `desktop_mode_register_icon()` * and rendered on the wallpaper). Payload: `{ id: string, * target: 'window' | 'url' }`. Fires BEFORE the default open * action — plugins cannot cancel the open from this hook, but * can use it to track click-throughs or augment behaviour (e.g. * play a sound, surface a confirmation toast). * * @since 0.5.0 */ DESKTOP_ICON_CLICKED: "desktop-mode.desktop-icon.clicked", /** * Action, fires after the wallpaper icon grid is rendered or * re-rendered. Payload: * * { * ids: string[]; // paint order * container: HTMLElement; //
* tiles: ReadonlyMap; // id → tile